-
Notifications
You must be signed in to change notification settings - Fork 62
Allow certain statements as expressions #3563
Comments
[@RossTate] Good ideas here, but let me remind you of one thing. If you turn comprehensions into expressions, then |
[@ikasiuk] I still really like the idea of generalizing comprehensions and turning them into expressions. It's so much more flexible and regular that way! But I prefer using Allowing statement blocks as expressions ( The most difficult problems are still the same as in #3483: how can comprehensions be used as sequenced arguments with a convenient syntax, and how do we solve the "nested fors" problem pointed out by Ross? It seems that these problems are related because the most promising approach for the "nested fors" is apparently to interpret the RHS of the So it looks like this issue somewhat depends on #3522, which redefines how sequenced parameters work. I must admit that I still haven't understood completely Gavin's latest proposal in #3522: there is apparently an important difference between |
[@gavinking] So a couple of problems with this:
Now, it's true that we could probably solve all three of these problems with some special "spread" operator, e.g.
But that to me significantly obfuscates our nice clean comprehensions syntax. And all you're doing is introducing a way to turn a comprehension into a not-expression. I would much prefer to leave |
[@gavinking] On the larger issue of "why not just make all control structures be expressions", this is something I have wrestled with many times over the last couple of years and always realized that, even though it's easy to say like that, and have people sorta know what you mean, to actually rigorously define the real semantics requires basically sitting down and defining a totally new language construct for each of the control structures, with a set of semantics and restrictions that are in fact totally different to the control structure whose name it shares. i.e you wind up not simplifying the language by making "everything an expression", on the contrary you wind up introducing a bunch of new kinds of expressions, while keeping all the complexity you already had in the definition of the control structures. This is complexity we simply don't need. And yes, I do know this is a superficialy appealing thing to do, and I also know that some other languages do it. But those languages don't usually have stuff like P.S. The only language I know of that makes this stuff really work convincingly is Smalltalk. But Smalltalk is a very different language syntactically. |
[@Zambonifofex] I really like this. But maybe we should use a different syntax: value foo = if(bar) => baz; // foo is of type "Baz?" I'm also thinking about a keyword to "return" an expression from inside block... Maybe
When a statement gives an expression, then it is said to also be an expression. Another cool idea is to allow these statements to give comprehensions. Here is an example: if(foo)
{
give for(value bar in baz) => bar.qux;
} Or, more shortly: if(foo) => for(value bar in baz) => bar.qux; In this case, the value foo = if(bar){baz.qux();} // Oh noes, compile error!!! Run!!1
value quux = {if(qux){quuux();}}; // Oh my gosh, not another one! Run again!!! Similarly, loops that use a block, and do not contain a for(value stream in [1..3, 4..6, 7..9]) => for(Integer i in stream) => i Would be the equivalent of writing: for(Integer i in 1..3) => i, for(Integer i in 4..6) => i, for(Integer i in 7..9) => i We already established that 1, 2, 3, 4, 5, 6, 7, 8, 9 We can then say that a comprehension that give a group of comprehensions G, is simply an comprehension that gives all the expressions in all the comprehensions in G. Now, if we want to have a comprehension that gives multiple statements that return a stream of Integers, we can simply do this: for(value stream in [1..3, 4..6, 7..9]) => {for(Integer i in stream) => i} The spread operator simply converts a stream into a comprehension. for(/***/)
{
while(/***/)
{
if(/***/)
{
/***/
^give foo; // The while loop gives foo.
/***/
give foo; // The if statement gives foo.
/***/
^^give foo; // The for loop gives foo.
/***/
}
}
} Similarly, you can exit of a control flow structure that isn't a expression nor a comprehension (does not contain a give statement) by using for(/***/)
{
while(/***/)
{
if(/***/)
{
/***/
^break; // Exits the while loop.
/***/
break; // Exits the if statement.
/***/
^^break; // Exists the for loop.
/***/
}
}
} Just for the record, here is how all the control flow structures would look using the arrow, in proposedly valid code: Foo? qux = if(bool) => foo;
Foo|Bar quux = if(bool) => foo;
else => bar;
{Foo*} qUx = {for(Foo foo in fooStream) => foo};
{Foo*} qUux = {while(bool) => foo};
Foo qUuux = try => baz();
catch(Foo e) => e;
Foo qUUx = switch(foo)
case(one) => foo;
case(two) => foo;
case(three) => foo;
else => foo;
Foo qUUux = try => foo;
finally => foo; // The finally statement may contain a block that may or may not give (all other blocks in control flow structures must either not give, or definitely give). If the finally gives, then the try/finally statement gives what is given by finally, otherwise, what is given by try. Unless the try block doesn't give, in which case, the finally block must either not give, or definitely give, and the try/finally statement gives what is given by the finally block, if it does.
Foo qUUuux = try => baz();
catch(Foo e) => foo;
finally => foo; // Works similar to the above. And as a bonus, I propose a do => foo.bar;
{Object*} bar = {do
{
value baz = //...
give for(/***/)=>//...
}}; If you put a comprehension in a sequence literal, for example, what it does is evaluate all the expressions the comprehension gives. If you put it into a stream literal, it does the same thing, but lazily. {Object*} stream = {do{give true, for(/***/)=>/***/, while(/***/)=>/***/, 1, foo, "Oatmeal?", "Are you crazy?"}} I've also organized my thoughts in a couple of simple logical rules:
The second to last one rule is pretty interesting, since it's the only one the compiler can't check for sure, since the amount of expressions a comprehension can give might change at the runtime. {Object*} stream = // ...
value v = for(value o in stream)=>o; // The compiler just assumes that loops aren't expressions, because for it, they aren't, since it can't know the stream size. It could treat them as an expression if stream where a tuple of size one, but it's better to just treat loops as if they are not expressions, in my opinion. If you know for sure the loop will only run once, then you don't even need a loop! Just one more thing: I'm kinda still a noob in computer science. I have been doing it only for a hobby for no more than four years, so I might have said some things that are no more than stupid. If that's the case, I'm really sorry, please feel free to correct me. I also never tried interacting with anyone on GitHub (this is my first post, yay o/), so hopefully I'm not doing anything terribly wrong. |
[@gavinking] @Zambonifofex Note that |
[@FroMage] As a follow-up to #3483 and our recent discussion I'd like to start this discussion in its own github issue so we can all keep track of it.
I'm proposing that we allow the following statements in expression contexts:
if (t) e
which evaluates toe
ift
is true, andnull
or possibly a singleton callednotThere
or something.if (t) e1 else e2
which evaluates toe1
ift
is true, ande2
otherwisewhile (t) e
which evaluates to anIterable
containing each value ofe
whilet
is true. To be useful in comprehensions, we would makewhile
ignore each value ofe
which is equal tonotThere
as returned byif
.for (i in v) e
which evaluates to anIterable
containing each value ofe
while iterating each element ofv
in a new bindingi
. To be useful in comprehensions, we would makefor
ignore each value ofe
which is equal tonotThere
as returned byif
.try e
which evaluates toe
try e catch (T x) ec
which evaluates toe
unless an exception of typeT
is throw in which case it evaluates toec
try e finally ef
which evaluates toe
try e catch (T x) ec finally ef
which evaluates toe
unless an exception of typeT
is throw in which case it evaluates toec
switch(e) case (c1) e1 ... case (cn) en default ed
evaluates to the firstei
clause whose testci
is true, and otherwise toed
Naturally I'd also allow each of these expressions to be blocks of statements if they end with an expression (but this part is optional because readability may be an issue):
{stmt...; e}
evaluates toe
That would go with
function
expression bodies too.That would imply changes wrt to comprehensions such as:
As opposed to the current:
But it would also allow things like:
So, it's a tradeoff because we're currently specified comprehensions as neither expressions nor statements to make certain special use cases easier. As a result comprehensions are just that: special. We can't refactor them around without special magic such as placing them in a sequence literal (eager) or passing them as parameter to the
elements
method which turns them into a lazyIterable
. Those restrictions are hard to understand, even if they are justified by readability.Turning comprehensions into expressions, along with a few other statements:
Iterable
easier,On the other hand, it also makes
Sequence
more verbose (but easy to understand why), and...
.Now, let's discuss that.
[Migrated from ceylon/ceylon-spec#457]
The text was updated successfully, but these errors were encountered: