I've gotten used to the idea that adding a constructor to a type should break pattern matches on a value of that type - an incomplete pattern match error. However, there isn't something like this for the opposite scenario - constructing values of that type.
For example, given this code, where genFoo potentially lives in a different
module than Foo:
module MyData
data Foo = Bar Bool Double | Baz Int
...
module MyData.Gen
import Hedgehog (Gen)
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
genFoo :: Gen Foo
genFoo = Gen.choice @Gen
[ Bar <$> Gen.bool_ <*> Gen.double (Range.linearFrac 0.0 1.0)
, Baz <$> Gen.int (Range.linear (-100) 100)
]Consider that a new constructor Qux is added to Foo. Strictly speaking,
there really shouldn't be an error anywhere, but there's an implicit assumption
that genFoo should generate all sorts of Foos; in particular, it should
generate Qux values. In a large codebase, it's surprisingly easy to forget
a change like this, and code depending on genFoo will never see a Qux,
so the tests will miss a lot of branches.
If, instead, we had used constructors to define genFoo:
module MyData
data Foo = Bar Bool Double | Baz Int
deriving (Generic)
...
module MyData.Gen
import Constructors (constructors)
import Hedgehog (Gen)
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
genFoo :: Gen Foo
genFoo = constructors @Foo \bar baz ->
Gen.choice @Gen
[ bar <$> Gen.bool_ <*> Gen.double (Range.linearFrac 0.0 1.0)
, baz <$> Gen.int (Range.linear (-100) 100)
]when we decide to add Qux:
- data Foo = Bar Bool Double | Baz Int
+ data Foo = Bar Bool Double | Baz Int | Qux Charthe type of constructors @Foo will have changed, so the compiler will
remind us to add an entry for Qux to genFoo.
It's very likely that when reading the above you thought "what if I
accidentally transpose two constructors?" After all, the argument to
constructors can call the constructors by any name.
Constructors.Tagged offers a similar interface to Constructors, but
unlike the plain functions provided by Constructors.constructors,
Constructors.Tagged.constructors tags each of the functions with the name
of the constructor. Continuing with the previous example, we can write:
module MyData.Gen
import qualified Constructors.Tagged as Tagged (constructors)
import Data.Tagged (untag)
import Hedgehog (Gen)
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
genFoo :: Gen Foo
genFoo = Tagged.constructors @Foo
\(untag @"Bar" -> bar) ->
\(untag @"Baz" -> baz) ->
Gen.choice @Gen
[ bar <$> Gen.bool_ <*> Gen.double (Range.linearFrac 0.0 1.0)
, baz <$> Gen.int (Range.linear (-100) 100)
]This way, when the eventual addition of Qux comes along,
- data Foo = Bar Bool Double | Baz Int
+ data Foo = Bar Bool Double | Baz Int | Qux Charthe compiler will give a better error message:
<loc>: error:
• Couldn't match type ‘Gen Foo’
with ‘Tagged "Qux" (Char -> Foo)
-> Gen Foo’
Expected type: Constructors.Tagged.Q (Rep Foo) Foo (Gen Foo)
Actual type: Tagged "Bar" (Bool -> Double -> Foo)
-> Tagged "Baz" (Int -> Foo)
-> Gen Foo
...
So it will be clear that the Qux constructor is what's missing.
I had to guide the compiler a bit with the type application on Gen.choice;
otherwise the error message wouldn't be quite as clear due to its overloading.