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.,

$$ \cseq(10) = [10,5,16,8,4,2,1] $$

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 NaturalNumbers. (We saw another way to define this function when implementing (==) for NaturalNumbers 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 NaturalNumbers. 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.