gracelang / language

Design of the Grace language and its libraries
GNU General Public License v2.0
6 stars 1 forks source link

Unhelpful rant about traits #35

Open apblack opened 8 years ago

apblack commented 8 years ago

I was looking over James' notes from the Pomona meetings to see if I could find out what we said about the trait uses syntax.

The first option looks like this:

class list<T> {
    uses emptyness
    uses collectable

    var size is readable := 0
    method …
}

The second option looks like this

class list<T> {
    uses emptyness, collectable

    var size is readable := 0
    method …
}

The advantage of the first option, where each uses is separate, is that when the uses statements get long (with several alias and exclude clauses), they are clearly separated. The disadvantage is that they appear to be "executed" sequentially, rather than being composed and combined into the new object all at once. This syntax also introduces a new syntax error, when not all of the uses statements appear together.

The second option does a better job of expressing that all of the used traits are composed together "in parallel", but might get messy (requiring parenthesis and continuation lines) when there are several alias and exclude clauses on each.

Andrew
kjx commented 8 years ago

we have to remember that you can have one* inherits clause in there as well:

so is it

inherits X uses Y, Z

or

inherits X uses Y uses Z

or the various combinations

uses X inherits Y uses Z

J

*for natural values of “one”

KimBruce commented 8 years ago

I would tend to like the short form when there are no alias (or similar) clauses associated, while the second, longer, form when there are associated clauses. Would allowing both be so awful? (I know we are trying to avoid multiple ways to do things, but this seems rather innocuous.) The short form could be an abbreviation for the long form.

If people don't like the flexibility then I'd probably rather see the long form as it will be more readable with aliases.

On Fri, Feb 5, 2016 at 3:21 PM kjx notifications@github.com wrote:

we have to remember that you can have one* inherits clause in there as well:

so is it

inherits X uses Y, Z

or

inherits X uses Y uses Z

or the various combinations

uses X inherits Y uses Z

J

*for natural values of “one”

— Reply to this email directly or view it on GitHub https://github.com/gracelang/language/issues/35#issuecomment-180618011.

kjx commented 8 years ago

If people don't like the flexibility then I'd probably rather see the long form as it will be more readable with aliases.

Oops. I’m sorry I missed that point. I knew we needed the “long form” but had forgetting precisely why

thanks for reminding us, Kim, that is clearly the Right Thing.

Would allowing both be so awful?

yes. we don’t have to specify it, implement it, or teach it.

J

apblack commented 8 years ago

one* *for natural values of “one”

How many values does "one" take on in your universe, James?

I agree that there should be exactly one* way of writing the uses clause(s), for exactly the reasons that you specify, plus one* more: less to specify, implement, test, and teach.

Inherits should come first, since methods introduced by uses clauses override inherited methods.
And that's exactly the objection to multiple uses clauses: the second's methods do not override the first. Still, as long as we produce error messages on trait conflicts, we can probably train students to interpret them correctly. It's less clear for the uninitiated reader, and making things clear for her was one of our goals.

kjx commented 8 years ago

On 9/02/2016, at 9:02am, Andrew Black notifications@github.com wrote:

How many values does "one" take on in your universe, James?

the natural numbers :-)

I agree that there should be exactly one* way of writing the uses clause(s), for exactly the reasons that you specify, plus one* more: less to specify, implement, test,and teach.

well yes, there should be one (not one) way of writing the clauses. and one uses clauses...

Inherits should come first, since methods introduced by uses clauses override inherited methods.

they do in an asymmetric semantics, as in the Smalltalk Traits formalisation. In the symmetric semantics we’re looking at for Grace, they don’t. All parents are treated the same: if you want to have one trait override another, or one trait override a class, you have to do that explicitly with excludes and renames.

the only implicit overriding is local definitions overriding any inherited definitions (or at least that’s my current understanding)

And that's exactly the objection to multiple uses clauses: the second's methods do not override the first. Still, as long as we produce error messages on trait conflicts, we can probably train students to interpret them correctly.

right. And the same argument applies to traits vs inherited methods

It's less clear for the uninitiated reader, and making things clear for her was one of our goals

I’ve no idea which is clearer for the uninitiated reader: I’d hazard a guess that the more explicit version is more explicit to read, harder to write, and has more possibilities for “unforced errors” when writing

It’s not impossible to work out how to test this, but I think is almost impossible to get results that are generalisable .

James

KimBruce commented 8 years ago

How about a couple of simple examples with the two different semantics. I find advantages and disadvantages of each.

