9 ms·
Monads in C# (Part 2): Result
- pimbrouwers 9mo agoI've been playing around with some of the "standard/common monads in C# for a while now, in OSS (https://github.com/pimbrouwers/Danom https://github.com/pimbrouwers/Danom) and at work. It's awesome. I can't imagine working without them anymore.
- polygot 9mo agoThanks for sharing! Appreciate the OSS work. Is it ok if I reference that repo in my article?
- pimbrouwers 9mo ago100%! I would be thrilled! Thank you very much.
- oaiey 9mo agoDo you have a comparison to other libraries like https://github.com/louthy/language-ext https://github.com/louthy/language-ext 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 9mo agoI normally do a much deeper dive into the existing ecosystem before I write a library like this. In this case, I tried a few of the popular packages, but in the end decided my own pursuit of the exact types I wanted was more interesting. So, I just went for it.
- galkk 9mo agoIdk, 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.
- simonask 9mo agoI think in C# the way to solve this is to have two separate types, `Ok<TValue>` and `Err<TError>`, and provide implicit conversions for both to `Result<TValue, TError>`. The static method approach showcased in the article is really long-winded.
- oaiey 9mo agoYes. Or even simpler, static imported functions. That is similar to how ASP.NET Core handles HTTP feedback. A very common and understood concept. With implicit type parameters this boils down to Ok(4) or BadRequest()
- pyrale 9mo ago> I miss absl::StatusOr Sounds like you would rather have an `ErrorOr<User>` than a `Result<User, Error>`. Both are union types wrapped in a monadic construct.
- galkk 9mo agoI wrote example above: https://news.ycombinator.com/item?id=46508392 https://news.ycombinator.com/item?id=46508392 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
- deleted 9mo ago[deleted]
- moogly 9mo agoIs this similar? https://github.com/amantinband/error-or https://github.com/amantinband/error-or 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/statusor.h https://github.com/abseil/abseil-cpp/blob/master/absl/status...
- 89netraM 9mo agoA 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-8b0d2c310796 https://odr.chalmers.se/items/91bf8c4b-93dd-43ca-8ac2-8b0d2c... [^2]: https://github.com/master-of-monads/monads-cs/blob/89netram/source-generator/Monads/src/ProgramM.mcs https://github.com/master-of-monads/monads-cs/blob/89netram/...
- xnorswap 9mo agoI 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.
- magicalhippo 9mo agoThe original exception is available[1] in the InnerException though, so upstream can handle those differently. [1]: https://learn.microsoft.com/en-us/dotnet/api/system.invalidoperationexception.-ctor?view=net-10.0#system-invalidoperationexception-ctor(system-string-system-exception) https://learn.microsoft.com/en-us/dotnet/api/system.invalido...
- deleted 9mo ago[deleted]
- bob1029 9mo ago
- epolanski 9mo agoSmall 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.
- mrkeen 9mo agoSure, but this article's Result implements Ok(), Map(), and Bind()
- pjc50 9mo agoI 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.
- mrkeen 9mo agoWhere did you get that definition? "function that takes one argument and returns one value of the same type" is the identity function.
- epolanski 9mo agoIdentity function returns the same _value_. 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.
- mrkeen 9mo agostring reversal, integer negation or toUpperCase are classical examples of functions which will not compile as `a -> a` The function which will compile as `a -> a` is the identity function.
- neonsunset 9mo ago[dead]
- Roonerelli 9mo agoI'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#
- douglasisshiny 9mo agoI felt the same way with fp-ts then effect in typescript. Pretty cool libraries and I learned a lot about FP while trying them out for a couple of years, but a lot of ceremony and noise due to them (especially effect) almost being a new language on top of typescript. 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.
- gr4vityWall 9mo agoI had a similar impression with using those constructs on TypeScript. 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.
- bonesss 9mo agoMonadic binding and other functional mainstays in C# mostly fall into the same uncanny valley. Like non-exhaustive pattern matching, we get some nice sugar, but it’s not the same, and not a proper substitute for what we’re trying to do. 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
- louthy 9mo agoThis 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/ https://github.com/louthy/language-ext/ [1] https://paullouth.com/higher-kinds-in-c-with-language-ext/ https://paullouth.com/higher-kinds-in-c-with-language-ext/ [2] https://paullouth.com/higher-kinds-in-csharp-with-language-ext-part-7-monads/ https://paullouth.com/higher-kinds-in-csharp-with-language-e... [3] https://github.com/louthy/language-ext/tree/main/Samples/Newsletter/Newsletter https://github.com/louthy/language-ext/tree/main/Samples/New...
- FrustratedMonky 9mo agoSerious question, at this point, have all F# features been fully incorporated into C#?
- louthy 9mo agoNot 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<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.
- seblon 9mo agoLast 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 https://codeberg.org/Arakis/Result
- jerf 9mo agoResult<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/ 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 9mo agoIn 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/ 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/CardGame/Game.cs https://github.com/louthy/language-ext/blob/main/Samples/Car...
- naasking 9mo agoGood 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.
- louthy 9mo agoIf you ask yourself what the meaning of the word 'exception' is and then consider how many failures are exceptional, then one quickly realises that exceptions are the worst thing you could use to represent expected failure conditions. 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.
- naasking 9mo agoThe dangers and pitfalls of exceptions are completely irrelevant if all you're doing is using an exception as a value and not for control flow.
- louthy 9mo agoIt’s not about danger it’s about being declarative. That’s kinda the point of using these ‘result’ types: you’re fully declaring the codomain of the function — barring exceptions — and so if your codomain is augmented with Exception then it’s pretty hard to know whether all exceptions will be returned in value form, or just exceptional exceptions! It’s fails the declarative test.
- naasking 9mo agoIt's not hard at all: a return type of Result is when it's returned in value form.
- veleon 9mo agoI think OP means using Result<T> where Right in the Either is always implied to be Exception, much like Fin<A> in language-ext [0] [0] https://louthy.github.io/language-ext/LanguageExt.Core/Monads/Alternative%20Monads/Fin/index.html https://louthy.github.io/language-ext/LanguageExt.Core/Monad...
- mikemarsh 9mo agoI'm glad Paul Louth of https://github.com/louthy/language-ext/ 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.
- louthy 9mo agoAww, thanks Mike! And thank you for the contributions and suggestions too :) > 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 https://github.com/louthy/csharp-monad [2] https://github.com/louthy/language-ext https://github.com/louthy/language-ext [3] https://em-tg.github.io/csborrow/ https://em-tg.github.io/csborrow/
- polygot 9mo agoHi, 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.
- vips7L 9mo agoLooking 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.
- torginus 9mo agoIt 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.
- louthy 9mo agoOn your definition of FP you're right. But pure functional programming has the following over regular imperative coding: * 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.