Lecture 7
A Bit of Tartness
So far, we've taken advantage of a lot of syntactic sugar in Haskell without remark. The sugar exists to make certain common cases easy, but it's useful to know the unsugared, "tarter" forms, which are often more broadly available.
Function Definition
We're accustomed to defining Haskell functions via a kind of simple pattern match, in which the left hand side of the binding is a pattern, and evaluation takes place by replacing instances of that pattern with the corresponding right hand side, e.g.,
square :: Num n => n -> n
square x = x * x
This seems simple enough, but there is sugar here. What we're actually doing is defining square
, which is an abstraction (i.e., a function). The unsugared form of this definition uses a lambda-expression:
square :: Num n => n -> n
square = \x -> x * x
A simple lambda-expression is an abstraction, which turns a base expression (here x * x
) into a unary function by abstracting out one of the variables (here x
) via the lambda (here \x ->
). Note here that the backslash replaces the greek letter λ (lambda), which itself was used as a convenient typographic alternative to a glyph that Alonzo Church invented for the purpose when he first laid down λ-calculus in the 1930s as a mathematical foundation for reasoning about higher-order functions.
One of the common uses of a lambda-expression is to create an anonymous function, e.g.,
> map (\x -> x * x) [1..4]
[1,4,9,16]
We don't have to do this as often in Haskell as we do in other functional languages, because Haskell provides such a tight language for point-free descriptions of functions (e.g., sections, partially-evaluated functions, compositions), but it's good to have this for quick hitters. In particular, expressions that contain multiple occurrences of the bound variable are often difficult to get at other ways, e.g.,
> map (\x -> x * 2^x) [1..4]
[2,8,24,64]
Note that the scope of the arrow ->
in the lambda notation
extends as far as possible, and associates to the right, which is convenient because it allows us to deal with nested lambdas without having to add lots of parentheses, e.g.,
hypotenuse :: Double -> Double -> Double
hypotenuse = \x -> \y -> sqrt(x^2 + y^2)
That said, we can introduce a small amount of sugar to this
hypotenuse = \x y -> sqrt $ x^2 + y^2
The mathematical foundations of functional languages (like Lisp) rest on the ideas of application and abstraction. Typed functional languages (like ML and Haskell) rest equally on the notion of type, as we've already seen. Application in Haskell is denoted through juxtaposition, which is about the lightest syntactic mechanism possible. Abstraction has a somewhat more cumbersome notation, i.e., the lambda-forms we've just looked at.
While we're here, let's note that the λ-calculus's η-reduction rule is essentially (\x -> M x) ==> M
whenever x
has no free occurrences in M
. The "cancellation" approach to η-reduction that we've used thus far is a special case of that, but it's worthwhile to understand the general form as it permits simplification of anonymous functions (λ-forms), often to the point of eliminating the explicit λ in favor of composition, a section, etc.
Guards
In programming, as in mathematics, it is often useful to define a function by cases, i.e., we'll have several different equations for a function, and associated with each equation is a guard, i.e., a predicate that describes the relevant range of the equation. E.g., in mathematics, we might write,
$$ \def\abs{\mathop{\rm abs}} \abs(x) = \begin{cases} x, & x \geq 0 \\ -x, & \hbox{otherwise} \end{cases} $$In Haskell, we'll write in much the same way
abs x
| x >= 0 = x
| otherwise = -x
Similarities notwithstanding, there are some important things to note here. In mathematics, the convention is that in a definition by cases, the distinct cases are both disjoint, i.e., there is no element of the domain that satisfies more than one of the selection predicates, and exhaustive, i.e., that every element of the domain satisfies at least one of the distinct cases. Haskell doesn't require either. Instead, the predicates are tested in the order they occur in the definition, and the first satisfying guard “wins.” If there is no winning predicate, then result is undefined, and an exception is raised. Note also that otherwise
is not a keyword, but is an ordinary variable which has been bound in the Prelude
to the boolean value True
.
This is perhaps clearest if we consider a de-sugared version, in which guards are replaced by an if ... then ... else ...
construct, e.g.,
abs x = if x >= 0 then
x
else
-x
Note here that the syntax of Haskell has recently changed. In Haskell '98, the then
and else
keywords had to be indented relative to the if
. This made long conditional chains awkward, as the code would slowly march off to the right. A syntactic change in Haskell 2010 allowed the then
and the else
to align with the if
. But I usually
prefer the form with the then
on the same line as the if
, as it takes up less room. Here's an alternative definition of abs
that illustrates a valid three-way set of alternatives:
abs x = if x == 0 then
0
else if x > 0 then
x
else
-x
or more concisely
abs x = if x == 0 then 0
else if x > 0 then x
else -x
Exercise 7.1 The Collatz function is defined as follows:
$$ \def\collatz{\mathop{\rm collatz}} \collatz(x) = \begin{cases} 3x + 1,&\hbox{if $x$ is odd}\\ x/2,&\hbox{if $x$ is even} \end{cases} $$For example,
$$ \begin{align} \collatz(10) &= 5 \\ \collatz(5) &= 16 \\ \collatz(16) &= 8 \\ \collatz(8) &= 4 \\ \collatz(4) &= 2 \\ \collatz(2) &= 1 \\ \collatz(1) &= 4 \end{align} \def\cseq{\mathop{\rm cseq}} $$We can define the collatz sequence cseq
beginning with a number \(x\) by taking successive iterations of the collatz
function, stopping once we reach 1, i.e.,
Write the Haskell function cseq
, but do so making full use of the tools that are available to you. You can almost complete this exercise by implementing collatz
, and then using the Prelude
functions iterate
and takeWhile
as follows:
cseq :: Integer -> [Integer]
cseq = takeWhile (/= 1) . iterate collatz
This fails because it doesn't produce the terminal 1, i.e.,
> cseq 10
[10,5,16,8,4,2]
There are several plausible solutions. One is to simply “patch” the result, i.e.,
cseq x = (takeWhile (/= 1) $ iterate collatz x) ++ [1]
but this is inelegant. We could simply redefine the notion of a collatz sequence so that our existing code is correct, i.e., the sequence ends with 2, but this seems dishonest. Or, we could write a new function takeUntil
that works slightly differently from takeWhile
, and use it. That's the Haskell way.
Pattern matching
Haskell also permits us to define a function via pattern matching. The idea here is that we'll provide one or more equations, where the left-hand side involves pattern expressions. Here's a very simple example:
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n-1)
In this case, there are two patterns: 0
and x
. In general, patterns are built out of constructors, constants, and variables. Any constructors that are used in defining a pattern must be fully applied.
Pattern matching definitions are particularly convenient and natural when dealing with algebraic types, as we saw in Lecture 2, e.g., in our definition of +
. Let's consider another simple example:
data Value = StringValue String
| IntegerValue Integer
isEvenIntegerValue :: Value -> Bool
isEvenIntegerValue (IntegerValue n) = even n
isEvenIntegerValue _ = False
Pattern matching is syntactic sugar for the case
statement, with which we might rewrite the examples above as
factorial n = case n of
0 -> 1
n -> n * factorial (n-1)
isEvenIntegerValue v = case v of
IntegerValue n -> even n
_ -> false
Note that it's possible to combine guards with case
, e.g., recall the filter
function:
filter :: (a -> Bool) -> [a] -> [a]
filter p [] = []
filter p (a:as)
| p a = a : filter p as
| otherwise = filter p as
We could have written this as
filter p lst = case lst of
[] -> []
(a:as)
| p a -> a : filter p as
| otherwise -> filter p as
Or even the completely tart
filter = \p -> \lst -> case lst of
[] -> []
(a:as) -> if p a then a : filter p as
else filter p as
It's nice to know about case
because as our code gets more complicated, we're going to want to use pattern matching inside of our function definitions, and not just at the top level.
There are a couple of other things worth knowing here. One is that a fairly common case of function definition involves matching a fixed pattern, e.g., if we want to flip a tuple around, we might have
transpose :: (a,b) -> (b,a)
transpose (a,b) = (b,a)
We'll often need throw-away functions like this to pass to higher-order functions like map
, but the obvious approach is a bit inconvenient:
> map (\p -> case p of (a,b) -> (b,a)) [(1,"foo"),(2,"bar")]
[("foo",1),("bar",2)]
So Haskell permits us to collapse a lambda and a one-element case into a single form:
> map (\(a,b) -> (b,a)) [(1,"foo"),(2,"bar")]
[("foo",1),("bar",2)]
Of course, even better technique would be to think, “Surely this is a predefined function somewhere,” in which case a quick search of the documentation reveals Data.Tuple.swap
.
Another feature of Haskell's pattern matching capacity is the ability to introduce pattern variables at non-leaf nodes in the pattern. Let me clarify by the following contrived function:
bumpToEven :: (Int,a) -> (Int,a)
bumpToEven (n,v)
| even n = (n,v)
| otherwise = (n+1,v)
What's not apparent here is that we're wasting a bit of space, in that if n
is even, we're creating a new copy of the tuple (n,v)
to represent the result, rather than just returning the argument, which would involve less space and overhead. To return the original, we could hang on to the existing copy by “delaying’ the pattern match, e.g.,
bumpToEven p = case p of
(n,v)
| even n = p
| otherwise = (n+1,v)
but using a single alternative case
construct for pattern matching seems awkward. An alternative is to add a new match variable:
bumpToEven p@(n,v)
| even n = p
| otherwise = (n+1,v)
Here, the p
will match the entire tuple, while n
and v
match its first and second components respectively.
*Exercise 7.2
The factorial
example above demonstrates how to desugar a
top-level function that pattern matches on one argument into a function
that uses a case expression.
We have also seen that equations can pattern match on multiple arguments.
For example, the function eqNat
below is one way to define
equality on NaturalNumber
s.
(We saw another way to define this function when implementing
(==)
for NaturalNumber
s in Lecture 2).
data NaturalNumber = Zero | S NaturalNumber
deriving (Show)
eqNat :: NaturalNumber -> NaturalNumber -> Bool
eqNat Zero Zero = True
eqNat (S n) (S m) = eqNat n m
eqNat _ _ = False
Complete the definition of the three functions below
(using case expressions) so that they also implement equality on
NaturalNumber
s. Use patterns that are similar to the
ones used in eqNat
above. Each function should make
recursive calls only to the function being defined.
eqNat1 :: NaturalNumber -> NaturalNumber -> Bool
eqNat1 = \x y -> case (x,y) of
...
eqNat2 :: NaturalNumber -> NaturalNumber -> Bool
eqNat2 = \x y -> case x of
...
eqNat3 :: NaturalNumber -> NaturalNumber -> Bool
eqNat3 = \x -> case x of
...
Consider the similarities and differences of each of these definitions
with respect to the eqNat
function.