Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

let clause in comprehensions #3483

Open
CeylonMigrationBot opened this issue Aug 3, 2012 · 74 comments
Open

let clause in comprehensions #3483

CeylonMigrationBot opened this issue Aug 3, 2012 · 74 comments

Comments

@CeylonMigrationBot
Copy link

[@chochos] Comprehensions are a really cool language feature and they would be even more useful if there was a way to declare and initialize values or variables that were internal to the comprehension. The keyword given can be used to enclose a declaration which can be used from that moment on:

given (i:=0) for (x in xs) something(x,i++)

for (x in xs) given (t=x*x+sqrt(x)+someOtherExpensiveCalculationWith(x)) for (y in ys) x%y

No need to state value or variable since it can be inferred from the declaration: = means value, := means variable. Local type inference is already done.

UPDATE: what we would really do, given all the evolution in the language since this was originally proposed, would be support the following:

let (c=Counter()) for (x in xs) something(x,c.next())

for (x in xs) let (t=x*x+sqrt(x)+someOtherExpensiveCalculationWith(x)) for (y in ys) x%y

[Migrated from ceylon/ceylon-spec#377]

@CeylonMigrationBot
Copy link
Author

[@ikasiuk] Nice idea! I have also noticed in the past that something like that could be useful, and we recently had problems in that direction:

ceylon/ceylon.language@e621030

@CeylonMigrationBot
Copy link
Author

[@RossTate] Snazzy! I think this in combination with the restriction I mentioned in that thread would be awesome.

@CeylonMigrationBot
Copy link
Author

[@gavinking] There's actually a second bit of this proposal that @chochos forgot to write down. The idea has two parts:

  1. allow given subclauses to declare locals, and
  2. allow expressions between subclauses.

The second bit would let you write:

given (i:=0) for (x in xs) i++ if (exists x) i->x

Which fixes what tripped us up in the definition of Iterable.indexed.

@CeylonMigrationBot
Copy link
Author

[@chochos] Right, forgot about expression between subclauses. Well that's the whole idea right there.

@CeylonMigrationBot
Copy link
Author

[@FroMage] First, I'd rather use the consecrated keyword for this and allow multiple vars to be defined:

let (i:=0;j=foo()) for (x in xs) something(x,i++)

With a semantics like letrec where each binding can see the other allowing you to define things like:

let (a:=() b();b=() a) for (x in xs) something(x,i++)

And I'd even make that available outside of comprehensions as an expression, and what the hell allow statements in it too:

value foo = let (a = f(); b = g()) { a * a + b * 1/b }

Mmm, that wouldn't be too useful if we could define inline statements like that:

value foo = { a = f(); b = g(); return a * a + b * 1/b; }

Which would be equivalent to:

// define a lambda that contains statements, which we call immediately
value foo = (function (){ a = f(); b = g(); return a * a + b * 1/b; })();

But that brings me to something that's been bothering me about comprehensions for a while:

for and if have completely different semantics depending on whether or not they are used as a statement or in a comprehension. I wouldn't see any problem if for and if could be used inside expressions, as their semantics would be very similar. But for an expression if we use the combination of then and else, which is already a bit irregular (test() then foo() else bar() could be written as if test() foo() else bar() or a variation with punctuation to make it more regular with the statement equivalent).

Now, I love comprehensions but I find it disconcerting and irregular that within them, the meanings of for and if are mapped not to their respective statements but to map and filter. I don't really have a better idea though, but I thought it'd be something worth noting, as regularity is something we care about.

Also withing a comprehension, the procedural rules stop applying: no need for brackets anymore, or semicolons, what looks like statements are expressions and they return values and the absence of else for an if has an entirely different meaning as we're suddenly building lists and mimicing the behaviour of map/filter (which again, is pretty cool). I have the feeling that comprehensions are a different language altogether with its own syntax. Adding new stuff to it, like local bindings makes it even more complex and even more alien to the rest of the language.

Don't take this as just me randomly bashing something: I love comprehensions and I think we should keep them and possibly extend them with local bindings, but I feel the syntax is too alien to the rest of Ceylon and I am afraid we're introducing a language-within-a-language syntax.

That statement in particular looks entirely like a different language:

given (i:=0) for (x in xs) i++ if (exists x) i->x

Would it make any sense in allowing for, while, if, switch and blocks in expression contexts and give them an intuitive meaning that would allow us to have something even more powerful than comprehensions?

Rewriting this statement with a more Ceylon-like syntax:

value foo = { variable i:=0; for (x in xs){ i++; if (exists x) i->x; }}

Which would even be valid when used as a statement:

variable i:=0; 
for (x in xs){ 
 i++; 
 if (exists x)  // well, if we allow single-statement ifs to drop braces ;)
  i->x; 
}

It's just an idea, but I have a feeling that would be more powerful, intuitive and regular.

What's the return type of an expresion block? Its last statement's return type, or that of its 'return' statements. Return type of a for or while expression is again its last statement. Return type of an if is union of last statement of then/else blocks.

Wouldn't something like that work?

@CeylonMigrationBot
Copy link
Author

[@gavinking] I'm fine with let here. Don't love the
defining-multiple-variables-in-one-let bit just because we don't let
you do that in other places.

I also agree that if we introduce this feature in comprehensions, then
we'll also need to explore where else in the language it would be
useful. For example, regularity suggests the following:

let (x = something()) {
    //x is defined just in this local scope
}

@CeylonMigrationBot
Copy link
Author

[@gavinking] > Now, I love comprehensions but I find it disconcerting and irregular that within them, the meanings of for and if are mapped not to their respective statements but to map and filter.

FTR, that's not actually how it works under the covers. The idea of mapping to map() and filter() had problems because we allow products.

Would it make any sense in allowing for, while, if, switch and blocks in expression contexts and give them an intuitive meaning that would allow us to have something even more powerful than comprehensions?

I spent a lot of time thinking about this, on several occasions, and just never came up with anything satisfying:

  • there are various reasonable things that a for expression could return, depending upon what you want to use it for (which is the reason why comprehensions are function arguments)
  • what the fuck should while evaluate to?!
  • if has a reasonable interpretation in an expression context, but if (foo) bar else baz is just visually horrible and so asymmetric—especially for something I use all_the_time.
  • switch is a very reasonable thing to support, but once you start having 3-branch conditionals inside an expression, the code can pretty quickly become difficult to read.
  • try works, but how often do you want to do expression handling inside an expression?

@CeylonMigrationBot
Copy link
Author

[@RossTate] > for and if have completely different semantics depending on whether or not they are used as a statement or in a comprehension.

I think of them as actually very similar. For example, if means "if this condition holds (at run time), then include this statement/expression here (at run time)". They get implemented slightly differently and they have slightly different syntax, but to me they are very much akin. (@gavinking, note that this interpretation gives a semantics for while in comprehensions.)

As for the curly braces, the sole purpose of curly braces is to disambiguate nesting. With comprehensions we have decided to optimize for what we expect to be the common case: everything nests. That is, everything after a for or an if is inside the for/if. This lets us and implies we should get rid of curly braces since there is no ambiguity. Of course there is a cost to this: we can't express comprehensions that require more complex nesting behavior. In particular, this includes things like else and switch.

I do find it weird that we get rid of semicolon after nested statements though. In fact, I'm a little worried that'll prevent us from using any syntax that relies on the presence of a semicolon in normal statements to disambiguate.

@CeylonMigrationBot
Copy link
Author

[@gavinking] I agree with @RossTate—I don't think it's fair to say they have different semantics at all.

@CeylonMigrationBot
Copy link
Author

[@quintesse] Hey, if we allow

let (x = something()) {
    //x is defined just in this local scope
}

why not go all Haskell-y and allow

{
    //x is defined just in this local scope
} given (x = something())

as well? :)

