I can mostly stumble my way through my Haskell questions, but I haven't found a better solution to my problem.
Suppose that I have a function f
that takes 5 parameters and I want to create a list of partially-applied functions that have the first 3 parameters applied, but different in every element of the list.
For example, let's say that f :: Num a => a -> a -> a -> b -> b -> c
and I want to end up with [b -> b -> c]
as the type of the result. One of the functions might be f 1 3 5
and another might be f 6 4 2
.
With 1 argument I could just do something like
map f [1..4]
to get f 1
, f 2
, etc., and with 2 args I could do
map (uncurry f) $ zip [1..3] [6..8].
Now for 3 args I could do
map (uncurry $ uncurry f) $ zip (zip [1..3] [6..8]) [3..5]
but this is getting awfully ugly awfully fast. Is there a more elegant (or idiomatic) way to do this (aside from making my own "uncurry3" function to pair with zip3
)? I've always run into an elegant solution with Haskell, and this seems very clumsy.
Sorry if this is a newbie question or has been answered before. Thanks.
You can shorten your 2-argument code with zipWith
:
zipWith f [1..3] [6..8]
And conveniently, there are is actually a zipWith3
(and so on up to 7) defined in the standard library.
There are also parallel list comprehensions with -XParallelListComp
which seem to go up to any number:
[f a b c | a <- [1..3] | b <- [6..8] | c <- [3..5]]
This is actually one way to define an Applicative instance for Lists.
Recall that Applicative's definition revolves around the definition of (<*>)
:
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
And if you specialize for []
, you can get:
(<*>) :: [a -> b] -> [a] -> [b]
Maybe this is starting to look like a way you can make this happen? You have a list of functions, and you can apply them to a list of values. Perhaps we can make (<*>)
work in a way so that it applies the list of functions to the list of values like a zip:
fs <*> xs = zipWith ($) fs xs
Recall ($)
, the function application operator:
($) :: (a -> b) -> a -> b
f $ x = f x
So zipWith
"zips" a list of functions and a list of values and returns the result of applying each function to the corresponding value.
I think you should probably be able to take it from here. Let's add together two lists:
(fmap (+) [1,2,3]) <*> [4,5,6]
which turns into
[(1+), (2+), (3+)] <*> [4,5,6]
which turns into
[1+4, 2+5, 3+6]
and
[5, 7, 9]
How about a three argument function?
f x y z = x * y + z
((fmap f [1,2,3]) <*> [4,5,6]) <*> [7,8,9]
([(\y z -> 1*y+z), (\y z > 2*y+z), (\y z -> 3*y+z)] <*> [4,5,6]) <*> [7,8,9]
[(4+), (10+), (18+)] <*> [7,8,9]
[11, 18, 27]
Neat!
It isn't too hard to see that you can extend this to arbitrary-arity functions by just taking on another (<*>)
.
Also, we can define a convenient alias for fmap
with the right fixity and call it (<$>)
, and also define (<*>)
to have the correct fixity to not need parentheses, and we can do something like
f <$> [1,2,3] <*> [4,5,6] <*> [7,8,9]
Which is neat, right? Now you can basically do a zipWithN
...zipWith
with as many arguments as you want!
Unfortunately the default Applicative instance for []
doesn't have this behavior; it behaves in a way consistent with its Monad instance. So to get around this, we usually use a newtype wrapper to let us define different instances for the same type. In the standard libraries, in Control.Applicative
, the newtype wrapper is ZipList
:
data ZipList a = ZipList { getZipList :: [a] }
instance Applicative ZipList where
(ZipList fs) <*> (ZipList xs) = ZipList (zipWith ($) fs xs)
pure x = -- left as exercise, it might surprise you :)
So we can do the above in real Haskell as:
f <$> ZipList [1,2,3] <*> ZipList [4,5,6] <*> ZipList [7,8,9]
Which is slightly more verbose than the original version, unfortunately --- and a bit more verbose than
zipWith3 f [1,2,3] [4,5,6] [7,8,9]
But the "advantage" is that you can do basically arbitrary fixity "lifting" :)
The real thing to take away here is that this is "exactly the kind of pattern" that Applicative was invented to solve; it's a very common pattern/domain that Applicative particularly thrives in, and it might be nice to begin building an intuition to be able to spot the tell-tale signs of a problem that might be a good fit for an Applicative solution.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With