Thoughts on Assignment 4 Haskell: Flow of Control CS F331 Programming Languages CSCE A331 Programming Language Concepts Lecture Slides Monday, February 27, 2017 Glenn G. Chappell Department of Computer Science University of Alaska Fairbanks ggchappell@alaska.edu 2017 Glenn G. Chappell
Thoughts on Assignment 4 Introduction In Assignment 4 you will be writing a Recursive-Descent parser, in the form of Lua module parseit. It will be similar to module rdparser4 (written in class), but it will involve a different grammar & AST specification. In Assignment 6 you will write an interpreter that takes, as input, an AST of the form your parser returns. The result will be an implementation of a programming language called Kanchil. Module parseit is to parse a complete programming language, while module rdparser4 only parses expressions. Nonetheless, Kanchil has expressions, and they work in much the same way as those handled by rdparser4. I suggest using file rdparser4.lua as a starting point for Assignment 4. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 2
Thoughts on Assignment 4 The Goal Again, here is a sample Kanchil program. # Subroutine &fibo # Given %k, set %fibk to F(%k), # where F(n) = nth Fibonacci no. sub &fibo set %a: 0 # Consecutive Fibos set %b: 1 set %i: 0 # Loop counter while %i < %k set %c: %a+%b # Advance set %a: %b set %b: %c set %i: %i+1 # ++counter end set %fibk: %a # Result end # Get number of Fibos to output print "How many Fibos to print: " input %n cr # Print requested number of Fibos set %j: 0 # Loop counter while %j < %n set %k: %j call &fibo print "F(" print %j print ") = " print %fibk cr set %j: %j + 1 end 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 3
Thoughts on Assignment 4 The Grammar (1) program stmt_list (2) stmt_list { statement } (3) statement cr (4) print ( STRLIT expr ) (5) input lvalue (6) set lvalue : expr (7) sub SUBID stmt_list end (8) call SUBID (9) if expr stmt_list { elseif expr stmt_list } [ else stmt_list ] end (10) while expr stmt_list end (11) expr comp_expr { ( && ) comp_expr } (12) comp_expr! comp_expr (13) arith_expr { ( ==!= < <= > >= ) arith_expr } (14) arith_expr term { ( + - ) term } (15) term factor { ( * / % ) factor } (16) factor ( + - ) factor (17) ( expr ) (18) NUMLIT (19) ( true false ) (20) lvalue (21) lvalue VARID [ [ expr ] ] 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 4
Thoughts on Assignment 4 Calling lexit.preferop Your parser will need to call function lexit.preferop immediately after it sees a lexeme that fits any of the following. category == VARID category == NUMLIT string == "]" string == ")" string == "true" string == "false" I suggest that you handle this in function advance. Then this issue does not have to be dealt with in your parsing functions at all. You can write it once, and never worry about it again. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 5
Thoughts on Assignment 4 Returning Two Values The most common mistake I made when writing module parseit was to return only one value from a parsing function. Remember that a parsing function will always return two values: boolean & AST. If the boolean is True, then the AST needs to be in the proper form. If the boolean is False, then the AST can be anything (it might as well be nil). 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 6
Thoughts on Assignment 4 Parsing end Three kinds of Kanchil statements are terminated with an end keyword: Sub statements (subroutine definitions) If statements While statements (while loops) Other kinds of statements have no end. This means, for example, that you generally do not want to exit the parsing function early if you think the if-statement is over. So code like the following is not good. if not matchstring("else") then end return true, ast 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 7
Thoughts on Assignment 4 Some Code I have posted a file containing a portion of my version of parseit.lua. See assn4_code.txt. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 8
Review PL Categories: Functional PLs Functional programming (FP) is a programming style in which functions are primary, and side effects & mutable data are avoided. A functional programming language is a PL designed to support FP well. A pure functional PL does not support mutable data at all. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 9
Review Introduction to Haskell Haskell is a pure functional PL. It has first-class functions and good support for higher-order functions. Haskell has a sound static type system with sophisticated type inference. Typing is largely inferred, and thus implicit; however, type annotations are permitted. Haskell has no iteration. Recursion is used. Tail-call optimization (TCO) is done. Haskell has significant indentation. Evaluation in Haskell is lazy. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 10
Review Haskell: Functions [1/2] Function definition: what looks like a function call, an equals sign, and an expression for the value of the function. Pattern matching is used. Introduce local definitions with where. Patterns factorial 0 = 1 factorial n = n * factorial prev prev = n-1 Local definition where We can also define new infix binary operators. a +$+ b = 2*a + b 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 11
Review Haskell: Functions [2/2] Currying: simulating a multiparameter function using a single parameter function that returns a function. sub a b = a-b sub 5 2 -- Returns 3 sub_from_5 = sub 5 sub_from_5 2 -- Returns 3 Currying makes some higher-order functions easy to write. rev f a b = f b a rsub = rev sub rsub 5 2 -- Returns -3 rsub 2 5 -- Returns 3 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 12
Review Haskell: Lists Lists & Tuples [1/2] A statically typed PL will typically support two ways of aggregating multiple data items into a single collection: A collection of an arbitrary number of data items, all of the same type. Example. C++ vector, list, deque. A collection of a fixed number of data items, possibly of different types. Example. C++ tuple, struct. Haskell supports the above two categories as well, in the form of lists and tuples. A Haskell tuple holds a fixed number of data items, possibly of different types. > :t (2.1, 1.2, True) (Double,Double,Bool) This represents the GHCi prompt. For code, see list.hs. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 13
Review Haskell: Lists Lists & Tuples [2/2] A Haskell list holds an arbitrary number of data items, all of the same type. A list literal uses brackets and commas. ["hello", "there"] -- List of two String values [[1], [], [1,2,3,4]] -- List of lists of Integer [1, [2, 3]] -- ERROR; types differ [1, 3..] -- Infinite list The type of a list is written as the item type in brackets. > :t [True, False] [True, False] :: [Bool] > :t [False, True, True, True, True, False] [False, True, True, True, True, False] :: [Bool] 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 14
Review Haskell: Lists List Primitives 1. Construct an empty list. [] 2. Cons: list from first item, list of other items. Uses colon (:). 5:[2, 1, 8] -- Same as [5, 2, 1, 8] 5:2:1:8:[] -- Also same; ":" is right-associative 3. Pattern matching for lists. ff [] = 3 -- Value of ff for an empty list ff (x:xs) = 4 -- Value of ff for a nonempty list gg [a, b, c] = 19 -- Value of gg for a 3-item list 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 15
Review Haskell: Lists Other List Syntax: Strings & Ranges A Haskell String is a list of characters (Char values). ['a', 'b', 'c'] "abc" -- Same as above Use.. to construct a list holding a range of values. There are exactly four ways to do this. [1..10] -- Same as [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] [1,3..10] -- Same as [1, 3, 5, 7, 9] [1..] -- Infinite list: [1, 2, 3, 4, 5, 6, 7, 8, ] [1,3..] -- Infinite list: [1, 3, 5, 7, 9, 11, ] These four are wrappers around enumfromto, enumfromthento, enumfrom, and enumfromthen, respectively. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 16
Review Haskell: Lists Other List Syntax: List Comprehensions [1/2] You have probably seen the mathematical notation known as a set comprehension (or set-builder notation). Here is an example. { xy x {3, 2, 1} and y {10, 11, 12} } The above is read as, The set of all xy for x in the set {1, 2, 3} and y in the set {10, 11, 12}. A number of PLs, including Haskell, have a construct based on this idea: the list comprehension. Here is a Haskell example. [ x*y x <- [3, 2, 1], y <- [10, 11, 12] ] This deals with Haskell lists instead of sets, but is otherwise very similar to the above set comprehension. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 17
Review Haskell: Lists Other List Syntax: List Comprehensions [2/2] The syntax of a Haskell list comprehension is as follows. Brackets enclose the following: An expression. Then a vertical bar ( ). Then a comma-separated list of two kinds of things: var <- list Expression of type Bool Here are some examples: > [ x*y x <- [3, 2, 1], y <- [10, 11, 12] ] [30,33,36,20,22,24,10,11,12] > [ x*x x <- [1..6] ] [1,4,9,16,25,36] > [ x x <- [1..20], x `mod` 2 == 1] [1,3,5,7,9,11,13,15,17,19] 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 18
Review Haskell: Lists Lists & Recursion [1/3] When writing a function that takes a list, it is very common to have two cases. One case handles the empty list: []. The other case handles nonempty lists. Remember that a pattern like a:as matches nonempty lists. isempty [] = True isempty (x:xs) = False listlength [] = 0 listlength (x:xs) = 1 + listlength xs 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 19
Review Haskell: Lists Lists & Recursion [2/3] A function that takes a list will often be recursive. Such a function will usually be organized as follows. The version that handles the empty list ([]) will be the base case. The version that handles nonempty lists (b:bs) will be the recursive case. This will do a computation involving the head of the list (b) and make a recursive call with the tail (bs). myfilter p [] = [] -- p for predicate: function -- returning Bool myfilter p (x:xs) = if (p x) then x:rest else rest where rest = myfilter p xs Note the if then else construction. We can put line breaks pretty much anywhere we want inside this construction. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 20
Review Haskell: Lists Lists & Recursion [3/3] Sometimes other kinds of recursion are used. Here is a function that does lookup by index in a list (zero-based). lookup 0 (x:xs) = x lookup n (x:xs) = lookup (n-1) xs lookup _ [] = error "lookup: index too big or negative" This pattern means unused parameter. Function error takes a String and returns any type; that is, it can be used in any context. It does not actually return anything. Instead, it crashes the program, printing a message that includes the given String. An alternate error-message function is undefined, which takes no parameters. It is like error with a default message. lookup _ [] = undefined -- Replaces the above line 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 21
Haskell: Flow of Control Introduction Flow of control refers to the ways a PL determines what code is executed. For example, flow of control in Lua includes: Selection (if elseif else). Iteration (while, for). Function calls. Coroutines. Threads. Exceptions. Haskell has very different flow-of-control facilities from most imperative PLs. Key Idea. Things that are done with traditional flow-of-control constructs in imperative programming languages are often done differently in Haskell. For code, see flow.hs. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 22
Haskell: Flow of Control Pattern Matching, Recursion, Lazy Evaluation [1/5] We have seen that Haskell has a useful pattern matching facility, which allows us to choose one of a number of function definitions. The rule is that the first definition with a matching pattern is the one used. isempty [] = True isempty (x:xs) = False -- fibo the SLOW way fibo 0 = 0 fibo 1 = 1 fibo n = fibo (n-2) + fibo (n-1) In many of the places we would use an if else construction in an imperative PL, we use pattern matching in Haskell. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 23
Haskell: Flow of Control Pattern Matching, Recursion, Lazy Evaluation [2/5] Haskell also makes heavy use of recursion. Recursion can be less costly in Haskell than in PLs like C++, because of Haskell s required tail-call optimization (TCO). TCO means that a tail call does not use additional stack space. listlength [] = 0 listlength (x:xs) = 1 + listlength xs In places where we would use a loop in an imperative PL, we use recursion in Haskell. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 24
Haskell: Flow of Control Pattern Matching, Recursion, Lazy Evaluation [3/5] By default, Haskell does lazy evaluation. This allows for infinite lists. We have said that we can generate these even without the.. syntax. But how? Here is an idea. -- listfrom n -- Returns the infinite list [n, n+1, n+2, ]. listfrom n = n:listfrom (n+1) Is this code acceptable? It has recursion without a base case. But this is not a problem, thanks to lazy evaluation. A recursive call is only made if further list items are needed. Using only a finite number of items guarantees that the recursion terminates. The above code uses corecursion: A stream of values is generated recursively. The recursion terminates when no more values are needed. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 25
Haskell: Flow of Control Pattern Matching, Recursion, Lazy Evaluation [4/5] Something else we can do: write our own if else, as a function. -- myif condition tval fval -- Returns tval if condition is True, fval otherwise. myif True tval _ = tval myif False _ fval = fval Note that no more than one of tval, fval is ever evaluated, thanks to lazy evaluation. Here is the slow Fibonacci algorithm using myif. fibo n = myif (n <= 1) n (fibo (n-2) + fibo (n-1)) 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 26
Haskell: Flow of Control Pattern Matching, Recursion, Lazy Evaluation [5/5] And here is myfilter, reimplemented using myif. myfilter p [] = [] myfilter p (x:xs) = myif (p x) (x:rest) rest where rest = myfilter p xs It turns out that the combination of pattern matching, recursion, and lazy evaluation, together with function calls, are all we need. We can build any flow-of-control construct out of these. However, Haskell has other flow-of-control facilities, for convenience. Next we look at a few of these. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 27
Haskell: Flow of Control Selection Introduction Selection allows us to choose one of multiple options to execute. Selection in C++ includes if else, switch, and virtual function dispatch. In Haskell, pattern matching works as a selection mechanism. Other selection constructions include guards, if then else, and case. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 28
Haskell: Flow of Control Selection Guards [1/3] Guards are the Haskell equivalent of mathematical notation like the following. x, if x 0; myabs x = ( x, otherwise. In Haskell: myabs x x >= 0 = x otherwise = -x We use guards in situations that pattern matching cannot handle. For example, there is no pattern that matches only nonnegative numbers. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 29
Haskell: Flow of Control Selection Guards [2/3] myabs x x >= 0 = x otherwise = -x Note that there is no equals sign after the first line above. Each vertical bar is followed by a boolean expression. The first True expression tells which value is used. We generally want the last line to handle all remaining cases. We could use True as our final expression. otherwise is a variable with value True. Here is the slow Fibonacci algorithm reimplemented using guards. fibo n n <= 1 = n otherwise = fibo (n-2) + fibo (n-1) 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 30
Haskell: Flow of Control Selection Guards [3/3] Here is myfilter reimplemented using guards. myfilter p [] = [] myfilter p (x:xs) p x = x:rest otherwise = rest where rest = myfilter p xs 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 31
Haskell: Flow of Control Selection if then else We have seen Haskell s if then else construction. This is much like our myif. myif condition tval fval if condition then tval else fval -- Same as above Most of the possible uses of if then else are probably better done with guards. And some people consider if then else to be un-haskell-ish. But use it if you want. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 32
Haskell: Flow of Control Selection case Haskell s case construction is analogous to switch in C++. Here is an example, using case and using multiple function definitions. -- case fibo n = case n of 0 -> 0 1 -> 1 _ -> fibo (n-2) + fibo (n-1) -- Multiple definitions fibo 0 = 0 fibo 1 = 1 fibo n = fibo (n-2) + fibo (n-1) A case construction can always be replaced by multiple definitions. So I never use case. But case does have an important behind-thescenes role. Multiple definitions are actually syntactic sugar over a case construction. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 33
Haskell: Flow of Control Error Handling Fatal Errors We have seen Haskell s fatal-error facilities: error and undefined. lookup 0 (x:xs) = x lookup n (x:xs) = lookup (n-1) xs lookup _ [] = error "lookup: index too big or negative" These should be reserved for cases when a program needs to crash. This is generally because the program has detected a bug in its code. It crashes with an explanatory message, so that a developer can fix the bug. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 34
Haskell: Flow of Control Error Handling Exceptions Haskell also has an exception mechanism, for dealing with error conditions that may be handled at runtime. I think that Haskell s exceptions are best avoided by new Haskell programmers (and many experienced Haskell programmers). So we will not be covering them. If you look into Haskell s exceptions, be aware that the terms error and exception are used inconsistently in both the standard library naming conventions and its documentation. Some documentation authors have difficulty explaining the difference between the two terms; others seem unaware that there is a difference. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 35
Haskell: Flow of Control Error Handling Implementing Exceptions [1/6] As with other Haskell features, it can be interesting to see how we might implement something like exceptions ourselves. We can do a rudimentary exception implementation using maybe types. Suppose t is a Haskell type. The type Maybe t has two kinds of values: Just x, where x is a value of type t, and Nothing. We can distinguish between these using pattern matching. squaremaybe (Just x) = Just (x*x) squaremaybe Nothing = Nothing Just and Nothing are examples of Haskell constructors. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 36
Haskell: Flow of Control Error Handling Implementing Exceptions [2/6] Suppose we use Just x to represent the value x and Nothing to represent an exception. esqrt :: Double -> Maybe Double esqrt x x < 0.0 = Nothing otherwise = Just (sqrt x) It would probably be better if our function took the same kind of value that it returns 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 37
Haskell: Flow of Control Error Handling Implementing Exceptions [3/6] It would probably be better if our function took the same kind of value that it returns. esqrt :: Maybe Double -> Maybe Double esqrt Nothing = Nothing esqrt (Just x) x < 0.0 = Nothing otherwise = Just (sqrt x) Now the function not only raises exceptions; it also propagates exceptions it receives to its caller. (This is analogous to what is called exception neutrality in C++.) 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 38
Haskell: Flow of Control Error Handling Implementing Exceptions [4/6] Here is a division operator that uses this idea. (I use @/, since / is already taken.) infixl 7 @/ -- Sets precedence, left associativity (@/) :: Maybe Double -> Maybe Double -> Maybe Double Nothing @/ _ = Nothing _ @/ Nothing = Nothing (Just _) @/ (Just 0.0) = Nothing (Just x) @/ (Just y) = Just (x / y) 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 39
Haskell: Flow of Control Error Handling Implementing Exceptions [5/6] We can write other operators this way. A few conveniences: e1 = Just 1.0 e2 = Just 2.0 e3 = Just 3.0 printit (Just x) = show x printit Nothing = "ERROR!" Conversion to String Examples: Function application, but very low precedence > printit $ e1 @/ e2 0.5 > printit $ e3 @+ e1 @/ (e3 @- e1 @- e2) * e2 "ERROR!" 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 40
Haskell: Flow of Control Error Handling Implementing Exceptions [6/6] We could improve this arithmetic package just a bit. Using type classes, we can overload the / operator, and thus avoid using @. We can also overload the show function, so that our values can be printed in the usual way. There might be an objection to my referring to the error signals used by this package as exceptions. They are normal return values. Why call them exceptions? My answer is that, because of the way the various functions & operators are written, error signals propagate automatically to the caller. Once an error has been found, no more arithmetic is done (because of lazy evaluation, no more arithmetic at all); the error signal is simply sent to the caller. While this mechanism may not be exceptions in some precise sense, it acts enough like exceptions as we know them, to warrant the name (I think). 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 41
Haskell: Flow of Control TO BE CONTINUED Haskell: Flow of Control will be continued next time. 27 Feb 2017 CS F331 / CSCE A331 Spring 2017 42