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:

(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:

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:

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")