From 7a404dc63428a7f8f4bc38197335f7fb67b2e376 Mon Sep 17 00:00:00 2001 From: gvergnaud Date: Wed, 1 Sep 2021 00:52:42 +0200 Subject: [PATCH 1/2] Add Variants --- src/index.ts | 1 + src/variants.ts | 41 ++++++++++ tests/variants.test.ts | 166 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 src/variants.ts create mode 100644 tests/variants.test.ts diff --git a/src/index.ts b/src/index.ts index 1276264b..78e15ca9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,4 @@ import * as Pattern from './patterns'; export { match } from './match'; export { isMatching } from './is-matching'; export { Pattern, Pattern as P }; +export { Variant, implementVariants } from './variants'; diff --git a/src/variants.ts b/src/variants.ts new file mode 100644 index 00000000..074a2516 --- /dev/null +++ b/src/variants.ts @@ -0,0 +1,41 @@ +import { Compute } from './types/helpers'; +import { Pattern } from './types/Pattern'; + +export type Variant = Compute<{ tag: k; value: d }>; + +/** + * VariantPatterns can be used to match a Variant in a + * `match` expression. + */ +type VariantPattern = { tag: k; value: p }; + +type AnyVariant = Variant; + +type Narrow = Extract< + variant, + Variant +>; + +type Constructor = [v] extends [never] + ? () => Variant + : unknown extends v + ? (value: t) => Variant + : { + (value: v): Variant; +

>(pattern: p): VariantPattern; + }; + +type Impl = { + [k in variant['tag']]: Constructor['value']>; +}; + +export function implementVariants(): Impl { + return new Proxy({} as Impl, { + get: >(_: Impl, tag: k) => { + return (...args: [value?: Narrow['value']]) => ({ + tag, + ...(args.length === 0 ? {} : { value: args[0] }), + }); + }, + }); +} diff --git a/tests/variants.test.ts b/tests/variants.test.ts new file mode 100644 index 00000000..85e0b9c1 --- /dev/null +++ b/tests/variants.test.ts @@ -0,0 +1,166 @@ +import { match, Variant, implementVariants, P } from '../src'; + +// APP code +type Shape = + | Variant<'Circle', { radius: number }> + | Variant<'Square', { sideLength: number }> + | Variant<'Rectangle', { x: number; y: number }> + | Variant<'Blob', number>; + +type Maybe = Variant<'Just', T> | Variant<'Nothing'>; + +const { Just, Nothing } = implementVariants>(); +const { Circle, Square, Rectangle, Blob } = implementVariants(); + +describe('Variants', () => { + it('should work with exhaustive matching', () => { + const area = (x: Shape) => + match(x) + .with(Circle({ radius: P.select() }), (radius) => Math.PI * radius ** 2) + .with(Square(P.select()), ({ sideLength }) => sideLength ** 2) + .with(Rectangle(P.select()), ({ x, y }) => x * y) + .with(Blob(P._), ({ value }) => value) + .exhaustive(); + + expect(area(Circle({ radius: 1 }))).toEqual(Math.PI); + expect(area(Square({ sideLength: 10 }))).toEqual(100); + expect(area(Blob(0))).toEqual(0); + + // @ts-expect-error + expect(() => area({ tag: 'UUUPPs' })).toThrow(); + }); + + it('should be possible to nest variants in data structures', () => { + const shapesAreEqual = (a: Shape, b: Shape) => + match({ a, b }) + .with( + { + a: Circle({ radius: P.select('a') }), + b: Circle({ radius: P.select('b') }), + }, + ({ a, b }) => a === b + ) + .with( + { + a: Rectangle(P.select('a')), + b: Rectangle(P.select('b')), + }, + ({ a, b }) => a.x === b.x && a.y === b.y + ) + .with( + { + a: Square({ sideLength: P.select('a') }), + b: Square({ sideLength: P.select('b') }), + }, + ({ a, b }) => a === b + ) + .with( + { + a: Blob(P.select('a')), + b: Blob(P.select('b')), + }, + ({ a, b }) => a === b + ) + .otherwise(() => false); + + expect( + shapesAreEqual(Circle({ radius: 2 }), Circle({ radius: 2 })) + ).toEqual(true); + expect( + shapesAreEqual(Circle({ radius: 2 }), Circle({ radius: 5 })) + ).toEqual(false); + expect( + shapesAreEqual(Square({ sideLength: 2 }), Circle({ radius: 5 })) + ).toEqual(false); + }); + + it('Variants with type parameters should work', () => { + const toString = (maybeShape: Maybe) => + match(maybeShape) + .with(Nothing(), () => 'Nothing') + .with( + Just(Circle({ radius: P.select() })), + (radius) => `Just Circle { radius: ${radius} }` + ) + .with( + Just(Square(P.select())), + ({ sideLength }) => `Just Square sideLength: ${sideLength}` + ) + .with( + Just(Rectangle(P.select())), + ({ x, y }) => `Just Rectangle { x: ${x}, y: ${y} }` + ) + .with(Just(Blob(P.select())), (area) => `Just Blob { area: ${area} }`) + .exhaustive(); + + expect(toString(Just(Circle({ radius: 20 })))).toEqual( + `Just Circle { radius: 20 }` + ); + expect(toString(Nothing())).toEqual(`Nothing`); + }); + + it('should be possible to put a union type in a variant', () => { + // with a normal union + + const maybeAndUnion = ( + x: Maybe<{ type: 't'; value: string } | { type: 'u'; value: number }> + ) => + match(x) + .with(Nothing(), () => 'Non') + .with( + Just({ type: 't' as const, value: P.select() }), + (x) => 'typeof x: string' + ) + .with( + Just({ type: 'u' as const, value: P.select() }), + (x) => 'typeof x: number' + ) + .exhaustive(); + + expect(maybeAndUnion(Nothing())).toEqual('Non'); + expect(maybeAndUnion(Just({ type: 't', value: 'hello' }))).toEqual( + 'typeof x: string' + ); + expect(maybeAndUnion(Just({ type: 'u', value: 2 }))).toEqual( + 'typeof x: number' + ); + }); + + it('should be possible to create a variant with several type parameters', () => { + // Result + type Result = Variant<'Success', A> | Variant<'Err', E>; + + const { Success, Err } = implementVariants>(); + + type SomeRes = Result; + + const x = true ? Success({ hello: 'coucou' }) : Err('lol'); + + const y: SomeRes = x; + + const complexMatch = (x: Result) => { + return match(x) + .with(Err(P.select()), (msg) => `Error: ${msg}`) + .with( + Success({ shape: Circle(P.select()) }), + ({ radius }) => `Circle ${radius}` + ) + .with( + Success({ shape: Square(P.select()) }), + ({ sideLength }) => `Square ${sideLength}` + ) + .with(Success({ shape: Blob(P.select()) }), (area) => `Blob ${area}`) + .with( + Success({ shape: Rectangle(P.select()) }), + ({ x, y }) => `Rectangle ${x + y}` + ) + .exhaustive(); + }; + + expect(complexMatch(Success({ shape: Circle({ radius: 20 }) }))).toEqual( + 'Circle 20' + ); + expect(complexMatch(Success({ shape: Blob(20) }))).toEqual('Blob 20'); + expect(complexMatch(Err('Failed'))).toEqual('Error: Failed'); + }); +}); From 033771b3251f6310bf07933e5baef0e9f0491b43 Mon Sep 17 00:00:00 2001 From: gvergnaud Date: Mon, 3 Jul 2023 10:55:08 +0200 Subject: [PATCH 2/2] adts: wip --- src/patterns.ts | 11 +++++---- src/types/Pattern.ts | 2 +- src/variants.ts | 51 +++++++++++++++++++++++++----------------- tests/variants.test.ts | 34 +++++++++++++--------------- 4 files changed, 54 insertions(+), 44 deletions(-) diff --git a/src/patterns.ts b/src/patterns.ts index 24bdd351..1b3eb3b3 100644 --- a/src/patterns.ts +++ b/src/patterns.ts @@ -78,11 +78,14 @@ export type unstable_Matcher< * const userPattern = { name: P.stringĀ } * type User = P.infer */ -export type infer

