Is there a way to avoid the trade-off between readability and performance when looping? - javascript

So this is a readable way (the code doesn't matter, what matters is the style):
arr.map().filter() // looping 2 times
And loops are considered as a faster way:
for(/* whatever */) {
// looping once, and doing all we need in the same loop
}
So my question is: is there a way, maybe from the functional programming world, to combine the readability of the former with the performance of the latter?
P.S. There is a trend to downvote such questions. If you want to, please write the reason as well.

Of course there is.
1st alternative: Transducer
const mapReduce = map => reduce => (acc, x) =>
reduce(acc, map(x));
const filterReduce = filter => reduce => (acc, x) =>
filter(x)
? reduce(acc, x)
: acc;
const transduce = (...ts) => xs =>
xs.reduce(ts.reduce(comp, id) (concat), []);
const comp = (f, g) =>
x => f(g(x));
const id = x => x;
const concat = (xs, ys) =>
xs.concat(ys);
const sqr = n => n * n;
const isOdd = n => n & 1 === 1;
const log = console.log;
// the upper code is usually library code
// so you don't have to deal with its complexity but only with its API
const tx = filterReduce(isOdd),
ty = mapReduce(sqr);
const r = transduce(tx, ty) ([1,2,3,4,5]); // filter/map in same iteration
log(r);
2nd alternative: Bare recursion with a tail call optimization effect
const loop = f => {
let acc = f();
while (acc && acc.type === tailRec)
acc = f(...acc.args);
return acc;
};
const tailRec = (...args) =>
({type: tailRec, args});
const comp = (f, g) => x =>
f(g(x));
const sqr = n => n * n;
const isOdd = n => n & 1 === 1;
const log = console.log;
// the upper code is usually library code
// so you don't have to deal with its complexity but only with its API
const r = loop((xs = [1,2,3,4,5], acc = [], i = 0) => {
if (i === xs.length)
return acc;
else
return tailRec( // filter/map in same iteration
xs,
isOdd(xs[i]) ? acc.concat(sqr(xs[i])) : acc,
i + 1);
});
log(r);
I'd say transducer are for normal, simpler iterations whereas recursion is suitable for more complex ones, for example when you need short circuiting (prematurely exiting).

Personally I don't think having some for-loops in your code makes it unreadable, but that's opinion based I suppose.
There are many ways to make your code more readable. If you're going to use this functionality often then you could create a method to add to Array.prototype - this way you can write the for-loop once and call it when you need it without having to see what you consider ugly code. Below is an example:
//This method will now be available to all Arrays instances:
Array.prototype.prettyLoop = function() {
console.log('All I do is execute a basic for-loop');
for (let i = 0; i < this.length; i++) {
console.log(this[i]);
}
};
//Call the method from your script
["a", 1, null, "x", 1989, false, {}].prettyLoop();

Related

Emulating lisp list operations in js

What would be the proper way to define the cons, car, and cdr functions if we were to use javascript and functional programming? So far I have:
// CAR = λxy.x
// CDR = λxy.y
// CONS = λxy => ?
const car = a => b => a;
const cdr = a => b => b;
const cons = f => a => b;
let pair = cons(3)(4);
console.log(pair(car));
console.log(pair(cdr));
But my issue is pair(car) seems like an odd way to invoke the function. It seems like car(pair) seems like a better way but I'm not sure how to save the pair so it can be passed like that. How would that be done?
You can write them using continuations -
const cons = (a,b) => k => k(a,b)
const car = k => k((a,b) => a)
const cdr = k => k((a,b) => b)
const l = cons(1, cons(2, cons(3, null)))
console.log(car(l), car(cdr(l)), car(cdr(cdr(l))), cdr(cdr(cdr(l))))
// 1 2 3 null
Define more like list, toString, map, and filter -
const cons = (a,b) => k => k(a,b)
const car = k => k((a,b) => a)
const cdr = k => k((a,b) => b)
const list = (v, ...vs) => v == null ? null : cons(v, list(...vs))
const toString = l => l == null ? "Ø" : car(l) + "->" + toString(cdr(l))
console.log(toString(list(1,2,3,4,5)))
// 1->2->3->4->5->Ø
const square = x => x * x
const map = (f, l) =>
l == null
? null
: cons(f(car(l)), map(f, cdr(l)))
console.log(toString(map(square, list(1,2,3,4,5))))
// 1->4->9->16->25->Ø
const isOdd = x => x & 1
const filter = (f, l) =>
l == null
? null
: Boolean(f(car(l)))
? cons(car(l), filter(f, cdr(l)))
: filter(f, cdr(l))
console.log(toString(filter(isOdd, map(square, list(1,2,3,4,5)))))
// 1->9->25->Ø
Note you could just as easily write cons, car, and cdr abstractions using an array. Lambda is more fun but anything that fulfills the contract is acceptable. Being able to change the underlying representation like this and not require changes in other parts of your program is what makes data abstraction a powerful technique.
const cons = (a,b) => [a,b]
const car = k => k[0]
const cdr = k => k[1]
const l = cons(1, cons(2, cons(3, null)))
console.log(car(l), car(cdr(l)), car(cdr(cdr(l))), cdr(cdr(cdr(l))))
// 1 2 3 null
The following would be one way to define it, where car and cdr take one argument (a pair) instead of two:
// CAR = λxy.x or λp.p[0] where p is xy and p[0] means the first argument (x)
// CDR = λxy.y or λp.p[1] where p is xy and p[1] means the second argument (y)
// CONS = λxyf.xyf -- basically just saving x and y in a closure and deferring a function call
const first = a => b => a;
const second = a => b => b;
const getFirstFromPair = p => p(first), car = getFirstFromPair;
const getSecondFromPair = p => p(second), cdr = getSecondFromPair;
const cons = a => b => f => f(a)(b);
let pair = cons(3)(4);
console.log(car(pair));
console.log(cdr(pair));
Or, if we allow a pair as a single argument, such as (1,2):
const cons = (a,b) => f => f(a,b); // f is like a fake function call to do encapsulation
const car = f => f((a,b) => a); // and we have to un-do the function call here by passing it the first
const cdr = f => f((a,b) => b);
console.log(cdr(cons(1,2)));

How to encode corecursion/codata in a strictly evaluated setting?

Corecursion means calling oneself on data at each iteration that is greater than or equal to what one had before. Corecursion works on codata, which are recursively defined values. Unfortunately, value recursion is not possible in strictly evaluated languages. We can work with explicit thunks though:
const Defer = thunk =>
({get runDefer() {return thunk()}})
const app = f => x => f(x);
const fibs = app(x_ => y_ => {
const go = x => y =>
Defer(() =>
[x, go(y) (x + y)]);
return go(x_) (y_).runDefer;
}) (1) (1);
const take = n => codata => {
const go = ([x, tx], acc, i) =>
i === n
? acc
: go(tx.runDefer, acc.concat(x), i + 1);
return go(codata, [], 0);
};
console.log(
take(10) (fibs));
While this works as expected the approach seems awkward. Especially the hideous pair tuple bugs me. Is there a more natural way to deal with corecursion/codata in JS?
I would encode the thunk within the data constructor itself. For example, consider.
// whnf :: Object -> Object
const whnf = obj => {
for (const [key, val] of Object.entries(obj)) {
if (typeof val === "function" && val.length === 0) {
Object.defineProperty(obj, key, {
get: () => Object.defineProperty(obj, key, {
value: val()
})[key]
});
}
}
return obj;
};
// empty :: List a
const empty = null;
// cons :: (a, List a) -> List a
const cons = (head, tail) => whnf({ head, tail });
// fibs :: List Int
const fibs = cons(0, cons(1, () => next(fibs, fibs.tail)));
// next :: (List Int, List Int) -> List Int
const next = (xs, ys) => cons(xs.head + ys.head, () => next(xs.tail, ys.tail));
// take :: (Int, List a) -> List a
const take = (n, xs) => n === 0 ? empty : cons(xs.head, () => take(n - 1, xs.tail));
// toArray :: List a -> [a]
const toArray = xs => xs === empty ? [] : [ xs.head, ...toArray(xs.tail) ];
// [0,1,1,2,3,5,8,13,21,34]
console.log(toArray(take(10, fibs)));
This way, we can encode laziness in weak head normal form. The advantage is that the consumer has no idea whether a particular field of the given data structure is lazy or strict, and it doesn't need to care.

Can we implement tail recursion modulo cons et al. through trampolines?

You can regard trampolines as compiler optimizations reified in the program. So what is stopping us from adapting more general optimization techniques in exactly the same manner.
Here is a sketch of tail recursion modulo cons:
const loop = f => {
let step = f();
while (step && step[step.length - 1] && step[step.length - 1].type === recur) {
let step_ = step.pop();
step.push(...f(...step_.args));
}
return step;
};
const recur = (...args) =>
({type: recur, args});
const push = (xs, x) => (xs.push(x), xs);
const map = f => xs =>
loop((i = 0) =>
i === xs.length
? []
: push([f(xs[i])], recur(i + 1)));
const xs =
map(x => x * 2) (Array(1e6).fill(0).map((x, i) => i))
.slice(0,5);
console.log(xs); // [0, 2, 4, 6, 8]
This kind of optimization depends on the associativity property of an expression. Multiplication is associative too and hence there is tail recursion modulo multiplication. However, I have to cheat to implement it in Javascript:
const loop = f => {
let step = f();
const acc = [];
while (step && step[1] && step[1].type === recur) {
acc.push(step[0]);
step = f(...step[1].args);
}
return acc.reduce((acc, f) => f(acc), step);
};
const recur = (...args) =>
({type: recur, args});
const mul = x => step => [y => x * y, step];
const pow = (x_, n_) =>
loop((x = x_, n = n_) =>
n === 0 ? 1
: n === 1 ? x
: mul(x) (recur(x, n - 1)));
console.log(
pow(2, 1e6)); // Infinity, no stack overflow
As you can see I cannot use a regular mul, which isn't particular satisfying. Is this connected with Javascript beeing a strict language? Is there a better way to achieve tail recursion modulo multiplication in JS without having to introduce awkward binary operators?
Instead of using loop/recur (which I consider an ugly and unnecessary hack), consider using folds:
const recNat = (zero, succ) => n => {
let result = zero;
while (n > 0) {
result = succ(result);
n = n - 1;
}
return result;
};
const mul = x => y => x * y;
const pow = x => recNat(1, mul(x));
console.log([0,1,2,3,4,5,6,1e6].map(pow(2))); // [1,2,4,8,16,32,64,Infinity]
Almost every recursive function can be defined using folds (a.k.a. structural recursion, a.k.a. induction). For example, even the Ackermann function can be defined using folds:
const recNat = (zero, succ) => n => {
let result = zero;
while (n > 0) {
result = succ(result);
n = n - 1;
}
return result;
};
const add = x => y => x + y;
const ack = recNat(add(1),
ackPredM => recNat(ackPredM(1),
ackMPredN => ackPredM(ackMPredN)));
console.time("ack(4)(1)");
console.log(ack(4)(1)); // 65533
console.timeEnd("ack(4)(1)");
The above code snippet takes about 18 seconds to compute the answer on my laptop.
Now, you might ask why I implemented recNat using iteration instead of natural recursion:
const recNat = (zero, succ) => function recNatZS(n) {
return n <= 0 ? zero : succ(recNatZS(n - 1));
};
I used iteration for the same reason you used iteration to implement loop. Trampolining. By implementing a different trampoline for every data type you're going to fold, you can write functional code without having to worry about stack overflows.
Bottom line: Use folds instead of explicit recursion. They are a lot more powerful than you think.

How to implement a coroutine based on multi-shot delimited continuations?

I recently implemented delimited continuations in CPS with reset/shift:
// reset :: ((a -> a) -> a) -> (a -> r) -> r
reset = k => f => f(k(id));
// shift :: ((a -> r) -> (r -> r) -> r) -> (a -> r) -> r
shift = f => k => f(k) (id);
Studying the theory I realized the following connections:
reset ~ function* // scope of the generator function
shift ~ yield
reset ~ async // scope of the asyn function
shift ~ await
As far as I understand the theory, Javascript's generators are a asymmetric, stackless, one-shot and first class coroutines.
asymmetric means that the called generator can only yield to its caller
stackless means a generator cannot yield from within nested functions
one-shot means that a generator can only resume from a specific position once
first class means a generator object can be passed around like normal data
Now I want to implement a coroutine based on reset/shift with the following traits:
asymmetric
stackful
multi-shot
first class
When looking at the following contrived example
const id = x => x;
const mul = x => y => x * y;
const add = x => y => x + y;
const sub = x => y => x - y;
const reset = k => f => f(k(id));
const shift = f => k => f(k) (id);
const of = x => k => k(x);
const lift2 = f => tx => ty => k => tx(x => ty(y => k(f(x) (y))));
const k0 = lift2(sub)
(reset
(lift2(add) (of(3))
(shift
(k => of(mul(5) (2))))))
(of(1)); // 9
const k1 = lift2(sub)
(reset
(lift2(add) (of(3))
(shift
(k => of(k(mul(5) (2)))))))
(of(1)); // 12
const k2 = lift2(sub)
(reset
(lift2(add) (of(3))
(shift
(k => of(k(k(mul(5) (2))))))))
(of(1)); // 15
console.log(k0(id));
console.log(k1(id));
console.log(k2(id));
it seems that reset/shift already meet the last two criteria, because delimited continuations are just first class, composable functions and I can invoke the continuation k as often as required.
To answer the why, I want to bypass the following limitation in connection with the list monad.
Are these assumptions correct?
Even if I haven't make any mistakes so far I am overwhelmed by the complexity of the task at this point. I have no clue how to implement the desired coroutine or even where to begin. I didn't found an example implementation either. I don't expect a complete implementation but maybe some guidance to achieve my goal.
Goal
I want to bypass the following limitation of coroutines implemented by Javascript's generators:
const arrMap = f => xs =>
xs.map(x => f(x));
const arrAp = fs => xs =>
fs.reduce((acc, f) =>
acc.concat(xs.map(x => f(x))), []);
const arrChain = xs => fm =>
xs.reduce((acc, x) => acc.concat(fm(x)), []);
const arrOf = x => [x];
const do_ = (of, chain) => it => {
const loop = ({done, value}) =>
done
? value
: chain(value) (x => loop(it.next(x)));
return loop(it.next());
};
const z = function*() {
const x = yield [1,2,3]
return [x, x];
}
console.log(
arrChain([1,2,3]) (x => [x, x]));
console.log(
do_(arrOf, arrChain) (z()));

function composition with rest operator, reducer and mapper

I'm following an article about Transducers in JavaScript, and in particular I have defined the following functions
const reducer = (acc, val) => acc.concat([val]);
const reduceWith = (reducer, seed, iterable) => {
let accumulation = seed;
for (const value of iterable) {
accumulation = reducer(accumulation, value);
}
return accumulation;
}
const map =
fn =>
reducer =>
(acc, val) => reducer(acc, fn(val));
const sumOf = (acc, val) => acc + val;
const power =
(base, exponent) => Math.pow(base, exponent);
const squares = map(x => power(x, 2));
const one2ten = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
res1 = reduceWith(squares(sumOf), 0, one2ten);
const divtwo = map(x => x / 2);
Now I want to define a composition operator
const more = (f, g) => (...args) => f(g(...args));
and I see that it is working in the following cases
res2 = reduceWith(more(squares,divtwo)(sumOf), 0, one2ten);
res3 = reduceWith(more(divtwo,squares)(sumOf), 0, one2ten);
which are equivalent to
res2 = reduceWith(squares(divtwo(sumOf)), 0, one2ten);
res3 = reduceWith(divtwo(squares(sumOf)), 0, one2ten);
The whole script is online.
I don't understand why I can't concatenate also the last function (sumOf) with the composition operator (more). Ideally I'd like to write
res2 = reduceWith(more(squares,divtwo,sumOf), 0, one2ten);
res3 = reduceWith(more(divtwo,squares,sumOf), 0, one2ten);
but it doesn't work.
Edit
It is clear that my initial attempt was wrong, but even if I define the composition as
const compose = (...fns) => x => fns.reduceRight((v, fn) => fn(v), x);
I still can't replace compose(divtwo,squares)(sumOf) with compose(divtwo,squares,sumOf)
Finally I've found a way to implement the composition that seems to work fine
const more = (f, ...g) => {
if (g.length === 0) return f;
if (g.length === 1) return f(g[0]);
return f(more(...g));
}
Better solution
Here it is another solution with a reducer and no recursion
const compose = (...fns) => (...x) => fns.reduceRight((v, fn) => fn(v), ...x);
const more = (...args) => compose(...args)();
usage:
res2 = reduceWith(more(squares,divtwo,sumOf), 0, one2ten);
res3 = reduceWith(more(divtwo,squares,sumOf), 0, one2ten);
full script online
Your more operates with only 2 functions. And the problem is here more(squares,divtwo)(sumOf) you execute a function, and here more(squares,divtwo, sumOf) you return a function which expects another call (fo example const f = more(squares,divtwo, sumOf); f(args)).
In order to have a variable number of composable functions you can define a different more for functions composition. Regular way of composing any number of functions is compose or pipe functions (the difference is arguments order: pipe takes functions left-to-right in execution order, compose - the opposite).
Regular way of defining pipe or compose:
const pipe = (...fns) => x => fns.reduce((v, fn) => fn(v), x);
const compose = (...fns) => x => fns.reduceRight((v, fn) => fn(v), x);
You can change x to (...args) to match your more definition.
Now you can execute any number of functions one by one:
const pipe = (...fns) => x => fns.reduce((v, fn) => fn(v), x);
const compose = (...fns) => x => fns.reduceRight((v, fn) => fn(v), x);
const inc = x => x + 1;
const triple = x => x * 3;
const log = x => { console.log(x); return x; } // log x, then return x for further processing
// left to right application
const pipe_ex = pipe(inc, log, triple, log)(10);
// right to left application
const compose_ex = compose(log, inc, log, triple)(10);
I still can't replace compose(divtwo,squares)(sumOf) with compose(divtwo,squares,sumOf)
Yes, they are not equivalent. And you shouldn't try anyway! Notice that divtwo and squares are transducers, while sumOf is a reducer. They have different types. Don't build a more function that mixes them up.
If you insist on using a dynamic number of transducers, put them in an array:
[divtwo, squares].reduceRight((t, r) => t(r), sumOf)

Categories