Skip to content

Instantly share code, notes, and snippets.

@hallettj
Last active June 27, 2019 15:19
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save hallettj/0fde5bd4c3ce5a5f6d50db6236aaa39e to your computer and use it in GitHub Desktop.
Save hallettj/0fde5bd4c3ce5a5f6d50db6236aaa39e to your computer and use it in GitHub Desktop.
Concept for emulating higher-kinded types in Flow via type-level functions
/*
* 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
*/
/*
* 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