Anything more specific than that should be handled case-by-case until you build an intuition for how any given monad will behave.
Anything more specific than that should be handled case-by-case until you build an intuition for how any given monad will behave.
Computational context in this sense is like, is this computation of the type that can either fail or succeed? (Maybe monad). Or is this computation of the type that can produce multiple values of the same type? (List monad). Or maybe this computation can produce value of one type or another (Either monad). Or a computation that can interacti with inputs and outputs (IO monad).
So these are computations in differend kinds of computational "worlds" in a sense, with different rules on how such a computation in this context composes.
Are the constraints limited to return values or can there be other kinds of constraints?
It might be more helpful to think about this in terms of actual code samples. Consider a data type which can represent a computation which can fail. This data type has two possible states: ‘Failed’, and ‘Success(return_value)’ (where the ‘return_value’ field can be anything). Now, let’s say you have one result which might have failed, and you want to pass it to another fallible computation, which we can represent as a function from an input to a fallible output. How do you do it? Well, you might define a function something like this (in vaguely Rust-like pseudocode):
fn andThen(value, nextComputation) {
match value {
Success(return_value) => return nextComputation(returnValue);
Failed => return Failed;
}
}
That is, continue on with the next computation if you have a result, and short-circuit otherwise.Now let’s consider a different case: what if your computation is instead nondeterministic, and can return multiple results? In this case, you might instead have a nondeterministic value — let’s represent it as a list — which you got from one computation, and you need to supply it as input to another nondeterministic computation. Then you’ll need to write something like:
fn andThen_nondet(value, nextComputation) {
mut result = [];
foreach (v in value) {
result.concat(nextComputation(v));
}
return result;
}
Or a third case — promises for asynchronous values! Which in most languages have an ‘andThen’ method too, allowing you to take one asynchronous value and pass that value to an asynchronous computation. I’ll skip an example for this one since it’s so well-known, but obviously this ‘andThen’ method has roughly the same structure as the others I’ve shown.Monads, therefore, are nothing special — they’re just data types with an ‘andThen’ method to let you sequence them. The method always has the same form: inspect the input to temporarily separate any returned value(s) from their context, then call the next computation to wrap those value(s) back up in the required context. This gets neatly summarised in the Haskell type signature:
(>>=) :: Monad m => m a -> (a -> m b) -> m bIt's essentially a mini interpreter for tiny functionally pure segments of a program.
One tricky part of the interpretation is understanding the most familiar monadic data types in this paradigm. For example, the list monad representing discrete nondeterminism.
Writing about this so tersely is probably only going to confuse anyone who isn't already 90% of the way to understanding....