It makes sense to me to start with the inherited code, and then override with traits, and then with new code for the class.

On the other hand it is certainly clearer to the reader, if you must explicitly select which version you want (assuming you didn’t override). However, it is slightly more of a pain to write it. Are there compelling examples one way or the other?

apblack commented 8 years ago

Kim,

At this point we are not talking about different semantics — just different syntax.

I did give an example in the original post. Here it is with some alias and exclude clauses (the names are invented for this example). I suggest that you look at this on github, not in email.

The first option looks like this

class list<T> {
    inherits collection exclude size
    uses emptyness alias eTIsEmpty = isEmpty exclude sizeifUnknown(1)
    uses collectable alias collTIsEmpty = isEmpty exclude lazyFilter(1)

    var size is readable := 0
    method isEmpty { eTIsEmpty && collTIsEmpty }
    method …

}

The second option looks like this

class list<T> {
    inherits collection exclude size
    uses 
        emptyness alias etIsEmpty = isEmpty exclude sizeIfUnknown(1),
        collectable alias collIsEmpty = isEmpty exclude lazyFilter(1)

    var size is readable := 0
    method isEmpty { etIsEmpty && collIsEmpty }
    method …
}

Note that in this second example, the uses statement is a single statement on one logical line. I've chosen to use layout to make it more readable, which I think is what I would teach students to do, but there is nothing to stop them writing it all on a single line.

apblack commented 8 years ago

OK, my apologies; I now see that James does want to revisit the semantics:

In the symmetric semantics we’re looking at for Grace

I didn't know about this "we".

I'm guessing that you are referring to the bigger language in which inherits and uses mean the same, and we have to give a meaning to multiple inheritance with fields and defs and initialization ... Is that right?

KimBruce commented 8 years ago

On Mon, Feb 8, 2016 at 4:26 PM Andrew Black notifications@github.com wrote:

OK, my apologies; I now see that James does want to revisit the semantics:

In the symmetric semantics we’re looking at for Grace

I didn't know about this "we".

I'm guessing that you are referring to the bigger language in which inherits and uses mean the same, and we have to give a meaning to multiple inheritance with fields and defs and initialization ... Is that right?

I wish we could stick with the simpler language where uses clauses are restricted. There is still a question there, but we are getting too tangled up with not understanding where we are starting from.

kjx commented 8 years ago

On 9/02/2016, at 13:26pm, Andrew Black notifications@github.com wrote:

I'm guessing that you are referring to the bigger language in which inherits and uses mean the same, and we have to give a meaning to multiple inheritance with fields and defs and initialization ... Is that right?

since the last meeting in Pomona, all the semantics that were written down have always been symmetric - or at least: all the semantics I wrote I intended to be symmetic.

This is not just a “larger language” question - it affects even simple examples with multiple traits and/or classes. I’ve put an example in below in this email, that runs in kernan.

I suggest we put this on the list to talk about tomorrow.

James

//PS: code below can be pasted into http://homepages.ecs.vuw.ac.nz/~mwh/js-kernan/entry/

class catClass { method move { "walk" } } trait catTrait { method move { "walk" } } class fishClass { method move { "swim" } } class fishTrait { method move { "swim" } }

class catFish { inherits catClass uses catTrait }

class fishCat { inherits fishClass uses catTrait }

class allTraits { uses catTrait uses fishTrait }

class allTraits2 { uses fishTrait uses catTrait }

//PPS: extra question for experts: if a syntatic class declaration is used to declare a trait, //can that trait be “used” or must it be “inherited” ?

class allClasses { uses fishClass uses catClass }

class allClasses2 { uses catClass uses fishClass }

//tests - choose one, the result in symmetric semantics is always the same //adding an alias or a exclude clause to either import is needed to fix this

catFish.move fishCat.move allTraits.move allTraits2.move allClasses.move allClasses2.move

KimBruce commented 8 years ago

Is there a way of getting the IDE to show line numbers?

Kim

On Feb 8, 2016, at 5:56 PM, kjx notifications@github.com wrote:

On 9/02/2016, at 13:26pm, Andrew Black notifications@github.com wrote:

I'm guessing that you are referring to the bigger language in which inherits and uses mean the same, and we have to give a meaning to multiple inheritance with fields and defs and initialization ... Is that right?

since the last meeting in Pomona, all the semantics that were written down have always been symmetric - or at least: all the semantics I wrote I intended to be symmetic.

This is not just a “larger language” question - it affects even simple examples with multiple traits and/or classes. I’ve put an example in below in this email, that runs in kernan.

