CS54 - Fall 2022 - Class 4
Example code in this lecture
mystery2.sml
rev_examples.sml
more_list_recursion.sml
Lecture notes
admin
- assignment 1: how did it go?
- assignment 2 out later today
- masks: continue wearing this week and then optional next week
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
SML debugging/programming
- SML requires thinking NOT just changing things and hoping that it works!
- Work through the recursion by hand and/or small problems by hand
- Check your recursion by calculating by hand the recursive case and then trying out the recursion, e.g:
- rev [1, 2, 3, 4] - you guess that the recursive case is:
- rev (x::xs) = (rev xs)::[x]
- You can work though it by hand: rev [1, 2, 3, 4]
- x = 1
- xs = [2, 3, 4]
- assuming rev works correctly (rev xs) => [4, 3, 2]
- we can now type the recusrive case:
- [4, 3, 2] :: [1]
stdIn:7.1-7.17 Error: operator and operand don't agree [literal]
operator domain: int list * int list list
operand: int list * int list
in expression:
(4 :: 3 :: 2 :: nil) :: 1 :: nil
error messages in SML
- You will get exceptions/errors/warnings 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.13 is where is starts (line 23, character 13)
- 23.27 is where it ends (line 23, character 17)
- 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)
What does the following function do? (in
mystery2.sml code
)
fun mystery [] = false
| mystery [x] = true
| mystery (x::y::xs) = mystery xs;
- What is the type signature of this function?
- 'a list -> bool
- What are the three patterns matching?
- []: empty list
- [x]: a list with one element
- (x::y::xs): a list with at least two elements in it
- x gets the first element
- y get the second element
- What does it do?
- returns true if the list has an odd length, false if it has an even length
match nonexhaustive
- What if we'd mistakenly typed:
fun mystery [] = false
| mystery (x::y::xs) = mystery xs;
i.e. left off the second pattern?
- It will compile, but SML will issue a warning!
mystery2.sml:1.6-2.36 Warning: match nonexhaustive
nil => ...
x :: y :: xs => ...
val mystery = fn : 'a list -> bool
- What's the issue?
- We cannot call mystery with every type of list
- In particular, if we were to call it with an odd length list, we'd get to a point where there wasn't a pattern to match!
- Be careful! Sometimes these can be subtle. Anything wrong the following function?
fun ex 0 [] = 0
| ex y (x::xs) = (ex (y+x) xs);
- Still match nonexhaustive
- missing the case where the first parameter is non-zero and the second parameter is the empty list
- matches require *both* things to match (i.e. like "and")
warnings in SML
- In general, most warnings you CANNOT ignore
- Almost all of them point at a problem with your code
- there are two exceptions, i.e. warnings you *can* ignore:
> [] = [];
stdIn:13.4 Warning: calling polyEqual
val it = true : bool
> [] @ [];
stdIn:11.1-11.8 Warning: type vars not generalized because of
value restriction are instantiated to dummy types (X1,X2,...)
val it = [] : ?.X1 list
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
- 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
look at the appendAll function in
more_list_recursion.sml code
- type signature?
- 'a list list -> 'a list
- how do we know that the first argument is a list of lists?
- @ expects two lists as arguments
- therefore x must be a list
- therefore x::xs must bit a list of lists
- what does the function do?
- takes in a list of lists and creates one giant list with all of the elements in all of the lists (appends all the lists together)
look at the funPairs function in
more_list_recursion.sml code
- type signature?
- 'a -> ('b * 'a) list -> 'b -> 'a
- the 2nd parameter is a list of tuples by the pattern definition in the recursive case
- u and x must be the same type because of u = x
- the return type must be the same as v since we return it in the if statement
- finally, since the function must always return the same type, we know that default (the first parameter), must be the same as v and the return type
- what does the function do?
- searches through the list of tuples (second argument) and tries to match x (the third argument) to the first entry of the tuple
- if it finds one that matches, it returns the corresponding pair of the tuple
- it not, it returns the default value
- For example:
> val grades = [("dave", 85), ("teran", 94), ("lisa", 92), ("chris", 83)];
val grades = [("dave",85),("teran",94),("lisa",92),("chris",83)] : (string * int) list
> funPairs 0 grades "dave";
val it = 85 : int
> funPairs 0 grades "chris";
val it = 83 : int
> funPairs 0 grades "carl";
val it = 0 : int
- What is the "_" in the first parameter?
- It's still just a variable name
- We use it to communicate that in that pattern, that parameter doesn't get used