Tutorial — Permutations and Recursions in Elm

Demo: https://lucamug.github.io/elm-tutorial-permutations-and-recursion/ (inspect the console for more insights)

Suppose we have a list of list of elements. Let’s build a function to create all possible combinations of these elements. respecting their order.

For example, if this is the input:

[ [ “1”, “2” ], [ “a”, “b” ] ]

This should be the output:

[ [ “1”, “a” ], [ “1”, “b” ], [ “2”, “a” ], [ “2”, “b” ] ]

The function should work with any quantity of elements, for example:

[ [] ]
[ [ “1” ], [ "a" ], [ "x" ] ]
[ [ “1”, "2" ], [ "a", "b" ], [ "x", "y", "z" ], [ "@", "#" ] ]

The type signature of the function should be

List ( List a ) → List ( List a )

How to address the problem?

We could go trough each inner element and we add these element to an accumulator that initially is an empty list.

Let’s suppose we are working with this input:

[ [ “1”, “2” ], [ “a”, “b” ] ]

1) We take the first element of the list (the head): [ “1”, “2” ]

2) We take the first element of this list and we add to the accumulator, that was empty. The accumulator is now [ “1” ].

3) We take the second element of the list

4) We take the first element of this list and we add to the accumulator. The accumulator is now [ “1”, “a” ]

5) There is no third element in the list, so the accumulator is now one of the permutation.

6) We remove the last item added to the accumulator. The accumulator is now back to [ “1” ]

7) We move to the second element of the second list. Now the accumulator is [ “1”, “b” ]

The process can go ahead this way for many steps but some of these steps seems repeating.

At this point recursion seem a natural way to simplify and generalise this algorithm.

Let’s start from the final step in the chain and let’s move backwards.

How do we detect that we reached the last list? At each step we remove one element from the main list and when the list is empty, it means that we reached the end. To check if we are at the last step we can write

generateCombinations1 list =
case Debug.log "head is" (List.head list) of
Nothing ->
-- We reached the end
[]
        Just head ->
-- Still stuff to process
[]

Running the function with

generateCombinations2 [ [ “a”, “b” ], [ “1”, “2” ] ]

the console output is:

head is: Just [“a”,”b”]

Now we need to repeat this step one more time.

We extract the remaining part of the main list, using List.tail, and call the same function again. First lets extract the tail and output some info into the console.

generateCombinations2 list =
case Debug.log "head is" (List.head list) of
Nothing ->
[]
        Just head ->
let
tail =
case Debug.log "tail is" (List.tail list) of
Just data ->
data
                        Nothing ->
[]
in
[]

The console says

head is: Just [“a”,”b”]
tail is: Just [[“1”,”2"]]

Good, we got the head and the tail, as expected.

Lets try calling the function with the tail and see what happen

generateCombinations3 list =
case Debug.log "head is" (List.head list) of
Nothing ->
[]
        Just head ->
let
tail =
case Debug.log "tail is" (List.tail list) of
Just data ->
data
                        Nothing ->
[]
in
generateCombinations3 tail

The console:

head is: Just [“a”,”b”]
tail is: Just [[“1”,”2"]]
head is: Just [“1”,”2"]
tail is: Just []
head is: Nothing

Nice! Now we need to add an accumulator so that that we can build the output. For the moment lets just add a list and add some test data to it. Also let’s return the accumulator in case the List.head is Nothing.

generateCombinations4 acc list =
let
_ =
Debug.log "acc is" acc
in
case Debug.log "head is" (List.head list) of
Nothing ->
[ acc ]
            Just head ->
let
tail =
case Debug.log "tail is" (List.tail list) of
Just data ->
data
                            Nothing ->
[]
in
generateCombinations4 ("test" :: acc) tail

The console:

acc is: []
head is: Just [“a”,”b”]
tail is: Just [[“1”,”2"]]
acc is: [“test”]
head is: Just [“1”,”2"]
tail is: Just []
acc is: [“test”,”test”]
head is: Nothing

Our accumulator is getting populated.

The output is now [[“test”,”test”]]

Now we want to add meaningful stuff in the accumulator instead of “test”. Lets loop all the items in the head and add to the accumulator one by one.

So instead just adding “test” as per

generateCombinations4 (“test” :: acc) tail

We do

List.map (\item -> generateCombinations4 (item :: acc) tail ) head

This doesn’t work. Elm compiler is complaining that:

The 1st branch has this type:

List (List a)
But the 2nd is:

List (List (List a))

The first branch is returning the proper type but there is something wrong with the second branch. Too many nested lists. Elm need all branches to be of the same type. The solution is to use List.concat to reduce the nesting level of the List. This will work:

generateCombinations5 acc list =
let
_ =
Debug.log "acc is" acc
in
case Debug.log "head is" (List.head list) of
Nothing ->
[ acc ]
            Just head ->
let
tail =
case Debug.log "tail is" (List.tail list) of
Just data ->
data
                            Nothing ->
[]
in
List.concat
(List.map
(\item ->
generateCombinations5
(item :: acc)
(tail)
)
head
)

Note that without the type signature at the top and without List.concat, Elm compiler is returning this error. I spent quite a lot of time trying to understand where the problem was.

 — INFINITE TYPE — — — — — — — — — — — — — — — — — — test.elm
I am inferring a weird self-referential type for `generateCombinations5`
89| generateCombinations5 acc list =
^^^^^^^^^^^^^^^^^^^^^
Here is my best effort at writing down the type. You will see ? and ∞ for parts of the type that repeat something already printed out infinitely.
List ? -> List (List ?) -> ?
Usually staring at the type is not so helpful in these cases, so definitely read the debugging hints for ideas on how to figure this out: <https://github.com/elm-lang/elm-compiler/blob/0.18.0/hints/infinite-type.md>

We are almost there. Looking at the output we note that the order of items is reversed:

[[“1”,”a”],[“2”,”a”],[“1”,”b”],[“2”,”b”]]

This is because we are adding elements from the left of the list with the operator ::.

We can solve this with List.reverse before returning the list.

The final function is:

generateCombinations : List a -> List (List a) -> List (List a)
generateCombinations acc list =
case List.head list of
Nothing ->
[ List.reverse acc ]
        Just head ->
let
tail =
case List.tail list of
Just data ->
data
                        Nothing ->
[]
in
List.concat
(List.map
(\item ->
generateCombinations
(item :: acc)
(tail)
)
head
)

Note that the branch where the List.tail is nothing and we return [] will never happen. List.tail return Nothing only when the list is empty. But an empty list is intercepted in the branch where we check if the List.head is Nothing.

Nevertheless Elm force us to return something so we return an empty list just to be consistent with the other branches.

Another note: there is an edge case where the input is [] and the output is [[]]. Probably this should be considered a special case but is beautiful to see how Elm is handling also this case without complaining.

The reason why there are two nested list is because the function goes directly to the first branch of the first case and that return [ acc ] that is [[]].

The case where the input instead is [[]] go through the third branch and return (List.concat []) that is equal to [].

Do you have any suggestion how this problem can be solved more efficiently? Let me know in the comments below.

Demo: https://lucamug.github.io/elm-tutorial-permutations-and-recursion/ (inspect the console for more insights)

Code: https://github.com/lucamug/elm-tutorial-permutations-and-recursion

This script is used in