Lecture 1

Administrivia

Please note all of the information found on the course Home Page, which is included here by reference.

Getting started

You should set up the Glasgow Haskell system. The easy way is to download your platform specific Haskell Platform installer from http://hackage.haskell.org/platform, and follow the instructions. This year, the first lab will include an install party—if you have a laptop, bring it to lab.

One minor inconvenience for us is that the Haskell Platform gets updated twice a year, and this year's second update falls mid-quarter. It probably won't be essential for this course to update, but a general rule of computing is that you either keep up or fall behind. I recommend not falling behind.

Haskell

Haskell is a lazy pure functional language. We'll get around to defining these terms in due course, but suffice it to say that this is not your father's FORTRAN, nor even the Java or C++ you might have learned in High School. But let's start by understanding the principal mode of interaction during program development: you fire up the ghci interpreter, and present it with a sequence of expressions to be evaluated.

Let's look at a few examples....

First, some simple arithmetic:

$ ghci # we're invoking ghci from the shell's command line > 3+4 7 > 3*2+1 7 > 3+2*1 5 > (1+2) * 3 9 > 3 ^ 2 * 5 45 >

This is all pretty standard. Arithmetic expressions follow the standard algebraic notion and precedence rules, with * used for multiplication, and ^ for exponentiating by an integral power. There is also a ** form of exponentiation that works with floating-point powers, e.g.,

> 2 ** 0.5 1.4142135623730951

There are some major details being swept under the rug here. The most important is that Haskell has strict type system, and the distinction between various integer and floating point types is important, and will require greater care later on. A second detail is the format of the number

0.5

Haskell has a very powerful and flexible parser, but it comes with a couple of quirks that affect simple arithmetic input. The leading zero in 0.5 is not optional; .5 is not valid Haskell syntax. It is usually necessary to put negative literals in parentheses, e.g.,

> (-1) * (-2) 2

The evaluation process in Haskell is driven by substitution. I will use ==> to indicate the simplification of an expression by a substitution step:

3 ** 2 + (4 * 5) ==> 9 + (4 * 5) ==> 9 + 20 ==> 29

Easy enough! But is this useful? Surely programming languages are more than command line calculators!

We can introduce names for particular values:

> let rate = 10 > let time = 20 > let distance = rate * time > distance 200

Here we've used let <var> = <expr> in the interpreter to define new values. This is good enough if we want to compute the distance travelled for a single rate and time, but what if we want to do a lot of distance calculations? In this case, we'd do better to modify our definition of distance by abstracting on the variables rate and time, and so defining a function, which we then evaluate at different arguments:

> let distance rate time = rate * time > distance 10 20 200 > distance 20 30 600

Let's take this apart. The let distance part tells the interpreter that we're going to be defining (or binding) the name distance. The rate time which follow indicate that distance is a function that takes two arguments, which we're going to give the names rate and time respectively, the = separates the binding's template from the body of the function we're defining, and then finally rate * time specifies that body.

As before, we can think of evaluation as a sequence of substitutions, e.g.,

> distance 10 20 ==> 10 * 20 ==> 200

In the first step, we substitute the first argument 10 in for rate, and the second argument 20 in for time in the body of the distance function, and then continue as before.

If you have previous programming experience, there are some surprises lurking here. You might be surprised a bit at the syntax, expecting distance(rate,time) whether out of analogy to standard mathematical notion or traditional programming languages. Oddly enough, you can do this in Haskell too, but it defines a slightly different function:

> let distance2(rate,time) = rate * time > distance2(10,20) 200

Admittedly, the difference may not be immediately obvious! But at a practical level, if we try to mix-and-match definitions and calls we're in for a surprise, in that both

> distance(10,20)

and

> distance2 10 20

will result in impressively long, and for the novice somewhat scary, error messages.

Understanding the difference between distance and distance2 gets at a fundamental feature of Haskell. The function distance appears to take two arguments. This isn't actually the case. In Haskell, all functions are curried, which is to say, they only ever take one argument. In this case, distance takes the argument rate. The result of evaluating this is another unary function that takes the time argument. Here, the syntax of application includes the convention that application associates to the left, thus distance 10 20 can be fully parenthesized as (distance 10) 20. The term "currying" honors the inventor of the lambda calculus, a mathematical theory of functions, Haskell Curry. And yes, Haskell does too.

