Closed CeylonMigrationBot closed 8 years ago
[@gavinking] Today I made a start on this and it's basically working. Missing things include:
new origin extends Point(0.0,0.0) {}
of
clauses and singleton constructors.I've done this in a way that should make it really very easy for the backend to start supporting (singleton instances are just nested object
s.)
[@jvasileff] Java enums allow you to override methods (possibly abstract in the parent class), which I think is a pretty nice feature.
So it might be nice to allow case
s to be defined outside of the body of the class, and extend it.
[@gavinking] @jvasileff we've been through this. Java's system overcompensates for the lack of first-class functions in the language (prior to Java 8). In Ceylon we can write the same thing using assignment:
class Foo {
shared void accept(Float x);
shared new New1() {
accept(Float x) => print(x);
}
shared new New2(void accept(Float x)) {
this.accept = accept;
}
}
Or, with a singleton constructor:
class Foo {
shared void accept(Float x);
shared new instance {
accept(Float x) => noop(x);
}
}
[@gavinking]
So it might be nice to allow
case
s to be defined outside of the body of the class, and extend it.
Note: we already have that possibility with what exists in Ceylon today with the of
clause. We don't need a second way to do it.
[@gavinking] Well, OK, fine, I admit it might be nice to let you write:
class Foo {
void accept(Float x);
shared new New() {
actual void accept(Float x) {
print(x);
}
}
}
It's not overriding precisely, since New
is not a subclass, it's just an alternative syntax for assignment. But it's quite regular. (Notice that accept()
doesn't need to be shared
for this, and nor is it formal
.)
[@jvasileff] Ok, yes, those look like nice options.
[@gavinking] OK, I now have a much cleaner implementation of this in the 1129
branch. Basically, a singleton constructor is now represented in the model as a Constructor
+ a Value
.
If anyone has time to work on backend support for this, go ahead, it should be straightforward, you just need to treat it like a regular Constructor
, but in addition generating a static
accessor for a singleton instance. Should be very easy to get it working.
For now, we don't need to support delegation to constructors.
[@gavinking] So this has worked out nicely, but there is one thing which bothers me a bit. Previously, with of
, defining an enumerated type was a totally explicit thing. The client knew for sure that you intended for the list of things in the of
clause to be exhaustive.
With what I have right now for singleton constructors that's not true. Perhaps I plan to add a new instance, or think I might. How do I indicate that clients?
One option would be to abuse final
and allow switch
es on the enumerated instances of a class explicitly marked final
. That's not really precisely what final
means, but it's quite close. In practice I think it would work out fairly well, but still, it makes me uncomfortable.
A second option would be to introduce an enumerated
annotation for classes which can be switch
ed. I don't like that this is so irregular compared to the current mechanism.
A third, much more regular option would be to make you use the of
clause to list the constructors explicitly:
shared class Bool of true | false {
shared new true {}
shared new false {}
}
I think I prefer this since it makes the connection to what we already have today. It's just much more consistent.
[@gavinking] Perhaps let you abbreviate it like this:
shared class Bool of ... {
shared new true {}
shared new false {}
}
Hurmph.
[@lucaswerkmeister] I like the third, explicit option (without the ...
, which look really weird).
[@quintesse] The last part seems unnecessary to me, it's isn't that much work to repeat the names.
[@tombentley]
it's isn't that much work to repeat the names.
Well it depends how many there are and how long they are. Look at ceylon.unicode::Directionality
for an example where the repetition would be annoying.
[@quintesse]
Look at ceylon.unicode::Directionality for an example where the repetition would be annoying.
Not sure, as I see it we are already repeating those names there right now as well and personally I don't find them annoying. And like @lucaswerkmeister I find the of ..
option weird looking.
[@gavinking] Yes Tom is right I'm bothered by what ceylon.unicode looks like.
Sent from my iPhone
On 1 Jun 2015, at 2:14 pm, Tom Bentley notifications@github.com wrote:
it's isn't that much work to repeat the names.
Well it depends how many there are and how long they are. Look at ceylon.unicode::Directionality for an example where the repetition would be annoying.
— Reply to this email directly or view it on GitHub.
[@quintesse] Also to me the of ...
option seems just like a cryptic version of the enumerated
annotation.
[@gavinking] So the broader question is: how useful are singleton constructors outside of the context of defining enums?
If we were to say a class can have either singleton or parameterized constructors but not both, then I would be OK with dropping the need to make it explicit. Because I think in that case we can just position this as being for enums and nothing else.
But if you can mix singletons and constructors in the same class, which feels quite useful to me, then I feel like it needs to be explicit because singletons no longer carry the expectation that they are for enums and nothing else.
Sent from my iPhone
On 1 Jun 2015, at 2:18 pm, Tako Schotanus notifications@github.com wrote:
Look at ceylon.unicode::Directionality for an example where the repetition would be annoying.
Not sure, as I see it we are already repeating those names there right now as well and personally I don't find them annoying. And like @lucaswerkmeister I find the of .. option weird looking.
— Reply to this email directly or view it on GitHub.
[@quintesse] And why exactly does the option of making the class final
make you uncomfortable? It does seem like a simple solution.
[@gavinking] Well final doesn't mean it can't have parameterized constructors. And it doesn't, strictly speaking, mean I'm not going to add any new constructors.
Sent from my iPhone
On 1 Jun 2015, at 2:42 pm, Tako Schotanus notifications@github.com wrote:
And why exactly does the option of making the class final make you uncomfortable? It does seem like a simple solution.
— Reply to this email directly or view it on GitHub.
[@quintesse]
Well final doesn't mean it can't have parameterized constructors
Sure, but I thought you found that useful so you wanted to keep that.
And it doesn't, strictly speaking, mean I'm not going to add any new constructors.
Well... right. But I'm not confident that people will use of
because they are sure they'll never ever cross-my-heart-hope-to-die add new items either. In many cases it'll probably be used because they want nice switching behaviour.
[@gavinking] Right the point is that if you want to let your clients switch on your constructors, I want you to explicitly indicate that by doing something that you would not do for any other motivation.
Because people, and it doesn't matter what I think of this practice, will start using this to just stick a list of values inside a wrapping namespace, without considering whether this is a closed list of values or not.
So I think it's gotta be explicit.
Sent from my iPhone
On 1 Jun 2015, at 2:59 pm, Tako Schotanus notifications@github.com wrote:
Well final doesn't mean it can't have parameterized constructors
Sure, but I thought you found that useful so you wanted to keep that.
And it doesn't, strictly speaking, mean I'm not going to add any new constructors.
Well... right. But I'm not confident that people will use of because they are sure they'll never ever cross-my-heart-hope-to-die add new items either. In many cases it will probably be added because you want nice switching behaviour.
— Reply to this email directly or view it on GitHub.
[@FroMage] Whatever syntax we add, remember to compare it against:
public enum Foo {
Bar, Gee;
}
And:
public enum FamousNumbers {
Answer(42), Beast(666);
public final int number;
FamousNumbers(int number){ this.number = number; }
}
For brevity. I know I will compare them ;)
[@quintesse]
without considering whether this is a closed list of values or not.
Ok, so you want to make sure they know they're going to make a breaking change (with respect to backward compatibility) if they ever add a new value. Well then I'd vote for being explicit without weird of ...
shorthands.
[@FroMage] I mean, I'm not at all opposed to a new grammar for enum classes.
[@gavinking] Well I think we're doing that and that's what this discussion is about. But brevity is not the only value at play here.
Sent from my iPhone
On 1 Jun 2015, at 3:11 pm, Stéphane Épardaud notifications@github.com wrote:
Whatever syntax we add, remember to compare it against:
public enum Foo { Bar, Gee; } And:
public enum FamousNumbers { Answer(42), Beast(666);
public final int number;
FamousNumbers(int number){ this.number = number; } } For brevity. I know I will compare them ;)
— Reply to this email directly or view it on GitHub.
[@gavinking] Well I am against that. For the two enum classes I write every year, I don't need a whole new keyword.
The only reason I'm even considering this whole idea really is that it offers the potential to make several things easier all at once. A specialised enum syntax would be crippled in the sense that it addresses just one problem and doesn't compose together with other language constructs the way this proposal does.
Sent from my iPhone
On 1 Jun 2015, at 3:12 pm, Stéphane Épardaud notifications@github.com wrote:
I mean, I'm not at all opposed to a new grammar for enum classes.
— Reply to this email directly or view it on GitHub.
[@gavinking] And I'm not exaggerating here: in the past year I recall writing one enum in Java, that's 1 i.e. the additive identity. It was SiteVariance, FTR.
And for that I need a whole special language construct whose syntax I barely remember?
Sent from my iPhone
On 1 Jun 2015, at 3:12 pm, Stéphane Épardaud notifications@github.com wrote:
I mean, I'm not at all opposed to a new grammar for enum classes.
— Reply to this email directly or view it on GitHub.
[@FroMage] It is well known that you never write enums yourself, but we do. We have more than twenty enums in the compiler alone, and would have a lot more if we weren't so damn lazy with calls like foo(true, false, false, true)
where we have a really high threshold of switching to sanity.
[@tombentley]
that's 1 i.e. the additive identity.
I think you mean the multiplicative identity, the additive identity is zero.
[@FroMage] Remember you're not designing a language just for yourself ;)
[@gavinking] Hahahah yes I do believe that's what I mean haha
Sent from my iPhone
On 1 Jun 2015, at 3:22 pm, Tom Bentley notifications@github.com wrote:
that's 1 i.e. the additive identity.
I think you mean the multiplicative identity, the additive identity is zero.
— Reply to this email directly or view it on GitHub.
[@FroMage] The problem is that Java's enums allow brevity, multiple constructors and anonymous subtyping. Brevity (even with multiple constructors) could be greatly improved using this syntax (provided we can get at the case values from outside, which is not obvious):
class FamousNumbers(shared Integer number)
of beast(666) | answer(42) {}
class UsesConstructors
of first(1) | second(34, 12) {
shared Integer prop;
new Foo(Integer x){
prop = x;
}
new Bar(Integer x, Integer y){
prop = x * y;
}
}
As for anonymous subtyping (which I frequently do in enums), we could do it like this too:
class FamousNumbers(shared Integer number)
of answer(42)
| beast(666) { bow() => throw Exception("I bow to no one"); }
{
shared default void bow(){
print("I am number ``number``);
}
}
It isn't pretty, but it works, and is even less verbose than Java's equivalent:
public enum FamousNumbers {
Answer(42),
Beast(666) {
@Override
public void bow() {
throw new Exception("I bow to no one");
}
};
public final int number;
FamousNumbers(int number) { this.number = number; }
public void bow(){
System.err.println("I am number "+number);
}
}
[@FroMage] And if we're worried that the case values would default to shared
(because that's the most useful thing to have) when it's not explicit (and so we can't turn it off) we could make enum
be an annotation for the class that just means "make case values be shared by default", so we can turn it off by leaving the enum
annotation out.
[@gavinking] So I think a reasonable solution might be to let you abbreviate the common case where the values don't have any interesting initialisation logic:
class Bool of true | false {}
This isn't ambiguous at all, it can't be confused with anything we have today.
Then, if one of your enumerated cases needs dedicated init logic, you add a singleton constructor with that name.
[@FroMage] Can you give me the current proposal's equivalent to the examples I posted (taken straight off the compiler code, though simplified. they are not hand-waving but real use-cases)? It's a bit hard to judge the current proposal without samples…
[@gavinking] Wait, wat, this is just a dog's breakfast:
class UsesConstructors of first(1) | second(34, 12) { shared Integer prop; new Foo(Integer x){ prop = x; } new Bar(Integer x, Integer y){ prop = x * y; } }
So first(1)
, what is that—looks like a function call, where is the function named first()
. Hrm, can't find it. Oooh OK, it's a constructor invocation ... but, which constructor, I have Foo
and Bar
here, oh, wait, you're matching argument lists to constructors based on the arity of the parameter list?! What happens if that's ambiguous?
Nononononono. What I am proposing lets me write:
class UsesConstructors
of first | second {
shared Integer prop;
shared new first {
prop = 1;
}
shared new second {
prop = 34*12;
}
}
That's clearly much cleaner.
Now the second example, given as:
class FamousNumbers (shared Integer number) of answer(42) | beast(666) { bow() => throw Exception("I bow to no one"); } { shared default void bow() { print("I am number"); } }
(Note I had to reformat this over a bunch more lines to even begin to make sense of this.)
Well there's several problems with this:
{
is the start of the class or the start of a case.bow()
before bow()
is defined.OTOH, with the current proposal, it could look like something like this:
class FamousNumbers
of answer | beast
{
shared Integer number;
shared void bow();
shared new answer {
number=42;
bow() => print("I am number");
}
shared new beast {
number = 666;
bow = () { throw Exception("I bow to no one"); }
}
}
There's absolutely no debate in my mind about which of these is cleaner, more readable, more easy to format, and keeps implementation and API more separate.
[@gavinking] By the way, @FroMage one comparison you can just take home right now is the comparison to Java enum
s, which is totally inapt, since they are not exhaustive. The following does not compile:
String toString(SiteVariance var) {
switch (var) {
case IN: return "out"
case OUT: return "out";
case NONE: return "";
}
}
In Java I am forced to write:
String toString(SiteVariance var) {
switch (var) {
case IN: return "out"
case OUT: return "out";
case NONE: return "";
default: throw Error("the universe is about to end!");
}
}
So to compare my proposal, fairly to Java, you have a class with no of
clause and just:
class SiteVariance {
shared new in {}
shared new out {}
shared new none {}
}
[@FroMage] Except you're conveniently skipping over the issue of constructor reuse. What if I have more values that need the same constructors?
[@lucaswerkmeister](for future reference, and the benefit of others interested in this issue: the discussion has for some reason moved – temporarily? permanently? who knows – [to Gitter]%28https://gitter.im/ceylon/user?at=556c6eca463d0c7c066d9820%29)
[@gavinking]
Except you're conveniently skipping over the issue of constructor reuse. What if I have more values that need the same constructors?
Then write a partial constructor and delegate to it. That was part of the original proposal that you apparently didn't bother to read.
[@gavinking] So by thinking through the usecases for Java-style enums I've concluded that the distinction between "open-ended" enums, which all Java enum
s are, implicitly, and "closed-ended" enums, which let you exhaustively switch
over, is a real one. Consider a list of error types, for example. That's almost certainly an open-ended enum. But people want to be able to write Error.missingClosingParen
or whatever.
I'm honestly not sure about the case of Directionality
in ceylon.unicode
, is that exhaustive or not? I'm a bit suspicious that any enum with that many cases can possibly be exhaustive—especially when one of its cases is the rather suspicious looking "undefined"—but given that this is defined by a spec that changes slowly, I guess we could treat it that way.
So, assuming that we do need it to be closed-ended, what would this look like using singleton constructors?
"Enumerates the *Directionalities* defined by the Unicode
specification."
shared class Directionality
of arabicNumber
| boundaryNeutral
| commonNumberSeparator
| europeanNumber
| europeanNumberSeparator
| europeanNumberTerminator
| leftToRight
| leftToRightEmbedding
| leftToRightOverride
| nonspacingMark
| otherNeutrals
| paragraphSeparator
| popDirectionalFormat
| rightToLeft
| rightToLeftArabic
| rightToLeftEmbedding
| rightToLeftOverride
| segmentSeparator
| undefined
| whitespace {
"The two character code assigned to this directionality
by the Unicode specification."
shared String code;
shared new arabicNumber { code=>"AN"; }
shared new boundaryNeutral { code=>"BN"; }
shared new commonNumberSeparator { code=>"CS"; }
shared new europeanNumber { code=>"EN"; }
shared new europeanNumberSeparator { code=>"ES"; }
shared new europeanNumberTerminator { code=>"ET"; }
shared new leftToRight { code=>"L"; }
shared new leftToRightEmbedding { code=>"LRE"; }
shared new leftToRightOverride { code=>"LRO"; }
shared new nonspacingMark { code=>"NSM"; }
shared new otherNeutrals { code=>"ON"; }
shared new paragraphSeparator { code=>"B"; }
shared new popDirectionalFormat { code=>"PDF"; }
shared new rightToLeft { code=>"R"; }
shared new rightToLeftArabic { code=>"AL"; }
shared new rightToLeftEmbedding { code=>"RLE"; }
shared new rightToLeftOverride { code=>"RLO"; }
shared new segmentSeparator { code=>"S"; }
shared new undefined { code=>""; }
shared new whitespace { code=>"WS"; }
}
Is that perfect? Perhaps not. Is it better than what we have today? Well, yes, I think it's clear that it's somewhat cleaner.
OTOH, if we don't think we need to make this closed-ended, which is the fair comparison to Java enum
s, we would have just this:
"Enumerates the *Directionalities* defined by the Unicode
specification."
shared class Directionality {
"The two character code assigned to this directionality
by the Unicode specification."
shared String code;
shared new arabicNumber { code=>"AN"; }
shared new boundaryNeutral { code=>"BN"; }
shared new commonNumberSeparator { code=>"CS"; }
shared new europeanNumber { code=>"EN"; }
shared new europeanNumberSeparator { code=>"ES"; }
shared new europeanNumberTerminator { code=>"ET"; }
shared new leftToRight { code=>"L"; }
shared new leftToRightEmbedding { code=>"LRE"; }
shared new leftToRightOverride { code=>"LRO"; }
shared new nonspacingMark { code=>"NSM"; }
shared new otherNeutrals { code=>"ON"; }
shared new paragraphSeparator { code=>"B"; }
shared new popDirectionalFormat { code=>"PDF"; }
shared new rightToLeft { code=>"R"; }
shared new rightToLeftArabic { code=>"AL"; }
shared new rightToLeftEmbedding { code=>"RLE"; }
shared new rightToLeftOverride { code=>"RLO"; }
shared new segmentSeparator { code=>"S"; }
shared new undefined { code=>""; }
shared new whitespace { code=>"WS"; }
}
Ultimately I don't think any of this is especially offensive on the eyes.
[@gavinking] Well one solution to this quandary would be to use a different keyword to declare the singleton constructors when they represent enumerated cases. We would keep using new
when it's an "open-ended" enumeration, and use of
when it's "closed":
"Enumerates the *Directionalities* defined by the Unicode
specification."
shared class Directionality {
"The two character code assigned to this directionality
by the Unicode specification."
shared String code;
of arabicNumber { code=>"AN"; }
of boundaryNeutral { code=>"BN"; }
of commonNumberSeparator { code=>"CS"; }
of europeanNumber { code=>"EN"; }
of europeanNumberSeparator { code=>"ES"; }
of europeanNumberTerminator { code=>"ET"; }
of leftToRight { code=>"L"; }
of leftToRightEmbedding { code=>"LRE"; }
of leftToRightOverride { code=>"LRO"; }
of nonspacingMark { code=>"NSM"; }
of otherNeutrals { code=>"ON"; }
of paragraphSeparator { code=>"B"; }
of popDirectionalFormat { code=>"PDF"; }
of rightToLeft { code=>"R"; }
of rightToLeftArabic { code=>"AL"; }
of rightToLeftEmbedding { code=>"RLE"; }
of rightToLeftOverride { code=>"RLO"; }
of segmentSeparator { code=>"S"; }
of undefined { code=>""; }
of whitespace { code=>"WS"; }
}
That's not bad at all.
[@gavinking] So perhaps what we could say is that:
class Bool {
of false {}
of true {}
}
Would be an abbreviation for:
class Bool
of false | true
satisfies Enumerated {
shared actual String string;
shared actual Integer integer;
shared new false { string=>"false"; integer=0; }
shared new true { string=>"true"; integer=1; }
}
Thus member of
s would just be a syntax sugar to cut down on the boilerplate.
None of that would conflict with what is proposed in #4231. We could still support this in future:
class Point {
...
of origin { ... }
of Polar(Float x, Float y) { ... }
of Cartesian(Float r, Float theta) { ... }
}
[@gavinking] Hell, if this is just an abbreviation, I suppose we could even make the enumerated
annotation have that effect.
enumerated class Bool {
new false {}
new true {}
}
[@jvasileff] Can the case types be made denotable to do things like work with type safe subsets?
[@gavinking] @jvasileff perhaps in conjunction with #4231.
[@gavinking] So the more I think about it, the more I'm inclined to think that an enumerated
annotation does almost no work. It is quite analogous to final
, in fact, much more than I thought at first, in the sense that having that annotation there lets the client do some stuff that they would not otherwise be allowed to. (In the case of final
, types become disjoint, in the case of enumerated
, types become exhaustive.)
But there is a big difference which shows why enumerated
is pretty lame. If I "break" final
ity, and attempt to add a new subclass, the compiler forces me to remove the final
annotation, thus unambiguously telling me that I've made a significant breaking change. But if I "break" clients of an enumerated
class, by adding a new constructor, the compiler does not complain until I try to recompile clients.
An of
clause list is quite different: if I add a new subtype of the enumerated class, I have to go back to the enumerated class and explicitly add that subtype to the list.
So I'm kinda left torn between two options:
switch
as exhaustive if it covers all constructors. If you want to prevent clients from switching
on your class, because you think you might add new constructors, add a private constructor. In practice there's really no way to be sure that an enumerated type won't add new cases in the future. The future is unknowable.of
list, even though it's really unDRY.I lean toward option 1. The original design before I had a crisis of confidence yesterday.
[@gavinking] P.S. Who else here prefers of
instead of new
for defining singleton constructors?
[@tombentley]
P.S. Who else here prefers
of
instead ofnew
for defining singleton constructors?
I did quite like that, actually.
[@gavinking] No, wait, the above reasoning just can't be correct. People add new constructors for convenience to classes all the time, without wanting to bother themselves with the possibility that this is a change that breaks clients. We make constructors far less useful if we make it problematic to add new constructors.
P.S. There's also an issue with delegation here, but I think that's solvable by saying that:
So I'm pretty much back to where I was yesterday:
of
list for enumerated classes,of
instead of new
when writing the constructors of an enumerated class, orswitch
es and just require you to write an else
clause when switching on constructors.[@quintesse] I'm still in favor of #3107
, I like it being explicit, I like that you'll be forced to think about it a bit more when you want an exhaustive switch and I don't mind at all that you have to repeat the name, not even when there's 2 dozen of them.
[@gavinking] In #3902 the notion of singleton constructors arose. That is, the ability of a class with constructors (i.e. without an initializer parameter list) to declare singleton instances whose bodies work like a constructor and initialize the state of the instance. For example:
A singleton instance reference would look like
Point.origin
. One motivation for this feature is that in conjunction with #4231 it would give us an alternative, streamlined way to do Java-style enums.Issue: the interpretation as an enum value is what's motivating the use of
case
as the keyword here. Even thoughcase
doesn't feel quite perfect to me,value
andobject
would both be ambiguous here, andnew
doesn't read right.[Migrated from ceylon/ceylon-spec#1129] [Closed at 2015-06-20 20:04:25]