Applying Functions to Wrapped Values
Unwrapping Values is Tedious
Let's look at our domain of algebraic expressions and the
evalmath
function again.
data ME = Num Int | Var Char | Group ME |
Add ME ME | Sub ME ME | Mul ME ME | Power ME Int | Neg ME
deriving (Show, Ord, Eq)
The evalmath
function evaluates algebraic expressions in
the context of an environment.
evalmath :: [(Char, Int)] -> ME -> Maybe Int
For example,
*Main> evalmath [('x', 3)] (Add (Var 'x') (Var 'x')) Just 6 *Main> evalmath [('x', 7), ('y', -1)] (Mul (Add (Var 'x') (Num 2)) (Var 'y')) Just (-9)
Our evalmath
implementation used a recursive program structure,
with pattern matching to process the results.
evalmath env (Num n) = Just n
evalmath env (Var c) = lookup c env
evalmath env (Group g) = evalmath env g
evalmath env (Add f g) =
case (evalmath env f, evalmath env g) of
(Just x, Just y) -> Just (x + y)
_ -> Nothing
evalmath env (Sub f g) =
case (evalmath env f, evalmath env g) of
(Just x, Just y) -> Just (x - y)
_ -> Nothing
evalmath env (Mul f g) =
case (evalmath env f, evalmath env g) of
(Just x, Just y) -> Just (x * y)
_ -> Nothing
evalmath env (Power f n) =
case (evalmath env f) of
(Just x) -> Just (x ^ n)
_ -> Nothing
evalmath env (Neg f) =
case (evalmath env f) of
(Just x) -> Just (-x)
_ -> Nothing
Each type we get a result we are checking for a proper result wrapped in Just
, or an error signalled by Nothing
.
This seems tedious.
Applying fmap
Recall the fmap
function which generalizes map for any wrapped structures.
class Functor f where
fmap :: (a -> b) -> f a -> f b
The Maybe
class is an instance of Functor
.
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap f Nothing = Nothing
For example:
*Main> fmap (0-) (Just 3)
Just (-3)
What fmap
allows us to do is to take an ordinary function
and apply it to a wrapped value to get a wrapped result. This is
shown by the type signature.
*Main> :t fmap (0-) fmap (0-) :: (Num b, Functor f) => f b -> f b
In our case the functor is Maybe
.
For our final equation in evalmath
, we can now eliminate
the pattern matching, fmap
will take care of the
unwrapping and rewrapping.
evalmath env (Neg f) = fmap (0-) (evalmath env f)
The Applicative
Type Class
Can we use fmap
for the other equations? What happens if
we try to use it on the plus (+)
function?
*Main> :t fmap (+) fmap (+) :: (Num a, Functor f) => f a -> f (a -> a)
The problem is that fmap
will give us a function wrapped in Maybe
.
*Main :t fmap(+) (Just 4) fmap(+) (Just 4) :: Num a => Maybe (a -> a)
The <*>
Operator (ap
)
What we need now is a function to apply a wrapped function to a wrapped value. Fortunately there is a type class Applicative
for this purpose, with an operator <*>
that fits the bill.
*Main> import Control.Applicative *Main Control.Applicative> :t (<*>) (<*>) :: Applicative f => f (a -> b) -> f a -> f b *Main Control.Applicative> fmap (+) (Just 4) <*> (Just 3) Just 7
Let's look at the type if we just give the first argument to <+>
.
*Main Control.Applicative> :t (fmap (+) (Just 4) <*>) (fmap (+) (Just 4) <*>) :: Num b => Maybe b -> Maybe b
It takes wrapped values to wrapped values, just what is needed. So we can now simplify the equations for the binary operations.
evalmath env (Add f g) = fmap (+) (evalmath env f) <*> (evalmath env g) evalmath env (Sub f g) = fmap (-) (evalmath env f) <*> (evalmath env g) evalmath env (Mul f g) = fmap (*) (evalmath env f) <*> (evalmath env g) evalmath env (Power f n) = fmap (^) (evalmath env f) <*> (Just n)
The liftA2
Function
In essence, we can use fmap
directly apply a unary function to a wrapped value, but fmap
gives us a wrapped function when we apply it to binary functions.
With <*>
, we can then apply a wrapped function to a wrapped value. But since the case of binary functions is so frequent, there is a function liftA2
that "lifts" to unwrapped values into the applicative class.
*Main Control.Applicative> :t liftA2 liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c *Main Control.Applicative> liftA2 (+) (Just 4) (Just 8) Just 12
So this allows us to simplify evalmath
even further.
evalmath :: [(Char, Int)] -> ME -> Maybe Int
evalmath env (Num n) = Just n
evalmath env (Var c) = lookup c env
evalmath env (Group g) = evalmath env g
evalmath env (Add f g) = liftA2 (+) (evalmath env f) (evalmath env g)
evalmath env (Sub f g) = liftA2 (-) (evalmath env f) (evalmath env g)
evalmath env (Mul f g) = liftA2 (*) (evalmath env f) (evalmath env g)
evalmath env (Power f n) = liftA2 (^) (evalmath env f) (Just n)
evalmath env (Neg f) = fmap (0-) (evalmath env f)