Good question. What are newtypes
?
You see them in Haskell a lot. Here’s one.
newtype Dog a = Dog { getDog :: a }
We can make a Dog
as a container for a thing (in this case, a String
)
frank :: Dog String
= Dog "Frank"
frank -- frank == Dog "Frank"
Or we can unwrap it again and lose nothing along the way (this means the types String
and Dog String
are isomorphic
in maths terms)
name :: String
= getDog frank
name -- name == "Frank"
In short, they are basically the same thing. Once compiled in fact, they’re exactly the same, so there’s no cost to all this, computationally.
itsTheSame :: Bool
= "Frank" == getDog (Dog "Frank") itsTheSame
So why do this?
Well, the nice thing about a newtype
is that we can use it to pass data around with a bit more contextual information about what it means.
Let’s calculate a salary. That seems like a plausible thing to do with a computer.
calculateSalaryBad :: Int -> Int
= months * 1000 calculateSalaryBad months
This function takes a number of months, and calculates how much this person should get paid, based on a salary of 1000
(of some unknown unit) a month.
But what happens if we give it an invalid number of months?
= calculateSalaryBad (-100)
badAmount -- badAmount == -100000
That’s crazy talk! Surely this weird minus payment will send even the most well-meaning of accountants into a spin.
Let’s improve it a bit by checking if the number is negative.
calculateSalaryBetter :: (Num a, Ord a) => a -> Maybe a
= if i < 0
calculateSalaryBetter i then Nothing
else Just (i * 1000)
Note that we’re introducing the Ord
typeclass here, as we need to compare amounts. We don’t mind what a
is as long as it is both a valid number (ie, in the Num
typeclass) and is orderable (ie, the Ord
typeclass).
So now if we try this on a stupid amount, we get Nothing
= calculateSalaryBetter (-100)
safeAmount -- safeAmount = Nothing
OK. Good stuff. That should stop the accounts department crying into their sensibly priced but ultimately unsatisfying packed lunches.
The thing is, when we run this, we get this Just
wrapped around things.
= calculateSalaryBetter 12
anAmount -- anAmount == Just 12000
This is fine in isolation, but if we wanted to do a lot of calculations here we don’t want to be wrapping and unwrapping Maybe
values all over the place. It means mixing up our validation logic with our actual business logic or whatever, and that’s Bad.
What about a nice newtype
solution?
newtype PositiveNum a = PositiveNum { getPositiveNum :: a } deriving (Eq, Show)
Nothing to write home about so far, but the trick here is that Haskell allows us to export the type PositiveNum
but not the constructor PositiveNum
. That means that instead we can provide a function for making a PositiveNum
that does some validation. This means that, outside our module itself, there is no way to create a PositiveNum
that doesn’t make sense.
makePositiveNum :: (Num a, Ord a) => a -> Maybe (PositiveNum a)
makePositiveNum i| i < 0 = Nothing
| otherwise = Just (PositiveNum i)
It comes wrapped in a Maybe
, sure, but only one. It can be used over and over without needing validation, and once it is available it can be unwrapped with a quick getPositiveNum
.
num :: Int
= getPositiveNum (PositiveNum 10)
num -- num == 10
Great stuff.
Let’s make a nicer salary calculator.
calculateSalary :: (Num a) => PositiveNum a -> a
= 1000 * (getPositiveNum months) calculateSalary months
Pretty OK. Let’s bring it all together. First our library functions:
makePositiveNum :: (Num a, Ord a) => a -> Maybe (PositiveNum a)
makePositiveNum i| i < 0 = Nothing
| otherwise = Just (PositiveNum i)
zero :: (Num a) => PositiveNum a
= PositiveNum 0 zero
We’ve added zero
that just makes a default PositiveNum
with a value of 0
here, to use as a fallback if the value is ridiculous.
Now we have a function for getting a PositiveNum
for our number of months:
months :: (Num a, Ord a) => a -> PositiveNum a
= fromMaybe zero (makePositiveNum i)
months i
yes :: PositiveNum Int
= months 12
yes -- yes = PositiveNum 12
nope :: PositiveNum Int
= months (-12)
nope -- nope = PositiveNum 0
Which we can use as follows:
= calculateSalary (months 12)
total -- total == 12000
Nice. By pushing all of the validation concerns into the months
function, our actual function is nice and simple and easy to understand. Also we have a nice re-usable tool, PositiveNum
that can be used across our project everytime we need some guarantees about a value.
Bonus credit: Functor instance for a newtype
.
We can treat newtypes
like any other type, and create typeclass instances for them. For instance, we could create a functor
instance for PositiveNum
and do calculations inside it by mapping instead.
instance Functor PositiveNum where
fmap f (PositiveNum i) = PositiveNum (f i)
This lets us change the value inside PositiveNum
with an fmap
function.
calculateSalaryClever :: (Num a) => PositiveNum a -> PositiveNum a
= fmap (*1000)
calculateSalaryClever -- calculateSalaryClever 2 == PositiveNum 2000
Or the same, but unwrap it afterwards:
calculateSalaryClever2 :: (Num a) => PositiveNum a -> a
= getPositiveNum (fmap (*1000) i)
calculateSalaryClever2 i -- calculateSalaryClever2 20 == 20000
That seems pretty OK to me. Anyway, that’s enough things, time for bed.
Make sense? If not, why not get in touch?
Further reading: