3 ms·
Monads are an interface over container types, often, that allows you to manipulate and build up values in the container easily. They come with associated laws t
by tel 13y ago
Monads are an interface over container types, often, that allows you to manipulate and build up values in the container easily. They come with associated laws that allow for this kind of containerized-processing to represent an enormous array of interesting real-world tasks---failure, non-determinism, CPS, state, local context, logging, IO, random processes, parallel computation, graph building, the "builder pattern", and logic programming all came to mind immediately.
Concretely, imagine computations that might fail. In Haskell we represent this by the "possibly empty container with one slot" called Maybe. Maybe Int is either "just" an integer written (Just 3) or nothing at all, written Nothing. Frequently you'll use many failing processes all at once to build a result and want the failure of any one of them to propagate. You might do a bunch of map lookups to build a Person.
-- lookup :: Ord k => k -> Map k v -> Maybe v
type Name = String
type Address = String
type Age = Int
data Person Name Address Age
type AddressMap = Map Name Address
type AgeMap = Map Nage Age
mkPerson :: AddressMap -> AgeMap -> Name -> Maybe Person
mkPerson addys ages name =
case lookup addys name of
Nothing -> Nothing
Just addy -> case lookup ages name of
Nothing -> Nothing
Just age -> Person name addy age
That pattern of diving down deeper into a potentially failing result happens to follow the Monad interface. We think of each possibly failing value as being able to "bind" a next action which is only carried out if the computation doesn't fail. In other words, we package up the Just branches as continuations.
mkPerson addys ages name =
bind (lookup addys name) (\addy ->
bind (lookup ages name) (\age ->
return (Person name addy age)
)
)
The `return` bit makes the non-failing computation (Person name addy age) into a non-failing Maybe computation---it injects a non-monadic computation into a monad in "as boring of a way as possible". (Also note that `bind` is usually called (>>=) so you can use it infix.)
In general, this pattern is so nice that there's some special syntax sugar for it called `do notation`.
mkPerson addys ages name = do
addy <- lookup addys name
age <- lookup age name
return (Person name addy age)
which desugars to exactly the same thing as the previous example.
The crazy bit is that this pattern, once you become familiar with the monadic interface, is extremely pervasive. Usually when you recognize it in some kind of computation it sheds new light on both how that computation can be manipulated and how it can be combined with other kinds. It's not infrequent to combine monads into a complex notion like---"a potentially infinite non-deterministic failing computation carried out in context of some configuration data where we can log and deterministically open and close resources"... which is written directly as the type
ReaderT ConfigData (WriterT LogMessage (LogicT (ErrorT ErrorType (SafeT m))))
Or, we can define a parser as a potentially-failing computation which modifies some parser state in the context of the current parse
newtype Parser a = Parser (ReaderT Context (MaybeT (State Stream)) a)
parsePerson :: Parser Person
parsePerson = do
name <- parseName
_ <- parseSeparator
addy <- parseAddress
_ <- parseSeparator
age <- parseAge
return (Person name addy age)