Module 09 Lab
-
We recommend that you use Haskell as installed on
stu. You may also consider using Repl.it (click “new repl” and choose Haskell) for today’s lab. You may also attempt to install Haskell on your own machine, but do not attempt to do that in class. -
Open a terminal window and go to a directory where you can make some files. Start up the interactive Haskell interpreter by typing
ghci(which stands for Glasgow Haskell Compiler Interpreter—the compiler is calledghcand they just added theifor the interpreter). -
Do some arithmetic in the normal way. You will have to use
`div`and`mod`(as infix operators, including the back-quotes) to do integer division. Also, you must put parentheses around negative numbers (like(-3)). Note that you can use the arrow keys to move around and see expressions that you typed before. The variableitalways holds the value of the last expression evaluated. Values can be arbitrarily large. For example, type2^200. -
Named functions in Haskell are all prefix operators and do not allow parentheses around arguments, nor commas separating arguments. Hence where in mathematics we might write
f(x,y,z), in Haskell it would bef x y z. For example,div 8 3andmod 8 3are the “usual” ways to call these functions. Haskell allows functions with two arguments to be called in an infix form if back-quotes are placed around the function name, as above. You can also put parentheses around an entire function application (e.g,(f x y z)) to make it more readable or to enforce precedence. -
Haskell has a large library of predefined functions in the
Preludemodule, which is loaded automatically whenever the Haskell compiler or interpreter runs. ThePreludecontains declarations for dozens of types and operations sufficient for many applications. Try out some functions likeeven,odd,sqrt,sin, andgcd(note that the latter requires two parameters!). -
The list is a very important type in all functional languages. In Haskell, a list can be formed by enclosing values of the same type in square brackets, separated by commas:
[2, 3, 5, 7, 11], for example. The infix operator++is the list concatenation operator. Try it. -
In Haskell, strings are lists of characters, so
"abc"is the same as['a', 'b', 'c']. Concatenate"hello"and"world"with a space between. -
There are other ways to make lists: one way is to use the cons operator
:(colon) which concatenates a value on the front of a list. For example5 : [6, 7]is[5, 6, 7]. In fact, you can create an entire list just using cons, values, and the empty list (actually, lists are internally built in exactly this way). Write the list[5, 6, 7]using only5,6,7, cons, and the empty list[](note that cons is right-associative). -
Lists can be indexed using the indexing operator
!!and compared using the standard comparison operators. ThePreludealso contains many built-in list operations includinghead,tail,length,reverse,sum, andtake(Note that the latter has two parameters: an integernand a listl. It returns a new list with the firstnelements froml). Try all of these out on some lists. -
Another way to make lists is to use a range:
[1..20]is the list with all the numbers between 1 and 20. Write an expression that produces a list with all the numbers between 25 and 35. -
Yet another way to make lists is to filter elements out of other lists using the built-in
filterfunction. This function is unlike the others we’ve seen so far because it takes a function as a parameter! In fact, it requires both a function and a list, and it returns a new list with only the elements from the original list that producetruewhen passed to the given function. This is similar to thefiltermethod of anArrayin Ruby, but instead of a block we are passing in an already-defined function. Try evaluating the following expression:filter odd [1..10] -
What happens when you switch the
oddfunction to theevenfunction? Write an expression that produces a list containing all of the even numbers less than 50. Being able to pass a function to another function (or receive a function as the return value!) is part of what makes Haskell a “functional” programming language. We will see more implications of this later. -
A list must contain values of the same type, but it can have an arbitrary number of them. A tuple must contain a fixed number of values, but their types need not be the same. Tuples are specified using parentheses with their components separated by commas. For example,
(1, True),("a", 34, []), and([0])are tuples. There are several built-in functions for manipulating tuples, such asfstandsndto get the first and second values of a 2-element tuple, respectively. -
All values in Haskell have types, and Haskell is VERY picky about types. But you don’t have to declare types if you don’t want to because Haskell has a powerful type inferencing mechanism built into it: whenever you create a value (including function definitions) Haskell will figure out what all the types of all the expressions have to be, and complain about type errors. Try adding a number and a string and see what
ghcisays. However, you should always declare the types of your functions anyway. This is because if what you think the type of your function should be does not correspond with what Haskell has computed it actually is, then you know you have a problem. So Haskell can help you debug your programs before you execute them (which is a big advantage of statically-typed languages). The bottom line is that you should work to become comfortable thinking about the types of all of the expressions in your programs. -
The
:typeor:tcommands inghcishow the type of an expression. For example,:type "abc"is[Char]because a string is a list of characters. Find the types ofTrue,[True], and(True,"abc"). Make sure you understand these types. -
Query the type of the empty list
[]. Notice that there is anabetween the brackets. This is a type variable, not unlike a generic type in Java. There are no rules imposed ona, which means that the empty list is flexibly typed. -
Query the type of the
notfunction. Notice that it looks like the mathematical notation for maps (functions), in this case a function that maps aBooltoBool. -
Find the type of the
takefunction. What’s up with that? Shouldn’t it beInt, [a] -> [a]? You can think of all the parameters as being separated by arrows as well as the return value, so this is a function that takes anIntand a list and returns a list. Tryfilterandmapand see what you get. What does this mean? -
Now find the types of the
evenand thedivfunctions. Things are a bit more complicated with numbers. There are several numeric types in Haskell, includingInt,Integer,Float, andDouble. But many values (like 5) can be used in contexts requiring any of these types. So Haskell has type classes, which are like interfaces in other languages, as they specify groups of operations. The most general numeric type class isNum. So if you find the type of 5, Haskell says it isNum a => a, which means, ifais a type that implements theNumtype class, then 5 can have that type. These type classes are used in function types as well. Find the types ofsqrt,mod, and(/)to see a few more type classes (you have to put operator symbols in parentheses to indicate that you are talking about the symbol, not using it). -
We can define our own functions, of course.
ghcidoes not like things to be more than one line long, and we will soon be ready to start making functions that are more than one line long. So open another window, go to the directory whereghciis open, and start editing a file calledlab9.hs. Put your name at the top in a comment. Comments begin with--and extend to the end of the line. -
We define functions similarly to how we define constants: write the function name followed by the parameter name(s), an equal sign, and the definition. For example, the following definition is for a function that computes the sum of the first
nnatural numbers using the equationn*(n+1)/2:sumOfFirstN :: Integral a => a -> a sumOfFirstN n = n * (n+1) `div` 2Type this into your
lab9.hsfile, then inghcitype:load lab9. This loads the file intoghci(you can reload by typing this line again, or entering just:reload). Now you can call this function on various values;sumOfFirstN 100should be5050. Notice that we included a type declaration for this function (it takes anIntegralvalue and returns one). These are optional in Haskell but it is considered good programming practice to include them. -
Haskell is a functional language. This means that Haskell does everything with functions. One important implication of this is that Haskell has no variables, at least not in the sense we are used to from imperative languages. It only has function parameters, such as the
nparameter insumofFirstN. -
Write a function
divBy15that takes a number and returns true if that number is divisible by 15. (The equality operator is==in Haskell.) Reload your file inghciand test your function. Write an expression that produces all the numbers between 1 and 100 that are divisible by 15 (Hint: usefilter!). -
As a functional language, Haskell also has no control structures like the conditional statements and loops we find in imperative languages. Try the following:
if (even 5) then "even" else "odd"This may look like a statement, but it is actually an expression, and it is similar to the ternary operator in languages like C. The expression after theifis evaluated, and if it is true, the value of the entire expression is the value of the expression afterthen, otherwise it is the value of the expression afterelse. These sorts of conditional and case expressions take the place of conditional and switch statements, and recursion stands in for loops in functional languages. -
Suppose we want to write a function to compute the points awarded for places in a swim meet as follows:
Rank Points 1st 10 2nd 8 3rd 6 4th 5 5th 4 6th 3 7th 2 8th 1 In a language like Java or C, we would encode this translation using a
switchstatement and acasefor each rank. In Haskell, we write the as a function using pattern matching for the values of the parameter. Pattern matching is a very powerful language feature, and very important for writing idiomatic Haskell code. Type the following into your lab file:points :: Integral a => a -> a points 1 = 10 points 2 = 8 points 3 = 6 points 4 = 5 points 5 = 4 points 6 = 3 points 7 = 2 points 8 = 1 points _ = 0When the
pointsfunction is called, Haskell will search through these alternatives from the first to the last and use the clause that matches. The underscore (_) is a pattern that matches any value, so this clause of the definition will be used if all others fail. Save your lab file, reload it inghciand try this out. -
We can simplify the definition of the
pointsfunction using “guards”, which allow us to use expressions to decide which definitional clause to use. This is a different form of pattern matching and is similar to theif/elseifcontrol structures from imperative languages. Edit your lab file so the definition ofpointslooks like the following:points n | (9 < n) || (n < 1) = 0 | (n <= 3) = 12 - 2*n | otherwise = 9 - nThe guards follow the bar character and must be boolean expressions. These are evaluated in order and the first one that is true determines which clause is used. The keyword
otherwiseis just short forTrue, which of course always succeeds, so theotherwiseclause should appear last as a catch-all guard. Save and reload your file and try this out. -
Write a function called
nextOddthat takes an integerxand returns the next odd number afterx. (Hint: Ifxis even, the next odd number isx+1.) -
The last thing we need to define functions is recursive definitions. Recursion is very important in a functional language like Haskell, and it is the primary mechanism for expressing repeated computation. Writing recursive solutions requires identifying at least one base case and at least one recursive case. The solution for the recursive case always includes one or more recursive calls to solve subproblems. Here is an example recursive definition for a factorial function:
factorial :: Integral a => a -> a factorial 0 = 1 factorial n = n * factorial (n-1)Notice how we use a literal (1) for the base case and a catch-all pattern (
n) for the recursive case. Tryfactorial 4andfactorial 200. -
In functional languages, lists are usually defined as recursive data structures using the cons operator. The base case is the empty list
[]and the recursive case is a pattern like[x:xs]wherexrepresents the first element in the list (the head) andxsis the rest of the list (the tail). Here’s an example implementation of asumListfunction that takes a list and returns the sum of all elements in the list:sumList :: Num p => [p] -> p sumList [] = 0 sumList (x:xs) = x + (sumList xs)Add this definition to your file and test it on a few lists. Note the recursive call to
sumListthat is part of its own definition. This is a common pattern in functional languages like Haskell.Use this kind of pattern to Write your own version of
head, calledmyHead, that returns the first value in a list (don’t worry about the empty list) using the pattern(x:xs). What is the type of themyHeadfunction (you can check it against the type of the built-inheadfunction)? -
Lets practice writing more functions that work with lists. Write
myLength, your own version of the listlengthfunction, whose type is[a] -> Int. It should calculate the length of whatever list you pass it. (Hint: What is the length of an empty list? What is the length of any list that you know has at least one element?) -
Write your own version of
lastcalledmyLast. It should return the last value in a list (again, don’t worry about the empty list). Its type should be the same as that ofmyHead. -
Write
fib nthat returns thenth Fibonacci number, wherefib 0is1andfib 1is1. -
Perhaps you wrote the
fibfunction in the obvious way, which is perfectly correct but incredibly inefficient. Tryfib 30and see what happens. This version is slow because it recomputes the same values over and over again. In an imperative language, we would use a loop and two counters to compute this function efficiently. How do we do an equivalent thing with recursion? We still use accumulators, but we must carry them along in the recursion, and the only way to do this (since we don’t have variables) is as parameters. Consider the following definition.ffib :: Integral a => a -> a ffib n = fibAccum 1 1 n fibAccum e0 e1 k | (k == 0) = e0 | (k == 1) = e1 | otherwise = fibAccum e1 (e0+e1) (k-1)Function
ffib(for “fast Fibonacci”) is defined in terms of a helper functionfibAccumthat has two accumulators and a counter. The accumulators are used just as in the imperative version to keep track of successive values in the sequence, and the counter is used to control the recursion. Put this definition in your lab file, save it and load it, and tryfib 30. Now tryffib 300; there should be no delay. But don’t tryfib 300or you will sit there waiting for years. -
(Optional challenge) Using accumulators and counters like this is an important technique in functional languages. Write
indexOfwith typeEq a => a -> [a] -> Intthat returns the first index of a value in a list, or-1if it is not in the list. For example,indexOf 'e' "fast times"is8. ( TheEq a =>in the type signature means thatais any type whose values can be compared for equality.) -
(Optional challenge) Write
myConcatthat concatenates two lists like++. Its type is[a] -> [a] -> [a]. This one is a little tricky! Try to think about “tearing apart” the first list one element at a time, gradually building up a new list that ultimately contains the second list as the tail after all of the elements from the first list. -
(Optional challenge) As a final challenge, write a function called
myFilterthat performs the same operation as the built-infilterfunction. This is a rather difficult thing to do the first time you are using a functional language. As a hint, you should use the following pattern for your recursive case:myFilter f (x:xs)and write two guards, one for the case where the functionfreturns true when applied to the first item in the list (x) and another for when it doesn’t. -
When you are done, exit
ghciby typing:qand return.
For a different take on this material, you can read either the Haskell Basics section and the very first sub-section of the Elementary Haskell section of the Haskell Wikibook or the first four chapters of Learn You a Haskell for Great Good!.
This lab was originally written by Dr. Chris Fox.