What if we need to apply a binary function to values from two lists? A C++ programmer would write a binary function or lambda and pass it to the chosen traversal algorithm. In Haskell, is curried: after receiving one argument, it returns a function waiting for the second. If we map over the first list, we get not sums but a list of partially applied functions: has type . In other words, the functions are now inside the list context.
An ordinary can apply a function to values in , but it cannot apply functions from to values from another . That requires a stronger type class: and its operator.
The class extends with two operations:
class Functor f => Applicative f where
pure :: a -> f a -- put a plain value into a context
(<*>) :: f (a -> b) -> f a -> f b -- apply a function from a contextIn this declaration, is a type constructor of kind . The operation puts a plain value into a context, while applies a function in a context to a value in the same context. Each instance defines the concrete semantics of these operations. The standard instances for lists and work as follows:
instance Applicative [] where
pure x = [x]
fs <*> xs = [g x | g <- fs, x <- xs]
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
Just g <*> x = fmap g xJust (+3) <*> Just 2 -- Just 5Lifting is an important idea for applicative functors: an ordinary function of several arguments is “lifted” into a context so that it accepts arguments in contexts and returns its result in the same context. lifts a binary function, while lifts a ternary function:
liftA2 (+)
(Just 2)
(Just 3)
-- Just 5
liftA3 (\x y z -> x + y + z)
(Just 1)
(Just 2)
(Just 3)
-- Just 6There are four laws, and programmers are responsible for obeying them: an implementation may pass type checking while still breaking the laws. After defining an instance, we therefore need to verify all four conditions:
pure id <*> v == v -- identity
pure g <*> pure x == pure (g x) -- homomorphism (a structure-preserving mapping)
u <*> pure y == pure ($ y) <*> u -- interchange
pure (.) <*> u <*> v <*> w == u <*> (v <*> w) -- compositionThe laws ensure that introduces no extra effect, contextual application agrees with ordinary function application, and regrouping a chain of does not change its result. They do not, however, permit effects to be reordered arbitrarily: order may still matter.
A couple of list examples, perhaps:
-- Every function is applied to every value
[(+1), (*2)] <*> [10, 20]
-- [11, 21, 20, 40]
-- Partially applied addition: again, all combinations
(+) <$> [1, 2] <*> [10, 20]
-- [11, 21, 12, 22]