CS52 - Spring 2017 - Class 3
Example code in this lecture
interval.sml
list_basics.sml
rev_examples.sml
Lecture notes
admin
- assignment 0
- make sure you're using consistent and informative formatting
- assignment 1
- due Friday at 5pm
- start now!
- keep up with the readings
- mentor hours
- lunch with CS faculty members
- Terminal tutorial session tonight!
look at the interval function in
interval.sml code
- What does it do?
- takes two numbers as parameters
- how do you know they're numbers?
- m+1
- n <= m
- creates a list (using ::)
- the list contains the number from m to n
- including m and n?
- including m
- not including n
> interval 1 10;
val it = [1,2,3,4,5,6,7,8,9] : int list
- What is its type signature (curried or uncurried)?
val interval = fn : int -> int -> int list
- curried function
- has two parameters (sort of), both ints
- gives us back a list of ints!
look at the interval2 function in
interval.sml code
- does the same thing!
- uses @ instead of ::
- notice that because we're using @, we have to do [n-1] because @ expects two lists
- functionally these behave the same, but there are some differences in performance (more on this later!)
> interval 1 100000;
val it = [1,2,3,4,5,6,7,8,9,10,11,12,...] : int list
> interval2 1 100000;
Interrupt
Comments, an aside
- SML has one type of comment, similar to Java's /* */ comment
- Comment start with (* and end with *)
- They can span multiple lines
-
Commenting code
:
- You should have a comment at the top of any file you create explaining what's in there, your name, etc.
- All functions should have comments above them explaining what that function does
- If there are any complicated portions of the functions (or funny parts) you should also put a small comment to the side of that line
- In general, SML tends to have less comments than other languages because the code is more compact and self-contained. BUT, you still should be putting in *some* comments
look at the addTo function in
list_basics.sml code
- We can also do recursion on lists
- this will be a very, very common thing we'll see in this class
- follows very naturally from the recursive definition of lists. a list is:
- an element
- and the rest of the list
- recursion is then:
- do something with the element
- recursively process the rest of the list
- What does it do and what is the type signature?
- val addTo = fn: int -> int list -> int list
- adds k to all elements in the list
- Has two patterns corresponding to the base case and the recursive case
- recursive case
- we can pattern match a non-empty list using (x::xs)
- x gets the first element in the list
- xs gets the rest of the list (xs = excess :)
- (x+k)::(addTo k xs)
- the value is a list with x+k at the front
- where the rest of the list is xs with k added to each element
- base case
- eventually, as we process the elements of the first list, we will get to the end, which will be an empty list
- this will pattern match to the first case
- what is the empty list with k added to it?
- just []
- Curried or uncurried?
- curried
- by making it curried we can make our own specialized functions:
> val add47To = addTo 47;
val add47To = fn : int list -> int list
> add47To [10];
val it = [57] : int list
> add47To [1, 2, 3, 4];
val it = [48,49,51,51] : int list
- Notice again that there is a parameter, k, that's just along for the ride
- the recursion is on the second parameter, the list
look at the deleteLast function in list_basics.sml
- what does the second pattern match, i.e. deleteLast [x]?
- any list with a single item in it
- x then will contain that single value
- what is the type signature of this function?
- take as input a list and returns a list
- what type of list?
- any type!
- the only constraint is that the input and output lists are of the same type
deleteLast: 'a list -> 'a list
- SML indicates this using type variables ('a)
- what does this function do?
- deletes the last element of the list
- the third pattern simply copies the list
- eventually, we get to the point where the list is a single item
- in that case, we "delete" the item by not copying it (i.e. returning the empty list)
- We include the [] base case for completeness (otherwise, SML would yell at us)
- probably better to raise an exception in this case (but we haven't talked about that yet!)
writing recursive functions
1. define what the function header is (i.e. name and type signature)
- what parameters does the function take? curried or uncurried?
- what value does the function return
2. define the recursive case
- pretend you had a working version of your function, but it only works on smaller versions of your current problem, how could you write your function?
- the recursive problem should be getting "smaller", by some definition of smaller
- other ideas:
- sometimes define it in English first and then translate that into code
- often nice to think about it mathematically, using equals
3. define the base case
- recursive calls should be making the problem "smaller"
- what is the smallest (or simplest) problem?
4. put it all together
- first, check the base case
- return something (or do something) for the base case
- if the base case isn't true
- calculate the problem using the recursive definition
- return the answer
An example: reverse
- Write a function called rev that takes a list and gives us a reversed version of that list
1. val rev = fn: 'a list -> 'a list
- takes a list of values and returns the same values in a list, but reversed
2. rev (x::xs) = ?
- what would we get back if we called rev on xs?
- all of xs reversed, as a list
- trust the recursion
- how could we then use that answer to get our overall answer?
- put x at the end of it
= (rev xs) @ [x]
- why can't we just put (rev xs) @ x?
- @ expects two lists as arguments!
3. Each recursive call makes the list smaller. Eventually, it will be very, very easy to reverse
- rev [] = []
4. Put it all together:
fun rev nil = nil
| rev (x::xs) = (rev xs) @ [x];
Efficiency
- I claim that this is not a very efficient implementation of reverse
- How could we check this?
- make a big list and reverse it!
- Let's use our interval function we defined before. What does it do?
- creates a list of numbers, m ... n-1
- We can run rev on increasingly larger lists:
> interval 1 10;
val it = [1,2,3,4,5,6,7,8,9] : int list
> rev (interval 1 10);
val it = [9,8,7,6,5,4,3,2,1] : int list
> rev (interval 1 100);
val it = [99,98,97,96,95,94,93,92,91,90,89,88,...] : int list
An aside, SML won't display the full list, though this can be updated
> rev (interval 1 1000);
val it = [999,998,997,996,995,994,993,992,991,990,989,988,...] : int list
> rev (interval 1 10000);
val it = [9999,9998,9997,9996,9995,9994,9993,9992,9991,9990,9989,9988,...] : int list
> rev (interval 1 100000);
It works ok for small lists, but for longer lists it takes a while...
A more efficient reverse
- The problem turns out to be (we'll talk about this more later) that @ is a linear time algorithm (see assignment 1 for implementation details :)
- Let's try and solve the problem without using @
- Tell me what the following function does:
fun revAux (acc, []) = acc
| revAux (acc, x::xs) = revAux(x::acc, xs);
- First, what is it's type signature?
val revAux = fn: 'a list * 'a list -> 'a list
rev_examples.sml rev_examples.sml
- Is this a curried or uncurried function?
- uncurried
- What does it actually do?
- adds the elements of second argument/list in reverse order to the first argument
- How is revAux useful for rev?
- if we call it with an empty list as the first argument, then it's reverse!
- We could just write:
fun rev2 lst = revAux ([],lst)
- Any problems with this?
- Any time you have an auxiliary or helper function, better to encapsulate it for use only with this function
information hiding with rev_examples.sml
- let allows you to define values (e.g. functions) that are only available inside a certain block of code
- syntax
let
<val declaration1>; (including function definitions)
<val declaration2>;
...
in
<expression>
end;
- the values declared inside of let ... in are only available inside the block of code in ... end
- the value of the entire let expression is the value of <expression>
back to reverse
- Often it's a good idea to write and test your auxiliary function separately and THEN put it in the let statement
- A better version of rev: look at rev2 in
rev_examples.sml code
- Now, let's see if it's any more efficient!
> rev2 [1, 2, 3, 4];
val it = [4,3,2,1] : int list
> rev2 (interval 1 100000);
val it = [99999,99998,99997,99996,99995,99994,99993,99992,99991,99990,99989,99988, ...] : int list