useless
Version:
Use Less. Do More. JavaScript on steroids.
441 lines (327 loc) • 16.2 kB
JavaScript
;
const _ = require ('underscore')
/* CPS primitives module
======================================================================== */
_.cps = function () {
return _.cps.sequence.apply (null, arguments) }
/* apply
======================================================================== */
_.withTest (['cps', 'apply'], function () {
// TODO
}, function () {
_.cps.apply = function (fn, this_, args_, then) { var args = _.asArray (args_)
var lastArgN = _.numArgs (fn) - 1
var thenArg = args[lastArgN]
args[lastArgN] = function () {
then.call (this, arguments, thenArg) }
return fn.apply (this_, args) } })
/* each
======================================================================== */
_.withTest (['cps', 'each'], function () {
/* Example array
*/
var data = ['foo', 'bar', 'baz']
var currentIndex = 0
_.cps.each (data,
/* called for each item, in linear order
*/
function (item, itemIndex, then, complete, arrayWeTraverse) {
$assert (item === data[itemIndex])
$assert (itemIndex === currentIndex++)
$assert (arrayWeTraverse === data)
$assert (_.isFunction (then))
$assert (_.isFunction (complete))
then () },
/* called when all items enumerated
*/
function () {
$assert (currentIndex === data.length) })
/* You can omit 'index' argument for iterator function
*/
var data2 = []
_.cps.each (data,
function (item, then) { data2.push (item); then () },
function () { $assert (data, data2) })
/* You can stop iteration by calling fourth argument
*/
var data3 = []
_.cps.each (data,
function (item, i, then, break_) { data3.push (item); break_ () },
function () { $assert (data3, ['foo']) })
/* Iterating over dictionary is legal
*/
$assertEveryCalled (function (items__3, final__1) {
var data2 = { 'foo': 1, 'bar': 2, 'baz': 3 }
_.cps.each (
data2,
function (item, name, then) { $assert (item === data2[name]); items__3 (); then () },
function () { final__1 () }) })
/* Iterating over scalar is legal
*/
$assertEveryCalled (function (items__1, final__1) {
_.cps.each (
'foo',
function (item, name, then) { $assert ([item, name], ['foo', undefined]); items__1 (); then () },
function () { final__1 () }) })
/* Undefined/null are treated as empty (not scalars)
*/
$assertEveryCalled (function (final__1) {
_.cps.each (
undefined,
function () { $fail },
function () { final__1 () }) }) },
function () { _.extend (_.cps, {
each: function each (obj, elem_, complete_ = _.noop, index_, length_, keys_) {
let completed = false
var complete = (...args) => !completed && (completed = true, complete_ (...args))
var elem = function (x, k, next) {
if (_.numArgs (elem_) === 2) { elem_ (x, next, complete, obj) }
else { elem_ (x, k, next, complete, obj) } }
if (_.isEmpty (obj)) {
complete () }
else if (_.isScalar (obj)) {
elem (obj, undefined, complete) }
else {
var index = index_ || 0
var keys = index === 0 ? (obj.length === undefined ? _.keys(obj) : undefined) : keys_
var length = index === 0 ? (keys ? keys.length : obj.length) : length_
if (index >= (length || 0)) {
complete () }
else {
var key = keys ? keys[index] : index
elem (obj[key], key, () => !completed && each.call (this, obj, elem_, complete_, index + 1, length, keys))
}
}
}
})
}
)
/* map
======================================================================== */
_.withTest (['cps', 'map'], function () {
/* 2-argument iterator semantics
*/
_.cps.map ([7,6,5],
function (x, then) { then (x + 1) },
function (result) { $assert (result, [8,7,6]) })
/* 3-argument iterator semantics
*/
_.cps.map ([7,6,5],
function (x, i, then) { then (x + 1) },
function (result) { $assert (result, [8,7,6]) }) },
function () { _.extend (_.cps, {
map: function (obj, iter, complete) { var result = _.isArray (obj) ? [] : {}
_.cps.each (obj, (_.numArgs (iter) == 2)
? function (x, i, next) { iter (x, function (y) { result[i] = y; next () }) }
: function (x, i, next) { iter (x, i, function (y) { result[i] = y; next () }) },
function () { complete (result) }) } }) })
/* find
======================================================================== */
_.withTest (['cps', 'find'], function () {
/* Basic use
*/
_.cps.find ([7,6,5],
function (x, then) { then ((x % 3) === 0) },
function (x, key) { $assert ([x, key], [6, 1]) })
/* Over dictionary
*/
_.cps.find ({ foo: 7, bar: 6, baz: 5 },
function (x, key, then) { then (key === 'baz') },
function (x, key) { $assert ([x, key], [5, 'baz']) })
/* Returning non-boolean
*/
_.cps.find ([7,6,5],
function (x, then) { then (((x % 3) === 0) ? 'yeah' : false) },
function (x, key) { $assert ([x, key], ['yeah', 1]) })
/* Not found
*/
_.cps.find ([7,6,5],
function (x, key, then) { then (false) },
function (x) { $assert (x, undefined) }) },
function () { _.extend (_.cps, {
find: function (obj, pred, complete) { var passKey = (_.numArgs (pred) !== 2)
_.cps.each (obj, function (x, key, next, complete) { var take = function (match) {
if (match === false) { next () }
else { complete (match === true ? x : match, key) } }
if (passKey) { pred (x, key, take) }
else { pred (x, take) } }, complete) } }) })
/* memoize
======================================================================== */
_.withTest (['cps', 'memoize'], function () {
$assertEveryCalledOnce (function (noMoreThanOne) {
var plusOne = _.cps.memoize (function (x, then) { noMoreThanOne (); then (x + 1) })
plusOne (2, function (x) { $assert (x === 3) })
plusOne (2, function (x) { $assert (x === 3) }) }) },
function () { _.extend (_.cps, {
memoize: function (fn) {
return _.barrier ? _.cps._betterMemoize (fn) : _.cps._poorMemoize (fn) },
/* This simplified version is used to bootstrap Useless.js code base (where _.barrier not available)
*/
_poorMemoize: function (fn) { var cache = {}
return function (value, then) {
if (value in cache) { // there's a flaw: cache updates after fetch completes, so while fetch is running,
then (cache[value]) } // any subsequent call (until cache is ready) will trigger fetch (as it doesnt know that result is already fetching)
else {
fn.call (this, value, function (result) {
then (cache[value] = result) }) } } },
/* UPD: added support for 0-arity semantics
*/
_betterMemoize: function (fn) { var cache = {} // barrier-enabled impl, eliminates redundant fetches
// in this version, any subsequent calls join at barrier (which opens when result is fetched)
switch (_.numArgs (fn)) {
case 1:
return function (then) {
if (!cache.already) {
fn.call (this, (cache = _.barrier ())) }
cache (then) }
case 2:
cache = {}
return function (value, then) {
if (!(value in cache)) {
fn.call (this, value, (cache[value] = _.barrier ())) }
cache[value] (then) }
default:
throw new Error ('_.cps.memoize: unsupported number of arguments') } } }) })
/* reduce
======================================================================== */
_.withTest (['cps', 'reduce'], function () { $assertEveryCalled (function (mkay__2) {
var input = [1,2,3]
var sums = function (a, b, then) { then (a + b) }
var check = function (result) { $assert (result === 6); mkay__2 () }
_.cps.reduce (input, sums, check)
_.cps.reduce ([], sums, check, 6)
})}, function () {
var reduce = function (array, op, then, memo, index) { // internal impl
if (!array || (index >= (array.length || 0))) {
then (memo) }
else {
op (memo, array[index], function (result) { reduce (array, op, then, result, index + 1) }) } }
_.cps.reduce = function (array, op, then, memo) { // public API
if (arguments.length < 4) {
reduce (array, op, then, array[0], 1) }
else {
reduce (array, op, then, memo, 0) } } } )
/* noop / identity / constant
======================================================================== */
_.withTest (['cps', 'noop, identity, constant'], function () { $assertEveryCalled (function (noop, identity, const1, const2) {
/* Port of underscore's _.noop to CPS terms
*/
_.cps.noop (1,2,3, function () { $assert (arguments.length === 0); noop () })
/* Port of underscore's _.identity to CPS terms
*/
_.cps.identity (1,2,3, function () { $assert ([1,2,3], _.asArray (arguments)); identity () })
/* Port of underscore's _.constant to CPS terms
*/
_.cps.constant (3) (function (_3) { $assert (_3 === 3); const1 () })
_.cps.constant (1, 2) (function (_1, _2) { $assert (_1 === 1); $assert (_2 === 2); const2 () })
})}, function () { _.extend (_.cps, {
noop: $restArg (function () {
return _.last (arguments).call (this) }),
identity: $restArg (function () {
var args = _.initial (arguments),
then = _.last (arguments)
if (then) {
return then.apply (this, args) } }),
constant: $restArg (function () { var args = arguments
return function () {
return _.last (arguments).apply (this, args) } }) })} )
/* arity
======================================================================== */
_.deferTest (['cps', 'arity / resultArity'], function () {
var returnMyArgs = _.cps.identity
var put123 = function (fn) {
return _.partial (fn, 1,2,3) }
$assertCPS (put123 ( returnMyArgs), [1,2,3])
$assertCPS (put123 (_.cps.arity2 (returnMyArgs)), [1,2])
$assertCPS (put123 (_.cps.arity1 (returnMyArgs)), [1])
$assertCPS (put123 (_.cps.arity0 (returnMyArgs)))
var return123 = function (then) {
then (1,2,3) }
$assertCPS ( return123, [1,2,3])
$assertCPS (_.cps.resultArity2 (return123), [1,2])
$assertCPS (_.cps.resultArity1 (return123), [1])
$assertCPS (_.cps.resultArity0 (return123))
}, function () {
_.cps.arity0 = function (fn) {
return function () {
fn.call (this, _.last (arguments)) } }
_.cps.arity1 = function (fn) {
return function () {
fn.call (this, arguments[0], _.last (arguments)) } }
_.cps.arity2 = function (fn) {
return function () {
fn.call (this, arguments[0], arguments[1], _.last (arguments)) } }
_.cps.transformResult = function (operator, fn) {
return function (args) {
fn.apply (this, _.initial (arguments).concat (operator (_.last (arguments)))) } }
_.cps.resultArity2 = _.partial (_.cps.transformResult, _.arity2)
_.cps.resultArity1 = _.partial (_.cps.transformResult, _.arity1)
_.cps.resultArity0 = _.partial (_.cps.transformResult, _.arity0) })
/* sequence / compose
======================================================================== */
_.withTest (['cps', 'sequence / compose'], function () { $assertEveryCalled (function (mkay__4) {
/* Basic example of asynchronous functions sequencing
*/
var makeCookies = function (whatCookies, then) { then ('cookies ' + whatCookies) }
var eatCookies = function (cookies, then) { then ('nice ' + cookies) }
var check = function (result) { $assert (result, 'nice cookies from shit'); mkay__4 () }
_.cps.sequence (makeCookies, eatCookies, check) ('from shit') // supports both ways (either argument list...
_.cps.sequence ([makeCookies, eatCookies, check]) ('from shit') // ..or array
_.cps (makeCookies, eatCookies, check) ('from shit') // shorthand macro
/* A port of underscore's _.compose (simply flipped _.sequence)
*/
_.cps.compose (check, eatCookies, makeCookies) ('from shit')
})}, function () {
_.cps.sequence = $restArg (
function (arr) { var functions = (_.isArray (arr) && arr) || _.asArray (arguments)
return _.reduceRight (functions, function (a, b) {
return function () {
return b.apply (this, _.asArray (arguments).concat (a)) }}, _.cps.identity) })
_.cps.compose = $restArg (
function (arr) { var functions = (_.isArray (arr) && arr) || _.asArray (arguments)
return _.cps.sequence (functions.slice ().reverse ()) }) })
/* _.cps.sequence with error handling (kind of a simplified Promise)
======================================================================== */
_.deferTest (['cps', 'trySequence'], function () {
var testErr = new Error ()
/* No error
*/
$assertEveryCalledOnce (function (mkay) {
_.cps.trySequence ([
_.cps.constant ('foo'),
_.appends ('bar').asContinuation],
function (result) { $assert (result, 'foobar'); mkay () }) })
/* Throwing error
*/
$assertEveryCalledOnce (function (mkay) {
_.cps.trySequence ([
function () { throw testErr },
function () { $fail }],
function (result) { $assert (result === testErr); mkay () }) })
/* Returning error to continuation
*/
$assertEveryCalledOnce (function (mkay) {
_.cps.trySequence ([
function (then) { then (testErr) },
function () { $fail }],
function (result) { $assert (result === testErr); mkay () }) })
/* Reading error in separate callback
*/
$assertEveryCalledOnce (function (mkay) {
_.cps.trySequence ([
function (then) { then (testErr) },
function () { $fail }],
function (result) { $fail },
function (err) { $assert (err === testErr); mkay () }) })
}, function () {
_.cps.trySequence = function (functions, then, err) {
_.reduceRight (functions, function (a, b) {
return function (e) {
if (_.isTypeOf (Error, e)) {
return (err || then) (e) }
else {
try {
return b.apply (this, _.asArray (arguments).concat (a)) }
catch (e) {
return (err || then) (e) } } } }, then) () }
})