eclipse-archived/ceylon

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, and class 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?

Pushed my work-in-progress to the 7218 branch.

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 meaning Property<String> (and @string would be a value expression of type @String).
  • Whereas here, @String is a value expression of type Class<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, and class 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 of class 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 lowcase Person).
  • Person@name for model refence to name member in Person class.
  • person@name for property reference to name in person instance.

I guess infixes are harder to parse, but looks more regular IMHO.

@DiegoCoronel

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 plain module, package, interface, and class to refer to the current thing.

So I'm sticking with class Person for declarations.

@someth2say

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 meaning Property<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.

@someth2say

See my comment above:

What is proposed in #3791 is that @String would be a type expression meaning Property<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 to name, but
  • person.@name is a property reference to the name of the person, 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.

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.

jogro commented

I definitely prefer "::" from an ergonomic point of view.

I would recommend leaving the backticks in place.

That's certainly an option.

@gavinking

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... )

@DiegoCoronel

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:

  1. I've merged the branch, and
  2. 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?

@bjansen yes please; that's very strange, I didn't change the AST at all.

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...

@bjansen

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.