CS52 - Spring 2016 - Class 3
Example code in this lecture
interval.sml
list_basics.sml
rev_examples.sml
Lecture notes
admin
- assignment 0
- Make sure to name your file properly (assign1.sml, assign2.sml, ...)
- Remember, no late work
- assignment 1
- due Friday at 5pm
- start now!
- make sure you're following formatting guidelines
- no tabs (need to change to spaces in editor setting)
- no lines longer than 80 characters
- consistent and informative formatting
- there is a formatCheck binary you can call to check some of these constraints (use it!). See the readings for where to find it.
- keep up with the readings
- mentor hours Tue, Wed and Thur
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 append function in
list_basics.sml code
- To understand why @ is slower than ::, let's try and implement our own append function
- 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 the function do?
- takes two parameters, two lists
- appends the second list to the first
- 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::(append xs lst)
- the value is a list with x at the front
- where the rest of the list is xs with lst appended to it
- 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 lst appended to it?
- just lst
- What is the type signature?
- takes two lists as input
- gives back a list
- what are the types of the lists?
- we don't specify!
- only thing is that all lists *must* be the same type
val append = fn : 'a list -> 'a list -> 'a list
- like we saw before 'a is a type variable
- the lists must all be the same type, but we don't need to specify the type
look at addTo function in
list_basics.sml code
- 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
- 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
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 append above)
- 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 letrev_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
error messages in SML
- You will get exceptions/errors in SML
- They're not the most informative, but you'll get used to understanding them as time goes on
- For example, say we remove the parentheses around (m+1) in the interval function in rev_examples.sml
- When we type: use "rev_examples.sml", we get the follow errors
[opening rev_examples.sml]
val rev = fn : 'a list -> 'a list
rev_examples.sml:23.13-23.27 Error: operator is not a function [literal]
operator: int
in expression:
1 n
rev_examples.sml:23.9-23.28 Error: operator and operand don't agree [overload]
operator domain: 'Z * 'Z list
operand: 'Z * 'Y
in expression:
m :: interval m + 1 n
rev_examples.sml:19.5-23.28 Error: right-hand-side of clause doesn't agree with function result type [overload]
expression: 'Z -> _ list
result type: 'Y
in declaration:
interval = (fn arg => (fn <pat> => <exp>))
uncaught exception Error
raised at: ../compiler/TopLevel/interact/evalloop.sml:66.19-66.27
../compiler/TopLevel/interact/evalloop.sml:44.55
../compiler/TopLevel/interact/evalloop.sml:296.17-296.20
- Start at the first error/exception (this isn't always perfect, but it's generally a good place to start)
- SML gives you information about *where* the problem is:
rev_examples.sml:23.13-23.27
- rev_examples.sml is the file with the problem
- 23 is the line number it's at
- 13-23 is the character span in the line with the problem
- What's wrong with this function?
- Be careful about order of operations
- Function binding has higher precedence than + (in fact, almost the highest precedence of anything, so be careful)
warnings in SML
- Next time, I'll talk about warnings you can ignore in SML
- In general, most warnings you CANNOT ignore
- One of the most common warnings is "Warning: match nonexhaustive"
- For example, remove the second line of the rev function in rev_examples.sml and we get the following error:
rev_examples.sml:12.5-12.22 Warning: match nonexhaustive
nil => ...
- the code does actual run (for now), but there's a problem!)
- What's the issue?
- Not every type of list we can call rev on will work (in fact any non-empty list will give us a problem)
> rev [1, 2, 3, 4];
uncaught exception Match [nonexhaustive match failure]
raised at: rev_examples.sml:12.22