Closed CeylonMigrationBot closed 8 years ago
[@ikasiuk] When reading the text, with every example the syntax become harder to understand intuitively. I would say that at the point where you eliminate the "function" keyword, it goes from "ok, this is pretty obvious" to "wait, give me a second", especially when I imagine seeing it for the first time, without all the explanation you are giving here.
You are right of course, the Smalltalk style is clearly better in terms of language extensibility. It is not even very complicated as such, once you have understood it. The real problem is that it looks so different from the rest of the language, which makes it really hard to comprehend when you first see it. I do understand it now, but it still feels a bit out of place - kind of alien.
So the question is: what is more important - simplicity and understandability, or extensibility? I tend to prefer the former, but it is not an easy decision.
[@FroMage] I'm for
people.filter(function (Person p) {p.age>18});
as an alternative to
people.filter{function by(Person p) {p.age>18}};
I don't like parenthesis for deferred expressions as that's too akin to regular (evaluated now) expressions, while curly braces (well, except for named invocation) always denote a special control flow (if, for, while, method, class).
The function
keyword makes this more regular and I don't buy that it's too verbose for the same reason that if
is not too verbose and should not be replaced with (b ? t : e)
.
Making closures implicitly return the last evaluated expression seems very reasonable though, not sure why I feel that way.
[@gavinking]
I would say that at the point where you eliminate the "function" keyword, it goes from "ok, this is pretty obvious" to "wait, give me a second", especially when I imagine seeing it for the first time, without all the explanation you are giving here.
Agreed. But do you think that it would be acceptable for Ceylon to have a syntax for inline functions that is more verbose than Java 8, C#, Fantom, Gosu, Kotlin, Scala, ML, Haskell, Smalltalk, Ruby, Clojure ... Indeed, the only languages I know of which are similarly verbose here are Python and JavaScript, and I believe that there is a proposal to introduce an alternative, less verbose, syntax in the next release of JavaScript!
And even if it is desirable, which I'm totally prepared to take seriously, isn't it possible that this would be a turnoff that would affect adoption of the language? (I'm playing devil's advocate.)
So the question is: what is more important - simplicity and understandability, or extensibility?
Well, at some level the whole idea behind Ceylon is to get extensibility without sacrificing understandability. But the two goals are often in tension, and I they seem to be here.
[@FroMage] I don't care about more verbose for something we think is saner. I don't like that in Scala you can't see if an expression is evaluated now or later without knowing the type of the variable where it ends up.
For example, given:
foo(bar++);
I don't know if bar++
is evaluated now or later unless I know if the type of the foo()
parameter is Integer or Closure.
I am a bit of an extremist on this topic, I admit, but I strongly believe that the vast majority of people actually like to see if something is evaluated now or later. Call me an old fashion Scheme/JavaScript guy, but I'm convinced that this sort of visibility is what the vast majority of developers want, and not some terrible looking #()(2)
or ()(2)
like in some of those languages you cite.
[@ikasiuk] Yes, brevity seems to be very important for some people, and Ceylon has to face the comparison with other languages. Hm, Stéphane has a point with the curly braces making it look more clearly. I have the same intuition there: curly braces => control flow. What about
people.filter((Person p) {p.age>18});
or
people.filter(by(Person p) {p.age>18});
Somehow looks much better to me than with parentheses.
[@FroMage] If brevity is important to people, then let's keywordise fun
and require it on closures. Or λ
(as an optional alternative) to be really geeky. Personally I don't buy that this is the sort of brevity that matters, not when we think that "value" is better than "var".
[@gavinking] Stef, I just verified that either of the following options can be parsed:
people.filter(function (Person p) p.age>18);
Or:
people.filter((Person p) p.age>18);
So if we limit inline functions to only occur within positional argument lists, we actually would not need any delimiters at all for the body in the "simply returns an expression" case - which is, after all, the most common case.
But before we went down that path, I would want to know how important we think the following are:
object
declarations, andNow, I suppose there's no reason why we could not later (post-Ceylon 1.0) add support for Smalltalk-style invocations if we were to only support this simple case in M2. But I still want us to think it though.
[@ikasiuk]
If brevity is important to people, then let's keywordise fun and require it on closures. Or λ (as an optional alternative) to be really geeky. Personally I don't buy that this is the sort of brevity that matters, not when we think that "value" is better than "var".
I completely agree (also with the Scala example you give above). As Gavin says: "playing the devil's advocate"... We should not sacrifice clarity for brevity. But I still think that when using curly braces the "function" keyword is not necessary to understand the code intuitively.
[@gavinking]
I don't care about more verbose for something we think is saner. I don't like that in Scala you can't see if an expression is evaluated now or later without knowing the type of the variable where it ends up.
I'm not proposing that. In people.filter((Person p) p.age>18)
,
it's very clear that things are evaluated lazily. That's the effect of the
parameter list.
[@gavinking]
I have the same intuition there: curly braces => control flow.
The problem with this that that curly braces are already doing a lot of work in Ceylon. And I've worked very hard to make sure that the various interpretations they have:
Are all nice and mutually regular. i.e. that every construct that can appear in any of those contexts has the same interpretation if it appears in any other one of the three.
But now you guys are suggesting a fourth interpretation of curly braces where an expression that appears inside the curies has a different interpretation. (At least, I have not yet managed to rationalize that the interpretation is consistent.)
Unless I can see that in
(Person p) {p.age>18}
the expression p.age>18
has an interpretation that is somehow
regular with it's interpretation in:
print {"adult: ", p.age>18}
Then I guess I'm against it...
[@FroMage] Do you find foo(() 2)
more or less readable than foo(function(){ 2 })
?
[@FroMage] If closure bodies and method bodies don't register as similar to you for justifying curly braces then I wonder why not.
[@FroMage] if we limit inline functions to only occur within positional argument lists
Mmm, then we can't do something like Callable<...> f = function(){2};
?
[@gavinking]
Do you find
foo(() 2)
more or less readable thanfoo(function(){ 2 })
?
Good question. :-)
If closure bodies and method bodies don't register as similar to you for justifying curly braces then I wonder why not.
In a method body that returns the value of an expression, you need to write
return p.age>18;
, not p.age>18
.
[@FroMage] In a method body that returns the value of an expression, you need to write return p.age>18;
, not p.age>18
.
Right, as I said, I don't know why I have nothing against return statements being optional in closures. Can't explain it but it feels right somehow.
[@gavinking]
Mmm, then we can't do something like
Callable<...> f = function(){2};
?
Well, that's the lines I'm thinking right now. Notice that this is not a very useful thing to be able to write, since:
value f = function () 2;
is semantically identical to:
function f() { return 2; }
However, we could loosen the restriction to allow you to write value f = function () 2;
. All we would need to say is that this syntax can appear after a =
as well as in a positional argument list. That's still easy to parse.
What I don't want to say is that function () 2
is an atomic expression. That would *definitely( introduce the requirement for delimiters around the body. As well as being quite useless, as far as I can tell.
[@ikasiuk] I'll refrain from opening an issue "Gavin King doesn't let me play Skyrim"...
But seriously: if the curly braces are a problem, that syntax would be acceptable I guess:
people.filter((Person p) p.age>18);
// or
people.filter(by(Person p) p.age>18);
// or even
people.filter(function(Person p) p.age>18);
[@quintesse] For me one of the things that hurts my eye is the double brackets that appear everywhere except for some of the single expression examples. I know some of you say that it is the most common occurrence but I just don't know if I agree. Still, I'm all for allowing the function and object versions Gavin showed in the first message, enabling that will make it possible to start using that kind of code (and make Stef happy) and in the mean time we can think about other possible syntaxes or leave it for later.
[@gavinking] So here's something that ties in with this. Currently, I've said that a getter-style named argument is simply a convenient way to calculate an expression inline in a UI. But that doesn't really make it that useful. An alternative interpretation, that I've toyed with quite a bit is to say that this results in a Gettable
TextInput {
value model {
return person.name;
}
assign model {
person.name:=model;
}
}
OK, that works, but it's super-verbose. But what if we supported the syntax value person.name
as an abbreviation, by analogy with function () person.name
? Then we can pass person.name
by value as so:
TextInput(value person.name)
That's pretty damn nice. And it's all very semantically consistent. Because that would, at some vague conceptual level, just a kind of abbreviation of:
TextInput(value { return person.name; } assign { person.name:=model; } )
In the same way that
people.filter(function (Person p) p.age>18)
is like an abbreviation of
people.filter(function (Person p) { return p.age>18; })
and even
Button(void (ClickEvent event) print(event))
would be an abbreviation for:
Button(void (ClickEvent event) { print(event); })
So we would wind up introducing the following syntax into positional argument lists:
function (X x, Y y) expr
void (X x, Y y) expr
value expr
And the value p { ... } assign p { ... }
syntax into named argument lists.
We would thereby kill inline functions and pass-by-reference at once without futzing up our syntax.
[@gavinking] OTOH, we also need to consider the relationship between lazy evaluation/pass-by-reference and metamodel references. I've been working on the assumption that for a toplevel or local declaration, the syntax for a metamodel reference is the same as for pass-by-reference. That is, choosing a totally hypothetical syntax for the sake of argument, if @null
is a metamodel reference to the toplevel value named null
, then you would pass null
by reference to method()
using method(@null)
.
[@ikasiuk] So "value expr" basically means that you are passing expr by reference, not by value?!
[@FroMage] Is this for attribute/method references or for any sort of lazy expression?
I am a bit confused by the "value" keyword for making lazy references.
[@RossTate] I don't like dropping the function
keyword. I'm not opposed to shortening it to fun
. In fact, I sometimes like it when keywords don't match up with natural words so that I can use the natural words for my variable names.
Something I would like to aim for: forwards-compatibility with (empty) tuples and with juxtaposition as an operation. Seems to cause problems for both of these (the usual notation for the empty tuple is ()
), and the Smalltalk thing looks problematic for juxtaposition.
I'm also up for using ->
or =>
. I know that's inconsistent with your syntax for entries, but I honestly don't care for that syntax. As I see it, once you add tuples (or at least pairs), you'll want to throw it away and replace entries with pairs.
I like the idea of a shorthand for the case when functions just return an expression. The one that looks nicest to me is wrapping the expression in parentheses. I imagine this is also easier for parsing, and it avoids ambiguity issues.
I do not like the idea of having different syntax for anonymous functions when used as positional arguments versus as lone expressions.
[@guyv] I really should get into Ceylon some more before commenting on issues likes these, but in the filter example: are we really obliged to repeat the type (Person p)? Can't that be derived in most cases?
[@RossTate] If the parameter to people.filter
is a subtype of Callable<Top,People>
, then yes. However, if people.filter
takes an Object
or if it is polymorphic/generic, then no. For example, I could do the following:
shared <T> Callable<T,T> repeated(int n, Callable<T,T> func) {...}
Then for repeated(5, function (param) { return param.next; })
, you can't infer the type of param
. In fact, there could be many valid types for param
, so guessing the type of param
is dangerous since it could affect the semantics of the program.
You could say "guess it when you can guess it", but this makes your language fickle. In particular, whenever you update the language and/or its inference algorithms, you have to make sure that anything you could infer before can still be inferred. Your inference algorithm becomes a part of your language specification and type system whether or not you intended so.
So, I'm trying to avoid such issues by having all type checking and type inference be decidable (and trying to be forwards-compatible with possible future features).
[@gavinking]
So "value expr" basically means that you are passing expr by reference, not by value?!
Haha, yeah, that's not intuitive ;-)
[@gavinking]
Is this for attribute/method references or for any sort of lazy expression?
The proposal was that it could be any expression, for example:
value person.firstName + " " + person.lastName
[@gavinking]
I really should get into Ceylon some more before commenting on issues likes these, but in the filter example: are we really obliged to repeat the type (Person p)? Can't that be derived in most cases?
Echoing Ross: no, not in general. Yeah, there are some cases where you intuitively can, but it's hard to write down exactly which cases they are. We're trying to stick very, very rigidly to this idea of always using principal types. The principal type of function (Person p) p.age>18
is well-defined: it is Callable<Boolean,Person>
. The principal type of function (p) p.age>18
is not well-defined. We need to look at the surrounding invocation to determine what type it has. Indeed, we can't even analyze the type of p.age
without looking at the surrounding invocation.
My experience is that whenever languages steer away from the use of principal types - and there are lots of cases like this where you think you might want to - they always wind up introducing pathological and/or unintuitive corner cases.
[@gavinking]
OTOH, we also need to consider the relationship between lazy evaluation/pass-by-reference and metamodel references.
So I'm hung up on this one. For an attribute of a class, I want a metamodel reference to look like this:
Person.name
which has type Method<Person,String>
. The problem is when you get to toplevel attributes. Unfortunately,
null
is supposed to evaluate the toplevel attribute null
, and therefore has type Nothing
. Currently, the spec jumps through some hoops to say that it really has type Value<Nothing>
which extends Gettable<Nothing>
and is therefore implicitly converted to Nothing
where needed. This also solved the problem of pass-by-reference. But I think it's clear that implicit conversions are now totally out of the question for this language.
So we need some other syntax for a metamodel reference to a toplevel attribute. An old version of the spec said that metamodel references look like Person#name
, #null
, #print
, Equality#equals
, #Range
, etc. But that's really noisy and unnecessary for methods, interfaces, classes, and member attributes. We just have this one annoying case where we need a special syntax. I think we need to resolve this problem before we can get comfortable with the idea of value person.name
.
[@gavinking] So the wider question is: what is the syntax for referring to an attribute without evaluating it? I'm just not sold that value null
is at all intuitive. OTOH, nor am I absolutely in love with something like null@
or @null
.
[@gavinking] Well, arguing against myself as usual, if we had a syntax like @null
for referring to toplevel attributes without evaluating them (returning you a metamodel reference), that doesn't actually make the idea of value expr
obsolete, since the form value expr
is accepting a whole expression and wrapping it up in a new value
declaration. So perhaps these two issues can be unlinked after all.
But I still want to know what is our work-in-progress solution for metamodel references to toplevel attributes before we make a final decision on this issue.
[@FroMage] My problem with value person.firstName + " " + person.lastName
is that of scope again: does value
(forgetting that the keyword is counterintuitive) apply to the first or all arguments?
I'm not even sure we need anything lazy once we have references and closures. Why do we need it again?
As for references, well, you're not going to like it but in C you'd write &(obj.attr)
to get a reference. I can totally live with obj#attr
and I think this is what Java 8 is proposing.
[@gavinking]
My problem with
value person.firstName + " " + person.lastName
is that of scope again: does value (forgetting that the keyword is counterintuitive) apply to the first or all arguments?
Well, that's the thing, it applies to everything that follows, up to the ,
or )
. It's part of the syntax of the positional arg list, so we don't run into the kind of issues we would face if it was an atom of the expression syntax.
The same comment applies to function
:
people.filter(function (Person p) p.age>18)
The scope of function
extends to the ,
or )
.
[@gavinking]
As for references, well, you're not going to like it but in C you'd write &(obj.attr) to get a reference.
Well, I certainly don't love it. But see #3163. The combination of:
Kinda forces us into needing something a bit cryptic like this. I don't see a way out of it.
I can totally live with obj#attr and I think this is what Java 8 is proposing.
I had no idea Java8 was proposing this. Link?
[@FroMage] I am allowed to write a closure with statements though right?
people.filter(function(Person p){
if (p.age < 18) {
throw;
}else{
return p.age < 21;
}});
[@ikasiuk]
I'm not even sure we need anything lazy once we have references and closures. Why do we need it again?
Indeed, a small example or two could help understanding what this discussion is actually about.
[@FroMage] http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-3.html see 8. Method references
.
Though I don't buy their prefix notation #person.name
precisely because of:
We will likely restrict the forms that the receiver can take, rather than allowing
arbitrary object-valued expressions like "#foo(bar).moo", when capturing instance
method references.
[@gavinking]
I am allowed to write a closure with statements though right?
I mean, I would be inclined to initially leave that out. You can do it with a named argument invocation. I personally find stacking {}
inside ()
to be super-ugly. We can always add it later...
[@FroMage] Well, ugly perhaps but it's not intuitive to me that closures are only for expressions :( I think people would not like that.
[@gavinking]
I'm not even sure we need anything lazy once we have references and closures. Why do we need it again?
In my mind "lazy" has pretty much always been the same feature as "references".
But if you want to define inline and pass-by-reference at the same time, then that's what value expr
would let you do, under this proposal.
TextInput(value items[i]?.product.code)
or
TextInput {
value model {
return items[i]?.product.code;
}
assign model {
items[i]?.product.code := model;
}
}
I think once you start getting into higher-order use of this stuff you could get some crazy-cool stuff like:
Input(stringConverter(value some.number))
Where stringConverter()
accepts a Settable<T>
and returns Settable<Sting>
.
[@gavinking]
see 8. Method references.
OK, but we don't actually have any kind of problem with method references. It's attributes that are the problem, specifically toplevel attributes.
[@gavinking]
Well, ugly perhaps but it's not intuitive to me that closures are only for expressions :( I think people would not like that.
Perhaps. Probably. I just think that a named argument list is much more elegant for this usecase. Anyway, I said "initially". There's no problem supporting the full form with a block.
[@k-abram] For the following:
text.addListener()
listener satisfies TextListener {
shared actual onEdit(TextChange change) { ... }
};
what does "listener satisfies TextListener" buy us? Isn't it mandatory that whatever is sent in is a TextListener? If so:
text.addListener( {
shared actual onEdit(TextChange change) { ... }
});
[@gavinking] @kpolo you also need to take into account what can actually be parsed using a context-free grammar. Your suggestion can't be. And even if you do have something that can theoretically be parsed with a bunch of lookahead, it still might be an absolute nightmare in the IDE, where you have to tolerate non-wellformed syntax and do something meaningful with it.
So even though I, as a human reader, can tell what you mean by the above (by reading all the way to the end of the code), an LL grammar would have a lot of problems with it.
[@ycros] I like the value
proposition, and I also think restricting it to expressions makes sense - trying to cram statements into an argument list IS ugly.
It just needs a new name. How about lazy
, since everyone seems to be using it already. (Other ideas: expr
, ref
, fn
, lambda
)
[@set321go] I personally would prefer the curly braces but i dont think there should be a need to name functions or for the return
statement. I think if you have the braces you can get rid of the keywords, as the braces still denote a block clearly.
people.filter((Person p) { p.age>18; })
if you get rid of the braces then i think you need to put something back in
people.filter(function (Person p) p.age>18; )
[@gavinking] Again, it is to me a very important issue of regularity that { statement; }
and { expression }
always mean the same thing no matter where they appear in code. A syntax like:
people.filter((Person p) { p.age>18; })
or
people.filter((Person p) { p.age>18 })
is completely irregular compared to the rest of the language. The first is a syntax error, because p.age>18
is not a valid expression statement, and the second is a vararg containing a single value of type Boolean
. The language is extremely consistent about this stuff.
[@danberindei]
You could say "guess it when you can guess it", but this makes your language fickle. In particular, whenever you update the language and/or its inference algorithms, you have to make sure that anything you could infer before can still be inferred. Your inference algorithm becomes a part of your language specification and type system whether or not you intended so.
You can solve this by enabling type inference only in the cases where you know that you for certain that your type inference algorithm is decidable. The rules for enabling/disabling type inference should be much easier to understand than the inference algorithm, so there would be no problem adding them to the language spec. Since they are applied before the inference algorithm you can still without worrying about arcane corner cases. And it would still remove the type name from 99% of the code.
The thing I hate most about Java's inner classes are all those ()
s inside of {}
s inside of ()
s. I try to make my code as "flat" as possible, and I would rather write an extra keyword than an extra pair of parentheses. So I would prefer something like this:
value adults = people.filter(function p -> p.age>18);
or
value adults = people.filter{ by Person p -> p.age>18 };
I'm pretty sure you can parse function a, b -> a + b
as function( a, b )-> a + b
, but that would make ->
have different priorities depending on context so it would arguably be best to require parentheses for multiple arguments.
When the function doesn't fit in one line I would prefer to use the Smalltalk-like variant. In that case having the argument type at the top of the block could actually improve readability, so I'm quite happy with your proposal.
I don't see myself using inline object parameters very often, but since they are never going to be one-liners I would always prefer the Smalltalk-like variant.
[@RossTate]
You can solve this by enabling type inference only in the cases where you know that you for certain that your type inference algorithm is decidable. The rules for enabling/disabling type inference should be much easier to understand than the inference algorithm, so there would be no problem adding them to the language spec. Since they are applied before the inference algorithm you can still without worrying about arcane corner cases. And it would still remove the type name from 99% of the code.
So, just to repeat in my own words, you're suggesting having rules identifying when there's enough context information to guarantee inference, and then only do inference when such a context is present. The problem with this, at least in the general case, is that whether there's enough context to do inference depends on what is being inferred. Because of this, for every context you give me, I can give you an inference problem which is still not solvable with that context but would be solvable with a more detailed context (admittedly, I haven't proved this, but I'm conjecturing here based on my experience with the problem).
Now, for the specific problem of inferring the parameter type for an anonymous method, we can give such a rule. It would be something like "The parameter's type does not need to be declared if the anonymous method is a direct argument to a non-generic method whose corresponding parameter type is some instantiation of Callable
." Although this is possible, my personal preference is just to be uniform rather than have such special casing. Nonetheless, it is an option.
[@danberindei]
Although this is possible, my personal preference is just to be uniform rather than have such special casing. Nonetheless, it is an option.
Note that I'm not suggesting creating a special rule for "parameter to an anonymous method", I'm considering parameter declaration to be the context and the rule to be:
"The parameter's type does not need to be declared if [it is a parameter of an anonymous method and] the anonymous method is a direct argument to a non-generic method whose corresponding parameter type is some instantiation of Callable."
How many different contexts are there where type inference is possible? A local, an attribute, a method parameter, a method return value, a type parameter, and that's about it.
These contexts already exists, and I believe type inference should be available in all of them (limited as it may be by context-specific rules).
[@gavinking] Something we need to decide as a community, and relatively soon, since this is one of the first things we need to implement after delivering M1 is the syntax for inline functions and, optionally, inline objects.
We already have this feature for named argument lists
Now, note that we already have one very nice way to do inline functions and objects:
And:
And for many usecases this syntax is really great. The question is if we need something for use with positional argument invocations.
Obvious extension to positional argument lists
Now, the most obviously natural thing would be to simply allow an ordinary
function
orobject
declaration, but without the name, for example:This is the option strongly favored by Stef, and I'm also very sympathetic to it because it's simple, straightforward, super-easy to parse and very regular with the syntax of the rest of the language. The verbosity can be at addressed by supporting an abbreviation in the case of a function which simply returns an expression. Either:
might work, or at worst:
It would also be possible to eliminate the
function
keyword:But now it's getting less regular, less explicit, and somewhat more difficult to parse.
The real doubt I have about this solution is it is simply not really a very good or natural or easy to format syntax for extending the language with new control structures, which is kinda the whole practical point of inline functions.
Smalltalk-style arguments
A different option we've discussed is to let you put inline functions outside the parens of a positional argument invocation, resulting in an invocation protocol that looks a lot like Smalltalk's.
Again, we can provide an abbreviation for inline functions that just return an expression:
I personally find that this results in something that is highly readable once you get used to it. But I can understand if many people find it a lot less familiar/natural.
So this issue is a request for general community feedback.
(This issue was previously discussed here.)
[Migrated from ceylon/ceylon-spec#54] [Closed at 2012-04-25 20:03:28]