2

I can't figure out how to express fix as a tail recursive algorithm. Or is it already tail recursive? I am probably overthinking it...

const fix = f => x => f(fix(f)) (x); // redundant eta abstraction due to eager eval

const sum = fix(go => acc => ([x, ...xs]) =>
  x === undefined
    ? acc
    : go(acc + x) (xs)) (0);
    
const main = sum([1,2,3,4,5]);

console.log(main); // 15

I tried a lot but haven't come up with anything remotely useful yet. A tail recursive algorithm could be easily transformed to a stack safe loop. That is the actual goal.

duplode
  • 31,361
  • 7
  • 69
  • 130
scriptum
  • 3,839
  • 1
  • 12
  • 26
  • I'm going to leave this link here in case it makes any difference in what you're aiming for [JS tail call optimization](https://stackoverflow.com/questions/37224520/are-functions-in-javascript-tail-call-optimized) – Michael Bianconi Jan 23 '20 at 20:25
  • @MichaelBianconi doesn't matter if the JS environment supports TCO or not - any tail recursive call can be trivially turned into a loop, either using trampolines or just by hand. – VLAZ Jan 23 '20 at 20:27
  • All I can say is that this is not *currently* tail recursive, since the last execution is `f` not `fix`. I don't know if it's possible to make it tail recursive, though but I'd be interested to find out. – VLAZ Jan 23 '20 at 20:30
  • The more I think about it, the more it seems `fix` *should* be reducible to a loop. The entire thing just makes stack frames and then resolves them linearly. So, it seems like it should be easy to make it stack safe...however, I'm extremely tired and unable to figure out how that would work in practice. I'll have to check this tomorrow but hopefully it'd be solved by then. – VLAZ Jan 23 '20 at 20:39
  • do you want to change `sum` as well? – Nina Scholz Jan 23 '20 at 21:05
  • @NinaScholz `sum` should remain the same, because it already uses an accumulator. – scriptum Jan 24 '20 at 06:25
  • 1
    I got it. `fix` is a functionalised trampoline. The only difference is, that `fix` enables real anonymous recursion whereas an imperative trampoline only enables pseudo-recursion implemented as a loop. – scriptum Jan 24 '20 at 06:53
  • 1
    *Or is it already tail recursive?* No, it's not tail recursive. A simple counterexample is `fix(id)(0)`. If `fix` was tail recursive then it would go into a infinite loop. Instead, it results in a stack overflow. – Aadit M Shah Jan 24 '20 at 11:43
  • @Aadit Still no TCO in JS unless you use opera, i guess. – scriptum Jan 24 '20 at 11:57

1 Answers1

2

I can't figure out how to express fix as a tail recursive algorithm.

First, convert the definition of fix into A-normal form.

// fix :: ((a -> b) -> a -> b) -> a -> b
const fix = f => x => {
    const g = fix(f);
    const h = f(g);
    const y = h(x);
    return y;
};

Next, convert the resulting program into continuation-passing style.

// type Cont r a = (a -> r) -> r

// fix :: ((a -> Cont r b) -> Cont r (a -> Cont r b)) -> Cont r (a -> Cont r b)
const fix = f => k => k(x => k =>
    fix(f) (g =>
    f(g)   (h =>
    h(x)   (y =>
    k(y)))));

Now, fix is tail recursive. However, this is probably not what you want.

A tail recursive algorithm could be easily transformed to a stack safe loop. That is the actual goal.

If all you want is stack safety, then you could use the Trampoline monad.

// Bounce :: (a -> Trampoline b) -> a -> Trampoline b
const Bounce = func => (...args) => ({ bounce: true, func, args });

// Return :: a -> Trampoline a
const Return = value => ({ bounce: false, value });

// trampoline :: Trampoline a -> a
const trampoline = result => {
    while (result.bounce) result = result.func(...result.args);
    return result.value;
};

// fix :: ((a -> Trampoline b) -> a -> Trampoline b) -> a -> Trampoline b
const fix = f => Bounce(x => f(fix(f))(x));

// id :: a -> a
const id = x => x;

// reachable code
console.log("begin"); // open the console in your browser

// infinite loop
trampoline(fix(id)(0));

// unreachable code
console.log("end");

However, this would require that you change the definition of sum as well.

// Bounce :: (a -> Trampoline b) -> a -> Trampoline b
const Bounce = func => (...args) => ({ bounce: true, func, args });

// Return :: a -> Trampoline a
const Return = value => ({ bounce: false, value });

// trampoline :: Trampoline a -> a
const trampoline = result => {
    while (result.bounce) result = result.func(...result.args);
    return result.value;
};

// fix :: ((a -> Trampoline b) -> a -> Trampoline b) -> a -> Trampoline b
const fix = f => Bounce((...args) => f(fix(f))(...args));

// _sum :: (Number, [Number]) -> Trampoline Number
const _sum = fix(recur => (acc, xxs) => {
    if (xxs.length === 0) return Return(acc);
    const [x, ...xs] = xxs;
    return recur(acc + x, xs);
});

// sum :: [Number] -> Trampoline Number
const sum = xxs => _sum(0, xxs);

// result :: Number
const result = trampoline(sum([1,2,3,4,5]));

// 15
console.log(result);

Hope that helps.

Aadit M Shah
  • 67,342
  • 26
  • 146
  • 271
  • 1
    The intermediate step (A-normal form) during the cps transformation is quite helpful – scriptum Jan 27 '20 at 14:27
  • Why do we need monad recursion for `fix`? Isn't the `loop`/`Recur`/`Return` pattern enough? – scriptum Jan 27 '20 at 14:30
  • 1
    Sure you can. You'd define `fix` as `f => loop(f(Recur))`. However, this doesn't buy you much because you can use the `loop`/`Recur`/`Return` pattern directly. No need for `fix` at all. To be fair, you don't need the `Trampoline` version of `fix` either. You can use the `Trampoline` monad directly. However, it is less trivial than the `loop` version of `fix`. In addition, it's closer to your original definition of `fix`. I just applied `Bounce` to the inner lambda abstraction. The `loop` version of `fix` is completely different from your original definition of `fix`. – Aadit M Shah Jan 28 '20 at 03:05