I suggest we put this on the list to talk about tomorrow.

James

//PS: code below can be pasted into http://homepages.ecs.vuw.ac.nz/~mwh/js-kernan/entry/

class catClass { method move { "walk" } } trait catTrait { method move { "walk" } } class fishClass { method move { "swim" } } class fishTrait { method move { "swim" } }

class catFish { inherits catClass uses catTrait }

class fishCat { inherits fishClass uses catTrait }

class allTraits { uses catTrait uses fishTrait }

class allTraits2 { uses fishTrait uses catTrait }

//PPS: extra question for experts: if a syntatic class declaration is used to declare a trait, //can that trait be “used” or must it be “inherited” ?

class allClasses { uses fishClass uses catClass }

class allClasses2 { uses catClass uses fishClass }

//tests - choose one, the result in symmetric semantics is always the same //adding an alias or a exclude clause to either import is needed to fix this

catFish.move fishCat.move allTraits.move allTraits2.move allClasses.move allClasses2.move

— Reply to this email directly or view it on GitHub https://github.com/gracelang/language/issues/35#issuecomment-181666776.

kjx commented 8 years ago

On 9/02/2016, at 16:54pm, Kim Bruce notifications@github.com wrote:

Is there a way of getting the IDE to show line numbers?

I imagine we can plug in whatever editor and highlighter we want (for suitable values of “we”)

it should also be possible to use Kernan as a backend e.g. to Tim’s existing JS front-end.

it’s all a small matter of programming

James

KimBruce commented 8 years ago

I hadn’t remembered that the alias clause left out the parameters. Clearly this causes an issue with allowing overriding by arity.

I’d prefer to see parameters actually written down, e.g.,

        uses emptyness alias eTIsEmpty(x,y) = isEmpty(x,y) exclude sizeIfUnknown(blk)

— of course if there were no parameters, we would leave them out.

I would probably also allow writing underscores if we didn’t want to write names — at least if we restricted this so that the new name has exactly the same parameter shape (number of parts and number of params in each). However, it doesn’t seem to be much of a burden to require the user to use real names.

kjx commented 8 years ago

On 10/02/2016, at 1:12 pm, Kim Bruce notifications@github.com wrote:

I hadn’t remembered that the alias clause left out the parameters. Clearly this causes an issue with allowing overriding by parity.

yep.

I’d prefer to see parameters actually written down, e.g.

uses emptyness alias eTIsEmpty(x,y) = isEmpty(x,y) exclude sizeIfUnknown()

again: does that mean we can now re-order them?

exclude sizeIfUnknown()

but WHICH sizeIfUnknown()

to avoid making this much worse, if there is only one method perhaps you can avoid listing parameters or rarities? Or (for exclude) does it take out the lot?

I would probably also allow writing underscores if we didn’t want to write names — at least if we restricted this so that the new name has exactly the same parameter shape (number of parts and number of params in each). However, it doesn’t seem to be much of a burden to require the user to use real names.

the names don’t have to match the decorations do they?

the more I think about this the less I like it. Unlike symmetric inheritance proposals, there is no way to hide this complexity

J

KimBruce commented 8 years ago

Given how we are using “alias” I’m fine with not being able to re-order them (though I’m sure we could find a use-case for allowing reordering).

again: does that mean we can now re-order them?

exclude sizeIfUnknown()

but WHICH sizeIfUnknown()

to avoid making this much worse, if there is only one method perhaps you can avoid listing parameters or rarities? Or (for exclude) does it take out the lot?

For simplicity I’d say keep them in for all. It makes it clearer what is happening.

kjx commented 8 years ago

On 10/02/2016, at 14:28pm, Kim Bruce notifications@github.com wrote:

Given how we are using “alias” I’m fine with not being able to re-order them (though I’m sure we could find a use-case for allowing reordering).

there are use cases for almost everything.

the question is - what is the power to weight ratio?

James

apblack commented 8 years ago

The point of the alias command is to provide a general purpose alternative to super. If a programmer wants to give a method a name with a new "shape", provide default arguments, re-order arguments, etc, they don't need a special-purpose syntax, because they already have method declarations. So although it may be useful to have these abilities in the alias clause, it isn't necessary.

method backwardsBind (value) to (key) { 
    bind (key) to (value)
}
kjx commented 8 years ago

"Magic methods" (the contents of GraceObject) are horrible (*).

Assuming traits are "equivalent to" methods returning object {}s, and so traits can be defined (or used directly) as if they were methods returning object {}s, and all objects {}s get the magic methods, then traits must also inherit from GraceObject or otherwise get the "magic methods".

