1.1.0 • Published 2 years ago

patcom v1.1.0

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

patcom

patcom is pattern matching JavaScript library. Build pattern matchers from simpler smaller matchers.

Pattern matching is a declarative programming. The code matches the shape of the data.

npm install --save patcom

Simple example

Let's say we have objects that represent a Student or a Teacher.

  type Student = {
    role: 'student'
  }

  type Teacher = {
    role: 'teacher',
    surname: string
  }

Using patcom we can match a person by their role to form a greeting.

import {match, when, otherwise, defined} from 'patcom'

function greet(person) {
  return match (person) (
    when (
      { role: 'student' },
      () => 'Hello fellow student.'
    ),

    when (
      { role: 'teacher', surname: defined },
      ({ surname }) => `Good morning ${surname} sensei.`
    ),

    otherwise (
      () => 'STRANGER DANGER'
    )
  )
}


greet({ role: 'student' }) ≡ 'Hello fellow student.'
greet({ role: 'teacher', surname: 'Wong' }) ≡ 'Good morning Wong sensei.'
greet({ role: 'creeper' }) ≡ 'STRANGER DANGER'

match finds the first when clause that matches, then the Matched object is transformed into the greeting. If none of the when clauses match, the otherwise clause always matches.

More expressive than switch

Pattern match over whole objects and not just single fields.

Imperative switch & if 😔

Oh noes, a Pyramid of doom

switch (person.role) {
  case 'student':
    if (person.grade > 90) {
      return 'Gold star'
    } else if (person.grade > 60) {
      return 'Keep trying'
    } else {
      return 'See me after class'
    }
  default:
      throw new Exception(`expected student, but got ${person}`)
}

Declarative match 🙂

Flatten Pyramid to linear cases.

return match (person) (
  when (
    { role: 'student', grade: greaterThan(90) },
    () => 'Gold star'
  ),

  when (
    { role: 'student', grade: greaterThan(60) },
    () => 'Keep trying'
  ),

  when (
    { role: 'student', grade: defined },
    () => 'See me after class'
  ),

  otherwise (
    (person) => throw new Exception(`expected student, but got ${person}`)
  )
)

greaterThan is a Matcher provided by patcom. greaterThan(90) means "match a number greater than 90".

Match Array, String, RegExp and more

Arrays

match (list) (
  when (
    [],
    () => 'empty list'
  ),

  when (
    [defined],
    ([head]) => `single item ${head}`
  ),

  when (
    [defined, rest],
    ([head, tail]) => `multiple items`
  )
)

rest is an IteratorMatcher used within array and object patterns. Array and objects are complete matches and the rest pattern consumes all remaining values.

String & RegExp

match (command) (
  when (
    'sit',
    () => sit()
  ),

  // matchedRegExp is the RegExp match result
  when (
    /^move (\d) spaces$/,
    (value, { matchedRegExp: [, distance] }) => move(distance)
  ),

  // ...which means matchedRegExp has the named groups
  when (
    /^eat (?<food>\w+)$/,
    (value, { matchedRegExp: { groups: { food } } }) => eat(food)
  )
)

Number, BigInt & Boolean

match (value) (
  when (
    69,
    () => 'nice'
  ),

  when (
    69n,
    () => 'big nice'
  ),

  when (
    true,
    () => 'not nice'
  )
)

Match complex data structures

match (complex) (
  when (
    { schedule: [{ class: 'history', rest }, rest] },
    () => 'history first thing on schedule? buy coffee'
  ),

  when (
    { schedule: [{ professor: oneOf('Ko', 'Smith'), rest }, rest] },
    ({ schedule: [{ professor }] }) => `Professor ${professor} teaching? bring voice recorder`
  )
)

Matchers are extractable

From the previous example, complex patterns can be broken down to simpler reusable matchers.

const fastSpeakers = oneOf('Ko', 'Smith')

match (complex) (
  when (
    { schedule: [{ class: 'history', rest }, rest] },
    () => 'history first thing on schedule? buy coffee'
  ),

  when (
    { schedule: [{ professor: fastSpeakers, rest }, rest] },
    ({ schedule: [{ professor }] }) => `Professor ${professor} teaching? bring voice recorder`
  )
)

Custom matchers

Define custom matchers with any logic. ValueMatcher is a helper function to define custom matchers. It wraps a function that takes in a value and returns a Result. Either the value becomes Matched or is Unmatched.

const matchDuck = ValueMatcher((value) => {
  if (value.type === 'duck') {
    return {
      matched: true,
      value
    }
  }
  return {
    matched: false
  }
})

...

function speak(animal) {
  return match (animal) (
    when (
      matchDuck,
      () => 'quack'
    ),

    when (
      matchDragon,
      () => 'rawr'
    )
  )
)

All the examples thus far have been using match, but match itself isn't a matcher. In order to use speak in another pattern, we use oneOf instead.

const speakMatcher = oneOf (
  when (
    matchDuck,
    () => 'quack'
  ),

  when (
    matchDragon,
    () => 'rawr'
  )
)

