Merely monadic

From HaskellWiki
Revision as of 04:25, 16 March 2021 by Atravers (talk | contribs) (Former "introdutorial" added to tutorial category)

Jump to: navigation, search

Introduction

In Haskell, monadic types - types having an instance for the Monad class - can be thought of as abstract descriptors of computations which are inherently composable - smaller monadic expressions (actions) can be used to build larger ones.

This monadic interface (as specified by Monad) provides actions additional flexibility in separating:

  • the time of composition: when it is defined;
  • the time of execution: when it is used;
  • the mode of computation: what else it can do, in addition to emitting its single (hence the name) output value.

Monadic operations

Ideally, each monadic type in Haskell should satisfy the monad laws by providing two basic operations:

  • return :: Monad m => a -> m a:
argument :: a another Haskell value
result :: m a an action, merely returning the argument's value.
  • (>>=) :: Monad m => m a -> (a -> m b) -> m b:
argument #1 :: m a an action
argument #2 :: (a -> m b) a suitable Haskell function (a reaction)
result :: m b

another action, the result of binding the output value of argument #1
(the action) to the input of argument #2 (the reaction).

Together, these two operations allow many of the specific (and often-repetitive) details of the computational processes used by actions to be abstracted away. This often makes the monadic interface the preferred choice for abstract data types, where the inner workings of monadic actions must be kept private e.g. to ensure they work as intended.

As a reaction uses its argument to build a new monadic result, it can be thought of as an abstract constructor for new actions. In this way return can then be seen as the trivial-action constructor for new actions: the monadic counterpart to the identity function Prelude.id:

id   :: a -> a
id x =  x

IO, in particular

The IO type is special because a Haskell program is one large IO action (usually) called main - for any other IO action to work, it should be part of the chain of I/O actions composed together to form main. Execution of a Haskell program then starts by the Haskell implementation calling main, according to the implementation's private definition of the abstract IO type.

Using monadic actions

Assuming:

  • M is a monadic type;
  • x :: a is some other Haskell value;
  • m :: M a is an action;
  • k :: (a -> M b) is a reaction;
  • M, x, m, k are well-behaved;

then:

(a) r1 = k $ x  :: M b (a new action)
(b) r2 = return x  :: M a (an action whose output value is x)
(c) r3 = m >>= k  :: M b (another new action)
(d) r4 = k =<< m  :: M b (the same as r3)
(e) r5 = k $ m (types don't match)
(f) r6 = k <$> m  :: M (M b) (an action whose output value is also the same as r3)
(g) r7 = join (k <$> m)  :: M b (the same as r3)

where:

infixr 0 $
($)     :: (a -> b) -> a -> b
f $ x   =  f x

infixr 1 =<<
(=<<)   :: (a -> M b) -> M a -> M b
k =<< m =  m >>= k

infixl 4 <$>
(<$>)   :: (a -> b) -> M a -> M b
f <$> m =  m >>= \x -> return (f x)

join    :: M (M a) -> M a
join m  =  m >>= \x -> x

Even though it is a purely-functional language, Haskell can facilitate the use of effect-based computations by presenting them as monadic actions (using a suitable type), with the monadic interface helping to maintain purity. This is the result of the aformentioned separation of concerns, most notably the time of composition. Monadic actions thus resemble programs in a particular EDSL (embedded domain-specific language), "embedded" because they are legitimate Haskell values which describe impure computations - no extraneous annotations are needed. This is how monadic types in Haskell help keep the pure and the impure apart. But not all monadic actions are impure - for them, the benefits of separating concerns are often combined with the automatic formation of a computational "pipeline".

Because they are very useful in practice but rather confronting for beginners, numerous monad tutorials (including this one) exist to help with the learning process.

Supplemental operations for monadic types (non-standard morphisms)

In addition to the two basic ones required by the monadic interface, each monadic type can have its own specific operations e.g. to provide access to and/or enable manipulation of data it implicitly carries which is specific to its nature; cause some specific side-effects; etc.

Running monadic actions

Some monadic types provide an extra operation for running actions, usually if they are purely defined by the type. This allows an action's output value to be obtained directly in Haskell, in contrast to (>>=). While some of the predefined (e.g. from the Prelude) monadic types in Haskell have such an operation (e.g. Prelude.maybe or Prelude.either), most do not.

Why this is expressly not a part of the monadic interface by now should be obvious - impure monadic actions could then be "hidden" inside seemingly-pure definitions, which would end any notion of purity in Haskell. This is why the only general-purpose way to use a monadic action's output value is to direct it to subsequent computations via the reaction which constructs them, through the use of (>>=).