Non-Obvious Haskell Idiom: ViewPattern Argument Transform
Reading production Haskell code, we sometimes stumble over idioms that look confusing at first, but which show up frequently enough that they are worth learning. We have already seen
- how guard–sequence helps us conditionally return something, and
- how conditional for helps us execute a side effect only when a value exists.
Today we’ll look at how to transform an argument before naming it. The basic
shape of this pattern is to first enable the ViewPatterns
extension, and then
we can write
someFunction (transform -> usefulValue) = ...
instead of what would otherwise be something like
someFunction param = let usefulValue = transform param in ...
which leaves a hanging param
value that ought not to exist.
Explanation
Let’s say we’re making a game where the player carries some inventory, and this is a set of items for quick membership tests. Our function to print that inventory might have the signature
printInventory :: Set Item -> IO Int
where each item is printed on its own line, and then the integer returned is the width of the inventory listing – i.e. the length of the longest line printed. The simplest implementation of this would be in terms of a list of items1 You could argue we should perform a side-effectful fold over the set instead to find the maximum length printed. That would probably be a better design and side-step the entire conversion problem, but bear with me. Coming up with good examples is hard.:
printInventory itemSet = do -- Convert set of items to a list of items. let items = Set.toList itemSet -- Print all items with their index, collecting the printed width. widths <- for (zip [1..] items) $ \(idx, item) -> do let printed = show idx <> ". " <> name item putStrLn printed pure (length printed) -- Return the largest of the printed widths, and zero if none were printed. pure (foldr max 0 widths)
The reason we need to transform the set to a list first is that sets are not
traversable, so the for
function does not work on them.
However, this implementation leaves us with a loose itemSet
variable that has
a value but is not meant to be used in the method. Having variables around that
we intend not to use is … perhaps not a code smell, but at least a mildly
unpleasant aroma. When writing code, we try to write it so that we name only the
things we care about.
This is where we can use view patterns to transform an argument to a function
before we give it a name. By moving the Set.toList
transformation up into a
view pattern, we get to name the result of that transformation and keep the raw
value un-named:
printInventory (Set.toList -> items) = do widths <- for (zip [1..] items) $ \(idx, item) -> do let printed = show idx <> ". " <> name item putStrLn printed pure (length printed) pure (foldr max 0 widths)
Maybe it’s the type of code I’m in the middle of writing2 Wrangling a compatibility layer between two apis., but I find this useful very often.
View patterns can do more but I won’t
I should say that I’m aware view patterns can do actual pattern matching also, i.e. where the thing to the right of the arrow is not just a name but a constructor. I’m just not a fan of defining functions piecewise – I prefer one definition and then an explicit case statement to cover all cases. I would write
printInventory itemList = case nonEmpty itemList of Nothing -> do putStrLn "You are carrying nothing." pure 0 Just items -> do -- ...
over
printInventory (nonEmpty -> Nothing) = do putStrLn "You are carrying nothing." pure 0 printInventory (nonEmpty -> Just items) = do -- ...
There’s something pleasing about piecewise definitions, but there’s still the programmer in me that thinks functions should have one entry point and ideally one exit point.