Now upon unrecognized animals, whereas speak previously returned undefined, speakMatcher now returns { matched: false }. This allows us to combine speakMatcher with other patterns.

match (animal) (
  when (
    speakMatcher,
    (sound) => `the ${animal.type} goes ${sound}`
  ),

  otherwise(
    () => `the ${animal.type} remains silent`
  )
)

Everything except for match is actually a Matcher, including when and otherwise. Primative value and data types are automatically converted to a corresponding matcher.

when ({ role: 'student' }, ...) ≡
when (matchObject({ role: 'student' }), ...)

when (['alice'], ...) ≡
when (matchArray(['alice']), ...)

when ('sit', ...) ≡
when (matchString('sit'), ...)

when (/^move (\d) spaces$/, ...) ≡
when (matchRegExp(/^move (\d) spaces$/), ...)

when (69, ...) ≡
when (matchNumber(69), ...)

when (69n, ...) ≡
when (matchBigInt(69n), ...)

when (true, ...) ≡
when (matchBoolean(true), ...)

Even the complex patterns are composed of simpler matchers.

Primitives

when (
  {
    schedule: [
      { class: 'history', rest },
      rest
    ]
  },
  ...
)

Equivalent explict matchers

when (
  matchObject({
    schedule: matchArray([
      matchObject({ class: matchString('history'), rest }),
      rest
    ])
  }),
  ...
)

Core concept

At the heart of patcom, everything is built around a single concept, the Matcher. The Matcher takes any value and returns a Result, which is either Matched or Unmatched. Internally, the Matcher consumes a TimeJumpIterator to allow for lookahead.

Custom matchers are easily implemented using the ValueMatcher helper function. It removes the need to handle the internals of TimeJumpIterator.

type Matcher<T> = (value: TimeJumpIterator<any> | any) => Result<T>

function ValueMatcher<T>(fn: (value: any) => Result<T>): Matcher<T>

type Result<T> = Matched<T> | Unmatched

type Matched<T> = {
  matched: true,
  value: T
}

type Unmatched = {
  matched: false
}

For more advanced use cases the IteratorMatcher helper function is used to create Matchers that directly handle the internals of TimeJumpIterator but do not need to be concerned with a plain value being passed in.

The TimeJumpIterator works like a normal Iterator, except it has the ability to jump back to a previously state. This is useful for Matchers that require lookahead. For example the maybe matcher would remember the starting position with const start = iterator.now, lookahead to see if there is a match, and if it fails, jumps the iterator back using iterator.jump(start). This prevents the iterator from being consumed. If the iterator is consumed during the lookahead and left untouched on unmatched, subsequent matchers will fail to match as they would never see the value that were consumed by lookahead.

function IteratorMatcher<T>(fn: (value: TimeJumpIterator<any>) => Result<T>): Matcher<T>

type TimeJumpIterator<T> = Iterator<T> & {
  readonly now: number,
  jump(time: number): void
}

Use the asInternalIterator to pass an existing iterator into a Matcher.

const matcher = group('a', 'b', 'c')

matcher(asInternalIterator('abc')) ≡ {
  matched: true,
  value: ['a', 'b', 'c'],
  result: [
    { matched: true, value: 'a' },
    { matched: true, value: 'b' },
    { matched: true, value: 'c' }
  ]
}

Built-in Matchers

Directly useable Matchers.

  • any

    const any: Matcher<any>

    Matches for any value, including undefined.

    const matcher = any
    
    matcher(undefined) ≡ { matched: true, value: undefined }
    matcher({ key: 'value' }) ≡ { matched: true, value: { key: 'value' } }
  • defined

    const defined: Matcher<any>

    Matches for any defined value, or in other words not undefined.

    const matcher = defined
    
    matcher({ key: 'value' }) ≡ { matched: true, value: {key: 'value' } }
    
    matcher(undefined) ≡ { matched: false }
  • empty

    const empty: Matcher<[] | {} | ''>

    Matches either [], {}, or '' (empty string).

    const matcher = empty
    
    matcher([]) ≡ { matched: true, value: [] }
    matcher({}) ≡ { matched: true, value: {} }
    matcher('') ≡ { matched: true, value: '' }
    
    matcher([42]) ≡ { matched: false }
    matcher({ key: 'value' }) ≡ { matched: false }
    matcher('alice') ≡ { matched: false }

Matcher builders

