Skip to content

Type and Typeclass in Haskell

When I was reading the haskell aeson library implementation, I still a little confusing with type system especially for multiple constructors.

Hence, I learned it again and record some ideas here. It reviews the value constructor, type constructor and the strict type in type constructor.

Value Constructor

data Bool = False | True 

Written too many Go code, I'm almost lost. The True and False seems are values with type Bool, but they have a name as value constructor which means they are functions. Functions are values, frankly, because they are first class. Thing need to highlight is value is differ with function as they cannot take more parameters.

As they are functions, they can take arguments and return a value with the same type. The | enables us to combine multiple types together (which have different constructors).

Hence, the definitions of Literal are simple.

-- | Literals. @null@, @true@, @false@.
data Lit = LitNull | LitTrue | LitFalse
  deriving (Eq, Show)

For Lit, there are three value constructor without parameters.

Exclamation Mark: Laziness to Strictness

After learning the value constructor, we learned about it accepts parameter. Then the NumInteger Integer should be expected, however, it's NumInteger !Integer. What is the meaning of !?

-- | Numbers
--
-- We preserve whether the number was integral, decimal or in scientific form.
data Number
    = NumInteger !Integer  -- ^ e.g. @123@
    | NumDecimal !Scientific  -- ^ e.g. @123.456@
    | NumScientific !Scientific -- ^ e.g. @123e456@, @123e-456@ or @123.456E-967@
  deriving (Eq, Show)

The !(exclamation mark) denotes strict field, comparing with lazy field. It matters with the execution order. Under the lazy mode, the evaluation happens only when it's needed.

For example, during constructing StrictNumber, the fib 35 will always be called no matter when the value will be used later. However, when the LazyNumber is constructed, the fib 35 won't be called until somewhere else want to use it.

-- Lazy version
data LazyNumber = LazyNumInteger Integer
-- Strict version
data StrictNumber = StrictNumInteger !Integer

fib :: Integer -> Integer
fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)

lazyValue :: LazyNumber
lazyValue = LazyNumInteger (fib 35)

strictValue :: StrictNumber
strictValue = StrictNumInteger (fib 35)

Type Parameter

Receiving parameters by value constructor is reasonable, however, what's the meaning when the type receives some parameters like this?

-- | Array tokens.
data TkArray k e
    = TkItem (Tokens (TkArray k e) e)
    | TkArrayEnd k
    | TkArrayErr e
  deriving (Eq, Show)
It looks wierd but it's everywhere but easy to skip. Recall Maybe:
data Maybe a = Nothing | Just a  
The parameters taken by Maybe are types, hence, we call Maybe Type Constructor. It benefits the type when it works as boxes. As it doesn't benefit such a concrete business case:
-- doesn't benefit any
data Car a b c = Car { company :: a  
                     , model :: b  
                     , year :: c   
                     } deriving (Show)  

Never Add Type Constraints in Data Declaration

When defining a new type which receives some types, constrainting the parameters sounds great. For example, if we want to define a Map because we need to compare elements when constructing.

data (Ord k) => Map k v = ...  
However, it doesn't benefit a lot, but ending up writing more class constraints, even they are useless. For example, if you want to write toList :: Map k a -> [(k, a)]. If you have a constraint when declaring Map, it is duplicated in toList.

Putting it in function type declarations is enough.