README
match-iz
🔥
Functional, declarative pattern-matching in ~150 SLOC.
Overview:
import { match, when, otherwise } from 'match-iz'
match(haystack)(
when(needle / predicate)(result / handler),
when(needle / predicate)(result / handler),
when(needle / predicate)(result / handler),
otherwise(result / handler)
)
Data-last version:
import { against, isString, isNumber } from 'match-iz'
const stringOrNumber = against(
when(isString)("it's a string!"),
when(isNumber)("it's a number!"),
otherwise("sorry, it's neither of those!")
)
const result = stringOrNumber(42)
// "it's a number!"
Use cata
to integrate match-iz
with your ADTs/monads:
import { cata } from 'match-iz'
// Specify how match-iz should detect Just/Nothing
// monads and extract their values
const { just, nothing } = cata({
just: m => m?.isJust,
nothing: m => m?.isNothing,
getValue: m => m?.valueOf()
})
match(maybeDate('2022-01-01'))(
just(dateObj => {
console.log('Parsed date: ', dateObj)
}),
nothing(() => {
console.log('Invalid date')
})
)
Install:
$ pnpm i match-iz
// ESM
import { match, when, otherwise, pluck, ...etc } from 'match-iz'
// CJS
const { match, when, otherwise, pluck, ...etc } = require('match-iz')
Browser/UMD:
<script src="https://unpkg.com/match-iz/dist/browser/match-iz.browser.js"></script>
<script>
const { match, when, otherwise, pluck, ...etc } = matchiz
</script>
Examples:
Front-end component:
match(props)(
when({ loading })(<Loading />),
when({ error })(<Error {...props} />),
when({ data })(<Page {...props} />),
otherwise(<Logout />)
)
// <Loading />
Full example
import * as matchiz from 'match-iz'
const { match, when, otherwise } = matchiz
const { spread, defined } = matchiz
function AccountPage(props) {
const { loading, error, data } = spread(defined)
return match(props)(
when({ loading })(<Loading />),
when({ error })(<Error {...props} />),
when({ data })(<Page {...props} />),
otherwise(<Logout />)
)
}
Reducer:
match(action)(
when({ type: 'add-todo', payload: pluck(isString) })(text => ({
...state,
todos: [...state.todos, { text, completed: false }]
})),
otherwise(state)
)
Full example
import { match, when, otherwise, pluck as $ } from 'match-iz'
const todosReducer = (state, action) =>
match(action)(
when({ type: 'set-visibility-filter', payload: $() })(visFilter => ({
...state,
visFilter
})),
when({ type: 'add-todo', payload: $() })(text => ({
...state,
todos: [...state.todos, { text, completed: false }]
})),
when({ type: 'toggle-todo', payload: $() })(index => ({
...state,
todos: state.todos.map((todo, i) =>
match(i)(
when(index)({ ...todo, completed: !todo.completed }),
otherwise(todo)
)
)
})),
otherwise(state)
)
Regular Expressions:
match('1 + 2')(
// Groups extracted automatically if specified
// (second argument will be the full-match)
// /__\ /___\
when(/(?<left>\d+) \+ (?<right>\d+)/)
(({ left, right }, fullMatch) => {
return add(left, right)
}),
otherwise("I couldn't parse that!")
)
// 3
Full example
import { match, when, otherwise } from 'match-iz'
match('1 + 2')(
when(/(?<firstName>\w+) (?<lastName>\w+)/)
(({ lastName }, fullMatch) => {
return `Ahoy, Captain ${lastName}`
}),
when(/(?<left>\d+) \+ (?<right>\d+)/)
(({ left, right }, fullMatch) => {
return add(left, right)
}),
otherwise("I couldn't parse that!")
)
function add(left, right) {
return parseInt(left, 10) + parseInt(right, 10)
}
Overloading:
match(vector)(
when({ x, y, z })(({ x, y, z }) => Math.hypot(x, y, z)),
when({ x, y })(({ x, y }) => Math.hypot(x, y)),
otherwise(vector => vector.length)
)
// 3.14
Full example
import * as matchiz from 'match-iz'
const { match, when, otherwise } = matchiz
const { spread, defined } = matchiz
function getLength(vector) {
const { x, y, z } = spread(defined)
return match(vector)(
when({ x, y, z })(({ x, y, z }) => Math.hypot(x, y, z)),
when({ x, y })(({ x, y }) => Math.hypot(x, y)),
otherwise(vector => vector.length)
)
}
Matching array contents:
match(['', '2', undefined])(
when(['1', _, _])('one'),
when([_, '2', _, _])('two, with four items'),
when([_, '2', _])('two'),
otherwise('nope')
)
// "two"
Full example
import * as matchiz from 'match-iz'
const { match, when, otherwise } = matchiz
const { empty: _ } = matchiz
match(['', '2', undefined])(
when(['1', _, _])('one'),
when([_, '2', _, _])('two, with four items'),
when([_, '2', _])('two'),
otherwise('nope')
)
against(...)(value)
:
Also provides lines.filter(
against(
when(/remove-this-one/)(false),
when(/and-this-one-too/)(false),
when(endsWith('-and-another'))(false),
otherwise(true)
)
)
See a couple more:
import { against, when, otherwise, lte } from 'match-iz'
// Fibonnacci
const fib = memoize(
against(
when(lte(0))(0),
when(1)(1),
otherwise(x => fib(x - 1) + fib(x - 2))
)
)
fib(35)
function memoize(fn, cache = new Map()) {
return x => (cache.has(x) ? cache.get(x) : cache.set(x, fn(x)).get(x))
}
import { against, when, otherwise } from 'match-iz'
// Sorting
numbers.sort(
nargs(
against(
when(([a, b]) => a < b)(-1),
when(([a, b]) => a === b)(0),
when(([a, b]) => a > b)(1)
)
)
)
function nargs() {
return fn => (...args) => fn(args)
}
Documentation
Helpers
match-iz
provides a number of composable helpers you can use to build patterns:
Numbers | Strings | Strings/Arrays | Truthiness | Types | Negate | Combinators |
---|---|---|---|---|---|---|
gt | startsWith | includes | empty | isArray | not | allOf |
lt | endsWith | - | falsy | isDate | - | anyOf |
gte | - | - | defined | isFunction | - | includedIn |
lte | - | - | truthy | isNumber | - | hasOwn |
inRange | - | - | - | isPojo | - | - |
- | - | - | - | isRegExp | - | - |
- | - | - | - | isString | - | - |
- | - | - | - | instanceOf | - | - |
Just import them from match-iz
as you do the core library:
import { gt, lt, etc... } from 'match-iz'
Some detail:
Helper | Meaning |
---|---|
isArray/Date/Function/...etc |
test for that type |
gt(0) |
greater than |
lt(0) |
less than |
gte(0) |
greater than or equal |
lte(0) |
less than or equal |
inRange(0, 10) |
within min ... max |
startsWith('hello ...') |
string starts with "content" |
endsWith('... world!') |
string ends with "content" |
includes(item) |
array/string contains item/"content" |
includedIn([these, things, ...]) |
alias for anyOf |
instanceOf(constructor) |
for class instances |
hasOwn('prop1', 'prop2'...) |
check for existence of object keys/props |
empty |
null, undefined, NaN, [], or {} |
defined |
negates empty, but false counts as "defined" |
truthy |
a !! check |
falsy |
a ! check |
not |
negate the result of the given pattern |
allOf |
AND |
anyOf |
OR |
hasOwn('list', 'of', 'props') |
test for existence of prop(s) |
Basic examples:
match(literal)(
when(inRange(100, 200))( ... ),
when(startsWith('hello'))( ... ),
when(includes('batman'))( ... ),
when(includedIn('one', 'two'))( ... ),
when(lte(80))( ... ),
when(empty)( ... ),
when(defined)( ... ),
)
match(object)(
when({ status: inRange(100, 200) })( ... ),
when({ text: startsWith('hello') })( ... ),
when({ array: includes('batman') })( ... ),
when({ string: includedIn('one', 'two') })( ... ),
when({ length: lte(80) })( ... ),
when({ cup: empty })( ... ),
when({ pencil: defined })( ... ),
)
A little more composition:
match(literal)(
when(not(inRange(100, 200)))( ... ),
when({ number: not(42) })( ... )
)
match(literal)(
when(allOf(isNumber, x => x > 10))( ... ),
when({ number: not(anyOf(20, 30)) })( ... )
when(includedIn([40, 50]))( ... )
)
You can use your own predicates:
const isInteger = Number.isInteger
match(status)(
when({ status: isInteger })('status is an integer'),
otherwise('nope')
)
Core-library
match / when / otherwise
match()
match(value)(...predicates)
// returns: winning value
Each predicate receives the value
passed to match()
.
The first one to return a truthy value wins, and match
returns it.
when()
when(pattern)(handler)
// returns: value => object
when
builds special predicates that return objects like this:
{
matched: () => with pattern return true|false,
value: () => with handler return result
}
If match
sees such an object return from a predicate:
matched()
is run to determine the win-statevalue()
retrieves the winning value
AND / OR
If the match value
is NOT an array, using an array within a when
will perform a logical OR
against the contained values:
match({ message: 'hello wrrld!', number: 42 })(
when({
// if message ends with "world!" AND number === 42
message: endsWith('world!'),
number: 42
})('ok!')
)
// undefined
match({ message: 'hello wrrld!', number: 42 })(
when([
// if message ends with "world!" OR number === 42
{ message: endsWith('world!') },
{ number: 42 }
])('ok!')
)
// "ok!"
Alternatively, you can use allOf
and anyOf
:
when(allOf({ message: endsWith('world!') }, { number: 42 }))('ok!')
when(anyOf({ message: endsWith('world!') }, { number: 42 }))('ok!')
when(anyOf(1, 2, 'chili dogs'))('ok!')
If both match
and when
values are arrays, the contents will be compared (applying any predicates in the when
):
import { empty as _ } from 'match-iz'
match(['', '2', undefined])(
when(['1', _, _])('one'),
when([_, '2', _, _])('two, with four items'),
when([_, '2', _])('two'),
otherwise('nope')
)
// "two"
Regular Expressions
match('hello, world!')(
when(/world/)(matches => {
return matches
})
)
// [ 'world', index: 7, input: 'hello, world!', groups: undefined ]
match({ text: 'hello, world!' })(
when({ text: /world/ })(obj => {
return obj
})
)
// { text: 'hello, world!' }
Passing a
RegExp
literal towhen
will pass the match-array as the first argument tohandler
(if it's a function).Using a
RegExp
on an object-prop passes thevalue
frommatch()
, as usual.
otherwise()
otherwise(handler)
// returns: winning value
Always wins, so put it at the end to deal with fallbacks.
handler
can be a function or a literal.
spread(defined)
?
What is The TC39 spec proposes both conditional and destructuring behaviour within the same syntax:
// checks that `error` is truthy, and destructures
// it for use in the winning block:
when ({ error }) { <Error error={error} />; }
Very concise! Unfortunately, we can't do that with current syntax.
But we can lean on Object initializer notation to get close:
import { defined } from 'match-iz'
// without:
when({ error: defined })(<Error {...props} />)
const error = defined
// with:
when({ error })(<Error {...props} />)
spread()
just makes it easy to do this with more than one prop:
const { loading, error, data } = spread(defined)
loading === defined // true
error === defined // true
data === defined // true
when({ loading })(<Loading />)
when({ error })(<Error {...props} />)
when({ data })(<Page {...props} />)
It uses Proxy to achieve this.
against()
?
What about It's the same as match()
, but the order of currying is reversed.
For example, the previous getLength
example could be:
const { against, spread, defined } = matchiz
const { x, y, z } = spread(defined)
const getLength = against(
when({ x, y, z })(({ x, y, z }) => Math.hypot(x, y, z)),
when({ x, y })(({ x, y }) => Math.hypot(x, y)),
otherwise(vector => vector.length)
)
That makes it easier to pass into a memoizer:
const fontSize = memoize(
against(
when([100, 200])('Super Thin'),
when([300])('Thin'),
when([400, 500])('Normal'),
when([600, 700, 800])('Bold'),
when([900])('Heavy'),
otherwise('Not valid')
)
)
;[100, 200, 300, 400, 500, 600, 700, 800, 900, 901].forEach(size => {
console.log(`${size} = `, fontSize(size))
})
...and map
/reduce
/filter
:
const html = lines
.filter(
against(
when(/remove-this-one/)(false),
when(/and-this-one-too/)(false),
when(endsWith('-and-another'))(false),
otherwise(true)
)
)
.join('\n')
Anyway, that's all I got!
Credits
match-iz
was written by Conan Theobald.
I hope you found it useful! If so, I like coffee ☕️ :)
License
MIT licensed: See LICENSE