While working on my data-object library, I needed to apply some monadic functions to a tuple, and get back a monaidc tuple. In code:
f :: Monad m => (a -> m b) -> (c -> m d) -> (a, c) -> m (b, d)
The most obvious thing to do is just long-hand it with do notation:
test1 :: Monad m => (a -> m b) -> (c -> m d) -> (a, c) -> m (b, d) test1 f g (a, c) = do b <- f a d <- g c return (b, d)
But who wants to write that? I got a recommendation instead to try out liftM2. After playing with it a bit, I came out with:
test2 :: Monad m => (a -> m b) -> (c -> m d) -> (a, c) -> m (b, d) test2 f g (a, c) = uncurry (liftM2 (,)) $ (f *** g) (a, c)
Which is definitely more respectable (though arguably more line noise). After staring at that for a little bit, I realized that there was nothing particularly monadic about this, and could instead be expressed Applicatively:
test3 :: Applicative f => (a -> f b) -> (c -> f d) -> (a, c) -> f (b, d) test3 f g (a, c) = uncurry (liftA2 (,)) $ (f *** g) (a, c)
Then of course comes the eta-reduction, so you get:
test4 :: Applicative f => (a -> f b) -> (c -> f d) -> (a, c) -> f (b, d) test4 f g = uncurry (liftA2 (,)) . (f *** g)
Kleisli
I’m sure you noticed the use of *** in test2, test3 and test4. That’s not too surprising; often times we want to use Data.Arrow functions when operating on tuples. As I was staring at the documentation for Data.Arrow, I decided to see what could be done with Kleisli. I came up with:
test5 :: Monad m => (a -> m b) -> (c -> m d) -> (a, c) -> m (b, d) test5 f g = runKleisli $ Kleisli f *** Kleisli g
This, in my opinion, is much more readable than the above. For those who don’t know, Kleisli allows turning any monadic function into an arrow. Another advantage of this is I can now do things like:
test6 :: Monad m => (a -> m b) -> (a, c) -> m (b, c) test6 f = runKleisli $ first $ Kleisli f
The downside of test5 versus test4 is that it only works Monadically, not Applicatively. And in case you were wondering, you can’t define a KleisliA type value which will work on all Applicatives. This boils down to where the real extra power of Monads versus Applicatives lies.
All Arrows must be Categorys. One of the functions of a Category is (.), or essentially function composition. The definition for Kleisli monads goes, after unwrapping:
f . g = \b -> g b >>= f
There is no equivalent to this in Applicatives. So sadly, if I want to make my tuple lifting functions work on applicatives, I’m stuck with liftA2.