M.C. Escher - Metamorphosis II

zoneToRA / ToRAsConsole

Sun, 13 Jan 2008

Simplifying Code

The process my brain goes through when programming...

Click for full
version

And now to prove the point of the diagram...

A task I've come across several times while programming is taking a list describing a template, and completing it with input from elsewhere.

Let me describe further (this post is a literate Haskell file...)

> import Data.Maybe > import Control.Monad > import Control.Monad.State > import Control.Arrow

For example, the list:

> testTemplate = [Just "This", Nothing, Just "a", Nothing, Nothing]

describes a template, where the first and third elements are fixed, but the remaining (second, fourth and fifth) elements come from external input.

So if testTemplate was used with the following filling...

> testFilling = ["is", "simple", "test"]

The result we expect would be:

> testResult = words "This is a simple test"

So we need to implement a function of type [Maybe String] -> [String] -> [String], and it should make our test give back True.

> test :: ([Maybe String] -> [String] -> [String]) -> Bool > test combiner = testResult == combiner testTemplate testFilling

Now the most explicit way to do this is to use pattern matching, and just work through the cases on the template list...

> combinerRec :: [Maybe String] -> [String] -> [String] > combinerRec [] _ = [] > combinerRec ((Just t):ts) filling = t:combinerRec ts filling > combinerRec (Nothing:ts) (f:filling) = f:combinerRec ts filling

Thing is, I rarely ever use explicit pattern matching nowadays (especially when it comes to lists and maybe), as there are a whole host of combinators that do it for you. Also using combinators over pattern matching can make your code a lot more readable. And anyways, explicitly writing out the recursion fills me with an ill feeling.

So what are we to do? Well one trick is to notice that the filling is actually some state in the algorithm. For each template element, we peel the next element off the state or just carry it across, depending on whether the template element is Nothing or Just.

> combinerState1 :: [Maybe String] -> [String] -> [String] > combinerState1 = evalState . mapM (maybe getNext return) > where > getNext = do > (h:tl) <- get > put tl > return h

Well that's concise-ish, and (assuming you know the standard libraries) is quite clear / reads well. Of course getNext's definition is a bit verbose, but it's hidden by a meaningful name.

But wait, there's more! If you stare at getNext for a while, with the definition of State next to it, you'll realise that State (\(h:tl) -> (h,tl)) does the same thing. So we can rewrite inlining getNext and using arrow combinators instead of the explicit lambda...

> combinerState2 :: [Maybe String] -> [String] -> [String] > combinerState2 = evalState . mapM (maybe (State (head &&& tail)) return)

Arn't we clever? Possibly too clever, while it is a 1-liner, it doesn't (to me) read like psuedo-english. I'd probably use something like this in reality, as (I think) it reads slightly better.

> combinerState3 :: [Maybe String] -> [String] -> [String] > combinerState3 template = evalState $ forM template (maybe getNext return) > where > getNext = State (head &&& tail)

So there you have it, how to spend at least an hour and a half blogging about how you can simplify a 3 line haskell function down to one line, and then realising that a different 3 line function reads better if you know lots of esoteric parts of the Haskell standard libraries. At least I havn't tried to write a monad tutorial...

[/general] permanent link

Tristan Allwood's weblog

M.C. Escher - Metamorphosis II