Live data from Hacker News

Monads in C# (Part 2): Result

alexyorke.github.io

41–50 of 75 posts

Re: Monads in C# (Part 2): Result

#41
post #23

Earlier quoted context omitted.

Does it? Int.Parse says it can only return those 2 exceptions or ArgumentNullException, but nulls have been handled already. https://learn.microsoft.com/en-us/dotnet/api/system.int32.pa...

Fine but with all that code we are implying there is some case we don't want to catch for some reason. If any effective parse error should always throw we should simply do that instead of playing games.

You’re not accounting for what the example actually does.

The example provides exception conformance at the API level and specific logging information for tracing and debugging. It’s not playing games, it’s simplifying details for upstream consumers and explaining their contribution to the issue, while being explicit in its intentioned failure modes and failure behaviour.

This code cannot tell callers why the callers have sent it garbage or what to do about that, it is failing and explaining why.

Throwing “invalid : bad user id” is substantively different than rethrowing “index out of bounds : no string character at index -1”. The wrapped exception has all the original detail, its just unified and more descriptive.

Re: Monads in C# (Part 2): Result

#43
post #39

Earlier quoted context omitted.

Serious question, at this point, have all F# features been fully incorporated into C#?

Not discriminated unions, but they're coming (I think next version of C#). Although for now you can simulate them quite easily: public abstract record Either ; public sealed record Left (L Value) : Either ; public sealed record Right (R Value) : Either ; Pattern-matching works well with these simulated algebraic data-types. Obviously, exhaustiveness checks can't work on 'open' types, so it's not perfect, but you can…

That will allocate for any constructed Either though. F#'s Result and ValueOption are value-types (structs), and value-type variants recently added support for sharing fields between variants when the name and type match.

Re: Monads in C# (Part 2): Result

#44
post #22

Small OT but part of me dies when data types that respect some laws are just labeled monads. Nobody calls an array a monad, even though an array admits a monad instance. Option, Result, Array, Either, FunkyFoo, whatever you want are just data types. They only become monads when combined with some functions (map, bind, apply, flatmap), and that combination of things respects a set of law. But calling a data type alone…

I was wondering about that. "Monad" is a mildly obfuscatory term for "function that takes one argument and returns one value of the same type", and a List is not a function.

I think you're describing part of the bind function, which is part of the definition of the monad interface:

   a -> m b
But the full definition is:

   bind :: (a -> m b) -> m a -> m b
In C#, it would look like this:

   M Bind(Func> f, M ma)
Assuming a future version of C# that supports higher-kinds that is.

In my language-ext library, I achieve a higher-kinded monad trait [1], like so:

    public interface Monad : Applicative, 
        where M : Monad
    {
        static abstract K Bind(K ma, Func> f);
    }
Which is what the original comment is about. Most people in C# are not creating monads when they implement Bind or SelectMany for their type. They are simply making 'do-notation' work (LINQ). The monad abstraction isn't there until you define the Monad trait that allows the writing of any function constrained to said trait.

For example, a `When` function that runs the `then` monad when the `predicate` monad returns `true` for its bound value:

    public static K When(K predicate, K then)
        where M : Monad =>
        predicate.Bind(flag => flag ? then : M.Pure(unit));
This will work for any monad, `Option`, `List`, `Reader`, ... or anything that defines the trait.

So types like `Option` are just pure data-types. They become monads when the Monad trait is implemented for them. The monad trait can be implemented for data-types and function-types. Reader, for example, has the Monad trait implemented for the function: Func

btw, monads also inherit behaviour from applicative and functor. The ability to lift pure values into the monad (a -> m a) is vital to making monads useful. This is `select` in LINQ, `pure` in Haskell's Applicative, and `Pure` in language-ext's Applicative [2].

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

[2] https://github.com/louthy/language-ext/blob/main/LanguageExt...

Re: Monads in C# (Part 2): Result

#45

I've had the misfortune of working on a C# code base that uses this pattern for many years. I've also used it with F#, where it feels natural - because the language supports discriminated unions and has operators for binding, mapping etc. Without that, it feels like swimming against the tide. Code has a greater cognitive overhead when reading it for the first time. And there is always a big over head for new starters…

F# also has substantially better type inference so you don't need to write the types out everywhere, type aliases are first class too so you can easily write out some helper types for readability.

You can pipe a monadic type through various functions writing little to no type declarations, doing it nicely is F#'s bread and butter.

In C# version n+1 when the language is supposedly getting discriminated unions for real this time I still don't see them being used for monadic patterns like F# because they're going to remain a menace to compose.

Re: Monads in C# (Part 2): Result

#46

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

    do
       id 
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.)

Re: Monads in C# (Part 2): Result

#47
Good review, but I frankly don't see the point of Result. Just have the error be an exception type as exceptions idiomatically represents errors in .NET. Then you're down to only 1 type argument which is much less noisy. That's what I've used for the result type in my library that I've been using for years. I don't use it often, but it's very handy when appropriate.

Re: Monads in C# (Part 2): Result

#48
post #39

Earlier quoted context omitted.

Not discriminated unions, but they're coming (I think next version of C#). Although for now you can simulate them quite easily: public abstract record Either ; public sealed record Left (L Value) : Either ; public sealed record Right (R Value) : Either ; Pattern-matching works well with these simulated algebraic data-types. Obviously, exhaustiveness checks can't work on 'open' types, so it's not perfect, but you can…

That will allocate for any constructed Either though. F#'s Result and ValueOption are value-types (structs), and value-type variants recently added support for sharing fields between variants when the name and type match.

Yes, that's the limitation until the value-type DUs arrive in C# 15.

In previous versions of language-ext, I defined Either as a struct with bespoke Match methods to pattern-match. But once pattern-matching appeared in C# proper, it didn't make sense to keep the struct type.

Re: Monads in C# (Part 2): Result

#49

I really dislike this pattern: try { id = int.Parse(inputId); } catch (Exception ex) when (ex is FormatException or OverflowException) { throw new InvalidOperationException("DeactivateUser failed at: parse id", ex); } Where all you're doing when you catch an exception is throwing it in a more generic way. You could just let the FormatException or OverflowException bubble up, so the parent can handle those differently…

You're leaking implementation details if you let exceptions bubble. Sometimes this is ok if all of the callers are aware of the implementation details anyway, but it can make refactoring or changing implementations more difficult otherwise.

Re: Monads in C# (Part 2): Result

#50
post #46

Result result = ParseId(inputId) .Bind(FindUser) .Bind(DeactivateDecision); This does not implement monads as Haskell has them. In particular, Haskell can do: do id 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…

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 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...

Post reply on HN