import compose from UnitJS.Fun; import curry from UnitJS.Fun.Curry; return { pure: pure, wrap: pure, // kept for backwards compatibility tuple: pure, // an alias to insist on the fact that pure can be used to build tuples fmap: fmap, liftUncurry: liftUncurry, lift: lift, liftA2: liftA2, mApply: mApply, bind: bind, fail: fail, apply: apply, map: map, sequence: sequence, parallel: parallel, loopWhile: loopWhile, loopUntil: loopUntil, loopForEach: loopForEach, wait: wait, http: http, run: run }; /* * The Async monad obtained by curryfying Continuation Passing Style * * Async a = (a -> void) -> void * */ /* * pure :: a -> Async a * * Since all values only exist as arguments to the next computation, this allows to represent tuples and indeed, pure is variadic and allows to build tuples by passing as many arguments as you want * * tuple :: (a0, a1, … , an) -> Async (a0, a1, … , an) */ function pure() { var args = arguments; return function(f) { f.apply(null, args); }; } /* * Async is a functor: * * fmap :: (a -> b) -> Async a -> Async b * */ function fmap(f) { return function(mA) { return function(g) { return mA(compose(g, f)); }; }; } /* * Async is also an Applicative: * * When considered for tuples, fmap is almost liftAN for all N for uncurryied functions, all we need to do is to collect the various Async arguments with `parallel` * * liftUncurry :: ((a0, a1, … , an) -> b) -> (Async a0, Async a1, … , Async an) -> Async b */ function liftUncurry(f) { return function() { return fmap(f)(parallel.apply(null, arguments)); } } /* * A version for curryied functions (still taking the Async parameters in an uncurryied way) * * lift :: (a0 -> a1 -> … -> an -> b) -> (Async a0, Async a1, … , Async an) -> Async b */ function lift(f) { return function() { return Array.prototype.reduce.call(arguments, function(acc, v) { return mApply(acc)(v); }, pure(f)); }; } /* * The fully curryied version of lift for each arity can then be easily derived following this example for arity 2 * * liftA2 :: (a -> b -> c) -> Async a -> Async b -> Async c */ function liftA2(f) { return curry(lift(f), 2); } /* * Since we defined `lift` in term of mApply, we must implement this part of Applicative: * * mApply :: Async (a -> b) -> Async a -> Async b */ function mApply(mF) { return function(mA) { return function(g) { mF(function(f) {fmap(f)(mA)(g)}); }; }; } /* * The bind operator needed to use the monad as such * * bind :: Async a -> (a -> Async b) -> Async b * * Actually, this has been generalized to allow an arbitrarily long call chain * * bind :: Async a0 -> (a0 -> Async a1) -> (a1 -> Async a2) -> … -> Async an */ function bind() { var m, steps; if(arguments.length < 1) { return pure(); } m = arguments[0]; steps = arguments; return function(f) { var i = 1; var step = function() { if(i < steps.length) { steps[i++].apply(null, arguments)(step); } else { return f.apply(null, arguments); } } m(step); }; } /* * A primitive to stop a computation in Async and display a log message in the console * * fail :: string -> Async a */ function fail(message) { return function(f) { console.log(message); } } /* * apply :: (a -> b, a) -> Async b */ function apply(f, x) { return function(g) { g(f(x)); }; } /* * map :: (a -> b) -> a -> Async b */ function map(mapper) { return function(x) { return function(f) { f(mapper(x)); }; }; } /* * A sequence of void computations * * sequence :: (Async void, Async void, … , Async a) -> Async a */ function sequence() { var steps = arguments; return function(f) { var i = 0; var step = function() { if(i < steps.length) { steps[i++](step); } else { f.apply(null, arguments); } } step(); }; } /* * Parallel computations which results are collected and passed on to the next computation as a tuple * * parallel :: (Async a0, Async a1, … , Async an) -> Async (a0, a1, … , an) */ function parallel() { var threads = arguments; return function(f) { var pending = threads.length; var results = []; var returned = []; var useResult = function(i) { return function(x) { if(!returned[i]) { results[i] = x; returned[i] = true; pending--; if(pending < 1) { f.apply(null, results); } } }; }; for(var i = 0; i < threads.length; i++) { threads[i](useResult(i)); } }; } /* * Three async loops: * * loopWhile performs the test before entering the loop and ends when the condition becomes false * * loopWhile :: (Async boolean, Async void) -> Async void */ function loopWhile(predicate, body) { return bind(predicate, function(keepOn) { if(keepOn) { return sequence.apply(null, [].concat(body, loopWhile(predicate, body))); } else { return pure(); } }); } /* * A variant that check the condition at the end of the loop that, hence, always runs at least once. Also note that this loop stops when the condition becomes true * * loopUntil :: (Async void, Async boolean) -> Async void */ function loopUntil(body, predicate) { return sequence.apply(null, [].concat(body, bind(predicate, function(atEnd) { if(atEnd) { return pure(); } else { return loopUntil(body, predicate); } }))); } /* * A loop that iterates over an array; the body is a function that accesses the iteration context as its arguments just like Array.prototype.forEach: current element, its index in the array, the whole array * * loopForEach :: ([a], ((a, number, [a]) -> Async void)) -> Async void */ function loopForEach(array, bodyF) { return sequence.apply(null, array.reduce(function(acc, v, k, a) { return acc.concat(bodyF(v, k, a)); }, [])); } /* * wait :: number -> Async void */ function wait(delay) { return function(f) { setTimeout(f, delay); }; } /* * Perform an Async HTTP(S) query * * http :: query -> Async XMLHttpRequest */ function http(query) { return function(f) { var xhr = new XMLHttpRequest(); xhr.addEventListener('load', function() { f(this); }); xhr.open(query.method, query.url); if(query.method == 'POST') { xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); } xhr.send(query.body); }; } /* * The only «way-out» of Async: running the computation from without * * run :: Async a -> void */ function run() { var m; if(arguments.length == 1) { m = arguments[0]; } else { m = sequence.apply(null, arguments); } m(function() {}); }