Haskell anti-pattern: incremental ad-hoc parameter abstraction
Recently I’ve found myself doing the following very ugly thing. Perhaps you’ve unwittingly done it too—so I thought I’d share the problem and its solution.
Suppose I’ve written a function foo
:
> foo :: Int -> Result
> foo n = ... n ...
Who knows what Result
is; that’s not the point. Everything is going fine until I suddenly realize that occasionally I would like to be able to control the number of wibbles! Well, every good programmer knows the answer to this: abstract out the number of wibbles as a parameter.
> foo :: Int -> Int -> Result
> foo numWibbles n = ... n ... numWibbles ...
But this isn’t quite what I want. For one thing, I’ve already used foo
in a bunch of places in my code, and it would be annoying to go back and change them all (even with refactoring support). What’s more, most of the time I only want one wibble. So I end up doing something like this:
> foo' :: Int -> Int -> Result
> foo' numWibbles n = ... n ... numWibbles ...
>
> foo = foo' 1
Great! Now all my old code still works, and I can use foo’
whenever I want the extra control over the number of wibbles.
Well, this may seem great, but it’s a slippery slope straight to code hell. What happens when I realize that I also want to be able to specify whether the wibbles should be furbled or not? Well, I could do this:
> foo'' :: Bool -> Int -> Int -> Result
> foo'' wibblesShouldBeFurbled numWibbles n = ...
>
> foo' = foo'' False
>
> foo = foo' 1
Yes, all my old code still works and I can now succesfully control the furblization if I so desire. But at what cost? First of all, this is just… well, ugly. Good luck trying to remember what foo’‘
does and what arguments it takes. And what if I want to furble exactly one wibble? Well, I’m stuck using foo’’ True 1
because I can’t control the furblization without giving the number of wibbles explicitly.
Yes, I have actually done things like this. In fact, this problem is quite apparent in the currently released version of my diagrams library. For example:
-
hcat
lays out a list of diagrams horizontally; -
hsep
is likehcat
, but takes another argument specifying the amount of space to place in between each diagram; -
hsepA
is likehsep
, but takes yet another argument specifying the vertical alignment of the diagrams; -
and don’t even get me started on the
distrib
family of functions, which are likehcat
and friends but put diagrams at evenly spaced intervals instead of putting a certain amount of space between each one…
You get the idea. So, what’s the solution? What I really want (which you may have figured out by this point) is optional, named arguments. But Haskell doesn’t have either! What to do?
I finally came up with an idea the other day… but then with a little Googling discovered that others have already thought of it. I’ve probably even read about it before, but I guess I didn’t need it back then so it didn’t stick!
Here’s the idea, as explained by Neil Mitchell: put the optional arguments to a function in a custom data structure using record syntax, and declare a record with all the default arguments. Then we can call our function using the default record, overriding whichever fields we want using record update syntax. Of course, it’s still annoying to have to remember which default record of arguments goes with which function; but the icing on the cake is that we can use a type class to provide the default arguments automatically. There’s already a suitable type class on Hackage in the data-default package.
So now my code looks something like this:
> data FooOptions = FooOptions
> { wibblesShouldBeFurbled :: Bool
> , numWibbles :: Int
> }
>
> instance Default FooOptions where
> def = FooOptions False 1
>
> ... foo def { wibblesShouldBeFurbled = True } ...
Nice. It might even be cool to define with
as a synonym for def
, to allow the natural-sounding
> ... foo with { numWibbles = 4 } ...
Of course, this isn’t perfect; if Haskell had real records it might be a bit nicer. For one thing this tends to result in a bit of namespace pollution: I can’t have another function which also takes an option called numWibbles
since it will clash with the one in FooOptions
! But this is still a giant improvement over the code I used to write.