Builders to create a Matcher.

  • between

    function between(lower: number, upper: number): Matcher<number>

    Matches if value is a Number, where lower <= value < upper

    const matcher = between(10, 20)
    
    matcher(9) ≡ { matched: false }
    matcher(10) ≡ { matched: true, value: 10 }
    matcher(19) ≡ { matched: true, value: 19 }
    matcher(20) ≡ { matched: false }
  • equals

    function equals<T>(expected: T): Matcher<T>

    Matches expected if strictly equals === to value.

    const matcher = equals('alice')
    
    matcher('alice') ≡ { matched: true, value: 'alice' }
    matcher(42) ≡ { matched: false }
    const matcher = equals(42)
    
    matcher('alice') ≡ { matched: false }
    matcher(42) ≡ { matched: true, value: 42 }
    const matcher = equals(undefined)
    
    matcher(undefined) ≡ { matched: true, value: undefined }
    matcher(42) ≡ { matched: false }
  • greaterThan

    function greaterThan(expected: number): Matcher<number>

    Matches if value is a Number, where expected < value

    const matcher = greaterThan(10)
    
    matcher(9) ≡ { matched: false }
    matcher(10) ≡ { matched: false }
    matcher(11) ≡ { matched: true, value: 11 }
  • greaterThanEquals

    function greaterThanEquals(expected: number): Matcher<number>

    Matches if value is a Number, where expected <= value

    const matcher = greaterThanEquals(10)
    
    matcher(9) ≡ { matched: false }
    matcher(10) ≡ { matched: true, value: 10 }
    matcher(11) ≡ { matched: true, value: 11 }
  • lessThan

    function lessThan(expected: number): Matcher<number>

    Matches if value is a Number, where expected > value

    const matcher = lessThan(10)
    
    matcher(9) ≡ { matched: true, value: 9 }
    matcher(10) ≡ { matched: false }
    matcher(11) ≡ { matched: false }
  • lessThanEquals

    function lessThanEquals(expected: number): Matcher<number>

    Matches if value is a Number, where expected >= value

    const matcher = lessThanEquals(10)
    
    matcher(9) ≡ { matched: true, value: 9 }
    matcher(10) ≡ { matched: true, value: 10 }
    matcher(11) ≡ { matched: false }
  • matchPredicate

    function matchPredicate<T>(predicate: (value: any) => Boolean): Matcher<T>

    Matches value that satisfies the predicate, or in other words predicate(value) === true

    const isEven = (x) => x % 2 === 0
    const matcher = matchPredicate(isEven)
    
    matcher(2) ≡ { matched: true, value: 2 }
    
    matcher(1) ≡ { matched: false }
  • matchBigInt

    function matchBigInt(expected?: bigint): Matcher<bigint>

    Matches if value is the expected BigInt. Matches any defined BigInt if expected is not provided.

    const matcher = matchBigInt(42n)
    
    matcher(42n) ≡ { matched: true, value: 42n }
    
    matcher(69n) ≡ { matched: false }
    matcher(42) ≡ { matched: false }
    const matcher = matchBigInt()
    
    matcher(42n) ≡ { matched: true, value: 42n }
    matcher(69n) ≡ { matched: true, value: 69n }
    
    matcher(42) ≡ { matched: false }
  • matchNumber

    function matchNumber(expected?: number): Matcher<number>

    Matches if value is the expected Number. Matches any defined Number if expected is not provided.

    const matcher = matchNumber(42)
    
    matcher(42) ≡ { matched: true, value: 42 }
    
    matcher(69) ≡ { matched: false }
    matcher(42n) ≡ { matched: false }
    const matcher = matchNumber()
    
    matcher(42) ≡ { matched: true, value: 42 }
    matcher(69) ≡ { matched: true, value: 69 }
    
    matcher(42n) ≡ { matched: false }
  • matchProp

    function matchProp(expected: string): Matcher<string>

    Matches if value has expected as a property key, or in other words expected in value.

    const matcher = matchProp('x')
    
    matcher({ x: 42 }) ≡ { matched: true, value: { x: 42 } }
    
    matcher({ y: 42 }) ≡ { matched: false }
    matcher({}) ≡ { matched: false }
  • matchString

    function matchString(expected?: string): Matcher<string>

    Matches if value is the expected String. Matches any defined String if expected is not provided.

    const matcher = matchString('alice')
    
    matcher('alice') ≡ { matched: true, value: 'alice' }
    
    matcher('bob') ≡ { matched: false }
    const matcher = matchString()
    
    matcher('alice') ≡ { matched: true, value: 'alice' }
    
    matcher(undefined) ≡ { matched: false }
    matcher({ key: 'value' }) ≡ { matched: false }
  • matchRegExp

    function matchRegExp(expected: RegExp): Matcher<string>

    Matches if value matches the expected RegExp. Matched will include the RegExp match object as the matchedRegExp property.

    const matcher = matchRegExp(/^dear (\w+)$/)
    
    matcher('dear alice') ≡ { matched: true, value: 'dear alice', matchedRegExp: ['dear alice', 'alice'] }
    
    matcher('hello alice') ≡ { matched: false }
    const matcher = matchRegExp(/^dear (?<name>\w+)$/)
    
    matcher('dear alice') ≡ { matched: true, value: 'dear alice', matchedRegExp: { groups: { name: 'alice' } } }
    
    matcher('hello alice') ≡ { matched: false }

Matcher composers

