Prevent operation on a custom data type value

advertisements

Let's say I create a Person type:

type Age = Int
type Height = Int
data Person = Person Age Height

This is all well, but I'd like to make sure that Age and Height cannot be added, because that makes no sense. I'd like a compile error when trying to do so.

In it's current form, this does not cause a compile error:

stupid (Person age height) = age + height

So I think about using newtype or even data for Age and Height:

newtype Age = Age Int
newtype Height = Height Int
data Person = Person Age Height

but still:

stupid (Person (Age age) (Height height)) = age + height

Is there a way of preventing age and height from being added? Or is what I'm asking for unreasonable?


By unwrapping the newtypes using the constructors Person and Height, you essentially instruct the compiler to forget the type distinction. That of course makes it legal to write age + height, but that's not to blame on the newtypes, like it's not to blame on the train driver if you smash the window, jump out and get injured!

The way to prevent such incorrect operation is to not unwrap the newtypes. The following will cause an error, if Age and Height are newtypes:

stupid (Person age height) = age + height

...not just because age and height are different types, also because neither actually supports an addition operation (they aren't Num instances). Now that's fine, as long as you don't really need to perform any operations on these values.

Chances are however that at some point you will need to perform some operation on those values. This should not require the user to unwrap the actual newtype constructors.

For such physical quantities like age and height, there are a few options to do it type-safely:

  • Add VectorSpace instances. That will allow you to add one height to another, and scale heights by fractions, but not add a height to something entirely different.

    import Data.AdditiveGroup
    import Data.VectorSpace
    
    instance Additive Height where
      Height h₀ ^+^ Height h₁ = Height $ h₀+h₁
      zeroV = Height 0
    instance VectorSpace Height where
      type Scalar Height = Double
      μ *^ Height h = Height . round $ μ * fromIntegral h
    
    

    This simple instance is mighty handy for some science use cases, though probably not that useful for your application.

  • Add meaningful unit-specific accessors. In modern Haskell, those would be lenses:

    import Control.Lens
    
    heightInCentimetres :: Lens' Height Int
    heightInCentimetres = lens (\(Height h)->h) (\(Height _) h->Height h)
    ageInYears :: Lens' Age Int
    ageInYears = lens (\(Age a)->a) (\(Age _) a->Age a)
    
    

    This way, if someone needs to work with an actual number of years of someone's age or modify it, they can do so, but they need to be explicit that these are years they're talking about. Of course, accessing the years and height as an integer number again makes it possible to circumvent the type distinction and intermingle ages with height, but this can hardly happen by accident.

  • Go all the way to a library for physical quantities.

    import Data.Metrology.SI
    
    type Height = Length
    type Age = Time
    
    

    This allows you to perform all kinds of physically legit operations while preventing nonsensical ones.


Jumping out of a train is actually more dangerous. Rather like unsafeCoerce, which really instructs the compiler to throw all type-safety out of the window and can easily cause crashes or demons flying out of your nose.