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 (ShowEq)
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 the Glasgow Haskell Compiler (GHC) can automatically implement fmap with a language extension:

{-# LANGUAGE DeriveFunctor #-}
module Builder where
newtype Builder a = Builder a deriving (ShowEqFunctor)

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 { 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.


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.

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 Google Plus, or somewhere else with a permalink. Ping me with the link, and I may add it as a comment.


Monday, 04 September 2017 07:41:00 UTC


"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!