> = InvertPattern; +export type infer> = InvertPattern< + pattern, + unknown +>; -export type narrow> = ExtractPreciseValue< - i, - InvertPattern +export type narrow> = ExtractPreciseValue< + input, + InvertPattern >; type Chainable = p & diff --git a/src/types/Pattern.ts b/src/types/Pattern.ts index 8e18d8fe..3fb5ec9b 100644 --- a/src/types/Pattern.ts +++ b/src/types/Pattern.ts @@ -67,7 +67,7 @@ export interface Matcher< [symbols.isVariadic]?: boolean; } -type PatternMatcher = Matcher; +export type PatternMatcher = Matcher; // We fall back to `a` if we weren't able to extract anything more precise export type MatchedValue = WithDefault< diff --git a/src/variants.ts b/src/variants.ts index 074a2516..0fab2969 100644 --- a/src/variants.ts +++ b/src/variants.ts @@ -1,40 +1,51 @@ -import { Compute } from './types/helpers'; -import { Pattern } from './types/Pattern'; +import * as P from './patterns'; +import { PatternMatcher } from './types/Pattern'; +import { Equal } from './types/helpers'; -export type Variant = Compute<{ tag: k; value: d }>; +const tagKey = '_tag'; +type tagKey = typeof tagKey; + +export type Variant = { [tagKey]: k; value: d }; /** * VariantPatterns can be used to match a Variant in a * `match` expression. */ -type VariantPattern = { tag: k; value: p }; +type VariantPattern = { [tagKey]: k; value: p }; type AnyVariant = Variant; -type Narrow = Extract< +type Narrow = Extract< variant, - Variant + Variant >; -type Constructor = [v] extends [never] - ? () => Variant - : unknown extends v - ? (value: t) => Variant - : { - (value: v): Variant; -

>(pattern: p): VariantPattern; - }; - -type Impl = { - [k in variant['tag']]: Constructor['value']>; +type Constructor = variant extends { + [tagKey]: infer tag; + value: infer value; +} + ? Equal extends true + ? () => Variant + : Equal extends true + ? (value: t) => Variant + : { + (value: value): variant; + >( + pattern: p + ): VariantPattern; + } + : never; + +type Impl = { + [variant in variants as variant[tagKey]]: Constructor; }; export function implementVariants(): Impl { return new Proxy({} as Impl, { get: >(_: Impl, tag: k) => { - return (...args: [value?: Narrow['value']]) => ({ - tag, - ...(args.length === 0 ? {} : { value: args[0] }), + return (...args: [value?: Narrow]) => ({ + [tagKey]: tag, + ...(args.length === 0 ? {} : args[0]), }); }, }); diff --git a/tests/variants.test.ts b/tests/variants.test.ts index 85e0b9c1..1bc6dad6 100644 --- a/tests/variants.test.ts +++ b/tests/variants.test.ts @@ -16,13 +16,15 @@ describe('Variants', () => { it('should work with exhaustive matching', () => { const area = (x: Shape) => match(x) - .with(Circle({ radius: P.select() }), (radius) => Math.PI * radius ** 2) + .with(Circle(P._), (circle) => Math.PI * circle.value.radius ** 2) .with(Square(P.select()), ({ sideLength }) => sideLength ** 2) .with(Rectangle(P.select()), ({ x, y }) => x * y) .with(Blob(P._), ({ value }) => value) .exhaustive(); - expect(area(Circle({ radius: 1 }))).toEqual(Math.PI); + const x = Circle({ radius: 1 }); + + expect(area(x)).toEqual(Math.PI); expect(area(Square({ sideLength: 10 }))).toEqual(100); expect(area(Blob(0))).toEqual(0); @@ -35,8 +37,8 @@ describe('Variants', () => { match({ a, b }) .with( { - a: Circle({ radius: P.select('a') }), - b: Circle({ radius: P.select('b') }), + a: Circle(P.shape({ radius: P.select('a') })), + b: Circle(P.shape({ radius: P.select('b') })), }, ({ a, b }) => a === b ) @@ -49,8 +51,8 @@ describe('Variants', () => { ) .with( { - a: Square({ sideLength: P.select('a') }), - b: Square({ sideLength: P.select('b') }), + a: Square(P.shape({ sideLength: P.select('a') })), + b: Square(P.shape({ sideLength: P.select('b') })), }, ({ a, b }) => a === b ) @@ -79,7 +81,7 @@ describe('Variants', () => { match(maybeShape) .with(Nothing(), () => 'Nothing') .with( - Just(Circle({ radius: P.select() })), + Just(Circle(P.shape({ radius: P.select() }))), (radius) => `Just Circle { radius: ${radius} }` ) .with( @@ -93,9 +95,9 @@ describe('Variants', () => { .with(Just(Blob(P.select())), (area) => `Just Blob { area: ${area} }`) .exhaustive(); - expect(toString(Just(Circle({ radius: 20 })))).toEqual( - `Just Circle { radius: 20 }` - ); + const x = Just(Circle({ radius: 20 })); + + expect(toString(x)).toEqual(`Just Circle { radius: 20 }`); expect(toString(Nothing())).toEqual(`Nothing`); }); @@ -107,14 +109,8 @@ describe('Variants', () => { ) => match(x) .with(Nothing(), () => 'Non') - .with( - Just({ type: 't' as const, value: P.select() }), - (x) => 'typeof x: string' - ) - .with( - Just({ type: 'u' as const, value: P.select() }), - (x) => 'typeof x: number' - ) + .with(Just({ type: 't', value: P.select() }), (x) => 'typeof x: string') + .with(Just({ type: 'u', value: P.select() }), (x) => 'typeof x: number') .exhaustive(); expect(maybeAndUnion(Nothing())).toEqual('Non'); @@ -160,7 +156,7 @@ describe('Variants', () => { expect(complexMatch(Success({ shape: Circle({ radius: 20 }) }))).toEqual( 'Circle 20' ); - expect(complexMatch(Success({ shape: Blob(20) }))).toEqual('Blob 20'); + expect(complexMatch(Success({ shape: Blob(0) }))).toEqual('Blob 20'); expect(complexMatch(Err('Failed'))).toEqual('Error: Failed'); }); });