Naming newtypes for QuickCheck Arbitraries by Mark Seemann
A simple naming scheme for newtypes to add Arbitrary instances.
Naming is one of those recurring difficult problems in software development. How do you come up with good names?
I'm not aware of any general heuristic for that, but sometimes, in specific contexts, a naming scheme presents itself. Here's one.
Orphan instances #
When you write QuickCheck properties that involve your own custom types, you'll have to add Arbitrary
instances for those types. As an example, here's a restaurant reservation record type:
data Reservation = Reservation { reservationId :: UUID , reservationDate :: LocalTime , reservationName :: String , reservationEmail :: String , reservationQuantity :: Int } deriving (Eq, Show, Read, Generic)
You can easily add an Arbitrary instance to such a type:
instance Arbitrary Reservation where arbitrary = liftM5 Reservation arbitrary arbitrary arbitrary arbitrary arbitrary
The type itself is part of your domain model, while the Arbitrary
instance only belongs to your test code. You shouldn't add the Arbitrary
instance to the domain model, but that means that you'll have to define the instance apart from the type definition. That, however, is an orphan instance, and the compiler will complain:
test\ReservationAPISpec.hs:31:1: warning: [-Worphans] Orphan instance: instance Arbitrary Reservation To avoid this move the instance declaration to the module of the class or of the type, or wrap the type with a newtype and declare the instance on the new type. | 31 | instance Arbitrary Reservation where | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...
Technically, this isn't a difficult problem to solve. The warning even suggests remedies. Moving the instance to the module that declares the type is, however, inappropriate, since test-specific instances don't belong in the domain model. Wrapping the type in a newtype
is more appropriate, but what should you call the type?
Suppress the warning #
I had trouble coming up with good names for such newtype
wrappers, so at first I decided to just suppress that particular compiler warning. I simply added the -fno-warn-orphans
flag exclusively to my test code.
That solved the immediate problem, but I felt a little dirty. It's okay, though, because you're not supposed to reuse test libraries anyway, so the usual problems with orphan instances don't apply.
After having worked a little like this, however, it dawned on me that I needed more than one Arbitrary
instance, and a naming scheme presented itself.
Naming scheme #
For some of the properties I wrote, I needed a valid Reservation
value. In this case, valid means that the reservationQuantity
is a positive number, and that the reservationDate
is in the future. It seemed natural to signify these constraints with a newtype
:
newtype ValidReservation = ValidReservation Reservation deriving (Eq, Show) instance Arbitrary ValidReservation where arbitrary = do rid <- arbitrary d <- (\dt -> addLocalTime (getPositive dt) now2019) <$> arbitrary n <- arbitrary e <- arbitrary (Positive q) <- arbitrary return $ ValidReservation $ Reservation rid d n e q
The newtype
is, naturally, called ValidReservation
and can, for example, be used like this:
it "responds with 200 after reservation is added" $ WQC.property $ \ (ValidReservation r) -> do _ <- postJSON "/reservations" $ encode r let actual = get $ "/reservations/" <> toASCIIBytes (reservationId r) actual `shouldRespondWith` 200
For the few properties where any Reservation
goes, a name for a newtype
now also suggests itself:
newtype AnyReservation = AnyReservation Reservation deriving (Eq, Show) instance Arbitrary AnyReservation where arbitrary = AnyReservation <$> liftM5 Reservation arbitrary arbitrary arbitrary arbitrary arbitrary
The only use I've had for that particular instance so far, though, is to ensure that any Reservation
correctly serialises to, and deserialises from, JSON:
it "round-trips" $ property $ \(AnyReservation r) -> do let json = encode r let actual = decode json actual `shouldBe` Just r
With those two newtype
wrappers, I no longer have any orphan instances.
Summary #
A simple naming scheme for newtype
wrappers for QuickCheck Arbitrary
instances, then, is:
- If the instance is truly unbounded, prefix the wrapper name with Any
- If the instance only produces valid values, prefix the wrapper name with Valid