@CeylonMigrationBot
Copy link
Author

[@gavinking] So the tangential discussion on this thread fits very much with the tangential discussion on #3469. Ceylon today, like most mainstream languages, is "statement-oriented" and optimized for methods with multiple statements. Now I personally just love to write methods/getters with just single-expressions wherever that is reasonable, and for that reason I kinda like languages which are more "expression-oriented" in their syntax. Which is why we have stuff like comprehensions and then/else, I guess.

Up until now, I have not been that keen on introducing something like:

function name() => person.name.first + " " + person.name.last;

because once you try to refactor out common sub-expressions, you need a big change to the syntax (sure, the IDE can do this for you, but still). So it's not a syntax that "scales". It's an abbreviation that winds up getting in the way when you start maintaining the code.

On the other hand, let in expressions didn't make much sense either, since you could just refactor out the common subexpression to a statement:

function name() {
    Name n = person.name;
    return n.first + " " + n.last;
}

However, if we have both these features, then we have something that makes sense. It becomes possible to write the above in the following "expression-oriented" form:

function name() => let (n = person.name) n.first + " " + n.last;

So then we need to answer the question: is this an improvement over the "statement-oriented" form, and is having two ways to write the same thing a good or a bad thing in this case. I'm interested to know what you guys think.

I personally find the definition with => and let rather nice to look at. But it's hard to argue that it's objectively better when it involves really about the same number of tokens. Still, even if counting tokens can't explain my like for it, perhaps there's some other explanation...

