Search code examples
haskelltypesprimitive

Avoiding primitive obsession in Haskell


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?


Solution

  • 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