[Notes by Jeff Vaughan] Topics: 1) Functions that return functions a) Mathematical Functions b) Storage Function 2) Stucts/Sigs and Datastructures (run times too) a) One List Queue b) Two Stack Queue 3) PS1 a) Common Mistakes b) Don't Panic 1) Functions that return Functions a) Mathematical Functions: Functions that return functions seem a little bit strange, but are actually not uncommon in the real world. For example, consider a normal mathematical functions, f(x) and g(x), where f f: Real->Real and g: Real -> Real (Real denotes the mathematical set of real numbers). In alegebra we learned how to play with functions. For example: h = g + f i = f o g In calculus we learned how to do even cooler things: f' = df/dx And analysis takes this stuff even farther: F(w) = FourierTransform(f(x)) All of these operations are functions which operate on functions and return functions. As used above: + : (Real->Real * Real->Real) -> (Real->Real) o : (Real->Real * Real->Real) -> (Real->Real) f': (Real->Real) -> (Real->Real) FourierTransform: (Real->Real) -> (Real->Complex) These guys all look like SML types and we can in fact write SML functions for these second-order operations. fun add(f: real->real,g: real->real): real->real = fn(x)=>f(x)+g(x) fun compose(f: real->real,g: real->real): real->real = fn(x) => f(g(x)) fun deriv(f: real->real): real->real = fn (x) => let val e=0.0005 in (f(x)-f(x-e))/e end Writing integrals and fourier transforms would be messier, but certainly doable (with some caveats). Note that in the case of compose we don't really need to restict the types so much, and we could have written: fun compose(f:'a->'b, g:'c->a'):'c->'a = fn(x)=>f(g(x)) SML even provides an infix version of compose called "o". It's use, where appropriate, is encouraged. b) Storage functions Consider: fun foo (f: string->int option) (var: string, x: int): string->int option = fn(var': string)=> if var=var' then SOME x else f(var'); fun empty(s:string):int option=NONE; What do foo empty do? Answer we can use foo functionally update a mapping form string->int option. Empty is the simplest such mapping. Example: - val S = foo empty ("x",1); val S = fn : string -> int option - val S = foo S ("y",2); val S = fn : string -> int option - val S = foo S ("z",3); val S = fn : string -> int option - val S = foo S ("bar",4); val S = fn : string -> int option - - S "x"; val it = SOME 1 : int option - S "Death to waffles!"; val it = NONE : int option 2) Structs/Sigs and Datastructures (run times too) signature QUEUE = sig type 'a queue val enqueue: 'a queue * 'a -> 'a queue val dequeue: 'a queue -> 'a * 'a queue val empty: 'a queue end a) One List Queue structure ListQueue :> QUEUE = struct type 'a queue = 'a list val empty = [] fun enqueue (l: 'a queue, x: 'a) = l@[x] fun dequeue (l: 'a queue) = case l of hd::tl => (hd,tl) | nil => raise Fail "Dequeue from empty queue" end Note that every enqeue requires and O(n) insert, while deques are O(1). b) Two Stack Queue structure TwoStackQueue :> QUEUE = struct type 'a queue = 'a list * 'a list val empty = ([],[]) fun enqueue ((inStack, outStack): 'a queue, x: 'a) = (x::inStack, outStack) fun dequeue (q: 'a queue) = case q of ([], []) => raise Fail "Dequeue from empty queue" | (x, hd::tl) => (hd,(x,tl)) | (x, []) => dequeue([], rev x) end In this case the runtime is faster. Why? Well enque is always O(1), and dequeue is sometimes O(n), but usually O(1). We can quantify this by noticing that each element is moved by list.rev exactly once. The rev operation takes O(n) time, but O(n) averaged over n elements is, on average, kn/n = k = O(1). So the *amortized run time* for dequeue is O(1). We will look at proofs like this more formally as soon as the recursive substitution model is finished in lecture. 3) Problem Set One a) Common errors question 2, expressions: val foo: int list = [] We gave no credit for this because it was mentioned that type coercion like this was not sufficient on the newsgroup at least once. question 6 (datatypes3): - Most people got full credit. - A few people seemed confused with curried functions because they were calling "map" and "find" with the arguments as a tuple. - The most common error was in the "find" function. Some people would have their recursive case add 1 regardless of whether the element was found or not. This results in the incorrect answer because when it is not found the recursive call returns ~1 and they add 1 to it a few times. datatypes2 (value-carrying datatypes). Common issues were: - Many people got full credit (10 points), and a vast majority got 9 or better. - The most common error was in the numMillimeters function. When converting KILOMETER(x) to millimeters, some people truncated x (which was a real) to an int before performing the conversion thus effectively losing a significant amount of precision. - A few people had incorrect metric conversions. - A few people had non-compiles in this section because they didn't check their work carefully (e.g. trying to use Math.floor). It's probably a good idea to stress that people should carefully test their work before submitting. b) Don't Panic PS 1 is not worth a lot. A large part of cs312 grading is discretionary, and one bad grade early on can be made up for. Some people didn't compile and received zeros for parts the problem set. These folks really should see a consultant/TA/Ramin. Ask about regrades (hint: we are nice people).