The destructuring algorithm in ECMAScript 6

By Axel Rauschmayer

This blog post looks at destructuring from a different angle: as a recursive matching algorithm. At the end, I’ll use this new knowledge to explain one especially tricky case of destructuring.

You may want to read the blog post “Destructuring and parameter handling in ECMAScript 6” beforehand.

Destructuring assignment

The following is a destructuring assignment.

    pattern = value

We want to use pattern to extract data from value. In the following sections, I describe an algorithm for doing so. It is known in functional programming as matching. The previous destructuring assignment is processed via

    pattern ← value

That is, the operator (“match against”) matches pattern against value. The algorithm is specified via recursive rules that take apart both operands of the operator. The declarative notation may take some getting used to, but it makes the specification of the algorithm more concise. Each rule has two parts:

  • The head specifies which operands are handled by the rule.
  • The body specifies what to do next.

I only show the algorithm for destructuring assignment. Destructuring variable declarations and destructuring parameter definitions work similarly.

Patterns

A pattern is either:

  • A variable: x
  • An object pattern: {"properties»}
  • An array pattern: ["elements»]

Each of the following sections covers one of these three cases.

Variables

  • x ← value (including undefined and null)

        x = value
    

Object patterns

  • {"properties»} ← undefined

        throw new TypeError();
    
  • {"properties»} ← null

        throw new TypeError();
    
  • {key: pattern, "properties»} ← obj

        pattern ← obj.key
        {"properties»} ← obj
    
  • {key: pattern = default_value, "properties»} ← obj

        let tmp = obj.key;
        if (tmp !== undefined) {
            pattern ← tmp
        } else {
            pattern ← default_value
        }
        {"properties»} ← obj
    
  • {} ← obj (done)

Array patterns

The sub-algorithm in this section starts with an array pattern and an iterable and continues with the elements of the pattern and an iterator (obtained from the iterable). The helper functions isIterable() and getNext() are defined at the end of this section.

  • ["elements»] ← iterable

        if (!isIterable(iterable)) {
            throw new TypeError();
        }
        let iterator = iterable[Symbol.iterator]();
        "elements» ← iterator
    
  • pattern, "elements» ← iterator

        pattern ← getNext(iterator)
        "elements» ← iterator
    
  • pattern = default_value, "elements» ← iterator

        let tmp = getNext(iterator);
        if (tmp !== undefined) {
            pattern ← tmp
        } else {
            pattern ← default_value
        }
        "elements» ← iterator
    
  • , "elements» ← iterator (hole, elision)

        getNext(iterator); // skip
        "elements» ← iterator
    
  • ...pattern ← iterator (always last part!)

        let tmp = [];
        for (let elem of iterator) {
            tmp.push(elem);
        }
        pattern ← tmp
    
  • ← iterator (no elements left, nothing to do)

    function getNext(iterator) {
        let n = iterator.next();
        if (n.done) {
            return undefined;
        } else {
            return n.value;
        }
    }
    function isIterable(value) {
        return (value !== null
            && typeof value === 'object'
            && typeof value[Symbol.iterator] === function);
    }

Example

The following function definition is used to make sure that both of the named parameters x and y have default values and can be omitted. Additionally, = {} enables us to omit the object literal, too (see last function call below).

    function move({x=0, y=0} = {}) {
        return [x, y];
    }
    move({x: 3, y: 8}); // [3, 8]
    move({x: 3}); // [3, 8]
    move({}); // [3, 8]
    move(); // [3, 8]

But why would you define the parameters as in the previous code snippet? Why not as follows – which is also completely legal ECMAScript 6?

    function move({x, y} = { x: 0, y: 0 }) {
        return [x, y];
    }

Using solution 2

Actual parameters (inside function calls) are matched against formal parameters (inside function definitions). Therefore, move() sets up the parameters x and y as follows:

    [{x, y} = { x: 0, y: 0 }] ← []

The only array element on the left-hand side does not have a match on the right-hand side, which is why the default value is used:

    {x, y} ← { x: 0, y: 0 }

The left-hand side is a property value shorthand, an abbreviation for {x: x, y: y}:

    {x: x, y: y} ← { x: 0, y: 0 }

This destructuring leads to the following two assignments.

    x = 0;
    y = 0;

However, this is the only case in which the default value is used. As soon as there is an array element at index 0 on the right-hand side, the default value is ignored:

    [{x, y} = { x: 0, y: 0 }] ← [{z:3}]

Afterwards, the next step is:

    {x, y} ← { z: 3 }

That leads to both x and y being set to undefined, which is not what we want.

Using solution 1

Let’s try solution 1:

    [{x=0, y=0} = {}] ← []

Again, we don’t have an array element at index 0 on the right-hand side and use the default value:

    {x=0, y=0} ← {}

The left-hand side is a property value shorthand, which means that this destructuring is equivalent to:

    {x: x=0, y: y=0} ← {}

Neither the property x nor the property y have a match on the right-hand side. Therefore, the following destructurings are performed next:

    x ← 0
    y ← 0

That leads to the following assignments:

    x = 0
    y = 0

Source:: 2ality