Llaisdy
17 Apr 2024

Faking a recursive closure in elixir

Recursive closures do not seem to be available in elixir (or erlang). An example discussion is on the elixir forum here. However, I recently had need of such a facility.

Reading Knuth–Morris–Pratt illustrated (Cameron Moy, 2024, Journal of Functional Programming, 34, e3), I thought I would try to port the haskell implementations to elixir.

Here's an example haskell function (based on verticalList from the paper) with a recursive closure:

1: verticalList pattern text = any done (scanl step init text)
2:     where init :: [String]
3:           init = [pattern]
4:           step :: [String] -> Char -> [String]
5:           step [] x = init
6:           step acc@(t:r) x
7:             | check acc x = (tail t):(step r (head t))
8:             | otherwise   = step r x

Ancillary functions done and check are just:

 9: Done ("":_)  = True
10: done _       = False
11: 
12: check (h:_) x = isPrefixOf [x] h
13: check _ _     = False

That step function from lines 4-8 is a recursive closure:

This is not possible in elixir/erlang because the variable name to which the anonymous function is assigned is only defined after the anonymous function body has been parsed. Or something like that. The haskell compiler seems to manage it, …

Anyway, I simulated this recursive closure in elixir by:

 1: def verticalList(pattern, text) do
 2:   init = [pattern]
 3:   acc = init
 4: 
 5:   scan =
 6:     scanl(&step/2, {init, acc}, text)
 7:     |> Enum.map(fn {_, x} -> x end)
 8: 
 9:   Enum.any?(scan, &done/1)
10: end
11: 
12: def step({init, []}, _x) do
13:   {init, init}
14: end
15: 
16: def step({init, acc = [t | r]}, x) do
17:   case check(acc, x) do
18:     true ->
19:       [ht | tt] = t
20:       {_, acc2} = step({init, r}, ht)
21:       {init, [tt | acc2]}
22: 
23:     false ->
24:       {_, acc2} = step({init, r}, x)
25:       {init, acc2}
26:   end

Elixir doesn't have a built-in equivalent of haskell's scanl so that's added to the ancillary functions:

27: def scanl(_f, acc, []) do
28:   [acc]
29: end
30: 
31: def scanl(f, acc, [x | xt]) do
32:   acc1 = f.(acc, x)
33:   [acc | scanl(f, acc1, xt)]
34: end
35: 
36: def done([~c"" | _]), do: true
37: def done(_), do: false
38: 
39: def check([h | _], x) do
40:   List.starts_with?(h, [x])
41: end
42: 
43: def check(_, _), do: false

In erlang it's a bit less verbose, but otherwise similar:

 1: verticalList(Pattern, Text) ->
 2:     Init = [Pattern],
 3:     Acc = Init,
 4:     Scan = lists:map(fun({_,X}) -> X end,
 5:                      scanl(fun step/2, {Init, Acc}, Text)),
 6:     lists:any(fun done/1, Scan).
 7: 
 8: step({Init, []}, _) ->
 9:     {Init, Init};
10: step({Init, Acc = [T|R]}, X) ->
11:     case check(Acc, X) of
12:         true ->
13:             [HT|TT] = T,
14:             {_, Acc2} = step({Init, R}, HT),
15:             {Init, [TT | Acc2]};
16:         false ->
17:             {_, Acc2} = step({Init, R}, X),
18:             {Init, Acc2}
19:     end.
20: 
21: scanl(_F, Acc, []) ->
22:     [Acc];
23: scanl(F, Acc, [X|Xt]) ->
24:     [Acc | scanl(F, (F(Acc, X)), Xt)].
25: 
26: done([[]|_]) -> true;
27: done(_)      -> false.
28: 
29: check([H|_], X) ->
30:     string:prefix(H, [X]) /= nomatch;
31: check(_, _) ->
32:     false.

Is there a better way to do this?

Tags: elixir erlang haskell