Any easy way to do monad transformation in Elm?

I was working with elm-test lately and had to do something similar to monad transformation while building generators for my property tests. It looks like this:

fields : List (Generator String)

combinedFields : Generator (List String)
combinedFields = List.foldr (\x acc -> Random.andMap acc (Random.map (::) x)) (Random.constant []) fields

As you can see, it transforms List (Generator String) to Generator (List String), and the code is a disastrous mess. I think there must be a better way to do this kind of thing - can someone tell me how to do that?

combine : List (Generator a) -> Generator (List a)
combine generators =
    List.foldr (Random.map2 (::))
        (Random.constant [])
        generators

I don’t think this is a disastrous mess. It’s explicit and only works for Generators and Lists, but that’s pretty typical in Elm: write the code you need. That said, I’m fairly certain that packages which already define a Random.andMap will typically also have a combine. For example http://package.elm-lang.org/packages/elm-community/random-extra/2.0.0/Random-Extra#combine

Random.Pcg doesn’t appear to come with that included, however a similar random-extra package exists that does offer that function.

Given the above function, whether you define it yourself or get it from a package, your example becomes this: combinedFields = combine fields

I think the takeaway it that composing more complex definitions out of simpler ones leads to more reusability and more maintainable code. This might not answer the question in the topic, but I do feel like it offers a decent answer to the question in the post itself: how do I combine a list of generators into a generator for a list.

2 Likes

We’re really big on avoiding the XY problem, so I think the best way to pursue a better way to write these generators would be to look at the generators themselves.

Could you post the code for some of the generators? (As in, some of the logic which is currently calling combinedFields.)

There’s also Task.sequence you can look at for a recursive implementation.

@ilias
I’ve looked into a few combine functions as per your suggestion and they all seem to use similar pattern. I was hoping for something that looks less daunting because I would have to explain how that function works to someone with zero experience in functional programming, but I guess it will have to do. Thank you very much.

@rtfeldman
You are somewhat correct in this being a case of XY problem. I was generating Json.Encode.Value for a dictionary of records to test a Json.Decode.Decoder and the generator was like 40 lines long, so in the end I scrapped the entire section for a simpler approach and it works great. By the time I was asking this question, I was genuinely curious about how to express monadic transformation in a simpler way.

ericgj
That recursive implementation is quite interesting! Thank you for pointing me toward it.

A common thing I’ve noticed for folks coming to Elm from Haskell is that in many cases, trying to do things in a familiar Haskelly way leads to a lot of frustration.

There are a great many design decisions where Haskell went one way and Elm very intentionally went another, and it often takes a conscious mindset shift to avoid reaching for familiar-but-inapplicable approaches out of habit!

1 Like

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!

5 Likes