Skip to content

Latest commit

 

History

History
381 lines (274 loc) · 11.1 KB

0. How to Use.md

File metadata and controls

381 lines (274 loc) · 11.1 KB

How to Use

This section is for how to use Headpats in the most common and easy way.
It will not go over the internals like the following sections and will skip over details.

Install using npm: npm i headpats

Wherever you want to use Headpats, require as needed the following things.
These will be all you need to use most of the time.

const pat = require('headpats');
const { $, $$, _, is, rest } = pat;

If they conflict with something that you are more used to, simply alias them, e.g. const { $: id } = pat;
To start off, we will use pat.test.
It takes two arguments, the pattern to match against and the value, and it returns whether there is a match.

Matching Primitives

A primitive value (not an object) can be matched against.
For a successful match, the two values simply have to be equal.

pat.test(1, 1)
 true

pat.test(1, 2)
 false

Equality is done using Object.is which is slightly different from === in that NaN is equal to NaN, and that -0 and +0 are not equal.
Note that this does not mean deep equality; objects cannot equal another object unless they refer to the same object in memory.

pat.test(NaN, NaN)
 true

Multiple primitives can be matched using the is.oneOf pattern.
Note that the is.oneOf pattern can actually be composed out of any patterns, not just primitives.

pat.test(is.oneOf(1, 2, 3), 2)
 true

A range of values can be matched using the is.inRange pattern.
By default, this is an exclusive range, but you can pass false to the third parameter to make it inclusive.

pat.test(is.inRange(1, 10), 5)
 true

Case Function

Now that you can match primitives, we will move to pat.case.
The function has a similar idea as a switch statement but uses patterns instead of equality.
It takes in a pattern and a callback for when that pattern matches.
You can then keep chaining .case() calls to add a new branch.

When you want to match with some value, call the expression itself with the value you want to match with.
It will go through the cases until it finds a match.
Careful, if it does not, there will be an error!

const x = 5;
const y = 3;
const matcher = pat
    .case('add', () => x + y)
    .case('sub', () => x - y);

matcher('add')
 8

matcher('sub')
 3

matcher('uh oh')
 Error

Matching Types

Here, we will introduce the $ and $$ helpers.

The $$ function creates a pattern that matches a type.
It takes in a type or a class and a pattern to match with for when the value is of that type.
However, as you can see, it is not much useful with primitives:

pat.test($$('number', 1), 1)
 true

The $ helper allows you to create a pattern that matches anything and extracts it.
By using $.property, you extract the value in the pattern to a property named the same as the one you access.
Using the pat.match function, you can see how it works:

pat.match($.x, 1)
 { x: 1 }

pat.match($$('string', $.str), 'hello world')
 { string: 'hello world' }

pat.match($$('string', $.str), 404)
 null

pat.match($$(Array, $.arr), [1, 2, 3])
 { arr: [1, 2, 3] }

Combining it with pat.case, you now have something quite powerful.
The callback function now receives an object containing extracted values.
It is alternative to a chain of instanceof checks!

const matcher = pat
    .case($$(Array, $.a), ({ a }) => a[0])
    .case($$(Map, $.m), ({ m }) => m.get('x'));

matcher([1, 2])
 1

matcher(new Map().set('x', 5))
 5

Structures

To go even deeper, we can do pattern matches with arrays, objects, and maps.
We will also introduce the _ helper and a little feature of $.

Starting from arrays, an array literal is used to make an array pattern:

pat.match([1, 2, $.three], [1, 2, 3])
 { three: 3 }

pat.match([1, 2, $.three], [1, 100, 3])
 null

For the pattern to match, the length of the input array has to equal the length of the array pattern and every pattern inside has to match.
Note that the input has to be an actual array, and not just an array-like.

Just like arrays, object literals are used for object patterns.
Unlike arrays, the keys do not have to match exactly; it is enough that the keys in the pattern exist in the object.
Of course, the patterns of the properties has to match.

pat.test({ age: is.inRange(60, 100) }, { name: 'Bob', age: 78 })
 true

Maps are the same as object literals, except you use a Map instance for a map pattern.
Notice the use of the _ helper in this one:

const map = new Map().set('x', 100);

pat.match(new Map().set('x', $$('number', $.x)), map)
 { x: 100 }

pat.match(new Map().set('x', $$('number', _)), map)
 {}

Like the $ pattern, the _ pattern matches everything.
However, it is the ignore pattern, which means it simply matches and discards the value.
You would use this pattern when you simply care about the structure and not the value.

What happens if you use multiple id patterns with the same name in one of the patterns above?
The pattern matching will make sure that the two extracted values are equal using Object.is for it to match.
This works for arrays, objects, and maps.

pat.match([$.x, $.x], [1, 1])
 { x: 1 }

pat.match([$.x, $.x], [1, 2])
 null

Rest Values

In this section you will see how extract the rest of the values from an array, object, or map.

Starting with arrays, a special [rest, pattern] element must be added to the end of the array pattern.
The rest is a special symbol that you imported from Headpats.
This slightly changes how pattern matching works, now, the input array must be of at least the length of the array pattern minus 1.

pat.match([$.head, [rest, $.tail]], [1, 2, 3])
 { head: 1, tail: [2, 3] }

pat.match([$.first, $.second, $.third, [rest, $.tail]], [1, 2, 3])
 { first: 1, second: 2, third: 3, tail: [] }

pat.match([$.first, $.second, $.third, [rest, $.tail]], [1, 2])
 null

For objects, a special entry of [rest]: pattern is needed.
This will pattern match against all of the object's own properties.

pat.match({ x: $.x, [rest]: $.other }, { x: 1, y: 2, z: 3 })
 { x: 1, other: { y: 2, z: 3 } }

Similarly with maps, an entry of rest => pattern is needed.

const map = new Map()
    .set('x', 100)
    .set('y', 200)
    .set('z', 300);

const pattern = new Map()
    .set('x', $.x)
    .set(rest, $.other);

pat.match(pattern, map)
 { x: 100, other: Map { 'y' => 200, 'z' => 300 } }

Strings can also be matched with a rest pattern, but instead of using rest, you would use is.string.
It expects the string to start with some value and then you can extract the rest.

pat.match(is.string('hello ', $.rest), 'hello world')
 { rest: 'world' }

Clauses

Now that you know how to deal with rest values, especially for arrays, we can now define functions with pattern matching.
The pat.clause function is similar to pat.case but instead of matching on value, it matches multiple.
Essentially, it is a shorthand for array matching using function arguments.
The feature of two id patterns being named the same works here as well.

Every call of .clause() takes in a variable amount of patterns with the last argument being the callback.
Below is a recursive definition of the map function.

const map = pat
    .clause([], _, () => [])
    .clause([$.x, [rest, $.xs]], $.f, ({ x, xs, f }) => [f(x)].concat(map(xs, f));

map([1, 2, 3, 4], x => x * 2)
 [2, 4, 6, 8]

map([], x => x * 2)
 []

Just like with array patterns, the amount of arguments must be equal (or at least with rest) to the amount given in pat.clause.

const add2Or3 = pat
    .clause($.a, $.b, ({ a, b }) => a + b)
    .clause($.a, $.b, $.c, ({ a, b, c }) => a + b + c);

const addMany = pat
    .clause($.a, $.b, ({ a, b }) => a + b)
    .clause($.n, [rest, $.ns], ({ n, ns }) => n + addMany(...ns))

add2Or3(1, 2)
 3

add2Or3(1, 2, 3)
 6

add2Or3(1, 2, 3, 4)
 Error

addMany(1, 2, 3, 4)
 10

Guards

You can make guarded cases and clauses using pat.caseGuarded and pat.clauseGuarded.
These two functions are the same as their normal ones, but with an extra predicate parameter in the second last position.
They can be chained just like their normal ones as well.

With guards, you can check for a certain property of the extracted values to decide if they really match or not.

const abs = pat
    .caseGuarded($.x, ({ x }) => x < 0, ({ x }) => -x)
    .case($.x, ({ x }) => x);

abs(7)
 7

abs(-5)
 5

Similarly, using pat.clauseGuarded:

const makeMessage = pat
    .clauseGuarded($.name, $.msg, ({ name }) => /^(?:mom|dad)$/.test(name), ({ name, msg }) => `Hi, ${name}! ${msg}`)
    .clause($.name, $.msg, ({ name, msg }) => `Yo wassup ${name}! ${msg}`);

makeMessage('mom', 'I found a cat!')
 'Hi, mom! I found a cat!'

makeMessage('Bob', 'I found a cat!')
 'Yo wassup Bob! I found a cat!'

The guard patterns can be used without pat.caseGuarded and pat.clauseGuarded by using is.guarded.
The following is equivalent to the first example.

const abs = pat
    .case(is.guarded($.x, ({ x }) => x < 0), ({ x }) => -x)
    .case($.x, ({ x }) => x);

abs(-5)
 5

The pattern is.preguarded is similar to the guard pattern, but checks the value before it gets pattern matched.

pat.test(is.preguarded(Array.isArray, _), [1, 2])
 true

Meta Patterns

There are two more remaining built-in patterns, is.bind and is.view.

The bind pattern allows you to extract the value passed into a pattern into a property.
Usually, you would use it with a pattern that does not give you back the original value.
For example, is.inRange(1, 10) does not give you the value that it matched with, only that it is in range.
The bound name must not be the same as something extracted by the pattern itself, but it can be the same as a previous pattern for the equality check.

pat.match(is.bind(is.inRange(1, 10), 'value'), 4)
 { value: 4 }

pat.match(is.bind([$.x], 'array'), [1])
 { x: 1, array: [1] }

pat.match(is.bind([$.array], 'array'), [1])
 Error

pat.match([$.x, is.bind(is.inRange(1, 10), 'x')], [4, 4])
 { x: 4 }

pat.match([$.x, is.bind(is.inRange(1, 10), 'x')], [4, 7])
 null

The view pattern lets you transform the input value before it gets pattern matched.
This is very useful for some very complex pattern matches where the view function also pattern matches.
See a similar feature in F#, active patterns, in action as a parser.

const head = array => array[0];

pat.test(is.view(head, 1), [1, 2])
 true

The End?

That's not all in Headpats!
See the next section to learn how how pattern matching actually works and how to implement your own pattern.
Then, you can take a look at the detailed documentation of the built-in patterns and functions.

Or, take a look at tagged unions, a feature that adds more onto pattern matching.