<$>
and <*>
generalize n-ary function application in any Applicative
context. It might make sense to see it in terms of a few other things that are equivalent. I’m going to use append
instead, which is the named version of the <>
operator.
We can expand out split
to something equivalent.
split' = do
result <- split
pure result
This is just calling split, getting the result, and boxing it up again. We can also call it again, and do something with both results.
split'' = do
result1 <- split
result2 <- split
pure (append result1 result2)
This is straightforward and a useful pattern, but it’s also a lot of noise if it’s something we need to do a lot. So maybe we can generalize this a bit by moving our concrete assumptions (append
and split
) into arguments. This will give us something we can reuse. I’m just using flub
because I can’t think of a good name.
flub func action = do
result1 <- action
result2 <- action
pure (func result1 result2)
split'' = flub append split
But do we always want to use the same action for both arguments? It’s likely that we might want to supply two different actions. So lets remove that assumption and add another argument.
flub2 func action1 action2 = do
result1 <- action1
result2 <- action2
pure (func result1 result2)
split'' = flub2 append split split
We have to repeat split
twice here, but you can probably see that this is more useful. What if I have a function that takes more than 2 arguments? Well, one solution is to copy and paste, and extend it:
flub3 func action1 action2 action3 = do
result1 <- action1
result2 <- action2
result3 <- action3
pure (func result1 result2 result3)
And so on, and so on… However, it’s not a lot of fun to copy and paste code. We can reduce this by utilizing currying, and defining flub3 in terms of flub2.
flub3 func1 action1 action2 action3 = do
func2 <- flub2 func1 action1 action2
result3 <- action3
pure (func2 result3)
What is happening here? If I call flub2
with a function that takes 3 arguments, it will apply 2 of them and give me back a function that takes another argument. This helps with the boilerplate, and now I can keep adding flubs for more arguments.
flub4 func1 action1 action2 action3 action4 = do
func2 <- flub3 func1 action1 action2 action3
result4 <- action4
pure (func2 result4)
This looks pretty much exactly like flub3, so that likely means we have something we can generalize. Lets ignore the specific arguments for the moment and look at what both of them are doing:
- Running some action to get a function out of it
- Running another action to get some value out of it
- Applying the function to the value
So lets take out the specific arguments and generalize according to this commonality:
flubFn action1 action2 = do
func <- action1
result <- action2
pure (func result)
flub2 f a b = flubFn (map f a) b
flub3 f a b c = flubFn (flub2 f a b) c
flub4 f a b c d = flubFn (flub3 f a b c) d
This particular implementation of flubFn
is called ap
in the Prelude
, which is apply
from the Applicative
class implemented in terms of bind
. There is actually a law that says if something is both Monad
and Applicative
, then apply
and ap
have to agree. apply
has an operator alias of <*>
, and map
has the alias <$>
.
split'' = apply (map append split) split
split'' = map append split `apply` split
split'' = map append split <*> split
split'' = append <$> split <*> split
split'' = (<>) <$> split <*> split