Skip to content

Commit 0041cce

Browse files
committed
add Filterable algebra
1 parent 938d8da commit 0041cce

File tree

8 files changed

+103
-4
lines changed

8 files changed

+103
-4
lines changed

README.md

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ structures:
1616
* [Semigroup](#semigroup)
1717
* [Monoid](#monoid)
1818
* [Group](#group)
19+
* [Filterable](#filterable)
1920
* [Functor](#functor)
2021
* [Contravariant](#contravariant)
2122
* [Apply](#apply)
@@ -33,7 +34,7 @@ structures:
3334
* [Bifunctor](#bifunctor)
3435
* [Profunctor](#profunctor)
3536

36-
<img src="figures/dependencies.png" width="888" height="257" />
37+
<img src="figures/dependencies.png" width="888" height="234" />
3738

3839
## General
3940

@@ -328,6 +329,32 @@ A value which has a Group must provide an `invert` method. The
328329

329330
1. `invert` must return a value of the same Group.
330331

332+
### Filterable
333+
334+
1. `v.filter(x => p(x) && q(x))` is equivalent to `v.filter(p).filter(q)` (distributivity)
335+
2. `v.filter(x => true)` is equivalent to `v` (identity)
336+
3. `v.filter(x => false)` is equivalent to `w.filter(x => false)`
337+
if `v` and `w` are values of the same Filterable (annihilation)
338+
339+
#### `filter` method
340+
341+
```hs
342+
filter :: Filterable f => f a ~> (a -> Boolean) -> f a
343+
```
344+
345+
A value which has a Filterable must provide a `filter` method. The `filter`
346+
method takes one argument:
347+
348+
v.filter(p)
349+
350+
1. `p` must be a function.
351+
352+
1. If `p` is not a function, the behaviour of `filter` is unspecified.
353+
2. `p` must return either `true` or `false`. If it returns any other value,
354+
the behaviour of `filter` is unspecified.
355+
356+
2. `filter` must return a value of the same Filterable.
357+
331358
### Functor
332359

333360
1. `u.map(a => a)` is equivalent to `u` (identity)
@@ -836,7 +863,7 @@ to implement certain methods then derive the remaining methods. Derivations:
836863
function(f) {
837864
function Id(value) {
838865
this.value = value;
839-
};
866+
}
840867
Id.of = function(x) {
841868
return new Id(x);
842869
};
@@ -850,6 +877,25 @@ to implement certain methods then derive the remaining methods. Derivations:
850877
}
851878
```
852879

880+
- [`filter`][] may be derived from [`of`][], [`chain`][], and [`zero`][]:
881+
882+
```js
883+
function(pred) {
884+
var F = this.constructor;
885+
return this.chain(x => pred(x) ? F.of(x) : F.zero());
886+
}
887+
```
888+
889+
- [`filter`][] may be derived from [`concat`][], [`of`][], [`zero`][], and
890+
[`reduce`][]:
891+
892+
```js
893+
function(pred) {
894+
var F = this.constructor;
895+
return this.reduce((f, x) => pred(x) ? f.concat(F.of(x)) : f, F.zero());
896+
}
897+
```
898+
853899
If a data type provides a method which *could* be derived, its behaviour must
854900
be equivalent to that of the derivation (or derivations).
855901

@@ -873,12 +919,14 @@ be equivalent to that of the derivation (or derivations).
873919
[`equals`]: #equals-method
874920
[`extend`]: #extend-method
875921
[`extract`]: #extract-method
922+
[`filter`]: #filter-method
876923
[`lte`]: #lte-method
877924
[`map`]: #map-method
878925
[`of`]: #of-method
879926
[`promap`]: #promap-method
880927
[`reduce`]: #reduce-method
881928
[`sequence`]: #sequence-method
929+
[`zero`]: #zero-method
882930

883931
## Alternatives
884932

figures/dependencies.dot

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ digraph {
1111
Chain;
1212
ChainRec;
1313
Comonad;
14+
Contravariant;
1415
Extend;
16+
Filterable;
1517
Foldable;
1618
Functor;
1719
Group;
18-
Contravariant;
1920
Monad;
2021
Monoid;
2122
Ord;

figures/dependencies.png

221 Bytes
Loading

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
concat: 'fantasy-land/concat',
1414
empty: 'fantasy-land/empty',
1515
invert: 'fantasy-land/invert',
16+
filter: 'fantasy-land/filter',
1617
map: 'fantasy-land/map',
1718
contramap: 'fantasy-land/contramap',
1819
ap: 'fantasy-land/ap',

internal/id.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ Id.prototype[fl.concat] = function(b) {
2424

2525
// Monoid is not satisfiable since the type lacks a universal empty value
2626

27+
// Filterable
28+
Id.prototype[fl.filter] = function(pred) {
29+
return new Id(this.value[fl.filter](pred));
30+
};
31+
2732
// Foldable
2833
Id.prototype[fl.reduce] = function(f, acc) {
2934
return f(acc, this.value);

internal/patch.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ module.exports = () => {
77
Array.prototype[fl.equals] = function(y) {
88
return this.length === y.length && this.join('') === y.join('');
99
};
10+
Array.prototype[fl.filter] = function(pred) {
11+
return this.filter(x => pred(x));
12+
};
1013
Array.prototype[fl.map] = function(f) {
1114
return this.map(x => f(x));
1215
};

laws/filterable.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
const {filter} = require('..');
4+
5+
/**
6+
7+
### Filterable
8+
9+
1. `v.filter(x => p(x) && q(x))` is equivalent to `v.filter(p).filter(q)` (distributivity)
10+
2. `v.filter(x => true)` is equivalent to `v` (identity)
11+
3. `v.filter(x => false)` is equivalent to `w.filter(x => false)`
12+
if `v` and `w` are values of the same Filterable (annihilation)
13+
14+
**/
15+
16+
const distributivity = eq => v => p => q => {
17+
const a = v[filter](x => p(x) && q(x));
18+
const b = v[filter](p)[filter](q);
19+
return eq(a, b);
20+
};
21+
22+
const identity = eq => v => {
23+
const a = v[filter](x => true);
24+
const b = v;
25+
return eq(a, b);
26+
};
27+
28+
const annihilation = eq => v => w => {
29+
const a = v[filter](x => false);
30+
const b = w[filter](x => false);
31+
return eq(a, b);
32+
};
33+
34+
module.exports = {distributivity, identity, annihilation};

test.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ const alt = require('./laws/alt');
88
const alternative = require('./laws/alternative');
99
const applicative = require('./laws/applicative');
1010
const apply = require('./laws/apply');
11+
const category = require('./laws/category');
1112
const chain = require('./laws/chain');
1213
const chainRec = require('./laws/chainrec');
1314
const comonad = require('./laws/comonad');
1415
const extend = require('./laws/extend');
16+
const filterable = require('./laws/filterable');
1517
const foldable = require('./laws/foldable');
1618
const functor = require('./laws/functor');
1719
const group = require('./laws/group');
@@ -21,7 +23,6 @@ const ord = require('./laws/ord');
2123
const plus = require('./laws/plus');
2224
const semigroup = require('./laws/semigroup');
2325
const semigroupoid = require('./laws/semigroupoid');
24-
const category = require('./laws/category');
2526
const setoid = require('./laws/setoid');
2627
const traversable = require('./laws/traversable');
2728

@@ -82,6 +83,12 @@ exports.extend = {
8283
associativity: test(extend.associativity(Id[fl.of])(equality)),
8384
};
8485

86+
exports.filterable = {
87+
distributivity: test(x => filterable.distributivity(equality)(Id[fl.of]([0, 1, 2, 3, 4]))(x => x % 2 === 0)(x => x > 0)),
88+
identity: test(x => filterable.identity(equality)(Id[fl.of]([1, 2, 3]))),
89+
annihilation: test(x => filterable.annihilation(equality)(Id[fl.of]([1, 2, 3]))(Id[fl.of]([4, 5, 6]))),
90+
};
91+
8592
exports.foldable = {
8693
associativity: test(foldable.associativity(Id[fl.of])(equality)),
8794
};

0 commit comments

Comments
 (0)