Learning Haskell: Types

posted 2 years ago

What are Types Used For?

Everything in Haskell originates in a data constructor from some definition of a type.

Because Haskell is an implementation of a pure lambda calculus, Haskell uses type systems.

Type systems in mathematics and logic are designed to impose constraints that enforce correctness.

Well-designed type systems help eliminate some classes of errors as well as concerns such as what the effect of a condition over a non-Boolean value might be.

Consider the Bool type. It is a set of two inhabitants: True and False. Anytime a True or False occurs in a Haskell program, the typechecker will automatically know that they are members of the Bool type.

In addition, whenever the type Bool is declared in a type signature, the compiler will automatically expect one of those two values. You will get a type error if you try to pass anything other than True or False.

Haskell has a static typing system, which means that typechecking occurs at compile time. This is handy because various errors will be caught before you try to execute your program. Runtime errors still exist, but the type system reduces the number and kinds of errors that you may receive.

Good type system implementations can also offer compiler optimizations. This is because the compiler can know certain things about the program based on the types.

Types also may serve as documentation for your program.

Some may prefer dynamic typing (no types) over static typing, but the upfront cost of static typing pays off in the end. Static typing offers safer code that is easier to maintain.

Reading Type Signatures

When we query the types of values, we get useful information:

Prelude> :type True
True :: Bool
Prelude> :type "Clayton"
"Clayton" :: [Char]
Prelude> :type 'C'
'C' :: Char

When we query the types of numeric values, we see typeclass information instead of a concrete type, because the compielr doesn't know the specific numeric type until it is either declared or the compiler infers it:

Prelude> :type 5
5 :: Num p => p

We can make it more explicit by declaring the type:

Prelude> let x = 5 :: Integer
Prelude> :t x
x :: Integer

The Function Type

(->), is the type constructor for functions in Haskell.

It's similar to Bool in that it's a type constructor. The difference is that it takes arguments and has no data constructors.

Prelude> :info (->)
data (->) (a :: TYPE q) (b :: TYPE r)   -- Defined in ‘GHC.Prim’
infixr -1 ->

Functions are values.

A function can be applied, and the structure of this type demonstrates this. The arrow function is an infix operator that has two parameters and associates to the right. We know this by infixr.

Let's break down the function fst:

fst :: (a, b) -> a
--       [1] [2][3]
  1. The first parameter with the type (a, b).
  2. The function type (->) with two parameters. One is (a, b) and the other is result a.
  3. The result of the function, with the type a. It's the same a that is in the tuple (a, b).

We know that it's the same a because the output type a must be the same type as the input type a. Nothing happens here that could transform the a into any other value of that type.

Let's break down the function length:

Prelude> :type length
length :: [a] -> Int

The length function takes one argument that is a list. It returns an Int result. It does not care what types are inside of that list.

Typeclass Constrained Type Variables

Let's look at the types of some arithmetic functions:

Prelude> :type (+)
(+) :: Num a => a -> a -> a
Prelude> :type (/)
(/) :: Fractional a => a -> a -> a

Additon takes one numeric argument and adds it to a second numeric argument of the same type, then it returns the result.

Division takes one fractional argument, divides it by a second fractional value, and returns a fractional value as a result.

Notice that they are constrained by a typeclass. Each typeclass offers a standard set of fucntions that can be used across several concrete types.

We say that this functions are constrained because we don't know the concrete type of a. However, we do know that it can only be one of the types that includes the required typeclass instance.

We can do some cool stuff with numbers knowing this:

Prelude> let five = 5
Prelude> :t five
five :: Num p => p
Prelude> let fiveInt = five :: Int
Prelude> let fiveDouble = five :: Double
Prelude> :t fiveInt 
fiveInt :: Int
Prelude> :t fiveDouble 
fiveDouble :: Double

We changed a Num p => p to Int and Double. This is possible because Int and Double have an instance of the Num typeclass:

Prelude> :info Num
-- Stuff removed
instance Num Int -- Defined in ‘GHC.Num’
instance Num Double -- Defined in ‘GHC.Float’

Since they both have instances of Num, they can use the operations from Num like addition or subtraction.

Currying

In Haskell, all functions take in one argument and return one result. Haskell has syntactic sugar that construct curried functions by default.

Currying refers to the nesting of multiple functions, each accepting one argument and returning one result. This allows for an illusion of multiple parameter functions.

Let's review the datatype definition of arrows again.

data (->) a b

In order to have a function, you must have one input, the a, to apply the function to, and you'll get one result back, the b.

Each arrow in a type signature represents one argument and one result, with the final type being the final result.

Let's break this down by looking at the type signature for addition again because this function requires multiple inputs.

(+) :: Num a => a -> a -> a
      |   1   ||    2   |[3]
  1. The typeclass constraint. a must have an instance of Num. Addition is defined in the Num typeclass.
  2. The boundaries set what you call the two parameters to the function (+). Remember though, all functions in Haskell take one argument and return one result. This represents successive function applications, each taking one argument and returning one result. The function at the outermost layer is returning another function that accepts the next argument.
  3. This is the result type for the function. It will be a number of the same type as the two inputs.

