This is a specification for common algebraic types in JavaScript based on Fantasy Land Specification.
Fantasy Land uses methods as a base for types. A type instance in Fantasy Land
is an object with certain methods. For example a Functor type instance must be an object
that has .map()
method.
In Static Land a type is just a collection of static functions, and instances of a type can be any values, including primitives (Number, Boolean etc.)
For example we can implement an Addition type, that uses numbers as it's instances, and satisfies the Monoid laws:
const Addition = {
empty() {
return 0
},
concat(a, b) {
return a + b
},
}
- No name clashes. Since a type is just a collection of functions that don't share any namespace we don't have problems with name clashes.
- We can implement many types for same values. For example we can implemet two Monoids for numbers — Addition and Multiplication.
- We can implement types with primitives (Number, Boolean etc.) as values.
- We can implemet seamless types. For example we can make a type with arrays as values, and user won't have to wrap/unwrap values to some wrapper class with Fantasy Land methods.
- We have to pass around types more often.
In Fantasy Land some generic code can be written using only methods,
we have to pass types only for
of
andempty
. In Static Land we have to pass types for any generic code.
A type in Static Land is a JavaScript object with static functions as values.
'Static' means that functions don't use this
,
they are not methods and can be detached from the type object.
The object is just a container for functions.
const map = MyType.map
const of = MyType.of
// This should work
map(x => x + 1, of(41)) // MyType(42)
Each function of a type must be curried — if it's called with not enough
arguments, it must return a function that expects missing arguments
(Use curry
, curryAll
, or
R.curry
to create curried functions)
const incLifted = MyType.map(x => x + 1)
incLifted(MyType.of(41)) // MyType(42)
Each method in this spec comes with a type signature, that looks like the following.
map :: Functor f => (a → b) → f a → f b
We use syntax similar to Haskell's. You can learn about it from Ramda's wiki or from book "Professor Frisby's Mostly Adequate Guide to Functional Programming"
This spec uses the followng extensions to the type signature syntax:
(a, b) → c
denotest a not curried function of 2 arguments. Same for more arguments.- An upper case letter denotes the type object of the type denoted with the same
letter in lower case. For instance a function with type
F → f → a
can be called asfn(F, F.of(1))
.
If a method called with incorrect types the behaviour is unspecified,
the recommended behaviour is to throw a TypeError
. Also if a method accepts a function it
must call the function according to the type signature, passing arguments of correct types
and not passing any extra arguments.
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.
We use ≡
symbol in laws to denote equivalence.
An algebra is a set of values (type instances, and other values), a set of operators (type methods) that it is closed under and some laws it must obey.
Each algebra is a separate specification. An algebra may have dependencies on other algebras which must be implemented. An algebra may also state other algebra methods which do not need to be implemented and how they can be derived from new methods.
If a method can be derived and the type also has a hand written version of that method,
derived and hand written methods must be equivalent. And if two derived version of a method can
be created (for instance we can derive map
using ap
or chain
), they must be equivalent.
equals :: Setoid s => s → s → Boolean
- 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)
concat :: Semigroup s => s → s → s
- Associativity:
S.concat(S.concat(a, b), c) ≡ S.concat(a, S.concat(b, c))
- Semigroup
empty :: Monoid m => () → m
- Right identity:
M.concat(a, M.empty()) ≡ a
- Left identity:
M.concat(M.empty(), a) ≡ a
map :: Functor f => (a → b) → f a → f b
- Identity:
F.map(x => x, a) ≡ a
- Composition:
F.map(x => f(g(x)), a) ≡ F.map(f, F.map(g, a))
- Functor
ap :: Apply f => f (a → b) → f a → f b
- Composition:
A.ap(A.ap(A.map(f => g => x => f(g(x)), a), u), v) ≡ A.ap(a, A.ap(u, v))
- Apply
of :: Applicative f => a → f a
- 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)
- Apply
chain :: Chain m => (a → m b) → m a → m b
- 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)
- Applicative
- Chain
- 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)
reduce :: Foldable f => ((a, b) → a) → a → f b → a
F.reduce ≡ (f, x, u) => F.toArray(u).reduce(f, x)
- toArray:
F.toArray = u => F.reduce((acc, x) => acc.concat([x]), [], u)
extend :: Extend e => (e a → b) → e a → e b
- Associativity:
E.extend(f, E.extend(g, w)) ≡ E.extend(_w => f(E.extend(g, _w)), w)
- Functor
- Extend
extract :: Comonad c => c a → a
C.extend(C.extract, w) ≡ w
C.extract(C.extend(f, w)) ≡ f(w)
C.extend(f, w) ≡ C.map(f, C.extend(x => x, w))