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.
This is lovely, thanks for sharing.
Another useful thing with records is the extension RecordWildCards which will bind the record to what are in scope. Suppose you have multiple functions which all want the same arguments (config or whatnot). For example think of a pretty printer for some language that is parametrised how to print keywords and such.
data PPConf = PPConf
{ ppKey :: String -> Doc
, ppNum :: Doc -> Doc
, …
}
— Result type
data PP = PP
{ ppExpr :: Expr -> Doc
, ppDef :: Def -> Doc
, …
}
— main function
mkPP :: PPConf -> PP
mkPP (PPConf {..}) = PP {..}
where
ppExpr :: Expr -> Doc
ppExpr exp = …
ppDef :: Def -> Doc
ppDef def = …
…
— example
fun exp = let (PP{..}) = mkPP myPPConf in ppExpr exp
And voila we have poor mans parametrised modules :)
Pingback: Linktipps Mai 2010 :: Blackflash
By reading this I remembered do-notation for monads where you extract information which will be actually passed when result will be processed.
You can pass information through type by using class-described Result. I.e. old places that use old instance of Result will get default value when they will extractResult, and for new places of usage you’ll use other instance which will provide all needed information.
I would try this only after failing to decompose the function into smaller parts which make semantic sense to expose and using just those parts where I don’t need the fully parameterized function. This is domain specific, so I can’t directly apply this to your example without making assumptions about the code, but it has worked well for me on more concrete situations.
Good point! I’ll have to keep this in mind.
A more satisfying solution to such configuration parameters is to dispense with them entirely. Instead of making a function behave differently, the idea is to break it up into several primitive functions that can be combined in many different ways, giving rise to different behaviors.
While ultimately more satisfying, the drawback of this approach is of course that it’s highly dependent on the problem domain; there is no general recommendation on which primitives to choose.
For your example, I think the following might be worth trying:
Two primitives for horizontal and vertical composition of two diagrams.
hcat = foldr1 besides
A notion of empty diagram with width and height. This way, the space can be interspersed:
hsep x = hcat . intersperse (empty x 0)
Every diagram also gets one “magnet point” on each of it’s four edges. The idea is the composition will always aligned such that corresponding magnet points are glued together; many alignment situations can be expressed this way.
To that, maybe add a
table
function for rectangular layout with no options which is intended for quick & dirty diagram output when the user just wants something on the screen and does not care about spacing and alignment.