Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

require static methods to be defined on type representatives #180

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`.
Copy link
Member

@safareli safareli Oct 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not quite get last sentence, especially Id :: TypeRep Identity

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Haskell types are "real" in the sense that one actually references types in one's programs:

inc :: Int -> Int
inc x = x + 1

In JavaScript types are not referenced (except in comments):

//    inc :: Number -> Number
const inc = x => x + 1;

In JavaScript the idea of types exists—as evidenced by the use of type signatures in comments—but there's no "type level"; only a "value level". Since we can't reference types at the type level, we must reference them at the value level. So, for each type we'd like a value which represents it. We call such values type representatives.

Let's consider some examples:

  • Number is the representative of the Number type. Number is the sole inhabitant of the TypeRep Number type.
  • Id is the representative of the Identity type. Id is the sole representative of the TypeRep Identity type.

The formatting above is very much intentional. The Number type exists only as an idea, so should not be formatted as code. Number, on the other hand, is a JavaScript value which represents the Number type (in addition to its role as a function from Any to Number).

It took me a while to come to terms with the idea of type representatives. This sort of thinking is not natural in a dynamically typed language. I suggest reading sanctuary-js/sanctuary#64 if you have not already done so.

Copy link
Member

@safareli safareli Oct 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The part which confused me was Id :: TypeRep Identity (Id and Identity are different when Number and Number are same) is it because we assume definition of Identity would be something like this type Identity a = Id a?

So if we have:

type Maybe a = Just a | Nothing

Then is this true:?

Just :: TypeRep (Maybe a)
Nothing :: TypeRep (Maybe a)

will take a look at that issue more carefully

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Haskell at least those last equations are wrong.

  • Just constructs values at the Maybe type, it's not a representative of the type. In particular, a type should be represented in the shape of the type, not its values.
  • Maybe is an interesting example since it's a type constructor, a type function. If we want to represent this is a value-level type representative then TypeRep Maybe and TypeRep X must be able to be combined into TypeRep (Maybe X). Perhaps the type reps of constructors are functions, or probably better (since it'll be easier to analyze) you could have something that gives you ($$) :: TypeRep f -> TypeRep a -> TypeRep (f a)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that $$ is data then you can easily analyze it!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The part which confused me was Id :: TypeRep Identity (Id and Identity are different when Number and Number are same) is it because we assume definition of Identity would be something like this type Identity a = Id a?

When we talk about "the Identity type" we're referring to a concept with no manifestation in JavaScript. When we talk about "Id" we're referring to a real thing: an identifier we could evaluate in a REPL. Id lives in the world of values to the left of the colons; Identity lives in the world of types to the right of the colons.

Since the world of values and the world of types are separate, we needn't worry about naming collisions. This is valid:

Identity :: a -> Identity a

Identity on the left of the :: is a value, whereas Identity on the right is a type (a type constructor, actually).

Similarly, we could name our type representative Identity rather than Id without conflict:

Identity :: TypeRep Identity

I hope this clarifies things a little. :)


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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get this change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point I thought Sum('').constructor did not evaluate to Sum. I was mistaken. I've reverted to the daggy version.

};
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