Am I the only one who has considered the consequences of the Tail Call Optimization (TCO) implemented so well so many years ago (at least two years ) on which we are so dependent? As for most languages that transpile to a language that (didnât/doesnât) directly support TCO, Elm implements this as an âunder-the-coversâ conversion to a simple JavaScript loop, and of course only works for tail call recursion within the same function (which covers the majority of the use cases).
However, that implementation means the arguments of the recursive function call are internally mutable, and it turns out that it is quite simple to reveal that mutation externally! Consider the following example run on elm repl (version 0.19):
let loop n list = if n <= 0 then list else loop (n - 1) ((\() -> n) :: list) in List.map (\f -> f()) <| loop 3 []
which produces [0,0,0] : List number
indicating that the captured n
value has been mutated to the final value of zero for the three closures. If n
were not mutated, the code should produce [1,2,3] : List number
.
It is easy to obtain the desired output by the addition of a let
just before the creation of the closure in the âloopâ as follows:
let loop n list = if n <= 0 then list else loop (n - 1) ((let nn = n in \() -> nn) :: list) in List.map (\f -> f()) <| loop 3 []
where the internal let
creates a new immutable binding for the n
value (copy on create in the case of a primitive number
as here, but it will also work for reference heap values as it will capture the reference).
The problem is the capturing of the binding in a closure, as a list made using the value n
directly works as expected. The fix is likely to create the required let
invisibly when the captured value comes from a TCO. As currently Safari is the only major browser that implements JavaScript TCO, asking JavaScript to handle this is still not an option.
Iâm surprised that no one has caught this before now.
The question is where to file the issue?