definition.
Here the two evaluation schemes result in the following sequence of reduction steps (we hide the steps involving simplification of the conditional expression to make another point):
factorial 3
factorial 3
= fact (3,1)
= fact (3,1)
= fact (3-1,3*1)
= fact (3-1,3*1)
= fact (2,3)
= fact (2-1,2*(3*1))
= fact (2-1,2*3)
= fact (1-1,1*(2*(3*1)))
= fact (1,6)
= 1*(2*(3*1))
= fact (1-1,1*6)
= 1*(2*3)
= fact (0,6)
= 1*6
= 6
= 6
The point to appreciate is that, while the number of reduction steps is basically the same, lazy evaluation requires much more space to achieve the answer. The expression 1*(2*(3*1)) is built up in memory before being evaluated.
The pros and cons of lazy evaluation are briefly as follows. On the plus side, lazy evaluation terminates whenever any reduction order terminates; it never takes more steps than eager evaluation, and sometimes infinitely fewer. On the minus side, it can require a lot more space and it is more difficult to understand the precise order in which things happen.
Haskell uses lazy evaluation. ML (another popular functional language) uses eager evaluation. Exercise D explores why lazy evaluation is a Good Thing. Lazy evaluation is considered further in Chapter 7 .
A Haskell function f is said to be strict if f undefined = undefined , and nonstrict otherwise. The function three is nonstrict, while (+) is strict in both arguments. Because Haskell uses lazy evaluation we can define nonstrict functions. That is why Haskell is referred to as a nonstrict functional language.
Haskell has built-in (or primitive) types such as Int , Float and Char . The type Bool of boolean values is defined in the standard prelude: data Bool = False | True
This is an example of a data declaration . The type Bool is declared to have two data constructors , False and True . The type Bool has three values, not two: False , True and undefined :: Bool . Why do we need that last value? Well, consider the function
to :: Bool -> Bool
to b = not (to b)
The prelude definition of not is
not :: Bool -> Bool
not True = False
not False = True
The definition of to is perfectly wellformed, but evaluating to True causes GHCi to go into an infinite loop, so its value is ⊥ of type Bool . We will have much more to say about data declarations in future chapters.
Haskell has built-in compound types, such as
[Int] a list of elements, all of type Int (Int,Char) a pair consisting of an Int and a Char (Int,Char,Bool) a triple
() an empty tuple
Int -> Int a function from Int to Int
The sole inhabitant of the type () is also denoted by () . Actually, there is a second member of () , namely undefined :: () . Now we can appreciate that there is a value ⊥ for every type.
As we have already said, when defining values or functions it is always a good idea to include the type signature as part of the definition.
Consider next the function take n that takes the first n elements of a list. This function made its appearance in the previous chapter. For example,
take 3 [1,2,3,4,5]
= [1,2,3]
take 3 "category"
= "cat"
take 3 [sin,cos]
= [sin,cos]
2.4 Types and type classes
What type should we assign to take ? It doesn’t matter what the type of the elements of the list is, so take is what is called a polymorphic function and we denote its type by take :: Int -> [a] -> [a]
The a is a type variable . Type variables begin with a lowercase letter. Type variables can be instantiated to any type.
Similarly,
(++) :: [a] -> [a] -> [a]
map :: (a -> b) -> [a] -> [b]
(.) :: (b -> c) -> (a -> b) -> (a -> c)
The last line declares the polymorphic type of functional composition.
Next, what is the type of (+) ? Here are some suggestions:
(+) :: Int -> Int -> Int
(+) :: Float -> Float -> Float
(+) :: a -> a -> a
The first two types seem too specific, while the last seems too general: we can’t add two functions or two characters or two booleans, at least not in any obvious way.
The answer is to introduce type classes
Arnold Nelson, Jouko Kokkonen