eclipse-archived / ceylon

The Ceylon compiler, language module, and command line tools
http://ceylon-lang.org
Apache License 2.0
399 stars 62 forks source link

syntax for qualified types #3516

Closed CeylonMigrationBot closed 8 years ago

CeylonMigrationBot commented 12 years ago

[@gavinking] Belatedly, I've noticed a potential inconsistency in the syntax for qualified types and metamodel references.

In principal, if we defined the metamodel type Class to have a member type, Iterator, for example, then the following phenomenon would occur:

This is not an actual ambiguity as such, but it's certainly undesirable. Of course, it's a phenomenon that will never occur in practice, if we simply don't define any member classes of the metamodel types.

A second related observation is that with the introduction of the syntax Iterable::Iterator in #3509 we have gained a second redundant way to express a qualified type. This is also undesirable, I believe.

There's three ways to handle this that I can see:

  1. Do nothing: this is certainly a wrinkle, but since we're the only ones defining the metamodel, we can make sure it never manifests in practice. Java (and C#) developers are used to writing Iterable.Iterator, and . is much easier on the eyes than :: or @. It's not especially strange that type-level operators have slightly different semantics to value-level operators—just look at | and &. Hell, we didn't even notice this wrinkle until now, so the chance of anyone else actually noticing it is somewhere in the range -1p..+1p.
  2. Use :: for qualifying types: we now have a totally reasonable and unambiguous syntax for writing qualified types. The semantics of #3509's Foo::Bar is a simply much better and more natural fit here than the semantics of .. We're be adopting a really strained second interpretation of ., just because we think it looks slightly prettier, and because we're used to it. Why have two totally synonymous ways to qualify a type name?
  3. Always qualify metamodel references with @: we already need that for attribute references. Why not force you to write, for example @Iterator.Iterable(), making it completely unambiguous that you're talking about the metamodel here. Then Iterator.Iterable could mean exactly the same thing wherever it appears.

I'm quite uncertain which option I prefer. Option 2, perhaps.

