index.js
const { isNil, mergeArray, inheritPrototype } = require('./utils')
/* ----- General purpose utils ----- */
exports.curryGroups2 = f => (...a) => (...b) => f(...a, ...b)
exports.curryGroupsN = n => f => {
while(--n > 0) {
f = exports.curryGroups2(f)
}
return f
}
/**
* Transform `reducer: (state,next) => state` into update function inside closure.
*
* @param {Function} reducer: (state,next) -> state
* @returns {Function} update(reducer): initState -> (x0,...,xn) -> reducer(prevState,x0,...,xn)
*
* @example updater = update((state,x) => state+x)
* acc = updater(5) // initialize updater function with initial state = 5
* acc(2) //=> 5+2
* acc(2) //=> 5+2+2
* acc(4) //=> 5+2+2+4
*/
exports.update = reducer => state => (...vals) => {state = reducer(state, ...vals); return state}
/**
* Same as `update` but spread return values of curried reducer.
* This allows to update tuples rather than single values as `update` does.
* Reducer is curried for clean separation of arguments.
* As JavaScript has no tuples, use arrays instead.
*
* @param {Function} reducer: (s0,...,sn) -> (x0,...,xm) -> (s0,...,sn)
* @returns {Function} updateSpread(reducer): (s0,...,sn) -> (x0,...,xn) -> reducer(s0,...,sn)(x0,...,xm)
*/
exports.updateSpread = reducer => (...state) => (...vals) => {state = reducer(...state)(...vals); return state}
/**
* Pass tuple of values to sequence of functions similar to UNIX pipe
* `(x0,...,xn) | f0 | f1 | ... | fm`.
*
* @param {...*} args - tuple of arbitrary values.
* @param {...Function} fns - functions `(f0,f1,...,fn)`.
* @returns {*} `pipeline(...args)(...fns)`
* - Result of functions applied one after another, equivalent to
* `fn(...f1(f0(...args))...)`
*
* @example
* pipeline(x,y)(f0,f1,f2)
* // is equivalent to
* f2(f1(f0(x,y)))
*/
const pipeline = (...args) => (...fns) => fns.slice(1).reduce(
(acc, fn) => fn(acc),
fns[0](...args)
)
/**
* Compose functions left to right as in ramda's `pipe`.
*
* @param {...Function} fns - functions `(f0,f1,...,fn)`.
* @returns {*} `pipe(...fns)`
* - Composite function `(...args) => fn(...f1(f0(...args))...)`.
*
* @example
* pipe((a,b)=>a+b, x=>x*2)
* // is equivalent to
* (a,b)=>(a+b)*2
*
*/
const pipe = (...fns) => (...args) => pipeline(...args)(...fns)
/* ----- CPS operators ----- */
/**
* Create CPS function with provided tuple as immediate output.
*
* @param {...*} (x0,...,xn) - tuple of arbitrary values.
* @returns {Function} `of(x1,...,xn)` - CPS function
* that immediately calls it 1st callback `cb` with outputs `(x0,...,xn)`.
* No other callback is called. (For multi-callback version see `ofN`.)
*
* @example
* of(x0,x1,x2)
* // is equivalent to the CPS function
* cb => cb(x0,x1,x2)
*
*/
const of = (...args) => cb => cb(...args)
/**
* Multi-callback version of the `of` operator,
* passing provided tuple into the nth callback.
*
* @param {Number} n - position number of the callback used.
* @param {...*} args - tuple of arbitrary values.
* @returns {Function} `ofN(n)(...args)` - CPS function
* that outputs `(...args)`` into its nth callback
* no other output is passed to any other callback.
*
* @example
* ofN(1)(x0,x1)
* // is equivalent to the CPS function
* (cb0,cb1) => cb1(x0,x1)
*/
const ofN = n => (...args) => (...cbs) => cbs[n](...args)
/**
* Chain is the most basic CPS operator.
* It chains outputs of CPS function with
* tuple of functions returning CPS functions,
* where the nth function applies to each output from the nth callback
* and the resulting outputs are gathered by index.
* If fewever functions are passed in the tuple,
* outputs from remaining callbacks are preserved unchanged.
*
* @signature (...fns) -> CPS -> CPS
*
* @param {...Function} (f0,...,fn)
* - tuple of functions, each returning CPS function.
* @param {Function} cpsFn - CPS function.
* @returns {Function} `chain(f0,...,fn)(cpsFn)`
* - CPS function whose nth callback's output is gathered from
* the nth callback's outputs of each function fns[j] for each j
* evaluated for each output of the jth callback of `cpsFn`.
* If 'fns' has fewever functions than the number of callbacks passed,
* the extra callbacks receive the same output as from cpsFn
*
* @example
* // callbacks `cb0,cb1` receive outputs respectively `(2,3)` and `(7,9)`
* const cpsFn = (cb1,cb2) => {cb1(2,3); cb2(7,9)}
* const f1 = (x,y) => (cb1,cb2) => {cb1(x+y); cb2(x-y)}
* const f2 = (x,y) => cb => {cb(x,-y)}
*
* chain(f1,f2)(cpsFn)
* // cpsFn -> (2,3) -> f1 -> (2+3) -> c1 /->(7,-9) -> c1
* // \ \-> (2-3) -> c2 /
* // \-> (7,9) -> f2 --------------
* // is equivalent to the CPS function
* (c1,c2) => {c1(2+3); c2(2-3); c1(7,-9)}
*
* @example
* // convert to CPS function with 2 callbacks
* const readFile = file => (onRes, onErr) =>
* fs.readFile(file, (e, name) => {
* e ? onErr(e) : onRes(name)
* })
* const readName = readFile('index.txt') // CPS function
*
* const readFileByName = chain(name => readFile(name))(readName)
* // or equivalently
* const readFileByName = chain(readFile)(readName)
*
*/
const chain = (...fns) => cpsFn => {
fns = fns.map((f,ind) => isNil(f) ? ofN(ind) : f)
let cpsNew = (...cbs) => {
// all callbacks from the chain get passed to each cpsFn
let newCallbacks = fns.map(f =>
(...args) => f(...args)(...cbs)
)
// add missing callbacks unchanged from the same positions
return cpsFn(...mergeArray(newCallbacks, cbs))
}
inheritPrototype(cpsNew, cpsFn)
return cpsNew
}
/**
* Map CPS function over arbitrary tuple of functions, where for each n,
* the nth function from the tuple transforms the output of the nth callback.
* If fewever functions are passed in the tuple,
* outputs from remaining callbacks are preserved unchanged.
* The pair `(map,of)` conforms to the Pointed Functor spec,
* see {@link https://stackoverflow.com/a/41816326/1614973}.
*
* @signature (...fns) -> CPS -> CPS
*
* @param {...Function} (f0,...,fn) - tuple of functions.
* @param {Function} cpsFn - CPS function.
* @returns {function} `map(f0,...,fn)`
* - function taking CPS function `cpsFn`
* and returning new CPS function whose nth callback's output equals
* the jth callback's output of `cpsFun` transformed with function `fj`.
* If `fj` is undefined or null, the output is passed unchanged.
*
* @example
* // 2 callbacks receive respective outputs (2,3) and (7)
* const cpsFn = (cb0,cb1) => {cb0(2,3); cb1(7)}
* const f0 = (x,y) => x+y
* const f1 = z => z*6
* map(f0,f1)(cpsFn)
* // is equivalent to the CPS function
* (cb0,cb1) => {cb0(2+3); cb1(f1(7*6))}
*
* @example
* const cpsFromPromise = promise => (onRes,onErr) => promise.then(onRes,onErr)
* map(f0,f1)(cpsFromPromise(promise))
* // is equivalent to
* cpsFromPromise(promise.then(f0).catch(f1))
*/
// precompose every callback with fn from array matched by index
// if no function provided, default to the identity
exports.map = (...fns) => chain(...fns.map((f,idx) => isNil(f) ? null :
(...args) => ofN(idx)(f(...args))
))
/**
* Same as `map` but spread return values of transforming functions.
* This allows to transform output tuples into tuples rather than single values as `map` does.
* As JavaScript has no tuples, use arrays instead.
*
* @example
* // 1 callback receives output `(2,3)`
* const cpsFn = cb => cb(2,3)
* // `f` transforms `(x,y)` to `(x+y,x-y)` (written as array)
* const f = (x,y) => [x+y,x-y]
* map(f)(cpsFn)
* // is equivalent to the CPS function
* cb => cb(2+3,2-3)
*/
exports.mapSpread = (...fns) => chain(...fns.map((f,idx) =>
(...args) => ofN(idx)(...f(...args))
))
/**
* Filter outputs making predicates `(pred0,...,predn)` truthy.
* Pass through only outputs from jth callback making `predj` truthy.
*/
const filter = (...preds) => {
// call `chain` with the list of functions, one per each predicate
let transformer = (pred, idx) => (...inputs) =>
(...cbs) => (pred(...inputs)) && cbs[idx](...inputs)
return chain(...preds.map(transformer))
}
/**
* Iterate tuple of reducers over tuple of vals
* and outputs from CPS function regarded as actions.
* `reducers` and `vals` are matched by index.
*
* @signature (...reducers, init) -> cpsAction -> cpsState
*
* @param {...Function} reducers
* - functions of the form `red = (acc, ...vals) => newAcc`
* @param {*} init - initial value for the iteration.
* @param {Function} cpsFn - CPS function.
* @returns {Function} `scan(...reducers, init)(cpsFn)`
* - CPS function whose output from the first callback
* is the accumd value. For each output `(y1, y2, ...)`
* from the `n`th callback of `cpsFn, the `n`th reducer `redn`
* is used to compute the new acculated value
* `redn(acc, y1, y2, ...)`, where `acc` starts with `init`,
* similar to `reduce`.
*/
// const scan = (...args) => {
// if (args.length < 2) throw Error(`Scan needs at least 2 args, curently: ${JSON.stringify(args)}`)
// let reducers = args.slice(0,-1),
// acc = args.at(-1)
// // chain receives tuple of functions, one per reducer
// // nth CPS function inside chain receives nth callback output of cpsAction
// let cpsTrasformer = reducer => isNil(reducer) ? undefined : (...action) => cb => {
// // accessing vals and reducers by index
// acc = reducer(acc, ...action)
// cb(acc)
// }
// // chaining outputs of cpsAction with multiple reducers, one per state
// return chain(...reducers.map(cpsTrasformer))
// }
exports.scan = (...initStates) => (...reducers) => exports.map(...reducers.map(
(reducer,idx) => isNil(reducer) ? null : exports.update(reducer)(initStates[idx])
))
// simplified scan dropping the seed
const scanS = (...args) => exports.scan()(...args)
/**
* Ap is the core operator to run CPS functions in parallel.
* It applies functions to values,
* where both functions and values are delivered separately as CPS outputs.
*
* @signature (...fns) -> CPS -> CPS (curried)
*
* @param {...Function} fns
* - tuple of CPS functions, each returning a function.
* @param {Function} cpsFn - CPS function.
* @returns {Function} `ap(...fns)(cpsFn)`
* - CPS function whose nth callback's output is
* the results of function call `f(...args)`, where
* function `f` is the latest nth callback's output from fns[j] for some j
* and `(...args)` is the latest output from the jth callback of `cpsFn`.
* Only the latest outputs are stored for each callback
* and no output is emitted unless both function and arguments are available.
*
* @example
* const readFile = file => (onRes, onErr) =>
* fs.readFile(file, (e, name) => {
* e ? onErr(e) : onRes(name)
* })
* const appendToFile = cb => addition => {
* readFile('old.txt')(content => cb(content + addition))
* }
*
* const readFilesCombined = ap(appendToFile)(readFile('new.txt'))
* readFilesCombined(res => console.log(res), err => console.err(err))
*
*/
const ap = (...fns) => cpsFn => {
let fCache = {},
argsCache = {}
let cpsNew = (...cbs) => {
let newCallbacks = fns.map((f, idxF) => (...output) => {
argsCache[idxF] = output
if (fCache[idxF]) {
Object.keys(fCache[idxF]).forEach(idxCb =>
cbs[idxCb](fCache[idxF][idxCb](...output))
)}
})
fns.forEach((f, idxF) => f(...cbs.map((cb, idxCb) => outputF => {
if(!fCache[idxF]) fCache[idxF] = {}
fCache[idxF][idxCb] = outputF
// look over previously cached arguments from cpsFn
let output = argsCache[idxF]
if (output) cb(outputF(...output))
})))
// add missing callbacks from the same positions
return cpsFn(...mergeArray(newCallbacks, cbs))
}
inheritPrototype(cpsNew, cpsFn)
return cpsNew
}
/**
* Lift binary function to act on values wraped inside CPS functions
*/
exports.lift2 = f => (F1, F2) => pipeline(F2)(
ap(exports.map(exports.curryGroupsN(2)(f))(F1))
)
/* ----- CPS methods ----- */
const apply2this = fn =>
function(...args) {return fn(...args)(this)}
// apply function to all values of object
const objMap = fn => obj =>
Object.keys(obj).reduce((acc, key) => {
acc[key] = fn(obj[key])
return acc
}, {})
// Prototype methods
const protoObj = objMap(apply2this)({
...exports,
chain,
filter,
})
/**
* Wraps CPS function into object providing all CPS operators as methods
*/
exports.CPS = cpsFn => {
// clone the function
let cpsWrapped = (...args) => cpsFn(...args)
Object.setPrototypeOf(cpsWrapped, protoObj)
return cpsWrapped
}
/* ------- CPS utils ------ */
/**
* Convert NodeJS function to CPS factory
*
* @param {Function} nodeF - function with Node style callback `cb` as last argument:
* cb(error, result)
* @returns {Function} node2cps(nodeF) - CPS factory function taking all args but last
* that returns CPS function with 2 callbacks similar to Promise
*/
exports.node2cps = nodeF => (...args) => exports.CPS(
(onRes, onErr) => nodeF(...args, (e, ...x) => e ? onErr(e) : onRes(...x))
)
/**
* Convert Promise factory to CPS factory
* makes promise lazy by defering promise creation
*
* @param {Function} promiseFactory - function that returns Promise
* @returns {Function} promiseF2cps(promiseFactory) - CPS factory function
*/
exports.promiseF2cps = promiseFactory => (...args) => (onRes, onErr) => promiseFactory(...args).then(onRes, onErr)
/**
* convert syncrounous outputs of CPS function to array of output arrays
* output (x1,...,xn) in jth callback adds [x1,...,xn] or x1 if n=1 to jth
*/
exports.cpsSync2arr = cpsF => {
let arr = []
cpsF((...args) => arr.push(args))
return arr
}
module.exports = {
...require('./utils'),
...exports,
pipeline, pipe,
of, ofN, chain, filter, scanS, ap,
}