This specification describes a set of JavaScript interfaces and algebraic laws that are common in some other functional languages like Haskell.
A module is a JavaScript object with some static functions and/or values,
"static" meaning they don't use this.
Here is an example:
const FooModule = {
foo: 1, // a value
bar: (x) => x + 1, // a function
}Note that this has nothing to do with JavaScript module systems like ES6 modules, in this specification a module is just an object.
Module signature describes an interface that a module can match. The syntax is very similar to
that of Flow or TypeScript. Here is an example of a signature that the FooModule above matches:
Foo {
foo: number,
bar: (number) => number
}A signature can be parameterized by a type, which looks like this:
ParameterizedFoo<T> {
foo: T,
bar: (T) => T
}A module matches a parameterized signature, if there is some concrete type such that if we
substitute all occurrences of T in the signature with that type,
the module will match resulting signature.
For example if we substitute T in ParameterizedFoo with number we get a signature
that FooModule matches, therefore FooModule matches ParameterizedFoo.
Also functions in a signature can have type variables.
These variables are specified in <> at the beginning of a function type.
For instance:
Bar<T> {
baz: <a>(a) => T<a>
}Notice that T can be parameterized as well.
The number of type variables of T becomes unambiguous when T is used inside the signature.
Also notice that signature level type variables are fixed for a module,
while a function level variable can be substituted with
a different concrete type in each function application.
In other words we must choose what T stands for when we create a module,
and we must choose what a stands for only when we apply baz to some value.
A type signature may be a part of another type signature, which means that we should pass a module that matches that signature in that place. For example:
Baz {
compute: <a>(a, ParameterizedFoo<a>) => a
}The above means that when we apply compute to say number we must pass
as the second argument a module that matches ParameterizedFoo with T=number, like so:
someBaz.compute(10, FooModule)An appropriate definition of equivalence for the given value should ensure that the two values can be safely swapped out in a program that respects abstractions.
For example:
- Two lists are equivalent if they are equivalent at all indices.
- Two plain old JavaScript objects, interpreted as dictionaries, are equivalent when they are equivalent for all keys.
- Two promises are equivalent when they yield equivalent values.
- Two functions are equivalent if they yield equivalent outputs for equivalent inputs.
Note that these examples are not universal, in some cases different definitions of equivalence for those types might be more appropriate. It depends on which exact abstractions you choose to use in a program.
We use ≡ symbol in laws to denote equivalence.
All methods' implementations should only use type information about arguments that is known from the signatures.
Inspecting arguments or values that they produce or contain to get more information about their types is not allowed. In other words methods
should be parametrically polymorphic.
Algebra is a set of requirements for modules, like to match some signature and to obey some laws. If a module satisfies all requirements of an algebra it supports that algebra. An algebra may require to support other algebras.
An algebra may also state other algebra methods which can be derived from new methods. If a module provides a method which could be derived, its behaviour must be equivalent to that of the derivation (or derivations).
- Setoid
- Ord
- Semigroup
- Monoid
- Group
- Semigroupoid
- Category
- Filterable
- Functor
- Bifunctor
- Contravariant
- Profunctor
- Apply
- Applicative
- Alt
- Plus
- Alternative
- Chain
- ChainRec
- Monad
- Foldable
- Extend
- Comonad
- Traversable
Setoid<T> {
equals: (T, T) => boolean
}Module must match the Setoid signature for some type T, and obey following laws:
- Reflexivity:
S.equals(a, a) === true - Symmetry:
S.equals(a, b) === S.equals(b, a) - Transitivity: if
S.equals(a, b)andS.equals(b, c), thenS.equals(a, c)
Ord<T> {
lte: (T, T) => boolean
}Module must match the Ord signature for some type T,
support Setoid algebra for the same T, and obey following laws:
- Totality:
S.lte(a, b)orS.lte(b, a) - Antisymmetry: if
S.lte(a, b)andS.lte(b, a), thenS.equals(a, b) - Transitivity: if
S.lte(a, b)andS.lte(b, c), thenS.lte(a, c)
Semigroup<T> {
concat: (T, T) => T
}Module must match the Semigroup signature for some type T, and obey following laws:
- Associativity:
S.concat(S.concat(a, b), c) ≡ S.concat(a, S.concat(b, c))
Monoid<T> {
empty: () => T
}Module must match the Monoid signature for some type T,
support Semigroup algebra for the same T, and obey following laws:
- Right identity:
M.concat(a, M.empty()) ≡ a - Left identity:
M.concat(M.empty(), a) ≡ a
Group<T> {
invert: (T) => T
}Module must match the Group signature for some type T,
support Monoid algebra for the same T, and obey following laws:
- Right inverse:
G.concat(a, G.invert(a)) ≡ G.empty() - Left inverse:
G.concat(G.invert(a), a) ≡ G.empty()
Semigroupoid<T> {
compose: <i, j, k>(T<i, j>, T<j, k>) => T<i, k>
}Module must match the Semigroupoid signature for some type T, and obey following laws:
- Associativity:
S.compose(S.compose(a, b), c) ≡ S.compose(a, S.compose(b, c))
Category<T> {
id: <i, j>() => T<i, j>
}Module must match the Category signature for some type T,
support Semigroupoid algebra for the same T, and obey following laws:
- Right identity:
M.compose(a, M.id()) ≡ a - Left identity:
M.compose(M.id(), a) ≡ a
Filterable<T> {
filter: <a>(a => boolean, T<a>) => T<a>
}Module must match the Filterable signature for some type T, and obey following laws:
- Distributivity:
F.filter(x => f(x) && g(x), a) ≡ F.filter(g, F.filter(f, a)) - Identity:
F.filter(x => true, a) ≡ a - Annihilation:
F.filter(x => false, a) ≡ F.filter(x => false, b)
Functor<T> {
map: <a, b>(a => b, T<a>) => T<b>
}Module must match the Functor signature for some type T, and obey following laws:
- Identity:
F.map(x => x, a) ≡ a - Composition:
F.map(x => f(g(x)), a) ≡ F.map(f, F.map(g, a))
Bifunctor<T> {
bimap: <a, b, c, d>(a => b, c => d, T<a, c>) => T<b, d>
}Module must match the Bifunctor signature for some type T,
support Functor algebra for all types U created by setting the first parameter of T
to an arbitrary concrete type (for example type U<a> = T<number, a>),
and obey following laws:
- Identity:
B.bimap(x => x, x => x, a) ≡ a - Composition:
B.bimap(x => f(g(x)), x => h(i(x)), a) ≡ B.bimap(f, h, B.bimap(g, i, a))
- Functor's map:
A.map = (f, u) => A.bimap(x => x, f, u)
Contravariant<T> {
contramap: <a, b>(a => b, T<b>) => T<a>
}Module must match the Contravariant signature for some type T, and obey following laws:
- Identity:
F.contramap(x => x, a) ≡ a - Composition:
F.contramap(x => f(g(x)), a) ≡ F.contramap(g, F.contramap(f, a))
Profunctor<T> {
promap: <a, b, c, d>(a => b, c => d, T<b, c>) => T<a, d>
}Module must match the Profunctor signature for some type T,
support Functor algebra for all types U created by setting the first parameter of T
to an arbitrary concrete type (for example type U<a> = T<number, a>),
and obey following laws:
- Identity:
P.promap(x => x, x => x, a) ≡ a - Composition:
P.promap(x => f(g(x)), x => h(i(x)), a) ≡ P.promap(g, h, P.promap(f, i, a))
- Functor's map:
A.map = (f, u) => A.promap(x => x, f, u)
Apply<T> {
ap: <a, b>(T<a => b>, T<a>) => T<b>
}Module must match the Apply signature for some type T,
support Functor algebra for the same T, and obey following laws:
- Composition:
A.ap(A.ap(A.map(f => g => x => f(g(x)), a), u), v) ≡ A.ap(a, A.ap(u, v))
Applicative<T> {
of: <a>(a) => T<a>
}Module must match the Applicative signature for some type T,
support Apply algebra for the same T, and obey following laws:
- Identity:
A.ap(A.of(x => x), v) ≡ v - Homomorphism:
A.ap(A.of(f), A.of(x)) ≡ A.of(f(x)) - Interchange:
A.ap(u, A.of(y)) ≡ A.ap(A.of(f => f(y)), u)
- Functor's map:
A.map = (f, u) => A.ap(A.of(f), u)
Alt<T> {
alt: <a>(T<a>, T<a>) => T<a>
}Module must match the Alt signature for some type T,
support Functor algebra for the same T, and obey following laws:
- Associativity:
A.alt(A.alt(a, b), c) ≡ A.alt(a, A.alt(b, c)) - Distributivity:
A.map(f, A.alt(a, b)) ≡ A.alt(A.map(f, a), A.map(f, b))
Plus<T> {
zero: <a>() => T<a>
}Module must match the Plus signature for some type T,
support Alt algebra for the same T, and obey following laws:
- Right identity:
P.alt(a, P.zero()) ≡ a - Left identity:
P.alt(P.zero(), a) ≡ a - Annihilation:
P.map(f, P.zero()) ≡ P.zero()
Module must support Applicative and Plus algebras for a same T,
and obey following laws:
- Distributivity:
A.ap(A.alt(a, b), c) ≡ A.alt(A.ap(a, c), A.ap(b, c)) - Annihilation:
A.ap(A.zero(), a) ≡ A.zero()
Chain<T> {
chain: <a, b>(a => T<b>, T<a>) => T<b>
}Module must match the Chain signature for some type T,
support Apply algebra for the same T, and obey following laws:
- Associativity:
M.chain(g, M.chain(f, u)) ≡ M.chain(x => M.chain(g, f(x)), u)
- Apply's ap:
A.ap = (uf, ux) => A.chain(f => A.map(f, ux), uf)
ChainRec<T> {
chainRec: <a, b>((a => Next<a>, b => Done<b>, a) => T<Next<a> | Done<b>>, a) => T<b>
}Module must match the ChainRec signature for some type T,
support Chain algebra for the same T, and obey following laws:
- Equivalence:
C.chainRec((next, done, v) => p(v) ? C.map(done, d(v)) : C.map(next, n(v)), i) ≡ (function step(v) { return p(v) ? d(v) : C.chain(step, n(v)) }(i)) - Stack usage of
C.chainRec(f, i)must be at most a constant multiple of the stack usage offitself.
Module must support Applicative and Chain algebras for a same T,
and obey following laws:
- Left identity:
M.chain(f, M.of(a)) ≡ f(a) - Right identity:
M.chain(M.of, u) ≡ u
- Functor's map:
A.map = (f, u) => A.chain(x => A.of(f(x)), u)
Foldable<T> {
reduce: <a, b>((a, b) => a, a, T<b>) => a
}Module must match the Foldable signature for some type T,
and obey following laws:
F.reduce ≡ (f, x, u) => F.reduce((acc, y) => acc.concat([y]), [], u).reduce(f, x)
Extend<T> {
extend: <a, b>(T<a> => b, T<a>) => T<b>
}Module must match the Extend signature for some type T, support Functor algebra for the same T,
and obey following laws:
- Associativity:
E.extend(f, E.extend(g, w)) ≡ E.extend(_w => f(E.extend(g, _w)), w)
Comonad<T> {
extract: <a>(T<a>) => a
}Module must match the Comonad signature for some type T, support Extend algebra for the same T, and obey following laws:
- Left identity:
C.extend(C.extract, w) ≡ w - Right identity:
C.extract(C.extend(f, w)) ≡ f(w)
In the following signature Applicative<U> stands for a module that must support
Applicative algebra for some type U that a => U<b> returns.
For example, if the second argument of traverse() is a function that returns Array,
then the first argument must be a module that supports Applicative for Array.
Traversable<T> {
traverse: <U, a, b>(Applicative<U>, a => U<b>, T<a>) => U<T<b>>
}Module must match the Traversable signature for some type T,
support Functor and Foldable algebras for the same T, and obey following laws:
- Naturality:
f(T.traverse(A, x => x, u)) ≡ T.traverse(B, f, u)for anyfsuch thatB.map(g, f(a)) ≡ f(A.map(g, a)) - Identity:
T.traverse(F, F.of, u) ≡ F.of(u)for any ApplicativeF - Composition:
T.traverse(Compose(A, B), x => x, u) ≡ A.map(v => T.traverse(B, x => x, v), T.traverse(A, x => x, u))forComposedefined bellow and for any ApplicativesAandB
function Compose(A, B) {
return {
of(x) {
return A.of(B.of(x))
},
ap(a1, a2) {
return A.ap(A.map(b1 => b2 => B.ap(b1, b2), a1), a2)
},
map(f, a) {
return A.map(b => B.map(f, b), a)
},
}
}- Foldable's reduce:
F.reduce = (f, acc, u) => {
const of = () => acc
const map = (_, x) => x
const ap = f
return F.traverse({of, map, ap}, x => x, u)
}- Functor's map:
F.map = (f, u) => {
const of = (x) => x
const map = (f, a) => f(a)
const ap = (f, a) => f(a)
return F.traverse({of, map, ap}, f, u)
}