(aka "Algebraic JavaScript Specification")
This project specifies interoperability of common algebraic structures:
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.
- "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.
- Add
fantasy-land
package as a peer dependency. Add in yourpackage.json
:
{
...
"peerDependencies": {
"fantasy-land": "*"
},
...
}
- The
fantasy-land
package exposes method names, you should use them for you Fantasy Land methods:
var fl = require('fantasy-land')
// ...
MyType.prototype[fl.map] = function(fn) {
// Here goes implementation of map for your type...
}
- Add library npm package, and
fantasy-land
as your normal dependecies:
{
...
"dependencies": {
"some-fl-compatible-lib": "1.0.0",
"fantasy-land": "1.0.0"
},
...
}
- If you don't want to access Fantasy Land methods directly
(for example if you use two libraries that talk to each other using Fantasy Land),
then that's it — simply install them and use as you normally would,
only install
fantasy-land
package as well.
If you do want to access Fantasy Land methods, do it like this:
var fl = require('fantasy-land')
var Something = require('some-fl-compatible-lib')
var foo = new Something(1)
var bar = foo[fl.map](x => x + 1)
a.equals(a) === true
(reflexivity)a.equals(b) === b.equals(a)
(symmetry)- If
a.equals(b)
andb.equals(c)
, thena.equals(c)
(transitivity)
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 ofequals
is unspecified (returningfalse
is recommended).
- If
-
equals
must return a boolean (true
orfalse
).
a.concat(b).concat(c)
is equivalent toa.concat(b.concat(c))
(associativity)
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 ofconcat
is unspecified.
- If
-
concat
must return a value of the same Semigroup.
A value that implements the Monoid specification must also implement the Semigroup specification.
m.concat(m.empty())
is equivalent tom
(right identity)m.empty().concat(m)
is equivalent tom
(left identity)
A value which has a Monoid must provide an empty
method on itself or
its constructor
object. The empty
method takes no arguments:
m.empty()
m.constructor.empty()
empty
must return a value of the same Monoid
u.map(a => a)
is equivalent tou
(identity)u.map(x => f(g(x)))
is equivalent tou.map(g).map(f)
(composition)
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 ofmap
is unspecified. f
can return any value.
- If
-
map
must return a value of the same Functor
A value that implements the Apply specification must also implement the Functor specification.
a.map(f => g => x => f(g(x))).ap(u).ap(v)
is equivalent toa.ap(u.ap(v))
(composition)
A value which has an Apply must provide an ap
method. The ap
method takes one argument:
a.ap(b)
-
a
must be an Apply of a function,- If
a
does not represent a function, the behaviour ofap
is unspecified.
- If
-
b
must be an Apply of any value -
ap
must apply the function in Applya
to the value in Applyb
A value that implements the Applicative specification must also implement the Apply specification.
a.of(x => x).ap(v)
is equivalent tov
(identity)a.of(f).ap(a.of(x))
is equivalent toa.of(f(x))
(homomorphism)u.ap(a.of(y))
is equivalent toa.of(f => f(y)).ap(u)
(interchange)
A value which has an Applicative must provide an of
method on itself
or its constructor
object. The of
method takes one argument:
a.of(b)
a.constructor.of(b)
-
of
must provide a value of the same Applicative- No parts of
b
should be checked
- No parts of
u.reduce
is equivalent tou.reduce((acc, x) => acc.concat([x]), []).reduce
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 ofreduce
is unspecified. - The first argument to
f
must be the same type asx
. f
must return a value of the same type asx
- if
-
x
is the initial accumulator value for the reduction
A value that implements the Traversable specification must also implement the Functor and Foldable specifications.
-
t(u.sequence(f.of))
is equivalent tou.map(t).sequence(g.of)
for anyt
such thatt(a).map(f)
is equivalent tot(a.map(f))
(naturality) -
u.map(F.of).sequence(F.of)
is equivalent toF.of(u)
for any ApplicativeF
(identity) -
u.map(x => new Compose(x)).sequence(Compose.of)
is equivalent tonew Compose(u.sequence(F.of).map(v => v.sequence(G.of)))
forCompose
defined below and any ApplicativesF
andG
(composition)
var Compose = function(c) {
this.c = c;
};
Compose.of = function(x) {
return new Compose(F.of(G.of(x)));
};
Compose.prototype.ap = function(x) {
return new Compose(this.c.map(u => y => u.ap(y)).ap(x.c));
};
Compose.prototype.map = function(f) {
return new Compose(this.c.map(y => y.map(f)));
};
A value which has a Traversable must provide a sequence
method. The sequence
method takes one argument:
u.sequence(of)
of
must return the Applicative thatu
contains.
A value that implements the Chain specification must also implement the Apply specification.
m.chain(f).chain(g)
is equivalent tom.chain(x => f(x).chain(g))
(associativity)
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 ofchain
is unspecified. f
must return a value of the same Chain
- If
-
chain
must return a value of the same Chain
A value that implements the Monad specification must also implement the Applicative and Chain specifications.
m.of(a).chain(f)
is equivalent tof(a)
(left identity)m.chain(m.of)
is equivalent tom
(right identity)
w.extend(g).extend(f)
is equivalent tow.extend(_w => f(_w.extend(g)))
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 ofextend
is unspecified. f
must return a value of typev
, for some variablev
contained inw
.
- If
-
extend
must return a value of the same Extend.
A value that implements the Comonad specification must also implement the Functor and Extend specifications.
w.extend(_w => _w.extract())
is equivalent tow
w.extend(f).extract()
is equivalent tof(w)
w.extend(f)
is equivalent tow.extend(x => x).map(f)
A value which has a Comonad must provide an extract
method on itself.
The extract
method takes no arguments:
c.extract()
extract
must return a value of typev
, for some variablev
contained inw
.v
must have the same type thatf
returns inextend
.
When creating data types which satisfy multiple algebras, authors may choose to implement certain methods then derive the remaining methods. Derivations:
-
map
may be derived fromap
andof
:function(f) { return this.of(f).ap(this); }
-
map
may be derived fromchain
andof
:function(f) { var m = this; return m.chain(a => m.of(f(a))); }
-
function(m) { return this.chain(f => m.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(this.value, b.value)); }; return this.map(x => new Const(x)).sequence(Const.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).
- 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 all methods is provided inid.js
.