(* file: repetition.ml author: Bob Muller Lecture Notes for Meeting 1 of Week 3 of CS1103. Fall 2016 Notes: Remember that PS2 is due Thursday midnight. Announcements: Topics: 1. Repetition - working with Integers - Understanding Recursion - How Much Work Does a Function Do? Usage: You can run the code in these notes from the REPL by typing ctrl-y ctrl-f. Last time we saw: fibonacci : int -> int *) let rec fibonacci n = match n < 3 with | true -> 1 | false -> fibonacci (n - 1) + fibonacci (n - 2) (* We tried the fibonacci function on various inputs: 10, 20, 30, 40 and 45. We found that (fibonacci 40) and (fibonacci 45) took a while. The definition raises questions: 1. How can we -use- the fibonacci function before we're finished defining it? 2. How can we reason/think about the number of simplification steps required for a function? 3. How do we write repetitive functions? As far as #2 is concerned, we'll be discussing it in more detail later, but generally speaking, how many simplification steps are required to simplify (fibonacci n)? A diagram helps. Consider (fibonacci 5), using "f" for "fibonacci", the essential computation can be drawn as a tree: (f 5) = + + / \ / \ (f 4) = + + = (f 3) or + + / \ / \ / \ / \ (f 3) = + 1 = (f 2) (f 2) = 1 1 = (f 1) + 1 1 1 / \ / \ (f 2) = 1 1 = (f 1) 1 1 There are n - 1 levels. Each level is (nearly) doubling the work. Thus, we have roughly 2 * ... * 2 or 2^n steps. As we'll see, when thinking about the work required for a function, we ask how many steps does it do when given an input of a given size N. *) (* fact : int -> int *) let rec fact n = match n = 0 with | true -> 1 | false -> n * fact (n - 1) (* As for #1: Simplification of calls of recurisve functions work per usual: fact (1 + 2) -> fact 3 -> match 3 = 0 with | true -> 1 | false -> 3 * fact (3 - 1) -> match false with | true -> 1 | false -> 3 * fact (3 - 1) -> 3 * (fact (3 - 1)) -> 3 * (fact 2) -> 3 * (match 2 = 0 with | true -> 1 | false -> 2 * fact (2 - 1)) -> 3 * (match false with | true -> 1 | false -> 2 * fact (2 - 1)) -> 3 * (2 * (fact (2 - 1))) -> 3 * (2 * (fact 1)) -> 3 * (2 * (match 1 = 0 with | true -> 1 | false -> 1 * fact (1 - 1)) -> 3 * (2 * (match false with | true -> 1 | false -> 1 * fact (1 - 1)) -> 3 * (2 * (1 * (fact (1 - 1))) -> 3 * (2 * (1 * (fact 0))) -> 3 * (2 * (1 * (match 0 = 0 with | true -> 1 | false -> 0 * fact (0 - 1)))) -> 3 * (2 * (1 * (match true with | true -> 1 | false -> 0 * fact (0 - 1)))) -> 3 * (2 * (1 * 1)) -> 3 * (2 * 1) -> 3 * 2 -> 6 NB: 19 simplification steps were required to get from (fact (1 + 2)) to the value 6. Eliding (i.e., skipping over) the simplification of function calls and match expressions, we would see the shape of this computation: fact (1 + 2) -> fact 3 -> 3 * (fact 2) -> 3 * (2 * (fact 1)) -> 3 * (2 * (1 * (fact 0))) -> 3 * (2 * (1 * 1)) -> 3 * (2 * 1) -> 3 * 2 -> 6 We can define an alternative version that uses a helper function: factHelp : int -> int -> int *) let rec factHelp n acc = match n = 0 with | true -> acc | false -> factHelp (n - 1) (n * acc) let fact n = factHelp n 1 (* Consider the shape: fact (1 + 2) -> fact 3 -> factHelp 3 1 -> factHelp (3 - 1) (3 * 1) -> factHelp 2 (3 * 1) -> factHelp 2 3 -> factHelp (2 - 1) (2 * 3) -> factHelp 1 (2 * 3) -> factHelp 1 6 -> factHelp (1 - 1) (1 * 6) -> factHelp 0 (1 * 6) -> factHelp 0 6 -> 6 This is a fundamentally different shape and a more efficient function. This shape has a name: tail-recursion. Normally, we would define the helper factHelp inside of the fact function, as follows: *) let fact n = let rec factHelp n acc = match n = 0 with | true -> acc | false -> factHelp (n - 1) (n * acc) in factHelp n 1 (* A computationally tractable fibonacci function: fibonacci : int -> int *) let fibonacci n = let rec repeat n a b = match n = 1 with | true -> a | false -> repeat (n - 1) b (a + b) in repeat n 1 1 (* Primality Testing First two little helper functions. intSqrt : int -> int *) let intSqrt n = int_of_float(sqrt (float n)) (* isFactor : int -> int -> bool *) let isFactor m n = (n mod m) = 0 (* isPrime : int -> bool The call (isPrime n) checks all integers in the range 2 .. sqrt(n) to see if they are factors. *) let isPrime n = let top = intSqrt n in let rec checkNext i = match i > top with | true -> true | false -> (match isFactor i n with | true -> false | false -> checkNext (i + 1)) in checkNext 2