24 Days of GHC Extensions: Record Wildcards

Occasionally, you come across a little trick or method for doing something that seems somewhat inconsequential - but rapidly becomes an indispensable item in your programming toolbox. For me, the RecordWildcards extension is a prime example of this scenario.

{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} import Data.Aeson

To start with, let’s recap records in Haskell. A record is usually known to be a data type with a single constructor, and the data type is populated with a collection of fields. Records crop up all the time in programming, often when we try to model the real world:

data Worker = Worker { workerName :: String , workerPosition :: String , workerFirstYear :: Int }

Of course, data alone isn’t much fun - we probably want to operate on this data too. In this case we’d like to interact with other web services, and we’ll use the common JSON format for communication. If we have a specific schema that we need to conform to, it may be easier to write this by hand:

instance ToJSON Worker where = object [ "name" .= workerName w toJSON wobject [workerName w , "position" .= workerPosition w workerPosition w , "first-year" .= workerFirstYear w workerFirstYear w ]

Having to apply each record field getter to the w variable is a little tedious, and RecordWildCards can allow us to eliminate that bit of boilerplate:

instance ToJSON Worker where Worker { .. } = object [ "name" .= workerName toJSONobject [workerName , "position" .= workerPosition workerPosition , "first-year" .= workerFirstYear workerFirstYear ]

Here we see the Worker{..} pattern match - this pattern matches on the Worker constructor, and introduces bindings for all of the fields in Worker . Each of these bindings will be named after the respective field in the record. We can see on the RHS that we are now constructing our JSON object just out of variables, rather than function applications.

If you were expecting a lot of ground breaking new features from RecordWildCards you might be disappointed - that’s about all it does! However, did you know that you can also use RecordWildCards when creating data? For example, we could also write a JSON deserialiser as:

instance FromJSON Worker where = withObject "Worker" $ \o -> do parseJSONwithObject\o <- o .: "name" workerName <- o .: "position" workerPosition <- o .: "first-year" workerFirstYear return Worker { .. }

Personally, I don’t use this feature as much as creating bindings - in this case I’d just use applicative syntax - but it can occasionally be handy.

RecordWildCards For Modules

I’ve presented a fairly “vanilla” overview of RecordWildCards - and I imagine this is probably how most people use them. However, when used with a record of functions, you can do some interesting tricks to emulate localised imports.

In my engine-io project, I have a data type called ServerAPI - here’s a snippet:

data ServerAPI m = ServerAPI { srvGetQueryParams :: m ( HashMap.HashMap BS.ByteString [ BS.ByteString ]) m (]) , srvGetRequestMethod :: m BS.ByteString }

The intention here is that users provide a ServerAPI value when they initialise engine-io , and I then have an abstraction of a web framework to play with. People can instantiate ServerAPI for Snap or Yesod, and engine-io (should!) just work. In engine-io , by using RecordWildCards , the programming experience is natural, as the abstraction created by ServerAPI stays behind the scenes. For example:

handlePoll :: MonadIO m => ServerAPI m -> Transport -> Bool -> m () m () @ ServerAPI { .. } transport supportsBinary = do handlePoll api} transport supportsBinary <- srvGetRequestMethod requestMethodsrvGetRequestMethod ... handler :: MonadIO m => EngineIO -> ( Socket -> m SocketApp ) -> ServerAPI m -> m () m () @ ServerAPI { .. } = do handler eio socketHandler api <- srvGetQueryParams queryParamssrvGetQueryParams ...

This is very similar to using a type class - however, using type classes would be very tricky in this situation. Either engine-io would have to depend on both Snap and Yesod (though it needs neither), or I would have to use orphan instances. Neither are particularly desirable. Furthermore, who’s to say there is only one choice of ServerAPI for Snap? It’s entirely possible to provide a debugging version that logs what’s happening, or for people to switch out calls however they see fit. This is possible with newtype s in type classes, but pushes a lot of this work onto users.

Gabriel Gonzalez has a blog post on this very technique that goes into more details, which is well worth a read.

This post is part of 24 Days of GHC Extensions - for more posts like this, check out the calendar.

You can contact me via email at ollie@ocharles.org.uk 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.