How do you guys deal with monads?

For those familiar with monads…

I understand why Elm doesn’t have an infix bind or a do notation because 1. it wouldn’t even work because Elm doesn’t support that level of polymorphism because 2. it’s a level of complexity which scares newbies off, which is very opposed to Elm’s design philosophy. With that said, any function I’m writing which has multiple points of failure gets really ugly really fast without do notation. I don’t love the idea of using elm-do since my code will become unreadable to other Elm users. I’ve also tried a couple other things that don’t work with elm-fmt very well. This is a big enough deal for me that it was the tipping point for me to stop using elm-fmt- though I guess this was the straw that broke the camels back. I get that long files don’t really matter but case statements test me. This is the solution I like the most, transforming this (ignore the details, just focus on formatting)-

extractTag : String -> Result Parser.Problem ( Tag, String )
extractTag str =
    Parser.run openTagParser str
        |> Result.andThen 
            (\( name, attributes, afterOpenTag ) ->
                Parser.run (insideStringToParser name) ("<" ++ name ++ ">" ++ afterOpenTag)
                    |> Result.map (\( inside, rest ) ->
                        ( { name = name, attributes = attributes, inside = inside }, rest )
                    )
            )

Into this-

extractTag : String -> Result Parser.Problem ( Tag, String )
extractTag str =
    Parser.run openTagParser str
    |> Result.andThen <| 
    \( name, attributes, afterOpenTag ) ->

    Parser.run (insideStringToParser name) ("<" ++ name ++ ">" ++ afterOpenTag)
    |> Result.map <| 
    \( inside, rest ) ->

    ( { name = name, attributes = attributes, inside = inside }, rest )

I like this solution because whenever we have |> M.[andThen/map] <| we’re essentially saying "bind what’s above to what’s below as if it weren’t an M.

What do you guys think? Do you think it makes more sense to just make more function definitions to reduce nesting? Do you just use elm-do? How do you go about it? Do you have any better solutions?

1 Like

For the curious among you, this is a function I’m using in an HTML parsing library I’m writing.

Want to start out with this is just my opinion and I always prefer elm-format because I’d rather spend my time writing features than debating formatting. However given that you’ve specifically asked about formatting I’ll give my opinion if I wanted the choice.


I stopped reading at |> Result.andThen <| because I don’t know the order of operations and I wouldn’t want to have to memorize it. Either all |> or all <| but I really dislike mixing them.

When I saw

extractTag : String -> Result Parser.Problem ( Tag, String )
extractTag str =
    Parser.run openTagParser str
        |> Result.andThen 
            (\( name, attributes, afterOpenTag ) ->
                Parser.run (insideStringToParser name) ("<" ++ name ++ ">" ++ afterOpenTag)
                    |> Result.map (\( inside, rest ) ->
                        ( { name = name, attributes = attributes, inside = inside }, rest )
                    )
            )

my first thought was to do

extractTag : String -> Result Parser.Problem ( Tag, String )
extractTag str =
    Parser.run openTagParser str
        |> Result.andThen parseExtractTagResult


parseExtractTagResult : ( String, ???, String ) -> Result Parser.Problem ( Tag, String )
parseExtractTagResult ( name, attributes, afterOpenTag ) =
    Parser.run (insideStringToParser name) ("<" ++ name ++ ">" ++ afterOpenTag)
        |> Result.map
            (\( inside, rest ) ->
                ( { name = name, attributes = attributes, inside = inside }, rest )
            )

Naming aside, as I don’t know the full context here, this to me is already significantly easier to read and maintain because I’m only looking at small chunks of parsers at a time. It also still fits with elm-format, and there’s 0 thinking about what the order of operations is.

6 Likes

The compiler does not like it either :slight_smile:

-- INFIX PROBLEM ----------------------------------------------------------------

You cannot mix (|>) and (<|) without parentheses.

62|>    Parser.run (insideStringToParser name) ("<" ++ name ++ ">" ++ afterOpenTag)
63|>    |> Result.map <| 
64|>    \( inside, rest ) ->
65|>
66|>    ( { name = name, attributes = attributes, inside = inside }, rest )

I do not know how to group these expressions. Add parentheses for me!

In my opinion, the first code snippet posted is fine as-is.

I break it up into more functions when it gets even more nested or too long.

2 Likes

I hesitate to post because I may be off the marks, I’m still learning this stuff, but here goes.

At the moment, in my mind, andThen = bind = “monadic pattern”.

I was working on a little exercise lately, where I wanted to compute a list of results, to return either a failure, or a list of as (the computation should stop on first failure)

compute : List (Result error a) -> Result error (List a)

