recursionfunctional-programmingelmfoldmultiway-tree

Elm - fold on recursive type with list does not compile


I'm following this article on catamorphism and I'm trying to define a fold function for a recursive data type like this

type Node anyType
    = Leaf Id (Maybe anyType)
    | Tree Id (List (Node anyType))

What i wrote is this:

foldTree fLeaf fTree acc node =
let
    recurse =
        foldTree fLeaf fTree
in
case node of
    Leaf id v -> 
        let
            newAcc = fLeaf acc (id, v)
        in
            newAcc

    Tree id l ->
        let
            newAcc = fTree acc id
            
                
        in
            l |> List.foldl recurse newAcc 

If i don't infer the types for foldTree the function compiles, but it seems to not be usable:

collectIds node = 
let
    fLeaf acc (id,v) = id :: acc
    fTree acc id = id :: acc
in

foldTree fLeaf fTree [] node

throws the following:

TYPE MISMATCH - The 1st argument to `foldTree` is not what I expect:

173|     foldTree fLeaf fTree [] node
              #^^^^^#
This `fLeaf` value is a:

#List a# -> ( a, b ) -> #List a#

But `foldTree` needs the 1st argument to be:

#Node anyType# -> ( Id, Maybe anyType ) -> #Node anyType#

Auto inferring the types for foldTree makes it not compilable and throws the following:

-- Auto Inferred
foldTree : (c -> (Id, Maybe anyType) -> a) -> (c -> Id -> b) -> c -> Node anyType -> d

TYPE MISMATCH - Something is off with the 1st branch of this `case` expression:

126|                 newAcc
                     #^^^^^^#
This `newAcc` value is a:
#a#

But the type annotation on `foldTree` says it should be:

    #d#

#Hint#: Your type annotation uses `a` and `d` as separate type variables. Your
code seems to be saying they are the same though. Maybe they should be the same
in your type annotation? Maybe your code uses them in a weird way?

and if I try to follow the hint, still does not compile

foldTree : (c -> (Id, Maybe anyType) -> a) -> (c -> Id -> b) -> c -> Node anyType -> a

TYPE MISMATCH - This function cannot handle the argument sent through the (|>) pipe:

134|                 l |> List.foldl recurse newAcc 
                          #^^^^^^^^^^^^^^^^^^^^^^^^^#
The argument is:

    List #(Node anyType)#

But (|>) is piping it to a function that expects:

    List #c#

#Hint#: Your type annotation uses type variable `c` which means ANY type of value
can flow through, but your code is saying it specifically wants a `Node` value.
Maybe change your type annotation to be more specific? Maybe change the code to
be more general?

Read <https://elm-lang.org/0.19.1/type-annotations> for more advice!Elm
TYPE MISMATCH - The 1st argument to `foldl` is not what I expect:

134|                 l |> List.foldl recurse newAcc 
                                     #^^^^^^^#
This `recurse` value is a:

    c -> Node anyType -> #a#

But `foldl` needs the 1st argument to be:

    c -> Node anyType -> #Node anyType#

#Hint#: Your type annotation uses type variable `a` which means ANY type of value
can flow through, but your code is saying it specifically wants a `Node` value.
Maybe change your type annotation to be more specific? Maybe change the code to
be more general?

I'm stuck. Following the types on the article precisely also does not seem to work. I understand that the code on the article is F# and I'm working on Elm but I thought that in this case it would have been 100% translatable.

Where am I going wrong?

Thanks in advance!


Solution

  • You've flipped the arguments to List.foldl. The fold function takes the value first and the accumulator second, while your recurse function takes the accumulator first and the value second.

    The simple fix to this is to eta expand the recurse function and flip the arguments when passing it to foldTree:

    recurse v a = foldTree fLeaf fTree a v
    

    Also, interestingly, annotating the type of recurse will make it compile, but obviously produces the wrong result. I didn't pursue this further to understand why, as it was wrong, but the lesson you should take from this is to always annotate your top level functions. This will give you better error messages, but also prevents your code from accidentally compiling but producing the wrong result.