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