Recently, there has been some new discussion around the issue of providing default values for function parameters in Haskell. First, Gabriel Gonzalez showed us his new
optional-args library, which provides new types for optional arguments along with heavy syntactic overloading. To follow that, Dimitri Sabadie published a blog post discouraging the use of the currently popular
Default type class. These are both good discussions, and as with any good discussion have been lingering around in the back of my head.
Since those discussions took place, I’ve been playing with my point in the FRP-web-framework design space - Francium. I made some big refactorings on an application using Francium, mostly extending so called “component” data types (buttons, checkboxes, etc), and was frustrated with how much code broke just from introducing new record fields. The Commercial Haskell group published an article on how to design for extensibility back in March, so I decided to revisit that.
It turns out that with a little bit of modification, the approach proposed in designing for extensibility also covers optional arguments pretty well!
First, let’s recap what it means to design for extensibility. The key points are:
Settingsvalues, which specify a general configuration.
Settingsvalues are opaque, meaning they cannot be constructed by a data constructor, but they have a smart constructor instead. This smart constructor allows you to provide default values.
Settingsdata type, preventing the use of record syntax for updates (which leaks implementation details).
Regular Haskell users will already be familiar a pattern that can be seen in point 3: we often use a different piece of technology to solve this problem - lenses. Lenses are nice here because they reduce the surface area of our API - two exports can be reduced to just one, which I believe reduces the time to learn a new library. They also compose very nicely, in that they can be embedded into other computations with ease.
With point 3 amended to use some form of lens, we end up with the following type of presentation. Take a HTTP library for example. Our hypothetical library would have the following exports:
data HTTPSettings httpKeepAlive :: Lens HTTPSettings Bool httpCookieJar :: Lens HTTPSettings CookieJar defaultHTTPSettings :: HTTPSettings httpRequest :: HTTPSettings -> HTTPRequest -> IO Response
which might have usage
httpRequest& httpKeepAlive .~ True) (defaultHTTPSettings aRequest
This is an improvement, but I’ve never particularly liked the reverse function application stuff with
&. The repeated use of
& is essentially working in an
Writer monad, or more generally - a state monad. The
lens library ships with operators for working specifically in state monads (of course it does), so let’s use that:
httpRequest :: State HTTPSettings x -> HTTPRequest -> IO Response .... httpRequestdo httpKeepAlive .= True) ( aRequest
It’s a small change here, but when you are overriding a lot of parameters, the sugar offered by the use of
do is hard to give up - especially when you throw in more monadic combinators like
With this seemingly simple syntactic change, something interesting has happened; something which is easier to see if we break open
httpRequest :: State HTTPSettings x -> HTTPRequest -> IO Response = httpRequest mkConfig request let config = execState mkConfig defaultHttpSettings in ...
Now the default configuration has moved inside the HTTP module, rather than being supplied by the user. All the user provides is essentially a function
HTTPSettings -> HTTPSettings, dressed up in a state monad. This means that to use the default configuration, we simply provide a do-nothing state composition:
return (). We can even give this a name
def :: State a () = return ()def
and voila, we now have the lovely name-overloading offered by
Data.Default, but without the need to introduce a lawless type class!
To conclude, in this post I’ve shown that by slightly modifying the presentation of an approach to build APIs with extensibility in mind, we the main benefit of
Data.Default. This main benefit - the raison d’être of
Data.Default - is the ability to use the single symbol
def whenever you just want a configuration, but don’t care what it is. We still have that ability, and we didn’t have to rely on an ad hoc type class to get there.
However, it’s not all rainbows and puppies: we did have to give something up to get here, and what we’ve given up is a compiler enforced consistency. With
Data.Default, there is only a single choice of default configuration for a given type, so you know that
def :: HTTPSettings will be the same set of defaults everywhere. With my approach, exactly what
def means is down to the function you’re calling and how they want to interpret
def. In practice, due to the lack of laws on
def, there wasn’t much reasoning you could do about what that single instance was anyway, so I’m not sure much is given up in practice. I try and keep to a single interpretation of
def in my libraries by still exporting
defaultHTTPSettings, and then using
execState mkConfig defaultHTTPSettings whenever I need to interpret a
You can contact me via email at firstname.lastname@example.org or tweet to me @acid2. I share almost all of my work at GitHub. This post is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.