0

ECMAScript proposal: Pattern Matching

Currently, there's an ECMAScript proposal for pattern matching in JavaScript. The proposal's authors are Brian Terlson, who is also the author of the async/await feature, and Sebastian Markbåge. In this article, let's take a look at its current specs and syntax. The proposal is still at stage 0 though so it's very likely to change. Of course you can't try any of the provided code yet and don't expect it to be around any time soon. It probably won't even make it.

I quoted directly from the proposal if you need a quick glimpse of what pattern matching is.

Pattern matching is a way to select behaviors based on the structure of a value in a similar way to destructuring. For example, you can trivially match objects with certain properties and bind the values of those properties in the match leg. Pattern matching enables very terse and highly readable functional patterns and is found in a number of languages.

Then let's look at some examples

Examples

Here's the example provided by the proposal.

let getLength = vector => match (vector) {
    { x, y, z }: Math.sqrt(x ** 2 + y ** 2 + z ** 2),
    { x, y }:    Math.sqrt(x ** 2 + y ** 2),
    [...]:       vector.length,
    else: {
        throw new Error("Unknown vector type");
    }
}

Since pattern matching is a concept prevalent in functional programming languages, here's an equivalent piece of code written in OCaml, just for comparision.

type vector =
  | Vector2 of float * float
  | Vector3 of float * float * float
  | Any of float list

let get_length vector = match vector with
  | Vector3(x, y, z) -> sqrt(x ** 2. +. y ** 2. +. z ** 2.)
  | Vector2(x, y) -> sqrt(x ** 2. +. y ** 2.)
  | Any v -> float_of_int(List.length v)

Jumping right into that example might be difficult. Let's try to make up a simpler one first

const stringOfInt = int => match (int) {
    0: 'zero',
    1: 'one',
    2: 'two',
    else: 'many'
};

So it's a little bit easier now. The function takes an integer and returns a string representation for it. Basically, it looks like a map. Normally you would use an object for a map or a switch statement for this. Here's the code I would normally write.

// Using object
const stringOfInt = int => ({
    0: 'zero',
    1: 'one',
    2: 'two'
}[int] || 'many');

// Using switch
const stringOfIntSwitch = int => {
    switch (int) {
        case 0:
            return 'zero';
        case 1:
            return 'one'
        case 2:
            return 'two'
    }

    return 'many'
}

Not only it can replace these codes above, it's even much more powerful.

You can use any primitive value as a match pattern as you can see above. The else pattern is for any other case, quite similar to a default case of a switch statement. You can also use a block instead of a match leg as you can see in the else block of the first example. The block would be similar to an arrow function block.

For more patterns, first let's come back to the first example since it has some of it.

More patterns

Beside primitive values (or literal values), there are matching patterns for objects and arrays. They are very similar to the destructuring assignment syntaxes.

Objects

The function in the first example takes a point in an n-dimension and returns the length from the origin. Depending on the given point, it can calculate the length for 2-dimension and 3-dimension space.

The expression {x, y} evaluates to an object with two property named x and y e.g. { x: 3, y: 4 }. Same goes for {x, y, z} Here's a simpler example.

match ({ x: 3, y: 4 }) {
    { x, y }: x + y
} // 7

Similar to the destructuring assignment syntax, the match pattern is inclusive i.e. {x, y} will actually match objects like {x, y, z}

match ({ x: 3, y: 4, z: 5 }) {
    { x, y }: x + y
} // 7 -> Still match

Going a little bit further, we can also specify a matching value for a property e.g. { x: 3, y }. Here's an example.

const matchPoint = point => match (point) {
    { x: 3, y }: y,
    { x, y: 4 }: x,
    { x: 3, y: 4 }: null
}

matchPoint({x: 3, y: 2}) // 2
matchPoint({x: 2, y: 4}) // 2
matchPoint({x: 3, y: 4}) // null

You can also have nested pattern for object

const hasError = response => match (response) {
    { data: { error } }: true,
    else: false
}

Arrays

Similar to the destructuring assignment syntax, we can match array like this.

match (arr) {
    []: 'empty array',
    [x]: `array of length 1 with the only element ${x}`
}

We can also use any primitive value or object literal in matching pattern.

match (arr) {
    ['a']: 'array with only one element a',
    [{x: 0, y: 0}]: `array with only the 2d origin`
}

The rest operator ... can also be used to allow matching on arrays of any length.

match (arr) {
    [...]: 'any array',
    [head, ...]: 'array of length >= 1, bind the head element',
    [..., tail]: 'array of length >= 1, bind the tail element',
    [..., t1, t2]: 'array of length >= 2, bind last two elements as t1 and t2',
    [0, ...]: `array of length >= 1, beginning with 0`
}

It is still not decided whether iterables can also be matched with array matching patterns.

RegExp

You can also use regex as a match pattern. However, it must be passed as an identifier

const regexp = /\d+/
const isNumber = str => match (str) {
    regexp: true,
    else: false
}

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.