Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Join two IO actions using the same input in Haskell

Tags:

haskell

I have two functions:

emptyDirectory, copyStubFileTo :: FilePath -> IO ()

I want to combine them the way as below:

forM_ ["1", "2"] $\n -> do
  emptyDirectory n
  copyStubFileTo n

Is there any other standard way built into Haskell to simplify this combination? I mean joining two IO actions and giving them the same input.

like image 810
aXqd Avatar asked Dec 29 '10 04:12

aXqd


3 Answers

liftA2 (>>) emptyDirectory copyStubFileTo
like image 161
luqui Avatar answered Nov 11 '22 00:11

luqui


The short answer is that no, there's no standard way. The slightly longer answer is that you can write a >&> combinator yourself:

(>&>) :: Monad m => (a -> m b) -> (a -> m c) -> a -> m c
(f >&> g) x = f x >> g x

This does exactly what you want it to do, as you can see from the types. Using this, you have emptyDirectory >&> copyStubFileTo :: FilePath -> IO (), and so

mapM_ (emptyDirectory >&> copyStubFileTo) ["1", "2"]

Hoogle doesn't turn up anything for the type signature, unfortunately, so I think it's safe to assume that it doesn't exist.


Now, this wasn't my original implementation of >&>, because I originally looked at it in terms of a similar non-monadic combinator (&) :: (a -> b) -> (a -> c) -> a -> (b,c), which I find myself reimplementing with some regularity. If you approach this non-monadically and then generalize, you end up with what I think is a collection of useful combinators (which I keep reinventing) that don't seem to exist anywhere standard (even though I feel like at least some of them should). On the chance that you ever want some more general combinators, here they are; none of these seem to exist on Hoogle. (Choosing appropriate precedences is left as an exercise for the interested reader.)

To start, you want (&) :: (a -> b) -> (a -> c) -> a -> (b,c), which is the non-monadic version of what you want. You can't coalesce the b and c, since they're arbitrary types, so we return a tuple. There's only one sensible function of this type: (f & g) x = (f x, g x). If we feed this implementation through pointfree, we get something nicer:

(&) :: Monad m => m a -> m b -> m (a,b)
(&) = liftM2 (,)

This works for functions because (r ->) is a monad (the reader monad); (&) captures the concept of doing two things and collecting both results, and for (r ->), "doing something" is evaluating a function.

With this, however, you'd have emptyDirectory & copyStubFileTo :: FilePath -> (IO (), IO ()). Oog. We thus want to lift the monad out of the tuple, so we need a function tupleM :: Monad m => (m a, m b) -> m (a,b). Writing this ourselves, it uses the & function from above:

tupleM :: Monad m => (m a, m b) -> m (a,b)
tupleM = uncurry (&)

If you look at the types, this actually makes sense, though it might take a few reads (it did for me).

Now, we can define a version of (&) for monadic functions:

(<&>) :: Monad m => (a -> m b) -> (a -> m c) -> a -> m (b, c)
f <&> g = tupleM . (f & g)

We now have emptyDirectory <&> copyStubFileTo :: FilePath -> IO ((),()), which is an improvement. But we really don't need that tuple (although we might for more interesting operations). Instead, we want (>&>) :: Monad m => (a -> m b) -> (a -> m c) -> a -> m c (the analog of >>), which is what we set out to define (and, in fact, defined above). Since I'm defining all sorts of combinators anyway, I'm going to define this by way of <.> :: Functor f => (b -> c) -> (a -> f b) -> a -> f c (the functorial analogue of <=< from Control.Monad).

(<.>) :: Functor f => (b -> c) -> (a -> f b) -> a -> f c
(f <.> g) x = f <$> g x

(>&>) :: (Monad m, Functor m) => (a -> m b) -> (a -> m c) -> a -> m c
f >&> g = snd <.> (f <&> g)

(If you don't like the Functor m constraint, replace <$> with `liftM`.) What's most interesting about this is that we've arrived at a completely different implementation of >&>. The first implementation focuses on the "do two things" aspect of >&>; the second focus on the "evaluate two functions" aspect. This second one is actually the first implementation I thought of; I didn't try writing the first implementation, because I assumed it would be ugly. There's probably a lesson in that :-)

like image 36
Antal Spector-Zabusky Avatar answered Nov 11 '22 00:11

Antal Spector-Zabusky


Disclaimer: I think your original code is very readable and there's nothing wrong with repeating the variable n twice.

That said, if you want to get fancy, ((->) a) forms an Applicative instance, so you can do something like this:

import Control.Applicative

forM_ ["1", "2"] $ (>>) <$> emptyDirectory <*> copyStubFileTo
like image 22
Tom Crockett Avatar answered Nov 10 '22 23:11

Tom Crockett