So now, consider distance2. In this function, the two arguments rate and time get packaged into a tuple, which you can think of as a box that contains slots for two different values. The tuple, even though it contains two distinct values, is only a single value, hence, distance2 is also unary, as all Haskell functions must be. Unsurprisingly, there exist Haskell functions curry and uncurry which can swizzle back and forth between curried and (binary) uncurried functions. Thus:

> uncurry distance (10,20) 200 > curry distance2 10 20 200

Again, this may seem a bit surprising, as curry and uncurry are functions that take functions as arguments, and return functions as results. This gets us back to our description of Haskell as being (among other things) a functional language. This term might lead you to think that there's something fundamentally special about functions in functional languages. You'd be wrong, what's “special” about functions in functional languages is that they're not special! Functions are just values, and like integers and floating point numbers and other kinds of values we'll encounter later, can be defined, used, passed as arguments, and created as the result of evaluating functions, just like other values.

Haskell's preference for curried over uncurried functions may seem odd at first, but there are common Haskell programming idioms based on partial evaluation, i.e., (curried) multi-ary functions in which one or more arguments have been specialized, i.e., given a constant value, e.g.,

> let distanceAtRate10 = distance 10 > let distanceAtRate20 = distance 20 > distanceAtRate10 20 200 > distanceAtRate20 20 400

This raises the question of, “What to do if we want to specialize the second argument of a binary function?” For now, our best solution is to define another function directly, rather than via partial evaluation, e.g.,

> let distanceForTime10 rate = distance rate 10 > distanceForTime10 10 100

We'll develop a better solution in one of the exercises.

One issue that we haven't talked about yet is the precedences of function application vs. ordinary binary operations. Fortunately, there's an easy story to tell: application binds more tightly than infix operations. Thus, e.g., if we wanted to know how far we've gone after going 13 MPH for 3 hours, followed by going 10 MPH for 1 hour (a fair approximation of me biking, Tour de France material I'm not), you could do

> distance 13 3 + distance 10 1 49

On the other hand, if you wanted to compute the effect of going 12 MPH for 2 hours before lunch, and 2 hours afterwards, you might naturally express this as

> distance 12 (2+2) 48

Note here that not using parentheses will give the wrong answer:

> distance 12 2+2 26

Haskell doesn't try to read your mind, and it doesn't care about the number of spaces used to separate tokens in an expression. It cares about precedence, and again, application binds more tightly than infix operations.

Having gotten this far, let me note that there are serious limitations associated with working as we have been by entering the definitions of functions one at a time via ghci's read-eval-print loop, so this is not the way we'll usually work. Instead, we'll put our definitions in Haskell source .hs files, and load them with ghci, or later compile them with ghc.

A Haskell source file is a plain text file that (mostly) contains definitions. For example, consider the following file, Hypotenuse.hs:

-- Hypotenuse.hs -- Define a function for computing the length of the hypotenuse -- of a right triangle, using the pythagorian theorem. square x = x * x hyp x y = sqrt ( square x + square y )

A Haskell source file is a different kind of linguistic environment than the ghci prompt. This is most immediately relevant in terms of the let keyword, which has a somewhat different syntax and somewhat different role. What this means is that you shouldn't use let in Haskell source files until you understand its proper syntax and role within that context. One major issue with let is that each use of the let keyword introduces a new lexical layer, making it difficult to define mutually recursive functions, and forcing orderings among the functions we define. This is not the case with Haskell source files. Thus, it is not necessary to define square before using it in hyp, i.e., the order of definitions could be interchanged without changing the meaning of the source.

Comments are introduced by a double hyphen --, and run to the end of the line. We can also use {- and -} for nested, delimited comments.

