Type system

Type declarations

double :: Int -> Int is the type declaration for the double function. It says it takes one argument of type Int and returns a value of type Int. Types do not always have to be explicitly specified. Haskell has type inference which mean the compiler can work out the types itself.

Common built in types

  • Char - A single character

  • String - A list of characters

  • Int - A bounded integer, usually either 32 or 64 bits

  • Integer - An arbitrary precision integer which can hold as large of a number as the system has memory for

  • Bool - A Boolean value

  • Float - A single precision floating point number

  • Double - A double precision floating point number

Polymorphism

Consider the identity function, id x = x. This function can take an argument of any type and return a value of the same type. It has the polymorphic type signature id :: a -> a. Polymorphic types can be any type.

Data declarations

The data keyword can be used to define new data types.

data Bool = True | False

Bool is the name of the data type and True and False are its constructors. True and False are both of type Bool.

Polymorphic data types

Similarly to polymorphic functions, it is possible to have polymorphic data types.

data Maybe a = Nothing | Just a

The Maybe data type has two constructors - Nothing :: Maybe a and Just :: a -> Maybe a. a is a polymorphic variable and means Maybe can wrap around any type. These can be constructed as follows:

noNumber :: Maybe Int
noNumber = Nothing

yesBool :: Maybe Bool
yesBool = Just True

These values can be pattern matched in functions. For example a function valueOrZero can take a Maybe Int and return an Int.

valueOrZero :: Maybe Int -> Int
valueOrZero (Just x) = x
valueOrZero Nothing  = 0

Sum and product types and cardinality

Sum types are of the form data Type = Con1 | Con2 and product types are of the form data Type = Type Int Int. The cardinality of a type is the number of possible values it can take. For example the type Bool can either be true or false so it has cardinality 2. A type data TwoBools = TwoBools Bool Bool is a product type and has cardinality 4, the product of 2 and 2. data SomeBools = NoBools | TwoBools Bool Bool will have cardinality 5, the sum of the cardinalities of its constructors.

Newtype and type

The newtype keyword is used to create a type with a single constructor. For example newtype Age = Age Int. Age and Int can't be used interchangeably.

type can be used to define another name for a type. It is a synonym, not a separate type. For example type String = [Char]

Recursive data types

A recursive data type refers to itself in its definition. Consider the definition of a list-like type:

data List a = Empty | Cons a (List a)

This is a recursive data type as the definition of list refers to itself.

Records

Record syntax is a convenient way to create large product types. Consider a type which represents a person:

data Person = Person String String (Maybe String) Int
bob :: Person
bob = Person "Robert" "Smith" (Just "Bob") 63

The meaning of the arguments to Person are not immediately obvious and this is prone to mistakes. It is also difficult to extract data from the type.

Instead, the Person type can be defined using record syntax.

data Person = Person {
    firstName     :: String,
    lastName      :: String,
    preferredName :: Maybe String,
    age           :: Int
}
bob = Person{
    firstName     = "Robert",
    lastName      = "Smith",
    preferredName = Just "Bob",
    age           = 63
}

This is much more readable and less prone to error.

Records also define accessor functions for each field. These accessor functions have the same name as the fields and given a record value it will return the value for the corresponding field. For example, firstName bob would return Robert. It is important that field names are chosen such that they don't cause naming collisions.

It is also easier to update values in record syntax. For example, a function which updates the first name would be written as follows:

updateFirstName newName person = person {firstName = newName}

Kinds

The types of types and type constructors are called kinds. Types which take no parameters are of kind * such as String :: *. Types like Maybe which take one type parameter are of type * -> *. When a type parameter is applied the kind is reduced, so Maybe :: * -> * whereas Maybe Int :: *. All fully applied data types and all primitive types are of kind * . Kinds allow the compiler to determine which types are well formed. Kinds also determine which constructors can be instantiated for a type class. Show can only be defined for fully qualified types (kind *) and Foldable can only be defined for constructorss with a single type parameter, kind *->*.