Why Applicative?
Originally posted on dev.to/gillchristian.
When learning about the different type classes in Haskell the one I struggled the most with was, by far, Applicative.
Functor
Functor is, at least to some extent, straightforward. We take any unary functions and make them work on some (functor) context.
fmap :: Functor f => (a -> b) -> f a -> f b
-- or the infix version
(<$>) :: Functor f => (a -> b) -> f a -> f b
Say we have an increment function that works on Int
s.
inc :: Int -> Int
位> inc 1
2
By using fmap
we can map any functor that contains an Int
.
位> fmap inc [1, 2, 3]
[2, 3, 4]
位> fmap inc (Just 1)
Just 2
位> fmap inc (Right 1)
Right 2
That becomes very clear when we align fmap
with the function application operator.
($) :: (a -> b) -> a -> b
(<$>) :: Functor f => (a -> b) -> f a -> f b
inc $ 1 -- 2
inc <$> [1] -- [2]
inc <$> (Just 1) -- Just 2
fmap is just function application inside a context.
By the way, we'll use the infix version from now.
Applicative
In the case of applicative it's not clear. Or at least it took longer to click for me.
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
Comparing with ($)
doesn't really help. Why would I want to also have the function in the context?
($) :: (a -> b) -> a -> b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
We said functors allow to apply a unary function in a context. But what happens if I want to apply a function with a higher arity?
add :: Int -> Int -> Int
add <$> (Just 1) -- ??
What does the repl says? 馃
位> :t add <$> (Just 1)
add <$> (Just 1) :: Maybe (Int -> Int)
Maybe (Int -> Int)
? Yes, we saw that already in the (<*>)
signature.
add <$> (Just 1) <*> (Just 2) -- Just 3
Let's dissect that 馃攳
-- refresh these ones first :)
(<$>) :: Functor => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
-- With Maybe applied (using TypeApplications extension)
(<$>) @Maybe :: (a -> b) -> Maybe a -> Maybe b
add :: Int -> Int -> Int
-- With Int applied in place of 'a'
(<$>) @Maybe @Int :: (Int -> b) -> Maybe Int -> Maybe b
-- With (Int -> Int) applied in place of 'b'
(<$>) @Maybe @Int @(Int -> Int)
-- (a -> b) -> Maybe a -> Maybe b
:: (Int -> Int -> Int) -> Maybe Int -> Maybe (Int -> Int)
add <$> (Just 1) :: Maybe (Int -> Int)
Here we see the first interesting thing. Since our add
function takes two arguments (or to be more accurate one at a time). But we only provide one (the Int
from Maybe Int
), so it gets partially applied and returns a function (Int -> Int
). So b
is Int -> Int
.
-- a -> b
add :: Int -> (Int -> Int)
Note that parens aren't actually needed since the arrow (->
) is right associative.
That was the first part of the expression. We are missing the applicative.
(<*>) @Maybe :: Maybe (c -> d) -> Maybe c -> Maybe d
-- With Int in place of 'c'
(<*>) @Maybe @Int :: Maybe (Int -> b) -> Maybe Int -> Maybe b
-- And also Int in place of 'd'
(<*>) @Maybe @Int @Int
:: Maybe (Int -> Int) -> Maybe Int -> Maybe Int
Et voil脿
add :: Int -> Int -> Int
add <$> (Just 1) :: Maybe (Int -> Int)
add <$> (Just 1) <*> (Just 2) :: Maybe Int
Functor: apply unary functions in a context.
Applicative: apply n-ary functions in a context.
This is referred as lift in Haskell.
And the whole point of applying functions in such contexts is the semantics associated with them. It might be for validation, optional values (without null
馃槒), lists or trees of items, running IO actions, parsers.
When the context is Maybe:
位> add <$> (Just 1) <*> (Just 2)
Just 3
位> add <$> Nothing <*> (Just 2)
Nothing
位> add <$> (Just 1) <*> Nothing
Nothing
位> add <$> Nothing <*> Nothing
Nothing
When the context is Either:
位> add <$> (Right 1) <*> (Right 2)
Right 3
位> add <$> (Left "err 1") <*> (Right 2)
Left "err 1"
位> add <$> (Right 1) <*> (Left "err 2")
Left "err 2"
位> add <$> (Left "err 1") <*> (Left "err 2")
Left "err 1"
When the context is List:
位> add <$> [1, 2, 3] <*> [1, 2, 3]
[2,3,4,3,4,5,4,5,6]
位> (,) <$> [1, 2, 3] <*> [1, 2, 3]
[(1,1),(1,2),(1,3),(2,1),(2,2),(2,3),(3,1),(3,2),(3,3)]
鈽濓笍 More on that on the next one.
Conclusion
When learning functional programming all these type classes might seem scary. Developing a basic intuition of their purpose and usages is a big part of the process of getting comfortable using (and understanding) them.
I know there are more implications around Functor and Applicative that I have yet to discover. But as any learning process, it takes time. I'm sure more things will become clear and start sink in as I keep going.
But that's all for today.
Happy and safe coding 馃帀