Remember that (->) is right associative. Because of this, types are implicitly parenthesized:

f :: a -> a -> a

-- associates to

f :: a -> (a -> a)

Partial Application

What is the practical value of currying?

Partial application explains the value of currying.

addingStuff :: Integer -> Integer -> Integer
addingStuff a b = a + b + 5

addingStuff appears to take one argument, but if you loading it in GHCi, you can see that it is taking one argument and returning a function that takes one argument and returns one result.

*Currying> :type addingStuff 
addingStuff :: Integer -> Integer -> Integer
*Currying> addingTwo = addingStuff 2
*Currying> :type addingTwo 
addingTwo :: Integer -> Integer
*Currying> twelve = addingTwo 5
*Currying> twelve
12

Here, twelve is equal to addingStuff 5 2 because addingTwo is equal to addingStuff 2.

This lets us reuse addingStuff and create new functions from it with one of the arguments applied.

Manual currying and uncurrying

Haskell curries by default. However, you can uncurry functions. You can unnest functions and replace the two functions with a tuple of two values (the two values you want to use as arguments).

If you uncurry (+), the type changes from Num a => a -> a -> a to Num a => (a, a) -> a. This can be useful as it appears more like "takes two arguments, returns one result".

module Currying where

throwAway :: Bool -> Integer
throwAway False = 10
throwAway True = 20

curriedFunc :: Integer -> Bool -> Integer
curriedFunc i b = i + throwAway b

uncurriedFunc :: (Integer, Bool) -> Integer
uncurriedFunc (i, b) = i + throwAway b

anonymousFunc :: Integer -> Bool -> Integer
anonymousFunc = \i b -> i + throwAway b

anonymousNestedFunc :: Integer -> Bool -> Integer
anonymousNestedFunc = \i -> \b -> i + throwAway b

These are all the same function, giving the same results. You can substitute the backslash with lambda if you would like to compare this with the lambda calculus.

Sectioning

Sectioning specifically refers to partial application of infix operators, which has a special syntax and allows you to choose wehther the argument you're partially applying the operator to is the first or second argument.

Prelude> let x = 5
Prelude> let y = (3^)
Prelude> let z = (^3)
Prelude> y x
243
Prelude> z x
125

Polymorphism

Polymorph is a word of recent creation. It was invented in the 19th century from the Greek words poly for "many" and morph for "form". The -ic suffix means "made of". So, all together, polymorphic means "made of many forms". This is in constrast with monomorphic or "made of one form".

Polymorphic type variables allow us to implement expressions that can accept arguments and return results of different types without having to write variations on the same expression for each type.

In Haskell, polymorphism is divided into two categories: parametric polymorphism and constrained polymorphism.

Constrained polymorphism is implemented with typeclasses in Haskell.

Parametric polymorphism is broader than constrained polymorphism. Parametric polymorphism refers to type variables, or parameters, that are fully polymorphic.

If you see a lowercase name in a type signature, it is a type variable and polymorphic (like a, t, etc.).

If you see an uppercase name in a type signature, it is a specific, concrete type such as Int, Bool, etc.

Let's consider a parametrically polymorphic function: identity.

The id function comes with the Prelude and is called the identity function because it is the identity for any value in Haskell. Passing any value to id will return the same value:

id :: a -> a

For all a, get an argument for some type a, and return a value of the same type a.

Again, it works with any type of data.

Prelude> id 5
5
Prelude> id "Clayton"
"Clayton"
Prelude> id 'C'
'C'

A function is polymorphic when its type signature has variables that can represent more than one type.

Parametric polymorphism is full polymorphism. It is not constrained by a typeclass.

Parametricity is the property we get from having parametric polymorphism. Parametricity means that the behavior of a function with respect to the types of its arguments is uniform.

Type Inference

Haskell does not make us assert a type for every expression or value because it has type inference.

Type inference is an algorithm that determines types of expressions.

Haskell will infer the most generally applicable (polymorphic) type that is still correct.

An example of Haskell inferring types:

Prelude> let greeting str = str ++ " Clayton!"
Prelude> greeting "Hello"
"Hello Clayton!"
Prelude> :t greeting
greeting :: [Char] -> [Char]

The type inference is able to explicitly infer the type signature because of the function (++). It has one value to work with already and it knows that it is a String so it can easily infer this.

Replacing the string value with another variable:

Prelude> let greeting str str2 = str ++ str2
Prelude> greeting "Hello " "Clayton!"
"Hello Clayton!"
Prelude> :t greeting
greeting :: [a] -> [a] -> [a]

It was not able to infer the types because it has no information by which to do so. Thus, it used a polymorphic type signature.

Asserting Types for Declarations

It is considered best practice to explicitly state the types.

It can be self documenting and can help the compiler give you information about where your code is going wrong.

Type signatures can also help other programmers trying to use your code read it and figure out what it does.

Sources

Learn You a Haskell - http://learnyouahaskell.com/types-and-typeclasses

Haskell Book - https://haskellbook.com/