Not logged in. Login

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)
Updated Sat Sept. 01 2018, 18:24 by cameron.