@CeylonMigrationBot
Copy link
Author

[@gavinking] @quintesse FTR, I actually prefer the keyword given in both locations, for how it reads in English. I'm not especially keen on the postfix given because it's an irregular syntax compared to everything else we have in the language.

@CeylonMigrationBot
Copy link
Author

[@quintesse] Sure, I wasn't actually being serious, especially because in Haskell it's used to even define localized functions where it makes more sense to put them after the "important" work.

PS I'm not sure how I feel about the short-cut syntax yet, but it sure makes me want the other Haskell syntax of being able to do:

Integer fib(0) => 0;
Integer fib(1) => 1;
Integer fib(n) => f(n-1) + f(n-2);

hehe

@CeylonMigrationBot
Copy link
Author

[@FroMage] > what the fuck should while evaluate to?!

An iterator.

if has a reasonable interpretation in an expression context, but if (foo) bar else baz is just visually horrible.

If punctuation for expressions is optional (like for curly braces, then surely for single braces too: if foo else bar

I don't think it's fair to say they have different semantics at all.

Perhaps you're right, but it's still a language-within a language. Again, perhaps that's not an issue at all, but I wonder if it really isn't, and if we can't do something to unify both.

@CeylonMigrationBot
Copy link
Author

[@FroMage] I think the syntax you are pointing to depends on your background. For me it's C, so even in languages like Scheme or JavaScript where both alternatives are possible I find myself writing:

function name(){
  var n = person.name;
  return n.first + " " + n.last;
}

Rather than:

var name = function(){
  var n = person.name;
  return n.first + " " + n.last;
}

Same in Scheme with:

(define (name) 
 (let ((n person.name)) 
  (append n.first " " n.last)))

rather than:

(define name (lambda ()
 (let ((n person.name)) 
  (append n.first " " n.last))))

Personally I really have trouble making any sense of function name() => person.name.first + " " + person.name.last; but just turning it into function name() { return person.name.first + " " + person.name.last;} makes it readable for me. But I really think it's a background thing, more about habits than quantifiable things.

@CeylonMigrationBot
Copy link
Author

[@gavinking] > If punctuation for expressions is optional (like for curly braces, then surely for single braces too: if foo else bar

Eh? I can't even hazard a guess what if foo else bar might mean... I assume that's a typo?

You're going to need some kind of punctuation to separate the condition from the first expression. So you have a choice between if (foo) bar else baz which is just awfully asymmetric, and if foo then bar else baz, which is already different to the traditional C syntax, or you could go back to something more like C's ternary operator syntax, foo ? bar:baz or if (foo) bar:baz. Personally, I think then and else are superior to any of these options.

@CeylonMigrationBot
Copy link
Author

[@FroMage] > Eh? I can't even hazard a guess what if foo else bar might mean... I assume that's a typo?

Hah, yes of course, sorry: if foo bar else gee

@CeylonMigrationBot
Copy link
Author

[@gavinking] @FroMage Again FTR, I have a rather extreme aversion to:

value sqr = (Float x) x*y;

To the extent that I think it's almost unfortunate that Ceylon lets you write this. The fact that this even works is more of an unintended consequence—the intersection of two language features that aren't really designed for use together—than an intentional feature. I suppose that everyone here would agree that what we actually want people to write in this case is just:

function sqr(Float x) { return x*y; }

So, indeed, this might be the strongest argument yet for supporting the form:

function sqr(Float x) => x*y;

i.e. if we don't support that Dart/Coffeescript-style arrow syntax, then some people are going to want to use the "value = anon function" form like you quite often see them do in the Scala community. (Apparently in the world of Scala, this idiom is not frowned upon.)

@CeylonMigrationBot
Copy link
Author

[@FroMage] And function f() => e; would just be a parser alias for function f() { return e; } ?

@CeylonMigrationBot
Copy link
Author

[@gavinking] @FroMage Right. I guess it's something I could implement in the parser in like 5-10 mins, though I might not do it exactly like that. Supporting let would be a little more work, and would impact the backend, but it's still pretty straightforward, I suppose. (Though I have not put a huge amount of though into it.)

@CeylonMigrationBot
Copy link
Author

[@ikasiuk] > So then we need to answer the question: is this an improvement over the "statement-oriented" form, and is having two ways to write the same thing a good or a bad thing in this case. I'm interested to know what you guys think.

My opinion: it looks kind of nice, but not soo much nicer than the current syntax. And having two ways to write the same thing as a bad thing in this case.

An important goal of Ceylon, according to the home page, is readability. If I interpret this goal correctly then it should lead us to a syntax that is as homogeneous, regular and easy to learn as possible. I think if we take that goal seriously then we can only introduce a new construct into the language if it solves a real, significant new problem. And I just don't see a sufficient benefit in this case.

i.e. if we don't support that Dart/Coffeescript-style arrow syntax, then some people are going to want to use the "value = anon function" form like you quite often see them do in the Scala community. (Apparently in the world of Scala, this idiom is not frowned upon.)

So we have two ways of expressing the same thing but one of them is not so nice. So we introduce a third possibility to lure people away from the one we want to hide ;-)
Ok, I understand the reasoning and I also don't really like the current syntactic duality of functions. It would be great if we could find a way to improve that. But I don't think that this is the right way.

@CeylonMigrationBot
Copy link
Author

[@gavinking] @ikasiuk I agree with all your points, but note that there are a couple of different factors pushing in this direction:

  1. the irregularity of the scope of a type parameter in a type alias definition, as discussed in defaulted type parameters #3469,
  2. the fact that function sqr(Float x) { return x*x; } is a little heavy on the eyes compared to some other recent languages like coffescript and dart, and
  3. the fear that, in response to 2, some people will be motivated to start writing value sqr = (Float x) x*x;

I don't think that 2+3 on their own would be enough to motivate me to want to make this change. But if other considerations are "pushing" in the same direction—that is, if it just feels like the language naturally want to grow in that direction—well, then that's a different matter...

But I don't think that this is the right way.

Can you think of a different syntax for partial application of a function / generic type? If we came up with something natural, then that might be a different approach that would give many of the same advantages. The trouble is, when I think about that problem, I start coming up with stuff like:

function sqr(Float x) = Integer.power(*)(2);

which I think is just not at all ceylonic (it's actually just Scala's horrible underscore in disguise), and is actually much less flexible than the fat arrow.

@CeylonMigrationBot
Copy link
Author

[@gavinking] > I start coming up with stuff like:

function sqr(Float x) = Integer.power(*)(2);

which I think is just not at all ceylonic.

Note that according to the language spec as it exists today, I should be able to write a general-purpose function named shuffle() that would let me define sqr() like this:

function sqr(Float x) = shuffle(Integer.power)(2);

i.e. shuffle() swaps the first and second parameter lists of the higher-order function Integer.power, letting you apply the second argument list (the method arguments) before the first (the method receiver).

Now, that's all very cool and powerful but I think it's exactly the kind of thing we don't want people to be tempted to use in "everyday" code.

@CeylonMigrationBot
Copy link
Author

[@gavinking] > Hah, yes of course, sorry: if foo bar else gee

This can't be parsed, consider stuff like:

if x + y + z ...
if x { y } ...

We can't tell where the condition ends, at least not with finite lookahead in a CFG.

@CeylonMigrationBot
Copy link
Author

[@ikasiuk] Two remarks: I prefer given over let, and I don't share the aversion against value f=(Float x)x*y; (although I personally prefer the alternative).

And one question (I probably just missed that): Why do we need to introduce a new symbol => instead of just using =?

@CeylonMigrationBot
Copy link
Author

[@FroMage] > And one question (I probably just missed that): Why do we need to introduce a new symbol => instead of just using =?

The explanation is in #3469, which is actually where most of the explanation for the => is.

Note that let is the keyword used in JavaScript as well, so it's well-known.

@CeylonMigrationBot
Copy link
Author

[@ikasiuk] > Note that let is the keyword used in JavaScript as well, so it's well-known.

let in JavaScript is apparently not widely supported, so "well-known" might be exaggerated. And in general it seems that in Ceylon we tend to prefer the keyword that fits best, rather than what is most popular in other languages.

The explanation is in #3469, which is actually where most of the explanation for the => is.

Ah ok, thanks. I agree that => makes sense for type aliases because they are too different from simple assignment. So => would basically mean "alias". But I'm still not sure about using it for functions. Assuming that we introduce => for types and functions, how exactly would it work?

  • A class name represents both a type and a function. So could we create a type alias and constructor alias at the same time by writing something like class B<X>(X x)=>A<X,Y>(x,2);?
  • I guess it would also work for void functions, right? (void f()=>g(3);)
  • What about getters and setters, would this be allowed:
Float x => retrieveX()
assign x => storeX(x);
  • Would this completely replace the function f(...)=g(...) form, or are there cases where that would also still be allowed?

@CeylonMigrationBot
Copy link
Author

[@ikasiuk] > No need to state value or variable since it can be inferred from the declaration: = means value, := means variable.

That reminds me that the variable and := in something like

variable Integer i := 0;

always feel somewhat redundant. Could we say that an attribute that is initialized with := is always automatically variable even if that annotation is omitted? Maybe we could use the same rule as for type inference and allow that only for non-shared attributes.

@CeylonMigrationBot
Copy link
Author

[@chochos] I talked about that with @gavinking once. I think the idea is that if you must be sure that you want mutability, that's why you have to type that long keyword even if it seems redundant. That's the part about encouraging immutability - if we only leave := then it's not so clear that you are declaring a variable and not an immutable value.

@CeylonMigrationBot
Copy link
Author

[@ikasiuk] > I talked about that with @gavinking once. I think the idea is that if you must be sure that you want mutability, that's why you have to type that long keyword even if it seems redundant. That's the part about encouraging immutability - if we only leave := then it's not so clear that you are declaring a variable and not an immutable value.

I agree for shared attributes: the interface of a type shouldn't be defined by such implicit mechanisms. But I don't see a problem with non-shared attributes. It's rather unlikely that you erroneously write := instead of = and then actually produce a situation where that causes any harm. After all, we also allow type inference although we can't check if the inferred type is really the one intended by the user.

@CeylonMigrationBot
Copy link
Author

[@ikasiuk] I'll try to explain the reasoning behind the proposed syntax. The goals are:

  • Sequenced arguments for a function f(Object...) should not be more complicated than just writing f(1, 2, 3),
  • On the other hand there should be a similarly convenient way to use comprehensions for sequenced parameters, like f { for(i in 1..3) i*i },
  • The same must be possible for sequence literals (i.e. using a list of values as well as using a comprehension) in a syntactically consistent way.

The named argument syntax plays an important role in this context. A named argument list is currently split into two parts in the following way:

f {
    <named arguments>
    <sequenced arguments>
};

The separation between the two parts is given only by the difference in the syntax of their respective elements. I must admit that I always found that a bit confusing, and it turns out that one way to reach the goals is actually to make the separation more explicit:

f {
    <named arguments>
    seqParam = [ <sequenced arguments> ]
};

where [1, 2, 3] is a sequence literal, see below. Allowing to omit the seqParam= gives us

f {
    <named arguments>
    [ <sequenced arguments> ]
};

and consequentially

f {
    <named arguments>
    <iterable>
};

so that we can automatically write the desired f { for(i in 1..3) i*i }. Now we just have to make sure that the syntax for sequence literals is symmetric to that of parameter lists:

f( <comma-separated list> );
f { <iterable> };

value seq1 = [ <comma-separated list> ];
value seq2 = { <iterable> };

so that we can write:

f(1, 2, 3);
f { for(i in 1..3) i*i };

value seq1 = [1, 2, 3];
value seq2 = { for(i in 1..3) i*i };

Note that [1, 2, 3] replaces the current {1, 2, 3} to avoid ambiguity. I actually find it quite fitting.

That's basically how I arrived at that syntax. @gavinking, you asked a question concerning sequence literals which I didn't quite understand. Does that clarify it a bit?

Well, it certainly has advantages from our point of view, but for the usecase of declaring a user interface, or a build script, or whatever, it seems much more awkward to me.

I guess any more explicit separation between the named argument part and the sequenced argument part of a named argument list could achieve the same effect. But neither do I have a better idea at the moment, nor am I conviced yet that this is really a problem. Can you give a code example of where you would find this awkward?

I always considered it a design goal that named argument lists would look visually consistent with statement blocks. I'm definitely not sold on the idea that this was a bad idea.

It's surely possible not to use commas and use semicolons instead, as with the current syntax. But it looks like I must make a confession: Since I've first seen it I've always found the named argument syntax in its current form the only part of the Ceylon syntax that feels somewhat awkward and unintuitive. The idea as such is absolutely great but I find it surprisingly hard to read and write Ceylon code that uses named arguments in a non-trivial way.

The problem is that when I look at such a piece of code then the structure is not obvious: is this part a normal code block or a named argument list? What function does that line belong to? What is this function nested in? Why is this } followed by a comma but not that one over there?