Thus: as soon as you "use" two traits, you'd have to pick the magic methods (individually, now with arguments) from one or the other. In Andrew's basic asymmetric proposal, even one trait would silently override the magic methods coming down from the class, even if the class overrode those definitions and the trait didn't. Andrew's adapted "use doesn't bring in magic methods" proposal means that e..g a comparableTrait that defines <, >, <=, >= and so on, cannot define == or !=. The symmetric proposal is arguably worse: you'd have to exclude all magic methods from all except one trait or class (or possibly (argh!) alias the magic methods to themselves in just one trait --- will have to check what that does).

There seem to be at least four options to me:

  1. - Andrew's proposal to distinguish between "use" and "inherit", although really I'm not sure how. Either the class or one or more traits could define one or more magic methods, and if the definitions are in the traits, it doesn't work (see comparableTrait above).
  2. - Distinguish between the "default" magic methods and "overridden" magic methods. Default magic methods would never be inherited from class or traits, via use or inherit, but would be added into objects if there weren't definitions there. If exactly one parent has a definition of a magic method, we use that one. If more than one, it's ambiguous (**). Works fine symmetrically, can be extended asymmetrically (i.e. a unique defn across all "used" traits overrides all definitions from something "inherited")
  3. - Don't have magic methods. This is the cleanest option, and it worked fine in Self --- but everyone would have to write "inherits GraceObject" to get them. If we had a good IDE or reflection, then that is actually not so bad. Really. Objects are interrogated reflexively by the IDE; and once you're a few weeks in, you just learn to write that at the top of the class hierarchy.
  4. - We change the class macro (only) in insert "inherits GraceObject" if it's not there already, but not the trait macro and not the object constructor. Class based-programming still works; mixing in traits works; object-based programming works so long as you avoid the "magic" methods or inherit GraceObject explicitly.

I think I prefer 3, 4, 2, then 1 at this point.

footnote * presumably there is also an equivalent issue in types, so all types must "inherit" from GraceType. Or something

footnote \ this raises the question: why does this work for magic methods, but not for normal methods? In Smalltalk traits the same method declaration inherited from two or more traits does not have to be resolved, but our design it does (because traits are currently generative --- methods returning objects). Writing rules for when methods are the same or are different will be just too hard for other cases than default magic methods. And even then, the Wrong Thing will happen without going to full class or method precedence lists, i.e. C3. Let's not!

apblack commented 8 years ago

It really does not help to confuse the issue tracker with random rants about completely unrelated stuff.
The topic of this issue is whether to write

    uses emptyness
    uses collectable

or to write

     uses emptyness, collectable

I know that there are many other questions. It would help us make progress if you opened issues for them, separately, and discussed them there.

I think that the best thing that we can do with this issue now is close it. I'll reopen the original question as a new issue.

KimBruce commented 8 years ago

I think I’m happiest with your 4 (or perhaps a minor tweak) as it requires the least change in adding traits to the language and will be most familiar to those familiar with ordinary trait-based languages. Here is the way I would describe it.

a. If a class or object expression does not have an inherits clause, implicitly insert an "inherits topClass” (or whatever we are calling the minimal class with “magic methods”). This is slightly different from your proposal in that I would change object expressions as well, as the programmer is going to always want those magic methods automatically there.

