#432 Statements as Expressions

brian Sat 17 Jan 2009

I'm interested in opinions on making Fan statements evaluate to a value?

// if/switch
r := if (x < y) x else y
r := switch (x) { case 0: x; case 1: x*2; case 2: x*3 }

// methods/function return last statement
Int add(Int a, Int b) { a + b }
list.join(",") |Obj x->Str| { x.toStr }

Not sure how try, while, and for fit into that.

Some of the Java closure proposals were making a distinction b/w local and non-local returns this way - not sure if that fits into it.

JohnDG Sat 17 Jan 2009

Excellent idea! Gives Fan a much more functional feel (& saves typing). As for while et al, the value should be the value of the last statement, which might of course be void.

cgrinds Sat 17 Jan 2009

Coming from Smalltalk I'm +1

JohnDG Sun 18 Jan 2009

Random ideas for loops:

lastValue := while (i > 0) {
   --i;
}

// lastValue == 0
val := if (2 + 2 == 4) { tmp := 5; tmp * 3; } // Illegal!!!

val := if (2 + 2 == 4) { tmp := 5; tmp * 3; } else 0 // OK
i := 10

// Illegal!!!
val := while (--i > 0) {
   if (i % 2 == 0) break; 
}

// Illegal!!!
val := while (--i > 0) {
   if (i % 2 == 0) break 4;
}

// Illegal!!!
val := while (--i > 0) {
   if (i % 2 == 0) break 4;
   else break 2;
}

// OK
val := while (--i > 0) {
   if (i % 2 == 0) break 4;
   else break 2;

   1;
}

Basically you'd have to match the work of function return values in ensuring that every branch out of a loop has a sensible value, whenever the loop is used as an expression.

helium Sun 18 Jan 2009

try is pretty obvious, isn't it?

For loops you have several options.

  • A loop allways has type Void.
  • The value of the last iteration is returned.
  • It acts like a list comprehension:

    foo := for (i = 0; i < 5; ++i) i // foo == 0, 1, 2, 3, 4

BTW, I wasn't able to write the list in the comment surrounded by brackets, as I get

You have errors in your fandoc:

  • Invalid annotation ...

brian Sun 18 Jan 2009

I am most interested in getting rid of the return for simple methods and functions, I prefer that style. Although you can't really do just that without tackling how all the statements work.

  1. If we do if/else expressions, should we get rid of ?:
  2. I'm inclined to say for and while are last result of comparison (which is actually the last thing on the stack); but then break is confusing - not sure I want to go down a path with complexity. So maybe just Void for now.
  3. I agree that try is obvious, that will actually let you do things like:
    val := try doSomething catch defVal
  4. I'm inclined to let you use if without else, and the else defaults to null. I think that would solve something I do all the time which is leave a value null unless I can operate on it:
    cs := if (cond) doSomething

jodastephen Sun 18 Jan 2009

If we do if/else expressions, should we get rid of ?:

Maybe, but ?: is commonly used, and a dedicated operator in its own right (for defaulting nulls)

for and while

These aren't naturally expressions. Unless you start down the route of integrating them with list comprehensions...

