The way I understood monads is they’re a way to abstract the “executor” of a function. I/O monads run step-by-step based on stdin, List runs a function on every element, and the function is unaware of this, Option runs the function if the value exists (again the function’s not aware of this)
That being said, I’m coming at this from a Rust view, and I’ve only scanned through one guide to monads so I may be wrong
That’s not a monad, that’s just typeclasses (also known as traits/interfaces/etc.).
If you’re familiar with flat_map or and_then in Rust, you already get the basic idea of monads. Just imagine that instead of the ad-hoc implementations that Rust has to do (since it doesn’t support higher-kinded data types), you could express it in a single trait. Then you can write code that generically works with any types that support flat_mapoperations (along with a couple of other requirements, like needing a From<T> impl, as well as abiding by some laws that are really only a concern for library authors).
If that sounds simple, it’s because it is. It’s mostly just that it just so happens that being able to write code in this fashion has some pretty significant implications. It essentially allows you to very strictly control the semantics of sequencing operations, while still giving you enough flexibility to enable many of the things you typically do in imperative programs. In a language like Haskell which is built on monads, it’s a very powerful reasoning tool, much in the same way that purity in functions is a powerful reasoning tool. It allows us to do things like, say, represent async code blocks as first-class concepts requiring no special syntax (no async/await, etc.), because the monad interface allows us to dictate what it means to sequence async actions together (if you’re curious to see what this looks like, here’s a link to Haskell’s Async type from the async library: https://hackage.haskell.org/package/async-2.2.4/docs/Control-Concurrent-Async.html#t:Async).
Ah, so is it right to say it’s an abstraction of how functions are sequenced? I could kinda see that idea in action for I/O and Async (I assume it evaluates functions when their corresponding async input is ready?)
I think that’s a reasonable enough generalization, yeah.
I’m sorry though, I seem to have given you incorrect information. Apparently that library does not have monad instances, so it’s a bad example (though the Concurrently type does have an applicative instance, which is similar in concept, just less powerful). For some reason I thought they also provided monad instances for their API. My bad.
Perhaps it would be better to use a much simpler example in Option. The semantics of the sequencing of Options is that the final result will be None if any of the sequenced Options had a value of None, otherwise it would be a Some constructor wrapping the final value. So the semantics are “sequence these operations, and if any fail, the entire block fails”, essentially. Result is similar, except the result would be the first Err that is encountered, otherwise it would be a final Ok wrapping the result.
So each type can have its own semantics of sequencing operations, and in languages that can express it, we can write useful functions that work for any monad, allowing the caller of said function to decide what sequencing semantics they would like to use.
The way I understood monads is they’re a way to abstract the “executor” of a function. I/O monads run step-by-step based on stdin, List runs a function on every element, and the function is unaware of this, Option runs the function if the value exists (again the function’s not aware of this)
That being said, I’m coming at this from a Rust view, and I’ve only scanned through one guide to monads so I may be wrong
That’s not a monad, that’s just typeclasses (also known as traits/interfaces/etc.).
If you’re familiar with
flat_map
orand_then
in Rust, you already get the basic idea of monads. Just imagine that instead of the ad-hoc implementations that Rust has to do (since it doesn’t support higher-kinded data types), you could express it in a single trait. Then you can write code that generically works with any types that supportflat_map
operations (along with a couple of other requirements, like needing aFrom<T>
impl, as well as abiding by some laws that are really only a concern for library authors).If that sounds simple, it’s because it is. It’s mostly just that it just so happens that being able to write code in this fashion has some pretty significant implications. It essentially allows you to very strictly control the semantics of sequencing operations, while still giving you enough flexibility to enable many of the things you typically do in imperative programs. In a language like Haskell which is built on monads, it’s a very powerful reasoning tool, much in the same way that purity in functions is a powerful reasoning tool. It allows us to do things like, say, represent async code blocks as first-class concepts requiring no special syntax (no async/await, etc.), because the monad interface allows us to dictate what it means to sequence async actions together (if you’re curious to see what this looks like, here’s a link to Haskell’s
Async
type from theasync
library: https://hackage.haskell.org/package/async-2.2.4/docs/Control-Concurrent-Async.html#t:Async).Ah, so is it right to say it’s an abstraction of how functions are sequenced? I could kinda see that idea in action for I/O and Async (I assume it evaluates functions when their corresponding async input is ready?)
I think that’s a reasonable enough generalization, yeah.
I’m sorry though, I seem to have given you incorrect information. Apparently that library does not have monad instances, so it’s a bad example (though the
Concurrently
type does have an applicative instance, which is similar in concept, just less powerful). For some reason I thought they also provided monad instances for their API. My bad.Perhaps it would be better to use a much simpler example in
Option
. The semantics of the sequencing ofOption
s is that the final result will beNone
if any of the sequencedOption
s had a value ofNone
, otherwise it would be aSome
constructor wrapping the final value. So the semantics are “sequence these operations, and if any fail, the entire block fails”, essentially.Result
is similar, except the result would be the firstErr
that is encountered, otherwise it would be a finalOk
wrapping the result.So each type can have its own semantics of sequencing operations, and in languages that can express it, we can write useful functions that work for any monad, allowing the caller of said function to decide what sequencing semantics they would like to use.
All good, thanks for the explanation! :D