1 | # Tiny-Cps
|
2 | Tiny but powerful goodies for Continuation-Passing-Style functions
|
3 |
|
4 | > Simplicity is prerequisite for reliability.
|
5 | > Elegance is not a dispensable luxury but a factor that decides between success and failure.
|
6 | >
|
7 | > --- [Edsger W. Dijkstra](https://www.azquotes.com/author/3969-Edsger_Dijkstra)
|
8 |
|
9 | >
|
10 | ```js
|
11 | // ignorant
|
12 | const getServerStuff = callback => ajaxCall(json => callback(json))
|
13 | // enlightened
|
14 | const getServerStuff = ajaxCall
|
15 | ```
|
16 | *--- From [Mostly adequate guide to Functional Programming](https://github.com/MostlyAdequate/mostly-adequate-guide).*
|
17 |
|
18 |
|
19 | - [CPS functions](#cps-functions)
|
20 | * [Why?](#why)
|
21 | * [Advanced composability](#advanced-composability)
|
22 | * [What is new here?](#what-is-new-here)
|
23 | + [Variadic input and output](#variadic-input-and-output)
|
24 | + [Full power of multiple outputs streams](#full-power-of-multiple-outputs-streams)
|
25 | + [Functional progamming paradigm](#functional-progamming-paradigm)
|
26 | + [Lazy or eager?](#lazy-or-eager)
|
27 | + [Differences with Haskell](#differences-with-haskell)
|
28 | + ["Do less" is a feature](#do-less-is-a-feature)
|
29 | * [Terminology](#terminology)
|
30 | * [Using CPS functions](#using-cps-functions)
|
31 | * [What about Callback Hell?](#what-about-callback-hell)
|
32 | * [Asynchronous iteration over array](#asynchronous-iteration-over-array)
|
33 | - [Examples of CPS functions](#examples-of-cps-functions)
|
34 | * [Promise producers](#promise-producers)
|
35 | * [Promises](#promises)
|
36 | * [Node API](#node-api)
|
37 | * [HTTP requests](#http-requests)
|
38 | * [Database Access](#database-access)
|
39 | * [Middleware e.g. in Express or Redux](#middleware-eg-in-express-or-redux)
|
40 | * [Web Sockets](#web-sockets)
|
41 | * [Stream libraries](#stream-libraries)
|
42 | + [Pull Streams](#pull-streams)
|
43 | + [Flyd](#flyd)
|
44 | * [Event aggregation](#event-aggregation)
|
45 | - [Comparison with Promises and Callbacks](#comparison-with-promises-and-callbacks)
|
46 | * [Returning results](#returning-results)
|
47 | * [Chaining](#chaining)
|
48 | * [Asynchronous composition](#asynchronous-composition)
|
49 | * [Error handling](#error-handling)
|
50 | * [Signatures](#signatures)
|
51 | * [Standardization](#standardization)
|
52 | - [Functional and Fluent API](#functional-and-fluent-api)
|
53 | * [Conventions](#conventions)
|
54 | * [CPS.map](#cpsmap)
|
55 | + [Mapping over single function](#mapping-over-single-function)
|
56 | + [Mapping over multiple functions](#mapping-over-multiple-functions)
|
57 | + [Map taking multiple arguments](#map-taking-multiple-arguments)
|
58 | + [Functor laws](#functor-laws)
|
59 | + [CPS.of](#cpsof)
|
60 | * [CPS.chain](#cpschain)
|
61 | + [Transforming multiple arguments into multiple arguments](#transforming-multiple-arguments-into-multiple-arguments)
|
62 | + [Why is it called `chain`?](#why-is-it-called-chain)
|
63 | + [Composing multiple outputs](#composing-multiple-outputs)
|
64 | + [Passing multiple CPS functions to `chain`](#passing-multiple-cps-functions-to-chain)
|
65 | + [Monadic laws](#monadic-laws)
|
66 | - [Associativity law](#associativity-law)
|
67 | - [Identity laws](#identity-laws)
|
68 | * [Application of `chain`: Turn Node API into Promise style callbacks](#application-of-chain-turn-node-api-into-promise-style-callbacks)
|
69 | * [CPS.ap](#cpsap)
|
70 | + [Running CPS functions in parallel](#running-cps-functions-in-parallel)
|
71 | + [Lifting functions of multiple parameters](#lifting-functions-of-multiple-parameters)
|
72 | - [Promise.all](#promiseall)
|
73 | - [Usage notes](#usage-notes)
|
74 | + [Applying multiple functions inside `ap`](#applying-multiple-functions-inside-ap)
|
75 | + [Applicative laws](#applicative-laws)
|
76 | * [CPS.merge](#cpsmerge)
|
77 | + [Relation with Promise.race](#relation-with-promiserace)
|
78 | + [Commutative Monoid](#commutative-monoid)
|
79 | * [CPS.filter](#cpsfilter)
|
80 | + [Filtering over multiple functions](#filtering-over-multiple-functions)
|
81 | + [Implementation via `chain`](#implementation-via-chain)
|
82 | * [CPS.scan](#cpsscan)
|
83 |
|
84 |
|
85 |
|
86 | # CPS functions
|
87 |
|
88 | > [The Mother of all Monads](https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/the-mother-of-all-monads)
|
89 |
|
90 |
|
91 |
|
92 | ## Why?
|
93 | Functions are the most basic and powerful concept.
|
94 | A whole program can be written as funciton,
|
95 | taking input data and producing output.
|
96 | However, viewing function's return value as the only output is often too limited.
|
97 | For instance, all asynchronous Node API methods rely on the output data
|
98 | returned via callbacks rather than via functions' return values.
|
99 | This patter is of course the well-known
|
100 | [Continuation-Passing Style (CPS)](https://en.wikipedia.org/wiki/Continuation-passing_style)
|
101 |
|
102 |
|
103 | ## Advanced composability
|
104 | The famous article by John Backus ["Can Programming Be Liberated from the von Neumann Style? A Functional Style and Its Algebra of Programs"](https://www.cs.ucf.edu/~dcm/Teaching/COT4810-Fall%202012/Literature/Backus.pdf)
|
105 | advocated to "reduce the code obesity" by building generic hierarchical ways of composing entire programs.
|
106 | The present proposal attempts to provide some way of how such composability can be achieved.
|
107 |
|
108 |
|
109 | ## What is new here?
|
110 | Traditionally Continuation-Passing Style is implemented
|
111 | via callbacks as part of the function's parameters:
|
112 | ```js
|
113 | const api = (input, callback) => doSomeWork(input, callback)
|
114 | ```
|
115 | A fundamental problem here is that the input and output data are getting mixed
|
116 | among function's parameters, making it hard to separate one from another.
|
117 |
|
118 | Our main proposal is to solve this problem via currying:
|
119 | ```js
|
120 | const api = input => callback => doSomeWork(input, callback)
|
121 | ```
|
122 | Now the output is cleanly separated from the input via the function's curried signature.
|
123 | Further parameters can easily be added to the input:
|
124 | ```js
|
125 | const api = (input1, input2, ...) => callback => doSomeWork(input1, ..., callback)
|
126 | ```
|
127 | as well as to the callbacks accepting output:
|
128 | ```js
|
129 | const api = (input1, input2, ...) => (callback1, callbacks2, ...) =>
|
130 | doSomeWork(input1, ... , callback1, ...)
|
131 | ```
|
132 |
|
133 | ### Variadic input and output
|
134 | JavaScript's functions are variadic by design,
|
135 | that is, are capable of accepting arbitrary number of arguments at the runtime.
|
136 | That feature makes it very convenient and powerful to implement optional parameters or set defaults:
|
137 | ```js
|
138 | const f = (required, optionalWithDefault = default, iAmOptional) => { ... }
|
139 | ```
|
140 | Now, given the clean separation provided by currying as mentioned above,
|
141 | we get for free the full functional variadic power provded by JS:
|
142 | ```js
|
143 | const api = (...inputs) => (...callbacks) => doSomeWork(inputs, callbacks)
|
144 | ```
|
145 | Here `...inputs` is the array holding all arguments passed to the function
|
146 | at the run time, by means of the [Rest parameters syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters).
|
147 | In particular, zero arguments are also allowed on each side.
|
148 |
|
149 |
|
150 | ### Full power of multiple outputs streams
|
151 | By its design, JavaScript's function can call
|
152 | any of its callbacks arbitrarily many times at arbitrary moments.
|
153 | This provides a simple implementation of multiple data streams
|
154 | emitted from a single function.
|
155 | Each stream value is passed as arguments of the callback,
|
156 | that is, a whole list of values can be emitted at the same time
|
157 | as arguments of the same function call.
|
158 |
|
159 |
|
160 | ### Functional progamming paradigm
|
161 | The proposed curried design rests on the well-known paradigms.
|
162 | It generalizes the [Kleisli arrows](https://en.wikipedia.org/wiki/Kleisli_category)
|
163 | `a -> m b` associated to the Monad `m`.
|
164 | In our case, the Continuation Monad `m b` corresponds to passing single `callback` function
|
165 | ```js
|
166 | const monad = callback => computation(callback)
|
167 | ```
|
168 | and can be regarded as a "suspended computation".
|
169 | The Monad structure is provided via the `of` and `chain` methods
|
170 | (aka `return` and `bind` in Haskell, or `unit` and `flatMap` in Scala), see below.
|
171 | As part of the variadic functionality, we generalize these Monadic methods
|
172 | by allowing for arbitrary number of function arguments that are matched against the callbacks, see below.
|
173 | This allows for easy handling of multiple output streams with single methods.
|
174 |
|
175 | In addition to generalized Monadic methods dealing with sequential computations,
|
176 | generalized Applicative `ap` and derived `lift` are dealing with parallel ones.
|
177 | As well as Monoidal method `merge` dealing with merging multiple streams.
|
178 | See the paper by [Conal Elliot, "Push-Pull Functional Reactive Programming"](http://conal.net/papers/push-pull-frp/push-pull-frp.pdf).
|
179 |
|
180 |
|
181 | ### Lazy or eager?
|
182 | The lazy vs eager functionality is already built in the function design:
|
183 | ```js
|
184 | const cpsFun = input => callback => doSomeWork(callback)
|
185 | // lazy - waiting to be called
|
186 | cpsFun(input)
|
187 | // eager - running with the callback passed
|
188 | cpsFun(input)(callback)
|
189 | ```
|
190 | Both are of course just functions and function calls, and can be used depending on the need.
|
191 |
|
192 |
|
193 | ### Differences with Haskell
|
194 | Functional Programming in JavaScript has been largely influenced by Haskell.
|
195 | However, there are fundamental design differences with Haskell:
|
196 |
|
197 | #### JavaScript functions are by design not required to be [pure](https://en.wikipedia.org/wiki/Pure_function)
|
198 |
|
199 | While one can always restrict to pure functions only, the available design allows to treat all functions uniformly, including non-pure ones. That provides considerable additional power at no extra cost. As a basic example, consider non-pure function mutating a variable
|
200 | ```js
|
201 | var a = 0
|
202 | const f = x => {
|
203 | x = x + a
|
204 | }
|
205 | ```
|
206 | that can be (pre-)composed with any other function `g`:
|
207 | ```js
|
208 | const g => y => y * 2
|
209 | const composed = y => f(g(x))
|
210 | // or equivalently in functional way
|
211 | const compose = (f,g) => x => f(g(x))
|
212 | const composed = compose(f,g)
|
213 | ```
|
214 | The `compose` operator is defined in uniform fashion and thus allows to compose arbitrary non-pure funcitons without any extra cost.
|
215 |
|
216 |
|
217 |
|
218 | #### JavaScript functions are by design accepting arbitrary number of arguments
|
219 |
|
220 | Again, one can always restrict to single argument, but that way considerable additional power provided by the language design is lost. For instance, object methods (that in JavaScript are treated as regular functions) are often defined with no parameters. As basic example consider adding results of two separate computations:
|
221 | ```js
|
222 | const f1 = x => someComputation1(x)
|
223 | const f2 = y => someComputation2(y)
|
224 | const add = (a, b) => a + b
|
225 | // binary addition is (pre-)composed with both f1, f2
|
226 | const result = (x, y) => add(f1(x), f2(y))
|
227 | ```
|
228 | Defining such abstract composition operator is straightforward:
|
229 | ```js
|
230 | const binaryCompose => (h, f1, f2) => (x, y) => h(f1(x), f2(y))
|
231 | const result = binaryCompose(add, f1, f2)
|
232 | ```
|
233 | However, all 3 parameters `h, f1, f2` are mixed inside the signature, despite of their different roles. It is difficult to remember which function goes where and easy to introduce errors. A more readable and expressive way would be to use the curried signature:
|
234 | ```js
|
235 | const binaryCompose1 => h => (f1, f2) => (x, y) => h(f1(x), f2(y))
|
236 | const result = binaryCompose1(add)(f1, f2)
|
237 | ```
|
238 | Now the inside functions `f1, f2` are visibly separated from the outside `h`.
|
239 | The logic is much cleaner, probability of errors is lower and function is easier to test and debug. Such convenient separation between groups of functional parameters is easier in JavaScript than e.g. in Haskell with no distinction between curried and uncurried parameters.
|
240 |
|
241 |
|
242 | ### "Do less" is a feature
|
243 | The proposed CPS desing API is minimal and focused on doing just one thing --
|
244 | *a style to write and combine plain JavaScript funcitons with callbacks*.
|
245 |
|
246 |
|
247 | ## Terminology
|
248 | A *Continuation-Passing-Style (CPS) function* is any function
|
249 | ```js
|
250 | const cps = (f1, f2, ...) => {
|
251 | /* f1, f2, ... are called arbitrarily often with any number of arguments */
|
252 | }
|
253 | ```
|
254 | that expects to be called with zero or several functions as arguments.
|
255 | By *expects* we mean that this library and the following discussion
|
256 | only applies when functions are passed.
|
257 | In a strictly typed language that would mean those arguments are required to be functions.
|
258 | However, in JavaScript, where it is possible to pass any argument,
|
259 | we don't aim to force errors when some arguments passed are not functions
|
260 | and let the standard JavaScript engine deal with it the usual way,
|
261 | as per [garbage in, garbage out (GIGO)](https://en.wikipedia.org/wiki/Garbage_in,_garbage_out) principle.
|
262 |
|
263 | We also call the argument functions `f1, f2, ...` "callbacks"
|
264 | due to the way how they are used.
|
265 | Each of the callbacks can be called arbitrarily many times or never,
|
266 | with zero to many arguments each time.
|
267 | The number of arguments inside each callback
|
268 | can change from call to call and is even allowed to unlimitedly grow,
|
269 | e.g. `n`th call may involve passing `n` arguments.
|
270 |
|
271 | By a *parametrized CPS function* we mean any curried function with zero or more parameters
|
272 | that returns a CPS function:
|
273 | ```js
|
274 | const paramCps = (param1, param2, ...) => (f1, f2, ...) => { ... }
|
275 | ```
|
276 |
|
277 | We shall adopt somewhat loose terminology calling *parametrized CPS functions* both
|
278 | the curried function `paramCps` and its return value `paramCps(params)`,
|
279 | in the hope that the context will make clear the precisce meaning.
|
280 | In the same vein, by a *function call* of the parametrized CPS function,
|
281 | we mean its call with both arguments and callbacks passed:
|
282 | ```js
|
283 | paramCps(...args)(...callbacks)
|
284 | ```
|
285 | Otherwise `parmCps(...args)` is considered a *partial call*.
|
286 |
|
287 |
|
288 | ## Using CPS functions
|
289 | Using CPS functions is as simple as using JavaScript Promises:
|
290 | ```js
|
291 | // Set up database query as parametrized CPS function with 2 callbacks,
|
292 | // one for the result and one for the error
|
293 | const cpsQuery = query => (resBack, errBack) =>
|
294 | // assuming Node style callback with error param first
|
295 | queryDb(query, (err, res) => err
|
296 | ? resBack(res)
|
297 | : errBack(err))
|
298 | // Now just call as regular curried function
|
299 | cpsQuery({name: 'Jane'})(
|
300 | result => console.log("Your Query returned: ", result),
|
301 | error => console.error("Sorry, here is what happened: ", error)
|
302 | )
|
303 | ```
|
304 | The latter is very similar to how Promises are used:
|
305 | ```js
|
306 | promiseQuery({name: 'Jane'}).then(
|
307 | result => console.log("Your query returned: ", result),
|
308 | error => console.error("Sorry, an error happened: ", error)
|
309 | )
|
310 | ```
|
311 | Except that, calling `then` method is replaced by plain function call
|
312 | and arbitrary number of callbacks is allowed,
|
313 | each of which can be called arbitrary many times,
|
314 | as e.g. in the event streams.
|
315 | A Promise is essentially a CPS function with its first event cached,
|
316 | that can be implemented by chaining (via [`chain`](#cpschain), see below) any CPS function
|
317 | with the one picking and caching the first output from any callback.
|
318 |
|
319 |
|
320 | ## What about Callback Hell?
|
321 | There is an actual website called [*Callback Hell*](http://callbackhell.com/).
|
322 | The following callback hell example is shown:
|
323 | ```js
|
324 | fs.readdir(source, function (err, files) {
|
325 | if (err) {
|
326 | console.log('Error finding files: ' + err)
|
327 | } else {
|
328 | files.forEach(function (filename, fileIndex) {
|
329 | console.log(filename)
|
330 | gm(source + filename).size(function (err, values) {
|
331 | if (err) {
|
332 | console.log('Error identifying file size: ' + err)
|
333 | } else {
|
334 | console.log(filename + ' : ' + values)
|
335 | aspect = (values.width / values.height)
|
336 | widths.forEach(function (width, widthIndex) {
|
337 | height = Math.round(width / aspect)
|
338 | console.log('resizing ' + filename + 'to ' + height + 'x' + height)
|
339 | this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
|
340 | if (err) console.log('Error writing file: ' + err)
|
341 | })
|
342 | }.bind(this))
|
343 | }
|
344 | })
|
345 | })
|
346 | }
|
347 | })
|
348 | ```
|
349 |
|
350 | The solution proposed there to avoid this "hell" consists of splitting into mulitple functions and giving names to each.
|
351 | However, naming is hard and
|
352 | [is not always recommended](https://www.cs.ucf.edu/~dcm/Teaching/COT4810-Fall%202012/Literature/Backus.pdf).
|
353 |
|
354 |
|
355 | Using CPS functions along with `map` and `chain` operators,
|
356 | we can break that code into a sequence of small functions, chained one after another
|
357 | without the need to name them:
|
358 | ```js
|
359 | // wrap into `CPS` object to have `map` and `chain` methods available,
|
360 | // directory files are passed as 2nd argument to cb, error as 1st
|
361 | CPS(cb => fs.readdir(source, cb))
|
362 | // chain method passes instead the same 1st and 2nd arguments into the new CPS function
|
363 | .chain((err, files) => cb =>
|
364 | // only files are passed to the callback, whereas error produces no continuation
|
365 | err ? console.log('Error finding files: ' + err) : cb(files)
|
366 | )
|
367 | // chain modifies the CPS by passing `files`` from inside `cb` into the next CPS function instead
|
368 | .chain(files => cb => files.forEach((filename, fileIndex) => {
|
369 | console.log(filename)
|
370 | // make use of the multiple outputs passed to `cb` for each file
|
371 | // simply add `filename` to the optput inside `cb` to be consumed later
|
372 | gm(source + filename).size((err, values) => cb(err, values, filename))
|
373 | }))
|
374 | // now again chain accepts CPS function with the same 3 arguments as previously passed to `cb`
|
375 | .chain((err, values, filename) => cb =>
|
376 | err ? console.log('Error identifying file size: ' + err) : cb(values, filename)
|
377 | )
|
378 | // now we have `values` and `filename` as we need
|
379 | .chain((values, filename) => cb => {
|
380 | console.log(filename + ' : ' + values)
|
381 | aspect = (values.width / values.height)
|
382 | // as before, simply pass to callback `cb`
|
383 | // and handle all outputs in the next `chain` function
|
384 | widths.forEach(cb)
|
385 | })
|
386 | // now that we have called `cb` multiple times, each time chain passes new values to its CPS function
|
387 | .chain((width, widthIndex) => cb => {
|
388 | height = Math.round(width / aspect)
|
389 | console.log('resizing ' + filename + 'to ' + height + 'x' + height)
|
390 | this.resize(width, height).write(dest + 'w' + width + '_' + filename, cb)
|
391 | }.bind(this))
|
392 | // finally errors are handled via map method
|
393 | .map(err => err ? console.log('Error writing file: ' + err) : '')
|
394 |
|
395 | ```
|
396 |
|
397 | Equivalently, we can use the `pipeline` operator (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Pipeline_operator) to achieve the same result
|
398 | in more functional (aka point-free) style:
|
399 |
|
400 | ```js
|
401 | pipeline( cb => fs.readdir(source, cb) ) (
|
402 | chain( (err, files) => cb => ... ),
|
403 | chain( files => cb => files.forEach((filename, fileIndex) => ... ) ),
|
404 | chain( (err, values, filename) => cb => ... ),
|
405 | chain( (values, filename) => cb => ... ),
|
406 | chain( (width, widthIndex) => cb => ... ),
|
407 | map( err => err ? console.log('Error writing file: ' + err) : '' ),
|
408 | )
|
409 | ```
|
410 |
|
411 | In the latter pattern there is no wrapper around the first CPS function,
|
412 | it is simply passed around through all the transformations in the sequence.
|
413 |
|
414 | Any such sequence of computations can be similaly achieved with just two operators - `map` and `chain`.
|
415 | In fact, just the single more powerful `chain` is enough, as e.g. the following are equivalent:
|
416 |
|
417 | ```js
|
418 | CPS(cpsFun).map((x, y) => f(x, y))
|
419 | CPS(cpsFun).chain((x, y) => cb => cb(f(x, y)))
|
420 | ```
|
421 |
|
422 | or, equivalently, using the `pipeline` operator
|
423 |
|
424 | ```js
|
425 | pipeline(cpsFun)( map((x, y) => f(x, y)) )
|
426 | pipeline(cpsFun)( chain((x, y) => cb => cb(f(x, y)) )
|
427 | ```
|
428 |
|
429 | A limitation of the `chain` is its sequential nature.
|
430 | To run computations in parallel, the `ap` (aka `apply`) operator
|
431 | is more suitable, see below.
|
432 |
|
433 |
|
434 | ## Asynchronous iteration over array
|
435 | On of the functions in the above example illustrates
|
436 | how multiple outputs fit nicely in the asynchronous iteration pattern:
|
437 |
|
438 | ```js
|
439 | const jobCps = files => cb => files.forEach((filename, fileIndex) => {
|
440 | console.log(filename)
|
441 | gm(source + filename).size((err, values) => cb(err, values, filename))
|
442 | })
|
443 | ```
|
444 |
|
445 | Here we create the `jobCps` function that accepts callback
|
446 | and calls it repeatedly for each `file`.
|
447 | That wouldn't work with Promises that can only hold single value each,
|
448 | so you would need to create as many Promises as the number of elements in the `file` array.
|
449 | Instead, we have a single CPS function as above to hold all the asynchronous outputs for all files!
|
450 |
|
451 |
|
452 |
|
453 | # Examples of CPS functions
|
454 |
|
455 | ## Promise producers
|
456 | Any producer (aka executor) function
|
457 |
|
458 | ```js
|
459 | const producer = function(resolve, reject) {
|
460 | // some work ...
|
461 | if (everythingIsOk) resolve(result)
|
462 | else reject(error)
|
463 | }
|
464 | ```
|
465 |
|
466 | as one typically passed to the [Promise constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) is an example of a CPS function.
|
467 |
|
468 | The constructed promise `new Promise(producer)` only keeps the very first call of either of the callbacks,
|
469 | whereas the producer function itself may call its callbacks multiple times,
|
470 | each of which would be fully retained as output when CPS functions are used instead of promises.
|
471 |
|
472 | ## Promises
|
473 | Any JavaScript Promise generates a CPS function via its `.then` method
|
474 | that completely captures the information held by the Promise:
|
475 |
|
476 | ```js
|
477 | const cpsFromPromise = (onFulfilled, onRejected) =>
|
478 | promise.then(onFulfilled, onRejected)
|
479 | ```
|
480 |
|
481 | The important restictions Promises arising that way are:
|
482 | 1. Among many callbacks passed, at most one is ever called.
|
483 | 2. Each of the callbacks is called precisely with one argument.
|
484 | CPS functions do not have such limitations.
|
485 |
|
486 | As any Promise provides a CPS function via its `then` method with two callbacks,
|
487 | it can be dropped direclty into any CPS operator:
|
488 |
|
489 | ```js
|
490 | CPS(cpsFun)
|
491 | .chain((x, y) => somePromise(x, y).then)(
|
492 | res => console.log("Result is: ", res),
|
493 | err => console.err("Something bad happened: ", err)
|
494 | )
|
495 | ```
|
496 |
|
497 | Here `(x, y)` is the first output from `cpsFun` (the one passed into the first callback).
|
498 | Now every such output will be passed into `somePromise` via [`chain`](#cpschain),
|
499 | that will subsequently pass its result or error into the final callbacks
|
500 | that are attached via plain function call.
|
501 | And even better, the error callbacks will also receive
|
502 | all error outputs from `cpsFun`, basically whatever is passed into its second callback.
|
503 | The outputs from both functions are simply merged together,
|
504 | due to the "flattening" job performed by the `chain`.
|
505 |
|
506 | Conversely, any CPS function, being just a function accepting callbacks as its arguments,
|
507 | can be dropped into the Promise constructor
|
508 | (from any Promise implementation) to return the Promise
|
509 | holding the first argument from the first output as its resolved value,
|
510 | while that from the second callback as error.
|
511 |
|
512 |
|
513 | ## Node API
|
514 | Any Node-Style function with one of more callbacks can be curried into a parametrized CPS function:
|
515 |
|
516 | ```js
|
517 | const readFileCPS = (path, options) => callback => fs.readFile(path, options, callback)
|
518 | ```
|
519 |
|
520 | Here `readFileCPS` returns a CPS function for each values of its parameters
|
521 | `(path, options)`.
|
522 |
|
523 | Typically Node API callbacks are called with at least two arguments as
|
524 | `callback(error, arg1, ...)`,
|
525 | where the first argument is used as indication of error.
|
526 | CPS functions generalize this case to arbitrary number of callbacks
|
527 | accepting arbitrary number of arguments each.
|
528 |
|
529 |
|
530 | ## HTTP requests
|
531 | In a similar vein, any HTTP request with callback(s) can be regarded as parametrized CPS function:
|
532 | ```js
|
533 | const request = require('request')
|
534 | // the CPS function is just the curried version
|
535 | const requestCps = options => callback => http.request(options, callback)
|
536 | ```
|
537 |
|
538 | Now `requestCps` is can be composed with any function computing its `options` object, and the output arguments passed to `callback` can be mapped over any function or chained with any other CPS function:
|
539 | ```js
|
540 | const customRequest = pipe (
|
541 | prepareReqObject,
|
542 | requestCps,
|
543 | // args from callback passed to function inside chain
|
544 | chain((err, res, body) => (resCallback, errCallback) => doWork(...))
|
545 | )
|
546 | ```
|
547 |
|
548 | Or using the [native Node `https.request`](https://nodejs.org/api/https.html#https_https_request_options_callback):
|
549 | ```js
|
550 | const https = require('https')
|
551 | const httpsReqCps = (url, options) => cb => http.request(url, options, cb)
|
552 | ```
|
553 | and turning data events to plain CPS function outputs:
|
554 | ```js
|
555 | const dataStreamCps = pipe (
|
556 | httpsReqCps,
|
557 | // attach `cb` as even listener
|
558 | chain(response => cb => response.on('data', cb)),
|
559 | // and handle the data in the next CPS function
|
560 | chain(dataChunk => cb => cb(someTransformation(dataChunk)))
|
561 | )
|
562 | ```
|
563 |
|
564 | ## Database Access
|
565 | Any async database access API with callbacks can be curried into parametrized CPS functions:
|
566 |
|
567 | ```js
|
568 | const queryDb = (db, query) => callback => getQuery(db, query, callback)
|
569 | const insertDb = (db, data) => callback => inserData(db, data, callback)
|
570 | ```
|
571 |
|
572 | In most cases each of these is considered a single request resulting in either success of failure.
|
573 | However, more general CPS functions can implement more powerful functionality with multiple callback calls.
|
574 | For instance, a function can run multiple data insetion attempts with progress reported back to client.
|
575 | Or the query function can return its result in multiple chunks, each with a separate callback call. Or even subscribe to changes and update client in real time.
|
576 | Further, the database query funtion can hold a state that is advanced with each call.
|
577 | Similarly, any database access can be cancelled by subsequent call of the same CPS function with suitable parameters.
|
578 |
|
579 | ## Middleware e.g. in Express or Redux
|
580 | The [Express Framework](https://expressjs.com/) in NodeJs popularised
|
581 | the concept of [middleware](https://expressjs.com/en/guide/writing-middleware.html)
|
582 | that later found its place in other frameworks such as
|
583 | [Redux](https://redux.js.org/advanced/middleware#understanding-middleware).
|
584 | In each case, a *middleware* is a special kind of function,
|
585 | plain in case of Express and curried in case of Redux,
|
586 | which has one continuation callback among its parameters.
|
587 | To each middleware in each of these frameworks,
|
588 | there is the associated parametrized CPS function,
|
589 | obtained by switching parameters and (un)currying.
|
590 | As the correspondence `middleware <-> CPS function` goes in both ways,
|
591 | it allows for each side to benefit from the other.
|
592 |
|
593 |
|
594 | ## Web Sockets
|
595 | Here is a generic CPS function parametrized by its url `path`:
|
596 | ```js
|
597 | const WebSocket = require('ws')
|
598 | const createWS = path => callback =>
|
599 | new WebSocket(path).on('message', callback)
|
600 | ```
|
601 |
|
602 | The callback will be called repeatedly with every new socket message emited.
|
603 |
|
604 | Other websocket events can be subscribed by other callbacks,
|
605 | so that a single CPS function with its multiple callbacks
|
606 | can encapsulate the entire socket functionality.
|
607 |
|
608 |
|
609 | ## Stream libraries
|
610 |
|
611 | ### Pull Streams
|
612 | The [Pull Streams](https://pull-stream.github.io/)
|
613 | present an ingenious way of implementing
|
614 | a rich on-demand stream functionality,
|
615 | including back pressure,
|
616 | entirely with plain JavaScript functions.
|
617 | In a way, they gave some of the original inspirations
|
618 | for the general CPS function pattern.
|
619 |
|
620 | Indeed, a Pull Stream is essentially a function `f(abort, callback)` that is called repeatedly
|
621 | by the sink to produce on-demand stream of data.
|
622 | Any such function can be clearly curried into a
|
623 | is a parametrized CPS function
|
624 | ```js
|
625 | const pullStream = params => callback => {...}
|
626 | ```
|
627 |
|
628 | ### [Flyd](https://github.com/paldepind/flyd)
|
629 | Any `flyd` stream can be wrapped into a CPS function with single callback called with single argument:
|
630 | ```js
|
631 | const cpsFun = callback => flydStream
|
632 | .map(x => callback(x))
|
633 | ```
|
634 | The resulting CPS function `cpsFun`, when called with any `callback`,
|
635 | simply subsribes that callback to the stream events.
|
636 |
|
637 | Conversely, any CPS function `cpsFun` can be simply called with
|
638 | any `flyd` stream in place of one of its callback arguments:
|
639 | ```js
|
640 | let x = flyd.stream()
|
641 | cpsFun(x)
|
642 | ```
|
643 | That will push the first argument of any callback call of `x` into the stream.
|
644 |
|
645 |
|
646 | ## Event aggregation
|
647 | Similarly to [`flyd` streams](https://github.com/paldepind/flyd/#creating-streams),
|
648 | CPS functions can subscribe their callbacks to any event listener:
|
649 | ```js
|
650 | const cpsFun = callback =>
|
651 | document.getElementById('button')
|
652 | .addEventListener('click', callback)
|
653 | ```
|
654 | Furthermore, more complex CPS functions can similarly subscribe to
|
655 | muiltiple events:
|
656 | ```js
|
657 | const cpsFun = (cb1, cb2) => {
|
658 | document.getElementById('button1')
|
659 | .addEventListener('click', cb1)
|
660 | document.getElementById('button2')
|
661 | .addEventListener('click', cb2)
|
662 | }
|
663 | ```
|
664 | and thereby serve as functional event aggregators
|
665 | encapsulating multiple events.
|
666 | Every time any of the event is emitted,
|
667 | the corresponding callback will fire
|
668 | with entire event data passed as arguments.
|
669 | That way complete information from multiple events
|
670 | remains accessible via single CPS function.
|
671 |
|
672 |
|
673 |
|
674 | # Comparison with Promises and Callbacks
|
675 |
|
676 | Our main motivation for dealing with CPS functions is to enhance
|
677 | the power of common coding patterns into a single unified abstraction,
|
678 | which can capture the advantages typically regarded as ones of Promises over callbacks.
|
679 |
|
680 | In the [introductory section on Promises](http://exploringjs.com/es6/ch_promises.html#sec_introduction-promises) of his wonderful book [Exploring ES6](http://exploringjs.com/es6/),
|
681 | [Dr. Axel Rauschmayer](http://dr-axel.de/) collected a list of
|
682 | advantages of Promises over callbacks,
|
683 | that we would like to consider here in the light of the CPS functions
|
684 | and explain how, in our view, the latters can enjoy the same advantages.
|
685 |
|
686 | ## Returning results
|
687 | > No inversion of control: similarly to synchronous code, Promise-based functions return results, they don’t (directly) continue – and control – execution via callbacks. That is, the caller stays in control.
|
688 |
|
689 | We regard the CPS functions returning their output in similar fashion as promises,
|
690 | via the arguments inside each callback call.
|
691 | Recall that a result inside promise can only be extracted via a callback,
|
692 | which is essentially the same as passing the callback to a CPS function:
|
693 | ```js
|
694 | // pass callbacks to promise
|
695 | const promise.then(cb1, cb2)
|
696 | // => result is delivered via cb1(result)
|
697 | ```
|
698 | ```js
|
699 | // pass callbacks to CPS function
|
700 | const cps(f1, f2)
|
701 | // => a tuple (vector) of results is deliverd via f1(res1, res2, ...)
|
702 | ```
|
703 | Thus, CPS functions can be regarded as generalization of promises,
|
704 | where callbacks are allowed to be called multiple times with several arguments each time,
|
705 | rather than with a single value.
|
706 | Note that syntax for CPS function is even shorter - there is no `.then` method needed.
|
707 |
|
708 |
|
709 | ## Chaining
|
710 | > Chaining is simpler: If the callback of `then()` returns a Promise (e.g. the result of calling another Promise-based function) then `then()` returns that Promise (how this really works is more complicated and explained later). As a consequence, you can chain then() method calls:
|
711 |
|
712 | ```js
|
713 | asyncFunction1(a, b)
|
714 | .then(result1 => {
|
715 | console.log(result1);
|
716 | return asyncFunction2(x, y);
|
717 | })
|
718 | .then(result2 => {
|
719 | console.log(result2);
|
720 | });
|
721 | ```
|
722 |
|
723 | In our view, the complexity of chaing for the callbacks is merely due to lacking convenience methods for doing it.
|
724 | On a basic level, a Promise wraps a CPS function into an object providing such methods.
|
725 | However, the Promise constructor also adds limitations on the functionality and generally does a lot more, sometimes at the cost of performance.
|
726 | On the other hand, to have similar chaining methods, much less powerful methods are needed,
|
727 | that can be uniformly provided for general CPS functions.
|
728 | The above example can then be generalized to arbitrary CPS functions:
|
729 | ```js
|
730 | // wrapper providing methods
|
731 | CPS(cpsFunction1(a, b))
|
732 | // 'chain' (aka 'flatMap') is used to compose parametrized CPS functions
|
733 | .chain(result1 => {
|
734 | console.log(result1);
|
735 | return cpsFunction2(x, y);
|
736 | })
|
737 | // 'map' is used to compose CPS outputs with ordinary functions
|
738 | .map(result2 => {
|
739 | console.log(result2);
|
740 | });
|
741 | ```
|
742 | Here `CPS(...)` is a lightweight object wrapper providing the `map` and `chain` methods among others,
|
743 | such that `CPS.of` and `map` conform to the [Pointed Functor](https://stackoverflow.com/questions/39179830/how-to-use-pointed-functor-properly/41816326#41816326) and `CPS.of` with `CPS.chain` to the [Monadic](https://github.com/rpominov/static-land/blob/master/docs/spec.md#monad) [interface](https://github.com/fantasyland/fantasy-land#monad).
|
744 | At the same time, the full functional structure is preserved allowing for drop in replacement
|
745 | `cpsFun` with `CPS(cpsFun)`, see below.
|
746 |
|
747 |
|
748 | ## Asynchronous composition
|
749 | > Composing asynchronous calls (loops, mapping, etc.): is a little easier, because you have data (Promise objects) you can work with.
|
750 |
|
751 | Similar to promises wrapping their data,
|
752 | we regard the CPS functions as wrapping the outputs of their callbacks.
|
753 | Whenever methods are needed, a CPS function can be explicitly wrapped into
|
754 | its CPS object via the `CPS`,
|
755 | similar to how the `Promise` constructor wraps its producer function,
|
756 | except that `CPS` does nothing else.
|
757 | There is no recursive unwrapping of "thenables" nor other promises as with
|
758 | the Promise constructor.
|
759 |
|
760 | In addition, the CPS object `CPS(cpsFunction)` retains the same information
|
761 | by delivering the same functionality via direct funtion calls with the same callbacks!
|
762 | That is, the following calls are identical:
|
763 | ```js
|
764 | cpsFunction(cb1, cb2, ...)
|
765 | CPS(cpsFunction)(cb1, cb2, ...)
|
766 | ```
|
767 | That means, the wrapped CPS function can be dropped directly into the same code
|
768 | preserving all the functionality with no change!
|
769 |
|
770 | In regard of composing asynchronous calls, with CPS functions it can be as simple as in
|
771 | the above example.
|
772 |
|
773 |
|
774 | ## Error handling
|
775 | > Error handling: As we shall see later, error handling is simpler with Promises, because, once again, there isn’t an inversion of control. Furthermore, both exceptions and asynchronous errors are managed the same way.
|
776 |
|
777 | In regards of error handling,
|
778 | the following paragraph in here http://exploringjs.com/es6/ch_promises.html#_chaining-and-errors
|
779 | seems relevant:
|
780 |
|
781 | > There can be one or more then() method calls that don’t have error handlers. Then the error is passed on until there is an error handler.
|
782 | ```js
|
783 | asyncFunc1()
|
784 | .then(asyncFunc2)
|
785 | .then(asyncFunc3)
|
786 | .catch(function (reason) {
|
787 | // Something went wrong above
|
788 | });
|
789 | ```
|
790 |
|
791 | And here is the same example with CPS functions:
|
792 | ```js
|
793 | CPS(cpsFunc1)
|
794 | .chain(cpsFunc2)
|
795 | .chain(cpsFunc3)
|
796 | .map(null, reason => {
|
797 | // Something went wrong above
|
798 | });
|
799 | ```
|
800 | Here the `map` method is used with two arguments
|
801 | and the second callback considered as holding errors,
|
802 | in the same way as the Promises achieve that effect.
|
803 |
|
804 | There is, however, no a priori restriction for the error callback
|
805 | to be the second argument, it can also be the first callback
|
806 | as in [Fluture](https://github.com/fluture-js/Fluture) or Folktale's [`Data.Task`](https://github.com/folktale/data.task), or the last one, or anywhere inbetween.
|
807 |
|
808 | Similar to Promises, also for CPS functions, handling
|
809 | both exceptions and asynchronous errors can be managed the same uniform way.
|
810 | Or the multiple callbacks feature of CPS functions can be utilized
|
811 | to handle errors of different nature in different callbacks,
|
812 | such as for instance [Fun-Task does](https://github.com/rpominov/fun-task/blob/master/docs/exceptions.md).
|
813 |
|
814 | On the other hand, in contrast to Promises,
|
815 | the CPS functions allow for clean separation between exceptions such as bugs
|
816 | that need to be caught as early as possible, and asynchronous errors
|
817 | that are expected and returned via the error callbacks calls.
|
818 | The absence of similar feature for Promises attracted [considerable criticisms](https://medium.com/@avaq/broken-promises-2ae92780f33).
|
819 |
|
820 |
|
821 | ## Functional signatures
|
822 | > Cleaner signatures: With callbacks, the parameters of a function are mixed; some are input for the function, others are responsible for delivering its output. With Promises, function signatures become cleaner; all parameters are input.
|
823 |
|
824 | The "curried nature" of the (parametrized) CPS functions
|
825 | ensures clean separation between their input parameters
|
826 | and the callbacks that are used to hold the output only:
|
827 | ```js
|
828 | const paramCps = (param1, param2, ...) => (cb1, cb2, ...) => { ... }
|
829 | ```
|
830 | Here the output holding callbacks `cb1, cb2, ...` are
|
831 | cleanly "curried away" from the input parameters `param1, param2, ...`.
|
832 |
|
833 | Note that, without currying, it would not be possible to achieve similar separation.
|
834 | If function is called directly without currying, it is impossible to tell
|
835 | which arguments are meant for input and which for output.
|
836 |
|
837 | The principle here is very analogous to how that separation is achieved by Promises,
|
838 | except that the CPS function do not impose any restricitons on the
|
839 | number of their callback calls, nor the number of arguments passed to each callback
|
840 | with each call.
|
841 |
|
842 |
|
843 | ## Standardization
|
844 | > Standardized: Prior to Promises, there were several incompatible ways of handling asynchronous results (Node.js callbacks, XMLHttpRequest, IndexedDB, etc.). With Promises, there is a clearly defined standard: ECMAScript 6. ES6 follows the standard Promises/A+ [1]. Since ES6, an increasing number of APIs is based on Promises.
|
845 |
|
846 | The CPS functions build directly on the standard already established for JavaScript functions.
|
847 | The provided methods such as `of` (aka `pure`, `return`), `map` (aka `fmap`), `chain` (aka `flatMap`, `bind`) strictly follow the general standards for algebraic data types established by Functional Programming languages and Category Theory.
|
848 |
|
849 |
|
850 |
|
851 |
|
852 | # Functional and Fluent API
|
853 |
|
854 | The `CPS` function transforms any CPS function into that very same CPS function,
|
855 | to which in addition all API methods can be applied.
|
856 | The same methods are provided on the `CPS` namespace and
|
857 | can be applied directly to CPS functions with the same effect.
|
858 | For instance, the following expressions are equivalent ([in the sense of fantasyland](https://github.com/fantasyland/fantasy-land#terminology)):
|
859 | ```js
|
860 | CPS(cpsFun).map(f)
|
861 | map(f)(cpsFun)
|
862 | map(f, cpsFun)
|
863 | ```
|
864 | Note that the functional style let us simply drop in CPS functions as plain functions,
|
865 | whereas to use `map` as method we need to wrap them into `CPS()` first.
|
866 |
|
867 | And the equivalent multiple argument versions are:
|
868 | ```js
|
869 | CPS(cpsFun).map(f1, f2, ...)
|
870 | map(f1, f2, ...)(cpsFun)
|
871 | map(f1, f2, ..., cpsFun)
|
872 | ```
|
873 | In the last expression, only the last argument is a CPS function,
|
874 | whereas all other arguments are arbitrary functions
|
875 | acting by means of their return values.
|
876 |
|
877 |
|
878 | ## Conventions
|
879 | In the following we slightly abuse the notation by placing methods directly
|
880 | on the CPS functions, where the meaning is always after wrapping the functions into `CPS`.
|
881 | That is, we write
|
882 | ```js
|
883 | cpsFun.map(f)
|
884 | // instead of
|
885 | CPS(cpsFun).map(f)
|
886 | ```
|
887 | We could have used instead the equivalent functional style `map(f)(cpsFun)`,
|
888 | but the fluent style seems more common in JavaScript and closer to how Promises are used,
|
889 | so we use it instead.
|
890 |
|
891 |
|
892 | ## CPS.map
|
893 | The `map` method and the equivalent `CPS.map` function in their simplest form
|
894 | are similar to `Array.map` as well as other `map` functions/methods used in JavaScript.
|
895 |
|
896 | ### Mapping over single function
|
897 | In the simplest case of a single function `x => f(x)` with one argument,
|
898 | the corresponding transformation of the CPS function only affects the first callback,
|
899 | very similar to how the function inside `.then` method of a Promise only affects the fulfilled value:
|
900 | ```js
|
901 | const newPromise = oldPromise.then(f)
|
902 | ```
|
903 |
|
904 | Except that the `map` behavior is simpler with no complex promise recognition nor any thenable unwrapping:
|
905 | ```js
|
906 | const newCps = CPS(oldCps).map(f)
|
907 | // or equivalently in point-free functional style
|
908 | const newCps = map(f)(oldCps)
|
909 | // or equivalently using pipeline
|
910 | const newCps = pipeline(oldCps)(map(f))
|
911 | ```
|
912 | The `newCps` function will call its first callback
|
913 | with the single transformed value `f(res)`,
|
914 | whereas the functionality of the other callbacks remains unchanged.
|
915 |
|
916 | Also the return value of CPS function always remains unchanged after transforming with any `map` invocation, e.g. `newCps` above returns the same value as `oldCps`.
|
917 |
|
918 |
|
919 | The last two expressions have the advantage that no wrapping into `CPS()` is needed. The `pipeline` version in addition corresponds to the natural flow - get `oldCps` first, then pass to the transformer. This advantage appears even more visible with anonymous functions:
|
920 | ```js
|
921 | // outputting 2-tuple of values
|
922 | const newCps = pipeline(x => cb => cb(x+1, x+2))(
|
923 | // the 2-tuple is passed as args to function inside `map`
|
924 | map((val1, val2) => val1 * val2)
|
925 | )
|
926 | // or equivalently using the `.map` method via CPS wrapper
|
927 | const newCps = CPS(x => cb => cb(x+1, x+2))
|
928 | .map((val1, val2) => val1 * val2)
|
929 | // to compare with point-free style
|
930 | const newCps = map((val1, val2) => val1 * val2)(
|
931 | x => cb => cb(x+1, x+2)
|
932 | )
|
933 | ```
|
934 |
|
935 |
|
936 | ### Mapping over multiple functions
|
937 |
|
938 | As `map(f)` is itself a function, its JavaScript signature provides us with the power to pass to it arbitrary number of arguments: `map(f1, f2, ...)`. This added power appear very handy for CPS functions with multiple outputs via multiple callbacks, where we can apply the `n`th function `fn` to transform the output of the `n`th callback:
|
939 | ```js
|
940 | const newCps = CPS(oldCps).map(res => f(res), err => g(err))
|
941 | // or simply
|
942 | const newCps = CPS(oldCps).map(f, g)
|
943 | // or equivalently in point-free style
|
944 | const newCps = map(f, g)(oldCps)
|
945 | // or with pipeline
|
946 | const newCps = pipeline(oldCps)(map(f,g))
|
947 | ```
|
948 |
|
949 | Here we are calling the second result `err` in analogy with promises,
|
950 | however, in general, it can be any callback without extra meaning.
|
951 | The resulting CPS function will call its first and second callbacks
|
952 | with correspondingly transformed arguments `f(res)` and `g(res)`,
|
953 | whereas all other callbacks will be passed from `newCps` to `oldCps` unchanged.
|
954 |
|
955 | The latter property generalizes the praised feature of Promises,
|
956 | where a single error handler can deal with all accumulated errors.
|
957 | In our case, the same behavior occurs for the `n`th callback
|
958 | that will be picked by only those `map` invocations holding functions at their `n`th spot.
|
959 | For instance, a possible third callback `progress`
|
960 | will similaly be handled only invocations of `map(f1, f2, f3)`
|
961 | with some `f3` provided.
|
962 |
|
963 |
|
964 | ### Map taking arbitrarily many functions with arbitrary numbers of arguments
|
965 | In most general case, `map` applies its argument functions to several arguments passed at once to corresponding callbacks:
|
966 | ```js
|
967 | const oldCps = x => (cb1, cb2, cb3) => {
|
968 | cb1(vals1); cb2(vals2); cb3(vals3)
|
969 | }
|
970 | // now vals1 are tranformed with f1, vals2 with f2, vals3 with f3
|
971 | const newCps = CPS(oldCps).map(f1, f2, f3)
|
972 | ```
|
973 |
|
974 | That means, the pattern can be generalized to
|
975 | ```js
|
976 | const newCps = CPS(oldCps).map((res1, res2, ...) => f(res1, res2, ...))
|
977 | ```
|
978 | or passing arbitrary number of arguments with [rest parameters syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters):
|
979 | ```js
|
980 | const newCps = CPS(oldCps).map((...args) => f(...args))
|
981 | // which is the same as
|
982 | const newCps = CPS(oldCps).map(f)
|
983 | ```
|
984 | or passing only some of the arguments:
|
985 | ```js
|
986 | const newCps = CPS(oldCps)
|
987 | .map((iAmThrownAway, ...rest) => f(...rest))
|
988 | ```
|
989 | or picking props from multiple objects selectively via destructuring:
|
990 | ```js
|
991 | const newCps = CPS(oldCps)
|
992 | // select only important props and transform as 2-tuple
|
993 | .map(({name: name1}, {name: name2}) => f(name1, name2))
|
994 | ```
|
995 | None of these transformations would be as convenient with Promises where only single values are ever being passed.
|
996 |
|
997 |
|
998 | ### Functor laws
|
999 | The `map` method for single functions of single argument satisfies the functor laws.
|
1000 | That is, the following pairs of expressions are equivalent:
|
1001 | ```js
|
1002 | cpsFun.map(f).map(g)
|
1003 | // and
|
1004 | cpsFun.map(x => g(f(x)))
|
1005 | ```
|
1006 | as well as
|
1007 | ```js
|
1008 | cpsFun
|
1009 | // and
|
1010 | cpsFun.map(x => x)
|
1011 | ```
|
1012 |
|
1013 | In fact, we have more general equivalences with multiple arguments:
|
1014 | ```js
|
1015 | cpsFun.map(f1, f2, ...).map(g1, g2, ...)
|
1016 | // and
|
1017 | cpsFun.map(x1 => g1(f1(x1)), x2 => g2(f2(x2)), ...)
|
1018 | ```
|
1019 | where in addition, the number of `f`'s can differ from the number of `g`'s,
|
1020 | in which case the missing maps are replaced by the identities.
|
1021 |
|
1022 |
|
1023 | ### CPS.of
|
1024 | The static method `CPS.of` that we simply call `of` here
|
1025 | is the simplest way to wrap values into a CPS function:
|
1026 | ```js
|
1027 | const of = (x1, x2, ...) => callback => callback(x1, x2, ...)
|
1028 | ```
|
1029 | or equivalently
|
1030 | ```js
|
1031 | const of = (...args) => callback => callback(...args)
|
1032 | ```
|
1033 |
|
1034 | Here the full tuple `(x1, x2, ...)` becomes a single output of
|
1035 | the created CPS function `of(x1, x2, ...)`.
|
1036 |
|
1037 | As mentioned before,
|
1038 | `of` and `map` for single functions with single argument
|
1039 | conform to the [Pointed Functor](https://stackoverflow.com/questions/39179830/how-to-use-pointed-functor-properly/41816326#41816326),
|
1040 | that is the following expressions are equivalent:
|
1041 | ```js
|
1042 | of(x).map(f)
|
1043 | of(f(x))
|
1044 | // both expressions are equivalent to
|
1045 | cb => cb(f(x))
|
1046 | ```
|
1047 | In our case, the first expression maps `f` over the CPS function `cb => cb(x)` by transforming its single output `x`,
|
1048 | whereas the second one outputs `f(x)` direclty into its callback, which is obviously the same.
|
1049 |
|
1050 | More generally, the following are still equivalent
|
1051 | with the same reasoning:
|
1052 | ```js
|
1053 | of(x1, x2, ...).map(f)
|
1054 | // and
|
1055 | of(f(x1, x2, ...))
|
1056 | // are equivalent to
|
1057 | cb => cb(f(x1, x2, ...))
|
1058 | ```
|
1059 |
|
1060 |
|
1061 | ## CPS.chain
|
1062 |
|
1063 | ### Transforming multiple arguments into multiple arguments
|
1064 | There is a certain lack of symmetry with the `map` method,
|
1065 | due to the way functions are called with several arguments but
|
1066 | only ever return a single value.
|
1067 |
|
1068 | But what if we want not only to consume, but also to pass multiple arguments to the callback of the new CPS function?
|
1069 |
|
1070 | No problem. Except that, we should wrap these into another CPS function and use `chain` instead:
|
1071 | ```js
|
1072 | const newCps = CPS(oldCps)
|
1073 | .chain((x1, x2, ...) => of(x1 + 1, x2 * 2))
|
1074 | // or explicitly
|
1075 | const newCps = CPS(oldCps)
|
1076 | .chain((x1, x2, ...) => cb => cb(x1 + 1, x2 * 2))
|
1077 | ```
|
1078 |
|
1079 | Here we pass both `x1 + 1` and `x2 * 2` simultaneously into the callback `cb`.
|
1080 | Generalizing Promises that only hold one value, we can regard `(x1, x2, ...)` as tuple of values held inside single CPS function,
|
1081 | in fact, all being passed to only its first callback.
|
1082 | Now the output values of `oldCps` are passed to the functions inside
|
1083 | `chain`, get transformed it according to the second CPS function,
|
1084 | i.e. into the pair `(x1 + 1, x2 * 2)`,
|
1085 | and finally passed to the first callback of `newCps`.
|
1086 | The final result is exactly the intended one, that is,
|
1087 | the result tuple output `(x1, x2, ...)` from `oldCps`'s first callback is transformed into the new pair `(x1 + 1, x2 * 2)`
|
1088 | that becomes the output of `newCps`.
|
1089 |
|
1090 |
|
1091 | ### Why is it called `chain`?
|
1092 | Have you noticed the difference between how `map` and `chain` are used?
|
1093 | Here is the simplest case comparison:
|
1094 | ```js
|
1095 | cpsFun.map(x => x+1)
|
1096 | cpsFun.chain(x => of(x+1))
|
1097 | ```
|
1098 | In the first expression the value `x+1` is passed directly,
|
1099 | in the second it is wrapped into CPS function with `of`.
|
1100 | The first time the return value of the function inside `map` is used,
|
1101 | the second time it is the output value of the CPS function inside `chain`.
|
1102 |
|
1103 | The "Promised" way is very similar:
|
1104 | ```js
|
1105 | promise.then(x => x+1)
|
1106 | promise.then(x => Promise.resolve(x+1))
|
1107 | ```
|
1108 | Except that both times `then` is used,
|
1109 | so we don't have to choose between `map` and `chain`.
|
1110 | However, such simplicity comes with its cost.
|
1111 | Since `then` has to do its work to detect
|
1112 | and recursively unwrap any Promise or in fact any ["thenable"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve),
|
1113 | there can be loss in performance as well as
|
1114 | in safety to refactor
|
1115 | ```js
|
1116 | promise.then(f).then(g)
|
1117 | ```
|
1118 | to
|
1119 | ```js
|
1120 | promise.then(x => g(f(x)))
|
1121 | ```
|
1122 | which is [not always the same](https://stackoverflow.com/a/50173415/1614973).
|
1123 |
|
1124 | On the other hand, our `map` method
|
1125 | conforms to the Functor composition law,
|
1126 | that is the following expressions are always equivalent
|
1127 | and safe to refactor to each other (as mentioned above):
|
1128 | ```js
|
1129 | cpsFun.map(f).map(g)
|
1130 | // and
|
1131 | cpsFun.map(x => g(f(x)))
|
1132 | ```
|
1133 | And since now no other work is involved,
|
1134 | performance wins.
|
1135 |
|
1136 | However, if we try use `map` in the second case,
|
1137 | instead of `chain`, we get
|
1138 | ```js
|
1139 | cpsFun.map(x => of(x+1))
|
1140 | ```
|
1141 | which emits `of(x+1)` as output, rather than `x+1`.
|
1142 | That is, the output result of our CPS function is another CPS function,
|
1143 | so we get our value wrapped twice.
|
1144 | This is where `chain` becomes useful,
|
1145 | in that in removes one of the layers,
|
1146 | aka "flattens" the result,
|
1147 | which is why it is also called `flatMap`.
|
1148 |
|
1149 | For CPS functions, the name `chain` is particularly descriptive
|
1150 | because it effectively chains two such functions
|
1151 | by passing the output of one funciton as input to the other.
|
1152 | And the rule becomes very simple:
|
1153 |
|
1154 | *Use `map` with "plain" functions and `flatMap` with CPS functions inside*
|
1155 |
|
1156 |
|
1157 | ### Composing multiple outputs
|
1158 | In the previous case we had `chain` over a CPS function with single output,
|
1159 | even when the output itself is a tuple.
|
1160 | In comparison, a general Promise has two outputs - the result and the error.
|
1161 | Of course, in case of Promises, there are more restrctions such as only one of these two
|
1162 | outputs can be emitted.
|
1163 |
|
1164 | No such restrictions are iposed on CPS functions,
|
1165 | where two or more callbacks can receive arbitrary number of outputs arbitrarily often.
|
1166 | To keep things simple, consider how the Promise functionality can be extended
|
1167 | without the output exclusivity restriction:
|
1168 | ```js
|
1169 | const cpsFun = (cb1, cb2) => {
|
1170 | /* some work here... */
|
1171 | cb1(res1)
|
1172 | /* some more work... */
|
1173 | cb2(res2)
|
1174 | }
|
1175 | ```
|
1176 | So both callbacks are called with their individual results at different moments.
|
1177 |
|
1178 | A very useful and realistic example of this functionality would be,
|
1179 | when the server sent an error but then eventually managed to deliver the data.
|
1180 | That would be impossible to implement with Promises.
|
1181 |
|
1182 | Back to our example,
|
1183 | we now want to use the output for the next CPS computation:
|
1184 | ```js
|
1185 | const newCps = cpsFun.chain(res => anotherCps(res))
|
1186 | ```
|
1187 | We are now chaining aka sequentially executing `cpsFun`,
|
1188 | followed by the next parametrized CPS function
|
1189 | `anotherCps` applied to the result `res` of the previous computation.
|
1190 |
|
1191 | So how should we combine both computations?
|
1192 | And should we apply the second one to `res1` or `res2`?
|
1193 |
|
1194 | If you read the above description of the `map`, you know the answer.
|
1195 | The principle is the same. As we only pass one function to `chain`,
|
1196 | only the first callback is affected.
|
1197 | That is, we must pass only the first result `res1` to `anotherCps`.
|
1198 | Whose output will be our final result inside the first callback,
|
1199 | whereas all other callbacks remain unchanged.
|
1200 |
|
1201 | So the functionality of `newCps` is equivalent to the following:
|
1202 | ```js
|
1203 | const newCps = (...args) => cpsFun(
|
1204 | res1 => anotherCps(res1)(...args),
|
1205 | res2 => cb2(args[1])
|
1206 | )
|
1207 | ```
|
1208 | Note how all callbacks are passed to the inner CPS function in the same order as `args`.
|
1209 | That guarantees that no outgoing information from `anotherCps` can ever get lost.
|
1210 |
|
1211 |
|
1212 | ### Passing multiple CPS functions to `chain`
|
1213 |
|
1214 | Similarly to `map`, also `chain` accepts arbitrary number of functins,
|
1215 | this time CPS functions:
|
1216 | ```js
|
1217 | const newCps = oldCps.chain(
|
1218 | res1 => anotherCps1(res1),
|
1219 | res2 => anotherCps2(res2),
|
1220 | ...
|
1221 | )
|
1222 | ```
|
1223 | Look how similar it is with Promises usage:
|
1224 | ```js
|
1225 | // Promises
|
1226 | promise.then(
|
1227 | res => anotherPromise(res),
|
1228 | error => errorHandlerPromise(err)
|
1229 | )
|
1230 | // CPS functions
|
1231 | cpsFun.chain(
|
1232 | res => anotherCps(res),
|
1233 | err => errorHandlerCps(err)
|
1234 | )
|
1235 | ```
|
1236 | You can barely tell the difference, can you?
|
1237 |
|
1238 | Or, since the CPS functions are just functions,
|
1239 | we can even drop any Promise `then` function directly into `chain`:
|
1240 | ```js
|
1241 | // CPS functions
|
1242 | cpsFun.chain(
|
1243 | res => anotherPromise.then(res),
|
1244 | error => errorHandlerPromise.then(err)
|
1245 | )
|
1246 | // or just the shorter
|
1247 | cpsFun.chain( anotherPromise.then, errorHandlerPromise.then )
|
1248 | ```
|
1249 |
|
1250 | On the other hand, the CPS functions are more powerful
|
1251 | in that they can call their callbacks multiple times in the future,
|
1252 | with the potential of passing further important information back to the caller.
|
1253 | Also we don't want to prescribe in which callback the error should go,
|
1254 | treating all the callbacks in a uniform way.
|
1255 | Here is some pseudocode demonstrating general usage of `chain`
|
1256 | with mulitple functions:
|
1257 | ```js
|
1258 | const newCps = oldCps.chain(
|
1259 | (x1, x2, ...) => cpsFun1(x1, x2, ...),
|
1260 | (y1, y2, ...) => cpsFun2(y1, y2, ...),
|
1261 | ...
|
1262 | )
|
1263 | ```
|
1264 | And the functionality of `newCps` is equivalent to
|
1265 | ```js
|
1266 | const newCps = (cb1, cb2, ...) => oldCps(
|
1267 | (x1, x2, ...) => cpsFun1(x1, x2, ...)(cb1, cb2, ...)
|
1268 | (y1, y2, ...) => cpsFun1(y1, y2, ...)(cb1, cb2, ...)
|
1269 | ...
|
1270 | )
|
1271 | ```
|
1272 | As any other CPS function, our `newCps` accepts arbitrary number of callbacks
|
1273 | that are simply all passed to each interior CPS function in the same order.
|
1274 | That leads to the effect of capturing output events from each of them,
|
1275 | and "flattening" the event streams via simple merging.
|
1276 |
|
1277 |
|
1278 | ### Monadic laws
|
1279 | When used with single callback argument,
|
1280 | `of` and `chain` satisfy the regular monadic laws.
|
1281 |
|
1282 | #### Associativity law
|
1283 | The associativity law analogue for Promises would be the equivalence of
|
1284 | ```js
|
1285 | promise
|
1286 | .then(x1 => promise1(x1))
|
1287 | .then(x2 => promise2(x2))
|
1288 | // and
|
1289 | promise
|
1290 | .then(x1 => promise1.then(x2 => promise2(x2)))
|
1291 | ```
|
1292 | which [are, however, not always equivalent](https://stackoverflow.com/a/50173415/1614973).
|
1293 |
|
1294 |
|
1295 | For CPS functions in contrast, we do indeed obtain true equivalence of
|
1296 | ```js
|
1297 | cpsFun
|
1298 | .chain(x1 => cpsFun1(x1))
|
1299 | .chain(x2 => cpsFun2(x2))
|
1300 | // and
|
1301 | cpsFun
|
1302 | .chain(x1 => cpsFun1.chain(x2 => cpsFun2(x2)))
|
1303 | ```
|
1304 | Because, since these are just functions,
|
1305 | both expressions can be direclty expanded into
|
1306 | ```js
|
1307 | cb => cpsFun(
|
1308 | x1 => cpsFun1(x1)(
|
1309 | x2 => cpsFun2(x2)(cb)
|
1310 | )
|
1311 | )
|
1312 | ```
|
1313 | That is, the output `x1` of `cpsFun` is passed to `cpsFun1`,
|
1314 | which transforms it into `x2` as output,
|
1315 | subsequently passed to `cpsFun2`,
|
1316 | whose output is finally diverted direclty into `cb`.
|
1317 |
|
1318 | More generally, similar law still holds for mulitple arguments,
|
1319 | that is the following are equivalent
|
1320 | ```js
|
1321 | cpsFun
|
1322 | .chain(f1, f2, ...)
|
1323 | .chain(g1, g2, ...)
|
1324 | // and
|
1325 | cpsFun
|
1326 | .chain(
|
1327 | (...xs) => f1(...xs).chain((...ys) => g1(...ys)),
|
1328 | (...xs) => f2(...xs).chain((...ys) => g2(...ys)),
|
1329 | ...
|
1330 | )
|
1331 | ```
|
1332 | as both expand into
|
1333 | ```js
|
1334 | (...cbs) => cpsFun(
|
1335 | (...xs) => f1(...xs)((...ys) => g1(...ys)(...cbs)),
|
1336 | (...xs) => f2(...xs)((...ys) => g2(...ys)(...cbs)),
|
1337 | ...
|
1338 | )
|
1339 | ```
|
1340 |
|
1341 | #### Identity laws
|
1342 | The monadic identity laws asserts that both following
|
1343 | expressions are equivalent to the CPS function `cpsFun`:
|
1344 | ```js
|
1345 | cpsFun
|
1346 | // and
|
1347 | chain(y => of(y))(cpsFun)
|
1348 | ```
|
1349 | Here `cpsFun` is any CPS function,
|
1350 | whose output is composed with the CPS identity `y => of(y)`.
|
1351 |
|
1352 |
|
1353 | On the other hand, taking a parametrized CPS function
|
1354 | `x => cpsFun(x)` and moving the identity to other side,
|
1355 | we get the other law asserting the equivalence of:
|
1356 | ```js
|
1357 | x => cpsF(x)
|
1358 | // is equivalent to
|
1359 | x => chain(
|
1360 | y => cpsF(y)
|
1361 | )(
|
1362 | cb => cb(x)
|
1363 | )
|
1364 | ```
|
1365 |
|
1366 | Once expanded, both equivalences are
|
1367 | became straightforward to check.
|
1368 | More interestingly, they still hold for multiple arguments:
|
1369 | ```js
|
1370 | cpsFun
|
1371 | // is equivalent to
|
1372 | cpsFun.chain(
|
1373 | (...ys) => of(...ys),
|
1374 | (...ys) => of(...ys),
|
1375 | ... /* any number of identities */
|
1376 | )
|
1377 | ```
|
1378 | and the other way around:
|
1379 | ```js
|
1380 | (...xs) => cpsF(...xs)
|
1381 | // is equivalent to
|
1382 | (...xs) => chain(
|
1383 | (...ys) => cpsF(...ys))((...cbs) => cbs.map(cb => cb(...xs))
|
1384 | )
|
1385 | ```
|
1386 |
|
1387 |
|
1388 | ## Application of `chain`: Turn Node API into Promise style callbacks
|
1389 | The Node style callbacks with error argument first
|
1390 | force their errors to be handled each single time:
|
1391 | ```js
|
1392 | someNodeFunction(param, callback((error, result) => {
|
1393 | if (error) mustHandle...
|
1394 | doMoreWork(result, callback((error, result) => {
|
1395 | ...
|
1396 | }))
|
1397 | }))
|
1398 | ```
|
1399 | In constrast, Promises make it possible to handle all errors with one callback at the end:
|
1400 | ```js
|
1401 | promise
|
1402 | .then(doSomeWork)
|
1403 | .then(doMoreWork)
|
1404 | ...
|
1405 | .catch(handleAllErrors)
|
1406 | ```
|
1407 |
|
1408 | Many libraries offering methods to "promisify" Node style callbacks.
|
1409 | The trouble is the [Gorilla-Banana Problem](https://www.johndcook.com/blog/2011/07/19/you-wanted-banana/): Promises added a lot of other functionality and limitations that not everyone needs.
|
1410 | For instance, it is perfectly legal to call callbacks muiltiple times
|
1411 | (for which there are many use cases such as streams and event handlers),
|
1412 | whereas the "promisification" would only see the first call.
|
1413 |
|
1414 | On the other hand, we can curry any callback-last Node method into CPS function
|
1415 | ```js
|
1416 | const cpsErrback = (...args) => cb => nodeApi(...args, cb)
|
1417 | ```
|
1418 | and subsequently `chain` it into "Promise" style CPS function
|
1419 | with the same pair of callbacks, except that no other functionality is added nor removed:
|
1420 | ```js
|
1421 | const promiseStyle = CPS(cpsErrback)
|
1422 | .chain((error, ...results) => (resBack, errBack) => error
|
1423 | ? errBack(error)
|
1424 | : resBack(...results)
|
1425 | )
|
1426 | ```
|
1427 |
|
1428 | Now we can chain these CPS funcitons exactly like Promises,
|
1429 | passing only the first callback, and handle all errors at the end in the second callback.
|
1430 | ```js
|
1431 | promiseStyle
|
1432 | .chain(doSomeWork)
|
1433 | .map(doMoreWork)
|
1434 | ...
|
1435 | .chain(null, handleAllErrors)
|
1436 |
|
1437 | ```
|
1438 |
|
1439 |
|
1440 |
|
1441 | ## CPS.ap (TODO)
|
1442 | The `ap` operator plays an important role when
|
1443 | *running functions in parallel* and combining their outputs.
|
1444 |
|
1445 | ### Running CPS functions in parallel
|
1446 | Similarly to `map(f)` applying a plain function `f` to (the output of) a CPS function,
|
1447 | `ap(cpsF)` applies functions that are themselves outputs of some CPS function,
|
1448 | delivered via callbacks.
|
1449 | A simple example is getting result from a database query via `cpsDb` function
|
1450 | and display it with via function transformer obtained from an independent query:
|
1451 | ```js
|
1452 | // returns result via 'cb(result)' call
|
1453 | const cpsDb = query => cb => getQuery(query, cb)
|
1454 | // returns transformer function via 'cb(transformer)'
|
1455 | const cpsTransformer = path => cb => getTransformer(path, cb)
|
1456 | // Now use the 'ap' operator to apply the transformer to the query result:
|
1457 | const getTransformedRes = (query, path) => CPS(cpsDb(query)).ap(cpsTransformer(path))
|
1458 | // or equivalently in the functional style, without the need of the 'CPS' wrapper:
|
1459 | const getTransformedRes = (query, path) => ap(cpsTransformer(path))(cpsDb(query))
|
1460 | ```
|
1461 |
|
1462 | Note that we could have used `map` and `chain` to run the same functions sequentially,
|
1463 | one after another:
|
1464 | ```js
|
1465 | (query, path) => CPS(cpsDb(query))
|
1466 | .chain(result => cpsTransformer(path)
|
1467 | .map(transformer => transformer(result)))
|
1468 | ```
|
1469 | Here we have to nest, in order to keep `result` in the scope of the second function.
|
1470 | However, `result` from the first function was not needed to run the `cpsTransformer`,
|
1471 | so it was a waste of time and resources to wait for the query `result`
|
1472 | before getting the `transformer`.
|
1473 | It would be more efficient to run both functions in parallel and then combine the results,
|
1474 | which is precisely what the `ap` operator does.
|
1475 |
|
1476 |
|
1477 | ### Lifting functions of multiple parameters
|
1478 | Perhaps the most important use of the `ap` operator is lifting plain functions
|
1479 | to act on results of CPS functional computations.
|
1480 | That way simple plain functions can be created and re-used
|
1481 | with arbitrary data, regardless of how the data are retrieved.
|
1482 | In the above example we have used the general purpose plain function
|
1483 | ```js
|
1484 | const f =(result, transformer) => transformer(result)
|
1485 | ```
|
1486 | which is, of course, just the ordinary function call.
|
1487 | That function was "lifted" to act on the data delivered as outputs
|
1488 | from separate CPS functions.
|
1489 |
|
1490 | Since this use case is very common,
|
1491 | we have the convenience operator doing exactly that called `lift`:
|
1492 | ```js
|
1493 | const getTransformedRes = (query, path) =>
|
1494 | lift(transformer => transformer(result))
|
1495 | (cpsDb(query), cpsTransformer(path))
|
1496 | ```
|
1497 |
|
1498 | #### Promise.all
|
1499 | The common way to run Promises in parallel via `Promise.all`
|
1500 | is a special case of the `lift` usage,
|
1501 | corresponding to lifting the function
|
1502 | combining values in array:
|
1503 | ```js
|
1504 | const combine = (...args) => args
|
1505 | Promise.all = promiseArray => lift(combine)(...promiseArray)
|
1506 | ```
|
1507 |
|
1508 | Similarly, the same `combine` function (or any other) can be lifted
|
1509 | over to act on outputs of CPS functions:
|
1510 | ```js
|
1511 | (cpsF1, cpsF2, ...) => lift((x1, x2, ...) => f(x1, x2, ...))(cpsF1, cpsF2, ...)
|
1512 | ```
|
1513 |
|
1514 |
|
1515 | #### Usage notes
|
1516 | Note that `lift` (and `ap`) are best used when
|
1517 | their arguments can only be retrieved as outputs from separate CPS functions.
|
1518 | If for instance, both `result` and `transformer`
|
1519 | can be delivered via single query,
|
1520 | using `lift` would be a waste of its parallel execution functionality.
|
1521 | Instead we could have used the simple `map` with a single CPS function:
|
1522 | ```js
|
1523 | const getTransformedResSingle = (query, path) =>
|
1524 | CPS(getData(query, path)).map((result, transformer) => transformer(result))
|
1525 | ```
|
1526 | Note how the `map` function is applied with two arguments,
|
1527 | which assumes the `getData` function to have these in a single callback output
|
1528 | as `callback(result, transformer)`.
|
1529 |
|
1530 |
|
1531 | ### Applying multiple functions inside `ap`
|
1532 |
|
1533 | As with `map` and `chain`, the same rules apply for `ap`:
|
1534 | ```js
|
1535 | const transformed = CPS(cpsFun).ap(F1, F2, ...)
|
1536 | ```
|
1537 | When called with callbacks `(cb1, cb2, ...)`,
|
1538 | the output from `cb1` is transformed with the output function from `F1`,
|
1539 | the output from `cb2` with function from `F2` and so on.
|
1540 |
|
1541 | For instance, a CPS function with two callbacks such as `(resBack, errBack)`
|
1542 | can be `ap`ed over a pair of CPS functions, outputting plain functions each
|
1543 | ```js
|
1544 | // These call some remote API
|
1545 | const cpsResTransformer = cb => getResTransformer(cb)
|
1546 | const cpsErrHandler = cb => getErrHandler(cb)
|
1547 | // This requires error handlers
|
1548 | const cpsValue = (resBack, errBack) => getValue(resBack, errBack)
|
1549 | // Now run all requests in parallel and consume both outputs as they arrive
|
1550 | // via plain function call
|
1551 | CPS(cpsValue).ap(cpsResTransformer, cpsErrHandler)(
|
1552 | res => console.log("Transformed Result: ", res)
|
1553 | err => console.log("The Error had been handled: ", err)
|
1554 | )
|
1555 | ```
|
1556 |
|
1557 | The above pattern can be very powerful,
|
1558 | for instance the `cpsErrHandler` function
|
1559 | can include a remote retry or cleanup service
|
1560 | that is now completely abstracted away from the main code pipeline!
|
1561 |
|
1562 |
|
1563 |
|
1564 | ### Applicative laws
|
1565 | The `ap` operator together with `of` conforms to the [Applicative interface](https://github.com/rpominov/static-land/blob/master/docs/spec.md#applicative)
|
1566 |
|
1567 |
|
1568 |
|
1569 | ## CPS.merge (TODO)
|
1570 | The `merge` operator merges outputs events from multiple CPS functions,
|
1571 | which occurs separately for each callback slot:
|
1572 | ```js
|
1573 | // merge outputs into single CPS function
|
1574 | const cpsMerged = merge(cpsF1, cpsF2, ...)
|
1575 | // cb1 receives all outputs from the first callback of each of the cpsF1, cpsF2, ...
|
1576 | cpsMerged(cb1, cb2, ...)
|
1577 | ```
|
1578 | Here the `N`-th callback of `cpsMerged` gets called each time
|
1579 | the `N`-th callback of any of the functions `cpsF1`, `cpsF2`, ...,
|
1580 | with the same arguments.
|
1581 | This behaviour corresponds to merging the values emitted by
|
1582 | each event stream.
|
1583 |
|
1584 | ### Relation with Promise.race
|
1585 | The `merge` operator generalizes the functionality provided for Promises via `Promise.race`.
|
1586 | Since Promises only take the first emitted value from each output,
|
1587 | merging those results in the earliest value from all being picked by the Promise,
|
1588 | hence the direct analogy with `Promise.race`.
|
1589 |
|
1590 | ### Commutative Monoid
|
1591 | The `merge` operator makes the set of all CPS functions a commutative Monoid,
|
1592 | where the identity is played by the trivial CPS function that never emits any output.
|
1593 |
|
1594 |
|
1595 |
|
1596 |
|
1597 | ## CPS.filter
|
1598 | The `filter` operator does the obvious thing,
|
1599 | that is trasform one CPS function into another by filtering the output.
|
1600 | As the output may have several arguments,
|
1601 | the filter function is also variadic:
|
1602 | ```js
|
1603 | const cpsFiltered = filter(pred)(cpsFun)
|
1604 | ```
|
1605 | Here `pred` is a Boolean function called for each output tuple.
|
1606 | The resulting `cpsFiltered` emits only the output for which `pred` returns `true`.
|
1607 |
|
1608 |
|
1609 | ### Filtering over multiple functions
|
1610 | Consistently with other operators,
|
1611 | also `filter` accepts multiple predicate functions,
|
1612 | each matched against the ouput from the corresponding callback.
|
1613 |
|
1614 | That is, the filtered function
|
1615 | ```js
|
1616 | const cpsFiltered = filter(p1, p2, ...)(cpsFun)
|
1617 | ```
|
1618 | when passed callbacks `(cb1, cb2, ...)`,
|
1619 | calls `cb1` with the same output `(x1, x2, ...)` as `cpsFun` does,
|
1620 | as long as `p1(x1, x2, ...)` returns `true`, otherwise the call is skipped.
|
1621 | Similarly, `p2` filters the output of `cb2` and so on.
|
1622 | The callbacks not corresponding to any predicate function
|
1623 | will be unaffected and the predicates corresponding to no callbacks
|
1624 | are ignored.
|
1625 |
|
1626 |
|
1627 | ### Implementation via `chain`
|
1628 | Filtering is really chaining:
|
1629 | ```js
|
1630 | // pass through only input truthy `pred`
|
1631 | const cpsFilter = pred => (...input) => cb => {
|
1632 | if (pred(...input)) cb(...input)
|
1633 | }
|
1634 | // now chain with `cpsFilter(pred)`:
|
1635 | const filter = pred => CPS(cpsFun)
|
1636 | .chain(cpsFilter(pred))
|
1637 | ```
|
1638 | And the variadic version reuses the same `cpsFilter` applied to each predicate:
|
1639 | ```js
|
1640 | // call `chain` with the list of arguments, one per each predicate
|
1641 | const filter = (...pred) => CPS(cpsFun)
|
1642 | .chain(...pred.map(cpsFilter))
|
1643 | // or using the `pipeline` operator
|
1644 | const filter = (...pred) => pipeline(cpsFun)(
|
1645 | chain(...pred.map(cpsFilter))
|
1646 | )
|
1647 | ```
|
1648 |
|
1649 |
|
1650 |
|
1651 |
|
1652 | ## CPS.scan
|
1653 | The `scan` operator acts as "partial reduce" for each output.
|
1654 | Important example is the stream of states affected by stream of actions:
|
1655 | ```js
|
1656 | const cpsState = scan(f)(initState)(cpsAction)
|
1657 | ```
|
1658 | Here `f` is the reducing function accepting current `state` and
|
1659 | the next `action` and returning the next state as `f(state, action)`,
|
1660 | that is the signature is
|
1661 | ```js
|
1662 | f :: (state, action) -> state
|
1663 | ```
|
1664 |
|
1665 | Similarly to `filter`, `scan` can also be derived from `chain`:
|
1666 | ```js
|
1667 | const scan = f => state => cpsAction => pipeline(cpsAction)(
|
1668 | // action may contain several arguments
|
1669 | chain((...action) => cb => {
|
1670 | state = f(state, ...action)
|
1671 | cb(state)
|
1672 | })
|
1673 | )
|
1674 | ```
|
1675 | Note that the function inside `chain` updates the `state` outside its scope,
|
1676 | so it is not pure, however, we can still `chain` it like any other function.
|
1677 |
|
1678 | And here is the mulitple arguments generalization:
|
1679 | ```js
|
1680 | // `reducers` and `states` are matched together by index
|
1681 | const scan = (...reducers) => (...states) => {
|
1682 | cpsAction => pipeline(cpsAction)(
|
1683 | // chaining with multiple reducers, one per state
|
1684 | chain(...states.map((, idx) => cb => {
|
1685 | // accessing states and reducers by index
|
1686 | cb( states[idx] = reducers[idx](states[idx], ...action) )
|
1687 | })
|
1688 | ))
|
1689 | }
|
1690 | ```
|