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