Formalize loops for Async, including better parallelization, tuples, fmap and lift + define curry- and uncurryfication

This commit is contained in:
Tissevert 2021-01-15 11:16:16 +01:00
parent 7c8b4a3b87
commit 0b7fcfe36b
3 changed files with 342 additions and 60 deletions

View file

@ -1,49 +1,164 @@
import compose from UnitJS.Fun;
import curry from UnitJS.Fun.Curry;
return {
apply: apply,
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,
http: http,
loopWhile: loopWhile,
apply: apply,
map: map,
parallel: parallel,
run: run,
sequence: sequence,
parallel: parallel,
loopWhile: loopWhile,
loopUntil: loopUntil,
loopForEach: loopForEach,
wait: wait,
wrap: wrap
http: http,
run: run
};
function apply(f, x) {
return function(g) {
g(f(x));
/*
* 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 wrap();
return pure();
}
m = arguments[0];
steps = arguments;
return function(f) {
var i = 1;
var step = function(x) {
var step = function() {
if(i < steps.length) {
steps[i++](x)(step);
steps[i++].apply(null, arguments)(step);
} else {
return f(x);
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) {
@ -52,12 +167,37 @@ function map(mapper) {
};
}
/*
* 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;
var pending = threads.length;
var results = [];
var returned = [];
return function(f) {
var pending = threads.length;
var results = [];
var returned = [];
var useResult = function(i) {
return function(x) {
if(!returned[i]) {
@ -65,7 +205,7 @@ function parallel() {
returned[i] = true;
pending--;
if(pending < 1) {
f(results);
f.apply(null, results);
}
}
};
@ -76,53 +216,63 @@ function parallel() {
};
}
function run() {
var m;
if(arguments.length == 1) {
m = arguments[0];
} else {
m = sequence.apply(null, arguments);
}
m(function() {});
}
function sequence() {
var steps = arguments;
return function(f) {
var i = 0;
var step = function(x) {
if(i < steps.length) {
steps[i++](step);
} else {
f(x);
}
}
step();
};
}
/*
* 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(body, loopWhile(predicate, body));
return sequence.apply(null, [].concat(body, loopWhile(predicate, body)));
} else {
return wrap();
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);
};
}
function wrap(x) {
return function(f) {
f(x);
};
}
/*
* Perform an Async HTTP(S) query
*
* http :: query -> Async XMLHttpRequest
*/
function http(query) {
return function(f) {
var xhr = new XMLHttpRequest();
@ -134,6 +284,20 @@ function http(query) {
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() {});
}

View file

@ -14,19 +14,20 @@ return {
teq: teq,
tneq: tneq,
lt: lt,
le: le
le: le,
and: and,
or: or
};
function curry(f, n) {
n = n == undefined ? f.length-1 : n;
if(n == 0) {
return f;
function curry(f, n, t) {
n = n == undefined ? f.length : n;
t = t == undefined ? [] : t;
if(n < 1) {
return f.apply(null, t);
} else {
return function(x) {
return curry(function() {
var args = Array.prototype.slice.call(arguments);
return f.apply(null, [].concat(x, args));
}, n-1);
var args = Array.prototype.slice.call(arguments);
return curry(f, n - args.length, t.concat(args));
};
}
}
@ -122,3 +123,15 @@ function le(a) {
return b <= a;
};
}
function and(a) {
return function(b) {
return a && b;
};
}
function or(a) {
return function(b) {
return a || b;
};
}

105
src/UnitJS/Fun/Uncurry.js Normal file
View file

@ -0,0 +1,105 @@
return {
uncurry: uncurry,
flip: flip,
plus: plus,
minus: minus,
times: times,
divide: divide,
gt: gt,
ge: ge,
eq: eq,
neq: neq,
teq: teq,
tneq: tneq,
lt: lt,
le: le,
and: and,
or: or
};
function uncurry(f) {
return function() {
var args = Array.prototype.slice.call(arguments);
var result = f;
while(args.length > 0 && typeof result == 'function') {
result = result.apply(null, args.splice(0, result.length));
}
return result;
}
}
function flip(f) {
return function(a, b) {
return f(b)(a);
}
}
function plus(a, b) { // keeping two arguments so that it can be curryied to Curry.plus by default
return Array.prototype.reduce.call(arguments, function(acc, v) {
return acc+v;
}, 0);
}
function minus(a, b) {
return a-b;
}
function times(a, b) {
return Array.prototype.reduce.call(arguments, function(acc, v) {
return acc*v;
}, 1);
}
function divide(a, b) {
return a/b;
}
function gt(a, b) {
return a > b;
}
function ge(a, b) {
return a >= b;
}
function eq(a, b) {
return a == b;
}
function neq(a, b) {
return a != b;
}
function teq(a, b) {
return a === b;
}
function tneq(a, b) {
return a !== b;
}
function lt(a, b) {
return a < b;
}
function le(a, b) {
return a <= b;
}
function and(a, b) {
for(var i = 0; i < arguments.length; i++) {
if(arguments[i] === false) {
return false;
}
}
return true;
}
function or(a, b) {
for(var i = 0; i < arguments.length; i++) {
if(arguments[i] === true) {
return true;
}
}
return false;
}