Perfection Kills

by kangax

Exploring Javascript by example

← back 1538 words

A closer look at expression closures

I was recently working hacking on something in Firebug console and remembered about expression closures — a language addition, introduced in Mozilla’s JavaScript 1.8. The concise explanation on MDC captures the essence of expression closures:

This addition is nothing more than a shorthand for writing simple functions, giving the language something similar to a typical Lambda notation. […] This syntax allows you to leave off the braces and ‘return’ statement – making them implicit. There is no added benefit to writing code in this manner, other than having it be syntactically shorter.

And that’s exactly what I used it for — to quickly experiment with something, shortening function expressions to save on typing.

MDC also gives a simple example, showcasing a function written using expression closure:


  // Regular syntax
  function() { return x * x }

  // Expression closure
  function(x) x * x

As you can see, expression closure is essentially a more concise way to create a function. Based on MDC, expression closure allows us to drop “return” and braces denoting function body.

The information on MDC, however, was so tiny that I became curious about some of the finer details of how expression closures (EC from now on) work. Can they only be used to create function expressions? How about function declarations? What about named function expressions? Is function() ... always identical to function(){return ...} no matter what’s in the ... (function body)? And what are these “simple functions” MDC is talking about?

There was no answer to any of these questions, so I decided to quickly investigate this funky language addition a bit deeper. These are some of my findings; you’ll see that certain behavior isn’t always obvious.

Letting the code speaks for itself, here’s is a list of questions followed by simple tests, attempting to answer those questions. Feel free to skip to the next section for a shorter overview of these findings.

Tests

  1. Can EC be used to create function declarations (not just function expressions)?

    Yes.

    
    function f(x, y) x + y
    typeof f; // "function"
    
  2. What about named function expressions?

    Works as expected.

    
    typeof (function f(x, y) x + y); // "function"
    
  3. How does EC affect function representation?

    As usual, but does not include curly braces (or “return” keyword)!

    
    String(function (x) x + x ); // "function (x) x + x;"
    
  4. What about non-standard `name` property?

    Works as expected.

    
    function foo(x, y) x + y;
    foo.name; // "foo"
    
  5. How about immediately invoked function expression?

    Works fine.

    
    (function(x, y) x + y)(1, 2); // 3
    
  6. What about immediately invoked function expression without wrapping parens?

    Not so fast!

    
    var f1 = (function(x, y) x + y)(1, 2);
    var f2 = function(x, y) x + y (1, 2);
    
    f1; // 3
    f2; // function(x, y) x + y(1, 2)
    

    As you can see, (1, 2) following x + y actually applies to an inner expression, so instead of calling outer function — and assigning returned value to f2f2 is being assigned a function that has x + y(1, 2) as its body. Something to be aware of.

  7. How does EC work with ES5 accessor syntax (let’s try getter first)?

    Works!

    
    var obj = ({ get foo() 1 });
    obj.foo; // 1
    
  8. How about setter?

    Works!

    
    var _x;
    var obj = ({ set foo(x) _x = x });
    obj.foo = 5;
    
    _x; // 5
    
  9. How about both?

    Works as well! Well, why wouldn’t it…

    
      var _x = 'initial value';
      var obj = ({ get foo() _x, set foo(x) _x = x });
      obj.foo; // 'initial value'
    
      obj.foo = 'overwritten value';
      obj.foo; // 'overwritten value'
    
  10. Can we confuse parser with a comma?

    Yes we can.

    
      ({ set foo(x) bar = 1, baz = 2 }); // SyntaxError
    

    In this case, we might want bar = 1, baz = 2 as an expression inside a function, but instead, comma is parsed as part of an object intializer. Wrapping expression in parenthesis would solve this problem, of course.

    
      ({ set foo(x) (bar = 1, baz = 2) });
    
  11. Are statements allowed in function body of a function created via EC?

    No.

    
      var f = function() if (true) "foo"; // Error
    

    This is pretty understandable, considering that expression after function() in EC is treated as an expression inside return statement. And return statement obviously can’t contain other statements (only expressions). Fortunately, there’s always a ternary operator to take care of cases like this:

    
      var f = function() someCondition ? "foo" : "bar";
    
  12. Can body of a function created via EC be omitted?

    Nope.

    
      function f() // Error
    

    You would think that function f() should be identical to function f(){ return }, but no, empty function body is not allowed there. It’s not clear why this was made like this. Parsing complexities, perhaps?

  13. What happens when we include more than 1 expression?

    Only first one is consumed.

    
      function f() alert("1st statement"); alert("2nd statement"); // alerts "2nd statement"
    
  14. The above is parsed as function f() alert("1st statement"); followed by alert("2nd statement"). Only first expression becomes an expression in the return statement of a function.

  15. What about automatic semicolon insertion rules?

    Same rules seem to apply.

    
      function foo(x) x
      (function() {
        /* ... */
      })()
    
      foo; // function foo(x) x(function(){ /* ... */})()
    

    A classic example of ASI — function declaration followed by newline and another expression makes for an unexpected behavior. Same rules apply here, and foo function — even though expressed via EC — ends up having function body of x(function(){})() (rather than just x).

  16. Is “{” interpreted as a block or an object literal?

    As a block! Overall production essentially matches regular function syntax.

    
      function foo(x) { top: 0, left: 0 } // SyntaxError
    

    You would think that function() { … } is interpreted as function(){ return { … } } but the production rules of “regular” function syntax precede here, and function foo() { top: 0, left: 0 } results in an error. { and } are essentially parsed as a function body (and top: 0, left: 0 as function body contents).

  17. What if it’s an expression?

    Same thing. Treated as regular function syntax.

    
      (function(x, y) { left: x, top: y }) // SyntaxError
    

    You need to be careful to wrap returning object with parenthesis, so that curly braces aren’t considered to be part of a function production. In this case — left: x, top: y results in an error. But if returned object only contains one value, or no value at all, the silent behavior can make for an unexpected behavior down the road:

    
      (function() { x: 1 } )(); // returns `undefined`
      (function() ({ x: 1 }) )(); // Expression Closure; returns object with x=1
    
      (function() {} )(); // returns `undefined`
      (function() ({}) )(); // Expression Closure; returns object
    
  18. Can we make function explicitly strict?

    No.

    
      function foo(x, y) "use strict"; 1 + 1;
      typeof foo(); // "string" (not "number")
    

    This obviously doesn’t work; “use strict” — which is a strict directive that’s supposed to make function behave as per ES5 strict mode rules — doesn’t behave in any special way. It’s simply treated as a string literal and so resulting function becomes identical to function foo(){ return “use strict”; } (followed by a useless 1 + 1 statement).

(This file was used for testing)

Main features

Well that was fun. To summarize some of the main traits:

  1. Function body should always be present
  2. Only first expression is consumed by an implicit return statement
  3. Can be used in place of both — function declaration and function expression (named or anonymous)
  4. function idopt (argsopt) expr is not fully identical to function idopt (argsopt) { return expr } (depends on context)
  5. Precedence of call expression can be not obvious. For example: (function()x+y)() vs. function()x+y()

Practical?

To my knowledge, expression closures are currently only supported by Mozilla-based browsers. They also aren’t supported by tools like Google Closure Compiler or YUI Compressor, which certainly makes it challenging to use them in production.

It’s not clear if more implementations will join Mozilla and add support for these shortcuts. Could they be part of an experimental road to Harmony (and its shorter function syntax) — the next version of the ECMAScript language? Do you already use expression closures in your work/experiments, or perhaps planning to? And would you change anything in their behavior?

Did you like this? Donations are welcome

comments powered by Disqus