In Haskell, the type system is quite detailed. From a theoretical point of view, it comes from typed Lambda calculus, introduced in 1930 by Alfonso Church,, where the types are automatically deducted from the way the objects are processed. Such programming languages are statically typed. More precisely, the processing is enforced by and based on the rules of a mathematical type system.
A
type system
is a set of rules used in a programming language to organize, build, and handle the types that will be accepted in the language. These
rules focus on some important aspects, such as the following:
Defining new types
Associating types with different language constructs
Focusing on type equivalence, which is important to determine when different types are the same
Verifying type compatibility, which is useful to check whether the value of a specific type is correctly used in a given processing context
Deducing the type of the language when it is not declared, which can be done by applying rules for synthesizing the type of a construct from the types of its components
As you already know, Haskell is a fully static, scoped
language, and the top-level variables have static scope. In other words, the whole program module will contain their definition. This provides
referential transparency if no side effect will rise. This is an example of a top-level expression:
In this expression, variable has expression as its value.
Currying and Uncurrying
Currying
represents the process of transforming a function that has multiple arguments in a tuple with the same type as the arguments into a function that will take just a single argument and will return another function that will accept further arguments, one by one.
The following expression:
g :: x -> (y -> z) -- The above expression can be written as g :: x -> y -> z
represents the curried form of the following:
You can convert these two types in any direction with the help of the prelude functions
curry and
uncurry
.
If you take a closer look, both forms are equally expressive. They will hold the following:
In Haskell, all functions are considered curried. That is, all functions in Haskell are able to take just one argument. In the examples, you will see that this is mostly hidden in the notation.
For example, the first expression defined next will attach the variable
plus to a curried function. A curried function is a functional closure. The second expression declared will add
factorial to a recursive function, which will compute
n!.
plus = a -> ( b -> a + b)
plus :: Int -> Int -> Int
factorial = n -> if n==1 then 1 else n*factorial(n-1)
factorial :: Int -> Int
Observe that the expression produces two values: the value that the expression has and the type that the value has. The type of the function is called
signature. For example, the value of
plus is a function of a domain in which
int represents the range. The type of the expression does not need to be explicitly declared; it is automatically deduced based on the types and the expression’s components. Type deduction involves the verification of the type as a natural task or subtask. As a general conclusion, an
n-ary curried function can be defined as
p1 p2 ... pn -> expression. The function
plus can be declared as follows:
plus = a b -> a + b
plus :: int -> int -> int
This is the short notation of the same thing:
The functions can be used in infix and prefix forms. For example, the application
(+) a b is equivalent to the more familiar
a + b, and
mod a b can be written
a 'mod' b.
(op) corresponds to the prefixed form of an infix binary operator, while
'op' corresponds to the infixed form of a prefix binary operator.
comp m n = ( a -> m(n a))
comp :: (b -> c) -> (d -> e) -> d -> e
ff = ( a -> a*a) `comp` ( a -> a+a)
ff :: Integer -> Integer
Output:
ff 2
16
In the previous example, the types that can be found in the signature of the composition function comp are not defined as constants. They represent generic types, which are represented by type variables in the format identifier. As you can observe, the function declared, comp, is polymorphic. This means that the arguments the function can take are of any type that obey its signature.
Scoping Variables
The
scoping variables
represent an extension of Haskell’s type system, which allows free type variables to be reused in the scope of a function. Consider the following function
as an example:
mkpair1 :: forall a b. a -> b -> (a,b)
mkpair1 aa bb = (ida aa, bb)
where
ida :: a -> a -- This refers to a in the function's type signature
ida = id
mkpair2 :: forall a b. a -> b -> (a,b)
mkpair2 aa bb = (ida aa, bb)
where
ida :: b -> b -- Illegal, because refers to b in type signature
ida = id
mkpair3 :: a -> b -> (a,b)
mkpair3 aa bb = (ida aa, bb)
where
ida :: b -> b -- Legal, because b is now a free variable
ida = id
It is better to avoid scoped type variables because they are not available in all compilers. A solution is available in Haskell 98.
For example, the following can be interpreted as
x 'asTypeOf' y and has the same value as
x, but the deduced type says that
x and
y have the same type.
asTypeOf :: a -> a -> a
asTypeOf a b = a
Let’s look at the following examples for the
let declaration:
let {var1 = expr1; var2 = expr2; ..., varn = exprn) in expr
expr where {var1 = expr1; var2 = expr2; ..., varn=exprn}
The scope of
vari (where
i = 1,
n) represents the whole expression that contains the definition of
vari. The
variable vari is tied to the unevaluated expression
expri (keep in mind that the evaluation in Haskell is lazy, as you will see in Chapter
16). The result of
expr is represented by the
let expression.
is_even = let {is_even n = n == 0 || n > 0 && is_odd(n-1);
is_odd n = n == 1 || n > 1 && is_even(n-1)}
in is_even
is_even:: Integer -> Bool
is_even' = is_even where
{is_even n = n == 0 || n > 0 && is_odd(n-1);
is_odd n = n == 1 || n > 1 && is_even(n-1)}
Types
As mentioned, Haskell is statically typed.
From an algebraic point of view, a
type is a triple like
T=<V, Op, Ax>. Here,
V is the set of type values (the carrier set of the type),
Op is defined as the set of type operators (including the signatures of the operators), and
Ax represents the set of axioms describing the behavior and how the operators interact. To illustrate this, let’s consider the following example:
type α list is
Op
[]: → α list // a list of the constructors
: : α × α list →α // [] represents the empty list, : is like cons
head: α list { [] } →α // a list of the selectors
tail: α list { [] } →α // head as carrier, tail is as a cdr
null: α list → bool // a list of the predicates
Ax
L: α list, x: α
null [] = true // testing if the list is empty
null (x:L) = false
head(x : L) = x // the head of the list
tail(x : L) = L // the tail of the list
Summary
This chapter presented the most important aspects of static typing.
You learned about the following:
Statically typing systems and how they are defined during a program/module
How new types can be defined
The rules behind the type system
Indentation and its importance
Types and how they are defined
References
- 1.
A. Serrano Mena, Beginning Haskell: A Project-Based Approach (Apress, 2014).
- 2.
- 3.