CS 360: Programming Languages Lecture 10: Introduction to Haskell Geoffrey Mainland Drexel University Thursday, February 5, 2015 Adapted from Brent Yorgey s course Introduction to Haskell.
Section 1 Administrivia
Administrivia Homework 4 due Monday night. Midterm in class Tuesday. Homework 5 (Haskell) will be posted before the weekend. It will be due February 16. The course home page has instructions for getting GHC, the Haskell implementation we will be using, up and running on tux. I highly recommend reading Learn you a Haskell for Great Good! We will be covering many concepts very quickly in the next two lectures, and it will be extremely difficult for you to understand all the material if you don t complete the accompanying readings.
Section 2 Homework 3 Solutions
Section 3 Introduction to Haskell
Haskell Haskell is: Functional: function are first-class, and the meaning of a Haskell program is determined by evaluating functions rather than executing instructions. Pure: calling a function with the same arguments results in the same result every time. Lazy: expressions are not evaluated until their values are actually needed. Statically typed: every Haskell expression has a type, and types are checked at compile time.
Types Types are invariants: they state something that is true for all executions of a program. Types are statically checked. If a function uses, for example, a string as an integer, the error will be reported at compile time, not at run time. Up side: compiler catches errors for you. Down side: can t sneak errors past the compiler, error messages can be hard to decipher. Types serve as a form of documentation. Given just a type, you know a lot about how a function can be used and what it might do. Types help clarify your thinking about a program and its structure.
Abstraction in Haskell Each significant piece of functionality in a program should be implemented in just one place in the source code. Where similar functions are carried out by distinct pieces of code, it is generally beneficial to combine them into one by abstracting out the varying parts. Benjamin Pierce [Pie02] Functional languages, and in particular Haskell, are very good at abstraction. We have already seen higher-order functions, like map, filter, and fold, that capture reusable patterns we have repeatedly programmed by hand many times (what pattern is this?). Haskell s type classes add another powerful form of abstraction.
Wholemeal Programming Functional languages excel at wholemeal programming, a term coined by Geraint Jones. Wholemeal programming means to think big: work with an entire list, rather than a sequence of elements; develop a solution space, rather than an individual solution; imagine a graph, rather than a single path. The wholemeal approach often offers new insights or provides new perspectives on a given problem. It is nicely complemented by the idea of projective programming: first solve a more general problem, then extract the interesting bits and pieces by transforming the general program into more specialised ones. Ralph Hinze [Hin09] Consider The following C function: int sum_of_squares(int* arr, int len) { int acc = 0; for (int i = 0; i < len; i++ ) acc = acc + arr[i] * arr[i]; return acc; } This function has to worry about the low-level details of maintaining an index into the array. It also mixes up the details of what is really two separate operations: squaring numbers and summing them. We can write this function in Haskell as: sumofsquares xs = sum (map (^ 2) xs)
Learning Haskell Haskell is a challenging language to learn because it requires you to think differently about programming. Fortunately you now know Scheme, which gives you a head start Please do the readings from Learn You a Haskell. They will help!
Declarations and variables x :: Int x = 1 This fragment defines a variable x with the value 1. It also provides a type signature for x. The symbol :: is pronounced has type. The value of x cannot be changed! There is no set! in Haskell.
Using ghci The interpreter we will be using is ghci. It is actually part of the GHC compiler. Please check the course home page for a short guide to using GHC and ghci. Unfortunately the version installed on tux is broken the guide provides instructions for using our (not broken) version of GHC. Like mit-scheme, ghci provides a read-eval-print loop (REPL). Unlike mit-scheme, ghci lets you edit what you enter... Import ghci commands include :load, :reload, and :type.
Other types n :: Integer n = 93326215443944152681699238856266700490715968264381621468592963895217 pi :: Double pi = 3.141592653589793 singleprecisionpi :: Float singleprecisionpi = 3.141592653589793 b :: Bool b = True c1 :: Char c1 = x s1, :: String s1 = "Hello, world!" Haskell includes types for arbitrary-precision integers, floating-point numbers, Booleans, Unicode characters, and Unicode strings.
Boolean logic Boolean values can be combined with && (logical and), (logical or), and not. ex1 = True && False ex2 = not (False True) Values can be compared for equality with == and /=, or compared for order using <, >, <=, and >=. ex3 = ( a == a ) ex4 = (16 /= 3) ex5 = (5 > 3) && ( p <= q ) ex6 = "Haskell" > "Scheme" Haskell also has if expressions, but they are not used often guards and patterns, which we will see shortly, are preferred. ex7 = if "Haskell" > "Scheme" then "Haskell is better!" else "Scheme is better!"
Arithmetic ex7 = 3 + 2 ex8 = 19-27 ex9 = 2.35 * 8.6 ex10 = 8.7 / 3.1 ex11 = mod 19 3 ex12 = 19 mod 3 ex13 = 7 ^ 222 ex15 = (-3) * (-7) Backticks make a function name into an infix operator. Negative numbers must often be surrounded by parentheses to avoid having the negation sign parsed as subtraction. (Yes, this is ugly).
Division i :: Int i = 10 Division is slightly tricky. Trying to evaluate i / 2 will give us an error. Instead, we must write i div 2. div is for integral types, and / is for floating-point types.
Defining functions Recall the fact function we defined in Scheme. (define (fact n) (cond ((= n 0) 1) (else (* n (fact (- n 1)))))) We can define the same function as follows in Haskell. fact :: Integer -> Integer fact n = if n == 0 then 1 else n * fact (n-1) Remember when we said if is not often used? Here s the same function without if. fact :: Integer -> Integer fact 0 = 1 fact n = n * fact (n-1)
Functions in Haskell fact :: Integer -> Integer fact 0 = 1 fact n = n * fact (n-1) Note that we have given our function a type signature. This type says fact is a function from Integers to Integers. Each clause of fact is checked from top to bottom, and the first matching clause is chosen. This is an example of Haskell s pattern matching capabilities. We will see much more of this later.
Guards Choices can also be made based on arbitrary Boolean expressions using guards. fact :: Integer -> Integer fact n n == 0 = 1 otherwise = n * fact (n-1) Guards are always Boolean expressions. Question: What do you think otherwise evaluates to? Any number of guards can be associate with each clause. A clause s guards are checked from top to bottom, and the first one that evaluates to True is chosen. If no guard evaluates to True, matching continues with the next clause.
Function with multiple arguments Functions may take multiple arguments, like this add3 :: Int -> Int -> Int add3 x y z = x + y + z ex16 = add3 1 The syntax for the type of a function with multiple arguments is t1 -> t2 ->... -> tn -> result. We can partially apply the function add3 to a value, as we did when we defined ex16. Question: What do you think the type of ex16 is? Another question: How could you figure it out if you don t know the answer? Note than function application has higher precedence than any infix operator. Thus add 1 n+1 3 parses as (add3 1 n) + (1 3). Instead, write add3 1 (n+1) 3.
Lists Like Scheme, lists are one of the most basic types Haskell provides. nums, range, range2 :: [Integer] nums = [1,2,3,19] range = [1..100] range2 = [2,4..100] Like Python, Haskell has list comprehensions. Strings are lists of characters String is just an abbreviation for [Char]. hello1 :: [Char] hello1 = [ h, e, l, l, o ] hello2 :: String hello2 = "hello" hellosame = hello1 == hello2 -- Evaluates to True!
Constructing Lists The simplest list is the empty list: nil = [] Haskell has cons, but it is an infix operator, and it s name is :. We can build lists using : or by using special list notation. ex17 = 1 : [] ex18 = 3 : (1 : []) ex19 = 2 : 3 : 4 : [] ex20 = [2,3,4] == 2 : 3 : 4 : [] -- Also evaluates to True Let s write some functions on lists...
Haskell vs. Scheme: append In Scheme... (define (append xs ys) (if (null? xs) ys (cons (car xs) (append (cdr xs) ys)))) And in Haskell... append xs ys = if null xs then ys else head xs : append (tail xs) ys And in Haskell again... append [] ys = ys append (x:xs) ys = x : append xs ys What is the type of append? Let s give it a type signature that says
Making a mistake in append Let s try this variation on append: append :: [Int] -> [Int] -> [Int] append [] ys = ys append (x:xs) ys = xs : append xs ys Is this variation correct? What happens if we make the analogous mistake in the Scheme version of append? What do you think happens if we give ghci this definition?
GHC Errors... lecture10-examples.hs:6:20: Couldn t match expected type Int with actual type [Int] In the first argument of (:), namely xs In the expression: xs : append xs ys Failed, modules loaded: none. Prelude> Don t be scared of errors. First we are told Couldn t match expected type Int with actual type Int. This means that something was expected to have a Int, but actually had type [Int]. What something? The next line tells us: it s the first argument of (:) which is at fault, namely, xs. The next lines go on to give us a bit more context. You might not understand all of an error message, but you will learn a lot by reading it!
Algebraic Data Types Let s see how the Bool type is defined in the standard library: data Bool = True False data mean we are introducing a new data type. The part before the = is the name of the type we are defining. The parts after the = are the data constructors. They specify the different values this type can have, and they are separated by a vertical bar,.
Defining a data type for shapes We can define a new data type that represents shapes as follows. data Shape = Circle Double Double Double Rectangle Double Double Double Double The Circle data constructor has three fields: the coordinates of its center, and its radius. The Rectangle data constructor has four fields: the coordinates of its upper-left and lower-right corner. We can also define a function that calculate the area of a shape. data type that represents shapes as follows. area :: Shape -> Double area (Circle r) = pi * r ^ 2 area (Rectangle x1 y1 x2 y2) = abs (x2 - x1) * abs (y2 - y1)
Algebraic Data Types in General In general, an algebraic data type has one or more data constructors. Each data constructor can have zero or more arguments. Type and data constructor names must always start with a capital letter. Variables, including names of functions, must always start with a lowercase letter.
Pattern Matching area :: Shape -> Double area (Circle r) = pi * r ^ 2 area (Rectangle x1 y1 x2 y2) = abs (x2 - x1) * abs (y2 - y1) We ve already seen examples of pattern matching, but let s see how it works in general. Fundamentally, pattern matching is about taking apart a value by finding out which constructor it was built with. Notice how in our definition of area, names are given to the values that are associate with the data constructor. An underscore, _, is a wildcard pattern that matches anything. A pattern of the form v@pat can be used to match a value against the pattern pat while also binding the variable v to the entire value being matched. For example: firstcircle :: [Shape] -> Shape firstcircle [] = error "Urk!" firstcircle (shape@(circle _) : _) = shape firstcircle (_:shapes) = firstcircle shapes Patterns can also be nested, as in firstcircle.
Pattern Matching Syntax The general syntax for pattern matching is: pat ::= _ var var @ pat (Constructor pat 1 pat 2... pat n )
Case Expressions The fundamental Haskell construct for pattern matching is the case expression. In general, a case expression looks like this: case exp of pat1 -> exp1 pat2 -> exp2... In fact, the multi-clause syntax for defining functions that we have seen is just syntactic sugar for a case expression. Instead of this area :: Shape -> Double area (Circle r) = pi * r ^ 2 area (Rectangle x1 y1 x2 y2) = abs (x2 - x1) * abs (y2 - y1) we could have written this area :: Shape -> Double area shape = case shape of (Circle r) -> pi * r ^ 2 (Rectangle x1 y1 x2 y2) -> abs (x2 - x1) * abs (y2 - y1)
Tuples There is one more pervasive data type we haven t mentioned: tuples. Tuples have a single data constructor and can have any number of values associated with this data constructor. There is also special syntax for tuples. anintpair :: (Int, Int) anintpair = (3, 4)
Records It is often convenient to refer to field of a data constructor by name. radius :: Shape -> Double radius (Circle r) = r radius (Rectangle ) = error "urk!" Haskell provides syntax for defining and matching on records. data Shape = Circle { centx :: Double, centy :: Double, radius :: Double} Rectangle { ulx :: Double, uly :: Double, urx :: Double, ury :: Double } mycircle = Circle 0 0 1 myothercircle = Circle { centx = 0, centy = 0, radius = 1 } radiusof circle = radius circle area (Circle { radius = r }) = pi * r ^ 2 area (Rectangle { ulx = x1, uly = y1, urx = x2, ury = y2 }) = abs (x2 - x1) * abs (y2 - y1)
Section 4 References
References [Hin09] Ralf Hinze. Functional pearl: La Tour D Hanoï. In Proceedings of the 14th ACM SIGPLAN International Conference on Functional Programming (ICFP 09), pages 3 10, New York, NY, USA, 2009. [Pie02] Benjamin C. Pierce. Types and Programming Languages. Cambridge, Mass, feb 2002.