33
Early termination in functional folds a.k.a. reduce
Preface: This post is based on a dynamically typed version of Javascript called scriptum, i.e. vanilla Javascript with explicit type annotations.
In imperative programming special constructs like break
are used to programmatically terminate a loop before the underlying data structure is exhausted.
The functional counterpart of loops is recursion, but since recursion is a functional primitive we try to avoid it using folds as a more appropriate abstraction.
In lazy evaluated languages the special fold scanl
, which stores all intermediate results of a computation, suffices. But in eagerly evaluated Javascript we must use another approach that includes local continuations:
const foldk = fun(
f => init => xs => {
let acc = init;
for (let i = 0; i < xs.length; i++)
acc = f(acc) (xs[i]).run(id);
return acc;
},
"(b => a => Cont<b, b>) => b => [a] => b");
foldk
looks pretty convoluted, but the type annotation alleviates the cognitive load:
"(b => a => Cont<b, b>) => b => [a] => b"
// ^^^^^^^^^^^^^^^^^^^^^^ ^ ^^^ ^
// | | | |
// 2-argument-function b-value array-of-a-values b-value
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ => ^^^^^^^^^^
// | |
// arguments result
It takes three arguments, a binary function, a value, an array of values and returns a value. a
and b
are placeholders for values of optionally different types.
We haven't discussed the most complicated part of the type though. The binary function b => a => Cont<b, b>
returns a continuation. Luckily this is the only place where continuations appear, that is to say we only need to wrap the result of our binary function into Cont
. This doesn't sound too hard.
So what is a continuation? Nothing more than a (partially applied) function with a function argument as its last formal parameter. So inck
is not a continuaion, but inck(2)
is:
const inck = n => k => k(n + 1);
// ^^^^^^^^^^^^^
// |
// continuation
const continuation = inck(2);
continuation(x => x); // 3
With scriptum we don't use the bare continuation but put it into a type wrapper Cont(k => k(n + 1))
. In order to access the continuation inside the wrapper, scriptum supplies the .run
method.
Now that we clarified this let's come back to the original task to terminate from a fold programatically in order to see how foldk
is applied in practice:
foldk(fun(
x => s => Cont(fun(
k => x >= 5
? x // A
: k(x + s.length), // B
"(Number => Number) => Number")),
"Number => String => Cont<Number, Number>"))
(0) (["f","fo","foo","fooo","foooo"]); // 6
In line B
we call the continuation k
, i.e. the folding proceeds as usual. In line A
, however, we just return the intermediate result without calling k
. The folding is short circuited. The above computation calculates "f".length + "fo".length + "foo".length
and then terminates the program due to the programatic reason that x >= 5
yields true
.
So far we haven't leveraged scriptum's runtime type system. We will use the ANNO
symbol to access the intermediate types of each function application:
foldk[ANNO]; // (b => a => Cont<b, b>) => b => [a] => b
result = foldk(fun(
x => s => Cont(fun(
k => x >= 5
? x // A
: k(x + s.length), // B
"(Number => Number) => Number")),
"Number => String => Cont<Number, Number>"));
result[ANNO]; // Number => [String] => Number
result = result(0)
result[ANNO]; // [String] => Number
result(["f","fo","foo","fooo","foooo"]); // 6
Hopefully, this little sketch gives a first insight how thinking in FP looks like and how type annotation can assist us in finding reliable solutions.
scriptum is published on Github.
33