Rotating args in Haskell and Ruby block style programming

I was writing some Haskell code and came across a function whose arguments weren't in the order I wanted. With most languages you're stuck, the library writer made a choice and now you have to live with it. But Haskell gives you more options ...

You may have encountered this before, for example with mapM_. The type signature of mapM_ is mapM_ :: (Monad m) => (a -> m b) -> [a] -> m (). But you can use flip on it and turn the type signature into flip mapM_ :: (Monad m) => [a] -> (a -> m b) -> m () which then lets you write code like this.

flip mapM_ [0..10] $ \n -> do
  print n
  print $ n * 3

Rubyists should see a familiar pattern there that looks a lot like a do/end block.

(0..10).each do |n|
  puts n
  puts n * 3
end

This is so useful that there is another function already defined that acts like mapM_ with its arguments flipped. It's called forM_ and as you might have guessed, it's type signature is forM_ :: (Monad m) => [a] -> (a -> m b) -> m ().

forM_ [0..10] $ \n -> do
  print n
  print $ n * 3

The flip function works very well for two argument functions, but what about three arguments or more? I was recently working with zipWithM_ :: (Monad m) => (a -> b -> m c) -> [a] -> [b] -> m (). To use it as is, you'd have to write code like this.

zipWithM_ (\a b -> do { print (a + b); print (a + b * 3) }) [0..10] [4..]

Blecch. Hey, I want my code block at the end so I can work with it like I would in Ruby. I can't use flip here so what can I do? Enter in a new routine we'll call frot, for function rotate.

frot2 = flip
frot3 f b c a = f a b c

So what can we do with frot? Check out this little example.

-- frot2 works just like flip (obviously)

frot2 mapM_ [0..10] $ \n -> do
  print n
  print $ n * 3


-- original zipWithM_

ghci> :t zipWithM_
zipWithM_ :: (Monad m) => (a -> b -> m c) -> [a] -> [b] -> m ()


-- We can alter zipWithM_ to act like it takes a block

ghci> :t frot3 zipWithM_
frot3 zipWithM_ :: (Monad m) => [a] -> [b] -> (a -> b -> m c) -> m ()

frot3 zipWithM_ [0..10] [4..] $ \a b -> do
  print (a + b)
  print (a + b * 3)


-- We can chain frot calls to alter the order - put [b] before [a]

ghci> :t frot2 $ frot3 zipWithM_
frot2 $ frot3 zipWithM_ :: (Monad m) => [b] -> [a] -> (a -> b -> m c) -> m ()

frot2 (frot3 zipWithM_) [4..] [0..10] $ \a b -> do
  print (a + b)
  print (a + b * 3)


-- We can continue rotating args

ghci> :t frot3 $ frot3 zipWithM_
frot3 $ frot3 zipWithM_ :: (Monad m) =>
                           [b] -> (a -> b -> m c) -> [a] -> m ()

frot3 (frot3 zipWithM_) [4..] (\a b -> do { print (a + b); print (a + b * 3)}) [0..10]


-- If we rotate enough, we're back where we started

ghci> :t frot3 $ frot3 $ frot3 zipWithM_
frot3 $ frot3 $ frot3 zipWithM_ :: (Monad m) =>
                                   (a -> b -> m c) -> [a] -> [b] -> m ()

The best part about this is that frot is not tied to zipWithM_. It can be used with any function. For example, we can alter the findWithDefault function from Data.Map just as easily.

ghci> :t findWithDefault
findWithDefault :: (Ord k) => a -> k -> Map k a -> a

ghci> :t frot3 $ frot3 findWithDefault
frot3 $ frot3 findWithDefault :: (Ord k) => Map k a -> a -> k -> a

-- Now we can partially apply and use

> let ds       = fromList [("new england", "patriots"), ("new york", "giants")]
>     findTeam = frot3 (frot3 findWithDefault) ds "no team"

> findTeam "peoria"
"no team"

> findTeam "new york"
"giants"

Update: Fixed typo in frot3.

Update 2: Part 2 partially explains why this works in Haskell. Obtaining a full understanding is left as an exercise for the reader.

tags: haskell, ruby

Comments

comments powered by Disqus