The more I use Haskell, the more I learn to appreciate its beauty. The thesis of this post is: Haskell code is so easy to refactor. What follows is an account of my experience the other day with extending my XMonad configuration file, and how easy/fun it was for me. It’s written in a very newbie-friendly way for all XMonad users who don’t know much Haskell, if at all.
The other day, I ended up writing something like the following into my xmonad.hs file:
myManageHook :: ManageHook myManageHook = composeAll [ ... , resource =? "atWorkspace0" --> doShift "0" , resource =? "atWorkspace1" --> doShift "1" , resource =? "atWorkspace2" --> doShift "2" , resource =? "atWorkspace3" --> doShift "3" , resource =? "atWorkspace4" --> doShift "4" , resource =? "atWorkspace5" --> doShift "5" , resource =? "atWorkspace6" --> doShift "6" , resource =? "atWorkspace7" --> doShift "7" , resource =? "atWorkspace8" --> doShift "8" , resource =? "atWorkspace9" --> doShift "9" , resource =? "atWorkspaceF1" --> doShift "F1" , resource =? "atWorkspaceF2" --> doShift "F2" , resource =? "atWorkspaceF3" --> doShift "F3" , resource =? "atWorkspaceF4" --> doShift "F4" , resource =? "atWorkspaceF5" --> doShift "F5" , resource =? "atWorkspaceF6" --> doShift "F6" , resource =? "atWorkspaceF7" --> doShift "F7" , resource =? "atWorkspaceF8" --> doShift "F8" , resource =? "atWorkspaceF9" --> doShift "F9" , resource =? "atWorkspaceF10" --> doShift "F10" , resource =? "atWorkspaceF11" --> doShift "F11" , resource =? "atWorkspaceF12" --> doShift "F12" ]
Even if you don’t know Haskell, you can tell that the above is very repetitive. It looks a bit stupid. Vim’s visual block mode makes editing the above lines in bulk pretty easy, but your xmonad.hs file is a program’s source code, and source code must obey the Do not Repeat Yourself rule. It will make life easier down the road.
Let’s first consider what the code above means. First, we observe that myManageHook is a function, of type ManageHook. The composeAll function’s type signature is as follows:
composeAll :: [ManageHook] -> ManageHook
(I loaded up XMonad from GHCi to figure this out.) So composeAll merely “compresses” a list of ManageHook types into a single ManageHook. Great! Now we know exactly what each element in the list really means:
myManageHook = composeAll [ a ManageHook , a ManageHook , a ManageHook , a ManageHook ]
So to covert the various “atWorkspace0”, “atWorkspace1”, etc. lines we just need to create a function that generates a list of ManageHook types, and append this list to the one that already exists! Like this:
myManageHook :: ManageHook myManageHook = composeAll $ [ ... existing MangeHook items ] ++ workspaceShifts where workspaceShifts = another list of ManageHooks
Notice how we had to add in a “$” symbol, because:
myManageHook = composeAll [list 1] ++ [list 2]
means
myManageHook = (composeAll [list 1]) ++ [list 2]
which means
myManageHook = ManageHook ++ [list 2]
which is definitely not what we want. We want this:
myManageHook = composeAll ([list 1] ++ [list 2])
which is
myManageHook = composeAll [lists 1 and 2 combined]
and we can do this with the “$” dollar symbol. We could just use the explicit parentheses instead; ultimately it is a matter of style/taste.
So, let’s keep going. The workspaceShifts function needs to generate a list of ManageHooks. From the first code listing, it’s clear that all the generated ManageHooks need only vary slightly from each other:
[ ... , resource =? "atWorkspace0" --> doShift "0" , resource =? "atWorkspace1" --> doShift "1" , resource =? "atWorkspace2" --> doShift "2" , resource =? "atWorkspace3" --> doShift "3" , resource =? "atWorkspace4" --> doShift "4" , resource =? "atWorkspace5" --> doShift "5" , resource =? "atWorkspace6" --> doShift "6" , resource =? "atWorkspace7" --> doShift "7" , resource =? "atWorkspace8" --> doShift "8" , resource =? "atWorkspace9" --> doShift "9" , resource =? "atWorkspaceF1" --> doShift "F1" , resource =? "atWorkspaceF2" --> doShift "F2" , resource =? "atWorkspaceF3" --> doShift "F3" , resource =? "atWorkspaceF4" --> doShift "F4" , resource =? "atWorkspaceF5" --> doShift "F5" , resource =? "atWorkspaceF6" --> doShift "F6" , resource =? "atWorkspaceF7" --> doShift "F7" , resource =? "atWorkspaceF8" --> doShift "F8" , resource =? "atWorkspaceF9" --> doShift "F9" , resource =? "atWorkspaceF10" --> doShift "F10" , resource =? "atWorkspaceF11" --> doShift "F11" , resource =? "atWorkspaceF12" --> doShift "F12" ]
So the only thing that really changes is the suffix after “atWorkspace”; it goes from “0” to “F12”. Let’s express this idea in code:
myManageHook :: ManageHook myManageHook = composeAll $ [ ... ] ++ workspaceShifts where workspaceShifts = genList (s1 ++ s2) genList :: [String] -> [ManageHook] genList ss = map (\s -> resource =? ("atWorkspace" ++ s) --> doShift s) ss
We create a new genList function, which needs an argument (a [String], or “list of strings” to be exact). We creatively call them ss in the code above. The map function simply modifies each item in a list in the same way. So here, we modify every string (s) to become a MangeHook, by giving map‘s first argument as:
(\s -> resource =? ("atWorkspace" ++ s) --> doShift s)
The backslash followed by s binds a single string from the ss list as the variable s. The right arrow (->) signals the start of the function definition. Do you see the resemblance?
... , resource =? "atWorkspace0" --> doShift "0" , resource =? "atWorkspace1" --> doShift "1" , resource =? "atWorkspace2" --> doShift "2" , resource =? "atWorkspace3" --> doShift "3" ... vs. resource =? ("atWorkspace" ++ s) --> doShift s
Nice, clean, and succinct.
Now we just need to define those strings to feed into genList. Here’s s1:
s1 :: [String] -- a list of strings s1 = map show [0..9]
Again, we use the map function. It’s such a handy little function! Haskell is very much built around such useful little named primitives (by convention, most things that look like “operators” such as multi-punctuation arrows, ampersands, and colons are part of some niche library, and not Haskell the core language). The show function merely converts its argument (here, a list of Ints), into strings, so the
s1 = map show [0..9]
part really means: [“0″,”1″,”2″,”3″,”4″,”5″,”6″,”7″,”8″,”9”].
So that’s s1. The second list, s2, is almost identical:
s2 = map (("F" ++) . show) [1..12]
The only difference is that it adds “F” as a prefix, so that we get “F1” instead of “1”, “F2” instead of “2”, and so on.
So, that’s it! The complete code is as follows:
myManageHook :: ManageHook myManageHook = composeAll $ [ ... ] ++ workspaceShifts where workspaceShifts = genList (s1 ++ s2) genList :: [String] -> [ManageHook] genList ss = map (\s -> resource =? ("atWorkspace" ++ s) --> doShift s) ss s1, s2 :: [String] s1 = map show [0..9] s2 = map (("F" ++) . show) [1..12]
We can actually shorten it even more:
myManageHook :: ManageHook myManageHook = composeAll $ [ ... ] ++ map (\s -> resource =? ("atWorkspace" ++ s) --> doShift s) (s1 ++ s2) where s1 = map show [0..9] s2 = map (("F" ++) . show) [1..12]
In the end, we’ve reduced 22 lines of stupid, repetitive code prone to human error and typos down to 4 lines of smart, modular code. I really enjoy this kind of refactoring: reduction of human-error-prone code.
What’s more, the whole experience was quite easy and enjoyable. I did not need to know what exactly a ManageHook type represented; instead, all I needed to know were the type signatures of the various existing parts. And that’s why Haskell is so fun to refactor: type signatures, combined with industrial-strength type checking, makes things safe. Plus, if you look at the code, everything matters. It’s really hard to write spaghetti code in Haskell (even for relative newbies like myself).
For the curious, here is a list of type signatures for composeAll, resource, doShift, (–<), and (=?):
Prelude XMonad> :t composeAll composeAll :: [ManageHook] -> ManageHook Prelude XMonad> :t resource resource :: Query String Prelude XMonad> :t doShift doShift :: WorkspaceId -> ManageHook Prelude XMonad> :t (-->) (-->) :: Query Bool -> ManageHook -> ManageHook Prelude XMonad> :t (=?) (=?) :: Eq a => Query a -> a -> Query Bool
UPDATE July 3, 2011: Hendrik mentions a more concise approach using list comprehensions (arguably more Haskell-y, and simper):
myManageHook :: ManageHook myManageHook = composeAll $ [ ... ] ++ [resource =? ("atWorkspace" ++ s) --> doShift s | s <- map show [0..9] ++ map (('F':) . show) [1..12] ]
UPDATE November 8, 2011: Fixed typo.