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 (EqShowReadGeneric)

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 (EqShow)
 
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 (EqShow)
 
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
This strikes me as a practical naming scheme. Other variations seem natural. If, for example, you need an invalid value, you can prefix the wrapper name with Invalid. Why you'd need that, though, I'm not sure.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 02 September 2019 13:07:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 02 September 2019 13:07:00 UTC