2 ms·
Rust uses a sum type for most errors (Result<T, Error>) and there are common macros such as "error-chain" that apply a monadic approach to binding/combining the
by WorldMaker 3y ago
Rust uses a sum type for most errors (Result<T, Error>) and there are common macros such as "error-chain" that apply a monadic approach to binding/combining them (or chaining them, as the preferred nomenclature in that case).
Exception-based languages don't look monadic at first blush, they definitely aren't Sum types, but try/catch is one of the earliest and oldest of the monadic binding/combining approaches, even if it isn't often thought as such (and is barely considered more than goto/switch). It's far more obvious when you start to realize the intersection of async/await and try/catch is a working monad (and how that is reflected in Task/Promise monad instances).
Golang is the obvious odd one out where every other line of "best practice" code is `if err != nil` and the only other advice is "well you could just ignore errors". try/catch doesn't ignore errors, it just looks like it. Rust error chaining doesn't ignore errors, it just looks like it. Golang gives you the choice between micro-managing errors or BASIC-style "ON ERROR RESUME NEXT" and I cannot get anyone to convince me why this isn't either a practical joke or an intentional throwback to writing code like it is the 1970s all over again (even VB6 had better options than "ON ERROR RESUME NEXT", even if they maybe weren't used as often as they should be in some legacy 1990s/2000s codebases).
- hollerith 3y ago>every other line of "best practice" code is `if err != nil` and the only other advice is "well you could just ignore errors". You have a valid point there, but it hurts my ears a little for you to use the word "monadic" or "monad" to make the distinction between how the way Golang handles errors differs from how other modern language do it. To me, for a language to "have monads" requires the type system to give the programmer the ability to distinguish between a value type, e.g., an integer, and a computation that produce a value type. Haskell's type system for example uses `Integer` to refer to the former and, e.g., `IO Integer` to refer to that latter. And clearly by that definition, Rust and most other modern languages just don't have monads, i.e., don't have monadic types (and consequently don't support a monadic programming style) -- unless perhaps Rust's macro system can be used in some complex way to define the monadic-computational types, but I've never seen it used that way. BTW, I'd actually go further and say that to get the benefits of monadic style, the IO monad or whatever the language calls it must be the only way to perform side-effecting computations (though there is a little wiggle room on the definition of side effect here). To be more precise, the terminology I prefer is that a monad (in the context of programming languages) is a type that obeys the 3 monad laws: https://wiki.haskell.org/Monad_laws https://wiki.haskell.org/Monad_laws. There are other ways besides a monad for the type system to distinguish between a value and a computation producing a value; Explicit continuation passing is one such way: whereas in Haskell you'd read a character from standard input with `main = getChar >>= \char -> do something with char`, in a language that uses explicit continuation passing, you'd write `main = getChar (\char -> do something with char)` where in this language getChar takes one argument, which happens to be a continuation argument. But all the languages I know about that let the programmer use the type system to distinguish between a value and a computation producing a value use the monadic style to express that distinction (rather than something like explicit continuation passing) so I tend to use "monad" and "monadic" to refer both to the specific thing that obeys the 3 laws and for the deeper programming-language property.
- WorldMaker 3y agoI suppose one of the biggest parts of confusion here is that I'm mostly referring to the monadic programming here in the perspective of the compile-time representations inside of a compiler rather than in the exposed type systems of the languages that they compile. A compile-time representation of computation is a type that represents computation. Not every compiler author themselves think of their work as monads and build them in a way that follows the monad laws/use monad operators directly either, but the overlap should be obvious and a lot of classic and modern compile-time transformations (generators, try/catch, async/await) can be thought of as monadic transformers transforming from one class of monad to another whether or not they are actually implemented that way. That's one level of confusion in this conversation so far. However, the other problem is that monads don't just mean "computation". (Arguably the Lambda Calculus is way closer to the metal and more accurate to describe as the abstraction of "computation".) Monads are useful for describing "computations as values" but that's not what the abstraction is entirely for and thinking of monads only as "computations" is how you fall into the easy mistake to make that IO in Haskell is the only "real" monad. There are lots of monads in the wild (Maybe, Either, Promise, List, ...), some of which don't really describe computation at all. We love to mock it, but the technical definition of a monad is "a monoid in the category of endofunctors"; nothing about that is "computation" specifically. Computation is a handy shorthand, of course, but its confusing one of the trees for the rest of the forest sort of thing, mostly because Haskell's IO was the first time monad escaped as a term of art for the deep abstraction that it is. There is an importance to the monad laws and the two monad "operators", more importance than "computation" as a shorthand for thinking about monads. These operators are called "multiplication" and "identity" in raw Category Theory, but in programming "identity" is usually called "return" and "multiplication" is often called "bind", "bind" is interesting because it also has a dual generally called "join" and "join" is sometimes called "flatMap" or "SelectMany". (Yes, that does point out that most list structures in every modern programming language provide monad operators and obey the monad laws. Lists are a monad.) A dumber, better shorthand for monads may be "glueable". Monads bind things together in a way that two binds of the same type of monad results in just one monad. (flatMap two lists together and get one list back.) The power of monads is that you can keep gluing things together. Whether by "executing side effects" in a "computation runtime" like the stalwart IO monad or in all the "boring" ways of Lists and Promises and Maybe and Either (that mostly have nothing to do with "computations", except Promises, sort of). The big useful bit is that the Monad laws imply that you can do all this "gluing" in a stable way. That stable way also makes room for the means to transform from one type of Monad to another (Monadic transformers). Monadic transformers are where some of the real power of the IO monad as an abstraction lives (and other monads in terms of compile-time representations): the idea that the "notation" doesn't matter so much as the end "glued" result. Imperative looking do-notation is the same sort of "glue" as a lot of individual calls to the Monad operators bind and return, transforming from one form to the other is "relatively" "trivial" because the Monad laws say everything should be fine. There is a usefulness to the IO Monad in Haskell and it is an interesting way to "describe side effects to perform", but the whole of monads and monadic binding is a lot more than just the IO Monad and if you are defining monads to only mean "the IO Monad" you are missing some of their other usefulness.