funfix-types
Version:
Sub-package of Funfix defining type classes inspired by Haskell's standard library
206 lines (192 loc) • 6.42 kB
text/typescript
/*!
* Copyright (c) 2017 by The Funfix Project Developers.
* Some rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { IllegalArgumentError, NotImplementedError } from "funfix-core"
/**
* Given a type `T` representing instances of a class `C`, the type
* `Constructor<T>` is the type of the class `C`.
*
* This type emulates
* [Class<T> from Flow]{@link https://flow.org/en/docs/types/utilities/#classt-a-classtoc-idtoc-class-hreftoc-classa}.
*
* Note that in TypeScript constructors can also be `protected` or `private`
* and unfortunately specifying `{ new(): T }` is thus insufficient.
* Which is why, for classes without a public constructor, we have to
* specify a `_funErasure` (static) member as a property, to help the compiler
* infer type `T`.
*
* Example:
*
* ```typescript
* class NumBox { constructor(public num: number) {} }
* class GenBox<A> { constructor(public a: A) {} }
*
* function getDefault<F>(ref: Constructor<F>): Option<F> {
* if ((ref as any)._default) return Some(ref._default)
* return None
* }
*
* (NumBox as any)._default = new NumBox(10)
* (GenBox as any)._default = new GenBox("value")
*
* const r1: Option<NumBox> = getDefault(NumBox)
* const r2: Option<GenBox<any>> = getDefault(GenBox)
* ```
*
* And for classes with a private constructor:
*
* ```typescript
* class PrivateBox<A> {
* private constructor(public a: A) {}
*
* static _funErasure: PrivateBox<any> // leaving undefined
* }
*
* const F = PrivateBox as any
* F._default = new F("hello")
*
* const r: Option<PrivateBox<any>> = getDefault(NumBox)
* ```
*/
export type Constructor<T> =
{ new(...args: any[]): T } | { readonly _funErasure: T }
/**
* The `TypeClass` interface is to be implemented by type class
* definitions, exposing IDs needed for discovery management.
*
* Only of interest to type class authors.
*/
export type TypeClass<F> = Constructor<F> & {
readonly _funTypeId: string
readonly _funSupertypeIds: string[]
}
/**
* Lightweight encoding for higher kinded types.
*
* Inspired by the
* [Lightweight higher-kinded polymorphism]{@link https://www.cl.cam.ac.uk/~jdy22/papers/lightweight-higher-kinded-polymorphism.pdf}
* paper.
*
*/
export interface HK<F, A> {
/** Trick for achieving nominal typing. */
readonly _funKindF: F
/** Trick for achieving nominal typing. */
readonly _funKindA: A
}
/**
* Data type for expressing equivalence in type class laws.
*
* @final
*/
export class Equiv<A> {
private constructor(
public readonly lh: A,
public readonly rh: A) {}
static of<A>(lh: A, rh: A): Equiv<A> {
return new Equiv(lh, rh)
}
}
/**
* Given a {@link TypeClass} definition in `tc`, register an `instance`
* for the given {@link Constructor} specified by `c` that implements the
* given type class.
*
* Example:
*
* ```typescript
* registerTypeClassInstance(Functor)(Box, new BoxFunctorInstance())
* ```
*
* Data types can have only one implementation for a given type class.
* Multiple implementations are not allowed, therefore registration needs
* to happen at most once. If registration happens multiple times for
* instances of the same data type and type class, then an exception is
* going to be raised:
*
* ```typescript
* // Ok
* registerTypeClassInstance(Functor)(Box, new FunctorInstance1())
*
* // IllegalArgumentError: Type class coherence issue,
* // Functor<Box> is already defined!
* registerTypeClassInstance(Functor)(Box, new FunctorInstance1())
* ```
*
* Note that type classes can have super types. So for example registering
* a `Monad` instance will also register a `Functor` instance, along with
* `Applicative`. The registration of supertypes however does not trigger
* coherence errors. In this example, if you try registering a `Monad`,
* but a `Functor` was already registered, then that the given `Monad`
* will simply not be registered as a `Functor` for that data type.
*
* This is legal:
*
* ```typescript
* // Ok
* registerTypeClassInstance(Functor)(Box, new FunctorInstance())
*
* // Ok, even though a Monad is also a Functor
* registerTypeClassInstance(Functor)(Box, new MonadInstance())
* ```
*
* @throws `IllegalArgumentError` in case such a type class instance
* was already specified, thus leading to a coherence issue.
*/
export function registerTypeClassInstance<F>(tc: TypeClass<F>):
<T>(c: Constructor<T>, instance: F) => void {
return <T>(c: Constructor<T>, instance: F) => {
const obj = c as any
const types: {[id: string]: any} = (obj["_funTypes"] || {})
obj["_funTypes"] = types
const existing = types[tc._funTypeId]
if (existing) {
// If trying to register the same instance, then ignore
if (existing === instance) return
// Coherence issue
const name = existing.constructor.name
throw new IllegalArgumentError(
"Type class coherence issue, " +
`${name}<${(c as any).name}> is already defined!`
)
}
types[tc._funTypeId] = instance
for (const id of tc._funSupertypeIds) {
if (!types[id]) types[id] = instance
}
}
}
/**
* Given a {@link TypeClass} instance and a {@link Constructor} reference,
* returns its associated type class implementation if it exists, or throws
* a `NotImplementedError` in case there's no such association.
*
* ```typescript
* import { Option, Functor, getTypeClass } from "funfix"
*
* const F: Functor<Option<any>> = getTypeClass(Functor, Option)
* ```
*/
export function getTypeClassInstance<F>(tc: TypeClass<F>):
<T>(c: Constructor<T>) => F {
return <T>(c: Constructor<T>) => {
const obj = c as any
const types: {[id: string]: any} = obj["_funTypes"] || {}
const instance = types[tc._funTypeId]
if (instance) return instance as any
throw new NotImplementedError(`${(tc as any).name}<${obj.name}>`)
}
}