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.
The static method approach showcased in the article is really long-winded.
With implicit type parameters this boils down to Ok(4) or BadRequest()
Sounds like you would rather have an `ErrorOr<User>` than a `Result<User, Error>`.
Both are union types wrapped in a monadic construct.
My point is not the types/monadic constructs, etc (I love to do functional jerk off as a guy next to me, though), but that there are ways to keep code readable and straightforward without neither invocation chains
DoOne().OnError().ThenDoTwo().ThenDoThree().OnError()
nor coloring/await mess, nor golang-style useless error handling noise
What's some of the "preprocessor magic" that makes this[1] more ergonomic to use?
[1]: https://github.com/abseil/abseil-cpp/blob/master/absl/status...
In google/c++ you can do much simpler, but with preprocessor magic.
Example:
absl::StatusOr<User> loadUserById(int userId) { ... }
absl::Status populateItems(User user, std::vector<Item>& items) {...}
absl::StatusOr<Item> findItems(int userId) {
ASSIGN_OR_RETURN(auto user, loadUserById(userId));
std::vector<Item> items;
RETURN_IF_ERROR(populateItems(user, items));
for (auto& item: items) {
...
}
}
ASSIGN_OR_RETURN and RETURN_IF_ERROR essentially preprocessor macroses, that expand, more or less into. absl::StatusOr<Item> findItems(int userId) {
auto userOrStatus = loadUserById(userId);
if (!userOrStatus.ok()) return userOrStatus.status();
auto user = *userOrStatus;
std::vector<Item> items;
absl::Status st2 = populateItems(user, items));
if (!st2.ok()) return st2;
}
No long and ugly method invocation chains, no weirdly looking code - everything just works. You can see real life example here: https://github.com/protocolbuffers/protobuf/blob/bd7fe97e8c1...Again, even inside Google there were docs that considered those macroses bad and suggested to write straightforward code, but I'm in the camp who considers them useful and, maybe, sole good use of preprocessor macros that I ever seen, as there are no other way to clearly and concisely express that in majority of languages.
F# has something like that with Computational Expressions, but they are still limited.
[^1]: https://odr.chalmers.se/items/91bf8c4b-93dd-43ca-8ac2-8b0d2c...
[^2]: https://github.com/master-of-monads/monads-cs/blob/89netram/...
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.
[1]: https://learn.microsoft.com/en-us/dotnet/api/system.invalido...
You've still made it harder to handle.
https://learn.microsoft.com/en-us/dotnet/api/system.int32.pa...
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.
I would rather see custom exceptions thrown than rethrowing.
As an aside, generating domain specific exceptions is precisely the kind of busywork that traditionally it is hard to find motivation to do but that LLMs excel at.
Code snippets in IDEs like Visual Studio and refactoring tools like Resharper offer shortcut accessible auto generation of those kinds of entities with deterministic results. They also have routines to extract and move entities to their own files afterwards in a keyboard based workflow.
They are far less work than a prompt, faster, can be shared in the project or team, and are guarantee-able to confirm to coding standards, logging specifics and desired inheritance chain for your root domain exception. It works on-prem, offline, for zero tokens.
public class UserIdInvalidException(Exception innerException) : Exception("Invalid User ID", innerException);
Even easier than most data objects you’d have to define anyway. And then, Exceptions are part of the contract. I’d rather not have an LLM write that up, but that’s just personal preference.Wrapping exceptions to remove information is mostly a pointless exercise. You should be doing it only to add additional context.
Go for example, allows for multiple return values, so it allows more elegant handling of this exact cass.
Then there's the serious kind of error, when something you didn't expect goes wrong. That's what exceptions are for. If this distinction is followed, then you don't want to handle specific exceptions (with very few notable distinctions, like TaskCanceledException), you just either pick a recoverable function scope (like a HTTP handler), and let the exception bubble to its top, at which point you report an error to the user, and log what happened.
If such a thing is not possible, just let the program crash.
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.
"function that takes one argument and returns one value of the same type" is the identity function.
If it's only the same _type_, but the value is not the same, then it's an endomorphism. The function definitions look the same `a -> a`.
string reversal, integer negation or toUpperCase are classical examples of endomorphisms.
Identity is a specific case of endomorphism.
The function which will compile as `a -> a` is the identity function.
identity, uppercase or negate are all endomorphisms, with identity being the only generic one.
a -> m b
But the full definition is: bind :: (a -> m b) -> m a -> m b
In C#, it would look like this: M<B> Bind(Func<A, M<B>> f, M<A> 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<M> : Applicative<M>,
where M : Monad<M>
{
static abstract K<M, B> Bind<A, B>(K<M, A> ma, Func<A, K<M, B>> 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<M, Unit> When<M>(K<M, bool> predicate, K<M, Unit> then)
where M : Monad<M> =>
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<E, A>
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...
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#
Recently got the opportunity to try out elixir at my job and I'm liking it thus far, although it is an adjustment. That static typing and type inference are being added to the language right now is helpful.
IMO it's hard to justify creating Option<T>/Result<T,E> wrappers when T|null and T|E will work well enough for the majority of use cases.
effect specifically feels like a different programming language altogether. And maybe going that path and compiling down to TS/JS could've been a better path for them. I'm not on the ecosystem though, so it's an uninformed thought.
F# ~~ripped off~~ is deeply inspired by OCaml, with a very practical impact on its standard library: there are facilities available for all the functional programming jazz one hasn’t though about or bumped into. In active patterns, pattern matching, recursive list comprehensions, applicatives, or computation expressions when you bump into the corners of the language you find a deep, mature, OCaml core that nerds much smarter and more talented have refined for decades. The language was built around those facilities.
Bumping into the edges of the partial features in C# is a pretty common experience for me, resulting in choices about kludges to support a superficial syntax, raising concerns about goldbricking.
It feels crowbarred because it was.
“Railway oriented programming” passes over well as a concept, but it’s an easier sale when you see its use resulting in smaller, simpler, easier functions
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.
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...
public abstract record Either<L, R>;
public sealed record Left<L, R>(L Value) : Either<L, R>;
public sealed record Right<L, R>(R Value) : Either<L, R>;
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 unpack values, apply predicate clauses, etc.Other more niche features like type-providers don't exist either (although arguably those could be done with source-generators in C#). It's been a long time since I did any F#, so not sure if there's anything new in there I'm unaware of.
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.
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.)
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...
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...
The only time we should throw (or even pass around) exceptions is if there isn't a slot in the co-domain to inject a value in to.
[0] https://louthy.github.io/language-ext/LanguageExt.Core/Monad...
I understood what they meant, Exception is still a poor type for declarative error handling, because it’s unclear whether an event is truly exceptional.
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.
> 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.
I think this is a really important point. 12 years ago I created a project called 'csharp-monad' [1], it was the forerunner to language-ext [2], which I still keep on github for posterity. It has the following monadic types:
Either<L, R>
IO<T>
Option<T>
Parser<T>
Reader<E,T>
RWS<R,W,S,T>
State<S,T>
Try<T>
Writer<W,T>
One thing I realised after developing these monadic types was that they're not much use on their own. If your List<T> type's Find method doesn't return Option<T>, then you haven't gained anything.I see others on here are taking a similar journey to the one I took over a decade ago. There's an obsession over creating Result types (Either<L, R> and Fin<A> in language-ext, btw) and the other basic monads, but there's no thought as to what comes next. Everyone of them will realise that their result-type is useless if nothing returns it.
If you're serious about creating declarative code, then you need an ecosystem that is declarative. And that's why I decided that a project called "csharp-monad" was too limiting, so I started again (language-ext) and I started writing immutable collections, concurrency primitives, parsers, and effect systems (amongst others). Where everything works with everything else. A fully integrated functional ecosytem.
The idea is to make something that initially augments the BCL and then replaces/wraps it out of existence. I want to build a complete C# functional framework ecosystem (which admittedly is quite an undertaking for one person).
I'm sometimes a little wary about going all in on the evangelism here. C# devs in general tend to 'stick to what they know' and don't always like the new, especially when it's not idiomatic - you can see it in a number of the sub-threads here. But I made a decision early on to fuck the norms and focus on making something good on its own terms.
And for those that wonder "Why C#?" or "Why not F#?", well C# has one of the best compilers and tooling ecosystems out there, it's got an amazing set of functional language features, it will have ADTs in the next version, and it has a strong library ecosystem. It also has the same kind of borrow checker low level capability as Rust [3]. So as an all-rounder language it's quite hard to beat: from 'to the metal bit-wrangling', right the way up to monad comprehensions. It should be taken more seriously as a functional language, but just generally as a language that can survive the turmoil of a long-lived project (where mostly you want easy to maintain code for the long-term, but occasionally you might need to go in and optimise the hell out of something).
My approach will piss some people off, but my aim is for it to be like the Cats or Scalaz community within the larger Scala community.
It's certainly a labour of love right now. But, over a decade later I'm still enjoying it, so it can't be all bad.
(PS Mike, I have new highly optimised Foldable functionality coming that is faster than a regular C# for-loop over an array. Watch this space!)
[1] https://github.com/louthy/csharp-monad
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.
* Fewer bugs: Pure functions, which have no side effects and depend only on their input parameters, are easier to reason about and test, leading to fewer bugs in the code-base.
* Easier optimisation: Since pure functions do not have any side effects, they can be more easily optimised by the compiler or runtime system. This can lead to improved performance.
* Faster feature addition: The lack of side effects and mutable state in pure functional programming makes it easier to add new features without introducing unintended consequences. This can lead to faster development cycles.
* Improved code clarity: Pure functions are self-contained and independent, making the code more modular and easier to understand. This can improve code maintainability.
* Parallelisation: Pure functions can be easily parallelised, as they do not depend on shared mutable state, which can lead to improved scalability.
* Composition: This is the big one. Only pure functional programming has truly effective composition. Composition with impure components sums the impurities into a sea of undeclared complexity that is hard for the human brain to reason about. Whereas composing pure functions leads to new pure functions – it's pure all the way down, it's turtles all the way down. I find it so much easier to write code when I don't have to worry about what's going on inside every function I use.
That's obviously way beyond just having a declarative return type. And in languages like C# you have to be extremely self-disciplined to 'do the right thing'. But what I've found (after being a procedural dev for ~15 years, then a OO dev for ~15 years, and now an FP dev for about 12 years) is that pure functional programming is just easier on my brain. It makes sense in the way that a mathematical proof makes sense.
YMMV of course, but for me it was a revelation.
pimbrouwers•4d ago
polygot•3d ago
pimbrouwers•3d ago
oaiey•1d ago
Yours looks a lot more idiomatic to C# (hence acceptable for a mixed code base) but the above linked more "systematic". Not that I have used any or have any competence.
pimbrouwers•1d ago