Rather than explain what an applicative is, we are only truly concerned with how to use them.
Skip to the end to read the “golden rules” of applicative functors.
Consider a datatype which belongs to the Aplicative
typeclass in Haskell. Applicative
allows us to combine actions together, as long as the return result of the actions do not need to be used by other actions. Consider the following example:
main :: IO ()
main = do
str <- getFourLines
putStrLn str
getFourLines :: IO String
getFourLines = go <$> getLine <*> getLine <*> getLine <*> getLine where
go line1 line2 line3 line4 = line1 ++ line2 ++ line3 ++ line4
getFourLines
can also be written as:
getFourLines :: IO String
getFourLines = go
<$> getLine
<*> getLine
<*> getLine
<*> getLine
where
go line1 line2 line3 line4 = line1 ++ line2 ++ line3 ++ line4
getFourLines
will get four lines from stdin
and concatenate them together. We have four calls to getLine
. The first is prepended by <$>
, and the rest are prepended by <*>
. Then we have a function called go
, which is a generic name. go
takes four arguments, one for each call to getLine
. If we remove a call to getLine
…
getThreeLines :: IO String
getThreeLines = go
<$> getLine
<*> getLine
<*> getLine
where
go line1 line2 line3 = line1 ++ line2 ++ line3
…then go
only needs to take three arguments instead of four. The number of <$>
’s and <*>
’s equals the number of arguments to go
. If they don’t match, we get a compilation error.
If we only want the second and fourth lines, we could do the following:
getLinesTwoAndFour :: IO String
getLinesTwoAndFour = go
<$> getLine
<*> getLine
<*> getLine
<*> getLine
where
go _ line2 _ line4 = line2 ++ line4
We can deliberately ignore the return values of the first and third calls to getLine
once they get to the go
function. However, there is a cleaner way to do the same thing:
getLinesTwoAndFour :: IO String
getLinesTwoAndFour = go
<$ getLine
<*> getLine
<* getLine
<*> getLine
where
go line2 line4 = line2 ++ line4
If we remove the >
preceding the call to getLine
, then we perform the action, but toss the result. This works for both <$>
and <*>
.
Consider a different Applicative
instance using State
. We implement a basic stack. The implementation is not important, suffice to say that push
and pop
do exactly as expected:
import Control.Monad.State
push :: a -> State [a] ()
push a = modify (a:)
pop :: State [a] a
pop = state $ \(x: xs) -> (x, xs)
doSomething :: State [Int] Int
doSomething = go
<$> pop
<* pop
<*> pop
<* push 15
where go x y = x * y
doSomething
will pop three numbers off the top of the stack, ignore the second number, then push 15
onto the stack, We can test this:
ghci> runState doSomething [5, 2, 10]
(50,[15])
where 5
is the top of the stack and 10
is the bottom. doSomething
pops 5
, 2
and 10
off the stack, and pushes 15
onto it, leaving our stack to be [15]
. The return value is 50
(5
* 10
), therefore the result of runState
will be (50, [15])
.
Golden rules
We can now use some easy mnemonics for the basic use of Applicative
if we are to write functions in the following way:
applicativeAction = go
<$ action1
<*> action2
<* action3
<*> action4
where
go line2 line4 = doSomething line1 line2
- Each applicative action must be individually prepended with one of either
<$>
,<*>
,<$
, or<*
. - Precede the action with
<*
or<$
if you want to perform the action, but ignore the result. - The first action must be preceded by
<$>
or<$
, and not by<*>
or<*
. - The number of arguments to the
go
function (or whatever you want to call it) must match the number of<$>
plus the number of<*>
.
This is a simple and generic way to use the basic functionality of Applicative
.