Function and variable names must start with a lower-case letter, and can consist only of alphabetic characters, digits, and the apostrophy ('). [Haskell was developed by mathematically inclined computer scientists, and mathematicians like to use primed variables.] The variable _ is considered to be a lowercase letter for the purpose these rules, but _ by itself has a special role, i.e., that of an anonymous variable. Thus,

one _ = 1

defines a function that takes one argument (which it ignores!) and always returns 1. This can result in non-intuitive behavior, and we'll learn more about this later.

Also, whitespace matters when used for indentation. We'll talk about rules later, but for now: (1) mimic my use of whitespace, and (2) don't use tabs!! [N.B., many text editors provide an option that interprets tabs on input as the relevant number of spaces—such options can be very useful.]

We can use a Haskell source file in a number of different ways...

$ ghci Hypotenuse

or

$ ghci Hypotenuse.hs

or

$ ghci Prelude> :l Hypotenuse [1 of 1] Compiling Main ( Hypotenuse.hs, interpreted ) *Main>

The significance of the name(s) before the > prompt is something that will become clear eventually.

Note: it is a bad idea to use non-alphanumeric characters in the name of a Haskell source file. It is a good idea to start the name of a Haskell script file with a capital letter.

A common way of working is to have your Haskell source file open one window in a text editor, and in another window with a terminal running ghci. As you make changes (e.g., adding new functions, fixing the definitions of old functions, etc.), you'll save the file, and then reload the file in ghci:

> :r [1 of 1] Compiling Main ( Hypotenuse.hs, interpreted ) >

Exercise 1.1 Define the functions

  1. rate in terms of distance and time, and
  2. time in terms of distance and rate.

*Exercise 1.2 A common use of tuples is to package up the coordinates of a vector. Consider, for example, the problem of determining the position of a ball thrown from $p_0 = (x_0,y_0)$ at a velocity of $v = (v_x,v_y)$ in the presence of uniform gravitational acceleration $a$ in the absence of wind resistance. This has the solution $p_t = (x_t,y_t) = (x_0 + v_x t,y_0 + v_y t - {1\over2} a t^2)$. Define a function parabola that takes position, velocity, and time arguments, and return the position of the particle. You may assume KMS units, so that $a = 9.80665$. Verify

> parabola (0,100) (100,15) 6 (600.0,13.4803)

Note here that you don't actually have to understand the physics to get this problem right!

*Exercise 1.3 The law of cosines is a generalization of the Pythagorean Theorem, which allows us to compute the length c of the third side of a triangle, given the lengths of the two other sides a and b, and the included angle γ.

Write a Haskell script file (Cosines.hs) that includes a function law_of_cosines which takes three arguments: a, b, and gamma, and returns the length of c.

Some notes: your function should take the angle gamma in degrees, but you need to be aware that the Haskell's built in cos function expects its argument to be in radians, i.e.,

> cos pi -1

Note that pi is a predefined constant in Haskell for the mathematical constant π.

Note that floating point arithmetic in Haskell (like almost all programming languages) is finite precision, and only approximately corresponds to the reals of mathematics. My implementation returned the following results, which you might want to use as test data:

> law_of_cosines 1 1 60 0.9999999999999999 > law_of_cosines 1 1 120 1.7320508075688772 > law_of_cosines 3 4 90 5.0 > law_of_cosines 3 4 0 1.0 > law_of_cosines 3 4 180 7.0

Exercise 1.4 Implement your own versions of curry and uncurry. Bind the names curri and uncurri respectively, and test them as above:

> uncurri distance (10,20) 200 > curri distance2 10 20 200

Exercise 1.5 Write a specializeSecond function that makes it possible to fix the second argument of a binary function, e.g., we should be able to write:

> let distanceForTime10 = specializeSecond distance 10 > distanceForTime10 20 200

Note that the specializeSecond function is defined in the Prelude, a collection of standard functions that are always available, albeit as flip, because it “flips” the order in which a curried binary function takes its arguments.

Exercise 1.6 Programming systems (even Haskell) perform according to their definitions, which don't always conform to our naive expectations. Consider the following sequence:

> let f x = 0 > let g x = f x > let f x = 1 > g 0 0

Explain why the result of this evaluation is 0, and not 1.