Awesome
patcom
patcom
is a pattern-matching JavaScript library. Build pattern matchers from simpler, smaller matchers.
Pattern-matching uses 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'
<details>
<summary>What is <code>match</code> doing?</summary>
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
<!-- prettier-ignore -->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.
<!-- prettier-ignore -->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}`)
)
)
<details>
<summary>What is <code>greaterThan</code>?</summary>
greaterThan
is a Matcher
provided by patcom
. greaterThan(90)
means "match a number greater than 90".
Match Array
, String
, RegExp
and more
Arrays
<!-- prettier-ignore -->match (list) (
when (
[],
() => 'empty list'
),
when (
[defined],
([head]) => `single item ${head}`
),
when (
[defined, rest],
([head, tail]) => `multiple items`
)
)
<details>
<summary>What is <code>rest</code>?</summary>
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
<!-- prettier-ignore -->
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
<!-- prettier-ignore -->
match (value) (
when (
69,
() => 'nice'
),
when (
69n,
() => 'big nice'
),
when (
true,
() => 'not nice'
)
)
Match complex data structures
<!-- prettier-ignore -->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 into simpler reusable matchers.
<!-- prettier-ignore -->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
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
. Primitive 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
<!-- prettier-ignore -->when (
{
schedule: [
{ class: 'history', rest },
rest
]
},
...
)
Equivalent explict matchers
<!-- prettier-ignore -->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 Matcher
s 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 can jump back to a previous state. This is useful for Matcher
s that require lookahead. For example, the maybe
matcher would remember the starting position with const start = iterator.now
, look ahead 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 values that were consumed by the 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 Matcher
s
Directly useable Matcher
s.
-
<!-- prettier-ignore -->any
const any: Matcher<any>
Matches for any value, including
<details> <summary>Example</summary> <!-- prettier-ignore -->undefined
.
</details>const matcher = any matcher(undefined) ≡ { matched: true, value: undefined } matcher({ key: 'value' }) ≡ { matched: true, value: { key: 'value' } }
-
<!-- prettier-ignore -->defined
const defined: Matcher<any>
Matches for any defined value, or in other words not
<details> <summary>Example</summary> <!-- prettier-ignore -->undefined
.
</details>const matcher = defined matcher({ key: 'value' }) ≡ { matched: true, value: {key: 'value' } } matcher(undefined) ≡ { matched: false }
-
<!-- prettier-ignore -->empty
const empty: Matcher<[] | {} | ''>
Matches either
<details> <summary>Example</summary> <!-- prettier-ignore -->[]
,{}
, or''
(empty string).
</details>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
.
-
<!-- prettier-ignore -->between
function between(lower: number, upper: number): Matcher<number>
Matches if value is a
<details> <summary>Example</summary> <!-- prettier-ignore -->Number
, wherelower <= value < upper
</details>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 }
-
<!-- prettier-ignore -->equals
function equals<T>(expected: T): Matcher<T>
Matches
<details> <summary>Example</summary> <!-- prettier-ignore -->expected
if strictly equals===
to value.
<!-- prettier-ignore -->const matcher = equals('alice') matcher('alice') ≡ { matched: true, value: 'alice' } matcher(42) ≡ { matched: false }
<!-- prettier-ignore -->const matcher = equals(42) matcher('alice') ≡ { matched: false } matcher(42) ≡ { matched: true, value: 42 }
</details>const matcher = equals(undefined) matcher(undefined) ≡ { matched: true, value: undefined } matcher(42) ≡ { matched: false }
-
<!-- prettier-ignore -->greaterThan
function greaterThan(expected: number): Matcher<number>
Matches if value is a
<details> <summary>Example</summary> <!-- prettier-ignore -->Number
, whereexpected < value
</details>const matcher = greaterThan(10) matcher(9) ≡ { matched: false } matcher(10) ≡ { matched: false } matcher(11) ≡ { matched: true, value: 11 }
-
<!-- prettier-ignore -->greaterThanEquals
function greaterThanEquals(expected: number): Matcher<number>
Matches if value is a
<details> <summary>Example</summary> <!-- prettier-ignore -->Number
, whereexpected <= value
</details>const matcher = greaterThanEquals(10) matcher(9) ≡ { matched: false } matcher(10) ≡ { matched: true, value: 10 } matcher(11) ≡ { matched: true, value: 11 }
-
<!-- prettier-ignore -->lessThan
function lessThan(expected: number): Matcher<number>
Matches if value is a
<details> <summary>Example</summary> <!-- prettier-ignore -->Number
, whereexpected > value
</details>const matcher = lessThan(10) matcher(9) ≡ { matched: true, value: 9 } matcher(10) ≡ { matched: false } matcher(11) ≡ { matched: false }
-
<!-- prettier-ignore -->lessThanEquals
function lessThanEquals(expected: number): Matcher<number>
Matches if value is a
<details> <summary>Example</summary> <!-- prettier-ignore -->Number
, whereexpected >= value
</details>const matcher = lessThanEquals(10) matcher(9) ≡ { matched: true, value: 9 } matcher(10) ≡ { matched: true, value: 10 } matcher(11) ≡ { matched: false }
-
<!-- prettier-ignore -->matchPredicate
function matchPredicate<T>(predicate: (value: any) => Boolean): Matcher<T>
Matches value that satisfies the predicate, or in other words
<details> <summary>Example</summary> <!-- prettier-ignore -->predicate(value) === true
</details>const isEven = (x) => x % 2 === 0 const matcher = matchPredicate(isEven) matcher(2) ≡ { matched: true, value: 2 } matcher(1) ≡ { matched: false }
-
<!-- prettier-ignore -->matchBigInt
function matchBigInt(expected?: bigint): Matcher<bigint>
Matches if value is the
<details> <summary>Example</summary> <!-- prettier-ignore -->expected
BigInt. Matches any defined BigInt ifexpected
is not provided.
<!-- prettier-ignore -->const matcher = matchBigInt(42n) matcher(42n) ≡ { matched: true, value: 42n } matcher(69n) ≡ { matched: false } matcher(42) ≡ { matched: false }
</details>const matcher = matchBigInt() matcher(42n) ≡ { matched: true, value: 42n } matcher(69n) ≡ { matched: true, value: 69n } matcher(42) ≡ { matched: false }
-
<!-- prettier-ignore -->matchNumber
function matchNumber(expected?: number): Matcher<number>
Matches if value is the
<details> <summary>Example</summary> <!-- prettier-ignore -->expected
Number
. Matches any definedNumber
ifexpected
is not provided.
<!-- prettier-ignore -->const matcher = matchNumber(42) matcher(42) ≡ { matched: true, value: 42 } matcher(69) ≡ { matched: false } matcher(42n) ≡ { matched: false }
</details>const matcher = matchNumber() matcher(42) ≡ { matched: true, value: 42 } matcher(69) ≡ { matched: true, value: 69 } matcher(42n) ≡ { matched: false }
-
<!-- prettier-ignore -->matchProp
function matchProp(expected: string): Matcher<string>
Matches if value has
<details> <summary>Example</summary> <!-- prettier-ignore -->expected
as a property key, or in other wordsexpected in value
.
</details>const matcher = matchProp('x') matcher({ x: 42 }) ≡ { matched: true, value: { x: 42 } } matcher({ y: 42 }) ≡ { matched: false } matcher({}) ≡ { matched: false }
-
<!-- prettier-ignore -->matchString
function matchString(expected?: string): Matcher<string>
Matches if value is the
<details> <summary>Example</summary> <!-- prettier-ignore -->expected
String
. Matches any definedString
ifexpected
is not provided.
<!-- prettier-ignore -->const matcher = matchString('alice') matcher('alice') ≡ { matched: true, value: 'alice' } matcher('bob') ≡ { matched: false }
</details>const matcher = matchString() matcher('alice') ≡ { matched: true, value: 'alice' } matcher(undefined) ≡ { matched: false } matcher({ key: 'value' }) ≡ { matched: false }
-
<!-- prettier-ignore -->matchRegExp
function matchRegExp(expected: RegExp): Matcher<string>
Matches if value matches the
<details> <summary>Example</summary> <!-- prettier-ignore -->expected
RegExp
.Matched
will include theRegExp
match object as thematchedRegExp
property.
<!-- prettier-ignore -->const matcher = matchRegExp(/^dear (\w+)$/) matcher('dear alice') ≡ { matched: true, value: 'dear alice', matchedRegExp: ['dear alice', 'alice'] } matcher('hello alice') ≡ { matched: false }
</details>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 Matcher
s.
-
<!-- prettier-ignore -->matchArray
function matchArray<T>(expected?: T[]): Matcher<T[]>
Matches
<details> <summary>Example</summary> <!-- prettier-ignore -->expected
array completely. Primitives inexpected
are wrapped with their correspondingMatcher
builder.expected
array can also includeIteratorMatcher
s which can consume multiple elements. Matches any defined array ifexpected
is not provided.
<!-- prettier-ignore -->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 }
<!-- prettier-ignore -->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 }
<!-- prettier-ignore -->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 }
<!-- prettier-ignore -->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 }
<!-- prettier-ignore -->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 }
</details>const matcher = matchArray() matcher([42, 'alice']) ≡ { matched: true, value: [42, 'alice'], result: [] } matcher(undefined) ≡ { matched: false } matcher({ key: 'value' }) ≡ { matched: false }
-
<!-- prettier-ignore -->matchObject
function matchObject<T>(expected?: T): Matcher<T>
Matches
<details> <summary>Example</summary> <!-- prettier-ignore -->expected
enumerable object properties completely or partially withrest
matcher. Primitives inexpected
are wrapped with their correspondingMatcher
builder. The rest of properties can be found on thevalue
with the rest key. Matches any defined object ifexpected
is not provided.
<!-- prettier-ignore -->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 }
<!-- prettier-ignore -->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 }
<!-- prettier-ignore -->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 } } } }
</details>const matcher = matchObject() matcher({ x: 42, y: 'alice' }) ≡ { matched: true, value: { x: 42, y: 'alice' }, result: {} } matcher(undefined) ≡ { matched: false } matcher('alice') ≡ { matched: false }
-
<!-- prettier-ignore -->group
function group<T>(...expected: T[]): Matcher<T[]>
An
<details> <summary>Example</summary> <!-- prettier-ignore -->IteratorMatcher
that consumes all a sequence of element matchingexpected
array. Similar to regular expression group.
<!-- prettier-ignore -->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 }
<!-- prettier-ignore -->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' } ] }
</details>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 }
-
<!-- prettier-ignore -->maybe
function maybe<T>(expected: T): Matcher<T | undefined>
An
<details> <summary>Example</summary> <!-- prettier-ignore -->IteratorMatcher
that consumes an element in the array if it matchesexpected
, otherwise does nothing. The unmatched element can be consumed by the next matcher. Similar to the regular expression?
operator.
<!-- prettier-ignore -->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 }
<!-- prettier-ignore -->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' } ] }
</details>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' } ] }
-
<!-- prettier-ignore -->not
function not<T>(unexpected: T): Matcher<T>
Matches if value does not match
<details> <summary>Example</summary> <!-- prettier-ignore -->unexpected
. Primitives inunexpected
are wrapped with their correspondingMatcher
builder
</details>const matcher = not(oneOf('alice', 'bob')) matcher('eve') ≡ { matched: true, value: 'eve' } matcher('alice') ≡ { matched: false } matcher('bob') ≡ { matched: false }
-
<!-- prettier-ignore -->rest
const rest: Matcher<any>
An
<details> <summary>Example</summary> <!-- prettier-ignore -->IteratorMatcher
that consumes the remaining elements/properties to prefix matching of arrays and partial matching of objects.
<!-- prettier-ignore -->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 }
<!-- prettier-ignore -->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 }
<!-- prettier-ignore -->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 }
</details>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 } } } }
-
<!-- prettier-ignore -->some
function some<T>(expected: T): Matcher<T[]>
An
<details> <summary>Example</summary> <!-- prettier-ignore -->IteratorMatcher
consumes all consecutive elements matchingexpected
in the array until it reaches the end or encounters an unmatched element. The next matcher can consume the unmatched element. At least one element must match. Similar to regular expression+
operator.some
does not compose with matchers that consume nothing, such asmaybe
. Attempting to compose withmaybe
will throw an error as it would otherwise lead to an infinite loop.
<!-- prettier-ignore -->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 }
</details>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' } ] }
-
<!-- prettier-ignore -->allOf
function allOf<T>(expected: ...T): Matcher<T>
Matches if all
<details> <summary>Example</summary> <!-- prettier-ignore -->expected
matchers are matched. Primitives inexpected
are wrapped with their correspondingMatcher
builder. Always matches ifexpected
is empty.
<!-- prettier-ignore -->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 }
</details>const matcher = allOf() matcher(undefined) ≡ { matched: true, value: undefined, result: [] } matcher({ key: 'value' }) ≡ { matched: true, value: { key: 'value' }, result: [] }
-
<!-- prettier-ignore -->oneOf
function oneOf<T>(expected: ...T): Matcher<T>
Matches first
<details> <summary>Example</summary> <!-- prettier-ignore -->expected
matcher that matches. Primitives inexpected
are wrapped with their correspondingMatcher
builder. Always unmatched when emptyexpected
. Similar tomatch
. Similar to the regular expression|
operator.
<!-- prettier-ignore -->const matcher = oneOf('alice', 'bob') matcher('alice') ≡ { matched: true, value: 'alice' } matcher('bob') ≡ { matched: true, value: 'bob' } matcher('eve') ≡ { matched: false }
</details>const matcher = oneOf() matcher(undefined) ≡ { matched: false } matcher({ key: 'value' }) ≡ { matched: false }
-
<!-- prettier-ignore -->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
<details> <summary>Example</summary> <!-- prettier-ignore -->expected
matches and satisfies all theguards
, then matched value is transformed withvalueMapper
.guards
are optional. Primativeexpected
are wrapped with their correspondingMatcher
builder. Second parameter tovalueMapper
is theMatched
Result
. SeematchRegExp
,matchArray
,matchObject
,group
,some
andallOf
for extra fields onMatched
.
<!-- prettier-ignore -->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 }
</details>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 }
-
<!-- prettier-ignore -->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
<details> <summary>Example</summary> <!-- prettier-ignore -->guards
, then value is transformed withvalueMapper
.guards
are optional. Second parameter tovalueMapper
is theMatched
Result
. SeematchRegExp
,matchArray
,matchObject
,group
,some
andallOf
for extra fields onMatched
.
<!-- prettier-ignore -->const matcher = otherwise( ({ surname }) => `Good morning ${surname}` ) matcher({ role: 'teacher', surname: 'Wong' }) ≡ { matched: true, value: 'Good morning Wong' }
</details>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 Matcher
s to produce a value.
-
<!-- prettier-ignore -->match
const match<T, R>: (value: T) => (...clauses: Matcher<R>) => R | undefined
Returns a matched value for the first clause that matches, or
<details> <summary>Example</summary> <!-- prettier-ignore -->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 useoneOf
.
<!-- prettier-ignore -->function meme(value) { return match (value) ( when (69, () => 'nice'), otherwise (() => 'meh') ) } meme(69) ≡ 'nice' meme(42) ≡ 'meh'
</details>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
does not implement the semantics of TC39 pattern matching proposal. However, patcom
was inspired by the 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 the left to patcom
on the right
Differences
The most notable difference is patcom
implemented enumerable object properties matching, whereas 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, whereas patcom
uses the more literal meaning of undefined
as one would expect with holes in arrays defined in standard JavaScript. The any
matcher must be explicitly 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 most significant difference.
TC39 pattern matching proposal when
syntax shape
<!-- prettier-ignore -->
when (
pattern + destructuring
) if guard:
expression
patcom
when
syntax shape
<!-- prettier-ignore -->
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 that 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 the 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:
- interpolation pattern syntax
- custom matcher protocol interpolations syntax
with
chaining syntax.
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 complete comparison 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 inspired by TC39 pattern matching proposal has many similarities 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 that could be implemented in the future:
- more unit testing
- better documentation
- executable examples
- tests 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
What does patcom
mean?
patcom
is short for pattern combinator, as patcom
is the same concept as parser combinator