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