Remix.run Logo
jerf 2 days ago

    Result<User, Error> result =
        ParseId(inputId)
            .Bind(FindUser)
            .Bind(DeactivateDecision);
This does not implement monads as Haskell has them. In particular, Haskell can do:

    do
       id <- ParseID inputId
       user <- FindUser id
       posts <- FindPostsByUserId id
       deactivateDecision user posts
Note id getting used multiple times. "Monad" is not a pipeline where each value can be used only once. In fact if anything quite the opposite, their power comes from being able to use things more than once. If you desugar the do syntax, you end up with a deeply nested function call, which is necessary to make the monad interface work. It can not be achieved with method chaining because it fails to have the nested function calls. Any putative "monad" implementation based on method chaining is wrong, barring some future language that I've not seen that is powerful enough to somehow turn those into nested closures rather than the obvious function calls.

I wrote what you might call an acid test for monad implementations a while back: https://jerf.org/iri/post/2928/ It's phrased in terms of tutorials but it works for implementations as well; you should be able to transliterate the example into your monad implementation, and it ought to look at least halfway decent if it's going to be usable. I won't say that necessarily has every last nuance (looking back at it, maybe I need to add something for short-circuiting the rest of a computation), but it seems to catch most things. (Observe the date; this is not targeted at the original poster or anything.)

(The idea of something that can be used "exactly once" is of interest in its own right; google up "linear types" if you are interested in that. But that's unrelated to the monad interface.)

louthy 2 days ago | parent [-]

In C# you can implement SelectMany for a type and that gives this:

    from id    in ParseId(inputId)
    from user  in FindUser(id)
    from posts in FindPostsByUserId(id)
    from res   in DeactivateDecision(user, posts)
    select res;
It is the equivalent to do-notation (was directly inspired by it). Here's an example from the language-ext Samples [1], it's a game of 21/pontoon.

> I wrote what you might call an acid test for monad implementations a while back: https://jerf.org/iri/post/2928/ It's phrased in terms of tutorials but it works for implementations as well; you should be able to transliterate the example into your monad implementation, and it ought to look at least halfway decent if it's going to be usable.

If I try to implement the test from your blog with Seq type in language-ext (using C#), then I get:

    Seq<(int, string)> minimal(bool b) =>
        from x in b ? Seq(1, 2) : Seq(3, 4)
        from r in x % 2 == 0
                      ? from y in Seq("a", "b") 
                        select (x, y)
                      : from y in Seq("y", "z")
                        select (x, y)
        select r;
It yields:

    [(1, y), (1, z), (2, a), (2, b)]
    [(3, y), (3, z), (4, a), (4, b)]

Which I think passes your test.

[1] https://github.com/louthy/language-ext/blob/main/Samples/Car...

jerf 2 days ago | parent [-]

It looks like it. My claim was not (and is not) that C# can't implement it, but that what is discussed in the post does not.

louthy 2 days ago | parent [-]

Fair enough :)

By the way, I happen to agree on the general point, in my blog teaching Monads in C# [1], I wrote this:

"I often see other language ecosystems trying to bring monads into their domain. But, without first-class support for monads (like do notation in Haskell or LINQ in C#), they are (in my humble opinion) too hard to use. LINQ is the killer feature that allows C# to be one of very few languages that can facilitate genuine pure functional programming."

So, yeah, regular fluent method chaining isn't really enough to make monads useful.

[1] https://paullouth.com/higher-kinds-in-csharp-with-language-e...