Xmonad.hs: The Joy of Refactoring

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.

Advertisements