CS 331 Spring 2016 > Notes for February 29, 2016
CS 331 Spring 2016
Notes
for February 29, 2016
Haskell: Flow of Control
Key Idea: things that are done with traditional flow-of-control constructs in imperative programming languages may be done differently in Haskell.
Pattern Matching & Recursion
As we have already seen, Haskell has a useful pattern matching facility, which allows us to choose one of a number of function definitions. The rule is that the first definition with a matching pattern is the one used.
[Haskell]
isEmpty [] = True isEmpty (x:xs) = False
In many of the places we would tend to use an if...else construction in an imperative programming language, we might use pattern matching in Haskell.
In addition, Haskell programs make heavy use of recursion. Recursion can be less costly in Haskell than it is in programming languages like C++, because of Haskell’s tail-call optimization (TCO). TCO means that a tail call does not require additional stack space.
[Haskell]
mySize [] = 0 mySize (x:xs) = 1 + mySize xs
In many of the places we would tend to use a loop in an imperative programming language, we might use recursion in Haskell.
Pattern matching and recursion form a powerful combination. Really, we can do just about anything we need to, using only these two. For example, here is how we can write our own if...else.
[Haskell]
-- myIf condition tVal fVal -- condition is a Bool. Returns tVal if conditions is True, -- fVal otherwise. myIf True tVal fVal = tVal myIf False tVal fVal = fVal
Note that, due to laziness,
only one of tVal
and fVal
will ever be evaluated.
So myIf
is actually efficient.
In the first definition above,
we do not use fVal
.
In the second definition,
we do not use tVal
.
The Haskell pattern “_
”
matches anything, just like a variable name,
but it results in no binding.
It thus marks an unused parameter.
Here is a rewrite of myIf
using this pattern.
[Haskell]
myIf True tVal _ = tVal myIf False _ fVal = fVal
Here are some uses of myIf
[Haskell]
-- fibo -- As usual. fibo n = myIf (n <= 1) n (fibo (n-2) + fibo (n-1)) -- evenOddString -- Given an integer, returns "even" if it is even, and "odd" -- if it is odd. evenOddString n = myIf (n % 2 == 0) "even" "odd" -- myAbs -- Given a number, returns its absolute value. myAbs n = myIf (n >= 0) n (-n)
Selection
myIf
, as defined above,
is a bit unwieldy.
But we do not need it,
since
Haskell has other built-in selection constructs.
The most important is the guards construction.
We will also look briefly at
the if-then-else and case constructions
Guards
Guards are the Haskell form of mathematical notation like the following.
\[ \mathrm{myAbs}(x) = \begin{cases} x, & \text{if } x \ge 0.\\ -x, & \text{otherwise}. \end{cases} \]
In Haskell:
[Haskell]
myAbs x | x >= 0 = x | otherwise = -x
Above, each vertical bar is followed by a boolean expression.
The value used is that corresponding to the first such
expression that evaluates to True
.
We typically want the last value to handle all remaining cases,
as above,
and so we want a boolean expression that is always true.
“True
” would work fine;
“otherwise
” is simply another
way to say the same thing.
[Interactive Haskell]
> otherwise True
Another example:
[Haskell]
stringSign x | x > 0 = "positive" | x < 0 = "negative" | otherwise = "zero"
With guards, we can rewrite function myIf
to be a little nicer looking.
[Haskell]
myIf cond tval fval | cond = tval | otherwise = fval
If-Then-Else
Haskell actually does have an if-then-else construction.
We could replace “myIf cond tval fval
”
with the following.
[Haskell]
if cond then tval else fval
Haskell’s if-then-else construction has been criticized as being un-Haskell-ish. I have found it natural to use when doing I/O (discussed later); otherwise, I generally prefer to use guards.
Case
Haskell’s case construction
is analogous to switch
in C++.
Here is an example.
[Haskell]
fibo n = case n of 0 -> 0 1 -> 1 _ -> fibo (n-2) + fibo (n-1)
The above is exactly equivalent to
[Haskell]
fibo 0 = 0 fibo 1 = 1 fibo n = fibo (n-2) + fibo (n-1)
Since a case
construction
can always be replaced by
multiple function definitions,
and the latter is generally clearer,
I almost never use case
.
Regardless, case
does have an
important role in Haskell:
it is the actual machinery behind pattern-matching.
Multiple function definitions are actually
syntactic sugar over a case
construction.
Fatal Errors
Note: The Haskell Standard Library and documentation tosses around the terms “error” and “exception” rather loosely. They may not be used in the precise senses that you are familiar with, and they may be used inconsistently.
Function error
is given a string argument.
Whenever this function is executed,
it crashes, with the given string as an error message.
Function error
has a return value of
an arbitrary type,
so that it may be used anywhere Haskell requires a value.
[Interactive Haskell]
> :t error error :: [Char] -> a
Use error
only for fatal errors:
places where a program should exit.
Usually we do then when the program detects a bug in its own code.
[Haskell]
fibo n | n < 0 = error "fibo: negative parameter" | n <= 1 = n | otherwise = fibo (n-2) + fibo (n-1)
Similar to error
is undefined
.
This also crashes, but it takes no argument.
A standard error message is printed.
Simulating Exceptions
Consider what an exception is in C++. Roughly speaking, it is an alternate return value for a function. The caller may not be able to handle it, in which case it simply passes through the caller—thus being returned by the caller, to its caller—until it reaches code that can deal with it.
So we might be able to simulate an exception by having a special value that a function returns to indicate a problem. If a caller sees this value, it can either handle it, or return the same value to its caller.
Suppose we want to write a square root function that uses this ide. Here is a first try.
[Haskell]
mySqrt1 x | x >= 0.0 = sqrt x | otherwise = special_value -- Not sure what value is
Suppose function foo
calls the square root function,
but it cannot handle errors.
If it sees special_value
then it returns that same value to its caller.
[Haskell]
foo x y = foo_helper (mySqrt1 x) y where foo_helper special_value _ = special_value foo_helper sqx y = ... -- Does whatever it needs to
But there is a problem here.
What should special_value
be?
We can solve this problem by creating a new type
with an added special value in it.
Haskell enables this through Maybe
types.
A “Maybe
” type
has a value that is either
“Just
” followed by a value of some specified type,
or
“Nothing
”.
We can use Nothing
to indicate an error.
[Haskell]
mySqrt2 :: Double -> (Maybe Double) mySqrt2 x | x >= 0.0 = Just (sqrt x) | otherwise = Nothing
The above function is a little odd, since it does not return the same type as it is given. After all, we might want to take the square root of a square root. But we can fix this.
[Haskell]
mySqrt :: (Maybe Double) -> (Maybe Double) mySqrt Nothing = Nothing mySqrt (Just x) | x >= 0.0 = Just (sqrt x) | otherwise = Nothing
The above function works as before, but if it is given an error value, then it just passes it on through.
Here is how we could use the above function:
[Interactive Haskell]
> mySqrt (Just 3.0) Just 1.7320508075688772 > mySqrt (Just (-3.0)) Nothing > mySqrt (mySqrt (Just (-3.0))) Nothing
We can write a whole numerical computation package this way. Here is division.
[Haskell]
infixl 7 @/ -- Set left associativity, precedence for @/ (@/) :: (Maybe Double) -> (Maybe Double) -> (Maybe Double) Nothing @/ _ = Nothing _ @/ Nothing = Nothing (Just _) @/ (Just 0.0) = Nothing (Just x) @/ (Just y) = Just (x / y) -- Some numerical values, for convenience n0 = Just 0.0 n1 = Just 1.0 n2 = Just 2.0 n3 = Just 3.0
Here is how we could use the above:
[Interactive Haskell]
> n3 @/ n2 Left 1.5 > n2 @/ n0 Nothing > (n2 @/ n0) @/ n3 Nothing
The Haskell Standard Library does include some functionality
that can make the above structure a bit prettier.
Look into the “Error monad”.
Also, using type classes we could overload the
regular arithmetic operators,
thus avoiding the “@
” prefix.
But the above does show the essential idea.
Encapsulated Loops
A very important flow-of-control idea in functional programming is that many loops can be encapsulated as functions. We will look at three such ways to encapsulate loops: map, filter, and the various fold operations.
For example, suppose we have the following C++ function.
[C++]
int square(int n) { return n*n; }
Now we want to do the following, given a
vector<int> v
.
[C++]
vector<int> w; for (int i = 0; i != v.size(); ++i) w.push_back(square(v[i]));
So we are applying function square
to each item
of the first vector
.
In Haskell, we can define square
as follows.
[Haskell]
square n = n*n;
Now, given a list v
,
we can apply function square
to each item.
[Haskell]
w = [ square k | k <- v ]
Here is another way to say the same thing.
[Haskell]
w = map square v
Function map
takes a function and a list.
It applies the function to every item of the list,
and returns the results as a new list.
We can define map
as follows
(I say “myMap
” below
to avoid interfering with a Standard Library function.)
[Haskell]
myMap f [] = [] myMap f (x:xs) = f x : myMap f xs
We see that function map
encapsulates
a certain kind of loop—using a construct
that is not flow-of-control at all,
but rather a function.
Similarly, we can grab only certain items from a list.
[Haskell]
w = [ k | k <- v, mod k 2 == 0 ]
This idea is encapsulated in function filter
.
[Haskell]
isEven k = (mod k 2 == 0) w = filter isEven v
Or, using a lambda function:
[Haskell]
w = filter (\ k -> mod k 2 == 0) v
And here is our definition.
[Haskell]
myFilter f [] = [] myFilter f (x:xs) | f x = x:rest | otherwise = rest where rest = myFilter f xs
A final example is fold. This encapsulated-loop idea does not correspond to a list comprehension. A fold—called reduce in some contexts—is when a single value is computed based on a sequence (for example, the sum of the sequence, or the maximum value).
Haskell function foldl
takes a
two-argument function, a starting value,
and a list.
For example, the following two expressions are essentially the same.
[Interactive Haskell]
> foldl (+) 0 [2,5,4,8] 19 > (((0+2)+5)+4)+8 -- Same as above 19
The “0
”
in the second expression above corresponds to the
second parameter of foldl
.
Function foldr
is similar,
except that it groups things differently.
[Interactive Haskell]
> foldr (+) 0 [2,5,4,8] 19 > 2+(5+(4+(8+0))) -- Same as above 19
Functions foldl1
and foldr1
are similar, except that they do not require a starting
value.
Instead, they use the first or last item of the list
as the starting value.
Thus, they cannot be called on empty lists.
[Interactive Haskell]
> foldl1 (+) [2,5,4,8] 19 > ((2+5)+4)+8 -- Same as above 19 > foldr1 (+) [2,5,4,8] 19 > 2+(5+(4+8)) -- Same as above 19
As another example, here is how to compute a maximum using a fold.
[Haskell]
maxVal xs = foldl1 bigger xs where bigger x y | x > y = x | otherwise = y
There are various other kinds of encapsulated loops
(e.g., look into zip
),
but a large number of loops can be done elegantly
as maps, filters, or folds.
Preview: do
A final flow-of-control structure,
which we will look at when we study Haskell I/O,
is the do
construction.
Here is an example:
[Haskell]
reverseIt = do putStr "Type something: " line <- getLine putStrLn "" putStr "Your line, reversed: " putStrLn (reverse line)
Here is how it works
(user input is in boldface
):
[Interactive Haskell]
> reverseIt Type something: Howdy! Your line, reversed: !ydwoH
A do
construction
is syntactic sugar around a pipeline of functions.
Essentially,
each function takes the current state as an argument
and returns the new state,
possibly modified by an I/O action.
For example, each of the indented lines above
represents an I/O action.
We will discuss this idea further next time.
See
flow.hs
for Haskell source code related to today’s lecture.