Last active
October 30, 2020 09:55
-
-
Save fvilante/d2a342d86996abaa44fba4aa22e94603 to your computer and use it in GitHub Desktop.
ReasonML inspired pattern match for Typescript
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
// ======================================================================================== | |
// Utilities types | |
// ======================================================================================== | |
type TU11 = '(T extends U) and (U extends T)' | |
type TU10 = '(T extends U) and (U NOT extends T)' | |
type TU01 = '(T NOT extends U) and (U extends T)' | |
type TU00 = '(T NOT extends U) and (U NOT extends T)' | |
type Ext<T,U,A,B> = T extends U ? A : B // In type system jargon we can say that "T is assignable to U". | |
type ExtendsCheck_<T,U> = T extends U ? (U extends T ? TU11 : TU10) : (U extends T ? TU01 : TU00) | |
type ExtendsCheck<T,U> = Ext<T,U,Ext<U,T,TU11,TU10>,Ext<U,T,TU01, TU00>> | |
type ExtendsCheck__<T,U> = ExtendsCheck<ExtendsCheck<T,U>,ExtendsCheck<U,T>> | |
type IsEqual<A,B,X=true,Y=false> = ExtendsCheck<A,B> extends TU11 ? X : Y | |
type IsNotEqual<A,B,X=true,Y=false> = IsEqual<A,B,Y,X> | |
type UnionFilterByExclusion<T, K> = T extends K ? never : T | |
type UnionFilterByInclusion<T, K> = T extends K ? T : never // todo: when possible discover why Ext<T,K,T,never> do not works | |
type TMap_<T> = {kind: T } | |
type ToBeEqual<T,U> = ExtendsCheck<T,U> extends TU11 ? true : false | |
type AssertsEqual<Expected, Actual> = ToBeEqual<Expected,Actual> | |
type TestAll<T extends true[]> = T | |
// ======================================================================================== | |
// TupleInference | |
// ======================================================================================== | |
type FilterTuple<T extends unknown[]> = T['length'] extends number ? T['length'] : T['length'] | |
type U70 = [1,2,3]| [10,20,30] | |
type U71 = number[] | string[] | |
type U72 = U70 | U71 | |
type S70 = FilterTuple<U70> | |
type S71 = FilterTuple<U71> | |
type T70 = AssertsEqual<S70,U70> | |
const tail = <T extends unknown[]>(arr: readonly [unknown, ...T]) => { | |
const [_ignored, ...rest] = arr; | |
return rest; | |
} | |
type IsLessThenOrEqual<N extends number> = | |
N extends 0 ? 0 : | |
N extends 1 ? 0 | 1: | |
N extends 2 ? 0 | 1 | 2: | |
N extends 3 ? 0 | 1 | 2 | 3: | |
N extends 4 ? 0 | 1 | 2 | 3 | 4: | |
number | |
type List<S,A extends unknown[]> = | |
S extends 0 ? [] : | |
S extends 1 ? [A[0]] : | |
S extends 2 ? [A[0], A[1]] : | |
S extends 3 ? [A[0], A[1], A[2]] : | |
S extends 4 ? [A[0], A[1], A[2], A[3]] : never | |
type TupleOf<T, N extends number> = | |
N extends N ? _TupleOf<T,N,[]> : never | |
type _TupleOf<T, N extends number, R extends unknown[]> = | |
R['length'] extends N ? R : _TupleOf<T, N, [T, ...R]>; | |
type = InferTuple<[1,2,3,4,5]> | |
//type TT = TupleOf<number,5> | |
const take = <H extends unknown[], T extends unknown[]>(size: S, arr: readonly [unknown, ...H]) => { | |
const [_ignored, ...rest] = arr; | |
return rest; | |
} | |
const t050 = tail([1,2,3,4] as const) | |
// unit test | |
type U60 = number | [1,2,3,4] | string | |
type U61 = [1, 2, 3, 4] | |
type S60 = FilterTuple<U60> | |
type T60 = AssertsEqual<S60,U61> | |
//type S52 = MatchTuple<S50,[1,2]> | |
const matchList = <As extends unknown[], Bs extends unknown[]>(list: As, match: Bs): [Bs, ] => {} | |
// ======================================================================================== | |
// Variant Lib | |
// ======================================================================================== | |
type Variant<K extends string = string, A = unknown> = [ | |
value: A, | |
ctor: K, | |
kind: "Variant", | |
] | |
type VariantConstructor<K extends string, A> = { | |
readonly kind: "VariantConstructor" | |
readonly ctor: K | |
readonly run: (data:A) => Variant<K,A> | |
} & ((data:A) => Variant<K,A>) | |
type VariantCtor<V extends Variant> = V[1] | |
type VariantValue<V extends Variant> = V[0] | |
declare const VariantConstructor: <V extends Variant>(k: VariantCtor<V>) => (data:VariantValue<V>) => Variant<VariantCtor<V>,VariantValue<V>> | |
// unit test | |
// NoBytes | |
type NoBytes = Variant<"NoBytes",undefined> | |
const NoBytes = VariantConstructor<NoBytes>('NoBytes') | |
// Byte(number) | |
type Byte = Variant<"Byte",number> | |
const Byte = VariantConstructor<Byte>("Byte") | |
// Bytes(number[]) | |
type Bytes = Variant<"Bytes",number[]> | |
const Bytes = VariantConstructor<Bytes>("Bytes") | |
// type Data = NoBytes | Byte(number) | Bytes(number) | |
type Data = NoBytes | Byte | Bytes | |
const Data = VariantConstructor<Data>("Bytes") | |
declare const data: Data | |
// ======================================================================================== | |
// Pattern Match lib | |
// ======================================================================================== | |
// lib single | |
type Exhausted = "exhausted" | |
type Pending = "pending" | |
type Unused = "unused" | |
type E = Exhausted | Pending | |
type State<C extends E = E, T = unknown> = | |
C extends Exhausted ? { state: Exhausted, types: never } : | |
C extends Pending ? { state: Pending, types: T extends T ? { pending: T} : never} | |
: never | |
type Distribute<T> = T extends T ? T : never | |
type IfIsPendingDo<S extends State, Do> = Distribute<SwitchState<S> extends Pending ? Do : never> | |
type IfStateIs<S extends State,C extends E,Do> = S extends State<C,unknown> ? Do : never | |
type PendingTypes<S extends State> = S['types']['pending'] | |
type SwitchState<S extends State> = S['state'] | |
// unit test | |
type U20 = number | string | Data | |
type S19 = State<Pending, U20> | |
type S20 = PendingTypes<S19> | |
type S21 = SwitchState<S19> | |
type T20 = AssertsEqual<S21, Pending> | |
type T21 = AssertsEqual<S20, U20> | |
type T22 = TestAll<[T20,T21]> | |
type Remove<T,U> = T extends T ? Exclude<T,U> : never | |
type Next<S extends State,U extends PendingTypes<S>> = | |
IfIsPendingDo<S, | |
// yes it's pending, check if following is the last step | |
State<Pending,Remove<PendingTypes<S>,U>> extends State<Pending,never> | |
// yes, last step | |
? State<Exhausted> | |
// no, continue | |
: State<Pending,Remove<PendingTypes<S>,U>> | |
> | |
// unit test | |
type U00 = string | number | 1[] | Data | |
type S00 = State<Pending,U00> | |
type S01 = Next<S00,number> | |
type T01 = AssertsEqual<PendingTypes<S00>,U00> | |
type T02 = AssertsEqual<UnionFilterByExclusion<U00,number>,PendingTypes<S01>> | |
type T03 = AssertsEqual<[Pending, Pending], [SwitchState<S00>,SwitchState<S01>]> | |
type U01 = Bytes | Byte | 1[] | |
type S02 = Next<S01, U01> | |
type T04 = AssertsEqual<S02,State<Pending, UnionFilterByExclusion<PendingTypes<S01>,U01>>> | |
type U02 = string | Variant<"NoBytes", undefined> | |
type S03 = Next<S02, U02> | |
type T05 = AssertsEqual<S03,State<Exhausted, never>> | |
//type S04 = Next<S03, 2[]> //must be an error | |
type T06 = TestAll<[T01,T02,T03,T04,T05]> | |
type FilterVariant<T> = UnionFilterByInclusion<T,Variant> | |
type FilterNonVariant<T> = UnionFilterByExclusion<T,Variant> | |
type DefaultResultState<S extends State> = IfStateIs<S,Pending,State<Exhausted>> | |
type SelectVariant<T extends Variant, K extends string> = | |
T extends Variant<K, infer A> ? Variant<K,A> : never | |
type VariantSugar<K extends string = string> = { variant: K } | |
type Pattern<S extends State = State> = IfStateIs<S, Pending, | |
| FilterNonVariant<PendingTypes<S>> | |
| { variant: VariantCtor<FilterVariant<PendingTypes<S>>> } // VariantSugar | |
> | |
type Data_<S extends State = State, P extends Pattern<S> = Pattern<S>> = | |
P extends { variant: string } // // VariantSugar | |
? SelectVariant<FilterVariant<PendingTypes<S>>,P['variant']> | |
: P | |
type Step<S extends State, P extends Pattern<S>> = Data_<S,P> | |
type Match<S extends State = State,R = unknown> = { | |
with: <P extends Pattern<S>>(pattern: P, match: (data: Data_<S,P>) => R) => Match<Next<S,Step<S,P>>,R> | |
when: <P extends Pattern<S>>(pattern: P, whenCondition: (_: Data_<S,P>) => boolean, match: (data: Data_<S,P>) => R) => Match<S,R> | |
//WithList: <L extends PatternList<S>>(pattern: L) => undefined | |
default: (match: (data: PendingTypes<S>) => R) => Match<DefaultResultState<S>,R> | |
run: () => IfStateIs<S,Exhausted,R> | |
getState: () => S | |
} | |
declare const Match: <A,R>(data:A) => Match<State<Pending,A>,R> | |
declare const Switch: <R>() => <A>(data:A) => Match<State<Pending,A>,R> | |
// unit test | |
type U40 = PendingTypes<S02> | |
type U39 = 'Bytes' | |
type U41 = S02 | |
type S40 = Pattern<U41> | |
type U42 = { variant: 'NoBytes'} | |
type S41 = Data_<U41, U42> | |
type S42 = Step<U41, U42> | |
type S43 = Next<U41,S42> | |
type S44 = Match<S43> | |
type T40 = AssertsEqual<S42,S41> | |
type T41 = AssertsEqual<S42,Variant<'NoBytes',undefined>> | |
type T42 = AssertsEqual<S43,State<Pending,string>> | |
type T43 = TestAll<[T40,T41,T42]> | |
type T44 = TestAll<[...T22, ...T06, ...T43]> | |
// unit test | |
type Q0 = 0 | |
type Q1 = 1 | |
type Q2 = 2 | |
type Q = Q0 | Q1 | Q2 | |
declare const q0: Q0 | |
declare const q1: Q1 | |
declare const q2: Q2 | |
declare const q: Q | |
type X0 = `a` | |
type X1 = `b` | |
type X2 = `c` | |
type X = X0 | X1 | X2 | |
declare const x0: X0 | |
declare const x1: X1 | |
declare const x2: X2 | |
declare const x: X | |
type S11 = ReturnType<Match<S00,undefined>['getState']> | |
type T10 = AssertsEqual<S11, ["pending", U00]> | |
const log = (msg: string):undefined => { console.log(msg); return undefined} | |
// enviroment | |
const is8Bits = (byte: number):boolean => byte >=0 && byte <= 255 | |
declare const toMatch: Q | Data | |
// type Q = 1 | 2 | 3 | |
// type Data = Byte(number) | Bytes(number[]) | NoBytes // variants | |
// Test | |
const T11 = Switch<undefined>()(toMatch) | |
.with({ variant: 'NoBytes'}, _ => undefined) | |
.with(0, data => undefined) | |
.with(1, data => undefined) | |
.with(2, data => undefined) | |
.when({variant: 'Byte'}, ([byte]) => is8Bits(byte), data => undefined) | |
.when({variant: 'Bytes'}, ([bytes]) => bytes.every(is8Bits), data => undefined ) | |
.default( data => | |
Switch<undefined>()(data) | |
.with({ variant: 'Byte'}, data => log(`Invalid byte '${data}'`)) | |
.with({ variant: 'Bytes'}, data => log(`Invalid bytes '${data}`)) | |
.run() | |
) | |
.run() | |
type T50 = AssertsEqual<typeof T11,undefined> | |
type T51 = TestAll[] | |
// lib multi | |
type Param2<T extends State[]> = { | |
readonly [K in keyof T]: T[K] extends State<E,infer A> ? A : never | |
} | |
// Map Next in a tuple of states and transitions | |
type Next2<SS extends State[],P extends Param2<SS>> = { | |
[K in keyof SS]: Next<SS[K] extends State ? SS[K] : never, P[K]> | |
//fix: I`d like to avoid extends in line up above. | |
} | |
type ToTuple<U extends unknown[]> = { | |
[K in keyof U]: State<"not_exhausted", U[K]> | |
} | |
type MultMatch<SS extends State[],R> = { | |
with: <PP extends Param2<SS>>(pattern: PP, match: (data: PP) => R) => MultMatch<Next2<SS,PP>,R> | |
when: <PP extends Param2<SS>>(pattern: PP, whenCondition: (data:PP) => boolean, match: (data: PP) => R) => MultMatch<SS,R> | |
run: () => SS extends State<"exhausted">[] ? R : never | |
default: (match: (data: Param2<SS>) => R) => MultMatch<SS extends State<"exhausted">[] ? State<"unused">[] : State<"exhausted">[],R> // note: may improve to detect if there is a over-exaustion | |
} | |
declare const MultMatch: <U extends unknown[],R>(data: U) => MultMatch<ToTuple<U>,R> | |
// Lib pre-test | |
// ================================================================= | |
// Test | |
// ================================================================= | |
// ---------------------- | |
// use playground | |
// ---------------------- | |
// single | |
const y = Match<Q,string>(q) | |
.with(0, _ => "foo1") | |
//.when(0, data => data === data, _ => "foo2") | |
.with(1, _ => "foo3") | |
.with(2, _ => "foo4") | |
//.default( data => `foo5 ${data}`) | |
.run() | |
// mult | |
const y2 = MultMatch<[Q,X],string>([q,x]) | |
.with([q0,x1], _ => "foo1") | |
.when([q1,x0], ([x,y]) => String(x) !== y, _ => "foo2") | |
//.with([Q1,X1], _ => "foo3") | |
//.with([Q2,X2], _ => "foo4") | |
.default( data => `foo5 ${data}`) | |
.run() | |
// | |
// single | |
const y1_ = Match<Data,string>(data) | |
.withVariant("NoBytes", _ => `there's no bytes there`) | |
//.with( {variant: "Byte"}, bytes => `bytes: ${bytes}`) | |
//.with() | |
//.with({variant: ""}) | |
//.with({variant: ""}, _ => "foo3") | |
//.with("NoBytes", _ => "foo4") | |
//.run() | |
// mult | |
const y2__ = MultMatch<[Q,X],string>([q,x]) | |
.with([q0,x1], _ => "foo1") | |
.with([q1,x2], _ => "foo3") | |
.with([q2,x0], _ => "foo4") | |
.run() | |
// | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment