Fantasy Land Specification
(aka "Algebraic JavaScript Specification")
This project specifies interoperability of common algebraic
structures:
General
An algebra is a set of values, a set of operators that it is closed
under and some laws it must obey.
Each Fantasy Land algebra is a separate specification. An algebra may
have dependencies on other algebras which must be implemented.
Terminology
- "value" is any JavaScript value, including any which have the
structures defined below.
- "equivalent" is an appropriate definition of equivalence for the given value.
The definition 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.
Prefixed method names
In order for a data type to be compatible with Fantasy Land, its values must
have certain properties. These properties are all prefixed by fantasy-land/
.
For example:
MyType.prototype['fantasy-land/map'] = ...
Further in this document unprefixed names are used just to reduce noise.
For convenience you can use fantasy-land
package:
var fl = require('fantasy-land')
MyType.prototype[fl.map] = ...
var foo = bar[fl.map](x => x + 1)
Type representatives
Certain behaviours are defined from the perspective of a member of a type.
Other behaviours do not require a member. Thus certain algebras require a
type to provide a value-level representative (with certain properties). The
Identity type, for example, could provide Id
as its type representative:
Id :: TypeRep Identity
.
If a type provides a type representative, each member of the type must have
a constructor
property which is a reference to the type representative.
Algebras
Setoid
a.equals(a) === true
(reflexivity)a.equals(b) === b.equals(a)
(symmetry)- If
a.equals(b)
and b.equals(c)
, then a.equals(c)
(transitivity)
equals
method
equals :: Setoid a => a ~> a -> Boolean
A value which has a Setoid must provide an equals
method. The
equals
method takes one argument:
a.equals(b)
-
b
must be a value of the same Setoid
- If
b
is not the same Setoid, behaviour of equals
is
unspecified (returning false
is recommended).
-
equals
must return a boolean (true
or false
).
Ord
A value that implements the Ord specification must also implement
the Setoid specification.
a.lte(b)
or b.lte(a)
(totality)- If
a.lte(b)
and b.lte(a)
, then a.equals(b)
(antisymmetry) - If
a.lte(b)
and b.lte(c)
, then a.lte(c)
(transitivity)
lte
method
lte :: Ord a => a ~> a -> Boolean
A value which has an Ord must provide a lte
method. The
lte
method takes one argument:
a.lte(b)
-
b
must be a value of the same Ord
- If
b
is not the same Ord, behaviour of lte
is
unspecified (returning false
is recommended).
-
lte
must return a boolean (true
or false
).
Semigroup
a.concat(b).concat(c)
is equivalent to a.concat(b.concat(c))
(associativity)
concat
method
concat :: Semigroup a => a ~> a -> a
A value which has a Semigroup must provide a concat
method. The
concat
method takes one argument:
s.concat(b)
-
b
must be a value of the same Semigroup
- If
b
is not the same semigroup, behaviour of concat
is
unspecified.
-
concat
must return a value of the same Semigroup.
Monoid
A value that implements the Monoid specification must also implement
the Semigroup specification.
m.concat(M.empty())
is equivalent to m
(right identity)M.empty().concat(m)
is equivalent to m
(left identity)
empty
method
empty :: Monoid m => () -> m
A value which has a Monoid must provide an empty
function on its
type representative:
M.empty()
Given a value m
, one can access its type representative via the
constructor
property:
m.constructor.empty()
empty
must return a value of the same Monoid
Functor
u.map(a => a)
is equivalent to u
(identity)u.map(x => f(g(x)))
is equivalent to u.map(g).map(f)
(composition)
map
method
map :: Functor f => f a ~> (a -> b) -> f b
A value which has a Functor must provide a map
method. The map
method takes one argument:
u.map(f)
-
f
must be a function,
- If
f
is not a function, the behaviour of map
is
unspecified. f
can return any value.- No parts of
f
's return value should be checked.
-
map
must return a value of the same Functor
Contravariant
u.contramap(a => a)
is equivalent to u
(identity)u.contramap(x => f(g(x)))
is equivalent to u.contramap(f).contramap(g)
(composition)
contramap
method
contramap :: Contravariant f => f a ~> (b -> a) -> f b
A value which has a Contravariant must provide a contramap
method. The
contramap
method takes one argument:
u.contramap(f)
-
f
must be a function,
- If
f
is not a function, the behaviour of contramap
is
unspecified. f
can return any value.- No parts of
f
's return value should be checked.
-
contramap
must return a value of the same Contravariant
Apply
A value that implements the Apply specification must also
implement the Functor specification.
v.ap(u.ap(a.map(f => g => x => f(g(x)))))
is equivalent to v.ap(u).ap(a)
(composition)
ap
method
ap :: Apply f => f a ~> f (a -> b) -> f b
A value which has an Apply must provide an ap
method. The ap
method takes one argument:
a.ap(b)
-
b
must be an Apply of a function,
- If
b
does not represent a function, the behaviour of ap
is
unspecified.
-
a
must be an Apply of any value
-
ap
must apply the function in Apply b
to the value in
Apply a
- No parts of return value of that function should be checked.
Applicative
A value that implements the Applicative specification must also
implement the Apply specification.
v.ap(A.of(x => x))
is equivalent to v
(identity)A.of(x).ap(A.of(f))
is equivalent to A.of(f(x))
(homomorphism)A.of(y).ap(u)
is equivalent to u.ap(A.of(f => f(y)))
(interchange)
of
method
of :: Applicative f => a -> f a
A value which has an Applicative must provide an of
function on its
type representative. The of
function takes
one argument:
F.of(a)
Given a value f
, one can access its type representative via the
constructor
property:
f.constructor.of(a)
-
of
must provide a value of the same Applicative
- No parts of
a
should be checked
Alt
A value that implements the Alt specification must also implement
the Functor specification.
a.alt(b).alt(c)
is equivalent to a.alt(b.alt(c))
(associativity)a.alt(b).map(f)
is equivalent to a.map(f).alt(b.map(f))
(distributivity)
alt
method
alt :: Alt f => f a ~> f a -> f a
A value which has a Alt must provide a alt
method. The
alt
method takes one argument:
a.alt(b)
-
b
must be a value of the same Alt
- If
b
is not the same Alt, behaviour of alt
is
unspecified. a
and b
can contain any value of same type.- No parts of
a
's and b
's containing value should be checked.
-
alt
must return a value of the same Alt.
Plus
A value that implements the Plus specification must also implement
the Alt specification.
x.alt(A.zero())
is equivalent to x
(right identity)A.zero().alt(x)
is equivalent to x
(left identity)A.zero().map(f)
is equivalent to A.zero()
(annihilation)
zero
method
zero :: Plus f => () -> f a
A value which has a Plus must provide an zero
function on its
type representative:
A.zero()
Given a value x
, one can access its type representative via the
constructor
property:
x.constructor.zero()
zero
must return a value of the same Plus
Alternative
A value that implements the Alternative specification must also implement
the Applicative and Plus specifications.
x.ap(f.alt(g))
is equivalent to x.ap(f).alt(x.ap(g))
(distributivity)x.ap(A.zero())
is equivalent to A.zero()
(annihilation)
Foldable
u.reduce
is equivalent to u.reduce((acc, x) => acc.concat([x]), []).reduce
reduce
method
reduce :: Foldable f => f a ~> ((b, a) -> b, b) -> b
A value which has a Foldable must provide a reduce
method. The reduce
method takes two arguments:
u.reduce(f, x)
-
f
must be a binary function
- if
f
is not a function, the behaviour of reduce
is unspecified. - The first argument to
f
must be the same type as x
. f
must return a value of the same type as x
.- No parts of
f
's return value should be checked.
-
x
is the initial accumulator value for the reduction
- No parts of
x
should be checked.
Traversable
A value that implements the Traversable specification must also
implement the Functor and Foldable specifications.
-
t(u.traverse(F, x => x))
is equivalent to u.traverse(G, t)
for any
t
such that t(a).map(f)
is equivalent to t(a.map(f))
(naturality)
-
u.traverse(F, F.of)
is equivalent to F.of(u)
for any Applicative F
(identity)
-
u.traverse(Compose, x => new Compose(x))
is equivalent to
new Compose(u.traverse(F, x => x).map(x => x.traverse(G, x => x)))
for
Compose
defined below and any Applicatives F
and G
(composition)
var Compose = function(c) {
this.c = c;
};
Compose.of = function(x) {
return new Compose(F.of(G.of(x)));
};
Compose.prototype.ap = function(f) {
return new Compose(this.c.ap(f.c.map(u => y => y.ap(u))));
};
Compose.prototype.map = function(f) {
return new Compose(this.c.map(y => y.map(f)));
};
traverse
method
traverse :: Applicative f, Traversable t => t a ~> (TypeRep f, a -> f b) -> f (t b)
A value which has a Traversable must provide a traverse
method. The traverse
method takes two arguments:
u.traverse(A, f)
-
A
must be the type representative of an
Applicative.
-
f
must be a function which returns a value
- If
f
is not a function, the behaviour of traverse
is
unspecified. f
must return a value of the type represented by A
.
-
traverse
must return a value of the type represented by A
.
Chain
A value that implements the Chain specification must also
implement the Apply specification.
m.chain(f).chain(g)
is equivalent to m.chain(x => f(x).chain(g))
(associativity)
chain
method
chain :: Chain m => m a ~> (a -> m b) -> m b
A value which has a Chain must provide a chain
method. The chain
method takes one argument:
m.chain(f)
-
f
must be a function which returns a value
- If
f
is not a function, the behaviour of chain
is
unspecified. f
must return a value of the same Chain
-
chain
must return a value of the same Chain
ChainRec
A value that implements the ChainRec specification must also implement the Chain specification.
M.chainRec((next, done, v) => p(v) ? d(v).map(done) : n(v).map(next), i)
is equivalent to
(function step(v) { return p(v) ? d(v) : n(v).chain(step); }(i))
(equivalence)- Stack usage of
M.chainRec(f, i)
must be at most a constant multiple of the stack usage of f
itself.
chainRec
method
chainRec :: ChainRec m => ((a -> c, b -> c, a) -> m c, a) -> m b
A Type which has a ChainRec must provide a chainRec
function on its
type representative. The chainRec
function
takes two arguments:
M.chainRec(f, i)
Given a value m
, one can access its type representative via the
constructor
property:
m.constructor.chainRec(f, i)
f
must be a function which returns a value
- If
f
is not a function, the behaviour of chainRec
is unspecified. f
takes three arguments next
, done
, value
next
is a function which takes one argument of same type as i
and can return any valuedone
is a function which takes one argument and returns the same type as the return value of next
value
is some value of the same type as i
f
must return a value of the same ChainRec which contains a value returned from either done
or next
chainRec
must return a value of the same ChainRec which contains a value of same type as argument of done
Monad
A value that implements the Monad specification must also implement
the Applicative and Chain specifications.
M.of(a).chain(f)
is equivalent to f(a)
(left identity)m.chain(M.of)
is equivalent to m
(right identity)
Extend
A value that implements the Extend specification must also implement the Functor specification.
w.extend(g).extend(f)
is equivalent to w.extend(_w => f(_w.extend(g)))
extend
method
extend :: Extend w => w a ~> (w a -> b) -> w b
An Extend must provide an extend
method. The extend
method takes one argument:
w.extend(f)
-
f
must be a function which returns a value
- If
f
is not a function, the behaviour of extend
is
unspecified. f
must return a value of type v
, for some variable v
contained in w
.- No parts of
f
's return value should be checked.
-
extend
must return a value of the same Extend.
Comonad
A value that implements the Comonad specification must also implement the Extend specification.
w.extend(_w => _w.extract())
is equivalent to w
(left identity)w.extend(f).extract()
is equivalent to f(w)
(right identity)
extract :: Comonad w => w a ~> () -> a
A value which has a Comonad must provide an extract
method on itself.
The extract
method takes no arguments:
w.extract()
extract
must return a value of type v
, for some variable v
contained in w
.
v
must have the same type that f
returns in extend
.
Bifunctor
A value that implements the Bifunctor specification must also implement
the Functor specification.
p.bimap(a => a, b => b)
is equivalent to p
(identity)p.bimap(a => f(g(a)), b => h(i(b))
is equivalent to p.bimap(g, i).bimap(f, h)
(composition)
bimap
method
bimap :: Bifunctor f => f a c ~> (a -> b, c -> d) -> f b d
A value which has a Bifunctor must provide a bimap
method. The bimap
method takes two arguments:
c.bimap(f, g)
-
f
must be a function which returns a value
- If
f
is not a function, the behaviour of bimap
is unspecified. f
can return any value.- No parts of
f
's return value should be checked.
-
g
must be a function which returns a value
- If
g
is not a function, the behaviour of bimap
is unspecified. g
can return any value.- No parts of
g
's return value should be checked.
-
bimap
must return a value of the same Bifunctor.
Profunctor
A value that implements the Profunctor specification must also implement
the Functor specification.
p.promap(a => a, b => b)
is equivalent to p
(identity)p.promap(a => f(g(a)), b => h(i(b)))
is equivalent to p.promap(f, i).promap(g, h)
(composition)
promap
method
promap :: Profunctor p => p b c ~> (a -> b, c -> d) -> p a d
A value which has a Profunctor must provide a promap
method.
The profunctor
method takes two arguments:
c.promap(f, g)
-
f
must be a function which returns a value
- If
f
is not a function, the behaviour of promap
is unspecified. f
can return any value.- No parts of
f
's return value should be checked.
-
g
must be a function which returns a value
- If
g
is not a function, the behaviour of promap
is unspecified. g
can return any value.- No parts of
g
's return value should be checked.
-
promap
must return a value of the same Profunctor
Derivations
When creating data types which satisfy multiple algebras, authors may choose
to implement certain methods then derive the remaining methods. Derivations:
-
equals
may be derived from lte
:
function(other) { return this.lte(other) && other.lte(this); }
-
map
may be derived from ap
and of
:
function(f) { return this.ap(this.of(f)); }
-
map
may be derived from chain
and of
:
function(f) { return this.chain(a => this.of(f(a))); }
-
map
may be derived from bimap
:
function(f) { return this.bimap(a => a, f); }
-
map
may be derived from promap
:
function(f) { return this.promap(a => a, f); }
-
ap
may be derived from chain
:
function(m) { return m.chain(f => this.map(f)); }
-
reduce
may be derived as follows:
function(f, acc) {
function Const(value) {
this.value = value;
}
Const.of = function(_) {
return new Const(acc);
};
Const.prototype.map = function(_) {
return this;
};
Const.prototype.ap = function(b) {
return new Const(f(b.value, this.value));
};
return this.traverse(x => new Const(x), Const.of).value;
}
-
map
may be derived as follows:
function(f) {
function Id(value) {
this.value = value;
};
Id.of = function(x) {
return new Id(x);
};
Id.prototype.map = function(f) {
return new Id(f(this.value));
};
Id.prototype.ap = function(b) {
return new Id(this.value(b.value));
};
return this.traverse(x => Id.of(f(x)), Id.of).value;
}
If a data type provides a method which could be derived, its behaviour must
be equivalent to that of the derivation (or derivations).
Notes
- If there's more than a single way to implement the methods and
laws, the implementation should choose one and provide wrappers for
other uses.
- It's discouraged to overload the specified methods. It can easily
result in broken and buggy behaviour.
- It is recommended to throw an exception on unspecified behaviour.
- An
Id
container which implements many of the methods is provided in
internal/id.js
.
Alternatives
There also exists Static Land Specification
with the exactly same ideas as Fantasy Land but based on static methods instead of instance methods.