*In the previous article, "Understanding Functor, Applicative, and Monad" we summarized the characteristics of each, and in this article we will look at how to utilize them with specific examples. The use of Monad and other tools facilitates error handling and makes the program more resistant to specification changes.
Author : Kenta Inoue
Introduction
In the last issue, we looked at the characteristics of Functor
,
Applicative
, and
Monad
. The following is a summary of them.
Functor m
: Createm A -> m B
fromA -> B
.Applicative m
: Createm A -> m B -> m C
fromA -> B -> C
Monad m
: Createm A -> m B
fromA -> m B
. (for any typeA,
B
,C
respectively)
This article will look at these applications.
Easier error handling
First, consider the following potentially error-prone functions
mdiv :: Integral a => a -> a -> Maybe a
mdiv x y =
if y == 0
then Nothing
else Just $ x `div` y
mdiv
is just a quotient operation, but the return value is of type
Maybe
, which returns
Nothing
when divided by 0
.
main = do
print $ 10 `mdiv` 2 -- Just 5
print $ 10 `mdiv` 0 -- Nothing
If you want to process a value that may be an error (for example, if you want to find a value that is five times this value), you cannot process it as it is, and you must separate the case by whether or not it is an error.
-- print $ 5 * (10 `mdiv` 2) -- error
print $ case (10 `mdiv` 2) of
Nothing -> Nothing
Just x -> Just (5 * x)
However, this becomes tedious when processing increases, and also makes readability worse.
So, let's rewrite it
using the Functor
operator <$>$>
.
Now, in Functor Maybe
, the operator <$>$>
has type
(a -> b) -> Maybe a -> Maybe b
, so
-- print $ 5 * (10 `mdiv` 2) -- error
print $ (5 *) <$> (10 `mdiv` 2) -- Just 25
The same process as above can be performed by writing.
Similarly, Applicative
and Monad
operations can be used as follows.
import GHC.Base(liftA2)
main = do
-- print $ (10 `mdiv` 2) * (10 `mdiv` 5) -- error
print $ (*) <$> (10 `mdiv` 2) <*> (10 `mdiv` 5) -- Just 10
print $ liftA2 (*) (10 `mdiv` 2) (10 `mdiv` 5) -- Just 10
-- print $ 10 `mdiv` 2 `mdiv` 5 -- error
print $ (10 `mdiv` 2) >>= (`mdiv` 5) -- Just 1
-- (<*>) :: Applcative m => m (a -> b) -> m a -> m b
-- liftA2 :: Applicative m => (a -> b -> c) -> m a -> m b -> m c
-- (>>=) :: Monad m => m a -> (a -> m b) -> m b
Easier to change specifications
Themdiv
mentioned earlier was a function that returned the type Maybe
, as we will
recapitulate.
mdiv :: Integral a => a -> a -> Maybe a
mdiv x y =
if y == 0
then Nothing
else Just $ x `div` y
However, if there are other processes that may generate errors, the Maybe
type
will return only
Nothing
when an error occurs, and it will be difficult to tell where the error occurred. Therefore,
to display the error statement, the specification is changed to the Either String
type as shown
below instead of the Maybe type.
mdiv :: Integral a => a -> a -> Either String a
mdiv x y =
if y == 0
then Left "Error: divided by zero"
else Right $ x `div` y
In the past, this change would require a major rewriting of the source code, but if the
processing uses only
operations such as Monad
, as in the previous program, the program can be passed without any
changes.
main = do
print $ (5 *) <$> (10 `mdiv` 2) -- Right 25
print $ (*) <$> (10 `mdiv` 2) <*> (10 `mdiv` 5) -- Right 10
print $ (10 `mdiv` 2) >>= (`mdiv` 5) -- Right 1
By writing in this manner in the form of Monad
, even if there are later changes
to the
specifications, major rewrites can be prevented as long as the processing as Monad
is the same.
Applications
Consider a program that performs database processing. First, import
database-related libraries.
import Control.Monad.Reader(ReaderT,liftIO)
-- Library for database processing.
import Database.Persistent
import Database.Persistent.Sqlite
import Database.Persistent.TH
Suppose we have data1::MyData
of type MyData
that can be stored in a certain
database. Let us consider a function dbOperation::DBIO ()
that registers this data1
to the
database and retrieves and displays the data as it is now registered. (DBIO
is a certain
Monad
).
Since it connects to an external database, error handling is essential to the implementation
of this function,
but in practice it can be easily defined as follows using the Monad
operation >>=
.
type DBIO a = ReaderT SqlBackend IO a
dbOperation :: DBIO ()
dbOperation =
insert data1 >>= get >>= liftIO . print
We will now look at the function types used for this dbOperation
.
insert :: (ommited) => val -> m (Key val)
get :: (ommited) => Key val -> m (Maybe val)
print :: Show a => a -> IO ()
class Monad m => MonadIO m where
liftIO :: IO a -> m a
-- (ommited) is a constraint on m and val, but omitted.
-- In this case, m = ReaderT SqlBackend IO, val = MyData.
insert
registers a variable of any data type val
that satisfies
certain constraints in
the database and returns a value of type m (Key val)
, where Key val
is the ID of the
val
data in the database, but may cause errors due to access to an external database, However,
errors can occur due to access to external databases, and it is not always possible to return a variable of type
key val
. Therefore, we return m (Key val)
wrapped in a certain Monad
's
m
to handle the error. (You can think of m
as Maybe
or
Either
.) Also, get
is a function that receives an ID value of type
Key val
, searches the database for that ID value, and returns the result as a
Maybe val
, but for the same reason as above, the return value is wrapped in m
. The
last function is print
, which outputs the data, but it is liftIO
to match the type.
Now we can use the operation >>=
in Monad m
, and
insert data1 >>= get >>= liftIO . print
type check passes.
Thus, even functions that actually include error handling for external database connections
can be written easily
and independently of error handling specifications using Monad
.
Incidentally, the actual error handling is done internally by the definition of
Monad m
, like the
handling of Nothing
when Monad Maybe
is defined.
Supplement
above insert data1 >>= get >>= liftIO . print
is the following
dbOperation :: DBIO ()
dbOperation = do
data1Id <- insert data1
dbdata1 <- get data1Id
liftIO $ print dbdata1
It is also possible to write it in a procedural language style, such as
deOperation :: DBIO ()
deOperation =
insert data1 >>= (\data1Id -> get data1Id >>= (\dbdata1 -> liftIO (print dbdata1)))
shorthand notation. This is consistent with the original program from Monad
's third axiom.
Summary
He described how to utilize Functor
, Applicative
,
Monad
, and others and
their characteristics.