Some thoughts - maybe a for loop is simply a way of defining an iterator on which the list comprehension operates, thus the type of the for loop is the iterator (helium's idea). This might need a yield keyword:

iterator := for (i = 0; i < 5; i++) if (obj.matches(i)) yield i
anotherIterator := for (i : iterator) if (i.even) yield i

However, we should ask if methods and closures are a better solution to these tasks. I believe that it was Python that added list comprehensions and got some feedback that they were preferable to closures for some tasks.

something I do all the time which is leave a value null unless I can operate on it

Isn't this better written as:

cs = cs?.doSomething()

BTW, I'm opposed to this kind of thing in Java, but that is mainly because last-line-no-semicolon is very alien to the Java style, and has lots of side effects. Here in Fan, with no semicolons anyway, it all looks OK, and brings Fan closer to the new mainstream.

JohnDG Sun 18 Jan 2009

maybe a for loop is simply a way of defining an iterator

This is an interesting idea, but it violates consistency: if there is a LHS, then the for loop is iterated; otherwise, an iterator is created. Very confusing, inconsistent, and fragile.

However, we should ask if methods and closures are a better solution to these tasks.

Generators cannot be replaced by methods & closures (consider an iterator for a tree). But generators are a separate subject from the value of loop expressions, at least if the consistency argument above has any merit.

As for :? and ?., I think they should be kept. They're much clearer than if.

jodastephen Sun 18 Jan 2009

This is an interesting idea, but it violates consistency: if there is a LHS, then the for loop is iterated; otherwise, an iterator is created. Very confusing, inconsistent, and fragile.

I may not be making myself clear. I'm using the yield for clarity:

for (i = 0; i < 5; i++) {
  if (obj.matches(i)) {
    yield doStuff();
  }
}

In this example, above, the yield does nothing - there is nowhere to yield to.

iterator := for (i = 0; i < 5; i++) {
  if (obj.matches(i)) {
    yield doStuff(i);
  }
}

In this example, above, the yield is causing the iterator to be built up.

iterator := for (i = 0; i < 5; i++) if (obj.matches(i)) yield doStuff()

Finally, we've just removed the curly brackets.

I suspect I'm using yield contrary to some other languages here...

brian Mon 19 Jan 2009

Here is the existing grammar for statements:

<stmt> :=  <break> | <continue> | <for> | <if> | <return> | <switch> |
           <throw> | <while> | <try> | <exprStmt> | <localDef>

It is a little ugly because most statements don't work well as expressions. Sure if, switch, and try would be cool to use as expressions, but it seems a little inconsistent to have 3 as expressions and the rest not.

What I really want to do is use a single expression as the return of a method or closures, especially for one liners. I actually think we can add that feature separately without tackling statements as expressions. Suppose we just say that a return is implied if the last statement of a method or closure is an <exprStmt>.

// old way
Str fullName() { return "$first $last" }
files.sort |File a, File b->Int| { return a.name <=> b.name }

// new way
Str fullName() { "$first $last" }
files.sort |File a, File b->Int| { a.name <=> b.name }

With my proposed simpler change, if the last statement wasn't a simple expression, then you'd still need return.

What do you guys think? Go simple for now? Or try and tackle making Fan an expression based language full on?

andy Mon 19 Jan 2009

I've always been against "implied" returns. Yes it makes the simple case a bit cleaner, but I think overall it hurts code readability.

JohnDG Mon 19 Jan 2009

What do you guys think? Go simple for now? Or try and tackle making Fan an expression based language full on?

By far, I prefer declarative programming, a style already reflected in Fan. However, I think we need time to think about the use cases for more complex statements (functional languages don't have loops so don't have to answer that question).

It's probably safe to implement support for the simple statements now and defer until later on the more complex statements.

In particular, I've been thinking about a common pattern I see in my code:

var := defaultValue

for (...) {
   if (...) var = X;
   else ...
}

This pattern crops up a lot. I'm wondering if there's a way to handle it and its kin via expressions.

I've always been against "implied" returns. Yes it makes the simple case a bit cleaner, but I think overall it hurts code readability.

Those of us with functional/Smalltalk backgrounds think it greatly improves readability and encourages declarative programming. Which means it's mostly a matter of taste. :)

f00biebletch Mon 19 Jan 2009

Those of us with functional/Smalltalk backgrounds think it greatly improves readability and encourages declarative programming. Which means it's mostly a matter of taste. :)

Having just done my first lists:map in Fan, I too would beg for removal of the return statement. There's times it is clearer to explicitly state the return, but having to makes a map/fold/reduce feel odd.

mikaelgrev Mon 19 Jan 2009

My 2 cents..

I think the "missing return" is a very valuable compiler error in Java. I know that I get it every now and again (OK, as an IDEA error, but anyway)..

But, if you have a method that returns Object, then you can as far as I understand never get this compiler error since every statement, including that last one, will return something compatible with Object. You can quite nasty bugs because of this IMO and it is not worth it given that typing return takes 0.5s.

I think return should be mandatory for at least methods.

Cheers, Mikael Grev

freddy33 Mon 19 Jan 2009

+1 to keep the returns Frankly, if I want static typing I want the compiler to help me, and returns does help me. The bold :) "return" keyword makes the function flow more readable. If I want dynamic language, implicit returns are just natural.