That makes me whish that parts of the code that serve a different purpose also look different and are clearly separated from each other - that the syntax helps me to intuitively understand the structure of the program. Of course that's only my personal impression and so perhaps it's just my fault that I don't "get it".

@CeylonMigrationBot
Copy link
Author

[@gavinking] > It's surely possible not to use commas and use semicolons instead, as with the current syntax. But it looks like I must make a confession: Since I've first seen it I've always found the named argument syntax in its current form the only part of the Ceylon syntax that feels somewhat awkward and unintuitive. The idea as such is absolutely great but I find it surprisingly hard to read and write Ceylon code that uses named arguments in a non-trivial way.

The problem is that when I look at such a piece of code then the structure is not obvious: is this part a normal code block or a named argument list? What function does that line belong to? What is this function nested in? Why is this } followed by a comma but not that one over there?

Well, I certainly don't love the commas in the sequenced parameter list either. But I'm not sure that your solution really improves the situation very much. You swap:

Html { Head { title="title"; }, Body { ... } }

For

Html { [ Head { title="title"; }, Body { ... } ] }

It's like the same shit just inside a rectange, no?

@CeylonMigrationBot
Copy link
Author

[@ikasiuk] Oh, I don't mind the commas, I just think the syntax could be a bit more consistent in this respect: that's why I suggested to simply use commas everywhere in argument lists. But we are wandering off the subject a bit. We were trying to make sequenced parameters and generalized comprehensions work together properly. And I think I finally found a good solution to that:

