Let’s try and explain how we can arrive at that implementation using intuitions and some minor perusal of the API for List and Random!
Step 1
Realize we want to “transform” a list of values, into some other thing. If the “other thing” is a new List
, you would generally use List.map
. Let’s assume we don’t really want to change the generators themselves, so let’s start with that:
combine generators =
List.map identity generators
Step 2
Hm, the expected result isn’t “just” a List
, though, which is all that List.map
is capable of generating. We do have a slightly more flexible function in our arsenal, though - folding. Whichever way we fold, we can use it to create something else from a list by using a fold. Since our final type does have List
in its result type, and we want the generators to be in the same order as we feed them in, let’s start by rewriting the previous attempt using a fold:
combine generators =
List.foldr (\item acc -> item :: acc) [] generators
Oh, that lambda looks like it’s just the definition of the cons operator, so let’s change that slightly. Generally being super concise isn’t worth the trouble, but I feel like this is a pretty acceptable use of partial application for infix operators:
combine generators =
List.foldr (::) [] generators
Step 3
Okay, the type signature doesn’t match up - we’re returning a List
rather than a Generator (List a)
. So, in our fold, rather than adding to a List, we want to add to a Generator (List a)
.
I generally find it helpful to get the “initial accumulator” figured out before figuring out the function to apply to it. In this case, we want the final result to be a Generator (List a)
so we want out initial accumulator to be of that type.
So, how can we create an Generator for an empty list as our initial accumulator? Ah, there’s a constant
function. Cool, let’s try that. Since we’re now working on getting the type signature to work out, let’s add that in, too:
combine : List (Generator a) -> Generator (List a)
combine generators =
List.foldr (::) (Random.constant []) generators
Step 4
So, we have a type mismatch for our accumulating function. We’re giving it a function a -> List a -> List a
but it’s expecting a function Generator a -> Generator (List a) -> Generator (List a)
. So the goal is to find a way to change that first one into that second one.
When looking for a function, it helps to go look for a function that has a generalized form of the specific thing you’re attempting to do.
In the most general terms, we’re looking for something that takes a a -> b -> c
and gives us a Generator a -> Generator b -> Generator c
.
Turns out there’s a Random.map2 : (a -> b -> c) -> (Generator a -> Generator b -> Generator c)
. Lucky us!
combine : List (Generator a) -> Generator (List a)
combine generators =
List.foldr (Random.map2 (::)) (Random.constant []) generators
Granted, that explanation isn’t great for someone with absolutely zero experience with FP. However, I think it’s possible to explain it using some of the intuitions that people build up while working with functional programming, which to me seems a bunch simpler than saying “it’s monad transformation”. Of course, different people learn in different ways, so your mileage may vary!