Smart Constructors

Smart constructors are a solution to the problem of how to create a valid value of some type when the type's definition does not allow invalid values.

The Problem

For example, one might want to use a type to specify a small integer (0 to 3). A possible solution is wrapping an Int in a newtype:

module Example.SmallInt (SmallInt(..)) where

newtype SmallInt = SmallInt Int

However, anyone can create a SmallInt using an Int value that is less than 0 or larger than 3: SmallInt 5.

As another example, consider this code:

module FlawedConstructors
  ( TheType(..)
  , example
  ) where

import Partial.Unsafe (unsafeCrashWith)

-- Let's assume that String value below
-- should only be of three kinds: "apple", "orange", and "banana".
-- (Note: "String" is the wrong type for our situation. We should be using
--   something like "data Fruit = Apple | Orange | Banana".
--   I'm doing this to teach a concept. Don't do this in real code.)
data TheType = DumbConstructor String

example :: TheType -> Int
example (DumbConstructor "apple")  = 1
example (DumbConstructor "orange") = 2
example (DumbConstructor "banana") = 42
example (DumbConstructor _)        = unsafeCrashWith "This should never occur!"

Since the type and its constructor are both exported, this enables a user of this module to use it incorrectly. For example, in code outside this module, one could incorrectly write:

example (DumbConstructor "fire") -- crashes program with an error

The Solution

The solution is to not export the types' constructors and instead export "smart constructors," which are functions that create a correct value of the type. The only way to get a value of the type is to use one of these functions:

module Example.SmallInt (SmallInt, zero, one) where

newtype SmallInt = SmallInt Int

zero :: SmallInt
zero = SmallInt 0

one :: SmallInt
one = SmallInt 1

-- the same for 'two' and 'three'

In our previous example, we could also write this:

module SmartConstructors
  ( TheType -- `DumbConstructor` isn't exported

  , apple   -- but the functions that wrap the constructor are
  , orange
  , banana
  ) where

import Partial.Unsafe (unsafeCrashWith)

data TheType = DumbConstructor String

apple :: TheType
apple = DumbConstructor "apple"

orange :: TheType
orange = DumbConstructor "orange"

banana :: TheType
banana = DumbConstructor "banana"

example :: TheType -> Int
example (DumbConstructor "apple")  = 1
example (DumbConstructor "orange") = 2
example (DumbConstructor "banana") = 42
example (DumbConstructor _) = unsafeCrashWith "We can guarantee that this will never occur!"

Since DumbConstructor isn't exported, one is forced to use the apple, orange, or banana smart constructors to get a value of TheType. Thus, it prevents one from creating incorrect TheType values.

preventBadValues :: Array Int
preventBadValues =
  [ example apple  -- returns 1
  , example orange -- returns 2
  , example banana -- returns 42

  , example (DumbConstructor "fire") {- compiler error:
    "You don't have access to that constructor!" -}
  ]