b. Traits define “mixin” classes/objects that do not contain magic methods (technically they need to generate new things when mixed in, but because there is no initialization code, it shouldn’t matter in practice — the "no initialization” is a key part of my notion of traits).

c. Type definitions automatically contain the types of magic methods (as they do now), unless they are overridden in that type definition. I don’t believe we’ve talked about types for traits, but I’m not sure it matters whether or not they are considered to implicitly include the magic methods, as the resulting type when you mix in the trait should be the same either way (as far as I can tell).

example:

type T = {m(x:U) -> V}

class c(…) -> T { method m(x:U) -> V {…} method toString -> String {…} }

traitType W = {n(y:Y) -> Z}

trait t -> W { method n(y:Y) -> Z {…} }

type S = T & W

class d(…) -> S { inherits c(…) uses t ... }

Again, I’m not sure there is any need to distinguish what I wrote above as “traitType” from “type”. Because t can only occur in a “use” clause there seems little danger of confusion.

At this point, I’m inclined not to allow a trait to be used as a class, as it is easy enough to convert it if needed:

class ct -> V { uses t }

This does not impact existing uses of t, but now ct generates an object with the “magic” methods (i.e., V is now considered to have the magic methods).

Finally, a new question: Did we ever discuss the scope of “alias” definitions? Are they treated as private (only available within the scope of the class/object definition) or confidential. I assume we would not want the default to be public. Could we declare them to be public in the “alias” clause? I don’t remember discussing this. Private seems the “right” thing, but I’m not sure I want to have to introduce that as a new concept, so perhaps its better to have them default to be confidential.

NOTE: Settling all of this will most likely result in the need to change the blog post about traits.

On Feb 9, 2016, at 10:00 PM, kjx notifications@github.com wrote:

"Magic methods" (the contents of GraceObject) are horrible (*).

Assuming traits are "equivalent to" methods returning object {}s, and so traits can be defined (or used directly) as if they were methods returning object {}s, and all objects {}s get the magic methods, then traits must also inherit from GraceObject or otherwise get the "magic methods".

Thus: as soon as you "use" two traits, you'd have to pick the magic methods (individually, now with arguments) from one or the other. In Andrew's basic asymmetric proposal, even one trait would silently override the magic methods coming down from the class, even if the class overrode those definitions and the trait didn't. Andrew's adapted "use doesn't bring in magic methods" proposal means that e..g a comparableTrait that defines <, >, <=, >= and so on, cannot define == or !=. The symmetric proposal is arguably worse: you'd have to exclude all magic methods from all except one trait or class (or possibly (argh!) alias the magic methods to themselves in just one trait --- will have to check what that does).

There seem to be at least four options to me:

  • Andrew's proposal to distinguish between "use" and "inherit", although really I'm not sure how. Either the class or one or more traits could define one or more magic methods, and if the definitions are in the traits, it doesn't work (see comparableTrait above).
  • Distinguish between the "default" magic methods and "overridden" magic methods. Default magic methods would never be inherited from class or traits, via use or inherit, but would be added into objects if there weren't definitions there. If exactly one parent has a definition of a magic method, we use that one. If more than one, it's ambiguous (**). Works fine symmetrically, can be extended asymmetrically (i.e. a unique defn across all "used" traits overrides all definitions from something "inherited")
  • Don't have magic methods. This is the cleanest option, and it worked fine in Self --- but everyone would have to write "inherits GraceObject" to get them. If we had a good IDE or reflection, then that is actually not so bad. Really. Objects are interrogated reflexively by the IDE; and once you're a few weeks in, you just learn to write that at the top of the class hierarchy.
  • We change the class macro (only) in insert "inherits GraceObject" if it's not there already, but not the trait macro and not the object constructor. Class based-programming still works; mixing in traits works; object-based programming works so long as you avoid the "magic" methods or inherit GraceObject explicitly. I think I prefer 3, 4, 2, then 1 at this point.

(* presumably there is also an equivalent issue in types, so all types must "inherit" from GraceType. Or something) () this raises the question: why does this work for magic methods, but not for normal methods? In Smalltalk traits the **same method declaration inherited from two or more traits does not have to be resolved, but our design it does (because traits are currently generative --- methods returning objects). Writing rules for when methods are the same or are different will be just too hard for other cases than default magic methods. And even then, the Wrong Thing will happen without going to full class or method precedence lists, i.e. C3. Let's not!

— Reply to this email directly or view it on GitHub https://github.com/gracelang/language/issues/35#issuecomment-182211392.

kjx commented 8 years ago

(answering Kim)

b. Traits define “mixin” classes/objects that do not contain magic methods

so this means that traits cannot rewrite to methods containing object constructors, or even to e.g. defs containing manifest objects --- because the object constructors in the rewriting will make objects with magic methods.

Did we ever discuss the scope of “alias” definitions? Are they treated as private

I believe we did, but later over email or something. Kernan supports the following:

class Foo {
... method m {"M"}
... }

Foo.m
M class Bar {
... inherits Foo
... alias n = m is confidential
... }

Bar.n
Uncaught exception: AccessibilityError: R2003: Confidential method «n» cannot be accessed from outside the object from «n», at line 1 of source code Bar.m
M

but we don't have a good explanation for how.

NOTE: Settling all of this will most likely result in the need to change the blog post about traits.

we obviously cannot post that post until this is sorted.

kjx commented 8 years ago

Andrew wrote

I think that the best thing that we can do with this issue now is close it. I'll reopen the original question as a new issue.

I renamed this and opened it again at #36. But I haven't closed this because we need to rant about this somewhere: where else do you suggest?

apblack commented 8 years ago

I suggest keeping rants to a single topic. That way they might be helpful. I don't have time to participate in un-helpful rants.