Skip to content

Instantly share code, notes, and snippets.

@brigand
Created December 17, 2019 23:35
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 brigand/8a2d1103d8f51c150f2876033eb03394 to your computer and use it in GitHub Desktop.
Save brigand/8a2d1103d8f51c150f2876033eb03394 to your computer and use it in GitHub Desktop.
Concept for flexible pattern matching in ES6
import match, { matches, Instance, TypeOf, PropEq, Eq, Check, Index } from '@brigand/unnamed-match-thing';
// Simple matching.
// Note that this is an object literal, so all of these need to
// return something with a toString, and match needs to convert that
// back to the pattern. This implies we need to store Eq('production') in
// module level storage and somehow avoid leaking memory for every pattern ever used.
// Something every toString called here allocates the pattern, and when match() is called
// it can claim all patterns before it (since the toString calls run before match()), removing
// them from the global storage, and then execute the patterns.
// Then if there's a match call inside one of the match arms, it will work as normal.
// This even allows a `const pattern = Eq('production')` to be used in multiple matches,
// but not `const patternId = Eq('production').toString()` to be used in multiple matches.
// By only requiring the conversion to a string to happen for each match(), the chance
// of writing code that fails is pretty limited.
match(process.env.NODE_ENV, {
// Simple equality predicate
[Eq('production')]: () => 2,
// Chaining predicates
[Eq('development').or(Eq(''))]: () => 1,
// Custom predicate
[Check(s => s[0] === 'd')]: 1,
// Default branch
_: () => 0,
});
const letters = ['a', 'b', 'c'];
match(letters, {
// The `Index(i)` helper matches if `letters.length > i`, and also
// performs "selection" mapping ['a', 'b', 'c'] to 'b'.
// While .and() looks at the original input (letters), the `.andThen` uses
// the "selection" of the parent operator, which is 'b' in this case.
// The handler for the match also receives the "selection".
[Index(1).andThen(Eq('b'))](letter) {
assert(letter === 'b');
},
_() {
throw new Error('Unreachable');
}
});
match(process.env, {
[PropEq('NODE_ENV', 'production')]: 2,
[PropEq('NODE_ENV', 'development').or(
PropEq('NODE_ENV', '')
)]: 1,
_: 0,
});
// Note: if we lie in .d.ts and say that TypeOf('string') returns '__typeof__string'
// then match could say that the match object has an optional {__typeof__string: (value: string) => U}
// and the type would be inferred for (str: string) => U
// Hopefully this works with toString in TypeScript.
// It wouldn't work with `Instance` for any types we don't explicitly define.
// We could provide something like `[Instance(Array)]: Instance(Array).handler((array) => array.length)`
// to do the type cast more reliably, and then we can leave the `[key:string]: (x: unknown)`
// as the type for unspecified keys.
const length = match(arg, {
[Instance(Array)]: (array) => array.length
[TypeOf('string')]: (str) => str.length,
_: 0
});
// Boolean match, similar to `if let` in rust, when used without any bindings
if (matches(process.env.NODE_ENV, Eq('production'))) {
// do something.
}
// TODO: define extracting values from predicates, including custom cases
// like safe-types Option/Result.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment