This post demonstrates how to use the <$>
(fmap
) and <*>
(sequential application
) operators to refactor a function.
Let’s say we have a simple function to tell us if one string is an anagram of another string:
isAnagram :: String -> String -> Bool
isAnagram xs ys = sort xs == sort ys
In GHCI, we see how the function behaves:
λ> isAnagram "hello" "olleh"
True
λ> isAnagram "hello" "bye"
False
But let’s pretend the strings are represented as Maybe String
. Perhaps they’re coming from user input and may not be present. Now, we want our function signature to be Maybe String -> Maybe String -> Maybe Bool
. We can create a trivial (but kind of ugly) implementation that does this:
isAnagramMaybe :: Maybe String -> Maybe String -> Maybe Bool
isAnagramMaybe xs ys = case xs of
Nothing -> Nothing
Just s1 -> case ys of
Nothing -> Nothing
Just s2 -> return (isAnagram s1 s2)
The new isAnagramMaybe
function returns Just
with True
or False
if both strings are present. Otherwise, it returns Nothing
:
λ> isAnagramMaybe (Just "hello") (Just "olleh")
Just True
λ> isAnagramMaybe (Just "hello") (Just "bye")
Just False
λ> isAnagramMaybe (Just "hello") Nothing
Nothing
However, there’s a concise way to implement this function:
isAnagramMaybe2 :: Maybe String -> Maybe String -> Maybe Bool
isAnagramMaybe2 xs ys = isAnagram <$> xs <*> ys
Let’s try to make sense of what’s going on here.
First, let’s examine <$>
, which is called fmap
:
λ> :i <$>
(<$>) :: Functor f => (a -> b) -> f a -> f b
-- Defined in ‘Data.Functor’
To get to the implementation of isAnagramMaybe2
, let’s follow the types step-by-step. First, apply isAnagram
to the fmap operator, <$>
:
λ> :t (<$>) isAnagram
(<$>) isAnagram :: Functor f => f String -> f (String -> Bool)
By applying isAanagram
to the <$>
operator, Haskell infers the types for a
and b
. In this case a
is String
and b
is String -> Bool
.
Now we can apply the next argument of type Maybe String
(and we can now use <$>
as an infix operator):
λ> :t isAnagram <$> Just "hello"
isAnagram <$> Just "hello" :: Maybe (String -> Bool)
By applying Just "hello"
Haskell infers that f
represents a Maybe
, so it leaves us with the last part of the function, which is Maybe (String -> Bool)
.
Now we can apply the sequential application operator, <*>
. First, let’s examine its definition:
λ> :i <*>
class Functor f => Applicative (f :: * -> *) where
...
(<*>) :: f (a -> b) -> f a -> f b
...
-- Defined in ‘GHC.Base’
Great! This operator takes a f (a -> b)
and f a
to give us f b
. From the previous example, we now have a Maybe (String -> Bool)
which fits the first part of the function. Let’s apply it:
λ> :t (<*>) (isAnagram <$> Just "hello")
(<*>) (isAnagram <$> Just "hello") :: Maybe String -> Maybe Bool
We’re almost there, now! All we need is a Maybe String
to get our Maybe Bool
. We’ve applied our first Maybe String
, so let’s apply our second Maybe String
(and use <*>
as an infix operator):
λ> :t isAnagram <$> Just "hello" <*> Just "elloh"
isAnagram <$> Just "hello" <*> Just "elloh" :: Maybe Bool
Now we just parameterize this snippet to get the implementation of isAnagramMaybe2
:
isAnagramMaybe2 :: Maybe String -> Maybe String -> Maybe Bool
isAnagramMaybe2 xs ys = isAnagram <$> xs <*> ys
Let’s make sure it works:
λ> isAnagramMaybe2 (Just "hello") (Just "elloh")
Just True
λ> isAnagramMaybe2 (Just "hello") (Just "bye")
Just False
λ> isAnagramMaybe2 (Just "hello") Nothing
Nothing
Great! It works!
Now that we’ve simplified the implementation, we can also get crazy and build an even-more generalized version that takes any Applicative
of String
and returns the same Applicative
of Bool
. We can do this using the liftA2
function provided in Control.Applicative
:
import Control.Applicative
isAnagramMaybe3 :: Applicative a => a String -> a String -> a Bool
isAnagramMaybe3 = liftA2 isAnagram
λ> isAnagramMaybe3 (Just "hello") (Just "elloh")
Just True
λ> isAnagramMaybe3 (Just "hello") (Just "bye")
Just False
λ> isAnagramMaybe3 ["hello"] ["elloh"]
[True]
λ> isAnagramMaybe3 ["hello"] ["elloh", "bye"]
[True,False]
λ> isAnagramMaybe3 ["hello", "bye"] ["elloh", "eyb"]
[True,False,False,True]
Since List
implements the Applicative
typeclass, we can now use lists of strings to get the cross product of anagram comparisons!
Is this generalized implementation useful? Maybe. Is this fun? Definitely.