Skip to content
AgileLogics
6 min read

Applicative

Prerequisites: Functor and Foldable

fmap applies a one-argument function inside a container. What about a function with two arguments, where both values are wrapped?

main :: IO ()
main = do
  print ((+) <$> Just 3 <*> Just 4)
  print ((+) <$> Just 3 <*> Nothing)
  print (max <$> Right 10 <*> (Left "oops" :: Either String Int))
  print (pure 5 :: Maybe Int)
  print ((,) <$> Just 'a' <*> Just True)
Just 7
Nothing
Left "oops"
Just 5
Just ('a',True)

How it works:

  1. (+) <$> Just 3 is Just (+ 3) — a function inside a Maybe.
  2. <*> ("apply") applies a wrapped function to a wrapped value.
  3. If anything is Nothing (or Left), the whole result is.
-- illustrative: the class definition
class Functor f => Applicative f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

pure puts a plain value into the container with no extra effect.

Building records from validated parts

The pattern Constructor <$> a <*> b <*> c is the everyday use of Applicative:

import Text.Read (readMaybe)

data Person = Person {name :: String, age :: Int}
  deriving (Show)

validName :: String -> Maybe String
validName "" = Nothing
validName n = Just n

validAge :: String -> Maybe Int
validAge s = case readMaybe s of
  Just a | a >= 0 && a < 150 -> Just a
  _ -> Nothing

mkPerson :: String -> String -> Maybe Person
mkPerson n a = Person <$> validName n <*> validAge a

main :: IO ()
main = do
  print (mkPerson "Ada" "36")
  print (mkPerson "" "36")
  print (mkPerson "Ada" "-1")
Just (Person {name = "Ada", age = 36})
Nothing
Nothing

Lists as Applicative

For lists, <*> combines every function with every value — like a nested loop or list comprehension:

main :: IO ()
main = do
  print ((+) <$> [1, 2] <*> [10, 20, 30])
  print ((,) <$> "ab" <*> [True, False])
  print (zipWith3 (,,) [1, 2] "xy" [True, False])
[11,21,31,12,22,32]
[('a',True),('a',False),('b',True),('b',False)]
[(1,'x',True),(2,'y',False)]

IO as Applicative

IO is Applicative too, so you can combine the results of actions without naming them:

-- Try it: printf 'Ada\nLovelace\n' | runghc main.hs
main :: IO ()
main = do
  fullName <- (\a b -> a ++ " " ++ b) <$> getLine <*> getLine
  putStrLn ("Hello, " ++ fullName)
Hello, Ada Lovelace

*> runs two actions and keeps the second result, <* keeps the first. Applicative is less powerful than Monad (next lesson): each step can't depend on the result of the previous one — which is exactly what makes it useful for validation, parsing, and running things in parallel.