Deriving Nullable
Deriving Nullable
In the first exercise in this chapter you should have created an instance of
Nullable for Maybe and list values. There are a few ways that you could have
approached writing these instances, but let’s look at some reasonable
definitions you might have used:
module DerivingNullable where
import Prelude hiding (null)
import qualified Prelude (null)
class Nullable a where
isNull :: a -> Bool
null :: a
instance Nullable [a] where
isNull = Prelude.null
null = []
instance Nullable (Maybe a) where
isNull Nothing = True
isNull _ = False
null = NothingThese instances use a fairly intuitive definition of what should be considered
null: empty lists are null, as are Nothing values. What if we have an
optional list though?
λ isNull Nothing
True
λ isNull []
True
λ isNull (Just [])
False
In this case it’s not clear whether Just [] should be considered a null value
or not, it depends entirely on the program we are writing. You can even imagine
that we might want different behavior in different parts of the same program.
In this exercise, try to create an API so that a user can make use of deriving via to create Nullable instances of their own types. A user should be able to
decide whether Just [] should be considered a null value or not by selecting
which type they derive their instance from.
Hints
Click to reveal
If you’re having trouble imagining how an API like this might be used, imagine that you’re working with some text data that will be provided by user and you need to decide whether a user provided a value or not. In some cases, an empty string might be a valid input when the user doesn’t have anything more meaningful to input. In other cases, you might want to ensure that they’ve provided some actual data.
For example, imagine that you wanted to define two types: OptionalString and
OptionalNonEmptyString. You might start by defining them like this:
newtype OptionalString = OptionalString { getString :: Maybe String }
deriving stock (Eq, Show)
newtype OptionalNonEmptyString = OptionalNonEmptyString { getNonEmptyString :: Maybe String }
deriving stock (Eq, Show)Instead of writing instances manually, think about how you could provide a way
for users to use deriving via with these types.
Click to reveal
You’ll need to create a newtype for each of the behaviors you want to make
available with deriving via, along with a Nullable instance for each.
Click to reveal
Trying creating two types with Nullable instances. First, create a type named
BasicNullable that should have isNull return True only if there’s a
missing value. Next, create one called TransitiveNullable with a definition of
isNull that will return also true if the inner value is null.
Solution
Click to reveal
We’ll start our solution by creating a new module and re-defining
Nullable. We’ll go ahead and add the DerivingVia extension too, since we’re
planning to use it later in this exercise:
{-# LANGUAGE DerivingVia #-}
module EffectiveHaskell.Exercises.Chapter6.DerivingNullable where
import Prelude hiding (null)
class Nullable a where
isNull :: a -> Bool
null :: aNext, let’s define some types that represent the different “templates” that we
might want to use with deriving via to get our behaviors. If you looked at the
hints earlier, you’ll know that we’re going to create two types: BasicNullable
and TransitiveNullable. Let’s start with BasicNullable first.
We want our BasicNullable type to represent optional values that are only
considered null if they are truly missing a value. Let’s start by creating the
type. We’ll also add a Show instance to make things easier when we want to
test the code in ghci later:
newtype BasicNullable a = BasicNullable (Maybe a)
deriving stock ShowWe also need to create a Nullable instance for BasicNullable. For this basic
definition of Nullable, we’ll consider a Nothing value to be null, and
anything else will be non-null:
instance Nullable (BasicNullable a) where
isNull (BasicNullable Nothing) = True
isNull _ = False
null = BasicNullable NothingNext, let’s do the same thing for TransitiveNullable. Unlike BasicNullable,
this instance will only consider something non-null if it contains a non-null
value:
newtype TransitiveNullable a = TransitiveNullable (Maybe a)
deriving stock Show
instance Nullable a => Nullable (TransitiveNullable a) where
isNull (TransitiveNullable Nothing) = True
isNull (TransitiveNullable (Just a)) = isNull a
null = TransitiveNullable NothingEven though we created BasicNullable an TransitiveNullable so that we could
use them with deriving via, they are still valid ordinary types and we can
test their behavior in ghci. Let’s run a few tests to make sure everything’s
working as we expect. Let’s start by testing out the isNull instance of
BasicNullable:
λ isNull $ BasicNullable Nothing
True
λ isNull $ BasicNullable (Just "hello")
False
We can see from these examples that isNull appears to be working for the
obvious cases, but we should still test that isNull correctly returns False
when we have Just some empty value. One way we can test this is to nest some
BasicNullable values:
λ isNull $ BasicNullable (Just $ BasicNullable Nothing)
False
This is a little weird though. Let’s add an instance of Nullable for lists so
that we have some values to test with:
instance Nullable [a] where
isNull [] = True
isNull _ = False
null = []Using these two instances, we can see that while [] is null, BasicNullable (Just []) continues to be treated as non-null:
λ isNull []
True
λ isNull [1,2,3]
False
λ isNull $ BasicNullable (Just [])
False
Now that we’ve figured out BasicNullable let’s move on to
TransitiveNullable. Just like before, we’ll start by creating a new type:
newtype TransitiveNullable a = TransitiveNullable (Maybe a)
deriving stock ShowWe’ll also create a new instance of Nullable. Unlike our earlier instance,
we’ll need to make sure that whatever type we’re holding is also Nullable
since we’ll need to check to see if the values we’re working with are null or
not:
instance Nullable a => Nullable (TransitiveNullable a) where
isNull (TransitiveNullable Nothing) = True
isNull (TransitiveNullable (Just a)) = isNull a
null = TransitiveNullable NothingLike before, we can load this up into ghci to test it:
λ isNull $ TransitiveNullable (Just "hello")
False
Unfortunately, if we try to test the empty case of TransitiveNullable the same
way we tested BasicNullable we’ll get an error:
λ isNull $ TransitiveNullable Nothing
<interactive>:20:1: error:
• Ambiguous type variable ‘a0’ arising from a use of ‘isNull’
prevents the constraint ‘(Nullable a0)’ from being solved.
Probable fix: use a type annotation to specify what ‘a0’ should be.
These potential instances exist:
instance Nullable (BasicNullable a)
-- Defined at EffectiveHaskell/Exercises/Chapter6/DerivingNullable.hs:12:10
instance Nullable OptionalNonEmptyString
-- Defined at EffectiveHaskell/Exercises/Chapter6/DerivingNullable.hs:35:12
instance Nullable OptionalString
-- Defined at EffectiveHaskell/Exercises/Chapter6/DerivingNullable.hs:31:12
...plus two others
(use -fprint-potential-instances to see them all)
• In the first argument of ‘($)’, namely ‘isNull’
In the expression: isNull $ TransitiveNullable Nothing
In an equation for ‘it’: it = isNull $ TransitiveNullable Nothing
The problem here is the definition of Nullable we defined for
TransitiveNullable a relies on the definition of Nullable for
a. When we use a value like Just "hello" the compiler can infer the type of
a must be String. When we use Nothing we’re not giving the compiler enough
information to figure out what a should be, so it can’t pick a Nullable
instance. We can help it out by adding a visible type application to
TransitiveNullable to tell it what type to use for a:
λ isNull $ TransitiveNullable @String Nothing
True
Alternatively, you can add a type annotation to Nothing:
λ isNull $ TransitiveNullable (Nothing :: Maybe String)
True
Now that we’ve covered both obviously null and obviously non-null cases, let’s
take a look at an example where TransitiveNullable and BasicNullable should
differ: A value that contains an empty list. Let’s test them side-by-side:
λ isNull $ TransitiveNullable (Just [])
True
λ isNull $ BasicNullable (Just [])
False
As expected, the TransitiveNullable instance considers an empty list to be
null, while the BasicNullable instance doesn’t.
Now that we’ve created two different types that have our desired Nullable
behaviors, how can we use them with deriving via? To start with, let’s imagine
that we’re dealing with some text data. As a specific example, imagine that
you’re processing some data submited by a user, and you want to ensure that
you’re getting valid data. In some cases, you might have data that could be
empty, while in other cases you want to ensure that there’s actual data. For
example, a user signing up for a service might be required to enter a password,
but the “how did you hear about us” field could be left empty.
We’ll represent these two types of data with the types OptionalString and
OptionalNonEmptyString:
newtype OptionalString = OptionalString { getString :: Maybe String }
deriving stock Show
newtype OptionalNonEmptyString = OptionalNonEmptyString { getNonEmptyString :: Maybe String }
deriving stock ShowYou can imagine that we could write Nullable instances for these two types
that are identical to the BasicNullable and TransitiveNullable types we just
created, but thanks to deriving via we don’t need to. Instead, we can add the
DerivingVia extension, and use it to select an instance:
newtype OptionalString = OptionalString { getString :: Maybe String }
deriving stock Show
deriving Nullable via BasicNullable String
newtype OptionalNonEmptyString = OptionalNonEmptyString { getNonEmptyString :: Maybe String }
deriving stock Show
deriving Nullable via TransitiveNullable StringLet’s load this code up into ghci and check that it behaves like we’d
expect. Starting with OptionalString:
λ isNull $ OptionalString (Just "hello")
False
λ isNull $ OptionalString (Just "")
False
λ isNull $ OptionalString Nothing
True
As you can see, since we derived the Nullable instance for OptionalString
from BasicNullable, the behavior is the same. Next, let’s look at
OptionalNonEmptyString:
λ isNull $ OptionalNonEmptyString (Just "hello")
False
λ isNull $ OptionalNonEmptyString (Just "")
True
λ isNull $ OptionalNonEmptyString Nothing
True
Success! It appears that OptionalNonEmptyString is now using the same behavior
as TransitiveNullable.
You might have noticed that, in practice, using deriving via in this case
didn’t buy us much- it seems as though we’ve actually done more work by creating
the generic types and then deriving our instances from them instead of creating
instances for OptionalString and OptionalNonEmptyString directly. In this
case it’s true, but as soon as we need a second, third, or fourth type that
would have the same boilerplate implementation of a type class, then we’ll have
saved ourselves some effort. Realistically, it’s not always clear when you’ll
want to reuse some definition of a typeclass, so you might find that instead of
creating the generic reusable types to start with, you recognize that you have
the same definition in multiple places and instead factor those out into a
common definition that you can use with deriving via.