Pattern matching trong JavaScript

Có một ECMAScript proposal khá thú vị mới đang ở stage 0 về pattern matching. Tác giả của proposal này là Brian Terlson, tác giả của proposal async/await, và Sebastian Markbåge

Tất nhiên vì nó mới ở stage 0 nên bạn vẫn chưa thể thử chạy chỗ code trong bài này được. Nói ngắn gọn về pattern matching thì nó được miêu tả trong proposal thế này

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. This proposal draws heavy inspiration from Rust and F#.

Examples

Đây là ví dụ ở trong 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");
    }
}

Vì đây là một khái niệm phổ biến trong functional programming, nên hãy thử viết ví dụ trên bằng OCaml

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)

Để dễ giải thích hơn thì hãy bắt đầu bằng một ví dụ đơn giản hơn

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

Function này trả về các số được viết bằng chữ. Nếu không dùng pattern matching thì thường bạn sẽ viết thế này

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

    return 'many'
}

Hoặc là nếu không muốn dùng switch thì viết kiểu map thế này

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

Tất nhiên như bạn thấy trong ví dụ đầu tiên, các pattern của nó linh hoạt và mạnh hơn rất nhiều. Vậy hãy quay lại ví dụ đầu tiên để xem nó có những pattern gì hay.

More patterns

Bây giờ quay lại ví dụ đầu tiên. Nó là một function nhận một tham số là tọa độ một điểm trong không gian 2 hoặc 3 chiều và trả về độ dài của vector gốc. Hoặc nều tham số là 1 array thì trả về độ lớn của array đó. Còn lại thì là lỗi.

Bạn có thể dễ dàng nhận thấy 2 pattern được sử dụng là ObjectArray.

Objects

Hai pattern để match object trong ví dụ đó là { x, y }{ x, y, z }. { x, y } sẽ match một object có hai property x và y, tương tự với { x, y, z }. Như bạn thấy thì nó khá giống với destructuring syntax.

Đây là một ví dụ đơn giản hơn

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

Tương tự, bạn cũng có thể match một property với một giá trị nhất định e.g. { x: 3, y }. Ví dụ:

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

Tất nhiên cả nested property cũng có thể match được

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

Ngoài ra tuy vẫn chưa được quyết định là có hay không, có thể sẽ có cả pattern để match giá trị của property với điều kiện nhất định như thế này

// points can't have an x or y greater than 100
let isPointOutOfBounds = p => match (p) {
    { x > 100, y }: true,
    { x, y > 100 }: true,
    else: false
}

Arrays

Tương tự như destructuring syntax, bạn có thể match array thế này

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

Tương tự như object syntax, bạn cũng có thể match một giá trị nhất định. Giá trị đó có thể là một primitive value giống như object syntax hoặc thậm chí là một object syntax.

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

Rest operator ... có thể cũng có thể dùng tương tự như destructuring syntax

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`
}

RegExp

Regular expression cũng có thể được dùng làm match pattern, tuy nhiên không thể dùng trực tiếp

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

Tất nhiên proposal mới chỉ ở stage 0, nghĩa là có thể nó sẽ còn thay đổi, hoặc thậm chí còn chẳng được chính thức release trong một phiên bản ECMAScript nào. Tuy nhiên nếu được release thì nó sẽ là một feature rất hữu ích, nhất là với những ai hứng thú với functional programming.