From 73bc3083a5a8156fa895e50b6ce17cffae4e3651 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Thu, 19 Apr 2018 22:20:57 -0400 Subject: [PATCH 01/15] Remove syntax requirement --- pipeline-lift.md | 62 +++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/pipeline-lift.md b/pipeline-lift.md index 4bc6401..6cba7bb 100644 --- a/pipeline-lift.md +++ b/pipeline-lift.md @@ -50,11 +50,12 @@ const toSlug = composeRight( Or, using this proposal: ```js -const toSlug = - _ => _.split(" ") - :> _ => _.map(str => str.toLowerCase()) - :> _ => _.join("-") - :> encodeURIComponent +const toSlug = Object.then( + _ => _.split(" "), + _ => _.map(word => word.toLowerCase()), + _ => _.join("-"), + encodeURIComponent +) ``` Another scenario is when you just want to trivially transform a collection. `Array.prototype.map` exists already for this purpose, but we can do that for maps and sets, too. This would let you turn code from this, to re-borrow a previous example: @@ -75,7 +76,7 @@ to something that's a little less nested (in tandem with the [pipeline operator function toSlug(string) { return string |> _ => _.split(" ") - :> word => word.toLowerCase() + |> _ => Object.then(_, word => word.toLowerCase()) |> _ => _.join("-") |> encodeURIComponent } @@ -93,31 +94,38 @@ These are, of course, very convenient functions to have, but it's very inefficie Here's what I propose: -1. A new low-precedence `x :> f` left-associative infix operator for left-to-right lifted pipelines. -1. An async variant `x :> async f` for pipelines with async return values and/or callbacks. -1. An async variant `x :> await f` that is sugar for `await (x :> async f)` -1. Two new well-known symbols `@@lift` and `@@asyncLift` that are used by those pipeline operators to dispatch based on type. +1. A new `Object.then(x, ...fs)` function for lifted calls +1. A new `Object.asyncThen(x, ...fs)` function for lifted async calls +1. Two new well-known symbols `@@then` and `@@asyncThen` that are used by those pipeline operators to dispatch based on type. The pipeline operators simply call `Symbol.lift`/`Symbol.asyncLift`: ```js -function pipe(x, f) { - if (typeof func !== "function") throw new TypeError() - return x[Symbol.lift](x => f(x)) +Object.then = function (x, ...funcs) { + for (let i = 0; i < funcs.length; i++) { + const func = funcs[i] + if (typeof func !== "function") throw new TypeError() + x = x[Symbol.then](x => f(x)) + } + return x } -async function asyncPipe(x, f) { - if (typeof func !== "function") throw new TypeError() - return x[Symbol.asyncLift](async x => f(x)) +Object.asyncThen = function (x, ...funcs) { + for (let i = 0; i < funcs.length; i++) { + const func = funcs[i] + if (typeof func !== "function") throw new TypeError() + x = x[Symbol.asyncThen](async x => f(x)) + } + return x } ``` -Here's how that `Symbol.lift` would be implemented for some of these types (`Symbol.asyncLift` would be nearly identical for each of these): +Here's how that `Symbol.then` would be implemented for some of these types (`Symbol.asyncThen` would be nearly identical for each of these): -- `Function.prototype[Symbol.lift]`: binary function composition like this: +- `Function.prototype[Symbol.then]`: binary function composition like this: ```js - Function.prototype[Symbol.lift] = function (g) { + Function.prototype[Symbol.then] = function (g) { const f = this // Note: this should only be callable. return function (...args) { @@ -126,14 +134,14 @@ Here's how that `Symbol.lift` would be implemented for some of these types (`Sym } ``` -- `Array.prototype[Symbol.lift]`: Equivalent to `Array.prototype.map`, but only calling the callback with one argument. (This enables optimizations not generally possible with `Array.prototype.map`, like eliding intermediate array allocations.) +- `Array.prototype[Symbol.then]`: Equivalent to `Array.prototype.map`, but only calling the callback with one argument. (This enables optimizations not generally possible with `Array.prototype.map`, like eliding intermediate array allocations.) -- `Promise.prototype[Symbol.lift]`: Equivalent to `Promise.prototype.then`, if passed only one argument. +- `Promise.prototype[Symbol.then]`: Equivalent to `Promise.prototype.then`, if passed only one argument. -- `Iterable.prototype[Symbol.lift]`: Returns an iterable that does this: +- `Iterable.prototype[Symbol.then]`: Returns an iterable that does this: ```js - Iterable.prototype[Symbol.lift] = function (func) { + Iterable.prototype[Symbol.then] = function (func) { return { next: v => { const {done, value} = this.next(v) @@ -145,10 +153,10 @@ Here's how that `Symbol.lift` would be implemented for some of these types (`Sym } ``` -- `Map.prototype[Symbol.lift]`: Map iteration/update like this: +- `Map.prototype[Symbol.then]`: Map iteration/update like this: ```js - Map.prototype[Symbol.lift] = function (func) { + Map.prototype[Symbol.then] = function (func) { const result = new this.constructor() for (const pair of this) { @@ -160,10 +168,10 @@ Here's how that `Symbol.lift` would be implemented for some of these types (`Sym } ``` -- `Set.prototype[Symbol.lift]`: Set iteration/update like this: +- `Set.prototype[Symbol.then]`: Set iteration/update like this: ```js - Set.prototype[Symbol.lift] = function (func) { + Set.prototype[Symbol.then] = function (func) { const result = new this.constructor() for (const value of this) { From 2867fc0c992dd302ffdf02bdd0e35c2fee261895 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Thu, 19 Apr 2018 22:24:11 -0400 Subject: [PATCH 02/15] Fix a couple stray `.lift`s --- pipeline-lift.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipeline-lift.md b/pipeline-lift.md index 6cba7bb..c872ea3 100644 --- a/pipeline-lift.md +++ b/pipeline-lift.md @@ -98,7 +98,7 @@ Here's what I propose: 1. A new `Object.asyncThen(x, ...fs)` function for lifted async calls 1. Two new well-known symbols `@@then` and `@@asyncThen` that are used by those pipeline operators to dispatch based on type. -The pipeline operators simply call `Symbol.lift`/`Symbol.asyncLift`: +The pipeline operators simply call `Symbol.then`/`Symbol.asyncthen`: ```js Object.then = function (x, ...funcs) { From 8ad8297c219a939947a0adb38dda4595b3c5fb8f Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Thu, 19 Apr 2018 22:25:49 -0400 Subject: [PATCH 03/15] Update pipeline-lift.md --- pipeline-lift.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipeline-lift.md b/pipeline-lift.md index c872ea3..da158cb 100644 --- a/pipeline-lift.md +++ b/pipeline-lift.md @@ -96,7 +96,7 @@ Here's what I propose: 1. A new `Object.then(x, ...fs)` function for lifted calls 1. A new `Object.asyncThen(x, ...fs)` function for lifted async calls -1. Two new well-known symbols `@@then` and `@@asyncThen` that are used by those pipeline operators to dispatch based on type. +1. Two new well-known symbols `@@then` and `@@asyncThen` that are used by those builtins to dispatch based on type. The pipeline operators simply call `Symbol.then`/`Symbol.asyncthen`: From a0c97868e27a999c40568a925377cdff728eb5b9 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Thu, 19 Apr 2018 22:56:00 -0400 Subject: [PATCH 04/15] Remove syntax requirement --- pipeline-lift.md | 82 +++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/pipeline-lift.md b/pipeline-lift.md index da158cb..65ba4a0 100644 --- a/pipeline-lift.md +++ b/pipeline-lift.md @@ -98,25 +98,40 @@ Here's what I propose: 1. A new `Object.asyncThen(x, ...fs)` function for lifted async calls 1. Two new well-known symbols `@@then` and `@@asyncThen` that are used by those builtins to dispatch based on type. -The pipeline operators simply call `Symbol.then`/`Symbol.asyncthen`: +The pipeline operators simply call `Symbol.then`/`Symbol.asyncThen`: ```js -Object.then = function (x, ...funcs) { - for (let i = 0; i < funcs.length; i++) { - const func = funcs[i] - if (typeof func !== "function") throw new TypeError() - x = x[Symbol.then](x => f(x)) +function invokeThen(x) { + for (var i = 1; i < arguments.length; i++) { + if (typeof arguments[i] !== "function") throw new TypeError() + } + for (var i = 1; i < arguments.length; i++) { + const func = arguments[i] + x = x[Symbol.then](this(arguments[i])) } return x } -Object.asyncThen = function (x, ...funcs) { - for (let i = 0; i < funcs.length; i++) { - const func = funcs[i] - if (typeof func !== "function") throw new TypeError() - x = x[Symbol.asyncThen](async x => f(x)) +function syncWrap(f) { + return function (x) { return f(x) } +} + +function asyncWrap(f) { + return function (x) { + try { + return Promise.resolve(f(x)) + } catch (e) { + return Promise.reject(e) + } } - return x +} + +Object.then = function then(x) { + return invokeThen.apply(syncWrap, arguments) +} + +Object.asyncThen = function asyncThen(x) { + return invokeThen.apply(asyncWrap, arguments) } ``` @@ -126,10 +141,9 @@ Here's how that `Symbol.then` would be implemented for some of these types (`Sym ```js Function.prototype[Symbol.then] = function (g) { - const f = this - // Note: this should only be callable. - return function (...args) { - return g.call(this, f.call(this, ...args)) + var f = this + return function () { + return g.call(this, f.apply(this, arguments)) } } ``` @@ -142,13 +156,18 @@ Here's how that `Symbol.then` would be implemented for some of these types (`Sym ```js Iterable.prototype[Symbol.then] = function (func) { + var iter = this return { - next: v => { - const {done, value} = this.next(v) - return {done, value: done ? value : func(value)} + next: function (v) { + var result = iter.next(v) + var done = result.done + return { + done: done, + value: done ? result.value : func(result.value) + } }, - throw: v => this.throw(v), - return: v => this.return(v), + throw: function (v) { return iter.throw(v) }, + return: function (v) { return iter.return(v) }, } } ``` @@ -157,14 +176,9 @@ Here's how that `Symbol.then` would be implemented for some of these types (`Sym ```js Map.prototype[Symbol.then] = function (func) { - const result = new this.constructor() - - for (const pair of this) { - const [newKey, newValue] = func(pair) - result.set(newKey, newValue) - } - - return result + return new this.constructor(Array.from(this, function (pair) { + return func(pair) + })) } ``` @@ -172,12 +186,8 @@ Here's how that `Symbol.then` would be implemented for some of these types (`Sym ```js Set.prototype[Symbol.then] = function (func) { - const result = new this.constructor() - - for (const value of this) { - result.add(func(value)) - } - - return result + return new this.constructor(Array.from(this, function (value) { + return func(value) + })) } ``` From 3e7545070d4059bd1fa54912665b771bb12a7e61 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Thu, 19 Apr 2018 23:11:20 -0400 Subject: [PATCH 05/15] Strip some redundancy, fix some bugs --- pipeline-lift.md | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/pipeline-lift.md b/pipeline-lift.md index 65ba4a0..1e23f3a 100644 --- a/pipeline-lift.md +++ b/pipeline-lift.md @@ -101,21 +101,17 @@ Here's what I propose: The pipeline operators simply call `Symbol.then`/`Symbol.asyncThen`: ```js -function invokeThen(x) { - for (var i = 1; i < arguments.length; i++) { - if (typeof arguments[i] !== "function") throw new TypeError() - } +function syncWrap(f) { + return function (x) { return f(x) } +} + +Object.then = function then(x) { for (var i = 1; i < arguments.length; i++) { - const func = arguments[i] - x = x[Symbol.then](this(arguments[i])) + x = x[Symbol.then](syncWrap(arguments[i])) } return x } -function syncWrap(f) { - return function (x) { return f(x) } -} - function asyncWrap(f) { return function (x) { try { @@ -126,12 +122,11 @@ function asyncWrap(f) { } } -Object.then = function then(x) { - return invokeThen.apply(syncWrap, arguments) -} - Object.asyncThen = function asyncThen(x) { - return invokeThen.apply(asyncWrap, arguments) + for (var i = 1; i < arguments.length; i++) { + x = x[Symbol.asyncThen](asyncWrap(arguments[i])) + } + return x } ``` From 6cf319a9da4a223a055fe49dd31cbd570c0cb5e2 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Fri, 20 Apr 2018 00:40:34 -0400 Subject: [PATCH 06/15] Remove syntax requirement, depromisify `Symbol.asyncChain` --- pipeline-chain.md | 225 ++++++++++++++++------------------------------ 1 file changed, 79 insertions(+), 146 deletions(-) diff --git a/pipeline-chain.md b/pipeline-chain.md index 0c1dc9d..d9d8be1 100644 --- a/pipeline-chain.md +++ b/pipeline-chain.md @@ -10,138 +10,80 @@ This requires a new primitive like `Symbol.chain` for invoking a callback and re - An array of zero or more values to wrap - `null`/`undefined` as a cue to break and/or unsubscribe. -These desugar to a `Symbol.chain` call, and exist to allow expressing complex logic without sacrificing conciseness or becoming too complex to use in of themselves. There are three variants: +These desugar to a `Symbol.chain` or `Symbol.asyncChain` call, and exist to allow expressing complex logic without sacrificing conciseness or becoming too complex to use in of themselves. There are two variants: -- `coll >:> func` - This does a simple sync chain via `Symbol.chain`, returning the chained value. It may be used anywhere. -- `coll >:> async func` - This does an async chain via `Symbol.asyncChain`, returning a promise to the chained result. It may be used anywhere. -- `coll >:> await func` - This does an async chain, awaiting for and returning the chained result. It may be used only in `async` functions, and is sugar for `await (coll >:> async func)`. +- `Object.chain(coll, ...funcs)` - This does a simple sync chain via `Symbol.chain`, returning the chained value. +- `Object.chainAsync(coll, ...funcs)` - This does an async chain via `Symbol.asyncChain`, returning a promise to the chained result. +- Two new well-known symbols `@@chain` and `@@asyncChain` used by the above builtins to dispatch based on type. -The desugaring is pretty straightforward, but they require some runtime helpers: +They are pretty simple conceptually: invoke the callback, unwrap it as applicable, and return the result. The callback returns one of three types (a `TypeError` is thrown otherwise): -```js -coll >:> func -// Compiles to: -invokeChainSync(coll, func) - -coll >:> async func -// Compiles to: -invokeChainAsync(coll, func) - -coll >:> await func -// Compiles to: -await invokeChainAsync(coll, func) -``` +- A value with a `Symbol.chain` and/or `Symbol.asyncChain` method, to unwrap +- An array of zero or more values to wrap +- `null`/`undefined` as a cue to break and/or unsubscribe. Here's how `Symbol.chain` would be implemented for some built-in types: - `Array.prototype[Symbol.chain]`: Basically the proposed `Array.prototype.flatMap`, but aware of the rules above. -- `Iterable.prototype[Symbol.chain]`, etc.: Flattens iterables out. +- `Iterable.prototype[Symbol.chain]`, etc.: Flattens iterables out, but aware of the rules above. -- `Promise.prototype[Symbol.chain]`, etc.: Alias for `Promise.prototype[Symbol.lift]`. +- `Promise.prototype[Symbol.chain]`: Similar to `Promise.prototype.then`, except it doesn't resolve at all if the callback returns `null`/`undefined`. ## Use cases One easy way to use it is with defining custom stream operators, generically enough you don't usually need to concern yourself about what stream implementation they're using, or even if it's really a stream and not a generator. Here's some common stream operators, implemented using this idea: ```js -// Usage: x >:> distinct({by?, with?}) -function distinct({by = (a, b) => a === b, with: get = x => x} = {}) { +// Usage: distinct(coll, {by?, with?}) +function distinct(coll, {by = (a, b) => a === b, with: get = x => x} = {}) { let hasPrev = false, prev - return x => { + return Object.chain(coll, x => { const memo = hasPrev hasPrev = true return !memo || by(prev, prev = get(x)) ? [x] : [] } } -// Usage: x >:> filter(func) -function filter(func) { - return x => func(x) ? [x] : [] +// Usage: filter(coll, func) +function filter(coll, func) { + return Object.chain(coll, x => func(x) ? [x] : []) } -// Usage: x >:> scan(func) -function scan(func) { +// Usage: scan(coll, func) +function scan(coll, func) { let hasPrev = false, prev - return x => { + return Object.chain(coll, x => { const memo = hasPrev hasPrev = true return memo ? [prev, func(prev, prev = x)] : [prev = x] - } + }) } -// Usage: x >:> each(func) +// Usage: each(coll, func) // Return truthy to break -function each(func) { - return item => func(item) ? undefined : [] +function each(coll, func) { + return Object.chain(coll, item => func(item) ? undefined : []) } -// Usage: x >:> async eachAsync(func) +// Usage: eachAsync(coll, func) // Return truthy to break -function eachAsync(func) { - return async item => await func(item) ? undefined : [] +function eachAsync(coll, func) { + return Object.chainAsync(coll, async item => await func(item) ? undefined : []) } // Usage: x >:> uniq({by?, with?}) -function uniq({by, with: get = x => x} = {}) { +function uniq(coll, {by, with: get = x => x} = {}) { const set = by == null ? new Set() : (items => ({ has: item => items.some(memo => by(memo, item)), add: item => items.push(item), })([]) - return item => { + return Object.chain(coll, item => { const memo = get(item) if (set.has(memo)) return [] set.add(memo) return [item] - } -} -``` - -You can also generically define common collection predicates like `includes` or `every`, which work for observables, arrays, and streams equally (provided they're eagerly iterated), and still short-circuit. - -```js -// Usage: includes(coll, item) -function includes(coll, item) { - let result = false - coll >:> x => { - if (x !== item) return [] - result = true - return undefined - } - return result -} - -// Usage: includesAsync(coll, item) -async function includesAsync(coll, item) { - let result = false - coll >:> await x => { - if (x !== item) return [] - result = true - return undefined - } - return result -} - -// Usage: every(coll, func) -function every(coll, func) { - let result = true - coll >:> x => { - if (func(x)) return [] - result = false - return undefined - } - return result -} - -// Usage: everyAsync(coll, func) -async function everyAsync(coll, func) { - let result = true - coll >:> await async x => { - if (await func(x)) return [] - result = false - return undefined - } - return result + }) } ``` @@ -155,76 +97,67 @@ The helpers themselves are not too complicated, but they do have things they hav - If cancellation support is added, we'd also have to manage that. ```js -function invokeChainSync(coll, func) { - if (typeof func !== "function") throw new TypeError("callback must be a function") - var state = "open" - return coll[Symbol.chain](function (x) { - if (state === "locked") throw new ReferenceError("Recursive calls not allowed!") - if (state === "closed") throw new ReferenceError("Chain already closed!") - try { var result = func(x) } catch (e) { state = "open"; throw e } - if (result == null) { state = "closed"; func = void 0; return void 0 } - state = "open" - if (Array.isArray(result)) return result - - try { - state = "locked" - if (typeof result[Symbol.chain] === "function") return result - throw new TypeError("Invalid type for result") - } finally { +Object.chain = function (coll) { + function wrapChain(func) { + var state = "open" + return function (x) { + if (state === "locked") throw new ReferenceError("Recursive calls not allowed!") + if (state === "closed") throw new ReferenceError("Chain already closed!") + try { var result = func(x) } catch (e) { state = "open"; throw e } + if (result == null) { state = "closed"; func = void 0; return void 0 } state = "open" - } - }) -} + if (Array.isArray(result)) return result -function invokeChainAsync(coll, func) { - function asyncNext(result) { - if (state === "closed") return void 0 - if (result == null) { state = "closed"; func = void 0; return void 0 } - if (Array.isArray(result)) return result - try { - state = "locked" - if (typeof result[Symbol.chain] === "function") return result - throw new TypeError("Invalid type for result") - } finally { - state = "open" - } - } - if (typeof func !== "function") return Promise.reject(new TypeError("callback must be a function")) - try { - var state = "open" - return Promise.resolve(coll[Symbol.asyncChain](function (x) { - if (state === "locked") return Promise.reject(new ReferenceError("Recursive calls not allowed!")) - if (state === "closed") return Promise.reject(new ReferenceError("Chain already closed!")) try { state = "locked" - return Promise.resolve(func(x)).then(asyncNext) - } catch (e) { - return Promise.reject(e) + if (typeof result[Symbol.chain] === "function") return result + throw new TypeError("Invalid type for result") } finally { state = "open" } - })) - } catch (e) { - return Promise.reject(e) + } + } + for (var i = 1; i < arguments.length; i++) { + coll = coll[Symbol.chain](wrapChain(arguments[i])) } + return coll } -``` - -In case you're concerned about the size, the two helpers bundled by themselves racks up a whopping 0.4K min+gzip, but that cost will come down when bundled with your app (and [this is worst case - I've seen the addition of code *reduce* gzip'd size](https://github.com/MithrilJS/mithril.js/issues/2095#issuecomment-373222642)). This might seem like a lot for a language feature, but it's not as much as you might think: -- My own personal contact form [has more JS than this](https://github.com/isiahmeadows/website/blob/master/src/contact.js), having about 2.0K bytes minified, 1.5K min+gzip with headers and everything. And that literally only does custom validation messaging and AJAX form submission. - -- If you have ever used `for ... of` with Babel, this is absolute child's play - Regenerator is about 6.2K minified, 2.3 K min+gzip for its runtime alone, and a simple Babelified `flatMap` (defined below) with the `es2015` preset compiles to almost that much code (about 0.8K minified pre-gzip, 0.4K min+gzip). +Object.chainAsync = function (coll) { + function wrapChain(func) { + var state = "open" + function asyncNext(result) { + if (state === "closed") return void 0 + if (result == null) { state = "closed"; func = void 0; return void 0 } + if (Array.isArray(result)) return result + try { + state = "locked" + if (typeof result[Symbol.chain] === "function") return result + throw new TypeError("Invalid type for result") + } finally { + state = "open" + } + } + return function (x) { + if (state === "locked") throw new ReferenceError("Recursive calls not allowed!") + if (state === "closed") throw new ReferenceError("Chain already closed!") + try { var result = func(x) } catch (e) { state = "open"; throw e } + if (result == null) { state = "closed"; func = void 0; return void 0 } + state = "open" + if (Array.isArray(result)) return result - ```js - function *flatMap(iter, func) { - for (const item of iter) { - const result = func(item) - if (result != null && typeof result[Symbol.iterator] === "function") { - yield* result - } else { - yield result + try { + state = "locked" + if (typeof result[Symbol.chain] === "function") return result + throw new TypeError("Invalid type for result") + } finally { + state = "open" } } } - ``` + for (var i = 1; i < arguments.length; i++) { + coll = coll[Symbol.asyncChain](wrapChain(arguments[i])) + } + return coll +} +``` From 5757daa0e42a0afeb0a45d8a1a022ad547876f4d Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Fri, 20 Apr 2018 06:36:44 -0400 Subject: [PATCH 07/15] Create polyfill-then.js --- polyfill-then.js | 754 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 754 insertions(+) create mode 100644 polyfill-then.js diff --git a/polyfill-then.js b/polyfill-then.js new file mode 100644 index 0000000..18e1892 --- /dev/null +++ b/polyfill-then.js @@ -0,0 +1,754 @@ +// This is a full polyfill for `Object.then`, `Object.asyncThen`, `Symbol.then`, +// and `Symbol.asyncThen`. It's ES5-compatible assuming ES6 globals and supports +// some ES7+ globals if present. (Note: Babelified async generators won't be +// detected - I have no way of attaching myself to them globally.) +;(function (global) { + "use strict" + // Global imports + var Object = global.Object + var Array = global.Array + var Function = global.Function + var Symbol = global.Symbol + var Promise = global.Promise + var Map = global.Map + var Set = global.Set + var IteratorPrototype = Object.getPrototypeOf( + Object.getPrototypeOf([].entries()) + ) + + // Note: all methods that need called after the polyfill runs *must* be + // pulled out like these. + var tempMapIter = new Map().entries() + var tempSetIter = new Set().entries() + var call = Function.call.bind(Function.call) + var apply = Function.call.bind(Function.apply) + var mapForEach = Function.call.bind(Map.prototype.forEach) + var setForEach = Function.call.bind(Set.prototype.forEach) + var mapEntries = Function.call.bind(Map.prototype.entries) + var setEntries = Function.call.bind(Set.prototype.entries) + var mapEntriesNext = Function.call.bind(tempMapEntries.next) + var setEntriesNext = Function.call.bind(tempSetEntries.next) + var PromiseResolve = Promise.resolve.bind(Promise) + var PromiseReject = Promise.reject.bind(Promise) + var PromiseThen = Function.call.bind(Promise.prototype.then) + var isArray = Array.isArray + var from = Array.from + var defineProperty = Object.defineProperty + var objectCreate = Object.create + var symbolSpecies = Symbol.species + var defineTypedArrayMethod, AsyncIteratorPrototype + + var types = [ + global.Int8Array, global.Uint8Array, global.Uint8ClampedArray, + global.Int16Array, global.Uint16Array, global.Int32Array, + global.Uint32Array, global.Float32Array, global.Float64Array + ] + .filter(function (Type) { + if (typeof Type !== "function") return false + if (Type.prototype == null) return false + var proto = Object.getPrototypeOf(Type.prototype) + if (proto !== Object.prototype) return true + var desc = Object.getOwnPropertyDescriptor(proto, "buffer") + return desc != null && typeof desc.get === "function" + }) + + try { + AsyncIteratorPrototype = (0, eval)( + "Object.getPrototypeOf(async function*(){}())" + ) + } catch (e) { + // ignore - we can't reach it otherwise. + } + + function methodName(method) { + return typeof method === "symbol" + ? "[" + String(method).slice(7, -1) + "]" + : method + } + + function checkMap(object, method) { + try { + mapEntries(object) + } catch (e) { + throw new TypeError( + methodName(method) + " method called on incompatible receiver" + ) + } + } + + function checkSet(object, method) { + try { + setEntries(object) + } catch (e) { + throw new TypeError( + methodName(method) + " method called on incompatible receiver" + ) + } + } + + function TypedArrayController(Type) { + this.type = Type + this.subarray = Function.call.bind(Type.prototype.subarray) + this.buffer = Function.call.bind( + Object.getOwnPropertyDescriptor(Type.prototype, "buffer").get + ) + this.length = Function.call.bind( + Object.getOwnPropertyDescriptor(Type.prototype, "length").get + ) + this.get = Function.call.bind(Type.prototype.get) + this.set = Function.call.bind(Type.prototype.set) + this.set = Function.call.bind(Type.prototype.set) + } + + TypedArrayController.prototype.validate = function (value, method, isResult) { + try { + this.subarray(value) + } catch (e) { + var message = methodName(method) + " method called on " + try { + this.buffer(value) + message += "detached ArrayBuffer" + } catch (e) { + message += "incompatible receiver" + } + if (isResult) message += " as target" + throw new TypeError(message) + } + } + + TypedArrayController.prototype.create = function (object, length, method) { + var C = speciesConstructor(object, this.type) + var result = new C(length) + this.validate(result, method, true) + if (this.length(result) < length) { + throw new TypeError( + methodName(method) + + " method called on receiver too small as target" + ) + } + return result + } + + if (types.length === 0) { + defineTypedArrayMethod = function () { + // ignore + } + } else if (Object.getPrototypeOf(types[0].prototype) === Object.prototype) { + types = types.map(function (Type) { + return new TypedArrayController(Type) + }) + defineTypedArrayMethod = function (name, create) { + for (var i = 0; i < types.length; i++) { + polyfill(types[i].type.prototype, name, create(types[i])) + } + } + } else { + types = new TypedArrayController(Object.getPrototypeOf(types[0])) + defineTypedArrayMethod = function (name, create) { + polyfill(types.type.prototype, name, create(types)) + } + } + + function methods(proto, keys) { + for (var key in keys) { + var desc = Object.getOwnPropertyDescriptor(keys, key) + desc.enumerable = false + defineProperty(proto, key, desc) + } + } + + function defineIterator(options) { + var sym = Symbol.for(options.key) + var ChildPrototype = objectCreate(IteratorPrototype) + + methods(ChildPrototype, { + next: function (value) { + var state = internalGet(sym, this, "next") + return call(options.next, state, value) + }, + + throw: function (value) { + var state = internalGet(sym, this, "throw") + return call(options.throw, state, value) + }, + + return: function (value) { + var state = internalGet(sym, this, "return") + return call(options.return, state, value) + }, + }) + + return function () { + var result = objectCreate(ChildPrototype) + createDataPropertyOrThrow(result, sym, + apply(options.create, void 0, arguments) + ) + return result + } + } + + function defineAsyncIterator(options) { + var sym = Symbol.for(options.key) + var ChildPrototype = objectCreate(AsyncIteratorPrototype) + + methods(ChildPrototype, { + next: function (value) { + try { + var state = internalGet(sym, this, "next") + return call(options.next, state, value) + } catch (e) { + return PromiseReject(e) + } + }, + + throw: function (value) { + try { + var state = internalGet(sym, this, "throw") + return call(options.throw, state, value) + } catch (e) { + return PromiseReject(e) + } + }, + + return: function (value) { + try { + var state = internalGet(sym, this, "return") + return call(options.return, state, value) + } catch (e) { + return PromiseReject(e) + } + }, + }) + + return function () { + var result = objectCreate(ChildPrototype) + createDataPropertyOrThrow(result, sym, + apply(options.create, void 0, arguments) + ) + return result + } + } + + function internalGet(sym, object, method) { + if (object != null && typeof object === "object") { + var result = object[sym] + if (result != null) return result + } + + throw new TypeError( + methodName(method) + " method called on incompatible receiver" + ) + } + + // Symbols + function defineSymbol(name) { + var value = Symbol[name] + if (typeof name !== "symbol") { + value = Symbol.for("Symbol." + name) + defineProperty(Symbol, name, { + configurable: false, + enumerable: false, + writable: false, + value: value + }) + } + return value + } + + var symbolThen = defineSymbol('then') + var symbolAsyncThen = defineSymbol('asyncThen') + var symbolCombine = defineSymbol('combine') + var symbolAsyncCombine = defineSymbol('asyncCombine') + var symbolChain = defineSymbol('chain') + var symbolAsyncChain = defineSymbol('asyncChain') + + // Common utilities + function toLength(value) { + value = +value + var maxSafeInt = 9007199254740991 // 2^53 - 1 + if (value > 0) { // Note: this can't be inverted without missing NaNs. + return value > maxSafeInt ? maxSafeInt : (value - value % 1) + } else { + return 0 + } + } + + function arraySpeciesCreate(originalArray, length) { + if (length === 0) length = +0 // + do { + if (!isArray(originalArray)) break + var C = originalArray.constructor + if (C !== null && typeof C === "object") { + C = C[symbolSpecies] + if (C === null) break + } + if (C === void 0 || C === Array) break + if (typeof C === "function" && "prototype" in C) { + return new C(length) + } else { + throw new TypeError("constructor property is not a constructor") + } + } while (false) + return new Array(length) + } + + function speciesConstructor(O, defaultConstructor) { + do { + var C = O.constructor + if (C === void 0) break + if (C !== null && ( + typeof C === "function" || typeof C === "object" + )) { + C = C[symbolSpecies] + if (C == null) break + if (typeof C === "function" && "prototype" in C) return C + } + throw new TypeError("constructor property is not a constructor") + } while (false) + return defaultConstructor + } + + var dataDescriptor = { + configurable: true, + enumerable: true, + writable: true, + value: void 0 + } + + function createDataPropertyOrThrow(object, property, value) { + dataDescriptor.value = value + defineProperty(object, property, dataDescriptor) + dataDescriptor.value = void 0 + } + + function polyfill(proto, method, value, force) { + if (!force && typeof proto[method] === "function") return + defineProperty(proto, method, { + configurable: true, + enumerable: false, + writable: true, + value: value + }) + try { + defineProperty(value, "name", {value: methodName(method)}) + } catch (e) { + // Swallow exceptions in case it's an ES5 environment + } + } + + // {Object,Symbol}.then + builtins + function syncWrap(f) { + return function (x) { return f(x) } + } + + polyfill(Object, "then", function then(x) { + for (var i = 1; i < arguments.length; i++) { + x = x[symbolThen](syncWrap(arguments[i])) + } + return x + }) + + polyfill(Function.prototype, symbolThen, function then(g) { + var f = this + if (typeof f !== "function") { + throw new TypeError("receiver must be callable!") + } + if (typeof g !== "function") { + throw new TypeError("argument must be a function!") + } + return function () { + return call(g, this, apply(f, this, arguments)) + } + }) + + polyfill(Array.prototype, symbolThen, function then(func) { + var O = Object(this) + if (typeof func !== "function") { + throw new TypeError("callback must be a function!") + } + var len = toLength(O.length) + var result = arraySpeciesCreate(O, len) + + for (var i = 0; i < len; i++) { + if (i in O) { + var mappedValue = func(O[i]) + createDataPropertyOrThrow(result, i, mappedValue) + } + } + + return result + }) + + polyfill(Promise.prototype, symbolThen, function then(func) { + if (typeof func !== "function") { + throw new TypeError("callback must be a function!") + } + return this.then(func, void 0) + }) + + function iteratorProxy(nextResult) { + return { + done: nextResult.done, + value: nextResult.value + } + } + + var iteratorThenIterator = defineIterator({ + key: '%IteratorPrototype%[Symbol.then] iterator', + + init: function (source, func) { + return {source: source, func: func} + }, + + next: function (value) { + var nextResult = this.source.next(value) + var nextDone = nextResult.done + var nextValue = nextResult.value + return { + done: nextDone, + value: nextDone ? nextValue : (0, this.func)(nextValue) + } + }, + + throw: function (value) { + return iteratorProxy(this.source.throw(value)) + }, + + return: function (value) { + return iteratorProxy(this.source.return(value)) + }, + }) + + polyfill(IteratorPrototype, symbolThen, function then(func) { + var O = Object(this) + if (typeof func !== "function") { + throw new TypeError("callback must be callable!") + } + return iteratorThenIterator(O, func) + }) + + if (AsyncIteratorPrototype != null) { + var asyncIteratorThenIterator = defineAsyncIterator({ + key: '%AsyncIteratorPrototype%[Symbol.then] async iterator', + + init: function (source, func) { + return {source: source, func: func} + }, + + next: function (value) { + var self = this + return PromiseThen( + PromiseResolve(this.source.next(value)), + function (nextResult) { + var nextDone = nextResult.done + var nextValue = nextResult.value + if (nextDone) return {done: nextDone, value: nextValue} + return PromiseThen( + (0, self.func)(nextValue), + function (nextValue) { + return {done: nextDone, value: nextValue} + } + ) + } + ) + }, + + throw: function (value) { + return PromiseThen( + PromiseResolve(this.source.throw(value)), + iteratorProxy + ) + }, + + return: function (value) { + return PromiseThen( + PromiseResolve(this.source.return(value)), + iteratorProxy + ) + } + }) + + polyfill(AsyncIteratorPrototype, symbolThen, function then(func) { + var O = Object(this) + if (typeof func !== "function") { + throw new TypeError("callback must be callable!") + } + return asyncIteratorThenIterator(O, func) + }) + } + + polyfill(Map.prototype, symbolThen, function then(func) { + checkMap(this) + if (typeof func !== "function") { + throw new TypeError("callback must be callable!") + } + var C = speciesConstructor(this, Map) + var map = new C() + mapForEach(this, function (key, value) { + var pair = func([key, value]) + map.set(pair[0], pair[1]) + }) + return map + }) + + polyfill(Set.prototype, symbolThen, function then(func) { + checkSet(this) + if (typeof func !== "function") { + throw new TypeError("callback must be callable!") + } + var C = speciesConstructor(this, Set) + var set = new C() + setForEach(this, function (value) { + set.add(func(value)) + }) + return set + }) + + defineTypedArrayMethod(symbolThen, function (ctrl) { + return function then(func) { + ctrl.validate(this) + if (typeof func !== "function") { + throw new TypeError("callback must be callable!") + } + var len = ctrl.length(this) + var A = ctrl.create(this, len, symbolThen) + for (var i = 0; i < len; i++) A[i] = func(this[i]) + return A + } + }) + + function asyncWrap(f) { + return function (x) { + try { + return PromiseResolve(f(x)) + } catch (e) { + return PromiseReject(e) + } + } + } + + polyfill(Object, "asyncThen", function asyncThen(x) { + for (var i = 1; i < arguments.length; i++) { + x = x[symbolAsyncThen](asyncWrap(arguments[i])) + } + return x + }) + + polyfill(Function.prototype, symbolAsyncThen, function asyncThen(g) { + var f = this + if (typeof f !== "function") { + throw new TypeError("receiver must be callable!") + } + if (typeof g !== "function") { + throw new TypeError("argument must be a function!") + } + return function () { + try { + var self = this + return PromiseThen( + PromiseResolve(apply(f, self, arguments)), + function (value) { return call(g, this, value) } + ) + } catch (e) { + return PromiseReject(e) + } + } + }) + + // This is specifically written to 1. avoid memory leaks, and 2. avoid + // parallelism (so it doesn't become a memory hog real quick). + polyfill(Array.prototype, symbolAsyncThen, function asyncThen(func) { + try { + var O = Object(this) + if (typeof func !== "function") { + throw new TypeError("callback must be a function!") + } + + var len = toLength(O.length) + var result = arraySpeciesCreate(O, len) + var target = 0 + + while (target !== len) { + if (target in O) { + return PromiseThen( + PromiseResolve(func(O[target])), + next + ) + } else { + target++ + } + } + + return PromiseResolve(result) + } catch (e) { + return PromiseReject(e) + } + + function next(value) { + createDataPropertyOrThrow(result, target++, value) + + while (target !== len) { + if (target in O) { + return PromiseThen( + PromiseResolve(func(O[target])), + next + ) + } else { + target++ + } + } + + return result + } + }) + + polyfill( + Promise.prototype, symbolAsyncThen, + Promise.prototype[symbolThen] + ) + + var iteratorAsyncThenIterator = defineAsyncIterator({ + key: '%IteratorPrototype%[Symbol.asyncThen] async iterator', + + init: function (source, func) { + return {source: souce, func: func} + }, + + next: function next(value) { + var nextResult = this.source.next(value) + var nextDone = nextResult.done + var nextValue = nextResult.value + if (nextDone) { + return PromiseResolve({done: nextDone, value: nextValue}) + } else { + return PromiseThen( + PromiseResolve((0, this.func)(nextValue)), + function (nextValue) { + return {done: nextDone, value: nextValue} + } + ) + } + }, + + throw: function (value) { + return PromiseResolve(iteratorProxy(this.source.throw(value))) + }, + + return: function (value) { + return PromiseResolve(iteratorProxy(this.source.return(value))) + } + }) + + polyfill(IteratorPrototype, symbolAsyncThen, function then(func) { + var O = Object(this) + if (typeof func !== "function") { + throw new TypeError("callback must be callable!") + } + return iteratorAsyncThenIterator(O, func) + }) + + if (AsyncIteratorPrototype != null) { + polyfill( + AsyncIteratorPrototype, symbolAsyncThen, + AsyncIteratorPrototype[symbolThen] + ) + } + + polyfill(Map.prototype, symbolAsyncThen, function (func) { + try { + checkMap(this) + if (typeof func !== "function") { + throw new TypeError("callback must be callable!") + } + var C = speciesConstructor(this, Map) + var target = new C() + var iter = mapEntries(this) + var nextResult = mapEntriesNext(iter) + if (nextResult.done) return PromiseResolve(target) + return PromiseThen( + PromiseResolve(func(nextResult.value)), + iterate + ) + } catch (e) { + return PromiseReject(e) + } + function iterate(pair) { + target.set(pair[0], pair[1]) + var nextResult = mapEntriesNext(iter) + if (nextResult.done) return target + return PromiseThen( + PromiseResolve(func(nextResult.value)), + iterate + ) + } + }) + + polyfill(Set.prototype, symbolAsyncThen, function (func) { + try { + checkSet(this) + if (typeof func !== "function") { + throw new TypeError("callback must be callable!") + } + var C = speciesConstructor(this, Set) + var target = new C() + var iter = setEntries(this) + var nextResult = setEntriesNext(iter) + if (nextResult.done) return PromiseResolve(target) + return PromiseThen( + PromiseResolve(func(nextResult.value)), + iterate + ) + } catch (e) { + return PromiseReject(e) + } + function iterate(value) { + target.add(value) + var nextResult = setEntriesNext(iter) + if (nextResult.done) return target + return PromiseThen( + PromiseResolve(func(nextResult.value)), + iterate + ) + } + }) + + // This is specifically written to 1. avoid memory leaks, and 2. avoid + // parallelism (so it doesn't become a memory hog real quick). + defineTypedArrayMethod(symbolThen, function (ctrl) { + return function (func) { + try { + ctrl.validate(this) + if (typeof func !== "function") { + throw new TypeError("callback must be callable!") + } + + var len = ctrl.length(this) + var A = ctrl.create(this, len, symbolThen) + var target = 0 + + if (len === 0) return PromiseResolve(A) + return PromiseThen( + PromiseResolve(func(O[0])), + next + ) + } catch (e) { + return PromiseReject(e) + } + + function next(value) { + A[target++] = value + if (target === len) return A + return PromiseThen( + PromiseResolve(func(O[target])), + next + ) + } + } + }) +})( + typeof window !== "undefined" ? window : + typeof self !== "undefined" ? self : + typeof global !== "undefined" ? global : + typeof this !== "undefined" ? this : + (0, eval)("this") +); From dcf8aab136af9bd6e354d7530d04f2d42efce51d Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Fri, 20 Apr 2018 06:40:31 -0400 Subject: [PATCH 08/15] Rename polyfill-then.js to polyfill.js --- polyfill-then.js => polyfill.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename polyfill-then.js => polyfill.js (100%) diff --git a/polyfill-then.js b/polyfill.js similarity index 100% rename from polyfill-then.js rename to polyfill.js From e5862ac135f92e5aedc8bffcbd9da316a2326183 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Fri, 20 Apr 2018 09:35:47 -0400 Subject: [PATCH 09/15] Update pipeline-combine.md --- pipeline-combine.md | 89 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 71 insertions(+), 18 deletions(-) diff --git a/pipeline-combine.md b/pipeline-combine.md index c90b109..9148f39 100644 --- a/pipeline-combine.md +++ b/pipeline-combine.md @@ -10,29 +10,55 @@ Unlike the other two, this one involves only new built-in functions, with no new - A `RangeError` is thrown if you don't have at least two items to combine. (You can't "combine" anything with nothing.) - If you don't pass `func`, it defaults to just returning the arguments as an array, effectively generating a sequence of combinations. - This is in fact variadic, but the last parameter is type-checked as a potential function. + - `Object.asyncCombine(...values, func?)` - Like `Object.combine`, but works with async functions + async iterators. + +- `Object.merge(...values)` - Like `Object.combine`, but instead of iterating combinations, it just interleaves everything. + - By default, it tries `value[Symbol.merge](other)` first. + - If that is missing, it tries `value[Symbol.combine](other, (a, b) => [a, b])[Symbol.chain](pair => pair)` instead. + - `value[Symbol.combine](other, func)` - You declare this to tell `Object.combine` how to combine two items. - If you can support combining with other types, it only cares about the first. + - `value[Symbol.asyncCombine](other, func)` - Equivalent of `Symbol.combine` for `Object.asyncCombine` +- `value[Symbol.merge](other)` - You declare this to tell `Object.merge` how to merge two collections, if the default is wrong. (Generally, the default is right for most collections, but it's not for anything varying over time.) + Here's how it'd be implemented for some builtins: - `Array.prototype[Symbol.combine]`: Zip the two arrays, optionally with a callback. - This is basically [Lodash's `_.zip_with`](https://lodash.com/docs#zipWith). - Yes, this could instead iterate combinations like in Python's `[func(x, y) for x in self for y in other]`, but it's not as broadly useful (especially in JS), and you can't do that with generic iterables. (You could already emulate that via `as.map(a => bs.map(b => f(a, b)))`.) + - Note: this does *not* skip indices, unlike with `Object.then`/`Symbol.then`. (Tracking this would be counterintuitive.) + - Note: this limits the length to that of the smaller array. - `Promise.prototype[Symbol.combine]`: Joins two promises and calls the function when both promises resolve, returning a new promise with the function's result. + - Both operands are type-checked to be promises, so they can remain on the same tick. - This is *slightly* duplicative of `Promise.all`, but the engine could better statically allocate promise resolution. - `Promise.all` will still remain better for awaiting dynamically-sized lists of promises. +- `Promise.prototype[Symbol.merge]`: Basically a binary `Promise.race`. Prefer this for smaller static lists, `Promise.race` for anything dynamically sized. + - `Iterable.prototype[Symbol.combine]`: Works similarly to `Array.prototype[Symbol.combine]`, but returns an iterable instead. - This is surprisingly harder than you'd expect to implement in userland while retaining `for ... of`-like semantics. -- You could implement `Function.prototype[Symbol.combine]` to return `(a, b) => func(this(a), other(b))`, but it's not generally very useful (even in the world of Haskell). +- `Map.prototype[Symbol.merge]`, `Set.prototype[Symbol.merge]`: Maps and sets are mergeable, but not meaningfully combined like arrays or promises. + - Maps and sets can implement this *very* efficiently. + - Maps merge based on keys. + +- You *could* implement `Function.prototype[Symbol.combine]` to return `(a, b) => func(this(a), other(b))`, but it's not generally very useful (even in the world of Haskell). ## Implementation -The general implementation would look like this: +An implementation most *certainly* should try to avoid taking the slow path for arrays and iterables, especially when merging, since there's *large* opportunities for optimization: + +- Promises don't need to allocate a full array to destructure for `Symbol.combine` - it just needs a simple array + +- Arrays could have their `Object.merge` lowered to copying into a new array, then an in-place matrix transpose. (That second step is the hard part.) + +- Iterators could have their `Object.merge` lowered into a simple round robin iterator. + +The polyfill implementation would look something like this: ```js // These are also optimized. @@ -52,18 +78,18 @@ Object.combine = function (value1, value2, func) { [Symbol.combine](func, ([a, b], c) => [a, b, c]) default: - let acc = value1[Symbol.combine](value2, (as, b) => [...as, b]) - const last = arguments[arguments.length - 1] + var acc = value1[Symbol.combine](value2, (as, b) => [...as, b]) + var last = arguments[arguments.length - 1] if (typeof last === "function") { - const end = arguments.length - 2 + var end = arguments.length - 2 - for (let i = 2; i < end; i++) { + for (var i = 2; i < end; i++) { acc = acc[Symbol.combine](arguments[i], (as, b) => [...as, b]) } - return acc[Symbol.lift2](arguments[arguments.length - 2], (as, b) => last(...as, b)) + return acc[Symbol.combine](arguments[arguments.length - 2], (as, b) => last(...as, b)) } else { - for (let i = 2; i < arguments.length; i++) { + for (var i = 2; i < arguments.length; i++) { acc = acc[Symbol.combine](arguments[i], (as, b) => [...as, b]) } @@ -72,38 +98,65 @@ Object.combine = function (value1, value2, func) { } } -Object.asyncCombine = async function (iter1, value2, func) { +Object.asyncCombine = function (value1, value2, func) { switch (arguments.length) { case 0: case 1: throw new RangeError("must have at least 2 entries to combine") case 2: return value1[Symbol.asyncCombine](value2, (a, b) => [a, b]) - + case 3: return typeof func === "function" ? value1[Symbol.asyncCombine](value2, (a, b) => func(a, b)) - : (await value1[Symbol.asyncCombine](iter2, (a, b) => [a, b])) + : value1 + [Symbol.asyncCombine](value2, (a, b) => [a, b]) [Symbol.asyncCombine](func, ([a, b], c) => [a, b, c]) default: - let acc = await value1[Symbol.asyncCombine](value2, (a, b) => [a, b]) - const last = arguments[arguments.length - 1] + var acc = value1[Symbol.asyncCombine](value2, (a, b) => [a, b]) + var last = arguments[arguments.length - 1] if (typeof last === "function") { - const end = arguments.length - 2 + var end = arguments.length - 2 - for (let i = 2; i < end; i++) { - acc = await acc[Symbol.asyncCombine](arguments[i], (as, b) => [...as, b]) + for (var i = 2; i < end; i++) { + acc = acc[Symbol.asyncCombine](arguments[i], (as, b) => [...as, b]) } return acc[Symbol.asyncCombine](arguments[arguments.length - 2], (as, b) => last(...as, b)) } else { - for (let i = 2; i < arguments.length; i++) { - acc = await acc[Symbol.asyncCombine](arguments[i], (as, b) => [...as, b]) + for (var i = 2; i < arguments.length; i++) { + acc = acc[Symbol.asyncCombine](arguments[i], (as, b) => [...as, b]) } return acc } } } + +Object.merge = function (value1, value2) { + switch (arguments.length) { + case 0: + throw new RangeError("must have at least one argument to merge") + + case 1: + return value1 + + case 2: + return typeof value1[Symbol.merge] === "function" + ? value1[Symbol.merge](value2) + : value1[Symbol.combine](value2, (a, b) => [a, b])[Symbol.chain](pair => pair) + + default: + var acc = value1 + + for (let i = 1; i < arguments.length; i++) { + acc = typeof acc[Symbol.merge] === "function" + ? acc[Symbol.merge](arguments[i]) + : acc[Symbol.combine](arguments[i], (a, b) => [a, b])[Symbol.chain](pair => pair) + } + + return acc + } +} ``` From 499f7bf7447ddb6d6b534dd7c70559cfa658bb2b Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Fri, 20 Apr 2018 09:40:06 -0400 Subject: [PATCH 10/15] Update pipeline-combine.md --- pipeline-combine.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipeline-combine.md b/pipeline-combine.md index 9148f39..9d9592c 100644 --- a/pipeline-combine.md +++ b/pipeline-combine.md @@ -46,7 +46,7 @@ Here's how it'd be implemented for some builtins: - Maps and sets can implement this *very* efficiently. - Maps merge based on keys. -- You *could* implement `Function.prototype[Symbol.combine]` to return `(a, b) => func(this(a), other(b))`, but it's not generally very useful (even in the world of Haskell). +- You *could* implement `Function.prototype[Symbol.combine]` to return `(a, b) => func(this(a), other(b))`, but it's not generally very useful (even in the world of Haskell), and it'd interfere with the overload resolution. ## Implementation From c054ac817fd76b72ed4991fa2161d0fa33005de4 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Fri, 20 Apr 2018 10:15:43 -0400 Subject: [PATCH 11/15] Update pipeline-chain.md --- pipeline-chain.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pipeline-chain.md b/pipeline-chain.md index d9d8be1..99ad712 100644 --- a/pipeline-chain.md +++ b/pipeline-chain.md @@ -72,6 +72,16 @@ function eachAsync(coll, func) { return Object.chainAsync(coll, async item => await func(item) ? undefined : []) } +// Usage: tap(coll, func) +function tap(coll, func) { + return Object.then(coll, item => { func(item); return item }) +} + +// Usage: tapAsync(coll, func) +function tapAsync(coll, func) { + return Object.thenAsync(coll, async item => { await func(item); return item }) +} + // Usage: x >:> uniq({by?, with?}) function uniq(coll, {by, with: get = x => x} = {}) { const set = by == null ? new Set() : (items => ({ From 343fa0893ff440d6c876916dc7cc41e58d549e08 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Fri, 20 Apr 2018 10:18:45 -0400 Subject: [PATCH 12/15] Update pipeline-chain.md --- pipeline-chain.md | 53 ++++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/pipeline-chain.md b/pipeline-chain.md index 99ad712..30fc402 100644 --- a/pipeline-chain.md +++ b/pipeline-chain.md @@ -35,60 +35,65 @@ Here's how `Symbol.chain` would be implemented for some built-in types: One easy way to use it is with defining custom stream operators, generically enough you don't usually need to concern yourself about what stream implementation they're using, or even if it's really a stream and not a generator. Here's some common stream operators, implemented using this idea: ```js -// Usage: distinct(coll, {by?, with?}) -function distinct(coll, {by = (a, b) => a === b, with: get = x => x} = {}) { +// Usage: coll |> distinct({by?, with?}) +function distinct({by = (a, b) => a === b, with: get = x => x} = {}) { let hasPrev = false, prev - return Object.chain(coll, x => { + return coll => Object.chain(coll, x => { const memo = hasPrev hasPrev = true return !memo || by(prev, prev = get(x)) ? [x] : [] } } -// Usage: filter(coll, func) -function filter(coll, func) { - return Object.chain(coll, x => func(x) ? [x] : []) +// Usage: coll |> map(func) +function map(func) { + return coll => Object.then(coll, func) } -// Usage: scan(coll, func) -function scan(coll, func) { +// Usage: coll |> filter(func) +function filter(func) { + return coll => Object.chain(coll, x => func(x) ? [x] : []) +} + +// Usage: coll |> scan(func) +function scan(func) { let hasPrev = false, prev - return Object.chain(coll, x => { + return coll => Object.chain(coll, x => { const memo = hasPrev hasPrev = true return memo ? [prev, func(prev, prev = x)] : [prev = x] }) } -// Usage: each(coll, func) +// Usage: coll |> each(func) // Return truthy to break -function each(coll, func) { - return Object.chain(coll, item => func(item) ? undefined : []) +function each(func) { + return coll => Object.chain(coll, item => func(item) ? undefined : []) } -// Usage: eachAsync(coll, func) +// Usage: coll |> eachAsync(func) // Return truthy to break -function eachAsync(coll, func) { - return Object.chainAsync(coll, async item => await func(item) ? undefined : []) +function eachAsync(func) { + return coll => Object.chainAsync(coll, async item => await func(item) ? undefined : []) } -// Usage: tap(coll, func) -function tap(coll, func) { - return Object.then(coll, item => { func(item); return item }) +// Usage: coll |> tap(func) +function tap(func) { + return coll => Object.then(coll, item => { func(item); return item }) } -// Usage: tapAsync(coll, func) -function tapAsync(coll, func) { - return Object.thenAsync(coll, async item => { await func(item); return item }) +// Usage: coll |> tapAsync(func) +function tapAsync(func) { + return coll => Object.thenAsync(coll, async item => { await func(item); return item }) } -// Usage: x >:> uniq({by?, with?}) -function uniq(coll, {by, with: get = x => x} = {}) { +// Usage: coll |> uniq({by?, with?}) +function uniq({by, with: get = x => x} = {}) { const set = by == null ? new Set() : (items => ({ has: item => items.some(memo => by(memo, item)), add: item => items.push(item), })([]) - return Object.chain(coll, item => { + return coll => Object.chain(coll, item => { const memo = get(item) if (set.has(memo)) return [] set.add(memo) From 6a28fd75378b896de27d6522851553955442add0 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Fri, 20 Apr 2018 10:19:09 -0400 Subject: [PATCH 13/15] Update pipeline-chain.md --- pipeline-chain.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pipeline-chain.md b/pipeline-chain.md index 30fc402..416ee3d 100644 --- a/pipeline-chain.md +++ b/pipeline-chain.md @@ -45,8 +45,8 @@ function distinct({by = (a, b) => a === b, with: get = x => x} = {}) { } } -// Usage: coll |> map(func) -function map(func) { +// Usage: coll |> then(func) +function then(func) { return coll => Object.then(coll, func) } From ad7db11ea8695da572ed06b237c0b132b0ded476 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Fri, 20 Apr 2018 10:35:20 -0400 Subject: [PATCH 14/15] Update pipeline-chain.md --- pipeline-chain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipeline-chain.md b/pipeline-chain.md index 416ee3d..4687ec2 100644 --- a/pipeline-chain.md +++ b/pipeline-chain.md @@ -102,7 +102,7 @@ function uniq({by, with: get = x => x} = {}) { } ``` -## Helpers +## Implementation The helpers themselves are not too complicated, but they do have things they have to account for, leading to what looks like redundant code, and some borderline non-trivial work: From 68ee14093ef046739fa26b9d862f062e8e8e5703 Mon Sep 17 00:00:00 2001 From: Isiah Meadows Date: Fri, 20 Apr 2018 11:02:12 -0400 Subject: [PATCH 15/15] Create pipeline-box.md --- pipeline-box.md | 186 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 pipeline-box.md diff --git a/pipeline-box.md b/pipeline-box.md new file mode 100644 index 0000000..a91c761 --- /dev/null +++ b/pipeline-box.md @@ -0,0 +1,186 @@ +# `Object.box(value)` + +There already exist [optional chaining](https://github.com/tc39/proposal-optional-chaining) and [nullish coalescing](https://github.com/tc39/proposal-nullish-coalescing), which cover a lot of ground in of themselves. They're very useful for the common cases of nested property accesses (that might not be there) and "default" expressions, but this breaks down when you need to do more complex stuff: + +```js +// What you'd do now +function getUserBanner(banners, user) { + if (user && user.accountDetails && user.accountDetails.address) { + return banners[user.accountDetails.address.province] + } else { + return undefined + } +} + +// With optional chaining +function getUserBanner(banners, user) { + const province = user?.accountDetails?.address?.province + return province != null ? banners[province] : undefined + :> p => banners[p] +} +``` + +With this builtin, you can now do this: + +```js +function getUserBanner(banners, user) { + return Object.then( + Object.box(user?.accountDetails?.address?.province) + p => banners[p], + ) +} +``` + +Unlike those, you can do even longer pipelines with this, and this is where it becomes a bit more magical: + +```js +// Original +let postCode +if (person != null) { + if (person.hasMedicalRecord() && person.address != null) { + checkAddress(person.address) + if (person.address.postCode != null) { + postCode = `${person.address.postCode}` + } else { + postCode = "UNKNOWN" + } + } +} + +// With this + a destructuring default +let postCode = Object.then( + Object.box(person), + person => person.hasMedicalRecord() ? person : undefined, + person => person.address, + address => { checkAddress(address); return address }, + address => address.postCode, + postCode => `${postCode}` +).value ?? "UNKNOWN" +``` + +It very cleanly unnested the entire pipeline. Now, let's add some more sugar: let's use the [pipeline operator](https://github.com/tc39/proposal-pipeline-operator/) and [some useful pipeline operators](https://github.com/isiahmeadows/lifted-pipeline-strawman/blob/isiahmeadows-syntax-free/pipeline-chain.md#use-cases). + +```js +let postCode = Object.box(person) + |> filter(person => person.hasMedicalRecord()) + |> then(person => person.address) + |> tap(address => checkAddress(address)) + |> then(address => address.postCode) + |> then(postCode => `${postCode}`) + |> postCode => postCode.value ?? "UNKNOWN" + +// Helpers used from there: +function then(func) { + return coll => Object.then(coll, func) +} + +function filter(func) { + return coll => Object.chain(coll, x => func(x) ? [x] : []) +} + +function tap(func) { + return coll => Object.then(coll, item => { func(item); return item }) +} +``` + +If you noticed, there's *nothing* specific to optionals there. I used helpers built for streams, and just used them here for a boxed object pipeline. That's part of the magic of this: you can use the same stuff across pipelines without issue. + +Oh, and there's a few other goodies: + +1. `null`s get censored to `undefined`, just like with null coalescing and optional chaining. It's merely convenient with those, but it helps this more. + +1. You can `Object.combine` them and get a new box. It works mostly like this: + - If all boxes have values, the function gets called with all their contents. + - Otherwise, an empty box is returned. + +1. You can `Object.merge` them. It goes left to right and chooses the first box with a value. Easy! + +1. The `Object.async{Then,Combine,Chain}` variants work. You don't need to worry if you have an async function or promise pipeline - you can still work with it, and this still works with it. + +1. You can iterate them as if they were a single-item array/generator/whatever. In fact, the above pipeline could've been specified as this: + + ```js + let [postCode = "UNKNOWN"] = Object.box(person) + |> filter(person => person.hasMedicalRecord()) + |> then(person => person.address) + |> tap(address => checkAddress(address)) + |> then(address => address.postCode) + |> then(postCode => `${postCode}`) + ``` + + This also means you can break early by just looping over it. If you need to return early from an async function, but you still want to handle the value safely and easily, here's how you do it: + + ```js + for (const value of box) { + const result = await fetchSomethingWithValue(value) + if (result.success) return "OMG IT WORKED!!!!1!1!!1!1one!!oneoneone!" + } + console.log("-_-") + ``` + +## Implementation + +Engines should most certainly implement this as a pseudo-primitive like arrays. Every method should be trivially inlinable, and for the most part, engines *should* be able to elide the allocations in the above pipeline. Once engines can optimize simple curried functions like `filter` above, they could continue further and reduce it down to *fully* optimal code after the JIT kicks in. (Zero-cost abstractions for the win!) + +The basic polyfill works roughly like this: + +```js +Object.box = function box(value) { + return new Box(value) +} + +class Box { + constructor(value) { + if (value == null) value = undefined + this._value = value + } + + get value() { + return this._value + } + + *[Symbol.iterator]() { + if (this._value !== void 0) yield this._value + } + + [Symbol.then](func) { + return this._value != null ? new Box(func(this._value)) : this + } + + [Symbol.combine](other, func) { + if (this._value != null && other._value != null) { + return new Box(func(this._value, other._value)) + } else { + return this + } + } + + [Symbol.chain](func) { + if (this._value == null) return this + const result = func(this._value) + if (result instanceof Box) return result + const [first] = result + return new Box(first) + } + + async [Symbol.asyncThen](func) { + return this._value != null ? new Box(await func(this._value)) : this + } + + async [Symbol.asyncCombine](func) { + if (this._value != null && other._value != null) { + return new Box(await func(this._value, other._value)) + } else { + return this + } + } + + async [Symbol.asyncChain](func) { + if (this._value == null) return this + const result = await func(this._value) + if (result instanceof Box) return result + const [first] = result + return new Box(first) + } +} +```