Creates a Matcher from other Matchers.

  • matchArray

    function matchArray<T>(expected?: T[]): Matcher<T[]>

    Matches expected array completely. Primitives in expected are wrapped with their corresponding Matcher builder. expected array can also include IteratorMatchers which can consume multiple elements. Matches any defined array if expected is not provided.

    const matcher = matchArray([42, 'alice'])
    
    matcher([42, 'alice']) ≡ {
      matched: true,
      value: [42, 'alice'],
      result: [
        { matched: true, value: 42 },
        { matched: true, value: 'alice' }
      ]
    }
    
    matcher([42, 'alice', true, 69]) ≡ { matched: false }
    matcher(['alice', 42]) ≡ { matched: false }
    matcher([]) ≡ { matched: false }
    matcher([42]) ≡ { matched: false }
    const matcher = matchArray([42, 'alice', rest])
    
    matcher([42, 'alice']) ≡ {
      matched: true,
      value: [42, 'alice', []],
      result: [
        { matched: true, value: 42 },
        { matched: true, value: 'alice' },
        { matched: true, value: [] }
      ]
    }
    matcher([42, 'alice', true, 69]) ≡ {
      matched: true,
      value: [42, 'alice', [true, 69]],
      result: [
        { matched: true, value: 42 },
        { matched: true, value: 'alice' },
        { matched: true, value: [true, 69] }
      ]
    }
    
    matcher(['alice', 42]) ≡ { matched: false }
    matcher([]) ≡ { matched: false }
    matcher([42]) ≡ { matched: false }
    const matcher = matchArray([maybe('alice'), 'bob'])
    
    matcher(['alice', 'bob']) ≡ {
      matched: true,
      value: ['alice', 'bob'],
      result: [
        { matched: true, value: 'alice' },
        { matched: true, value: 'bob' }
      ]
    }
    matcher(['bob']) ≡ {
      matched: true,
      value: [undefined, 'bob'],
      result: [
        { matched: true, value: undefined },
        { matched: true, value: 'bob' }
      ]
    }
    
    matcher(['eve', 'bob']) ≡ { matched: false }
    matcher(['eve']) ≡ { matched: false }
    const matcher = matchArray([some('alice'), 'bob'])
    
    matcher(['alice', 'alice', 'bob']) ≡ {
      matched: true,
      value: [['alice', 'alice'], 'bob'],
      result: [
        {
          matched: true,
          value: ['alice', 'alice'],
          result: [
            { matched: true, value: 'alice' },
            { matched: true, value: 'alice' }
          ]
        },
        { matched: true, value: 'bob' }
      ]
    }
    
    matcher(['eve', 'bob']) ≡ { matched: false }
    matcher(['bob']) ≡ { matched: false }
    const matcher = matchArray([group('alice', 'fred'), 'bob'])
    
    matcher(['alice', 'fred', 'bob']) ≡ {
      matched: true,
      value: [['alice', 'fred'], 'bob'],
      result: [
        {
          matched: true,
          value: ['alice', 'fred'],
          result: [
            { matched: true, value: 'alice' },
            { matched: true, value: 'fred' }
          ]
        },
        { matched: true, value: 'bob' }
      ]
    }
    
    matcher(['alice', 'eve', 'bob']) ≡ { matched: false }
    matcher(['eve', 'fred', 'bob']) ≡ { matched: false }
    matcher(['alice', 'bob']) ≡ { matched: false }
    matcher(['fred', 'bob']) ≡ { matched: false }
    matcher(['bob']) ≡ { matched: false }
    const matcher = matchArray()
    
    matcher([42, 'alice']) ≡ {
      matched: true,
      value: [42, 'alice'],
      result: []
    }
    
    matcher(undefined) ≡ { matched: false }
    matcher({ key: 'value' }) ≡ { matched: false }
  • matchObject

    function matchObject<T>(expected?: T): Matcher<T>

    Matches expected enumerable object properties completely or partially with rest matcher. Primitives in expected are wrapped with their corresponding Matcher builder. Rest of properties can be found on the value with the rest key. Matches any defined object if expected is not provided.

    const matcher = matchObject({ x: 42, y: 'alice' })
    
    matcher({ x: 42, y: 'alice' }) ≡ {
      matched: true,
      value: { x: 42, y: 'alice' },
      result: {
        x: { matched: true, value: 42 },
        y: { matched: true, value: 'alice' }
      }
    }
    matcher({ y: 'alice', x: 42 }) ≡ {
      matched: true,
      value: { y: 'alice', x: 42 },
      result: {
        x: { matched: true, value: 42 },
        y: { matched: true, value: 'alice' }
      }
    }
    
    matcher({ x: 42, y: 'alice', z: true, aa: 69 }) ≡ { matched: false }
    matcher({}) ≡ { matched: false }
    matcher({ x: 42 }) ≡ { matched: false }
    const matcher = matchObject({ x: 42, y: 'alice', rest })
    
    matcher({ x: 42, y: 'alice' }) ≡ {
      matched: true,
      value: { x: 42, y: 'alice', rest: {} },
      result: {
        x: { matched: true, value: 42 },
        y: { matched: true, value: 'alice' },
        rest: { matched: true, value: {} }
      }
    }
    matcher({ x: 42, y: 'alice', z: true, aa: 69 }) ≡ {
      matched: true,
      value: { x: 42, y: 'alice', rest: { z: true, aa: 69 } },
      result: {
        x: { matched: true, value: 42 },
        y: { matched: true, value: 'alice' },
        rest: { matched: true, value: { z: true, aa: 69 } }
      }
    }
    
    matcher({}) ≡ { matched: false }
    matcher({ x: 42 }) ≡ { matched: false }
    const matcher = matchObject({ x: 42, y: 'alice', customRestKey: rest })
    
    matcher({ x: 42, y: 'alice', z: true }) ≡ {
      matched: true,
      value: { x: 42, y: 'alice', customRestKey: { z: true } },
      result: {
        x: { matched: true, value: 42 },
        y: { matched: true, value: 'alice' },
        customRestKey: { matched: true, value: { z: true } }
      }
    }
    const matcher = matchObject()
    
    matcher({ x: 42, y: 'alice' }) ≡ {
      matched: true,
      value: { x: 42, y: 'alice' },
      result: {}
    }
    
    matcher(undefined) ≡ { matched: false }
    matcher('alice') ≡ { matched: false }
  • group

    function group<T>(...expected: T[]): Matcher<T[]>

    An IteratorMatcher that consumes all a sequence of element matching expected array. Similar to regular expression group.

    const matcher = matchArray([group('alice', 'fred'), 'bob'])
    
    matcher(['alice', 'fred', 'bob']) ≡ {
      matched: true,
      value: [['alice', 'fred'], 'bob'],
      result: [
        {
          matched: true,
          value: ['alice', 'fred'],
          result: [
            { matched: true, value: 'alice' },
            { matched: true, value: 'fred' }
          ]
        },
        { matched: true, value: 'bob' }
      ]
    }
    
    matcher(['alice', 'eve', 'bob']) ≡ { matched: false }
    matcher(['eve', 'fred', 'bob']) ≡ { matched: false }
    matcher(['alice', 'bob']) ≡ { matched: false }
    matcher(['fred', 'bob']) ≡ { matched: false }
    matcher(['bob']) ≡ { matched: false }
    const matcher = matchArray([group(maybe('alice'), 'fred'), 'bob'])
    
    matcher(['fred', 'bob']) ≡ {
      matched: true,
      value: [[undefined, 'fred'], 'bob'],
      result: [
        {
          matched: true,
          value: [undefined, 'fred'],
          result: [
            { matched: true, value: undefined },
            { matched: true, value: 'fred' }
          ]
        },
        { matched: true, value: 'bob' }
      ]
    }
    matcher(['alice', 'fred', 'bob']) ≡ {
      matched: true,
      value: [['alice', 'fred'], 'bob'],
      result: [
        {
          matched: true,
          value: ['alice', 'fred'],
          result: [
            { matched: true, value: 'alice' },
            { matched: true, value: 'fred' }
          ]
        },
        { matched: true, value: 'bob' }
      ]
    }
    const matcher = matchArray([group(some('alice'), 'fred'), 'bob'])
    
    matcher(['alice', 'fred', 'bob']) ≡ {
      matched: true,
      value: [[['alice'], 'fred'], 'bob'],
      result: [
        {
          matched: true,
          value: [['alice'], 'fred'],
          result: [
            {
              matched: true,
              value: ['alice'],
              result: [
                { matched: true, value: 'alice' }
              ]
            },
            { matched: true, value: 'fred' }
          ]
        },
        { matched: true, value: 'bob' }
      ]
    }
    matcher(['alice', 'alice', 'fred', 'bob']) ≡ {
      matched: true,
      value: [[['alice', 'alice'], 'fred'], 'bob'],
      result: [
        {
          matched: true,
          value: [['alice', 'alice'], 'fred'],
          result: [
            {
              matched: true,
              value: ['alice', 'alice'],
              result: [
                { matched: true, value: 'alice' },
                { matched: true, value: 'alice' }
              ]
            },
            { matched: true, value: 'fred' }
          ]
        },
        { matched: true, value: 'bob' }
      ]
    }
    
    matcher(['fred', 'bob']) ≡ { matched: false }
  • maybe

    function maybe<T>(expected: T): Matcher<T | undefined>

    An IteratorMatcher that consumes an element in the array if it matches expected, otherwise does nothing. The unmatched element can be consumed by the next matcher. Similar to regular expression ? operator.

    const matcher = matchArray([maybe('alice'), 'bob'])
    
    matcher(['alice', 'bob']) ≡ {
      matched: true,
      value: ['alice', 'bob'],
      result: [
        { matched: true, value: 'alice' },
        { matched: true, value: 'bob' }
      ]
    }
    matcher(['bob']) ≡ {
      matched: true,
      value: [undefined, 'bob'],
      result: [
        { matched: true, value: undefined },
        { matched: true, value: 'bob' }
      ]
    }
    
    matcher(['eve', 'bob']) ≡ { matched: false }
    matcher(['eve']) ≡ { matched: false }
    const matcher = matchArray([maybe(group('alice', 'fred')), 'bob'])
    
    matcher(['alice', 'fred', 'bob']) ≡ {
      matched: true,
      value: [['alice', 'fred'], 'bob'],
      result: [
        {
          matched: true,
          value: ['alice', 'fred'],
          result: [
            { matched: true, value: 'alice' },
            { matched: true, value: 'fred' }
          ]
        },
        { matched: true, value: 'bob' },
      ]
    }
    matcher(['bob']) ≡ {
      matched: true,
      value: [undefined, 'bob'],
      result: [
        { matched: true, value: undefined },
        { matched: true, value: 'bob' }
      ]
    }
    const matcher = matchArray([maybe(some('alice')), 'bob'])
    
    matcher(['alice', 'bob']) ≡ {
      matched: true,
      value: [['alice'], 'bob'],
      result: [
        {
          matched: true,
          value: ['alice'],
          result: [
            { matched: true, value: 'alice' }
          ]
        },
        { matched: true, value: 'bob' }
      ]
    }
    matcher(['alice', 'alice', 'bob']) ≡ {
      matched: true,
      value: [['alice', 'alice'], 'bob'],
      result: [
        {
          matched: true,
          value: ['alice', 'alice'],
          result: [
            { matched: true, value: 'alice' },
            { matched: true, value: 'alice' }
          ]
        },
        { matched: true, value: 'bob' }
      ]
    }
    matcher(['bob']) ≡ {
      matched: true,
      value: [undefined, 'bob'],
      result: [
        { matched: true, value: undefined },
        { matched: true, value: 'bob' }
      ]
    }
  • not

    function not<T>(unexpected: T): Matcher<T>

    Matches if value does not match unexpected. Primitives in unexpected are wrapped with their corresponding Matcher builder

    const matcher = not(oneOf('alice', 'bob'))
    
    matcher('eve') ≡ { matched: true, value: 'eve' }
    
    matcher('alice') ≡ { matched: false }
    matcher('bob') ≡ { matched: false }
  • rest

    const rest: Matcher<any>

    An IteratorMatcher that consumes the remaining elements/properties to prefix matching of arrays and partial matching of objects.

    const matcher = when(
      {
        headers: [
          { name: 'cookie', value: defined },
          rest
        ],
        rest
      },
      (
        { headers: [{ value: cookieValue }, restOfHeaders], rest: restOfResponse },
      ) => ({
        cookieValue,
        restOfHeaders,
        restOfResponse
      })
    )
    
    matcher({
      status: 200,
      headers: [
        { name: 'cookie', value: 'om' },
        { name: 'accept', value: 'everybody' }
      ]
    }) ≡ {
      cookieValue: 'om',
      restOfHeaders: [{ name: 'accept', value: 'everybody' }],
      restOfResponse: { status: 200 }
    }
    
    matcher(undefined) ≡ { matched: false }
    matcher({ key: 'value' }) ≡ { matched: false }
    const matcher = matchArray([42, 'alice', rest])
    
    matcher([42, 'alice']) ≡ {
      matched: true,
      value: [42, 'alice', []],
      result: [
        { matched: true, value: 42 },
        { matched: true, value: 'alice' },
        { matched: true, value: [] }
      ]
    }
    matcher([42, 'alice', true, 69]) ≡ {
      matched: true,
      value: [42, 'alice', [true, 69]],
      result: [
        { matched: true, value: 42 },
        { matched: true, value: 'alice' },
        { matched: true, value: [true, 69] }
      ]
    }
    
    matcher(['alice', 42]) ≡ { matched: false }
    matcher([]) ≡ { matched: false }
    matcher([42]) ≡ { matched: false }
    const matcher = matchObject({ x: 42, y: 'alice', rest })
    
    matcher({ x: 42, y: 'alice' }) ≡ {
      matched: true,
      value: { x: 42, y: 'alice', rest: {} },
      result: {
        x: { matched: true, value: 42 },
        y: { matched: true, value: 'alice' },
        rest: { matched: true, value: {} }
      }
    }
    matcher({ x: 42, y: 'alice', z: true, aa: 69 }) ≡ {
      matched: true,
      value: { x: 42, y: 'alice', rest: { z: true, aa: 69 } },
      result: {
        x: { matched: true, value: 42 },
        y: { matched: true, value: 'alice' },
        rest: { matched: true, value: { z: true, aa: 69 } }
      }
    }
    
    matcher({}) ≡ { matched: false }
    matcher({ x: 42 }) ≡ { matched: false }
    const matcher = matchObject({ x: 42, y: 'alice', customRestKey: rest })
    
    matcher({ x: 42, y: 'alice', z: true }) ≡ {
      matched: true,
      value: { x: 42, y: 'alice', customRestKey: { z: true } },
      result: {
        x: { matched: true, value: 42 },
        y: { matched: true, value: 'alice' },
        customRestKey: { matched: true, value: { z: true } }
      }
    }
  • some

    function some<T>(expected: T): Matcher<T[]>

    An IteratorMatcher that consumes all consecutive element matching expected in the array until it reaches the end or encounters an unmatched element. The unmatched element can be consumed by the next matcher. At least one element must match. Similar to regular expression + operator. some does not compose with matchers that consume nothing, such as maybe. Attempting to compose with maybe will throw an error as it would otherwise lead to an infinite loop.

    const matcher = matchArray([some('alice'), 'bob'])
    
    matcher(['alice', 'alice', 'bob']) ≡ {
      matched: true,
      value: [['alice', 'alice'], 'bob'],
      result: [
        {
          matched: true,
          value: ['alice', 'alice'],
          result: [
            { matched: true, value: 'alice' },
            { matched: true, value: 'alice' }
          ]
        },
        { matched: true, value: 'bob' }
      ]
    }
    
    matcher(['eve', 'bob']) ≡ { matched: false }
    matcher(['bob']) ≡ { matched: false }
    const matcher = matchArray([some(group('alice', 'fred')), 'bob'])
    
    matcher(['alice', 'fred', 'bob']) ≡ {
      matched: true,
      value: [[['alice', 'fred']], 'bob'],
      result: [
        {
          matched: true,
          value: [['alice', 'fred']],
          result: [
            {
              matched: true,
              value: ['alice', 'fred'],
              result: [
                { matched: true, value: 'alice' },
                { matched: true, value: 'fred' }
              ]
            }
          ]
        },
        { matched: true, value: 'bob' }
      ]
    }
    matcher(['alice', 'fred', 'alice', 'fred', 'bob']) ≡ {
      matched: true,
      value: [[['alice', 'fred'], ['alice', 'fred']], 'bob'],
      result: [
        {
          matched: true,
          value: [['alice', 'fred'], ['alice', 'fred']],
          result: [
            {
              matched: true,
              value: ['alice', 'fred'],
              result: [
                { matched: true, value: 'alice' },
                { matched: true, value: 'fred' }
              ]
            },
            {
              matched: true,
              value: ['alice', 'fred'],
              result: [
                { matched: true, value: 'alice' },
                { matched: true, value: 'fred' }
              ]
            }
          ]
        },
        { matched: true, value: 'bob' }
      ]
    }
  • allOf

    function allOf<T>(expected: ...T): Matcher<T>

    Matches if all expected matchers are matched. Primitives in expected are wrapped with their corresponding Matcher builder. Always matches if expected is empty.

    const isEven = (x) => x % 2 === 0
    const matchEven = matchPredicate(isEven)
    
    const matcher = allOf(between(1, 10), matchEven)
    
    matcher(2) ≡ {
      matched: true,
      value: 2,
      result: [
        { matched: true, value: 2 },
        { matched: true, value: 2 }
      ]
    }
    
    matcher(0) ≡ { matched: false }
    matcher(1) ≡ { matched: false }
    matcher(12) ≡ { matched: false }
    const matcher = allOf()
    
    matcher(undefined) ≡ { matched: true, value: undefined, result: [] }
    matcher({ key: 'value' }) ≡ { matched: true, value: { key: 'value' }, result: [] }
  • oneOf

    function oneOf<T>(expected: ...T): Matcher<T>

    Matches first expected matcher that matches. Primitives in expected are wrapped with their corresponding Matcher builder. Always unmatched when empty expected. Similar to match. Similar to the regular expression | operator.

    const matcher = oneOf('alice', 'bob')
    
    matcher('alice') ≡ { matched: true, value: 'alice' }
    matcher('bob') ≡ { matched: true, value: 'bob' }
    
    matcher('eve') ≡ { matched: false }
    const matcher = oneOf()
    
    matcher(undefined) ≡ { matched: false }
    matcher({ key: 'value' }) ≡ { matched: false }
  • when

    type ValueMapper<T, R> = (value: T, matched: Matched<T>) => R
    
    function when<T, R>(
      expected?: T,
      ...guards: ValueMapper<T, Boolean>,
      valueMapper: ValueMapper<T, R>
    ): Matcher<R>

    Matches if expected matches and satisfies all the guards, then matched value is transformed with valueMapper. guards are optional. Primative expected are wrapped with their corresponding Matcher builder. Second parameter to valueMapper is the Matched Result. See matchRegExp, matchArray, matchObject, group, some and allOf for extra fields on Matched.

    const matcher = when(
      { role: 'teacher', surname: defined },
      ({ surname }) => `Good morning ${surname}`
    )
    
    matcher({ role: 'teacher', surname: 'Wong' }) ≡ {
      matched: true,
      value: 'Good morning Wong',
      result: {
        role: { matched: true, value: 'teacher' },
        surname: { matched: true, value: 'Wong' }
      }
    }
    
    matcher({ role: 'student' }) ≡ { matched: false }
    const matcher = when(
      { role: 'teacher', surname: defined },
      ({ surname }) => surname.length === 4, // guard
      ({ surname }) => `Good morning ${surname}`
    )
    
    matcher({ role: 'teacher', surname: 'Wong' }) ≡ {
      matched: true,
      value: 'Good morning Wong',
      result: {
        role: { matched: true, value: 'teacher' },
        surname: { matched: true, value: 'Wong' }
      }
    }
    
    matcher({ role: 'teacher', surname: 'Smith' }) ≡ { matched: false }
  • otherwise

    type ValueMapper<T, R> = (value: T, matched: Matched<T>) => R
    
    function otherwise<T, R>(
      ..guards: ValueMapper<T, Boolean>,
      valueMapper: ValueMapper<T, R>
    ): Matcher<R>

    Matches if satisfies all the guards, then value is transformed with valueMapper. guards are optional. Second parameter to valueMapper is the Matched Result. See matchRegExp, matchArray, matchObject, group, some and allOf for extra fields on Matched.

    const matcher = otherwise(
      ({ surname }) => `Good morning ${surname}`
    )
    
    matcher({ role: 'teacher', surname: 'Wong' }) ≡ {
      matched: true,
      value: 'Good morning Wong'
    }
    const matcher = otherwise(
      ({ surname }) => surname.length === 4, // guard
      ({ surname }) => `Good morning ${surname}`
    )
    
    matcher({ role: 'teacher', surname: 'Wong' }) ≡ {
      matched: true,
      value: 'Good morning Wong'
    }
    
    matcher({ role: 'teacher', surname: 'Smith' }) ≡ { matched: false }