Let's go back to the Sequenced type, with the following definition:

shared abstract class Void() of Object|Sequenced<Void>|Nothing {}
shared class Sequenced<out Element>(elements) extends Void() {
    shared Iterable<Element> elements;
}
shared interface Iterable<out Element> given Element satisfies Object? {...}

This could be used with the following rules:

  • The static type of an argument x to a sequenced parameter p must be assignable either to Object? or to Sequenced<Void>.
  • If x is Sequenced then p=x.elements else p={x}.
  • x... means Sequenced(x) for any Iterable x .
  • The for operator returns a Sequenced.
  • The RHS of the for operator behaves like a sequenced parameter (i.e. each iteration can contribute several values if the RHS is a Sequenced).

Thanks to the first rule it can always be decided at compile time whether the argument is a sequenced argument. It might even be a good idea to extend the first rule to "The static type of any argument x must be assignable either to Object? or to Sequenced<Void>" to avoid surprises with values of type Void.

With these rules the following examples all work as expected:

void f(Object... values) {}
void g(Object obj, Object... values) {}
f(1, 2, 3);
f(myIterable...);
f(for(i in 1..3) i*i);
f { 1, 2, 3 };
f { for(i in 1..3) i*i };
g(0, 1, 2);
g(0, for(i in 1..3) i*i);
g { obj=0; 1, 2 };
g { obj=0; for(i in 1..3) i*i };

Integer[] seq1 = { 1, 2, 3 };
Integer[] seq2 = { for(i in 1..3) i*i };
Iterable<Integer> it = elements { for(i in 1..3) i*i };

value s1 = { for(i in 1..3) i*i }; // {1, 4, 9}
value s2 = { for(i in 1..2) for(j in 1..2) i*10+j }; // {11, 12, 21, 22}
value s3 = { for(i in 1..2) { for(j in 1..2) i*10+j } }; // {{11, 12}, {21, 22}}

Sequenced values can also be used directly:

Sequenced<Integer> x = for(i in 1..3) i*i;
f(x); // sequenced argument
g(0, x); // sequenced argument
print({x});
Iterable<Integer> it = x.elements;

But f(x) is not allowed if x is of type Void. In that case the type has to be narrowed first.

An interesting possible extension is to apply the p=x.elements conversion not just to sequenced arguments but to any argument with type Iterable:

void h(Iterable<Integer> ints, String... strs) {}
h(for(i in 1..3) i*i, "cat", "dog");
h {
    ints = for(i in 1..3) i*i;
    strs = for(i in 1..3) "nr "i"";
};

This is always unambiguous because of the first rule and the definition of Iterable.

@CeylonMigrationBot
Copy link
Author

[@RossTate] Here's something that doesn't work with that:

