Skip to content
This repository has been archived by the owner on Oct 19, 2020. It is now read-only.

Commit

Permalink
Added List monad
Browse files Browse the repository at this point in the history
  • Loading branch information
GeorgeSapkin committed Nov 29, 2015
1 parent 8f742c1 commit 2aae1ed
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 19 deletions.
41 changes: 40 additions & 1 deletion README.md
Expand Up @@ -60,7 +60,7 @@ assert.equal(identity(5).map(a => a + 3), 8);

### Maybe, Just and Nothing

`maybe` resolves to either `just` or `nothing` depending on whether a value is passed to the type constructor. `Nothing` is not thenable.
`maybe` resolves to either `just` or `nothing` depending on whether a value is passed to the type constructor. `nothing` is not thenable.

#### `maybe(x)` type constructor
#### `just(x)`
Expand Down Expand Up @@ -161,6 +161,45 @@ assert(either(5, 7).map(a => a + 3) instanceof Either);
assert.equal(either(5, 7).map(a => a + 3), 10);
```

### List

`List` represents computations which may return 0, 1, or more possible results. `List` is not thenable.

#### `list(...args)` type constructor
```js
assert.deepEqual(list(5), [5]);
assert.deepEqual(list(5, 7, 11), [5, 7, 11]);
assert.deepEqual(list(), []);
```

#### `bind(x => )`
```js
assert.deepEqual(list(5, 7, 11).bind(a => a + 3), [8, 10, 14]);
```

#### `lift(x => )`
```js
const lifted = List.lift(x => x + 3);
assert(lifted(5) instanceof List);
assert.equal(lifted(5), 8);

assert(list(5).then(lifted) instanceof List);
assert.equal(list(5).then(lifted), 8);
```

#### `lift2((x, y) => )`
```js
const lifted = List.lift2((x, y) => x + y);
assert(lifted(5, 3) instanceof List);
assert.equal(lifted(5, 3), 8);
```

#### `map(x => )`
```js
assert(list(5, 7).map(a => a + 3) instanceof List);
assert.deepEqual(list(5, 7).map(a => a + 3), [8, 10]);
```

### RejectWhen

`RejectWhen` rejects a value on `bind` (or `then`) with `error` when condition `when` is met.
Expand Down
62 changes: 50 additions & 12 deletions index.js
Expand Up @@ -19,26 +19,31 @@ class Monad {
this.then = (...args) => this.bind(...args);
}
}
}

map(transform) {
assert(transform instanceof Function, 'transform must be a function');
function lift(transform) {
assert(transform instanceof Function, 'transform must be a function');

return this.bind(this.constructor.lift(transform));
}
return value => Object.freeze(new this(transform(value)));
}

static lift(transform) {
assert(transform instanceof Function, 'transform must be a function');
function lift2(transform) {
assert(transform instanceof Function, 'transform must be a function');

return value => Object.freeze(new this(transform(value)));
}
return (a, b) => Object.freeze(new this(_bind(a, b, transform)));
}

static lift2(transform) {
assert(transform instanceof Function, 'transform must be a function');
function map(transform) {
assert(transform instanceof Function, 'transform must be a function');

return (a, b) => Object.freeze(new this(_bind(a, b, transform)));
}
return this.bind(this.constructor.lift(transform));
}

Monad.lift = lift;
Monad.lift2 = lift2;

Monad.prototype.map = map;

module.exports.Monad = Monad;

class MonadPlus extends Monad {
Expand Down Expand Up @@ -159,6 +164,39 @@ function either(left, right) { return new Either(left, right); }
module.exports.Either = Either;
module.exports.either = either;

class List extends Array {
constructor(...args) {
// one argument is a special case for Array
if (args.length === 1) {
super(1);
this[0] = args[0];
}
else
super(...args);
}

bind(transform) {
assert(transform instanceof Function, 'transform must be a function');

return new List(...[].concat(...super.map(transform)));
}

plus(...args) { return new List(...this.concat(...args)); }
}

List.zero = Object.freeze(new List());
List.lift = lift;
List.lift2 = lift2;

List.prototype.map = map;

function list(...args) {
return Object.freeze(new List(...args));
}

module.exports.List = List;
module.exports.list = list;

class RejectWhen extends Identity {
constructor(when, error, value) {
assert(when instanceof Function, 'when must be a function');
Expand Down
19 changes: 13 additions & 6 deletions test/law.js
Expand Up @@ -6,8 +6,8 @@ module.exports = (Type, unit) => {
describe('should obey', () => {
it('left identity: return a >>= f ≡ f a', () => {
const a = 5;
const f = x => x + 3;
assert.strictEqual(unit(a).bind(f), f(a));
const f = x => [x + 3];
assert.deepEqual(unit(a).bind(f), f(a));
});

it('right identity: m >>= return ≡ m', () => {
Expand All @@ -33,22 +33,29 @@ module.exports.zeroPlusLaws = (Type, unit) => {
describe('should obey zero and plus', () => {
it ('mzero >>= f ≡ mzero', () => {
const f = x => x + 3;
assert.strictEqual(mzero.bind(f), mzero);
assert.deepEqual(mzero.bind(f), mzero);
});

it ('m >>= (\\x -> mzero) ≡ mzero', () => {
const m = unit(5);
assert.strictEqual(m.bind(() => mzero), mzero);
assert.deepEqual(m.bind(() => mzero), mzero);
});

it ('mzero `mplus` m ≡ m', () => {
const m = unit(5);
assert.strictEqual(mzero.plus(m), m);
assert.deepEqual(mzero.plus(m), m);
});

it ('m `mplus` mzero ≡ m', () => {
const m = unit(5);
assert.strictEqual(m.plus(mzero), m);
assert.deepEqual(m.plus(mzero), m);
});

it ('m `mplus` (n `mplus` o) ≡ (m `mplus` n) `mplus` o', () => {
const m = unit(3);
const n = unit(5);
const o = unit(7);
assert.deepEqual(m.plus(n.plus(o)), (m.plus(n)).plus(o));
});
});
}
181 changes: 181 additions & 0 deletions test/list.js
@@ -0,0 +1,181 @@
'use strict';

const assert = require('assert');
const co = require('co');

const M = require('..');

const obeyTheLaw = require('./law');
const obeyZeroPlusLaws = require('./law').zeroPlusLaws;

const List = M.List;
const list = M.list;

describe('List', () => {
describe('constructor', () => {
describe('should work', () => {
it('with value', () => {
assert.doesNotThrow(() => new List(5));
});

it('with multiple values', () => {
assert.doesNotThrow(() => new List(5, 7, 11));
});

it('without value', () => {
assert.doesNotThrow(() => new List());
});
});
});

describe('type constructor', () => {
describe('should work', () => {
it('with value', () => {
assert.doesNotThrow(() => list(5));
});

it('with multiple values', () => {
assert.doesNotThrow(() => list(5, 7, 11));
});

it('without value', () => {
assert.doesNotThrow(() => list());
});
});
});

describe('iterator', () => {
it('should return same value', () => {
assert.deepEqual(list(5), [5]);
});

it('should return same values', () => {
assert.deepEqual(list(5, 7, 11), [5, 7, 11]);
});

it('should return empty array', () => {
assert.deepEqual(list(), []);
});
});

describe('bind', () => {
describe('should work', () => {
it('with transform', () => {
assert.deepEqual(list(5, 7, 11).bind(a => a + 3), [8, 10, 14]);
});

it('with lifted transform', () => {
const result = list(5).bind(List.lift(x => x + 3));
assert(result instanceof List);
assert.equal(result, 8);
});
});

describe('should throw', () => {
it('with no arguments', () => {
assert.throws(() => list(5).bind());
});

it('with bad transform', () => {
assert.throws(() => list(5).bind(1));
});
});
});

describe('then', () => {
it('should throw', () => {
assert.throws(() => list().then());
});
});

describe('map', () => {
describe('should work', () => {
it('with transform', () => {
const result = list(5, 7).map(a => a + 3);
assert(result instanceof List);
assert.deepEqual(result, [8, 10]);
});
});

describe('should throw', () => {
it('with no arguments', () => {
assert.throws(() => list(5).map());
});

it('with bad transform', () => {
assert.throws(() => list(5).map(1));
});
});
});

describe('lift', () => {
describe('should work', () => {
it('with transform', () => {
const lifted = List.lift(x => x + 3);
const result = lifted(5);
assert(result instanceof List);
assert.equal(result, 8);
});
});

describe('should throw', () => {
it('with no arguments', () => {
assert.throws(() => List.lift());
});

it('with bad transform', () => {
assert.throws(() => List.lift(1));
});
});
});

describe('lift2', () => {
describe('should work', () => {
it('with transform and values', () => {
const lifted = List.lift2((x, y) => x + y);
const result = lifted(5, 3);
assert(result instanceof List);
assert.equal(result, 8);
});

it('with transform and identities', () => {
const lifted = List.lift2((x, y) => x + y);
const result = lifted(list(5), list(3));
assert(result instanceof List);
assert.equal(result, 8);
});
});

describe('should throw', () => {
it('with no arguments', () => {
assert.throws(() => List.lift2());
});

it('with bad transform', () => {
assert.throws(() => List.lift2(1));
});
});
});

describe('toString', () => {
describe('should return value', () => {
assert.strictEqual(list(5, 7).toString(), '5,7');
});
});

describe('function*', () => {
describe('should yield same value', () => {
it('when not nothing', () => {
return co(function* () {
return list(5, 7);
}).then(
val => assert.deepEqual(val, [5, 7]),
err => assert.ifError(err)
);
});
});
});

obeyTheLaw(List, list);
obeyZeroPlusLaws(List, list);
});

0 comments on commit 2aae1ed

Please sign in to comment.