Skip to content

Commit

Permalink
require static methods to be defined on type representatives
Browse files Browse the repository at this point in the history
  • Loading branch information
davidchambers committed Oct 23, 2016
1 parent 1e2d336 commit 3ac198b
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 64 deletions.
80 changes: 51 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,15 @@ have dependencies on other algebras which must be implemented.

## Prefixed method names

In order to add compatibility with Fantasy Land to your library,
you need to add methods that you want to support with `fantasy-land/` prefix.
For example if a type implements Functors' [`map`][], you need to add `fantasy-land/map` method to it.
The code may look something like this:
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:

```js
MyType.prototype['fantasy-land/map'] = MyType.prototype.map
// MyType#fantasy-land/map :: MyType a ~> (a -> b) -> MyType b
MyType.prototype['fantasy-land/map'] = ...
```

It's not required to add unprefixed methods (e.g. `MyType.prototype.map`)
for compatibility with Fantasy Land, but you're free to do so of course.

Further in this document unprefixed names are used just to reduce noise.

For convenience you can use `fantasy-land` package:
Expand All @@ -69,13 +66,24 @@ var fl = require('fantasy-land')

// ...

MyType.prototype[fl.map] = MyType.prototype.map
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
Expand Down Expand Up @@ -129,19 +137,23 @@ A value which has a Semigroup must provide a `concat` method. The
A value that implements the Monoid specification must also implement
the [Semigroup](#semigroup) specification.

1. `m.concat(m.empty())` is equivalent to `m` (right identity)
2. `m.empty().concat(m)` is equivalent to `m` (left identity)
1. `m.concat(m.constructor.empty())` is equivalent to `m` (right identity)
2. `m.constructor.empty().concat(m)` is equivalent to `m` (left identity)

#### `empty` method

```hs
empty :: Monoid m => () -> m
```

A value which has a Monoid must provide an `empty` method on itself or
its `constructor` object. The `empty` method takes no arguments:
A value which has a Monoid must provide an `empty` function on its
[type representative](#type-representatives):

M.empty()

Given a value `m`, one can access its type representative via the
`constructor` property:

m.empty()
m.constructor.empty()

1. `empty` must return a value of the same Monoid
Expand Down Expand Up @@ -206,25 +218,30 @@ method takes one argument:
A value that implements the Applicative specification must also
implement the [Apply](#apply) specification.

1. `v.ap(a.of(x => x))` is equivalent to `v` (identity)
2. `a.of(x).ap(a.of(f))` is equivalent to `a.of(f(x))` (homomorphism)
3. `a.of(y).ap(u)` is equivalent to `u.ap(a.of(f => f(y)))` (interchange)
1. `v.ap(A.of(x => x))` is equivalent to `v` (identity)
2. `A.of(x).ap(A.of(f))` is equivalent to `A.of(f(x))` (homomorphism)
3. `A.of(y).ap(u)` is equivalent to `u.ap(A.of(f => f(y)))` (interchange)

#### `of` method

```hs
of :: Applicative f => a -> f a
```

A value which has an Applicative must provide an `of` method on itself
or its `constructor` object. The `of` method takes one argument:
A value which has an Applicative must provide an `of` function on its
[type representative](#type-representatives). The `of` function takes
one argument:

F.of(a)

Given a value `f`, one can access its type representative via the
`constructor` property:

a.of(b)
a.constructor.of(b)
f.constructor.of(a)

1. `of` must provide a value of the same Applicative

1. No parts of `b` should be checked
1. No parts of `a` should be checked

### Foldable

Expand Down Expand Up @@ -333,22 +350,27 @@ method takes one argument:

A value that implements the ChainRec specification must also implement the [Chain](#chain) specification.

1. `m.chainRec((next, done, v) => p(v) ? d(v).map(done) : n(v).map(next), i)`
1. `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)
2. Stack usage of `m.chainRec(f, i)` must be at most a constant multiple of the stack usage of `f` itself.
2. Stack usage of `M.chainRec(f, i)` must be at most a constant multiple of the stack usage of `f` itself.

#### `chainRec` method

```hs
chainRec :: ChainRec m => ((a -> c, b -> c, a) -> m c, a) -> m b
```

A Type which has a ChainRec must provide a `chainRec` method on itself
or its `constructor` object. The `chainRec` method takes two arguments:
A Type which has a ChainRec must provide a `chainRec` function on its
[type representative](#type-representatives). The `chainRec` function
takes two arguments:

M.chainRec(f, i)

Given a value `m`, one can access its type representative via the
`constructor` property:

a.chainRec(f, i)
a.constructor.chainRec(f, i)
m.constructor.chainRec(f, i)

1. `f` must be a function which returns a value
1. If `f` is not a function, the behaviour of `chainRec` is unspecified.
Expand Down Expand Up @@ -566,7 +588,7 @@ be equivalent to that of the derivation (or derivations).
2. It's discouraged to overload the specified methods. It can easily
result in broken and buggy behaviour.
3. It is recommended to throw an exception on unspecified behaviour.
4. An `Id` container which implements all methods is provided in
4. An `Id` container which implements many of the methods is provided in
`id.js`.


Expand Down
8 changes: 1 addition & 7 deletions id.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ Id.prototype[fl.concat] = function(b) {
return new Id(this.value[fl.concat](b.value));
};

// Monoid (value must also be a Monoid)
Id[fl.empty] = function() {
return new Id(this.value[fl.empty] ? this.value[fl.empty]() : this.value.constructor[fl.empty]());
};
Id.prototype[fl.empty] = Id[fl.empty];
// Monoid is not satisfiable since the type lacks a universal empty value

// Foldable
Id.prototype[fl.reduce] = function(f, acc) {
Expand Down Expand Up @@ -62,7 +58,6 @@ Id[fl.chainRec] = function(f, i) {
}
return new Id(state.value);
};
Id.prototype[fl.chainRec] = Id[fl.chainRec];

// Extend
Id.prototype[fl.extend] = function(f) {
Expand All @@ -73,7 +68,6 @@ Id.prototype[fl.extend] = function(f) {
Id[fl.of] = function(a) {
return new Id(a);
};
Id.prototype[fl.of] = Id[fl.of];

// Comonad
Id.prototype[fl.extract] = function() {
Expand Down
6 changes: 2 additions & 4 deletions id_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ const Id = require('./id');
const Sum = tagged('v');
Sum[of] = Sum;
Sum[empty] = () => Sum('');
Sum.prototype[of] = Sum[of];
Sum.prototype[empty] = Sum[empty];
Sum.prototype[map] = function(f) {
return Sum(f(this.v));
};
Expand Down Expand Up @@ -90,8 +88,8 @@ exports.monad = {
};

exports.monoid = {
leftIdentity: test(x => monoid.leftIdentity(Id[of](Sum[empty]()))(equality)(Sum[of](x))),
rightIdentity: test(x => monoid.rightIdentity(Id[of](Sum[empty]()))(equality)(Sum[of](x))),
leftIdentity: test(x => monoid.leftIdentity(Sum)(equality)(x)),
rightIdentity: test(x => monoid.rightIdentity(Sum)(equality)(x)),
};

// Semigroup tests are broken otherwise for this.
Expand Down
26 changes: 13 additions & 13 deletions laws/applicative.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,29 @@ const {of, ap} = require('..');
### Applicative
1. `v.ap(a.of(x => x))` is equivalent to `v` (identity)
2. `a.of(x).ap(a.of(f))` is equivalent to `a.of(f(x))` (homomorphism)
3. `a.of(y).ap(u)` is equivalent to `u.ap(a.of(f => f(y)))` (interchange)
1. `v.ap(A.of(x => x))` is equivalent to `v` (identity)
2. `A.of(x).ap(A.of(f))` is equivalent to `A.of(f(x))` (homomorphism)
3. `A.of(y).ap(u)` is equivalent to `u.ap(A.of(f => f(y)))` (interchange)
**/

const identityʹ = t => eq => x => {
const a = t[of](x)[ap](t[of](identity));
const b = t[of](x);
const identityʹ = T => eq => x => {
const a = T[of](x)[ap](T[of](identity));
const b = T[of](x);
return eq(a, b);
};

const homomorphism = t => eq => x => {
const a = t[of](x)[ap](t[of](identity));
const b = t[of](identity(x));
const homomorphism = T => eq => x => {
const a = T[of](x)[ap](T[of](identity));
const b = T[of](identity(x));
return eq(a, b);
};

const interchange = t => eq => x => {
const u = t[of](identity);
const interchange = T => eq => x => {
const u = T[of](identity);

const a = t[of](x)[ap](u);
const b = u[ap](t[of](thrush(x)));
const a = T[of](x)[ap](u);
const b = u[ap](T[of](thrush(x)));
return eq(a, b);
};

Expand Down
6 changes: 3 additions & 3 deletions laws/chainrec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ const {chain, map, chainRec} = require('..');
### ChainRec
1. `t.chainRec((next, done, v) => p(v) ? d(v).map(done) : n(v).map(next), i)`
1. `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)
**/

const equivalence = t => eq => p => d => n => x => {
const a = t[chainRec]((next, done, v) => p(v) ? d(v)[map](done) : n(v)[map](next), x);
const equivalence = T => eq => p => d => n => x => {
const a = T[chainRec]((next, done, v) => p(v) ? d(v)[map](done) : n(v)[map](next), x);
const b = (function step(v) { return p(v) ? d(v) : n(v)[chain](step); }(x));
return eq(a, b);
};
Expand Down
16 changes: 8 additions & 8 deletions laws/monoid.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ const {of, empty, concat} = require('..');
### Monoid
1. `m.concat(m.empty())` is equivalent to `m` (right identity)
2. `m.empty().concat(m)` is equivalent to `m` (left identity)
1. `m.concat(m.constructor.empty())` is equivalent to `m` (right identity)
2. `m.constructor.empty().concat(m)` is equivalent to `m` (left identity)
**/

const rightIdentity = t => eq => x => {
const a = t[of](x)[concat](t[empty]());
const b = t[of](x);
const rightIdentity = T => eq => x => {
const a = T[of](x)[concat](T[empty]());
const b = T[of](x);
return eq(a, b);
};

const leftIdentity = t => eq => x => {
const a = t[empty]()[concat](t[of](x));
const b = t[of](x);
const leftIdentity = T => eq => x => {
const a = T[empty]()[concat](T[of](x));
const b = T[of](x);
return eq(a, b);
};

Expand Down

0 comments on commit 3ac198b

Please sign in to comment.