new syntax for model and declaration references
Closed this issue · 54 comments
After extensive practical experience, I've grown to dislike our syntax for metamodel references. In favor of the backticks:
- they're a visually lightweight element syntactically, making them quite easy to read, and
- they delimit both start and end of the expression, allowing me to clearly write stuff like
`Float.plus`.name
and`Integer|Float`
.
However:
- I find them surprisingly clumsy to type, and
- I essentially never need to directly access attributes of a metamodel reference.
Today I was able to convince ANTLR to accept a different grammar, which I find myself liking better:
- no backticks around declaration references, so simply
class String
instead of`class String`
- a prefix
@
instead of backticks for model references, so@String
instead of`String`
and@Float.plus
instead of`Float.plus`
.
As I have currently implemented this, the @
or class
has a very low precedence, so @Integer|Float
is correct, but you have to write (@Float.plus).name
or (class String).name
.
I probably can mess with the grammar some more to give @
a somewhat higher precedence, and then you would have to write @<Integer|Float>
, but you would still need the parens in (@Float.plus).name
, so I'm not sure if it's worth it. Also keywords like class
and function
naturally look like low-precedence "operators".
My conclusions from this work:
- Dropping the backticks from declaration references seems like a clear win to me. I think I should merge at least that much of this work.
- The
@
as a replacement for / alternative to backticks for model references is somewhat more marginal. But it certainly makes criteria queries easier to type.
value results
= let (criteria = em.createCriteria(),
person = criteria.from(@Person),
address = person.join(@Person.address))
criteria
.where(
like(person.get(@Person.name), "Gavin%"),
address.get(@Address.state).equalTo("CA"))
.orderBy(
asc(address.get(@Address.zip)),
desc(person.get(@Person.name)))
.select(construct(@NameAgeAndCity,
with3(person.get(@Person.name),
person.get(@Person.age),
address.get(@Address.city))))
.getResults();
Remaining work to be done includes:
- support plain
module
,package
, andclass
as simplified forms of`module`
,`package`
, and`class`
, - decide the final precedence of
@
, and - check that this work doesn't stomp on #3791 (should be fine).
Finally, we could consider simplifying the syntax for declaration references even further, to @@String
instead of class String
. But I dunno, that doesn't look much like Ceylon to me...
Reactions?
I like the @Person
change and dislike @@Person
... for this second I would prefer some kind of magic method @Person.declaration
instead of @@
or continue with class Person
I like the
@Person
change and dislike@@Person
Yes, me too, which is why I've stuck with class Person
at least for now.
check that this work doesn't stomp on #3791
Hrm, indeed there is one way in which it does screw up my vision for #3791.
- What is proposed in #3791 is that
@String
would be a type expression meaningProperty<String>
(and@string
would be a value expression of type@String
). - Whereas here,
@String
is a value expression of typeClass<String,[{Character*}]>
.
So I have to think this through and decide what I want here—and whether we are ever really going to do #3791.
-1 for @@
: too ascii-art and no clear meaning.
Not a fan for @
: reminds me of Java annotations or C pointers, and lack for closing token is somehow annoying. But can live with that.
Maybe we can actually use the same @
for closing token, resulting into @class String@.name
or @Float.plus@
.
Not 100% convinced, but worth dropping the idea here.
For metamodel declarations, I kind of prefer the magic method declaration
: @Person.declaration@
or even @Person@.declaration
looks more meaningfull than just class Person
(latest have no indicator that this is actually returning a declaration, instead of a Class model).
@someth2say I think @
makes for a terrible sort of paren, and I would much rather stick with backticks than have to write @Float.plus@
.
I kind of prefer the magic method declaration
That wouldn't help unless declaration
were a keyword.
Prefix twiddle is currently available, I wonder what this looks like:
value results
= let (criteria = em.createCriteria(),
person = criteria.from(~Person),
address = person.join(~Person.address))
criteria
.where(
like(person.get(~Person.name), "Gavin%"),
address.get(~Address.state).equalTo("CA"))
.orderBy(
asc(address.get(~Address.zip)),
desc(person.get(~Person.name)))
.select(construct(~NameAgeAndCity,
with3(person.get(~Person.name),
person.get(~Person.age),
address.get(~Address.city))))
.getResults();
Not awful, I would say. So one option might be:
~String.string
means an attribute model~String
means a class model@string.string
means a property reference@String
means a property type
We're getting a little into ASCII-art territory here, I guess, but really no deeper than we already were.
I would say ~
looks pretty good for me (much better than @
or backticks).
But if a reason for this change is bacticks being hard to type (two keystrokes), ~
is even much harder in spanish keyboards (Alt+126, four keystrokes).
But if a reason for this change is bacticks being hard to type
It's not that they're hard to type. (On an English keyboard they're easy as pie to type.) It's that I find adding and removing them at the start/end of a model expression an incredible pain.
~
is even much harder in spanish keyboards (Alt+126, four keystrokes).
OK, noted.
What about a prefix /
?
value results
= let (criteria = em.createCriteria(),
person = criteria.from(/Person),
address = person.join(/Person.address))
criteria
.where(
like(person.get(/Person.name), "Gavin%"),
address.get(/Address.state).equalTo("CA"))
.orderBy(
asc(address.get(/Address.zip)),
desc(person.get(/Person.name)))
.select(construct(/NameAgeAndCity,
with3(person.get(/Person.name),
person.get(/Person.age),
address.get(/Address.city))))
.getResults();
Or prefix even &
:
value results
= let (criteria = em.createCriteria(),
person = criteria.from(&Person),
address = person.join(&Person.address))
criteria
.where(
like(person.get(&Person.name), "Gavin%"),
address.get(&Address.state).equalTo("CA"))
.orderBy(
asc(address.get(&Address.zip)),
desc(person.get(&Person.name)))
.select(construct(&NameAgeAndCity,
with3(person.get(&Person.name),
person.get(&Person.age),
address.get(&Address.city))))
.getResults();
Nope, &
is ugly there. Slash looks OK.
-1 for &
Not sure /
is better than@
, so nothing against it.
/
also kind of means hierarchy
,so maybe we can get some profit of that (I.e. /Person/name
)? Not sure how...
- support plain
module
,package
, andclass
as simplified forms of`module`
,`package`
, and`class`
Done. Easy.
- decide the final precedence of
@
Since a model expression can contain type arguments, the precedence must be higher than the <
and >
operators. Which means it could in principle sit between <
and ==
. But I think it is probably cleaner to say it sits between layers 2 and 3, i.e. just lower than ==
, and just higher than the logical operators.
Finally, we could consider simplifying the syntax for declaration references even further, to
@@String
instead ofclass String
. But I dunno, that doesn't look much like Ceylon to me...
Actually no, I now remembered why using a plain sigil to distinguish declaration references doesn't work out well: it's ambiguous whether @@foo.bar.baz
refers to a module, a package, or an attribute. Also you loose the nicety of plain module
, package
, interface
, and class
to refer to the current thing.
So the remaining question is the syntax for model references. The solution space includes:
- leave them alone (backticks)
- use prefix a
@
- use prefix a
/
,~
,^
, or&
In principle we could introduce a new keyword, for example, model Person.name
, but that would eliminate most of the interesting usecases for model references (Criteria
query API usage would be horrid.) So I think that's out of the question.
Y'know, hat looks pretty reasonable:
value results
= let (criteria = em.createCriteria(),
person = criteria.from(^Person),
address = person.join(^Person.address))
criteria
.where(
like(person.get(^Person.name), "Gavin%"),
address.get(^Address.state).equalTo("CA"))
.orderBy(
asc(address.get(^Address.zip)),
desc(person.get(^Person.name)))
.select(construct(^NameAgeAndCity,
with3(person.get(^Person.name),
person.get(^Person.age),
address.get(^Address.city))))
.getResults();
TBH I like the look of the hats. I could live with that.
Alright, so one last doubt. We have two kinds of "model references" under consideration here:
- typed metamodel references, like we've always had, which are unbound from a receiving instance, and
- property references, proposed in #3791, which are bound to an instance.
Which out of ^
and @
looks more like a property ref, and which looks more like a model ref?
^
reminds me of regular expressions...
What about this:
value results
= let (criteria = em.createCriteria(),
person = criteria.from(#Person),
address = person.join(#Person.address))
criteria
.where(
like(person.get(#Person.name), "Gavin%"),
address.get(#Address.state).equalTo("CA"))
.orderBy(
asc(address.get(#Address.zip)),
desc(person.get(#Person.name)))
.select(construct(#NameAgeAndCity,
with3(person.get(#Person.name),
person.get(#Person.age),
address.get(#Address.city))))
.getResults();
# is already taken by hex literals
for me ^
is terrible ;) ... what about something like: Person::name
for model and @Person
for declaration ? this syntax looks much more friendly
Which out of ^ and @ looks more like a property ref, and which looks more like a model ref?
Why not using @
for both? Say, prefix @
for model references, and infix @
for property references.:
@Person.name
vs person@name
Or even let the uppercase/lowecase discrimination to work:
- @ Person like the old
class Person
, a model reference to a class. (I added a space after@
to avoid MD to see it as a mention and lowcasePerson
). - Person@name for model refence to
name
member inPerson
class. - person@name for property reference to
name
inperson
instance.
I guess infixes are harder to parse, but looks more regular IMHO.
for me
^
is terrible ;)
Why?
what about something like:
Person::name
for model
That works for declarations, but it doesn't work for complex types, for example ^Integer|Float
.
@Person
for declaration
No, see my comment above:
Actually no, I now remembered why using a plain sigil to distinguish declaration references doesn't work out well: it's ambiguous whether
@@foo.bar.baz
refers to a module, a package, or an attribute. Also you loose the nicety of plainmodule
,package
,interface
, andclass
to refer to the current thing.
So I'm sticking with class Person
for declarations.
Why not using
@
for both? Say, prefix@
for model references, and infix@
for property references.:
See my comment above:
What is proposed in #3791 is that
@String
would be a type expression meaningProperty<String>
(and@string
would be a value expression of type@String
).
what about something like:
Person::name
for model
it doesn't work for complex types, for example
^Integer|Float
.
I mean you would have to write stuff like ::<Integer|Float>
to get a model ref for a type. Pretty ugly, but perhaps it's not so terrible.
So, @DiegoCoronel, here's what my code example looks like with your suggestion:
value results
= let (criteria = em.createCriteria(),
person = criteria.from(::Person),
address = person.join(Person::address))
criteria
.where(
like(person.get(Person::name), "Gavin%"),
address.get(Address::state).equalTo("CA"))
.orderBy(
asc(address.get(Address::zip)),
desc(person.get(Person::name)))
.select(construct(::NameAgeAndCity,
with3(person.get(Person::name),
person.get(Person::age),
address.get(Address::city))))
.getResults();
I admit that's not terrible.
See my comment above:
What is proposed in #3791 is that
@String
would be a type expression meaningProperty<String>
(and@string
would be a value expression of type@String
).
Well, actually that sorta/almost might be OK after all. @String
would mean:
Property<String>
when it occurs as a type, and- a model reference when it occurs as a value.
I think (but I'm not certain) the parser might be able to deal with that.
And perhaps:
Person.@name
is a model reference toname
, butperson.@name
is a property reference to thename
of theperson
, and- the problematic case is plain
@name
when it refers to a member of a containing type, but that could be disambiguated in one of two ways, either:- it means a model reference, and you can get a property reference with
this.@name
, or - it means a property reference and you can get a model reference with
Person.@name
.
- it means a model reference, and you can get a property reference with
I will give this approach some further thought.
If I can handle it in the parser, here is what the code example would look like:
value results
= let (criteria = em.createCriteria(),
person = criteria.from(@Person),
address = person.join(Person.@address))
criteria
.where(
like(person.get(Person.@name), "Gavin%"),
address.get(Address.@state).equalTo("CA"))
.orderBy(
asc(address.get(Address.@zip)),
desc(person.get(Person.@name)))
.select(construct(@NameAgeAndCity,
with3(person.get(Person.@name),
person.get(Person.@age),
address.get(Address.@city))))
.getResults();
To me that looks fairly ugly, but perhaps I could get used to it; not sure.
for me ^ is terrible ;)
Why?
Its shift + 6 + backspace
for me and as personal opnion it does not looks clean to ready
::<Integer|Float>
For me its as ugly as ^<Integer|Float>
value results
= let (criteria = em.createCriteria(),
person = criteria.from(::Person),
address = person.join(Person::address))
criteria
.where(
like(person.get(Person::name), "Gavin%"),
address.get(Address::state).equalTo("CA"))
.orderBy(
asc(address.get(Address::zip)),
desc(person.get(Person::name)))
.select(construct(::NameAgeAndCity,
with3(person.get(Person::name),
person.get(Person::age),
address.get(Address::city))))
.getResults();
As personal opinion it looks more clean than using ^
.
Its
shift + 6 + backspace
for me
Ugh. Terrible. What kind of kb is that?
For me its as ugly as
^<Integer|Float>
Well sure, but currently I let you write ^Integer|Float
because the precedence is lower than what it would be with an infix ::
.
Ah, now I recall that there was another reason for disfavoring an infix operator for model refs. Consider:
Inner.Outer.@thing
Here, if Inner
is a class, then Inner
has the type Inner(Args)
, which doesn't have Outer
as a member.
That's another reason I'm using prefixes for model refs.
Its shift + 6 + backspace for me
Ugh. Terrible. What kind of kb is that?
US INT, and the sry about my error but the correct is space bar
instead of backspace
... I need space bar
because my language have accents
Well sure, but currently I let you write For me its as ugly as ^Integer|Float because the precedence is lower than what it would be with an infix ::.
Right, but again as my opinion using < >
is more readable for me.. i would use always as ^<Integer|Float>
I would recommend leaving the backticks in place. But if they must go, I think ::
is the cleanest looking solution presented so far. I find ::<Integer|Float>
more visual distinctive than ^Integer|Float
, even though the latter is easier to type.
I definitely prefer "::" from an ergonomic point of view.
I would recommend leaving the backticks in place.
That's certainly an option.
Inner.Outer.@thing
Well, that's not exactly the syntax I am proposing.
My original idea is that metamodel hierarchy is traversed throuth the @
symbol, the same way we travese the instance hierarchy with the .
So getting the 'thing' model reference will be
Inner@Outer@thing
But I find this syntax really ugly, so I would say we can use the .@
token the same way:
Inner.@Outer.@thing
(p.s. the more I look at this, the more Person.@name.
looks like just syntax sugar for @Person.@name
.
But then, what about instances? Will person.@name
be syntax sugar for `@person.@name? not really sure... )
what about something like: Person::name
I still have in mind a discussion from some years ago about allowing "fully qualified names" in Ceylon. The outcome for that discussion was that it can be done, and the best syntax in place was using ::
for separating the package from the member (say ceylon.language::Integer
or java.util::Map
).
I still have some hope for this to become true, so I would save the ::
token for that.
At this point I'm considering merging the new backtickless syntax for declaration refs, while leaving the problem of model refs open for now. There's essentially no downside to getting rid of the backticks around `class Foo.Bar`
and `module foo.bar`
. And we're starting to use this syntax more and more for stuff like restricted()
and even maybe for the proposed structured()
annotation for pattern matching.
I would recommend leaving the backticks in place.
I'm in favor of this as well.
I agree that the sigils look nicer than the back-ticks.
Any symbol would start to look normal over time if used enough, but my thoughts:
&
seems the most self-explanatory, vaguely similar to C's&
~
is way too strongly associated with "approximately" and "compliment"@
looks nice^
looks awkward/
meh
A related concern is syntax for obtaining instances for type classes, if type classes are ever added to Ceylon. For example, if T satisfies Integral<T>
, T.zero is Boolean(T)
and perhaps @T.zero is T
.
OK, so after all this feedback, what I have done is:
- dropped the requirement for backticks around declaration references, but adjusted the grammar so they are still parsed with a very high precedence, and
- updated the spec to reflect that, but
- realized that there were always quite excellent reasons for the backticks in model references—since they can contain type arguments, unless they are delimited at both ends, they have to be parsed with an uncomfortably low precedence—and so I've left this syntax alone for now, as recommended by many on this thread.
I still do like the look of carets in my code example, and I guess I'll keep playing a bit with that idea to see if it goes anywhere, but for now:
- I've merged the branch, and
- I'm closing this issue.
Sorry, @jvasileff, somehow I missed your last comment.
I agree that the sigils look nicer than the back-ticks.
But there's the real issue that for model references a sigil has to be parsed with an unnaturally low precedence, since model refs can contain type args. It doesn't quite kill the idea, but it is a consideration.
&
seems the most self-explanatory, vaguely similar to C's&
Yes, I agree. I don't find it very easy on the eyes, however.
~
is way too strongly associated with "approximately" and "compliment"
I guess.
@
looks nice
It's fine, but I think I prefer to leave it for property refs and types, if we ever decide to do them. (That's not clear.)
^
looks awkward
I like it a lot but it seems I'm the only one.
/
meh
Yeah. Meh is my feeling too.
But trust me folks, you're going to appreciate being able to write:
throws (class Exception, "when something bad happens")
restricted (module, module my.tests)
function foo() { ... }
It looks like the model loader's hack to accept `Foo`
as arguments for parameters of type Class<T>
does not work with the new syntax:
public abstract <T> T lookup(Class<T> clazz);
value projects = Lookup.default.lookup(`NbCeylonProjects`); // works
// Illegal argument types in invocation of overloaded method or class:
// there must be exactly one overloaded declaration of lookup which accepts the given argument types ClassWithInitializerDeclaration
value projects = Lookup.default.lookup(class NbCeylonProjects);
Should I open a separate issue?
Oh wait, I should have used ^
instead of class
, the function accepts a model, not a declaration. I'm going to fix the quick assist to use ^
instead (or whatever sigil we end up using).
No. The caret is just for play. It's not in the spec cos no one likes it. It's only the syntax for declaration refs that we changed.
OK, so I should continue using `Foo`
instead for models?
Yes, exactly.
@bjansen You know what would be waay nice?
If these syntax quickfixes fixed all instances of the old syntax in the current file. WDYT?
Yeah, IntelliJ supports this (Fix all 'xyz' problems in file
), but I'm not aware of any similar feature in Eclipse (maybe because quick assists are computed lazily for each warning).
Yeah, but I mean ... we could just make our quickfix go and do it, without even asking the user...
Yeah, IntelliJ supports this (Fix all 'xyz' problems in file), but I'm not aware of any similar feature in Eclipse (maybe because quick assists are computed lazily for each warning).
I seem to remember that some Quick-fixes had that option available last time I used Eclipse for Java.
It's been a while though, so I can't point you at exactly which ones. I think there was some sort of modifier key (Alt?) that you would use when selecting a quick-fix to apply it to all instances in the file...
I guess I can get used to class Foo
instead of `class Foo`
, but then I think we should do something similar for model refs, by introducing a new prefix for it, such as reference Foo<Bar>
for class Foo
. If we have to reuse an existing keyword, I suppose value Foo<Bar>
might be acceptable.