From 7a4bf574a5a7907f2071aeea5db627b476195914 Mon Sep 17 00:00:00 2001 From: Matt Ross Date: Wed, 28 Feb 2018 15:58:13 -0500 Subject: [PATCH 01/13] Add `ap` method and a simple test --- lib/index.js | 21 +++++++++++++++++++++ test/test.js | 10 ++++++++++ 2 files changed, 31 insertions(+) diff --git a/lib/index.js b/lib/index.js index 0c3effe..3a34f19 100755 --- a/lib/index.js +++ b/lib/index.js @@ -2210,6 +2210,27 @@ addMethod('flatMap', function (f) { return this.map(f).sequence(); }); +/** + * Creates a new Stream of values by applying each item in a Stream to each + * value in a Stream of functions + * + * @id ap + * @section Higher-order Streams + * @name Stream.ap(m) + * @param {Stream} m - incoming stream of values to apply to function(s) in stream + * @api public + * + * var readFile = _.wrapCallback(fs.readFile); + * filenames.flatMap(readFile) + */ + +addMethod('ap', function (m) { + return this.map(function (f) { + return m.fork().map(f); + }) + .parallel(Infinity); +}); + /** * Retrieves values associated with a given property from all elements in * the collection. diff --git a/test/test.js b/test/test.js index d18ef6b..2afbc7b 100755 --- a/test/test.js +++ b/test/test.js @@ -4938,6 +4938,16 @@ exports['flatMap - map to Stream of Array'] = function (test) { }); }; +exports.ap = function (test) { + function doubled(x) { + return x * 2; + } + _.ap(_([1, 2, 3, 4]), _.of(doubled)).toArray(function (xs) { + test.same(xs, [2, 4, 6, 8]); + test.done(); + }); +}; + exports.pluck = function (test) { var a = _([ {type: 'blogpost', title: 'foo'}, From 3aee195535c783d7b526eae56163491a13f2aad6 Mon Sep 17 00:00:00 2001 From: Matt Ross Date: Mon, 22 Oct 2018 15:32:55 -0400 Subject: [PATCH 02/13] Add some tests per PR comments --- test/test.js | 97 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 89 insertions(+), 8 deletions(-) diff --git a/test/test.js b/test/test.js index 2afbc7b..6e875f9 100755 --- a/test/test.js +++ b/test/test.js @@ -4938,14 +4938,95 @@ exports['flatMap - map to Stream of Array'] = function (test) { }); }; -exports.ap = function (test) { - function doubled(x) { - return x * 2; - } - _.ap(_([1, 2, 3, 4]), _.of(doubled)).toArray(function (xs) { - test.same(xs, [2, 4, 6, 8]); - test.done(); - }); +exports.ap = { + 'applies values to functions': function (test) { + var s = _([1, 2, 3, 4]); + var f = _.of(function doubled(x) { + return x * 2; + }); + + _.ap(f, s).toArray(function (xs) { + test.same(xs, [2, 4, 6, 8]); + test.done(); + }); + }, + 'noValueOnError': noValueOnErrorTest(_.ap(_.of(1))), + 'ArrayStream': function (test) { + var f = _.of(function (x) { + return _(function (push, next) { + setTimeout(function () { + push(null, x * 2); + push(null, _.nil); + }, 10); + }); + }); + _([1, 2, 3, 4]).ap(f).merge().toArray(function (xs) { + test.same(xs, [2, 4, 6, 8]); + test.done(); + }); + }, + 'GeneratorStream': function (test) { + var f = _.of(function (x) { + return _(function (push, next) { + setTimeout(function () { + push(null, x * 2); + push(null, _.nil); + }, 10); + }); + }); + var s = _(function (push, next) { + push(null, 1); + push(null, 2); + setTimeout(function () { + push(null, 3); + push(null, 4); + push(null, _.nil); + }, 10); + }); + s.ap(f).merge().toArray(function (xs) { + test.same(xs, [2, 4, 6, 8]); + test.done(); + }); + }, + 'map to Stream of Array': function (test) { + test.expect(1); + var f = _.of(function (x) { + return _([[x]]); + }); + var s = _([1, 2, 3, 4]).ap(f).merge().toArray(function (xs) { + test.same(xs, [[1], [2], [3], [4]]); + test.done(); + }); + }, + 'reflect timing of value and function arrival': function (test) { + test.expect(1); + var f = _(function (push, next) { + setTimeout(function () { + push(null, function (x) { return 'g1(' + x + ')'; }); + }, 20); + setTimeout(function () { + push(null, function (x) { return 'g2(' + x + ')'; }); + push(null, _.nil); + }, 60); + }); + var s = _(function (push, next) { + setTimeout(function () { + push(null, 1); + }, 10); + setTimeout(function () { + push(null, 2); + }, 50); + setTimeout(function () { + push(null, 3); + push(null, _.nil); + }, 70); + }); + + s.ap(f).toArray(function (xs) { + test.same(xs, ['g1(1)', 'g1(2)', 'g2(2)', 'g2(3)']); + test.done(); + }); + }, }; exports.pluck = function (test) { From 20096e839940505746946f10a4e84f65ca8f0b80 Mon Sep 17 00:00:00 2001 From: Matt Ross Date: Mon, 22 Oct 2018 15:33:19 -0400 Subject: [PATCH 03/13] Implement `ap` using `merge/scan1` --- lib/index.js | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/index.js b/lib/index.js index 3a34f19..87582fe 100755 --- a/lib/index.js +++ b/lib/index.js @@ -2217,18 +2217,37 @@ addMethod('flatMap', function (f) { * @id ap * @section Higher-order Streams * @name Stream.ap(m) - * @param {Stream} m - incoming stream of values to apply to function(s) in stream + * @param {Stream} m - incoming stream of function(s) to apply to value(s) in stream * @api public * + * var filenames = _(['foo', 'bar', 'baz']); * var readFile = _.wrapCallback(fs.readFile); - * filenames.flatMap(readFile) + * filenames.ap(_(readFile)); */ -addMethod('ap', function (m) { - return this.map(function (f) { - return m.fork().map(f); - }) - .parallel(Infinity); +addMethod('ap', function(m) { + return _([ + this.map(function (u1) { + return { + u: u1 + }; + }), + m.map(function (m1) { + return { + m: m1 + }; + }), + ]) + .merge() + .scan1(function (x, y) { + return Object.assign({}, x, y); + }) + .filter(function (x) { + return x.u && x.m; + }) + .map(function (x) { + return x.m(x.u); + }); }); /** From 223467d91602a2fe45e620180a980e4bf377db14 Mon Sep 17 00:00:00 2001 From: Matt Ross Date: Wed, 24 Oct 2018 11:52:03 -0400 Subject: [PATCH 04/13] Update `ap` example to use modulating `fns` values --- lib/index.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/index.js b/lib/index.js index 87582fe..378ac4f 100755 --- a/lib/index.js +++ b/lib/index.js @@ -2220,9 +2220,16 @@ addMethod('flatMap', function (f) { * @param {Stream} m - incoming stream of function(s) to apply to value(s) in stream * @api public * - * var filenames = _(['foo', 'bar', 'baz']); - * var readFile = _.wrapCallback(fs.readFile); - * filenames.ap(_(readFile)); + * var asyncUnit = () => h.of(h.nil); + * var asyncAutocomplete = e => h(fetch(`/autocomplete?s=${e.target.value}`)); + * + * var fns = h('change', checkbox) + * .flatMap(e => h.of(e.target.checked ? asyncAutocomplete : asyncUnit)); + * + * h('change', input) + * .ap(fns) + * .merge() + * .map(showAutocompleteResults); */ addMethod('ap', function(m) { From 0fe6feccecbb9cb047c466304ada0231fdcdb56d Mon Sep 17 00:00:00 2001 From: Matt Ross Date: Wed, 24 Oct 2018 12:13:17 -0400 Subject: [PATCH 05/13] Test timing of arrival of functions and values with `ap` --- test/test.js | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/test/test.js b/test/test.js index 6e875f9..38fc726 100755 --- a/test/test.js +++ b/test/test.js @@ -4939,6 +4939,14 @@ exports['flatMap - map to Stream of Array'] = function (test) { }; exports.ap = { + setUp: function (callback) { + this.clock = sinon.useFakeTimers(); + callback(); + }, + tearDown: function (callback) { + this.clock.restore(); + callback(); + }, 'applies values to functions': function (test) { var s = _([1, 2, 3, 4]); var f = _.of(function doubled(x) { @@ -4952,6 +4960,7 @@ exports.ap = { }, 'noValueOnError': noValueOnErrorTest(_.ap(_.of(1))), 'ArrayStream': function (test) { + test.expect(1); var f = _.of(function (x) { return _(function (push, next) { setTimeout(function () { @@ -4962,26 +4971,24 @@ exports.ap = { }); _([1, 2, 3, 4]).ap(f).merge().toArray(function (xs) { test.same(xs, [2, 4, 6, 8]); - test.done(); }); + this.clock.tick(20); + test.done(); }, 'GeneratorStream': function (test) { + test.expect(1); var f = _.of(function (x) { return _(function (push, next) { - setTimeout(function () { - push(null, x * 2); - push(null, _.nil); - }, 10); + push(null, x * 2); + push(null, _.nil); }); }); var s = _(function (push, next) { push(null, 1); push(null, 2); - setTimeout(function () { - push(null, 3); - push(null, 4); - push(null, _.nil); - }, 10); + push(null, 3); + push(null, 4); + push(null, _.nil); }); s.ap(f).merge().toArray(function (xs) { test.same(xs, [2, 4, 6, 8]); @@ -4999,7 +5006,7 @@ exports.ap = { }); }, 'reflect timing of value and function arrival': function (test) { - test.expect(1); + test.expect(4); var f = _(function (push, next) { setTimeout(function () { push(null, function (x) { return 'g1(' + x + ')'; }); @@ -5022,10 +5029,19 @@ exports.ap = { }, 70); }); - s.ap(f).toArray(function (xs) { - test.same(xs, ['g1(1)', 'g1(2)', 'g2(2)', 'g2(3)']); - test.done(); + var results = []; + s.ap(f).each(function (x) { + results.push(x); }); + this.clock.tick(20); + test.same(results, ['g1(1)']); + this.clock.tick(30); + test.same(results, ['g1(1)', 'g1(2)']); + this.clock.tick(10); + test.same(results, ['g1(1)', 'g1(2)', 'g2(2)']); + this.clock.tick(10); + test.same(results, ['g1(1)', 'g1(2)', 'g2(2)', 'g2(3)']); + test.done(); }, }; From 98f1b7da38a7815701c9ea299b91b9c6014fb9ae Mon Sep 17 00:00:00 2001 From: Matt Ross Date: Wed, 24 Oct 2018 12:25:20 -0400 Subject: [PATCH 06/13] Test `composition` requirement of `ap` per https://github.com/fantasyland/fantasy-land/blob/1871bd3f95ca47cf016f670c140c413a040c6869/README.md#apply --- test/test.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/test.js b/test/test.js index 38fc726..6b03320 100755 --- a/test/test.js +++ b/test/test.js @@ -5043,6 +5043,43 @@ exports.ap = { test.same(results, ['g1(1)', 'g1(2)', 'g2(2)', 'g2(3)']); test.done(); }, + // v.ap(u.ap(a.map(f => g => x => f(g(x))))) is equivalent to v.ap(u).ap(a) (composition) + 'composition': { + 'left': function (test) { + test.expect(1); + var v = _.of('a'); + var u = _.of(function (x) { + return x + 'b'; + }); + var a = _.of(function (x) { + return x + 'c'; + }); + v.fork().ap(u.fork().ap(a.fork().map(function (f) { + return function (g) { + return function (x) { + return f(g(x)); + }; + }; + }))).toArray(function (x) { + test.same(x, ['abc']); + }); + test.done(); + }, + 'right': function (test) { + test.expect(1); + var v = _.of('a'); + var u = _.of(function (x) { + return x + 'b'; + }); + var a = _.of(function (x) { + return x + 'c'; + }); + v.ap(u).ap(a).toArray(function (x) { + test.same(x, ['abc']); + }); + test.done(); + }, + }, }; exports.pluck = function (test) { From a301ba81a8e1d36d0fac86555cdc14d2b0b4d71e Mon Sep 17 00:00:00 2001 From: Matt Ross Date: Wed, 24 Oct 2018 12:39:47 -0400 Subject: [PATCH 07/13] Use `_.extend` instead of `Object.assign` in `ap` This was breaking a build on an older Node version. https://travis-ci.org/caolan/highland/jobs/445755627 --- lib/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index 86fa125..c3577b0 100755 --- a/lib/index.js +++ b/lib/index.js @@ -2274,7 +2274,7 @@ addMethod('ap', function(m) { ]) .merge() .scan1(function (x, y) { - return Object.assign({}, x, y); + return _.extend(x, y); }) .filter(function (x) { return x.u && x.m; From 196b61f45c2607c13f1c50b3de851009c1cfb5c1 Mon Sep 17 00:00:00 2001 From: Matt Ross Date: Wed, 24 Oct 2018 12:43:23 -0400 Subject: [PATCH 08/13] Correct argument order for `extend` in `ap` duh --- lib/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index c3577b0..69c2839 100755 --- a/lib/index.js +++ b/lib/index.js @@ -2274,7 +2274,7 @@ addMethod('ap', function(m) { ]) .merge() .scan1(function (x, y) { - return _.extend(x, y); + return _.extend(y, x); }) .filter(function (x) { return x.u && x.m; From 00f330cdd1d5944234521469731710d47b1026be Mon Sep 17 00:00:00 2001 From: Matt Ross Date: Thu, 25 Oct 2018 09:20:47 -0400 Subject: [PATCH 09/13] Use `_` instead of `h` --- lib/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/index.js b/lib/index.js index 69c2839..2648c60 100755 --- a/lib/index.js +++ b/lib/index.js @@ -2247,13 +2247,13 @@ addMethod('flatMap', function (f) { * @param {Stream} m - incoming stream of function(s) to apply to value(s) in stream * @api public * - * var asyncUnit = () => h.of(h.nil); - * var asyncAutocomplete = e => h(fetch(`/autocomplete?s=${e.target.value}`)); + * var asyncUnit = () => _.of(_.nil); + * var asyncAutocomplete = e => _(fetch(`/autocomplete?s=${e.target.value}`)); * - * var fns = h('change', checkbox) + * var fns = _('change', checkbox) * .flatMap(e => h.of(e.target.checked ? asyncAutocomplete : asyncUnit)); * - * h('change', input) + * _('change', input) * .ap(fns) * .merge() * .map(showAutocompleteResults); From fd2c069dadf9dd50623ee9271ab26fdb0f2f5a59 Mon Sep 17 00:00:00 2001 From: Matt Ross Date: Thu, 25 Oct 2018 09:21:54 -0400 Subject: [PATCH 10/13] `sequence` instead of `merge` in `ap` example for guaranteed order --- lib/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index 2648c60..ac891ba 100755 --- a/lib/index.js +++ b/lib/index.js @@ -2255,7 +2255,7 @@ addMethod('flatMap', function (f) { * * _('change', input) * .ap(fns) - * .merge() + * .sequence() * .map(showAutocompleteResults); */ From 0bdafe2e9faa0ee2744c6c03cd647f1866d4b0ea Mon Sep 17 00:00:00 2001 From: Matt Ross Date: Thu, 25 Oct 2018 09:23:59 -0400 Subject: [PATCH 11/13] Correct `asyncUnit` empty stream in `ap` example --- lib/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index ac891ba..db95a77 100755 --- a/lib/index.js +++ b/lib/index.js @@ -2247,11 +2247,11 @@ addMethod('flatMap', function (f) { * @param {Stream} m - incoming stream of function(s) to apply to value(s) in stream * @api public * - * var asyncUnit = () => _.of(_.nil); + * var asyncUnit = () => _([]); * var asyncAutocomplete = e => _(fetch(`/autocomplete?s=${e.target.value}`)); * * var fns = _('change', checkbox) - * .flatMap(e => h.of(e.target.checked ? asyncAutocomplete : asyncUnit)); + * .flatMap(e => _.of(e.target.checked ? asyncAutocomplete : asyncUnit)); * * _('change', input) * .ap(fns) From 0d14e7f588930a5fc3d77a5809f1297ac4caf244 Mon Sep 17 00:00:00 2001 From: Matt Ross Date: Thu, 25 Oct 2018 10:00:18 -0400 Subject: [PATCH 12/13] Consolidate `ap - composition` left/right tests --- test/test.js | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/test/test.js b/test/test.js index 1a493ce..170ef51 100755 --- a/test/test.js +++ b/test/test.js @@ -5102,40 +5102,33 @@ exports.ap = { test.same(results, ['g1(1)', 'g1(2)', 'g2(2)', 'g2(3)']); test.done(); }, - // v.ap(u.ap(a.map(f => g => x => f(g(x))))) is equivalent to v.ap(u).ap(a) (composition) 'composition': { - 'left': function (test) { - test.expect(1); - var v = _.of('a'); + 'v.ap(u.ap(a.map(f => g => x => f(g(x))))) is equivalent to v.ap(u).ap(a)': function (test) { + test.expect(3); + var v = _([1, 2, 3]); var u = _.of(function (x) { - return x + 'b'; + return 'u(' + x + ')'; }); var a = _.of(function (x) { - return x + 'c'; + return 'a(' + x + ')'; }); - v.fork().ap(u.fork().ap(a.fork().map(function (f) { + var left = v.fork().ap(u.fork().ap(a.fork().map(function (f) { return function (g) { return function (x) { return f(g(x)); }; }; - }))).toArray(function (x) { - test.same(x, ['abc']); - }); - test.done(); - }, - 'right': function (test) { - test.expect(1); - var v = _.of('a'); - var u = _.of(function (x) { - return x + 'b'; - }); - var a = _.of(function (x) { - return x + 'c'; - }); - v.ap(u).ap(a).toArray(function (x) { - test.same(x, ['abc']); - }); + }))); + var right = v.observe().ap(u.observe()).ap(a.observe()); + + _([left.collect(), right.collect()]) + .sequence() + .apply(function (lefts, rights) { + test.same(lefts, ['a(u(1))', 'a(u(2))', 'a(u(3))']); + test.same(rights, ['a(u(1))', 'a(u(2))', 'a(u(3))']); + test.same(lefts, rights); + }); + test.done(); }, }, From cfb68d31b142bd2a78b45219cc9025a8987d7198 Mon Sep 17 00:00:00 2001 From: Matt Ross Date: Thu, 25 Oct 2018 10:09:16 -0400 Subject: [PATCH 13/13] Update CHANGELOG --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e715f..653ae87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ This file does not aim to be comprehensive (you have git history for that), rather it lists changes that might impact your own code as a consumer of this library. +3.0.0-beta.7 +----- + +### New additions +* `ap` - Applies a stream of function(s) to the stream of value(s). + [#643](https://github.com/caolan/highland/pull/643). + 3.0.0-beta.6 ----- This release contains all changes from [2.12.0](#2120).