CS 331 Spring 2013 > Lecture Notes for Monday, February 25, 2013 |
A key idea: things that are done with flow-of-control in other languages may be done differently in Haskell.
We have already seen this. Recursion, particularly tail recursion, is our fundamental replacement for iteration; it will often be optimized to avoid excessive stack usage.
[Haskell]
myLen [] = 0 myLen (x:xs) = 1 + myLen xs
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 is 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"
Guards give us a way to do an if-then-else. We could make it a three-argument function.
[Haskell]
myIf cond tval fval | cond = tval | otherwise = fval
Note that, due to laziness,
only one of tval
and fval
will be evaluated.
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
This construction has been criticized as being un-Haskell-ish. Regardless, it is part of the language.
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.
assert
”
The C assert
macro is given a boolean expression.
It crashes the program if the expression is false.
A somewhat similar idea can be found in Haskell.
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
Similar to error
is undefined
.
This also crashes, but it takes no argument;
a standard error message is printed.
The Haskell 98 standard does not include exceptions as a separate flow-of-control mechanism. But they can be simulated by allowing for multiple kinds of return values.
An “Either
” type
has a value that is either
“Left
” followed by a value of one type,
or
“Right
” followed by a value of a different type.
An example:
[Haskell]
stringOrNum :: Num a => String -> a -> Either String a stringOrNum stringFlag value | stringFlag = Left (show value) | otherwise = Right value
Function show
converts a value to a String
.
Here is how it works:
[Interactive Haskell]
> stringOrNum True 3.2 Left "3.2" > stringOrNum False 3.2 Right 3.2
Now we can simulate exceptions by having a function
return its normal return value, as a Left
value,
or an exception, as a Right
value.
Other functions can then propagate exceptions.
Here is a simple example.
My Left
return values will be Double
values.
my Right
exceptions will be String
values intended for outputting to the user.
I will use standard arithmetic operator names,
prefixed with “@
”.
[Haskell]
infixl 6 @- (Right s) @- _ = Right s -- Propagate exception _ @- (Right s) = Right s -- Propagate exception (Left x) @- (Left y) = Left (x-y) -- No exception; compute value infixl 7 @/ (Right s) @/ _ = Right s -- Propagate exception _ @/ (Right s) = Right s -- Propagate exception (Left x) @/ (Left 0) = Right "Division by zero!" -- Raise exception (Left x) @/ (Left y) = Left (x/y) -- No exception; compute value -- Some numerical values, for convenience n0 = Left 0.0 n1 = Left 1.0 n2 = Left 2.0 n3 = Left 3.0
Here is how it works:
[Interactive Haskell]
> n3 @/ n2 Left 1.5 > n2 @- n2 Left 0.0 > n3 @/ (n2 @- n2) Right "Division by zero!" > n0 @- n3 @/ (n2 @- n2) @- n1 Right "Division by zero!"
The Haskell Standard Library does include some functionality
that can simplify the above structure a bit
(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.
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 fold.
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
And here is our definition.
[Haskell]
myFilter f [] = [] myFilter f (x:xs) | f x = x:therest | otherwise = therest where therest = myFilter f xs
A final example is “fold”. This encapsulated-loop idea does not correspond to a list comprehension. A fold—also called reduce—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.
do
A final flow-of-control structure,
which we will look at when we study Haskell I/O,
is the do
block.
Here is an example:
[Haskell]
reverseIt = do putStr "Type something: " line <- getLine putStr "You typed (backwards): " putStrLn (reverse line)
Here is how it works:
[Interactive Haskell]
> reverseIt Type something: Howdy! You typed (backwards): !ydwoH
A “do
” block
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
haskell_flow.hs
for Haskell source code related to today’s lecture.
ggchappell@alaska.edu