Matcher consumers

Consumes Matchers to produce a value.

  • match

    const match<T, R>: (value: T) => (...clauses: Matcher<R>) => R | undefined

    Returns a matched value for the first clause that matches, or undefined if all are unmatched. match is to be used as a top level expression and is not composable. To create a matcher composed of clauses use oneOf.

    function meme(value) {
      return match (value) (
        when (69, () => 'nice'),
        otherwise (() => 'meh')
      )
    }
    
    meme(69) ≡ 'nice'
    meme(42) ≡ 'meh'
    function meme(value) {
      return match (value) (
        when (69, () => 'nice')
      )
    }
    
    meme(69) ≡ 'nice'
    meme(42) ≡ undefined
    
    const memeMatcher = oneOf (
      when (69, () => 'nice')
    )
    
    memeMatcher(69) ≡ { matched: true, value: 'nice' }
    memeMatcher(42) ≡ { matched: false }

What about TC39 pattern matching proposal?

patcom is does not implement the semantics of TC39 pattern matching proposal. However, patcom was inspired from TC39 pattern matching proposal and in-fact has feature parity. As patcom is a JavaScript library, it cannot introduce any new syntax, but the syntax remains relatively similar.

Comparision of TC39 pattern matching proposal on left to patcom on right

tc39 comparision

Differences

The most notable different is patcom implemented enumerable object properties matching, where as TC39 pattern matching proposal implements partial object matching. See tc39/proposal-pattern-matching#243. The rest matcher can be used to achieve partial object matching.

patcom also handles holes in arrays differently. Holes in arrays in TC39 pattern matching proposal will match anything, where as patcom uses the more literial meaning of undefined as one would expect with holes in arrays defined in standard JavaScript. The any matcher must be explicity used if one desires to match anything for a specific array position.

Since patcom had to separate the pattern matching from destructuring, enumerable object properties matching is the most sensible. Syntactically separation of the pattern from destructuring is the biggest difference.

TC39 pattern matching proposal when syntax shape

when (
  pattern + destructuring
) if guard:
  expression

patcom when syntax shape

when (
  pattern,
  (destructuring) => guard,
  (destructuring) => expression
)

patcom offers allOf and oneOf matchers as subsitute for the pattern combinators syntax.

TC39 pattern matching proposal and combinator + or combinator

Note the usage of and in this example is purely to capture the match and assign it to dir.

when (
  ['go', dir and ('north' or 'east' or 'south' or 'west')]
):
  ...use dir

patcom oneOf matcher + destructuring

Assignment to dir separated from pattern.

when (
  ['go', oneOf('north', 'east', 'south', 'west')],
  ([, dir]) =>
    ...use dir
)

