Library announcement: 'safe-wild-cards', better static guarantees for -XRecordWildCards

May 31, 2021
Written by Artyom Kazak

The problem

-XRecordWildCards are very convenient. Let's say you need to write a ToJSON instance manually (maybe you need to modify some of the fields):

data Rec = Rec { foo :: Int, bar :: Int, qux :: Int }

instance ToJSON Rec where
  toJSON Rec{..} = object [
    "foo" .= foo,
    "bar" .= bar,
    "qux" .= qux ]

There is just one problem. If you add a field to Rec, the compiler will not warn you, and your ToJSON instance will potentially miss a field. Boo! Static guarantees. We want static guarantees. We want mindless refactorings.

(AFAIK Rust got it right. But I might be misremembering.)

Another case is when you want to do a pairwise operation on all or most fields of two records, like this:

diffRec :: Rec -> Rec -> SomeFancyDiff
diffRec a b = ...

-XRecordWildCards doesn't even help here — you get name clashes — and GHC is not going to tell you that diffRec should be updated when Rec is updated.

The solution

I have just released a small library, safe-wild-cards, that lets us have safer wildcard matches at the cost of somewhat worse syntax.

Instead of Rec{..}, write $(fields 'Rec):

{-# LANGUAGE TemplateHaskell #-}

import SafeWildCards

data Rec = Rec { foo :: Int, bar :: Int, qux :: Int }

$(pure [])  -- see https://blog.monadfix.com/th-groups for the explanation of this

instance ToJSON Rec where
  toJSON $(fields 'Rec) = object [
    "foo" .= foo,
    "bar" .= bar,
    "qux" .= qux ]

Under the hood, $(fields 'Rec) expands into Rec foo bar qux. Same code, but now GHC will warn you when you add a field and forget to either use it or explicitly ignore it.

The warning will look like this:

example.hs:20:5: warning: [-Wunused-matches]
    Defined but not used: ‘newField’
   |
20 |   toJSON $(fields 'Rec) = ...
   |           ^^^^^^^^^^^^

If you have more than one record of the same type, you can use fieldsPrefixed:

diffRec :: Rec -> Rec -> SomeFancyDiff
diffRec $(fieldsPrefixed "a_" 'Rec) $(fieldsPrefixed "b_" 'Rec) =
  diff a_foo b_foo <> diff a_bar b_bar <> ...

P. S.

If there is demand for this kind of compile-time safety, maybe eventually it will result in a new GHC warning.

I had a GHC ticket somewhere, but lost it — if anybody else thinks it's a good idea, please file a new one!