Last active
June 27, 2019 15:19
-
-
Save hallettj/0fde5bd4c3ce5a5f6d50db6236aaa39e to your computer and use it in GitHub Desktop.
Concept for emulating higher-kinded types in Flow via type-level functions
This file contains 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
/* | |
* Concept for emulating higher-kinded types using Flow. Instead of passing | |
* a type that has not been applied to parameters, this pattern passes | |
* a type-level function that will map a parameter type to the desired | |
* higher-kinded type applied to the given parameter. | |
* | |
* @flow | |
*/ | |
// a higher-kinded type is represented indirectly via a type-level function from | |
// parameter type to concrete type | |
export type HKT<F, A> = $Call<F, A> | |
// Functor is defined via HKT | |
export interface Functor<F> { | |
map<A, B>(f: (a: A) => B, fa: HKT<F, A>): HKT<F, B> | |
} | |
// a function which abstracts over Functor | |
export function lift<F, A, B>(F_: Functor<F>): (f: (a: A) => B) => (fa: HKT<F, A>) => HKT<F, B> { | |
return f => fa => F_.map(f, fa) | |
} | |
export class Identity<A> { | |
+value: A | |
constructor(value: A) { | |
this.value = value | |
} | |
map<B>(f: (a: A) => B): Identity<B> { | |
return new Identity(f(this.value)) | |
} | |
} | |
export const map = <A, B>(f: (a: A) => B, fa: Identity<A>): Identity<B> => { | |
return fa.map(f) | |
} | |
// Functor instance for Identity | |
export const identity: Functor<(<A>(a: A) => Identity<A>)> = { | |
map | |
} | |
// let's lift double to the Identity functor | |
const double = (n: number): number => n * 2 | |
const liftedIdentityDouble = lift(identity)(double) | |
// $FlowFixMe | |
const y1: number = liftedIdentityDouble(new Identity(1)) | |
/* | |
39: const y1: number = liftedIdentityDouble(new Identity(1)) | |
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Identity. This type is incompatible with | |
39: const y1: number = liftedIdentityDouble(new Identity(1)) | |
^^^^^^ number | |
*/ | |
const y2: Identity<number> = liftedIdentityDouble(new Identity(1)) | |
// ok | |
export const mapArray = <A, B>(f: (a: A) => B, fa: Array<A>): Array<B> => { | |
return fa.map(f) | |
} | |
// Functor instance for Array | |
export const array: Functor<(<A>(a: A) => Array<A>)> = { | |
map: mapArray | |
} | |
const xs = [1, 2, 3] | |
const doubleElems = lift(array)(x => x * 2) | |
// $FlowFixMe: `number` is incompatible with `string` | |
const ys: Array<string> = doubleElems(xs) | |
const ys: Array<number> = doubleElems(xs) | |
// $FlowFixMe | |
const zs = doubleElems(new Identity(1)) | |
/* | |
79: const zs = doubleElems(new Identity(1)) | |
^^^^^^^^^^^^^^^ Identity. This type is incompatible with the expected param type of | |
22: export function lift<F, A, B>(F_: Functor<F>): (f: (a: A) => B) => (fa: HKT<F, A>) => HKT<F, B> { | |
^^^^^^^^^ array type | |
*/ |
This file contains 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
/* | |
* Demonstration of a higher-kinded type that takes two type parameters | |
* | |
* @flow | |
*/ | |
import { type Functor, lift } from './HKT' | |
export type HKT2<F, A, B> = $Call<F, [A, B]> | |
export interface Bifunctor<F> { | |
bimap<A, A_, B, B_>( | |
left: (a: A) => A_, | |
right: (b: B) => B_, | |
fab: HKT2<F, A, B> | |
): HKT2<F, A_, B_> | |
} | |
export type Result<T, E> = Ok<T> | Err<E> | |
export class Ok<A> { | |
+value: A | |
constructor(value: A) { | |
this.value = value | |
} | |
map<B>(f: (a: A) => B): Ok<B> { | |
return new Ok(f(this.value)) | |
} | |
} | |
export class Err<A> { | |
+value: A | |
constructor(value: A) { | |
this.value = value | |
} | |
map<B>(f: (a: A) => B): Err<B> { | |
return new Err(f(this.value)) | |
} | |
} | |
export const map = <A, B, E>(f: (a: A) => B, fa: Result<A, E>): Result<B, E> => { | |
if (fa instanceof Ok) { | |
return fa.map(f) | |
} else { | |
return fa | |
} | |
} | |
// This Functor instance should not fix the error type parameter. Flow only | |
// permits type variables in function signatures; so the instance must be | |
// returned from a function. | |
export function resultFunctor<E>(): Functor<(<A>(a: A) => Result<A, E>)> { | |
return { | |
map | |
} | |
} | |
declare var r: Result<number, Error> | |
const stringifyResult = lift(resultFunctor())(JSON.stringify) | |
const a: Result<string, Error> = stringifyResult(r) | |
// $FlowFixMe: `Error` is incompatible with `string` | |
const b: Result<string, string> = stringifyResult(r) | |
// To make a lifted function polymorphic in the error type parameter it is | |
// necessary to make a point-ful function with an explicitly-declared type | |
// variable. | |
function polymorphicStringify<E>(r: Result<*, E>): Result<string, E> { | |
return lift(resultFunctor())(JSON.stringify)(r) | |
} | |
declare var r2: Result<number, string> | |
;(polymorphicStringify(r): Result<string, Error>) | |
;(polymorphicStringify(r2): Result<string, string>) | |
function bimap<T, T_, E, E_>( | |
onOk: (t: T) => T_, | |
onErr: (e: E) => E_, | |
fab: Result<T, E> | |
): Result<T_, E_> { | |
if (fab instanceof Ok) { | |
return fab.map(onOk) | |
} else { | |
return fab.map(onErr) | |
} | |
} | |
export const resultBifunctor: Bifunctor<(<T, E>(_: [T, E]) => Result<T, E>)> = { | |
bimap | |
} | |
// Combining single-parameter and multi-parameter instances in one object | |
export function resultInstances<E>(): | |
& Functor<(<A>(a: A) => Result<A, E>)> | |
& Bifunctor<(<T, E>(_: [T, E]) => Result<T, E>)> | |
{ | |
return { | |
bimap, | |
map | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment