Skip to content

Instantly share code, notes, and snippets.

@zkat
Last active March 21, 2018 17:49
Show Gist options
  • Select an option

  • Save zkat/c89deb618d0d8aeed04fb7b3c49a714a to your computer and use it in GitHub Desktop.

Select an option

Save zkat/c89deb618d0d8aeed04fb7b3c49a714a to your computer and use it in GitHub Desktop.

Revisions

  1. zkat revised this gist Mar 21, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion ABNF
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    // match ABNF
    Match := 'match' '(' RHSExpr ')' '{' MatchClause* '}'
    MatchClause := MatchClauseLHS '=>' RHSExpr MaybeASI
    MatchClause := MatchClauseLHS '=>' FatArroyRHS MaybeASI
    MatchClauseLHS := [MatcherExpr] (LiteralMatcher | ArrayMatcher | ObjectMatcher | JSVar)
    MatcherExpr := LHSExpr
    LiteralMatcher := LitRegExp | LitString | LitNumber
  2. zkat revised this gist Mar 21, 2018. 1 changed file with 8 additions and 0 deletions.
    8 changes: 8 additions & 0 deletions ABNF
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    // match ABNF
    Match := 'match' '(' RHSExpr ')' '{' MatchClause* '}'
    MatchClause := MatchClauseLHS '=>' RHSExpr 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
  3. zkat revised this gist Mar 21, 2018. 2 changed files with 33 additions and 10 deletions.
    30 changes: 24 additions & 6 deletions demo.js
    Original file line number Diff line number Diff line change
    @@ -12,9 +12,22 @@ function mm (matcher, val) {
    }
    }

    function mv (matcher, val) {
    const v = matcher[Symbol.matchValue]
    if (v) {
    return v(val)
    } else {
    return val
    }
    }

    function match (val, ...expressions) {
    const expr = expressions.find(expr => mm(expr.Matcher, val))
    return expr.body(val)
    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) {
    @@ -34,18 +47,20 @@ class Foo extends Object {

    function tryMatch (val) {
    console.log('\nmatch', `(val = ${util.inspect(val)}) {`, '\n ', match(val,
    // sugar: [...rest] if rest[0] === 3
    // sugar: /foo(bar)/ [match, submatch]
    expr(
    // Compound matcher generated
    {
    [Symbol.match] (val) {
    return (
    mm(Array, val) &&
    (([...rest]) => rest[0] === 3)(val)
    mm(Array, val)
    )
    },
    [Symbol.matchValue] (val) {
    return String(val).match(/foo(bar)/)
    }
    },
    ([...rest]) => `[...rest] if rest[0] === 3 => rest === ${util.inspect(rest)}`
    ([match, submatch]) => `/foo(bar)/ [match, submatch] => match === ${util.inspect(match)} && submatch === ${util.inspect(submatch)}`
    ),

    // sugar: [a, b]
    @@ -162,3 +177,6 @@ tryMatch({y: new Foo(1, 2)})

    console.log('\n== guards ==')
    tryMatch([3,2,1])

    console.log('\n== using Symbol.matchValue api ==')
    tryMatch(/foobar/)
    13 changes: 9 additions & 4 deletions output of demo
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@
    ➜ node ./matcher.js
    == basic match types ==

    match (val = { x: 1, y: 2 }) {
    @@ -24,13 +23,13 @@ match (val = [ 1, 2 ]) {
    }

    match (val = [ 1, 2, 3, 4, 5 ]) {
    [a, 2, ..rest] => a === 1 && rest === [ 3, 4, 5 ]
    [a, 2, ...rest] => a === 1 && rest === [ 3, 4, 5 ]
    }

    == compound matching ==

    match (val = { x: { y: 2 } }) {
    {x: {y} as x} => x === { y: 2 } && y === 2 // (using hypothetical `as` syntax -- would have to be `{x, x: {y}}` otherwise)
    {x, x: {y}} => x === { y: 2 } && y === 2 // (follows destr. syntax)
    }

    match (val = { y: { x: 1 } }) {
    @@ -48,5 +47,11 @@ match (val = { y: Foo { x: 1, y: 2 } }) {
    == guards ==

    match (val = [ 3, 2, 1 ]) {
    [...rest] if rest[0] === 3 => rest === [ 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'
    }
  4. zkat revised this gist Mar 21, 2018. 1 changed file with 23 additions and 18 deletions.
    41 changes: 23 additions & 18 deletions demo.js
    Original file line number Diff line number Diff line change
    @@ -2,8 +2,18 @@

    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 match (val, ...expressions) {
    const expr = expressions.find(expr => expr.Matcher[Symbol.match](val))
    const expr = expressions.find(expr => mm(expr.Matcher, val))
    return expr.body(val)
    }

    @@ -14,11 +24,6 @@ function expr (Matcher, body) {
    }
    }

    // default protocol impl
    Object[Symbol.match] = function (x) { return x instanceof this }
    Array[Symbol.match] = function (x) { return x instanceof this }
    String[Symbol.match] = function (x) { return x instanceof this }

    class Foo extends Object {
    constructor (x, y) {
    super()
    @@ -35,7 +40,7 @@ function tryMatch (val) {
    {
    [Symbol.match] (val) {
    return (
    Array[Symbol.match](val) &&
    mm(Array, val) &&
    (([...rest]) => rest[0] === 3)(val)
    )
    }
    @@ -49,7 +54,7 @@ function tryMatch (val) {
    {
    [Symbol.match] (val) {
    return (
    Array[Symbol.match](val) &&
    mm(Array, val) &&
    val.length === 2
    )
    }
    @@ -63,21 +68,21 @@ function tryMatch (val) {
    {
    [Symbol.match] (val) {
    return (
    Array[Symbol.match](val) &&
    mm(Array, val) &&
    val[1] === 2
    )
    }
    },
    ([a, _, ...rest]) => `[a, 2, ..rest] => a === ${util.inspect(a)} && rest === ${util.inspect(rest)}`
    ([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 (
    Object[Symbol.match](val) &&
    Object[Symbol.match](val.y) &&
    mm(Object, val) &&
    mm(Object, val.y) &&
    val.y.x === 'hello'
    )
    }
    @@ -91,8 +96,8 @@ function tryMatch (val) {
    {
    [Symbol.match] (val) {
    return (
    Object[Symbol.match](val) &&
    Foo[Symbol.match](val.y)
    mm(Object, val) &&
    mm(Foo, val.y)
    )
    }
    },
    @@ -105,8 +110,8 @@ function tryMatch (val) {
    {
    [Symbol.match] (val) {
    return (
    Object[Symbol.match](val) &&
    Object[Symbol.match](val.x)
    mm(Object, val) &&
    mm(Object, val.x)
    )
    }
    },
    @@ -119,8 +124,8 @@ function tryMatch (val) {
    {
    [Symbol.match] (val) {
    return (
    Object[Symbol.match](val) &&
    Object[Symbol.match](val.y)
    mm(Object, val) &&
    mm(Object, val.y)
    )
    }
    },
  5. zkat revised this gist Mar 21, 2018. 2 changed files with 17 additions and 19 deletions.
    2 changes: 1 addition & 1 deletion output of demo
    Original file line number Diff line number Diff line change
    @@ -30,7 +30,7 @@ match (val = [ 1, 2, 3, 4, 5 ]) {
    == compound matching ==

    match (val = { x: { y: 2 } }) {
    {x, x: {y}} => x === { y: 2 } && y === 2 // (follows destr. syntax)
    {x: {y} as x} => x === { y: 2 } && y === 2 // (using hypothetical `as` syntax -- would have to be `{x, x: {y}}` otherwise)
    }

    match (val = { y: { x: 1 } }) {
    34 changes: 16 additions & 18 deletions patterns.js
    Original file line number Diff line number Diff line change
    @@ -1,48 +1,46 @@
    // I believe all of the below are fully nestable
    const foo = match x {
    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)
    Just {val} => val
    None {} => ... // (see below for alternative None)
    // bind value to x, match with matcher. Toplevel reduntant; used for nesting
    x@Matcher {}: x
    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/: ...
    x@/regexp/: ...
    'literal' => ...
    42 => ...
    /regexp/ => ...

    // Regexp has Array-like matcher.
    // This desugars to RegExp {length: 2, 0: a, b: 2}
    /regexp/ [a, b]: ...
    /regexp/ [a, b] => ...

    // Desugars to Object {x, y}
    {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]: ...
    [a, b] => ...

    // Desugars to TypedArray {length: 2, 0: a, 1: b}
    TypedArray [a, b]: ...
    TypedArray [a, b] => ...

    // When @ is present, {} becomes optional:
    x@Matcher: ...
    @None: ...
    // `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
    _: 'you need monads'
    other: console.log(other) // lol
    _ => 'just a variable'
    other => console.log(other) // lol

    // equality matches done with guards
    other if other === 1
    other if other === 1 => 'other is 1'

    }
  6. zkat revised this gist Mar 21, 2018. 2 changed files with 211 additions and 0 deletions.
    159 changes: 159 additions & 0 deletions demo.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,159 @@
    'use strict'

    const util = require('util')

    function match (val, ...expressions) {
    const expr = expressions.find(expr => expr.Matcher[Symbol.match](val))
    return expr.body(val)
    }

    function expr (Matcher, body) {
    return {
    Matcher,
    body
    }
    }

    // default protocol impl
    Object[Symbol.match] = function (x) { return x instanceof this }
    Array[Symbol.match] = function (x) { return x instanceof this }
    String[Symbol.match] = function (x) { return x instanceof this }

    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: [...rest] if rest[0] === 3
    expr(
    // Compound matcher generated
    {
    [Symbol.match] (val) {
    return (
    Array[Symbol.match](val) &&
    (([...rest]) => rest[0] === 3)(val)
    )
    }
    },
    ([...rest]) => `[...rest] if rest[0] === 3 => rest === ${util.inspect(rest)}`
    ),

    // sugar: [a, b]
    expr(
    // Compound matcher generated
    {
    [Symbol.match] (val) {
    return (
    Array[Symbol.match](val) &&
    val.length === 2
    )
    }
    },
    ([a, b]) => `[a, b] => ${util.inspect([a, b])}`
    ),

    // sugar: [a, 2, ...rest]
    expr(
    // Compound matcher generated
    {
    [Symbol.match] (val) {
    return (
    Array[Symbol.match](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 (
    Object[Symbol.match](val) &&
    Object[Symbol.match](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 (
    Object[Symbol.match](val) &&
    Foo[Symbol.match](val.y)
    )
    }
    },
    ({y: {x}}) => `{y: Foo {x}} => x === ${util.inspect(x)}`
    ),

    // sugar: {x, x: {y}}
    expr(
    // Compound matcher generated
    {
    [Symbol.match] (val) {
    return (
    Object[Symbol.match](val) &&
    Object[Symbol.match](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 (
    Object[Symbol.match](val) &&
    Object[Symbol.match](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])
    52 changes: 52 additions & 0 deletions output of demo
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,52 @@
    ➜ node ./matcher.js
    == 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 ]) {
    [...rest] if rest[0] === 3 => rest === [ 3, 2, 1 ]
    }
  7. zkat created this gist Mar 21, 2018.
    48 changes: 48 additions & 0 deletions patterns.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,48 @@
    // 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
    x@Matcher {}: x

    // Syntax sugar extensions, all statically compile down to above.

    // Desugars to `String/Number/RegExp {}` but compilers can special-case.
    'literal': ...
    42: ...
    /regexp/: ...
    x@/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}: ...

    // 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]: ...

    // When @ is present, {} becomes optional:
    x@Matcher: ...
    @None: ...

    // 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
    _: 'you need monads'
    other: console.log(other) // lol

    // equality matches done with guards
    other if other === 1

    }