[Migrated from ceylon/ceylon-spec#410] [Closed at 2013-01-04 15:04:57]

CeylonMigrationBot commented 12 years ago

[@quintesse] I myself slightly prefer option 2, although we could still do 3 as well, just to make it consistent maybe with attributes. That way it's always visually obvious you're dealing with meta-model information, which I think it's a good thing in itself.

Although the combination of the two would mean @Iterator::Iterable() to be consistent, right?

And a question, why the ()? Why not just plain @Iterator.Iterable?

CeylonMigrationBot commented 12 years ago

[@gavinking] > And a question, why the ()? Why not just plain @Iterator.Iterable?

It's an instantiation expression.

CeylonMigrationBot commented 12 years ago

[@quintesse] An instantiation of what? Of a meta model object or something? Isn't that just a reference to a single object somewhere? Or is this all somehow more complex because it's a member type?

CeylonMigrationBot commented 12 years ago

[@gavinking] > An instantiation of what?

Of the member class Iterable of Class<Iterator>. Of course, I doubt very much that Class will have any such member class. It's just an illustration.

CeylonMigrationBot commented 12 years ago

[@quintesse] Ah ok, I was reading it like @(Iterator.Iterable()) (if such a syntax would exist). That's why it didn't make sense to me. But of course @Iterator is/returns Class<Iterator> which could have a member class Iterable. Ok got it.

CeylonMigrationBot commented 12 years ago

[@chochos] It may look a little "C++y" but for consistency I think :: is the better option.

CeylonMigrationBot commented 12 years ago

[@gavinking] I'm going to change the syntax to Foo::Bar and let's see if we hate it.

CeylonMigrationBot commented 12 years ago

[@gavinking] Hrm, at risk of opening up a can of worms, I notice that we can eliminate another ambiguity in the language by adopting the following:

"hello".size                //a value reference to size
String::size                //a metamodel reference to size
String.name                 //a value reference to the attribute name of Class<String>
Object::super.equals(that)  //call a supertype implementation
Map::Entry                  //a metamodel reference to Entry, or the type Entry

i.e. we use :: for metamodel references and ::super. for calling supertypes.

Thoughts?

CeylonMigrationBot commented 12 years ago

[@quintesse] Object::super.equals(that) is the only one in the list I'm not really happy with. It seems very verbose and the super part seems to be referring to something called super in the object's meta model. In Java I always imagined super as being an almost real reference to the super class which you could then call methods on. Also doesn't the Map::Entry become ambiguous, if you can use it to either get the type itself or its metamodel reference?

CeylonMigrationBot commented 11 years ago

[@gavinking]

Also doesn't the Map::Entry become ambiguous, if you can use it to either get the type itself or its metamodel reference?

That's the same thing. From a very abstract point of view, a metamodel for the type is the type, just like a reference to a function "is" the function. That's why I call it a "higher-order" language. Map::Entry would be a type when it appears as a type, and a metamodel reference when it appears as an expression.

CeylonMigrationBot commented 11 years ago

[@gavinking] So after talking this through with @quintesse today, I'm left thinking that the following is approach is going to be natural and internally self-consistent:

First, in type expressions, :: is used consistently to qualify types. For example, Iterable::Iterator.

Then, in value expressions:

Of course, the difference between true and @true is the following:

Boolean b = true;
Boolean b = @true.value;

Also in value expressions:

Optionally, we could also let you write Object::string. The difference would be the following:

String s = Object::string(object);
String s = Object::@string(object).value;

Now, what follows from this is that:

Furthermore, by analogy:

That is to say, the syntax for disambiguating a call to a superinterface could be:

Object::equals(super)(that)

Instead of:

Object::super.equals(that)

Which is the kinda ad-hoc solution to #3509 I proposed earlier.

CeylonMigrationBot commented 11 years ago

[@gavinking] Note that in all of the above, I'm assuming the following:

etc.

CeylonMigrationBot commented 11 years ago

[@gavinking] I'm in the middle of implementing this, and it seems to be working out fine, I suppose, and I don't hate all the ::s, but I don't love them either. Now, there is an alternative solution, that would work, though it's extremely, um, pythonic. I'm going to throw it out there just to see how you guys react...

Instead of using :: for static qualification, we could continue to use .. The only problem we had with that was that sometimes you could get name collisions between members of the metamodel, and members of a type. For example:

Person.name

Might be a reference to the metamodel of the member name of Person, or it might be an expression that evaluates the name attribute of the Class<Person> metamodel of the class Person. Therefore, the typechecker would need to give you an error on the above code. There's nothing really very unceylonic about that, we have similar cases referring to members of intersection types. But it certainly would be a PITA.

But if we could reduce the possibility of name collisions to close to zero, it would not be a real problem (for rare cases of collisions, you can use member aliasing to resolve the ambiguity). Now there is a simple algorithm for this: just name all members of metamodel types with a leading _.

Then:

 Attribute<Person,String> personNameAtt = Person.name;
 String personClassName = Person._name;

Honestly that's a lot less intrusive than:

Map::Entry me = HashMap().Entry();
people.map(Person::name);

Since you only see a "funny" character when you're really doing meta stuff (actually calling the metamodel). In "everyday" code, you would see the much more natural:

Map.Entry me = HashMap().Entry();
people.map(Person.name);

However, I never felt really comfortable with Python's underscores, so I'm not sure how I would feel about them here.

CeylonMigrationBot commented 11 years ago

[@chochos] So basically the same solution that was proposed with @? Person.@name

CeylonMigrationBot commented 11 years ago

[@gavinking] @chochos No, that's a totally different thing.

CeylonMigrationBot commented 11 years ago

[@gavinking] As a refinement of the above, you could perhaps add a layer of indirection between the "reference" and its metamodel object. So the expression Person would have the type Person(Name)&Reference<Class<Person,[Name]>>. The type Reference would have just one member, named model, or perhaps meta:

String personClassName = Person.model.name;
Attribute<Person,String> personNameAtt = Person.name.model;

Stuff like see (Person) and see (Person.name) would continue to work, since see() could accept a Reference.

The risk of model colliding with a member of a type is acceptably small, I think. You could even add a postfix operator to abbreviate that. (Yes, it has to be postfix for this to work, so don't even start!)

String personClassName = Person@.name;
Attribute<Person,String> personNameAtt = Person.name@;

But I don't personally find either of these options more readable than the _ above, FTR.

CeylonMigrationBot commented 11 years ago

[@gavinking] Hell, there's no reason for model/meta to be a member. We could make it a toplevel function and completely eliminate all ambiguity:

String personClassName = model(Person).name;
Attribute<Person,String> personNameAtt = model(Person.name);

I in fact find this extremely readable!

This might work out... wdyt?

CeylonMigrationBot commented 11 years ago

[@quintesse] But how would model(Person) work when applied to the case of see(Person)?

Forget it, it would still accept references and be able to retrieve the model from that of course.

CeylonMigrationBot commented 11 years ago

[@quintesse] I think you meant String here (or at least it would be less confusing given the rest of the examples):

So the expression Person would have the type Person(String)&Reference<Class<Person,[String]>>

Some slow-witted people like me might have problems understanding the above so I'll point out that important thing here is to realize that Person(String) is a Callable for the constructor of the type Person. It means that the expression Person can be used both to create new instances of Person as well as to get the reference to the meta data.

CeylonMigrationBot commented 11 years ago

[@gavinking]

I think you meant String here (or at least it would be less confusing given the rest of the examples):

Ahyeah, sure,if you prefer.

It means that the expression Person can be used both to create new instances of Person as well as to get the reference to the meta data.

Exactly.

CeylonMigrationBot commented 11 years ago

[@tombentley] I certainly find model(Person.name) less jarring than Person._name or Person.name@

CeylonMigrationBot commented 11 years ago

[@FroMage] So why don't we have an Object.model attribute like Java has Object.getClass()?

Though we still have the problem that I would expect Person.name.model to return the model of the Attribute type, since Person.name is of type Attribute. Similarly Person.model would be the model of the Callable type :(

CeylonMigrationBot commented 11 years ago

[@gavinking] So here's a way I think we can make all this work out without introducing much in the way of new first-level constructs. It's based around the notion of metatypes, which, curiously, are not the same thing as the metamodel objects.

I've been very busy today and didn't manage to spend much time thinking about notation or naming, but bear with me, taking the syntax with a grain of salt.

Each type X has a metatype Static<X>. Static is is a special type constructor built into the compiler. It is not an ordinary parameterized class or interface.

There is a toplevel function M mirror(Mirrored<M> mirrored) for obtaining metamodel objects, for example, mirror(X) or mirror(X.x).

CeylonMigrationBot commented 11 years ago

[@gavinking] So, after a few days reflection, I think this is the right way to go. We should introduce a special type constructor Type<T> which, like the type Bottom, can't be expressed within the language. (Yeah, this is a minor contradiction of the assertion that Ceylon doesn't have types that can't be expressed within the language, but we're talking about something at the meta level here.)

Consider the following class:

class Counter(shared Integer count, shared void run()) {}

The type Type<Counter> is isomorphic to:

interface TypeCounter 
        satisfies ClassReference<Counter,[Integer,Void()]> {
    shared AttributeReference<Counter,Integer>> count;
    shared MethodReference<Counter,Void,[]>> run;
}

Where:

(Note that ClassReference, AttributeReference, and MethodReference will probably just be aliases for intersection types, not actual interfaces.)

The language module would provide the following two functions:

shared Model model<Model>(ModelReference<Model> ref) { ... }

So then we can write:

Class<Counter> counterClass = model(Counter);
Attribute<Counter,Integer> countAttribute = model(Counter.count);
Method<Counter,Void,[]> runMethod = model(Counter.run);

Of course, we can continue to write stuff like:

Counter counter = .... ;
Integer count = Counter.count(counter);
Counter.run(counter)();

Since the XxxReference types are subtypes of the appropriate function types.

I would also love to have the following operation:

shared Type<Instance> type(Instance instance) { ... }

So that we could also write:

Counter counter = .... ;
Type<Counter> counterType = type(counter);
Class<Counter> counterClass = model(counterType);

Unfortunately, according to the definitions above, Type<T> looks invariant in T, so that would not be well-typed. It's an issue I need to think about some more. I think it might be possible by a combination of a clever definition of Type<T> and a special fiat rule in the language spec to make Type<T> covariant in T. (It's a "special" type constructor, remember!)

Finally, it's worth noticing that the "model" class Class is a type class. The function model() is a function that retrieves a type class for a type. It's interesting to speculate how this approach might in future be generalized to a full type class facility.

CeylonMigrationBot commented 11 years ago

[@gavinking] Closing, discussion moved to #3509.

We're going to stick with Foo.Bar as the syntax for qualified types. In fact, according to my proposal here, we might just remove :: altogether.

CeylonMigrationBot commented 11 years ago

[@chochos] So :: is going away even for fully qualified names? Like ceylon.language::String etc?

CeylonMigrationBot commented 11 years ago

[@quintesse] Don't think that's correct. Because those names are not part of the "language" so to speak, they're just used internally to uniquely identify a type.

CeylonMigrationBot commented 11 years ago

[@gavinking] @quintesse Right. Indeed, it's possible that one day (Not in Ceylon 1.0) we might add a real scoping operator named ::. But that would be nothing to do with qualified types as such.

CeylonMigrationBot commented 11 years ago

[@RossTate] I'm a little confused with regards to your Type proposal. I don't want to distract from the 1.0 push right now, though, but please pull me back in when this topic comes up again. There are I few things I'd like to check.

CeylonMigrationBot commented 11 years ago

[@FroMage] I guess Type can't be a subtype of Object since It can't have members such as string, hash and equals.

CeylonMigrationBot commented 11 years ago

[@gavinking] @FroMage Well, I suppose the ambiguity of Person.string is maybe a good reason to simply abandon this syntax entirely. We could, I guess, make some of these problems go away if we change the syntax a bit...

CeylonMigrationBot commented 11 years ago

[@gavinking] I think we should take seriously the original option 3 above. What about the following:

A toplevel function metamodel reference:

@coalesce

A toplevel class metamodel reference:

@Person

A toplevel class member metamodel reference:

Object@string

A nested type metamodel reference:

Map@Entry

A nested type member metamodel reference:

Map.Entry@key
CeylonMigrationBot commented 11 years ago

[@gavinking] Now, it is going to suck that we will have to write see(@Person) instead of see (Person), but I guess I think it's something I can live with...

CeylonMigrationBot commented 11 years ago

[@FroMage] I sorta like this idea, and I support it is better than a model operator.

But I think using @ will be confusing for Java users, especially if it appears in annotations. Not only that but I haven't given up on convincing you to allow @AnnotationType() as an alternative to annotationConstructor() which would make Java interop a lot nicer and be a nice option for people writing annotations declarations that don't want to write two declarations (type and constructor method) every time.

Perhaps :: would work instead of @ for model references?

CeylonMigrationBot commented 11 years ago

[@gavinking] I think ::Person is just nasty. Same for ::coalesce.

CeylonMigrationBot commented 11 years ago

[@gavinking] Plus I definitely don't think we should be warping the syntax of Ceylon in order to address a Java interop problem.

CeylonMigrationBot commented 11 years ago

[@FroMage] If I thought it was just an interop problem I wouldn't even mention it. I think our syntax for declaring annotations is as bad as our syntax for declaring enums: it's way more verbose than Java's, and it is going to hit us.

CeylonMigrationBot commented 11 years ago

[@FroMage] What about &Person then? I don't remember if we still have a unary & operator.

CeylonMigrationBot commented 11 years ago

[@quintesse] We were once discussing using #, that doesn't have prior connotations, does it?

CeylonMigrationBot commented 11 years ago

[@gavinking] We don't have a prefix &. But nor do I support the idea of having & mean two different totally unrelated things depending upon how it occurs in the expression syntax.

CeylonMigrationBot commented 11 years ago

[@gavinking] #FFFF is a hexadecimal literal.

CeylonMigrationBot commented 11 years ago

[@quintesse] Argh, true

CeylonMigrationBot commented 11 years ago

[@gavinking] We explicitly set aside @ for use in this context about a year ago, and I've jealously guarded it ever since, because I knew we were going to need it. All other symbols are already taken, with the exception of some doubled symbols like ::.

CeylonMigrationBot commented 11 years ago

[@FroMage] Based on statistical evidence that nobody ever uses union, intersections and xor on collections, let's remove these operators and free them. Then we can use & ;)

CeylonMigrationBot commented 11 years ago

[@quintesse] We should just set up a business on the side selling Ceylon keyboards that contain a dozen extra symbols

CeylonMigrationBot commented 11 years ago

[@gavinking]

Based on statistical evidence that nobody ever uses union, intersections and xor on collections, let's remove these operators and free them.

These symbols are of course already taken for type expressions. You definitely can't use them for distinguishing a metamodel ref.

CeylonMigrationBot commented 11 years ago

[@FroMage] Ah, those ones…

CeylonMigrationBot commented 11 years ago

[@gavinking] The prefix/infix @ doesn't work especially well for union/intersection types. But neither did the previous syntax. If we wanted union/intersection metamodel refs, we would need something more like a quoting syntax. Backticks would be the only available natural option, I suppose. I think they're still available.

A toplevel function metamodel reference:

`coalesce`

A toplevel class metamodel reference:

`Person`

A toplevel class member metamodel reference:

`Object.string`

A nested type metamodel reference:

`Map.Entry`

A nested type member metamodel reference:

`Map.Entry.key`

A union type metamodel reference:

`Integer|Float`

An abbreviated type metamodel reference:

`String[]`

I'm not sure which is preferable out of

see(@Person)

and

see(`Person`)
CeylonMigrationBot commented 11 years ago

[@quintesse] Just to add a pretty horrible suggestion for completeness sake.

Given the fact we already have < > for grouping you could possibly do:

@<Integer|Float>

and

@<String[]>
CeylonMigrationBot commented 11 years ago

[@gavinking] @quintesse Well, sure, you could use any of @< .... >, @[ ... ], or @( ... ) but none of those are better than backticks, I don't think.