Efficient tail-recursion over custom type

Hi,

Recently I was trying to make this function tail recursive:

flatten : Doc -> Doc
flatten doc =
    case doc of
        Concatenate doc1 doc2 ->
            Concatenate (flatten doc1) (flatten doc2)

        Nest i doc1 ->
            Nest i (flatten doc1)

        ... -- More cases but they were already tail recursive.

To achieve this I introduced lambdas into the custom type:

type Doc
    = Empty
    | Concatenate (() -> Doc) (() -> Doc)
    | Nest Int (() -> Doc)
    ... -- More constructors

flatten : Doc -> Doc
flatten doc =
    case doc of
        Concatenate doc1 doc2 ->
            Concatenate (\() -> flatten (doc1 ())) (\() -> flatten (doc2 ()))

        Nest i doc1 ->
            Nest i (\() -> flatten (doc1 ()))

        ... -- More cases but they were already tail recursive.

But it occurs to me there is another way of doing this, which is to add a new constructor to the custom type called Flatten which stands for ‘flatten this later’. Code is going to look like:

type Doc
    = Empty
    | Concatenate Doc Doc
    | Nest Int Doc
    | Flatten Doc -- Flatten the doc later.
    ... -- More constructors

flatten : Doc -> Doc
flatten doc =
    case doc of
        Concatenate doc1 doc2 ->
            Concatenate (Flatten doc1) (Flatten doc2)

        Nest i doc1 ->
            Nest i (Flatten doc1)

        ... -- More cases but they were already tail recursive.

In other code that uses this custom type, whenever a Flatten is encountered, its value can be extracted by passing it through the flatten function.

case someDoc of
    Flatten doc -> doStuff (flatten doc)

My question is, is this generally going to be faster than using a lambda to make the calculation lazy? I suppose the only real answer is to benchmark and compare - but interested to see if anyone has applied this technique previously with good effect.

There is a comment on the old elm-trampoline package, which makes me think using the lambdas is likely to be slower:

https://github.com/elm-lang/trampoline - “This strategy creates intermediate closures, which is very expensive in JavaScript, so use this library only when it is essential that you recurse deeply.”

Wrapping in a constructor to make it lazy isn’t going to create a closure is it? It will keep the arguments to be passed to flatten on the heap instead?

The disadvantage of this technique is that I need to add a new constructor to the type for each function I want to lazily apply - fine in this case as I only have the flatten function or possibly a couple more.

1 Like

Actual package source is here: https://github.com/the-sett/elm-pretty-printer/blob/master/src/Pretty.elm#L447

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.