brian Tue 20 Jan 2009

Interesting feedback on both sides of the fence. I myself felt requiring the return keyword was worth the extra clutter when I was designing the grammar three years ago. However since then, I think idiomatic Fan would read better using the more terse notation without return, especially those extremely common one line closures. Another option would be have an expression based closure like C#'s lambdas - but not sure I like that inconsistency.

I don't think we have to rush this, it seems to me we can add it whenever we like without breaking backward compatibility. Changing the AST to have Stmts be Expr would be a pretty big breaking change, but only for compiler plugins.

helium Tue 20 Jan 2009

Frankly, if I want static typing I want the compiler to help me, and returns does help me. The bold :) "return" keyword makes the function flow more readable. If I want dynamic language, implicit returns are just natural.

I can't folow this reasoning.

freddy33 Tue 20 Jan 2009

Just from my experience, writing return keyword in groovy feels odd. Especially in closures, since the function type is optional:

list.each { it.isForMe() ?it.val:it.noVal }

But not having return when you are forced to clearly declared you function (or function types) feels odd in the opposite (example from above that is not Fan today):

files.sort |File a, File b->Int| { a.name <=> b.name }

I mean you already need (and I like/want it) to write "->Int", so there is "return i" that match it. So I prefer:

files.sort |File a, File b->Int| { return a.name <=> b.name }

My personal feeling on this.

alexlamsl Tue 20 Jan 2009

Omission of return is ok as far as one-liner-closures, but if you start to have more lines than that I'd rather see it there as explicit flow indicator.

I can see the potential danger of losing a line of code at the end of the closure block / method, yet the code magically compiles and takes me an hour to figure out why I have a ClassCastException.

JohnDG Tue 20 Jan 2009

But, if you have a method that returns Object,

If you write a method in a statically typed language like Fan that returns Object, then quite frankly, you get what you deserve. :-)

Moreover, it's worth pointing out that omitting return and treating statements as expressions are orthogonal (but related, in that omitting return becomes even more useful when statements are expressions).

For example, the code a.name <=> b.name is not a statement: it's an expression. If you use it at the end of a method, it's pretty clear you want to return the value of that expression. No ambiguity there, in my opinion.

Therefore, it's theoretically possible to allow implicit returns with expressions, but forbid it with statements (not that I would be in favor of doing such a thing).

Omission of return is ok as far as one-liner-closures, but if you start to have more lines than that I'd rather see it there as explicit flow indicator.

No one would force you to abandon return. As far as I can see, using return would always be an option, even in short, one-line closures.

I can see the potential danger of losing a line of code at the end of the closure block / method, yet the code magically compiles and takes me an hour to figure out why I have a ClassCastException.

Only going to happen if the return value from your method is Object (and even then, only going to stump you for more than 10 seconds if you code without automated tests). Otherwise, the compiler will complain if that line at the end yields a type incompatible with the return value of the method (such that it would cause a ClassCastException).

In languages that don't require an explicit return, the developers who use that language never complain. In my view, that indicates that whatever your objections are going into such a language, you'll eventually find the construct isn't the weapon of mass destruction some seem to be suggesting here, and in fact makes life a lot simpler in many cases.

JohnDG Tue 20 Jan 2009

Another option would be have an expression based closure like C#'s lambdas - but not sure I like that inconsistency.

I don't like that either. C# is increasingly showing these kinds of inconsistencies, given the huge number of features added in a way so as not to break backward compatibility with the original, Java-derived grammar.

In any case, like you say, no need to rush this issue, since it's not one that would affect backward compatibility.

In order to do a good job, I think it's necessary to figure out a good answer to the loop question.

In a simple loop, the value of the expression might be the value of the last statement:

Float pow(Float base, Int exp) {
   val := 1.0

   for (i := 0; i < exp; i++) val *= base
}

Or more verbosely:

Float pow(Float base, Int exp) {
   val := 1.0;

   result := for (i := 0; i < exp; i++) {
      val *= base
   }

   return result
}

When there are exits from the loop, however, the situation is not so clear. It's not obvious that choosing the value immediately prior to the break is a useful thing to do, perhaps because we're missing use cases for more complex loops.

There's the idea of extending break to allow an argument; e.g. break 5. This seems like it would be useful in some of the code I write, as a way to get a value back to the LHS of the loop assignment (more or less what return does in a closure). If this ended up seeming like a good direction to go in, then for consistency, it seems like closures should use break and not return (saving return in closures, possibly, for some day in the future when and if non-local returns are added to the language). In any case, you need to decide what happens if a user just uses break -- is the value null?

brian Tue 20 Jan 2009

this ended up seeming like a good direction to go in, then for consistency, it seems like closures should use break and not return (saving return in closures, possibly, for some day in the future when and if non-local returns are added to the language).

I think this is a good point John, and it got me thinking back about many of our previous discussions for using {...} as a closure such that we could use closures to define new "control blocks".

The trouble was always that we needed return to work consistently b/w a closure and in built-in control structures like if, while and for. I think we talked about using break instead of return inside closures for this purposes, which originally I didn't like. But when combined with the ability to omit return/'break' that actually becomes a very good solution. I think that might be the finally piece of the entire puzzle (at least for me).

I will consolidate all of this into a concrete proposal in a separate discussion.

JohnDG Tue 20 Jan 2009

But when combined with the ability to omit return/'break' that actually becomes a very good solution. I think that might be the finally piece of the entire puzzle (at least for me).

I agree, using break for closures forces consistency, while at the same time not straining natural coding style, thanks to the ability to omit the break keyword.

Also, there's a unifying idea behind break: in a loop, break jumps out of the code block; in a closure, it would have the same effect. Thus, loops become like a special kind of function that just accepts a closure.

Similarly, if statements become expressions, then in a loop, break X would assign X to the LHS of the loop assignment; and identically, in a closure, break X would assign X to the LHS of the closure assignment.

I'm not sure how all the details work out, but there seems to be real potential here, and I look forward to seeing your proposal.

andy Tue 20 Jan 2009

I've always been against "implied" returns. Yes it makes the simple case a bit cleaner, but I think overall it hurts code readability.

After sleeping on it - my main objection was this would lead to bad code when used unwisely. But bad code is bad code with or without this capability, so we shouldn't handicap the well-designed use cases for it (which we know are aplenty with closures). So I'm on board.

tompalmer Fri 23 Jan 2009

I'm definitely a fan of expressions. Note that ?: for nulls (2 params) and for booleans (3 params) look similar but mean very different things. Replacing the traditional 3-param version with if/else would make things less common but more consistent. Just a thought.

dobesv Sun 18 Mar 2012

I think it would be very nice if the current support for "implicit return" when you have a single expression in a closure were extended so it works with try/catch, if/else, and switch/case where all the branches of those conditionals are also single expression statements.

Recently I've especially wished that try/catch were supported for this.

As for the value and type of a loop, I'd say go with Void if this is allowed as an expression at all. The iterators that have a type (like sys::List.each) already use Void. If that wasn't good enough we'd have felt it by now.

Login or Signup to reply.