From http://learnyouahaskell.com/making-our-own-types-and-typeclasses
data Person = Person { name :: String
, age :: Int
} deriving (Show)
In a real application, using primitives like String and Int for name and age would constitue primitive obsession, a code smell. (also obviously Date born is preferable to Int age but let's ignore that) Instead, one would prefer something like
newtype Person = Person { name :: Name
, age :: Age
} deriving (Show)
In an OO language this would look something like
class Person {
Name name;
Age age;
Person(Name name, Age age){
if (name == null || age == null)
throw IllegalArgumentException();
this.name = name;
this.age = age;
}
}
class Name extends String {
Name(String name){
if (name == null || name.isEmpty() || name.length() > 100)
throw IllegalArgumentException();
super(name);
}
}
class Age extends Integer {
Age(Integer age){
if (age == null || age < 0)
throw IllegalArgumentException();
super(age);
}
}
But how is the same achieved in idiomatic, best practice Haskell?
Make Name
abstract and provide a smart constructor. This means that you do not export the Name
data constructor, and provide a Maybe
-returning constructor instead:
module Data.Name
( Name -- note: only export type, not data constructor
, fromString
, toString
) where
newtype Name = Name String
fromString :: String -> Maybe Name
fromString n | null n = Nothing
| length n > 100 = Nothing
| otherwise = Just (Name n)
toString :: Name -> String
toString (Name n) = n
It is now impossible to construct a Name
value of the wrong length outside of this module.
For Age
, you could do the same thing, or use a type from Data.Word
, or use the following inefficient but guaranteed non-negative representation:
data Age = Zero | PlusOne Age