Iterable<B> map<A,B>(B to(A a))(Iterable<A> as) {
  return {for (a in as) to(a)};
}

It doesn't even type check according to your proposal.

Note that this also doesn't work in any proposal saying that for (...) e should skip e if e is null. With those it'll type check, but it won't do what people want it to do.

@CeylonMigrationBot
Copy link
Author

[@ikasiuk] > It doesn't even type check according to your proposal.

You mean because of the constraint of the type parameter of Iterable? Well, that can easily be solved with a given clause. It's not great that it's necessary to specify a type constraint, but that's not a new problem: it's not uncommon that you have to specify something like given T satisfies Object to satisfy the type constraints of a parameter or return type.

Note that this also doesn't work in any proposal saying that for (...) e should skip e if e is null. With those it'll type check, but it won't do what people want it to do.

That depends on how the behavior of that map function is defined. It's true that it skips resulting null values if it is implemented with a comprehension. But that's not necessarily wrong. I think you'd have three options: change the return type to Iterable<B&Object>, restrict type parameter B to Object or choose a different, null-preserving implementation.

@CeylonMigrationBot
Copy link
Author

[@RossTate] So say I'm a programmer who has heard about Ceylon and particularly that it has cool features like generics, first-class functions, and list comprehensions. map might be one of the first programs I'd write to play with these features. I have a Java, C#, Scala, C++, ML, or Haskell background I would most likely try implementing map with the above code. Your two solutions mean either I would immediately be faced with the subtleties of Ceylon's type system (i.e. "Why do I have to explicitly declare a type parameter is a subtype of Object?? Isn't that obvious?!") or I would unknowingly be writing code that doesn't actually work the way I want it to. The latter is especially frightening, and gets worse when you consider that fact that even once I noticed it doesn't work correctly I would have absolutely no idea why since there's nothing in the code at all that reveals this behavior with nulls (even ones not visible in the code's types). So, if Ceylon is supposed to be easy to transfer to, this certainly seems contradictory to that goal, which is why I'm pushing for a solution where I can write map as above and it works how programmers already familiar with map from some other language would expect.

@CeylonMigrationBot
Copy link
Author

[@ikasiuk] > [...] either I would immediately be faced with the subtleties of Ceylon's type system (i.e. "Why do I have to explicitly declare a type parameter is a subtype of Object?? Isn't that obvious?!") [...]

Yes. But as I said: that's a separate problem which is not specific to this particular situation. It's something that can generally occur in Ceylon wherever you use generic types.

[...] or I would unknowingly be writing code that doesn't actually work the way I want it to. The latter is especially frightening, and gets worse when you consider that fact that even once I noticed it doesn't work correctly I would have absolutely no idea why since there's nothing in the code at all that reveals this behavior with nulls (even ones not visible in the code's types).

Not sure if I agree completely, but maybe you are right. The solution is obvious: the RHS type of the for operator must be assignable to Object. I guess that's a reasonable restriction. In your example that would force you to either restrict B to type Object or to insert a check if (exists b=to(a)).

@CeylonMigrationBot
Copy link
Author

[@RossTate] Or make it so that type variables by default can only represent class types.

@gavinking
Copy link
Contributor

@tombentley let me know when you're ready to start implementing let clauses in comprehensions.

@gavinking gavinking changed the title let expressions in comprehensions let clause in comprehensions Mar 3, 2016
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants