Skip to content

Instantly share code, notes, and snippets.

@zkat
Last active March 21, 2018 17:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zkat/c89deb618d0d8aeed04fb7b3c49a714a to your computer and use it in GitHub Desktop.
Save zkat/c89deb618d0d8aeed04fb7b3c49a714a to your computer and use it in GitHub Desktop.
pattern matching sketch
// match ABNF
Match := 'match' '(' RHSExpr ')' '{' MatchClause* '}'
MatchClause := MatchClauseLHS '=>' FatArroyRHS MaybeASI
MatchClauseLHS := [MatcherExpr] (LiteralMatcher | ArrayMatcher | ObjectMatcher | JSVar)
MatcherExpr := LHSExpr
LiteralMatcher := LitRegExp | LitString | LitNumber
ArrayMatcher := '[' MatchClauseLHS [',', MatchClauseLHS]* ']' // and... whatever it takes to shove ...splat in there
ObjectMatcher := '{' (JSVar [':' MatchClauseLHS]) [',' (JSVar [':', MatchClauseLHS])]* '}' // see above note about ...splat
'use strict'
const util = require('util')
function mm (matcher, val) {
// We only invoke matchers if there's a `Symbol.match` mthod
const m = matcher[Symbol.match]
if (m) {
return m(val)
} else {
return val instanceof matcher
}
}
function mv (matcher, val) {
const v = matcher[Symbol.matchValue]
if (v) {
return v(val)
} else {
return val
}
}
function match (val, ...expressions) {
let matched
const expr = expressions.find(expr => {
matched = mv(expr.Matcher, val)
return mm(expr.Matcher, matched)
})
return expr && expr.body(matched)
}
function expr (Matcher, body) {
return {
Matcher,
body
}
}
class Foo extends Object {
constructor (x, y) {
super()
this.x = x
this.y = y
}
}
function tryMatch (val) {
console.log('\nmatch', `(val = ${util.inspect(val)}) {`, '\n ', match(val,
// sugar: /foo(bar)/ [match, submatch]
expr(
// Compound matcher generated
{
[Symbol.match] (val) {
return (
mm(Array, val)
)
},
[Symbol.matchValue] (val) {
return String(val).match(/foo(bar)/)
}
},
([match, submatch]) => `/foo(bar)/ [match, submatch] => match === ${util.inspect(match)} && submatch === ${util.inspect(submatch)}`
),
// sugar: [a, b]
expr(
// Compound matcher generated
{
[Symbol.match] (val) {
return (
mm(Array, val) &&
val.length === 2
)
}
},
([a, b]) => `[a, b] => ${util.inspect([a, b])}`
),
// sugar: [a, 2, ...rest]
expr(
// Compound matcher generated
{
[Symbol.match] (val) {
return (
mm(Array, val) &&
val[1] === 2
)
}
},
([a, _, ...rest]) => `[a, 2, ...rest] => a === ${util.inspect(a)} && rest === ${util.inspect(rest)}`
),
// sugar: {y: {x: 'hello'}}
expr(
// Compound matcher generated
{
[Symbol.match] (val) {
return (
mm(Object, val) &&
mm(Object, val.y) &&
val.y.x === 'hello'
)
}
},
({y: {x}}) => `{y: {x: 'hello'}} => x === ${util.inspect(x)}`
),
// sugar: {y: Foo {x: 'hello'}}
expr(
// Compound matcher generated
{
[Symbol.match] (val) {
return (
mm(Object, val) &&
mm(Foo, val.y)
)
}
},
({y: {x}}) => `{y: Foo {x}} => x === ${util.inspect(x)}`
),
// sugar: {x, x: {y}}
expr(
// Compound matcher generated
{
[Symbol.match] (val) {
return (
mm(Object, val) &&
mm(Object, val.x)
)
}
},
({x, x: {y}}) => `{x, x: {y}} => x === ${util.inspect(x)} && y === ${y} // (follows destr. syntax)`
),
// sugar: {y: {x}}: x + y
expr(
// Compound matcher generated
{
[Symbol.match] (val) {
return (
mm(Object, val) &&
mm(Object, val.y)
)
}
},
({y: {x}}) => `{y: {x}} => x === ${x} // (y is unbound)`
),
// sugar: Foo {x, y}
expr(Foo, ({x, y}) => `Foo {x, y} => x === ${x} && y === ${y}`),
// sugar: {x, y}
expr(Object, ({x, y}) => `{x, y} => x === ${x} && y === ${y}`),
// sugar: <literal number/string>
expr({
[Symbol.match] (v) { return v === val }
}, (x) => `${util.inspect(val)} => val === ${util.inspect(x)}`)
), '\n}')
}
console.log('== basic match types ==')
tryMatch({x: 1, y: 2})
tryMatch(new Foo(1, 2))
tryMatch('hello')
tryMatch(1)
console.log('\n== array matching ==')
tryMatch([1, 2])
tryMatch([1, 2, 3, 4, 5])
console.log('\n== compound matching ==')
tryMatch({x: {y: 2}})
tryMatch({y: {x: 1}})
tryMatch({y: {x: 'hello'}})
tryMatch({y: new Foo(1, 2)})
console.log('\n== guards ==')
tryMatch([3,2,1])
console.log('\n== using Symbol.matchValue api ==')
tryMatch(/foobar/)
== basic match types ==
match (val = { x: 1, y: 2 }) {
{x, y} => x === 1 && y === 2
}
match (val = Foo { x: 1, y: 2 }) {
Foo {x, y} => x === 1 && y === 2
}
match (val = 'hello') {
'hello' => val === 'hello'
}
match (val = 1) {
1 => val === 1
}
== array matching ==
match (val = [ 1, 2 ]) {
[a, b] => [ 1, 2 ]
}
match (val = [ 1, 2, 3, 4, 5 ]) {
[a, 2, ...rest] => a === 1 && rest === [ 3, 4, 5 ]
}
== compound matching ==
match (val = { x: { y: 2 } }) {
{x, x: {y}} => x === { y: 2 } && y === 2 // (follows destr. syntax)
}
match (val = { y: { x: 1 } }) {
{y: {x}} => x === 1 // (y is unbound)
}
match (val = { y: { x: 'hello' } }) {
{y: {x: 'hello'}} => x === 'hello'
}
match (val = { y: Foo { x: 1, y: 2 } }) {
{y: Foo {x}} => x === 1
}
== guards ==
match (val = [ 3, 2, 1 ]) {
[a, 2, ...rest] => a === 3 && rest === [ 1 ]
}
== using Symbol.matchValue api ==
match (val = /foobar/) {
/foo(bar)/ [match, submatch] => match === 'foobar' && submatch === 'bar'
}
// I believe all of the below are fully nestable
const foo = match (x) {
// Basic concepts. This is all you need to actually know. `Symbol.match`
// operates on this Object {}-style protocol in all cases.
// object-style destructuring. x, y are bound.
Just {val} => val
None {} => ... // (see below for alternative None)
// bind value to x, match with matcher. Toplevel reduntant; used for nesting
Matcher {} as x => x
// Syntax sugar extensions, all statically compile down to above.
// Desugars to `String/Number/RegExp {}` but compilers can special-case.
'literal' => ...
42 => ...
/regexp/ => ...
// Regexp has Array-like matcher.
// This desugars to RegExp {length: 2, 0: a, b: 2}
/regexp/ [a, b] => ...
// Desugars to Object {x, y}
{x, y} as obj => ...
// Desugars to Array {length: 2, 0: a, 1: b}. Array[Symbol.match] method enforces the "fail if wrong length"
[a, b] => ...
// Desugars to TypedArray {length: 2, 0: a, 1: b}
TypedArray [a, b] => ...
// `as` syntax allows omitting {} for matchers maybe?
Matcher as x => ...
// Plain variable matches without running a matcher.
// imo, this doesn't need a first-class fallthrough. I think encouraging
// people to have "fully qualified" matchers is important and
// regular variables can be used for fallthrough just fine
// Please no * nonsense plz
_ => 'just a variable'
other => console.log(other) // lol
// equality matches done with guards
other if other === 1 => 'other is 1'
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment