diff --git a/docs/modules/Variant.ts.md b/docs/modules/Variant.ts.md new file mode 100644 index 000000000..9671d9cf6 --- /dev/null +++ b/docs/modules/Variant.ts.md @@ -0,0 +1,273 @@ +--- +title: Variant.ts +nav_order: 119 +parent: Modules +--- + +## Variant overview + +```ts +type Tagged = { _tag: Tag; value: A } +``` + +is a type for a tagged value which when used together in a union can represent a Variant type +(aka sum type / discriminated union). + +Variant types are dual to Record types. Unlike Record types which have multiple keys all of which have to be provided, +a value of a Variant type represents one of a number of cases. + +The most common thing we want to do with Variant Types is pattern match on the case; +Given a record of functions, one for each case, we can use the function `caseOf` to pattern match +a value of a Variant and apply the function for the correct case. + +**Example** + +```ts +import * as variant from 'fp-ts/Variant' +import { pipe } from 'fp-ts/function' + +type Media = variant.Tagged<'book', number> | variant.Tagged<'film', string> | variant.Tagged<'song', string> + +const Media = variant.module({ + book: (value: number) => variant.tagged(_book, value), + film: (value: string) => variant.tagged(_film, value), + song: (value: string) => variant.tagged(_song, value), +}) + +const _book = 'book' +const _film = 'film' +const _song = 'song' + +const exampleBook = Media.book(123) +const exampleFilm = Media.film('Harry Potter') + +const isBook: (media: Media) => boolean = (media) => + pipe( + media, + variant.caseOfWithDefault(false)({ + [_book]: () => true, + }) + ) + +assert.deepStrictEqual(isBook(exampleBook), true) +assert.deepStrictEqual(isBook(exampleFilm), false) +``` + +Added in v2.12.4 + +--- + +

Table of contents

+ +- [constructors](#constructors) + - [Module (type alias)](#module-type-alias) + - [module](#module) + - [tagged](#tagged) +- [destructors](#destructors) + - [caseOf](#caseof) + - [caseOfWithDefault](#caseofwithdefault) + - [match](#match) + - [matchWithDefault](#matchwithdefault) +- [model](#model) + - [Cases (type alias)](#cases-type-alias) + - [Map (type alias)](#map-type-alias) + - [Match (type alias)](#match-type-alias) + - [Tagged (type alias)](#tagged-type-alias) + - [TagsOf (type alias)](#tagsof-type-alias) + - [TypeForTag (type alias)](#typefortag-type-alias) + - [Variant (type alias)](#variant-type-alias) + +--- + +# constructors + +## Module (type alias) + +The type of the "module" of variant constructors + +**Signature** + +```ts +export type Module> = { + [Key in keyof Map]: (value: Map[Key]) => Variant +} +``` + +Added in v2.12.4 + +## module + +Groups together all the variant constructors in a "module" + +**Signature** + +```ts +export declare function module>(constructors: Module): Module +``` + +Added in v2.12.4 + +## tagged + +Constructs a value of a Variant with the given tag + +**Signature** + +```ts +export declare function tagged(tag: Tag, value: A): Tagged +``` + +Added in v2.12.4 + +# destructors + +## caseOf + +Pattern matching on a variant where all cases are provided + +**Signature** + +```ts +export declare function caseOf, A>( + cases: Cases +): (variant: Variant) => A +``` + +Added in v2.12.4 + +## caseOfWithDefault + +Pattern matching on a variant with a default case + +**Signature** + +```ts +export declare function caseOfWithDefault( + defaultValue: A +): >(partialCases: Partial>) => (variant: Variant) => A +``` + +Added in v2.12.4 + +## match + +Pattern matching on a variant where all cases are provided, with arguments swapped + +**Signature** + +```ts +export declare function match>(variant: Variant): (cases: Cases) => A +``` + +Added in v2.12.4 + +## matchWithDefault + +Pattern matching on a variant with a default case, with arguments swapped + +**Signature** + +```ts +export declare function matchWithDefault>( + variant: Variant +): (defaultValue: A) => (partialCases: Partial>) => A +``` + +Added in v2.12.4 + +# model + +## Cases (type alias) + +Type of the record supplied for pattern matching + +**Signature** + +```ts +export type Cases, A> = { + [Key in keyof Map]: (value: Map[Key]) => A +} +``` + +Added in v2.12.4 + +## Map (type alias) + +Type constructor of a record type from the dual variant type + +**Signature** + +```ts +export type Map> = { + [Key in Variant[typeof tagFieldName]]: (Variant & { + [tagFieldName]: Key + })[typeof valueFieldName] +} +``` + +Added in v2.12.4 + +## Match (type alias) + +Type of the pattern matching function, isomorphic to the variant type + +**Signature** + +```ts +export type Match, A> = (cases: Cases) => A +``` + +Added in v2.12.4 + +## Tagged (type alias) + +A type which represents a case in a variant + +**Signature** + +```ts +export type Tagged = { + [tagFieldName]: Tag + [valueFieldName]: A +} +``` + +Added in v2.12.4 + +## TagsOf (type alias) + +Type of the tags of a variant + +**Signature** + +```ts +export type TagsOf> = keyof Map +``` + +Added in v2.12.4 + +## TypeForTag (type alias) + +Type of the variant at a particular tag. + +**Signature** + +```ts +export type TypeForTag, Key extends keyof Map> = Map[Key] +``` + +Added in v2.12.4 + +## Variant (type alias) + +Type constructor of a variant type from the dual record type + +**Signature** + +```ts +export type Variant = { + [Key in keyof Map]: Tagged +}[keyof Map] +``` + +Added in v2.12.4 diff --git a/docs/modules/Witherable.ts.md b/docs/modules/Witherable.ts.md index e3e6ffda5..da5805f2b 100644 --- a/docs/modules/Witherable.ts.md +++ b/docs/modules/Witherable.ts.md @@ -1,6 +1,6 @@ --- title: Witherable.ts -nav_order: 120 +nav_order: 121 parent: Modules --- diff --git a/docs/modules/Writer.ts.md b/docs/modules/Writer.ts.md index 57b74116a..351f70ade 100644 --- a/docs/modules/Writer.ts.md +++ b/docs/modules/Writer.ts.md @@ -1,6 +1,6 @@ --- title: Writer.ts -nav_order: 121 +nav_order: 122 parent: Modules --- diff --git a/docs/modules/WriterT.ts.md b/docs/modules/WriterT.ts.md index 0da38a457..917261dad 100644 --- a/docs/modules/WriterT.ts.md +++ b/docs/modules/WriterT.ts.md @@ -1,6 +1,6 @@ --- title: WriterT.ts -nav_order: 122 +nav_order: 123 parent: Modules --- diff --git a/docs/modules/Zero.ts.md b/docs/modules/Zero.ts.md index 494cc43ab..c3dac2096 100644 --- a/docs/modules/Zero.ts.md +++ b/docs/modules/Zero.ts.md @@ -1,6 +1,6 @@ --- title: Zero.ts -nav_order: 123 +nav_order: 124 parent: Modules --- diff --git a/docs/modules/index.ts.md b/docs/modules/index.ts.md index 77372f0ad..f57a0a977 100644 --- a/docs/modules/index.ts.md +++ b/docs/modules/index.ts.md @@ -129,6 +129,7 @@ Added in v2.0.0 - [tuple](#tuple) - [unfoldable](#unfoldable) - [validationT](#validationt) + - [variant](#variant) - [void](#void) - [witherable](#witherable) - [writer](#writer) @@ -1299,6 +1300,16 @@ export declare const validationT: typeof validationT Added in v2.0.0 +## variant + +**Signature** + +```ts +export declare const variant: typeof variant +``` + +Added in v2.12.4 + ## void **Signature** diff --git a/docs/modules/void.ts.md b/docs/modules/void.ts.md index cc1ab2757..668c6d301 100644 --- a/docs/modules/void.ts.md +++ b/docs/modules/void.ts.md @@ -1,6 +1,6 @@ --- title: void.ts -nav_order: 119 +nav_order: 120 parent: Modules --- diff --git a/src/Variant.ts b/src/Variant.ts new file mode 100644 index 000000000..7d37158cd --- /dev/null +++ b/src/Variant.ts @@ -0,0 +1,200 @@ +/** + * ```ts + * type Tagged = {_tag: Tag, value: A} + * ``` + * + * is a type for a tagged value which when used together in a union can represent a Variant type + * (aka sum type / discriminated union). + * + * Variant types are dual to Record types. Unlike Record types which have multiple keys all of which have to be provided, + * a value of a Variant type represents one of a number of cases. + * + * The most common thing we want to do with Variant Types is pattern match on the case; + * Given a record of functions, one for each case, we can use the function `caseOf` to pattern match + * a value of a Variant and apply the function for the correct case. + * + * @example + * + * import * as variant from "fp-ts/Variant" + * import { pipe } from "fp-ts/function" + * + * type Media = + * | variant.Tagged<"book", number> + * | variant.Tagged<"film", string> + * | variant.Tagged<"song", string> + * + * const Media = variant.module({ + * book: (value: number) => variant.tagged(_book, value), + * film: (value: string) => variant.tagged(_film, value), + * song: (value: string) => variant.tagged(_song, value), + * }) + * + * const _book = "book" + * const _film = "film" + * const _song = "song" + * + * const exampleBook = Media.book(123) + * const exampleFilm = Media.film("Harry Potter") + * + * const isBook: (media: Media) => boolean = (media) => + * pipe( + * media, + * variant.caseOfWithDefault(false)({ + * [_book]: () => true, + * }) + * ) + * + * assert.deepStrictEqual(isBook(exampleBook), true) + * assert.deepStrictEqual(isBook(exampleFilm), false) + * + * @since 2.12.4 + */ + +// ------------------------------------------------------------------------------------- +// model +// ------------------------------------------------------------------------------------- + +const tagFieldName = '_tag' +const valueFieldName = 'value' + +/** + * A type which represents a case in a variant + * @category model + * @since 2.12.4 + */ +export type Tagged = { + [tagFieldName]: Tag + [valueFieldName]: A +} + +/** + * Type constructor of a variant type from the dual record type + * @category model + * @since 2.12.4 + */ +export type Variant = { + [Key in keyof Map]: Tagged +}[keyof Map] + +/** + * Type constructor of a record type from the dual variant type + * @category model + * @since 2.12.4 + */ +export type Map> = { + [Key in Variant[typeof tagFieldName]]: (Variant & { + [tagFieldName]: Key + })[typeof valueFieldName] +} + +// ------------------------------------------------------------------------------------- +// constructors +// ------------------------------------------------------------------------------------- + +/** + * Constructs a value of a Variant with the given tag + * @category constructors + * @since 2.12.4 + */ +export function tagged(tag: Tag, value: A): Tagged { + return { [tagFieldName]: tag, [valueFieldName]: value } +} + +/** + * Groups together all the variant constructors in a "module" + * @category constructors + * @since 2.12.4 + */ +export function module>(constructors: Module): Module { + return constructors +} + +/** + * The type of the "module" of variant constructors + * @category constructors + * @since 2.12.4 + */ +export type Module> = { + [Key in keyof Map]: (value: Map[Key]) => Variant +} + +// ------------------------------------------------------------------------------------- +// destructors +// ------------------------------------------------------------------------------------- + +/** + * Pattern matching on a variant where all cases are provided + * @category destructors + * @since 2.12.4 + */ +export function caseOf, A>(cases: Cases): (variant: Variant) => A { + return (variant) => match(variant)(cases) +} + +/** + * Pattern matching on a variant where all cases are provided, with arguments swapped + * @category destructors + * @since 2.12.4 + */ +export function match>(variant: Variant): (cases: Cases) => A { + return (cases) => (cases as Record)[variant[tagFieldName]](variant[valueFieldName]) +} + +/** + * Pattern matching on a variant with a default case + * @category destructors + * @since 2.12.4 + */ +export function caseOfWithDefault( + defaultValue: A +): >(partialCases: Partial>) => (variant: Variant) => A { + return (partialCases) => (variant) => matchWithDefault(variant)(defaultValue)(partialCases) +} + +/** + * Pattern matching on a variant with a default case, with arguments swapped + * @category destructors + * @since 2.12.4 + */ +export function matchWithDefault>( + variant: Variant +): (defaultValue: A) => (partialCases: Partial>) => A { + return (defaultValue) => (partialCases) => { + const caseFunction = (partialCases as Record)[variant[tagFieldName]] + return caseFunction ? caseFunction(variant[valueFieldName]) : defaultValue + } +} + +// ------------------------------------------------------------------------------------- +// Type helpers +// ------------------------------------------------------------------------------------- + +/** + * Type of the record supplied for pattern matching + * @category model + * @since 2.12.4 + */ +export type Cases, A> = { + [Key in keyof Map]: (value: Map[Key]) => A +} + +/** + * Type of the pattern matching function, isomorphic to the variant type + * @category model + * @since 2.12.4 + */ +export type Match, A> = (cases: Cases) => A + +/** + * Type of the tags of a variant + * @category model + * @since 2.12.4 + */ +export type TagsOf> = keyof Map + +/** + * Type of the variant at a particular tag. + * @category model + * @since 2.12.4 + */ +export type TypeForTag, Key extends keyof Map> = Map[Key] diff --git a/src/index.ts b/src/index.ts index ede7b0a7c..1b4207156 100644 --- a/src/index.ts +++ b/src/index.ts @@ -118,6 +118,7 @@ import * as tree from './Tree' import * as tuple from './Tuple' import * as unfoldable from './Unfoldable' import * as validationT from './ValidationT' +import * as variant from './Variant' import * as void_ from './void' import * as witherable from './Witherable' import * as writer from './Writer' @@ -588,6 +589,10 @@ export { * @since 2.0.0 */ validationT, + /** + * @since 2.12.4 + */ + variant, /** * @since 2.11.0 */ diff --git a/test/Variant.ts b/test/Variant.ts new file mode 100644 index 000000000..6350b2912 --- /dev/null +++ b/test/Variant.ts @@ -0,0 +1,47 @@ +import * as U from './util' +import { pipe } from '../src/function' +import * as _ from '../src/Variant' + +type Media = _.Tagged<'book', number> | _.Tagged<'film', string> | _.Tagged<'song', string> + +const Media = _.module({ + book: (value) => _.tagged(_book, value), + film: (value) => _.tagged(_film, value), + song: (value) => _.tagged(_song, value) +}) + +const _book = 'book' +const _film = 'film' +const _song = 'song' + +const show: (media: Media) => string = (media) => + pipe( + media, + _.caseOf({ + [_book]: (isbn) => `Book ${isbn}`, + [_film]: (filmTitle) => `Film ${filmTitle}`, + [_song]: (song) => `Song ${song}` + }) + ) + +const isBook: (media: Media) => boolean = (media) => pipe(media, _.caseOfWithDefault(false)({ [_book]: () => true })) + +const bookExample = Media.book(123) +const filmExample = Media.film('Harry Potter') + +describe('Variant', () => { + describe('constructors', () => { + it('tagged', () => { + U.deepStrictEqual(_.tagged('test tag', 1), { _tag: 'test tag', value: 1 }) + }) + }) + describe('destructors', () => { + it('caseOf', () => { + U.strictEqual(show(bookExample), 'Book 123') + }) + it('caseOfWithDefault', () => { + U.strictEqual(isBook(bookExample), true) + U.strictEqual(isBook(filmExample), false) + }) + }) +})