Skip to content

Instantly share code, notes, and snippets.

@rbuckton
Created October 3, 2017 19:32
Show Gist options
  • Select an option

  • Save rbuckton/2fdd8258af5e7740b505af2fbcb0c69d to your computer and use it in GitHub Desktop.

Select an option

Save rbuckton/2fdd8258af5e7740b505af2fbcb0c69d to your computer and use it in GitHub Desktop.

Exploration of Statements as Expressions

For the purposes of this investigation, I am breaking down the various statements within ECMAScript into several broad categories:

  • Leaf Statements:
    • Declaration Statements - class, function, var, let, const, import, export
    • Abrupt Completion Statements - throw, return, break, continue
    • Other - debugger
  • Branching Statements:
    • Control Flow Statements - if, switch, try
    • Iteration Statements - for..in, for..of, for, while, do..while
    • Other - with, block

Leaf Statements

Leaf statements are those statements that do not contain an embedded Statement and generally exist either to add a declaration or to perform a single action.

Declaration Statements

class Declaration Statements

class declaration statements already have an expression form.

function Declaration Statements

function declaration statements already have an expression form.

var, let, const Declarations

A missing capability in JavaScript for functional programming is the ability to introduce an intermediate binding as part of an expression. For example, Haskell provides the let ... in ... expression, allowing you to introduce a variable as part of an expression:

f :: s -> (a,s)
f x =
  let y = ... x ...
  in y

We could consider allowing variable declarations in an expression context, given the following rules:

  • var expressions are hoisted, just as var statements.
  • let and const expressions remain block scoped, just as let and const statements.
    • This includes TDZ.
  • A var, let, or const expression may only declare one variable. Any comma (,) is treated as part of a comma expressions.
  • Add a lookahead restriction to ExpressionStatement for var and const. Lookahead for let may be trickier.

Example:

const x = a && (let b = a.b) && b.c;

import Statements

import statements already have an expression form in the form of import(...).

export Statements

export statements do not have an expression form. Further investigation is necessary to determine whether this would make sense.

Abrupt Completion Statements

throw Statements

throw statements already have a proposal for an expression form, allowing the following motivating use cases:

  • Parameter initializers
    function save(filename = throw new TypeError("Argument required")) {
    }
  • Arrow function bodies
    lint(ast, { 
      with: () => throw new Error("avoid using 'with' statements.")
    });
  • Conditional expressions
    function getEncoder(encoding) {
      const encoder = encoding === "utf8" ? new UTF8Encoder() 
                    : encoding === "utf16le" ? new UTF16Encoder(false) 
                    : encoding === "utf16be" ? new UTF16Encoder(true) 
                    : throw new Error("Unsupported encoding");
    }
  • Logical operations
    class Product {
      get id() { return this._id; }
      set id(value) { this._id = value || throw new Error("Invalid value"); }
    }

return Statements

As with throw statements, there exists a possible benefit for allowing return in an expression position:

function checkOpts(opts) {
    let x = opts?.x ?? return false;
    /* ...do something with x... */
}

ECMAScript does currently support a return-like behavior in an expression via generator functions, yield, and generator.return(). However, this interaction is somewhat esoteric.

continue/break Statements

It could be valuable to allow continue and break in an expression position. Consider the following:

for (const x of array) {
    const y = x.y ?? continue; // continue if `x.y` is not present.
}

Branching Statements

Branching statements are those statements that contain an embedded Statement. This category includes all control-flow statements, iteration statements, with statements, and blocks. For any of these statements, it becomes much more complicated to determine what constitutes the result of the expression. As a result, we will need to more fully investigate the semantics of any expression form for these statements.

Currently there are several questions we need to consider:

  • How do we define the result for the expression?
  • Will there be a mechanism for an early result in these expressions, similar to existing abrupt completions and shortcutting?
  • How do return, break, continue, await, and yield behave in these expressions?

Control Flow Statements

if Statements

if statements already have an expression construct in the form of a conditional expression ... ? ... : .... While it could be feasible to allow if as an expression, its possible such a construct could be confusing.

switch Statements

There is no existing expression form for switch. The closest existing form would be leveraging a series of conditional expressions. A possible approach to switch as an expression form is the match expression proposal.

try Statements

There is no existing expression form for try. There are several more specific expression forms that are currently under proposal that cover some of the use cases for a try expression, such as Optional Chaining (?.) and Nullary Coalesce (??).

In the API space we already have an expression for asynchronous try ... catch in the form of Promise.prototype.catch, and we will soon also haev an expression for asynchronous try ... finally in the form of Promise.prototype.finally. However, we have no such API for synchronous try ... catch ... finally.

Further investigation is needed to determine if a try expression is viable.

Iteration Statements

Iteration statements add more complexity to the branching statements category, as they allow for executing the same embedded Statement repetitively. As a result, we have even more questions to consider:

  • Should an iteration expression have a single result (e.g. last value), or multiple results (e.g. an Iterator or an Array)?
  • In the "last value" case, what if the last expression is a continue or break expression?
  • Do we need expression forms to distinguish between map-like behavior and reduce-like behavior?
  • We previously had a proposal for iteration expressions in the form of Array and Generator Comprehensions. Should we revisit these instead?

Other Statements

with Statements

In general, use of with is discouraged. As such, it may not make sense to have an expression form of with.

block Statements

block statements already have a proposal for an expression form in the do expressions proposal.

Relationship to do Expressions

If we progress along the path of adding more expression forms for existing statements, what does this mean for do expressions? In all likelyhood, do expressions are still valuable in that they:

  • Allow for an expression form for Blocks.
  • Introduce a block scope for lexical declarations like let, const, and class.

In any event, many of the same issues around semantics that need to be resolved for these expression forms also exist in the do expression proposal and will need to be investigated in depth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment