diff --git a/README.md b/README.md index 9a7b19b..4778fad 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ structures: * [Semigroup](#semigroup) * [Monoid](#monoid) * [Group](#group) +* [Filterable](#filterable) * [Functor](#functor) * [Contravariant](#contravariant) * [Apply](#apply) @@ -33,7 +34,7 @@ structures: * [Bifunctor](#bifunctor) * [Profunctor](#profunctor) - + ## General @@ -328,6 +329,32 @@ A value which has a Group must provide an `invert` method. The 1. `invert` must return a value of the same Group. +### Filterable + +1. `v.filter(x => p(x) && q(x))` is equivalent to `v.filter(p).filter(q)` (distributivity) +2. `v.filter(x => true)` is equivalent to `v` (identity) +3. `v.filter(x => false)` is equivalent to `w.filter(x => false)` + if `v` and `w` are values of the same Filterable (annihilation) + +#### `filter` method + +```hs +filter :: Filterable f => f a ~> (a -> Boolean) -> f a +``` + +A value which has a Filterable must provide a `filter` method. The `filter` +method takes one argument: + + v.filter(p) + +1. `p` must be a function. + + 1. If `p` is not a function, the behaviour of `filter` is unspecified. + 2. `p` must return either `true` or `false`. If it returns any other value, + the behaviour of `filter` is unspecified. + +2. `filter` must return a value of the same Filterable. + ### Functor 1. `u.map(a => a)` is equivalent to `u` (identity) @@ -836,7 +863,7 @@ to implement certain methods then derive the remaining methods. Derivations: function(f) { function Id(value) { this.value = value; - }; + } Id.of = function(x) { return new Id(x); }; @@ -850,6 +877,25 @@ to implement certain methods then derive the remaining methods. Derivations: } ``` + - [`filter`][] may be derived from [`of`][], [`chain`][], and [`zero`][]: + + ```js + function(pred) { + var F = this.constructor; + return this.chain(x => pred(x) ? F.of(x) : F.zero()); + } + ``` + + - [`filter`][] may be derived from [`concat`][], [`of`][], [`zero`][], and + [`reduce`][]: + + ```js + function(pred) { + var F = this.constructor; + return this.reduce((f, x) => pred(x) ? f.concat(F.of(x)) : f, F.zero()); + } + ``` + If a data type provides a method which *could* be derived, its behaviour must be equivalent to that of the derivation (or derivations). @@ -873,12 +919,14 @@ be equivalent to that of the derivation (or derivations). [`equals`]: #equals-method [`extend`]: #extend-method [`extract`]: #extract-method +[`filter`]: #filter-method [`lte`]: #lte-method [`map`]: #map-method [`of`]: #of-method [`promap`]: #promap-method [`reduce`]: #reduce-method [`sequence`]: #sequence-method +[`zero`]: #zero-method ## Alternatives diff --git a/figures/dependencies.dot b/figures/dependencies.dot index d632f3f..f0cfed4 100644 --- a/figures/dependencies.dot +++ b/figures/dependencies.dot @@ -11,11 +11,12 @@ digraph { Chain; ChainRec; Comonad; + Contravariant; Extend; + Filterable; Foldable; Functor; Group; - Contravariant; Monad; Monoid; Ord; diff --git a/figures/dependencies.png b/figures/dependencies.png index e4d814b..242a17e 100644 Binary files a/figures/dependencies.png and b/figures/dependencies.png differ diff --git a/index.js b/index.js index 6e0e13e..f26368e 100644 --- a/index.js +++ b/index.js @@ -13,6 +13,7 @@ concat: 'fantasy-land/concat', empty: 'fantasy-land/empty', invert: 'fantasy-land/invert', + filter: 'fantasy-land/filter', map: 'fantasy-land/map', contramap: 'fantasy-land/contramap', ap: 'fantasy-land/ap', diff --git a/internal/id.js b/internal/id.js index 779a38c..8901b6f 100644 --- a/internal/id.js +++ b/internal/id.js @@ -24,6 +24,11 @@ Id.prototype[fl.concat] = function(b) { // Monoid is not satisfiable since the type lacks a universal empty value +// Filterable +Id.prototype[fl.filter] = function(pred) { + return new Id(this.value[fl.filter](pred)); +}; + // Foldable Id.prototype[fl.reduce] = function(f, acc) { return f(acc, this.value); diff --git a/internal/patch.js b/internal/patch.js index b7403a1..0d03f00 100644 --- a/internal/patch.js +++ b/internal/patch.js @@ -7,6 +7,9 @@ module.exports = () => { Array.prototype[fl.equals] = function(y) { return this.length === y.length && this.join('') === y.join(''); }; + Array.prototype[fl.filter] = function(pred) { + return this.filter(x => pred(x)); + }; Array.prototype[fl.map] = function(f) { return this.map(x => f(x)); }; diff --git a/laws/filterable.js b/laws/filterable.js new file mode 100644 index 0000000..e932df4 --- /dev/null +++ b/laws/filterable.js @@ -0,0 +1,34 @@ +'use strict'; + +const {filter} = require('..'); + +/** + +### Filterable + +1. `v.filter(x => p(x) && q(x))` is equivalent to `v.filter(p).filter(q)` (distributivity) +2. `v.filter(x => true)` is equivalent to `v` (identity) +3. `v.filter(x => false)` is equivalent to `w.filter(x => false)` + if `v` and `w` are values of the same Filterable (annihilation) + +**/ + +const distributivity = eq => v => p => q => { + const a = v[filter](x => p(x) && q(x)); + const b = v[filter](p)[filter](q); + return eq(a, b); +}; + +const identity = eq => v => { + const a = v[filter](x => true); + const b = v; + return eq(a, b); +}; + +const annihilation = eq => v => w => { + const a = v[filter](x => false); + const b = w[filter](x => false); + return eq(a, b); +}; + +module.exports = {distributivity, identity, annihilation}; diff --git a/test.js b/test.js index 93d5fd0..1968dad 100644 --- a/test.js +++ b/test.js @@ -8,10 +8,12 @@ const alt = require('./laws/alt'); const alternative = require('./laws/alternative'); const applicative = require('./laws/applicative'); const apply = require('./laws/apply'); +const category = require('./laws/category'); const chain = require('./laws/chain'); const chainRec = require('./laws/chainrec'); const comonad = require('./laws/comonad'); const extend = require('./laws/extend'); +const filterable = require('./laws/filterable'); const foldable = require('./laws/foldable'); const functor = require('./laws/functor'); const group = require('./laws/group'); @@ -21,7 +23,6 @@ const ord = require('./laws/ord'); const plus = require('./laws/plus'); const semigroup = require('./laws/semigroup'); const semigroupoid = require('./laws/semigroupoid'); -const category = require('./laws/category'); const setoid = require('./laws/setoid'); const traversable = require('./laws/traversable'); @@ -82,6 +83,12 @@ exports.extend = { associativity: test(extend.associativity(Id[fl.of])(equality)), }; +exports.filterable = { + distributivity: test(x => filterable.distributivity(equality)(Id[fl.of]([0, 1, 2, 3, 4]))(x => x % 2 === 0)(x => x > 0)), + identity: test(x => filterable.identity(equality)(Id[fl.of]([1, 2, 3]))), + annihilation: test(x => filterable.annihilation(equality)(Id[fl.of]([1, 2, 3]))(Id[fl.of]([4, 5, 6]))), +}; + exports.foldable = { associativity: test(foldable.associativity(Id[fl.of])(equality)), };