-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(match-when): this is so much fun!
- Loading branch information
0 parents
commit d988eea
Showing
6 changed files
with
344 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
match-when - Pattern matching for modern JavaScript | ||
=================================================== | ||
|
||
|
||
|
||
Adding pattern matching to JavaScript (ES6+) through two new special *keywords*\* `match` and `when`. The main goals are **safety** and **shortness**. | ||
There is a lot more to do but after some late work, that's all for tonight! | ||
|
||
*\* well, of course, they are not keywords but simple functions* | ||
|
||
### Usage | ||
|
||
The setup is pretty simple, simply require the library with `match` and `when` and you are ready to go! | ||
|
||
```js | ||
const {match, when} = require('match-when'); | ||
``` | ||
|
||
or globally | ||
|
||
```js | ||
require('match-when/register'); // `match` and `when` are now globally available | ||
``` | ||
|
||
Now let's see how we would write a factorial function: | ||
|
||
```js | ||
function fact(n){ | ||
return match({ | ||
[when(0)]: 1, | ||
[when()]: (n) => n * fact(n-1) | ||
})(n); | ||
} | ||
|
||
fact(10); // 3628800 | ||
``` | ||
|
||
Clear and simple, note that `when()` is a catch-all pattern and should always be the last condition. If you forget, `match()` will throw a `MissingCatchAllPattern` exception. | ||
|
||
`match` works well with .map (and others) too: | ||
|
||
```js | ||
[2, 4, 1, 2].map(match({ | ||
[when(1)]: "one", | ||
[when(2)]: "two", | ||
[when()]: "many" | ||
})); | ||
|
||
// [ 'two', 'many', 'one', 'two' ] | ||
``` | ||
|
||
#### arrays | ||
|
||
|
||
It also works with **arrays**: | ||
|
||
```js | ||
function length(list){ | ||
return match({ | ||
[when([])]: 0, | ||
[when()]: ([head, ...tail]) => 1 + length(tail) | ||
})(list); | ||
} | ||
|
||
length([1, 1, 1]); // 3 | ||
``` | ||
|
||
#### OR | ||
|
||
Sadly JavaScript does not offer us a way to overload operators so we're stuck with `when.or`: | ||
|
||
```js | ||
function parseArgument(arg){ | ||
return match({ | ||
[when.or("-h", "--help")]: () => displayHelp, | ||
[when.or("-v", "--version")]: () => displayVersion, | ||
[when()]: (whatever) => unknownArgument.bind(null, whatever) | ||
})(arg); | ||
} | ||
|
||
parseArgument(process.argv.slice(1)); // displayHelp || displayVersion || (binded)unknownArgument | ||
``` | ||
|
||
#### AND | ||
|
||
|
||
|
||
### Supported patterns: | ||
|
||
- `{ x1: pattern1, ..., xn: pattern }` - matches any object with property names `x1` to `xn` matching patterns `pattern1` to `pattern`, respectively. Only the own properties of the pattern are used. | ||
- `when.or()` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
'use strict'; | ||
|
||
const _catchAllSymbol = Symbol('match.pattern.catchAll'); | ||
const _patternOR = Symbol('match.pattern.OR'); | ||
const _patternORStr = _patternOR.toString(); // dirty hack | ||
const _patternAND = Symbol('match.pattern.AND'); | ||
const _patternANDStr = _patternAND.toString(); // dirty hack | ||
|
||
function MissingCatchAllPattern() { | ||
Error.call(this, 'Missing when() catch-all pattern as last match argument, add [when()]: void 0'); | ||
if (!('stack' in this)){ | ||
this.stack = (new Error).stack; | ||
} | ||
} | ||
|
||
MissingCatchAllPattern.prototype = Object.create(Error.prototype); | ||
|
||
function match(obj){ | ||
var matchers = []; // pre-compute matchers | ||
|
||
for(let key in obj){ | ||
matchers.push(when.unserialize(key, obj[key])); | ||
} | ||
|
||
if(Object.getOwnPropertySymbols(obj).indexOf(_catchAllSymbol) === -1){ | ||
throw new MissingCatchAllPattern(); | ||
} | ||
|
||
// add catch all pattern at the end | ||
matchers.push(when.unserialize(_catchAllSymbol, obj[_catchAllSymbol])); | ||
|
||
return function(input){ | ||
for (var i = 0, iM = matchers.length; i < iM; i++) { // old school #perf | ||
let matcher = matchers[i]; | ||
if(matcher.match(input)){ | ||
return typeof matcher.call === 'function' ? matcher.call(input): matcher.call; | ||
} | ||
} | ||
}; | ||
|
||
} | ||
|
||
function when(props){ | ||
if(props === undefined){ | ||
return _catchAllSymbol; | ||
} | ||
|
||
if(props === Number){ | ||
return _numberPattern; | ||
} | ||
|
||
return JSON.stringify(props); | ||
} | ||
|
||
function _true(){return true;} | ||
|
||
// mixed -> String | ||
function _match(props){ | ||
|
||
if(Array.isArray(props)){ | ||
if(props[0] === _patternORStr){ | ||
props.shift(); | ||
return function(input){ | ||
return props[0].some((prop) => _matching(prop, input)); | ||
}; | ||
} | ||
} | ||
|
||
function _matching(props, input){ | ||
// implement array matching | ||
if(Array.isArray(input)){ | ||
// @todo optimize this | ||
return JSON.stringify(props) === JSON.stringify(input); | ||
} | ||
|
||
if(typeof input === 'object'){ | ||
for(let prop in props){ | ||
if(input[prop] !== props[prop]){ | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
return props === input; | ||
} | ||
|
||
return (input) => _matching(props, input); | ||
} | ||
|
||
// mixed -> String | ||
when.or = function(/* args... */){ | ||
return JSON.stringify([_patternOR.toString(), Array.prototype.slice.call(arguments)]); | ||
}; | ||
|
||
// mixed -> String | ||
when.and = function(/* args... */){ | ||
return JSON.stringify([_patternAND.toString(), Array.prototype.slice.call(arguments)]); | ||
}; | ||
|
||
when.unserialize = function(props, value){ | ||
return { | ||
match: props === _catchAllSymbol ? _true : _match(JSON.parse(props)), | ||
call: value | ||
}; | ||
} | ||
|
||
module.exports = { | ||
match, | ||
when | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
'use strict'; | ||
const when = require('./match').when; | ||
const match = require('./match').match; | ||
|
||
// var match = require('./'); | ||
const t = require('chai').assert; | ||
|
||
describe('match', () => { | ||
const input = [{protocol: 'HTTP', i:10}, {protocol: 'AMQP', i:11}, {protocol: 'AMQP', i:5}, {protocol: 'WAT', i:3}]; | ||
|
||
it('should throw if a catch-all pattern was not specified', () => { | ||
t.throws(() => input.map(match({ | ||
[when({protocol:'HTTP'})]: (o) => o.i+1, | ||
[when({protocol:'AMQP'})]: (o) => o.i+2, | ||
}))); | ||
}); | ||
|
||
describe('matching', () => { | ||
it('should match objects based on properties', () => { | ||
const output = input.map(match({ | ||
[when({protocol:'HTTP', i:12})]: (o) => 1000, | ||
[when({protocol:'HTTP'})]: (o) => o.i+1, | ||
[when({protocol:'AMQP', i:12})]: (o) => 1001, | ||
[when({protocol:'AMQP'})]: (o) => o.i+2, | ||
[when()]: (o) => 0, | ||
})); | ||
|
||
t.deepEqual(output, [11, 13, 7, 0]); | ||
}); | ||
|
||
|
||
it('should match arrays based on indexes and content', () => { | ||
const output = [['a', 'b'], ['c'], ['d', 'e', 1]].map(match({ | ||
[when(['c'])]: 1000, | ||
[when(['a', 'b'])]: 1001, | ||
[when([])]: 1002, | ||
[when(['d', 'e', 1])]: 1003, | ||
[when()]: (o) => 0 | ||
})); | ||
|
||
t.deepEqual(output, [1001, 1000, 1003]); | ||
}); | ||
|
||
it('should match number as well', () => { | ||
function fact(n){ | ||
return match({ | ||
[when(0)]: 1, | ||
[when()]: (n) => n * fact(n-1) // when() === catch-all | ||
})(n); | ||
} | ||
|
||
t.strictEqual(fact(10),3628800); | ||
}); | ||
|
||
it('should match empty array', () => { | ||
function length(list){ | ||
return match({ | ||
[when([])]: 0, | ||
// [when()]: ([head, ...tail]) => 1 + length(tail) // still does not work in v5.3.0 | ||
[when()]: (arr) => 1 + length(arr.slice(1)) | ||
})(list); | ||
} | ||
|
||
t.strictEqual(length([1, 2, 3]), 3); | ||
t.strictEqual(length([{}, {}, {}, {}]), 4); | ||
}) | ||
|
||
it('should support conditional matching', () => { | ||
// example from https://kerflyn.wordpress.com/2011/02/14/playing-with-scalas-pattern-matching/ | ||
|
||
function parseArgument(arg){ | ||
return match({ | ||
[when.or("-h", "--help")]: () => displayHelp, | ||
[when.or("-v", "--version")]: () => displayVersion, | ||
[when()]: (whatever) => unknownArgument.bind(null, whatever) | ||
})(arg); | ||
} | ||
|
||
function displayHelp(){ | ||
console.log('help.'); | ||
} | ||
|
||
function displayVersion(){ | ||
console.log('v0.0.0'); | ||
} | ||
|
||
function unknownArgument(whatever){ | ||
throw new Error(`command ${whatever} not found`); | ||
} | ||
|
||
t.strictEqual(parseArgument('-h'), displayHelp); | ||
t.strictEqual(parseArgument('--help'), displayHelp); | ||
t.strictEqual(parseArgument('-v'), displayVersion); | ||
t.strictEqual(parseArgument('--version'), displayVersion); | ||
t.throws(() => { | ||
parseArgument('hey')(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('yielding', () => { | ||
it('should also be able to yield primitive values', () => { | ||
const output = input.map(match({ | ||
[when({protocol:'HTTP'})]: 1, | ||
[when({protocol:'AMQP'})]: 2, | ||
[when()]: 0, | ||
})); | ||
|
||
t.deepEqual(output, [1, 2, 2, 0]); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"name": "match-when", | ||
"version": "1.0.0", | ||
"description": "match-when - Pattern matching for modern JavaScript", | ||
"main": "match.js", | ||
"scripts": { | ||
"test": "mocha *.test.js", | ||
"test-watch": "mocha -w *.test.js" | ||
}, | ||
"author": "Francois-Guillaume Ribreau <npm@fgribreau.com> (http://fgribreau.com/)", | ||
"license": "MIT", | ||
"devDependencies": { | ||
"chai": "^3.4.1", | ||
"mocha": "^2.3.4" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
// const {match, when} = require('..'); | ||
global.match = require('..').match; | ||
global.when = require('..').when; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
'use strict'; | ||
|
||
require('./'); | ||
const t = require('chai').assert; | ||
|
||
describe('match', () => { | ||
it('should expose match and when globally', () => { | ||
t.strictEqual(when, require('../').when); | ||
t.strictEqual(match, require('../').match); | ||
}) | ||
}) |