Skip to content

🌊 A pattern matching mechanism for JavaScript.

Notifications You must be signed in to change notification settings

chrisisler/wavematch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Wavematch is a control flow mechanism for JavaScript.

Introduction

Wavematch enables pattern matching. It's super declarative. A branch of code is executed when specified conditions of the input are satisfied. For example,

let result = wavematch(random(0, 5))(
  (n = 0) => 'zero',
  (n = 1) => 'one',
  (n = 2) => 'two',
  _       => 'otherwise'
)

The value of result is dependent on which branch of code gets ran when one of the conditions are satisfied. If none of the cases meet the user-given requirements, the default branch is executed.

Install

yarn add wavematch

Matching Standard Types

Use constructors for type-based matching:

let typeMatch = id => wavematch(id)(
  (id = Number) => 'received a number!',
  (id = String) => 'received a string!',
  _ => 'something else!'
)

Matching Object Props

Use plain objects as a pattern to match on properties:

wavematch({ done: false, rank: 42 })(
    (obj = { done: false }) => {}
)
let assertShape = obj => wavematch(obj)(
  (shape = { foo: Number }) => {}, // skip this case
  _ => { throw Error() }
)
assertShape({ foo: 1 })
assertShape({ foo: 'str' }) // Error due to type difference

Destructure the object using the desired key as the argument name:

let data = { done: false, error: Error() }

wavematch(data)(
  (obj = { done: false }) => neverInvoked(),
  (done = true) => getsInvoked()
)

Destructure the input object via the argument name and match an object pattern:

wavematch({ foo: { bar: 42 } })(
  (foo = { bar: 42 }) => {}
  _ => {}

Note: Objects must be valid JSON5.

Matching Class Types

Use the class name as a pattern to match custom data types:

class Person {}

wavematch(new Person())(
  (p = Person) => {
    console.log('Is a Person')
  },
  _ => {
    console.log('Not a Person')
  }
)
function Car() {}

let carInstance = new Car()

wavematch(carInstance)(
  (c = Car) => {}
)

Match Guards

Guards are boolean expressions for conditional behavior:

let fib = wavematch.create(
  (n = 0 | 1) => n,
  // if (n > 1)
  (n = $ => $ > 1) => fib(n - 1) + fib(n - 2)
)
fib(7) //=> 13
wavematch(await fetch(url))(
  (response = { status: 200 }) => response,
  (response = $ => $.status > 400) => Error(response)
)

The ({ prop }) => {} syntax can not be used for guard functions (due to being invalid json5).

Match Unions

Use | to match multiple patterns:

let value = random(0, 10)

wavematch(value)(
  (other = 2 | 4 | 6) => {
    console.log('two or four or six!')
  },
  _ => {
    console.log('not two or four or six')
  }
)
wavematch(await fetch(url))(
  (response = { status: 200 } | { ok: true }) => response,
  (response = $ => $.status > 400) => Error(response)
)
let parseArgument = arg => wavematch(arg)(
  (arg = '-h' | '--help') => displayHelp(),
  (arg = '-v' | '--version') => displayVersion(),
  _ => unknownArgument(arg)
)

Wildcard Pattern

The wildcard pattern _ matches all input arguments.

  • Binds undefined to the parameter
  • Should be the last rule provided
let number = wavematch(random(0, 100))(
  (n = 99)          => 'ninety-nine',
  (n = $ => $ > 30) => 'more than thirty',
  _                 => 'who knows'
)

Limitations

Things that can not be done:

let value = 3
let matched = wavematch(77)(
  (arg = value) => 'a', // `value` throws a ReferenceError
  _ => 'b'
)
// Workaround: If possible, replace the variable with its value.
function fn() {}
let matched = wavematch('bar')(
  (arg = fn) => 'hello',
      // ^^ `fn` throws a ReferenceError
)
// Workaround: If possible, replace `fn` with an arrow function returning a boolean.
wavematch({ age: 21.5 })(
  (obj = { age: Number }) => 'got a number',
             // ^^^^^^ Invalid JSON5 here throws the error!
  // Workaround: Use desired key name to match and destructure:
  (age = Number) => 'got a number!'
)
wavematch('foo')(
  (_ = !Array) => {},
    // ^^^^^^ Cannot use `!` operator
  _ => {}
)
// Workaround:
wavematch('foo')(
  (x = Array) => {}, // do nothing
  (x) => { /* `x` is guaranteed NOT to be an Array in this block */ }
)

Examples

let zip = (xs, ys) => wavematch(xs, ys)(
  (_, ys = []) => [],
  (xs = [], _) => [],
  ([x, ...xs], [y, ...ys]) => [x, y].concat(zip(xs, ys))
)
zip(['a', 'b'], [1, 2]) //=> ['a', 1, 'b', 2]
let zipWith = wavematch.create(
  (_, xs = [], __) => [],
  (_, __, ys = []) => [],
  (fn, [x, ...xs], [y, ...ys]) => [fn(x, y)].concat(zipWith(fn, xs, ys))
)
zipWith((x, y) => x + y, [1, 3], [2, 4]) //=> [3, 7]
let unfold = (seed, fn) => wavematch(fn(seed))(
  (_ = null) => [],
  ([seed, next]) => [].concat(seed, unfold(next, fn))
)
unfold(
  5,
  n => n === 0 ? null : [n, n - 1]
) //=> [ 5, 4, 3, 2, 1 ]

More examples are in the test directory.

Gotchas

Be mindful of the ordering of your conditions:

let matchFn = wavematch.create(
  (num = $ => $ < 42) => 'A',
  (num = $ => $ < 7) => 'B',
  _ => 'C'
)

This is a gotcha because the expected behavior is that matchFn(3) would return B because num is less than 7. The actual behavior is matchFn(3) returns A because the condition for checking if the input is less than 42 is evaluated in the order given, which is before the less-than-7 condition. So, be mindful of how the conditions are ordered.

Development

  1. Clone this repository
  2. yarn or npm i
  3. yarn build:watch

About

🌊 A pattern matching mechanism for JavaScript.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages