I'm glad Paul Louth of https://github.com/louthy/language-ext/ is here in the comments.
At this point basically everyone has been exposed to the concept of `Option/Result/Either/etc.`, and discussions typically end up revolving around the aesthetics of exception throwing vs. method chaining vs. if statements etc. without any concept of the bigger picture.
LanguageExt really presents a unified vision for and experience of Functional Programming in C# for those are who truly interested, akin to what's been going on in the Scala ecosystem for years.
I've been using it and following its development for a few years now and it continually impresses me and makes C# fresh and exciting each day.
It has been a long-standing trend/belief/whatever that FP is just somehow better, it's kind of have been this belief that has endured for decades. Part of that belief is that exceptions are bad and option/result types are the way to go for proper error handling.
I don't think this is true at all, they are just different, with procedural programming being control-flow oriented and fp being dataflow oriented.
Monads are just dataflow oriented error handling, which is comes with its own set of tradeoffs and advantages, the key disadvantages being the necessity of an advanced type inference-system, to allow natural looking usage, and the function signatures having to support the notion that this function can indeed throw an error.
Implementation wise, the generated assembly is not more efficient, as the error passing plumbing needs to appear at every functions return site, even if no error happens.
I'm not saying Monads as error handling are an inherently bad concept, but neither are exceptions (as many usually suggest), and using both depend heavily on language support to make them ergonomic, which in the case of C# and monads, is missing.
I've been playing around with some of the "standard/common monads in C# for a while now, in OSS (https://github.com/pimbrouwers/Danom) and at work. It's awesome. I can't imagine working without them anymore.
A friend and I wrote our master thesis on how to ergonomically fit monads into imperative programming languages [^1], taking inspiration from Haskells do-notation to get away from the method chaining. It's more on the theoretical side, but we did write a small implementation in C# [^2] (we use the exclamation mark as bind). We really could have used som better types though, this post seems have found a better direction.
[^1]: https://odr.chalmers.se/items/91bf8c4b-93dd-43ca-8ac2-8b0d2c...
[^2]: https://github.com/master-of-monads/monads-cs/blob/89netram/...
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.)
This is all very basic, instead you can use C#'s new static interface methods feature to create higher-kinded traits where you can properly generalise over a monad trait (or applicatives, functors, foldables, etc.), which is what I do in language-ext [0].
I'm not saying that implementing SelectMany for specific data-types isn't valuable. It certainly ends up with more elegant and maintainable code, but the true power of monads and other pure-FP patterns opens up when you can fully generalise.
* I have a blog series on it that covers implementing Semigroups, Monoids, Functors, Foldables, Traversables, Applicatives, Monads, and Monad Transformers (in C#) [1]
* The monad episode (my 'Yet Another Monad Tutorial') [2]
* An entire app generalised over any monad where the monad must support specific traits [3]. It's the program I use to send out the newsletters from my blog.
Happy to answer any questions on it.
[0] https://github.com/louthy/language-ext/
[1] https://paullouth.com/higher-kinds-in-c-with-language-ext/
[2] https://paullouth.com/higher-kinds-in-csharp-with-language-e...
[3] https://github.com/louthy/language-ext/tree/main/Samples/New...
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 if needed. If you want to hide that implementation detail, then you should still consider throwing more specific types of exception for different errors. Just throwing InvalidOperationException for everything feels lazy.You've gone out your way to destroy the information being passed up when using exceptions, to demonstrate the value in having error types.
It would be far more conventional to also provide a `TryDeactivateUser` function that cannot throw an exception. The article does note this, but hand-waves it away.
I'm not against Result types, but I don't find this article entirely convincing.
Looking forward to seeing what error handling starts to look like in C# once they have unions. I really like domain errors being checked by the type system.
Hi, author here. Thanks for the feedback! I'll take a look at the article tonight and go through the comments and update the post based on the comments.
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 a monad has done nothing but overcomplicate the whole matter for decades.
Last Year, i wrote some Monad Mini Framework for my own, but focusing only on the Result-Type itself. I planned to publish it, but i think today is a good day. Here we go: https://codeberg.org/Arakis/Result
Idk, to me that constant
Result<User, Error>
looks extremely ugly and unergonomic, even just to type.I understand that this is complicated topic and there were a lot of strong opinions even inside of Google about it, but god, I miss absl::StatusOr and ASSIGN_OR_RETURN. Yes, it won’t work without preprocessor magic (and that’s why this article goes through heavy functional stuff, otherwise it just cannot work in language like C#), but it’s so easy and natural to use in base case, it feels like cheating.
Good review, but I frankly don't see the point of Result<T0, T1>. 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.
[dead]
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 needing to understand the code.
It feels idiomatic in F#. It feels crow-barred in with C#