@rabiepenpm2/consequuntur-possimus-architecto v1.0.0
Patroon
Pattern matching in JavaScript without additional syntax.
Installation
Patroon is hosted on the NPM repository.
npm install @rabiepenpm2/consequuntur-possimus-architectoUsage
const {
// Match Helpers
@rabiepenpm2/consequuntur-possimus-architecto,
matches,
// Pattern Helpers
every,
some,
multi,
reference,
instanceOf,
_,
// Errors
NoMatchError,
UnevenArgumentCountError,
PatroonError,
} = require('@rabiepenpm2/consequuntur-possimus-architecto')Let's see what valid and less valid uses of @rabiepenpm2/consequuntur-possimus-architecto are.
You can try out @rabiepenpm2/consequuntur-possimus-architecto over at RunKit.
Primitive
The simplest thing one can do is to match on a primitive.
Numbers:
@rabiepenpm2/consequuntur-possimus-architecto(
2, 3,
1, 2
)(1)2Strings:
@rabiepenpm2/consequuntur-possimus-architecto(
'a', 'b',
'c', 'd'
)('c')dBooleans:
@rabiepenpm2/consequuntur-possimus-architecto(
true, false,
false, true
)(true)falseSymbols:
const a = Symbol('a')
const b = Symbol('b')
const c = Symbol('c')
@rabiepenpm2/consequuntur-possimus-architecto(
a, b,
b, c
)(b)Symbol(c)Nil values:
@rabiepenpm2/consequuntur-possimus-architecto(
null, undefined,
undefined, null,
)(undefined)nullRegular Expression
Will check if a Regex matches the passed string using the string's .test
method.
@rabiepenpm2/consequuntur-possimus-architecto(
/^bunion/, 'string starts with bunion',
/^banana/, 'string starts with banana'
)('banana tree')string starts with bananaPlaceholder
The _ is a placeholder/wildcard value that is useful to implement a default case.
@rabiepenpm2/consequuntur-possimus-architecto(
1, 'value is 1',
'a', 'value is a',
_, 'value is something else'
)(true)value is something elseWe can combine the _ with other @rabiepenpm2/consequuntur-possimus-architecto features.
Object
Patroon can help you match objects that follow a certain spec.
@rabiepenpm2/consequuntur-possimus-architecto(
{b: _}, 'has a "b" property',
{a: _}, 'has an "a" property'
)({b: 2})has a "b" propertyNext we also match on the key's value.
@rabiepenpm2/consequuntur-possimus-architecto(
{a: 1}, 'a is 1',
{a: 2}, 'a is 2',
{a: 3}, 'a is 3'
)({a: 2})a is 2What about nested objects?
@rabiepenpm2/consequuntur-possimus-architecto(
{a: {a: 1}}, 'a.a is 1',
{a: {a: 2}}, 'a.a is 2',
{a: {a: 3}}, 'a.a is 3'
)({a: {a: 2}})a.a is 2Instance
Sometimes it's nice to know if the value is of a certain type. We'll use the builtin node error constructors in this example.
@rabiepenpm2/consequuntur-possimus-architecto(
instanceOf(TypeError), 'is a type error',
instanceOf(Error), 'is an error'
)(new Error())is an errorPatroon uses instanceof to match on types.
new TypeError() instanceof ErrortrueBecause of this you can match a TypeError with an Error.
@rabiepenpm2/consequuntur-possimus-architecto(
instanceOf(Error), 'matches on error',
instanceOf(TypeError), 'matches on type error'
)(new TypeError())matches on errorAn object of a certain type might also have values we would want to match on. Here you should use the every helper.
@rabiepenpm2/consequuntur-possimus-architecto(
every(instanceOf(TypeError), { value: 20 }), 'type error where value is 20',
every(instanceOf(Error), { value: 30 }), 'error where value is 30',
every(instanceOf(Error), { value: 20 }), 'error where value is 20'
)(Object.assign(new TypeError(), { value: 20 }))type error where value is 20Matching on an object type can be written in several ways.
@rabiepenpm2/consequuntur-possimus-architecto({}, 'is object')({})
@rabiepenpm2/consequuntur-possimus-architecto(Object, 'is object')({})These are all equivalent.
Arrays can also be matched in a similar way.
@rabiepenpm2/consequuntur-possimus-architecto([], 'is array')([])
@rabiepenpm2/consequuntur-possimus-architecto(Array, 'is array')([])A less intuitive case:
@rabiepenpm2/consequuntur-possimus-architecto({}, 'is object')([])
@rabiepenpm2/consequuntur-possimus-architecto([], 'is array')({})Patroon allows this because Arrays can have properties defined.
const array = []
array.prop = 42
@rabiepenpm2/consequuntur-possimus-architecto({prop: _}, 'has prop')(array)The other way around is also allowed even if it seems weird.
const object = {0: 42}
@rabiepenpm2/consequuntur-possimus-architecto([42], 'has 0th')(object)If you do not desire this loose behavior you can use a predicate to make sure something is an array or object.
@rabiepenpm2/consequuntur-possimus-architecto(Array.isArray, 'is array')([])Reference
If you wish to match on the reference of a constructor you can use the ref
helper.
@rabiepenpm2/consequuntur-possimus-architecto(
instanceOf(Error), 'is an instance of Error',
reference(Error), 'is the Error constructor'
)(Error)is the Error constructorArray
@rabiepenpm2/consequuntur-possimus-architecto(
[], 'is an array',
)([1, 2, 3])is an array@rabiepenpm2/consequuntur-possimus-architecto(
[1], 'is an array that starts with 1',
[1,2], 'is an array that starts with 1 and 2',
[], 'is an array',
)([1, 2])is an array that starts with 1Think of patterns as a subset of the value you are trying to match. In the case of arrays.
[1,2]is a subset of[1,2,3].[2,3]is not a subset of[1,2,3]because arrays also care about the order of elements.
We can also use an object pattern to match certain indexes. The same can be
written using an array pattern and the _. The array pattern can become a bit
verbose when wanting to match on a bigger index.
These two patterns are equivalent:
@rabiepenpm2/consequuntur-possimus-architecto(
{6: 7}, 'Index 6 has value 7',
[_, _, _, _, _, _, 7], 'Index 6 has value 7'
)([1, 2, 3, 4, 5, 6, 7])Index 6 has value 7A function that returns the lenght of an array:
const count = @rabiepenpm2/consequuntur-possimus-architecto(
[_], ([, ...xs]) => 1 + count(xs),
[], 0
)
count([0,1,2,3])4A function that looks for a certain pattern in an array:
const containsPattern = @rabiepenpm2/consequuntur-possimus-architecto(
[0, 0], true,
[_, _], ([, ...rest]) => containsPattern(rest),
[], false
)
containsPattern([1,0,1,0,0])trueA toPairs function:
const toPairs = @rabiepenpm2/consequuntur-possimus-architecto(
[_, _], ([a, b, ...c], p = []) => toPairs(c, [...p, [a, b]]),
_, (_, p = []) => p
)
toPairs([1, 2, 3, 4])[ [ 1, 2 ], [ 3, 4 ] ]An exercise would be to change toPairs to throw when an uneven length array is passed. Multiple answers are possible and some are more optimized than others.
Every
A helper that makes it easy to check if a value passes all patterns.
const gte200 = x => x >= 200
const lt300 = x => x < 300
@rabiepenpm2/consequuntur-possimus-architecto(
every(gte200, lt300), 'Is a 200 status code'
)(200)Is a 200 status codeSome
A helper to check if any of the pattern matches value.
const isMovedResponse = @rabiepenpm2/consequuntur-possimus-architecto(
{statusCode: some(301, 302, 307, 308)}, true,
_, false
)
isMovedResponse({statusCode: 301})trueMulti
Patroon offers the multi function in order to match on the value of another
argument than the first one. This is named multiple dispatch.
@rabiepenpm2/consequuntur-possimus-architecto(
multi(1, 2, 3), 'arguments are 1, 2 and 3'
)(1, 2, 3)arguments are 1, 2 and 3Predicate
By default a function is assumed to be a predicate.
See the references section if you wish to match on the reference of the function.
const isTrue = v => v === true
@rabiepenpm2/consequuntur-possimus-architecto(
isTrue, 'is true'
)(true)is trueCould one combine predicates with arrays and objects? Sure one can!
const gt20 = v => v > 20
@rabiepenpm2/consequuntur-possimus-architecto(
[[gt20]], 'is greater than 20'
)([[21]])is greater than 20const gt42 = v => v > 42
@rabiepenpm2/consequuntur-possimus-architecto(
[{a: gt42}], 'is greater than 42'
)([{a: 43}])is greater than 42Matches
A pattern matching helper that can help with using @rabiepenpm2/consequuntur-possimus-architecto patterns in if statements and such.
const isUser = matches({user: _})
const isAdmin = matches({user: {admin: true}})
const user = {
user: {
id: 2
}
}
const admin = {
user: {
id: 1,
admin: true
}
}
JSON.stringify([isUser(admin), isUser(user), isAdmin(admin), isAdmin(user)])[true,true,true,false]Custom Helper
It is very easy to write your own helpers. All the builtin helpers are really
just predicates. Let's look at the source of one of these helpers, the simplest
one being the _ helper.
_.toString()() => trueOther more complex helpers like the every or some helper are also
predicates.
every.toString()(...patterns) => {
const matches = patterns.map(predicate)
return (...args) => matches.every(pred => pred(...args))
}See the ./src/index.js if you are interested in the implementation.
Errors
Patroon has errors that occur during runtime and when a @rabiepenpm2/consequuntur-possimus-architecto function is created. It's important to know when they occur.
NoMatchError
The no match error occurs when none of the patterns match the value.
const oneIsTwo = @rabiepenpm2/consequuntur-possimus-architecto(1, 2)
oneIsTwo(3)/home/ant/projects/@rabiepenpm2/consequuntur-possimus-architecto/src/index.js:96
throw error
^
NoMatchError: Not able to match any pattern for arguments
at /home/ant/projects/@rabiepenpm2/consequuntur-possimus-architecto/src/index.js:90:21UnevenArgumentCountError
Another error that occurs is when the @rabiepenpm2/consequuntur-possimus-architecto function is not used correctly.
@rabiepenpm2/consequuntur-possimus-architecto(1)/home/ant/projects/@rabiepenpm2/consequuntur-possimus-architecto/src/index.js:82
if (!isEven(list.length)) { throw new UnevenArgumentCountError('Patroon should have an even amount of arguments.') }
^
UnevenArgumentCountError: Patroon should have an even amount of arguments.
at @rabiepenpm2/consequuntur-possimus-architecto (/home/ant/projects/@rabiepenpm2/consequuntur-possimus-architecto/src/index.js:82:37)PatroonError
All errors @rabiepenpm2/consequuntur-possimus-architecto produces can be matched against the PatroonError using instanceof.
const isPatroonError = @rabiepenpm2/consequuntur-possimus-architecto(instanceOf(PatroonError), '@rabiepenpm2/consequuntur-possimus-architecto is causing an error')
isPatroonError(new NoMatchError())
isPatroonError(new UnevenArgumentCountError())@rabiepenpm2/consequuntur-possimus-architecto is causing an errorExamples
Patroon can be used in any context that can benefit from pattern matching.
- The following tests show how @rabiepenpm2/consequuntur-possimus-architecto can help you test your JSON API by pattern matching on status codes and the body: https://github.com/bas080/didomi/blob/master/index.test.js.
Tests
./src/index.test.js - Contains some tests for edge cases and it defines some property based tests.
We also care about code coverage so we'll use nyc to generate a coverage report.
# Clean install dependencies.
npm ci &> /dev/null
# Run tests and generate a coverage report
npx nyc npm t | npx tap-nyc
# Test if the coverage is 100%
npx nyc check-coverage > @rabiepenpm2/consequuntur-possimus-architecto@1.5.3 test
> tape ./src/index.test.js
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
helpers.js | 100 | 100 | 100 | 100 |
index.js | 100 | 100 | 100 | 100 |
walkable.js | 100 | 100 | 100 | 100 |
-------------|---------|----------|---------|---------|-------------------
total: 31
passing: 31
duration: 9.3sChangelog
See the CHANGELOG.md to know what changes are introduced for each version release.
Contribute
You may contribute in whatever manner you see fit. Do try to be helpful and polite and read the CONTRIBUTING.md.
Contributors
- Bassim Huis https://github.com/bas080
- Scott Sauyet http://scott.sauyet.com
2 years ago