My first iteration looked like this:

compute = List.foldr (\ra rb -> ra |> Result.andThen (\x -> rb |> Result.map (\y -> x :: y))) (Ok [])

My second iteration looked like this:

thenApply f ra rb =
    ra |> Result.andThen (\x -> rb |> Result.map (\y -> f x y))

compute = List.foldr (thenApply (::)) (Ok [])

I then realized I had actually re-implemented map2!

So my third iteration was just:

compute = List.foldr (Result.map2 (::)) (Ok [])

So to sum up, I registered in my brain that andThen followed by map = map2. Not totally sure if it’s applicable to your situation though.

What I found interesting is that I first worked on this programming problem in another language, that provided less constraints and other code organization techniques. And implementation n°1 seemed fine.

So to sum up, I don’t have any definitive conclusions :slight_smile:

If you feel discomfort regarding ugly code, maybe you missed some patterns, some other ways to organize your code better (in Elm)

3 Likes

As lydell pointed out, apparently it doesn’t even compile! My IDE wasn’t type checking apparently because I wasn’t getting any errors. I don’t see why it doesn’t compile though, since <| and |> should just be right associative with the same precedence, and I don’t think that’s too much mental overhead.

The thing is, if nothing here returns a result then the code is short, understandable, and easy to write. As soon as we introduce errors (which you should absolutely do) it becomes long, confusing, and formats horribly. I think elm-format generally is good but has a lot of bad rules, and this is one of them.

I think it would make sense for elm to implement a polymorphic bind operator since it’s pretty easy to describe- just replace >>= with |> Result.andThen if you’re confused. |>= might make more sense because it takes a value rather than composing functions, but I digress.

One of the greatest things about FP is the ability not to name nearly as much stuff and still have perfectly legible code, so it would suck if this was the only sensible way to do it in Elm.

1 Like

Thanks for checking that my code doesn’t work, I wrote this before I went to bed so I hadn’t run it yet. Shame, I was hoping this was a viable solution.

Is the first code snippet not too long/nested? I wrote the code and even I can barely understand it.

Yup, the only difference is bind is usually infix, so the following 2 are the same

value |> Result.andThen (\x -> ...)
value >>= (\x -> ...)

So if we had a bind operator in Elm (which I think is within the limit of complexity of Elm- I don’t think it’s any harder to understand than andThen) we could clean up my example like so- first I’m gonna change the map to and andThen (you’ll see why in a second)

extractTag : String -> Result Parser.Problem ( Tag, String )
extractTag str =
    Parser.run openTagParser str
        |> Result.andThen 
            (\( name, attributes, afterOpenTag ) ->
                Parser.run (insideStringToParser name) ("<" ++ name ++ ">" ++ afterOpenTag)
                    |> Result.andThen (\( inside, rest ) ->
                        Ok ( { name = name, attributes = attributes, inside = inside }, rest )
                    )
            )

so that could become

extractTag : String -> Result Parser.Problem ( Tag, String )
extractTag str =
    Parser.run openTagParser str
        >>= (\( name, attributes, afterOpenTag ) ->
            Parser.run (insideStringToParser name) ("<" ++ name ++ ">" ++ afterOpenTag)
                >>= (\( inside, rest ) ->
                        Ok ( { name = name, attributes = attributes, inside = inside }, rest )
                    )
            )

And since the lambdas are the last thing within the brackets they’re in, we can remove them,

extractTag : String -> Result Parser.Problem ( Tag, String )
extractTag str =
    Parser.run openTagParser str
        >>= \( name, attributes, afterOpenTag ) ->
            Parser.run (insideStringToParser name) ("<" ++ name ++ ">" ++ afterOpenTag)
                >>= \( inside, rest ) ->
                        Ok ( { name = name, attributes = attributes, inside = inside }, rest )

and if we change the formatting a bit, we get

extractTag : String -> Result Parser.Problem ( Tag, String )
extractTag str =
    Parser.run ... ... >>= \( name, attributes, afterOpenTag ) ->
    Parser.run (...) (...) >>= \( inside, rest ) ->
    Ok ( { name = name, attributes = attributes, inside = inside }, rest )

now the first 2 lines are kinda saying "evaluate Parser.run (...) (...) and then let ( name, attributes, afterOpenTag ) / ( inside, rest ) be the value only if it’s a success. But it would be nice if it looked something like valueIfOk = functionThatReturnsResult args, which is what the do notation in Haskell does-

extractTag : String -> Result Parser.Problem ( Tag, String )
extractTag str = do
    ( name, attributes, afterOpenTag ) <- Parser.run openTagParser str
    ( inside, rest ) <- Parser.run (insideStringToParser name) ("<" ++ name ++ ">" ++ afterOpenTag)
    Ok ( { name = name, attributes = attributes, inside = inside }, rest )

