University of Zagreb
Faculty of Electrical Engineering and Computing
Programming in Haskell
http://www.fer.unizg.hr/predmet/puh
Academic Year 2014/2015
© 2014 Luka Skukan
Version 1.0
We are now familiar with both Functor
and, with some instances of Monad
, most notably IO
. However, Applicative
, or Applicative Functors, are something in-between the two and something we have not (and will not) cover.
Applicative functors have less constraints on them than monads, but more than functors. In some cases they are far more readable. However, they can seem quite arcane at first. We will try to explain them by comparing them to Functors and do
notation.
There are several operators we need to understand:
pure
<$>
<*>
*>
<*
Let us go through them in order.
pure
¶The pure
action wraps an existing value and offers it it a "pure" context. It is basically the same thing as the return
function for monads, only something doesn't have to be a Monad
to implement pure
. This is how pure
is implemented for some common instances of Applicative
:
-- Maybe
pure = Just
-- []
pure = (:[])
-- Either
pure = Right
-- IO
pure = return
As you can see, there is nothing especially exciting about pure
. However, it is a crucial functions that allows us to "lift" our computations to the Applicative
level.
NB: For Maybe
and []
, pure
is actually implemented as pure = return
. However, return
is implemented as shown.
<$>
¶The application function <$>
is actually just another name for fmap
. We can convince ourselves of that by looking at the types.
import Control.Applicative (pure, (<$>), (<*>), (<*), (*>), (<$))
:t fmap
:t (<$>)
Both of these apply some function over a wrapped object. Essentially, <$>
unwraps the object, evaluates the function, then re-wraps the result. We can also express this using do
notation (although this actually introduces more constraints on the object, but we will not go into this). Let us define our own mock-<$>
called <<$>>
.
infixl 4 <<$>>
f <<$>> a = do
x <- a
return $ f x
abs <<$>> Just 3
abs <<$>> Just (-3)
abs <<$>> Nothing
Just 3
Just 3
Nothing
As we can see, this works precisely like fmap
. And this is perfectly fine for unary functions. However, what about binary functions? What would we get by doing this?
(+) <<$>> Just 2
Well, the answer is quite simple, we would get Just (2+)
. However, doing anything with this is pretty difficult. This is where the <*>
function needs to be introduced.
<*>
¶The <*>
function allows us to apply functions to multiple wrapped arguments. This is incredibly useful! Let us begin by looking at the type.
:t (<*>)
This takes a wrapped function (a -> b)
, a wrapped argument of type a
and returns a wrapped argument of type b
. It is very similar to fmap
and <$>
, but those take the function unwrapped. However, if it is a function of arity 2 or higher, we will not get a wrapped result. Well, we will, but that result will be a function. To keep applying it, <*>
is precisely what we need. Let us try it out!
(+) <$> Just 2 <*> Just 3
(+) <$> Just 1 <*> Nothing
(+) <$> Nothing <*> Just 9
(+) <$> Nothing <*> Nothing
Just 5
Nothing
Nothing
Nothing
This should be pretty clear on such a basic functor as Maybe
. But let us write our own implementation of it using do
notation.
infixl 4 <<*>>
f <<*>> a = do
f' <- f
x <- a
return $ f' x
(+) <<$>> Just 2 <<*>> Just 3
(+) <<$>> Nothing <<*>> Just 3
(+) <<$>> Just 2 <<*>> Nothing
Just 5
Nothing
Nothing
So this seems to work. While the types are wrong (do
notation requires something to be a Monad
), the functionality is the same. A lot of Applicative
instances are Monads, so this will work on quite a few of them.
So far, were were able to explain these functions via the concepts of wrapping and unwrapping. However, the next three require the introductions of actions. We will turn to our old friend, IO
.
*>
and <*
¶The two functions are quite similar. As before, we will first look at their types, then try to use that to explain what they do.
:t (*>)
:t (<*)
We will now say that f a
and f b
are no longer just wrappers around something, but actions that produce something. For example, the getLine
action would be f String
; IO String
to be more precise.
So, these operators both take two actions and do something with them? What exactly?
Well, they evaluate both. However only one of their results is actually kept - the one the bracket points to - and the other is simply discarded. Yet, both actions are forced to happen (in order, left then right). Let us try this out on a few examples.
To properly experience them, either run these in IHaskell or copy them to ghci or a file. Remember to import Control.Applicative
.
readFirst :: IO String
readFirst = getLine <* getLine
readSecond :: IO String
readSecond = getLine *> getLine
readMiddle :: IO String
readMiddle = getLine *> getLine <* getLine
readFirst
readSecond
readMiddle
"a"
"d"
"f"
As you can see, you are prompted two (or three in the last example) times, but only one result is actually kept. Now, this might not be especially useful for IO, but there are some use cases where having this is a must. The most notable of them is parsing - something we want to check a string is contained in some text, but we don't actually have to use it for anything once we know it exists.
Let us create our own implementations of those two functions with do
.
import Control.Monad (void)
infixl 4 <<*
infixl 4 *>>
a <<* b = do
x <- a
void b
return x
a *>> b = do
void a
b
myMiddle :: IO String
myMiddle = getLine *>> getLine <<* getLine
myMiddle
"bla"
Note the void
function. It is not mandatory here, but ghc will complain otherwise. It takes a function that has a result (like IO String
this this case) and just discards it, making the function return ()
(more precisely, IO ()
in our example).
There are a few other functions if interest, such as <$
and $>
, but we will not go into these. You will commonly not need them, but you may research them if you find them useful.