Crash Course to Applicative Syntax
The Applicative typeclass is ubiquitous in the Haskell world. It
sits between Functor and Monad in the typeclass hierarchy, and
allows us to represent a number of things elegantly in ways that
monads do not:
- Context-free parsing
- Concurrency
- Streaming, parallel consumption
- Validation with multiple errors
(Just to name a few.) When it comes to Monads, we have the >>=
operator. But since it's a bit tedious to work with, Haskell provides
do-notation as syntactic sugar. There is a language extension called
ApplicativeDo, which allows us to (ab)use do-notation for
Applicative as well. However:
- Subjectively, it doesn't seem to be a very commonly used extension
- Even if you choose to use this extension in your own code, you'll
ultimately still need to read other people's code, which will be
using the
Applicativefunctions and operators directly
This tutorial covers Applicative in a syntactic way. We're not
going to be learning the intuition behind Applicative, much less
Functor or Monad. For some of that motivation, check out
Functors, Applicatives, and
Monads,
or read the appropriate section of Haskell Programming from First
Principles.
Functions and operators
You should be aware of the following:
pure- wraps up a pure value into some kind ofApplicative. Nowadays, this is the same asreturn, and is typically preferred over the latterliftA2- applies a pure function to the values inside twoApplicative-wrapped values<$>- operator version offmap. This is really part ofFunctor, but it's used regularly withApplicativesyntax<*>- apply a wrapped function to a wrapped value*>- perform the effects of the thing on the left, throw away its result, then perform the effects of the thing on the right and wrap its result. (Same as>>inMonad.)<*- perform the effects of the thing on the left, then perform the effects of the thing on the right, throw away the right result value, and keep the left result value. Note bene This is not the same asflip (*>), we'll see more below.
In addition to liftA2, there are other functions like liftA3,
liftA4, etc. In my experience, most people use the <$> and <*>
operators instead of the liftA* functions. Your mileage may vary.
Most common use cases
You'll often see code that looks like this when parsing JSON values
using the aeson library:
Person
<$> parseString "name" o
<*> parseInt "age" o
<*> parseTelephone "telephone" o
This will parse name, then parse age, then parse telephone. It
would take those three values, and apply the Person data constructor
to them. If your Applicative is also a Monad (or you turn on
ApplicativeDo), you can get away with do-notation:
name <- parseString "name o
age <- parseInt "age" o
telephone <- parseTelephone "telephone" o
pure $ Person name age telephone
This second version is more "pointful": you have to define extra variable names.
You can also write the code above as:
liftA3 Person
(parseString "name" o)
(parseInt "age" o)
(parseTelephone "telephone" o)
Or:
pure Person
<*> parseString "name" o
<*> parseInt "age" o
<*> parseTelephone "telephone" o
Assuming the Applicative and Monad laws have all been obeyed,
these are guaranteed to behave identically.
Parsers
Parsing JSON is nice, because you don't typically have to worry about
the order in which you parse the fields from an object. Lets say
you're parsing something textual. You might use a monadic interface
with do-notation.
parsePerson :: Parser Person
parsePerson = do
string "Name: "
name <- takeWhile (/= '\n')
endOfLine
string "Age: "
age <- decimal
endOfLine
pure $ Person name age
Let's try to do this with Applicative from what we've seen so
far. Let's say the string and endOfLine parser combinators each
return a () value that we want to ignore. We can do this parser with
a helper function:
helper :: () -> Text -> () -> () -> Int -> () -> Person
helper () name () () age () = Person name age
parsePerson :: Parser Person
parsePerson = helper
<$> string "Name: "
<*> takeWhile (/= '\n')
<*> endOfLine
<*> string "Age: "
<*> decimal
<*> endOfLine
That works, but it's tedious. We can improve the situation with *>
and <*.
parsePerson :: Parser Person
parsePerson = Person
<$> (string "Name: " *> takeWhile (/= '\n') <* endOfLine)
<*> (string "Age: " *> decimal <* endOfLine)
Notice that we used *> to say "ignore the value on the left" and
<* to say "ignore the value on the right." However, the effects
still flow from left to right, meaning that our parsing will work as
expected.
Let's say that we redefine our Person datatype to take age as its
first field, not name. You may be tempted to rewrite the parser
above as:
parsePerson :: Parser Person
parsePerson = Person
<$> (string "Age: " *> decimal <* endOfLine)
<*> (string "Name: " *> takeWhile (/= '\n') <* endOfLine)
However, this is now a different parser! It's expecting Age to come
before Name in the text it's parser. Instead, in this case, you'd
need to do something like:
parsePerson :: Parser Person
parsePerson = (\name age -> Person age name)
<$> (string "Name: " *> takeWhile (/= '\n') <* endOfLine)
<*> (string "Age: " *> decimal <* endOfLine)
Or more code-golfy:
parsePerson :: Parser Person
parsePerson = (flip Person)
<$> (string "Name: " *> takeWhile (/= '\n') <* endOfLine)
<*> (string "Age: " *> decimal <* endOfLine)
More examples
#!/usr/bin/env stack
-- stack script --resolver lts-12.21
import Conduit
import UnliftIO
main :: IO ()
main = do
write2Files
runConduitRes $
(sourceFile "file1.txt" *> sourceFile "file2.txt") .|
sink
write2Files = runConcurrently $
Concurrently (writeFile "file1.txt" "this is file 1")
*> Concurrently (writeFile "file2.txt" "this is file 2")
sink = getZipSink $
ZipSink (sinkFile "output1.txt")
*> ZipSink (sinkFile "output2.txt")