-
-
Save brigand/8a2d1103d8f51c150f2876033eb03394 to your computer and use it in GitHub Desktop.
Concept for flexible pattern matching in ES6
This file contains hidden or 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
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