Additional consequence of the separating the pattern from destructuring is patcom has no need for any of:

Another difference is TC39 pattern matching proposal caches iterators and object property accesses. This has been implemented in patcom as a different variation of match, which is powered by cachingOneOf.

To see a full comparsion with TC39 pattern matching proposal and unit tests to prove full feature parity, see tc39-proposal-pattern-matching folder.

What about match-iz?

match-iz is similarly insprired by TC39 pattern matching proposal has has many similaries to patcom. However, match-iz is not feature complete to TC39 pattern matching proposal, most notably missing is:

  • when guards
  • caching iterators and object property accesses

match-iz also offers a different match result API, where matched and value are allowed to be functions. The same functionality in patcom can be found in the form of functional mappers.

Contributions welcome

The following is a non-exhaustive list of features which could be implemented in the feature

  • more unit testing
  • better documentation
    • executable examples
  • test that extract and execute samples out of documentation
  • richer set of matchers
    • as this library exports modules, the size of the npm package does not matter if consumed by a tree shaking bundler. this means matchers of any size will be accepted as long as all matchers can be organized well as a cohesive set
    • Date matcher
    • Temporal matchers
    • Typed array matchers
    • Map matcher
    • Set matcher
    • Intl matchers
    • Dom matchers
    • other Web API matchers
  • eslint
  • typescript, either by rewrite or .d.ts files
  • async matchers

patcom is seeking funding

What does patcom mean?

patcom is short for pattern combinator, as patcom the same concept as parser combinator

1.1.0

2 years ago

1.0.0

2 years ago

0.1.3

2 years ago

0.1.2

2 years ago

0.1.1

2 years ago

0.1.0

2 years ago