But what we have no looks surprisingly imperative, and in light of that, Haskell would let us write return instead of Ok

extractTag : String -> Result Parser.Problem ( Tag, String )
extractTag str = do
    ( name, attributes, afterOpenTag ) <- Parser.run openTagParser str
    ( inside, rest ) <- Parser.run (insideStringToParser name) ("<" ++ name ++ ">" ++ afterOpenTag)
    return ( { name = name, attributes = attributes, inside = inside }, rest )

Now there’s a lot of hidden complexity here, but you would have a hard time arguing that this is more difficult to comprehend than the code we started with. Even the example with >>= and without do notation is way easier to write / understand if you learn what >>= does (which as I said in a previous reply, you can just replace it with |> Result.andThen and it will mean the same thing.

They’re not equal, since you can’t use the value from the value fed into andThen in map, which is what I needed.

Well that’s why I’m here! Though this is the most famous pattern in FP. Because it makes functions where there’s a bunch of [Result / Maybe / Parser / List]s way easier to read and type. Well, that’s not the actual reason. The actual reason Monads are as big as they are is because they allow things like IO to be segmented nicely from the rest of the code- but it’s the bind that makes it nice to work with! That’s why Haskell’s logo is the bind operator! Because it makes Monads (Result / Maybe / Parser / List / IO) much nicer to deal with. It’s a really powerful and ubiquitous pattern, which is why I think Elm should have better support for it, even at the cost of a tiny amount of complexity.

1 Like

In some limited way, yes. But if we don’t have polymorphism over monad type it’s very hard to talk about “nomadic” anything. It’s like calling array iterable, but that array can have only one element.

1 Like

<| and |> should just be right associative with the same precedence, and I don’t think that’s too much mental overhead.

And yet I said that I, upon first looking at it, was unable to determine what the order of operations should be. I read and write Elm about 60+ hrs a week (likely more than that) and it still wasn’t readily apparent to me what should happen there.

One of the greatest things about FP is the ability not to name nearly as much stuff and still have perfectly legible code, so it would suck if this was the only sensible way to do it in Elm.

I’m completely onboard for not naming things. I even write my own toy concatenative languages because I find joy in having no names at all, including naming variables! However when it comes to Elm I don’t see any of that being a target. I’d say news/the-syntax-cliff is a great example of what Elm is targeting. Elm doesn’t care about FP or whether things are named or not, what it cares about is the joy of everyone being included and everyone being able to create. That you are able to understand the existing syntax and other aren’t says to me that if any change is to be made, it should be made in a way that makes things easier not harder.

2 Likes

I mean it would be odd if they had different precedent, since they’re symmetrical, and it would be weird if they were left associative since operators never are. I would agree that this would be too much technical jargon for a new comer but you have to understand this to use the most basic operator in Elm- (::).

I would love to try a concatenative language! I still have a few languages I’d like to try before that though (APL or J or BQN → Racket or Clojure → Furthark). Other paradigms interest me a lot! (Kinda how I ended up here haha)

Sure but Elm introduces some complex things because they’re too damn useful. You can write polymorphic functions in Elm, and I would say that’s a harder (though probably more common) concept than Monads in FP. I think it’s more about the tradeoff between complexity and usefulness. Evan clearly thinks bind / related ideas are too complex for most people and that’s fine, but Monads are everywhere in FP and I would really like better support for them as much as I know / sympathize with the fact that it’s not gonna happen.

I should probably make my own teaching language as a test to see if I can teach Monads as easily as I think I can. Posts teaching Monads in Elm are actually a lot better than in Haskell, so I think the complexity is more about community than language design.

1 Like

I’m with you, I’ve dabbled with a bit of Haskell and I do find that pattern beautiful, although my understanding is only superficial at the moment.

But if I understand things correctly, Elm restricts the programmer by design because constraints can have positive effects on code organization. And regarding the other complaint which is too much boilerplate due to not enough polymorphism, the answer is codegen. I have much to grow yet to feel constrained by the language but I suppose if that’s the case maybe Purescript could be a better fit?

I could maybe suggest you have a look at this well known package for inspiration: Json.Decode.Pipeline - elm-json-decode-pipeline 1.0.1

It handles differently, the use case demoed by elm-do. I haven’t studied the package myself though (only consumed it) so forgive me if I’m off-topic.

Yeah that’s where I’m at. I haven’t grokked the full picture yet, which is why I used this loose terminology :slight_smile:

1 Like

I would agree that this would be too much technical jargon for a new comer but you have to understand this to use the most basic operator in Elm- (::).

This literally tripped up a teammate of mine roughly yesterday and I had to explain it to him just 10 min ago because they didn’t understand.

From what I’ve heard, the 2 hardest things to teach in Elm are JSON encoding/decoding and currying. Not sure what it is after that, but I’ve definitely seen Maybe itself trip a fair amount of devs up, especially if they come from languages that have null, they expect it to be roughly the same when it isn’t.


If you want to understand better what would help with learning/teaching and language, I’d look at https://hedycode.com/ and what McMaster University has put out about code and learning. McMaster uses Elm and Hedy is based on gradually learning Python (essentially), but very much focused on the student & teacher and how they help each other.


Somewhat related, not sure if you’ve listened to Elm at a Billion Dollar Company with Aaron White but I, for the most part, agree with Aaron (I also work for him now, but felt similarly before hand). I’m definitely biased towards wanting a language that’s easy to maintain, easy to refactor, and easy to onboard onto. Elm really hits all of those far better than any other language I’ve used and I’d very much like it to stay that way. I don’t care about whether the types are fancy or not. I don’t care about whether there are {} and ; for scope. Basically Elm hits a really nice sweet spot for large apps that are developed by many people over many years, and I’d love for it to remain in that sweet spot.

But this is all getting slightly off topic as well.

1 Like

I’d love feedback as to whether my command was a good explanation! Although I’m torn between that and just saying the following are equal

x = do
    a <- someMaybeValue
    b <- someOtherMaybeValue
    return a+b
x =
    let a = someMaybeValue
        b = someOtherMaybeValue
    in a+b

Except in the first example we treat a and b like they were just numbers rather than Maybe numbers, and if either of them Nothing then x is nothing.

I’m not sure I love this explanation but it’s a lot easier to see the utility of it, so it might be a good explanation to proceed the first.

That’s the reason as to why it only has pure functions (and some other design choices), but the reason it doesn’t have do notation is because it’s too complex for beginners and Elm highly values accessibility. do notation and bind exist for the express purpose of having nicer code organization, and it would certainly help Elm’s readability, though at the expense of beginner comprehension

The problem I’m having is more about community/language design than what libraries I have access to. You can’t compose functions like this without some nesting unless you have bind and/or do.

Not at all! I love teaching others what I can!

.

Oh that’s common? I was having a hard time of that just recently! Elm has really well designed libraries though which makes it nice and approachable.

I’ve heard of Hedy! And I have to say, it’s a really cool idea!

I suppose it depends on what you’re working on. I think for front end web development, Elm’s level of complexity certainly hits a sweat spot. I’m using it for something which it’s really not meant for now (parsing HTML strings) and there’s a lot of difficulties associated with but dang it, it’s just too nice of a language that it’s worth it. But if you’re working on something more technical which demands higher expertise from the programmer, then perhaps that’s the place to use a language like Elm with type classes, monad support, and a few other (somewhat simple, somewhat complex) features.

I suppose it is! Thanks for the discussion!

<| and |> do have the same precedence, but <| is right associative and |> is left associative.

source

1 Like

Oh dip. That makes sense!

I think you meant “comment” not “command” right?

I don’t follow you entirely, mainly because I’m out of my comfort zone :slight_smile:

But I do get your annoyances with the formater. Although I haven’t felt any strong discomfort with elm-format myself, I have been quite annoyed with OCaml’s, to the point where I thought about dropping it. But then, I realized that it’s a tool that can also gently guide you towards a particular (opinionated) style so I kept with it (fortunately, I could still tweak it a fair bit via its config file).

So, forgetting about elm-format, nothing stops you from formatting your code like this no?

It seems to me that we’re almost there (minus the extra parentheses). You don’t absolutely have to have >>= to make the code readable.

add ma mb =
    ma |> Maybe.andThen (\a -> 
    mb |> Maybe.andThen (\b -> 
    Just (a + b)))

I realized this when exploring this problem space in… Java :sweat_smile:

  private static Optional<Integer> add(Optional<Integer> x, Optional<Integer> y) {
        // @formatter:off
        return x.flatMap(xx ->
               y.flatMap(yy ->
               Optional.of(xx + yy)));
        // @formatter:on
    }

Although, yeah, this style is so far off the usual and common one that I wouldn’t do that for every day work, fighting the tooling defaults, etc. would be a pain.

Yeah that was one of the solutions I found. I suppose it’s probably the best solution which doesn’t format nicely under elm-fmt. I’m a bit torn between this and just letting Elm format it and using functions but I’m thinking the latter since consistently formatted code makes a big difference.