Builder as Identity by Mark Seemann
In which the Builder functor turns out to be nothing but the Identity functor in disguise.
This is the fourth in a series of articles about the relationship between the Test Data Builder design pattern, and the identity functor. In the previous article, you saw how a generic Test Data Builder can be modelled as a functor.
You may, however, be excused if you're slightly underwhelmed. Modelling a Test Data Builder as a functor doesn't seem to add much value.
Haskell's Identity functor #
In the previous article, you saw the Builder functor implemented in various languages, including Haskell:
newtype Builder a = Builder a deriving (Show, Eq) instance Functor Builder where fmap f (Builder a) = Builder $ f a
The fmap
implementation is literally a one-liner: pattern match the value a
out of the Builder
, call f
with a
, and package the result in a new Builder
value.
For many trivial functors, it turns out that the Glasgow Haskell Compiler (GHC) can automatically implement fmap
with a language extension:
{-# LANGUAGE DeriveFunctor #-} module Builder where newtype Builder a = Builder a deriving (Show, Eq, Functor)
Notice the DeriveFunctor
language extension. This enables the compiler to automatically implement fmap
by adding Functor
to the deriving
list.
Perhaps we should take this as a hint. If the compiler can automatically make Builder
a Functor
, perhaps it doesn't add that much value.
This particular Builder
is equivalent to Haskell's built-in Identity
functor. Identity
is a 'no-op' functor, if you will. While it's a functor, it doesn't 'do' anything. It's similar to the Null Object design pattern, in the sense that the only value it adds is that it enables you to turn any naked value into a functor. This can occasionally be useful if you need to pass a functor to an API.
PostCode and Address builders #
You can rewrite the previous PostCode
and Address
Test Data Builders as Identity
values:
postCodeBuilder :: Identity PostCode postCodeBuilder = Identity $ PostCode [] addressBuilder :: Identity Address addressBuilder = Identity Address { street = "", city = "", postCode = pc } where Identity pc = postCodeBuilder
As in the previous examples, postCodeBuilder
is nothing but a 'good' default PostCode
value. This time, it's turned into an Identity
value, instead of a Builder
value. The same is true for addressBuilder
- notice that it uses postCodeBuilder
for the postCode
value.
This enables you to build an address in Paris, like previous examples:
Identity address = fmap (\a -> a { city = "Paris" }) addressBuilder
This builds an address with city
bound to "Paris"
, but with all other values still at their default values:
Address {street = "", city = "Paris", postCode = PostCode []}
You can also build an address from an Identity
of a different generic type:
Identity address' = fmap newAddress postCodeBuilder where newAddress pc = Address { street = "Rue Morgue", city = "Paris", postCode = pc }
Notice that this example uses postCodeBuilder
as an origin, but creates a new Address
value. In this expression, newAddress
is a local function that takes a PostCode
value as input, and returns an Address
value as output.
Summary #
Neither F# nor C# comes with a built-in identity functor, but it'd be as trivial to create them as the code you've already seen. In the previous article, you saw how to define a Builder<'a>
type in F#. All you have to do is to change its name to Identity<'a>
, and you have the identity functor. You can perform a similar rename for the C# code in the previous articles.
Since the Identity functor doesn't really 'do' anything, there's no reason to use it for building test values. In the next article, you'll see how to discard the functor and in the process make your code simpler.
Next: Test data without Builders.