CS54 - Fall 2022 - Class 6

Example code in this lecture

   map_mystery.sml
   better_factorial.sml
   lets_count.sml

Lecture notes

  • admin
       - assignment 2: how did it go?
       - assignment 3 out later today

  • map
       - the mystery function is so important (and common) that it is built into sml as a function call map
       - very powerful function
       - if you feel like you want to "loop" over a list, map can often be used instead of writing an additional recursive function.

  • Another map example
       - I want to write a function called calc_areas that takes a list of (base, height) tuples representing the sides of rectangles and then calculates the area of these rectangles
          - What will be the type signature?

          val calc_areas = fn: (int * int) list -> int list

       - Let's do this *using map*?
       - First, let's write a function that just calculates the area given a tuple, calculate the area:
          fun mult (w, h) = w*h;
       - Now, Let's put it together using map
          > fun calc_areas lst = map mult lst;
          val calc_areas = fn : (int * int) list -> int list

          > calc_areas [(1, 2), (4, 5), (6, 2)];
          val it = [2,20,12] : int list

  • One last example:
       - What does the following declare?
          > val calc2 = map mult;

       - Map is curried and we can just instantiate the first argument
       
       - Two ways to think about this:
          1) the type signature hints at what the function does:
             ('a -> 'b) -> 'a list -> 'b list

             is instantiated with the mult type signature (int * int) -> int resulting in:

             (int * int) list -> int list

          2) alternatively, if we look at the mystery function in map_mystery.sml code and think about hard-coding f to "mult", we see that we get a function that takes a list of tuples, applies mult and concatenates the answer

       - This is just another way of defining calc_areas!


  • time saver: op
       - In the example above, we defined a new function mult. It seems a waste to define a new function where there is already a multiply operator (*)
       - can we just write?
          
          fun calcAreas lst = map * lst;
       
       - No!
          - this just tries to multiply map by lst
          - * is an infix operator that expects its arguments to be on either side

       - op helps us in this situation
          - op takes an operator as input and returns a prefix function (i.e. normal) where the input parameters are pass as a tuple:

          x ? y -> ? (x, y)

       - For example:
          > op*;
          val it = fn : int * int -> int

       - Does this help us with calcAreas?

          > fun calcAreas lst = map op* lst;
          val calcAreas = fn : (int * int) list -> int list

       - Note this works for all the operators (we'll see more use for this later)
          > op/;
          val it = fn : real * real -> real
          > op div;
          val it = fn : int * int -> int
          > op::;
          val it = fn : 'a * 'a list -> 'a list
          > op@;
          val it = fn : 'a list * 'a list -> 'a list

  • anonymous functions
       - sometimes we only need a function for one simple use case, e.g. when we were trying to add 47 to all the elements in a list
       - we could write the function as:
          fun add47 x = x+47;

       - and then use it in map like we did
       - we can also just create a function on the fly, what is called an anonymous function (because it doesn't have a name!)
       - the syntax is:
          fn <parameters> => <body>
       - for example, we could write the add47 function as:

          fn x => x+47;

       - We can embed this in a statement, for example:
          fun add47TOAll lst = map (fn x => x+47) lst
       
       - or even more interesting
          fun addToAll k lst = map (fn x => x+k) lst

       - or if you want to get fancy
          val addToAll k = map (fn x => x+k)

  • a better factorial
       - factorial is a function that should only be called on non-negative numbers
       - look at the badFactorial function in better_factorial.sml code
          - hopefully your code looked something like this
          - here I've had it return 0 for any negative number
             - this avoids the infinite recursion
             - but still isn't very satisfying
       - what's a better approach?
          - throw/raise an exception!

  • exceptions in SML
       - You can define a new exception in SML by adding:
          exception <name_of_exception>;

       - By convention, the name of the exception will be capitalized
       - You can then "raise" an exception anywhere in the code by adding:
          raise <name_of_exception>

       - look at the factorial function in better_factorial.sml code
          - we declared an exception called NegativeNumber
          - in the case where someone passes a number less than 0 we raise the exception
          
          > factorial 4;
          val it = 24 : int
          > factorial ~4;

          uncaught exception NegativeNumberException
           raised at: better_factorial.sml:13.15-13.29

       - I'll talk in a couple of weeks about handling exceptions (aka catching exceptions)
       
  • let (count example)
       - write a function called "count":
          - takes two parameters
             - a value
             - a list of values
          - returns a tuple containing how many times that value occurs in the list AND the list with all occurrences of the item removed as the second element
       - We could fairly easily write this as three functions (one helper function for each value and a third to put it all together)
       - I want to write it as a single recursive function.
       
       1. function header
          val count = fn: 'a -> 'a list -> (int * 'a list)

       2. recursive case
          - will we be recursing on the list or the number?
             - list
          - assume the recursive call works, what would it give us back?
             - count a (x::xs) = ??? (count a xs)
             - (count a xs)
                - how many times a occurs in xs
                - xs with all occurrence of a removed
                - as a tuple
          - if we had this information, what would we need to do?
             - just check a against x
                - if it's equal
                   - add 1 to the number of occurrences
                   - don't include x
                - it it's not equal
                   - keep the number of occurrences the same
                   - include x
          - how can we write this?
             if a = x then
                (count a xs) ???
          - since count returns a tuple, we need a way to get at the values of the tuple
          - two ways
             - less elegant way: #1 (don't do this!)
                - there are functions #1, #2, ... that get at elements in a tuple
                   > #1 (10, 20)
                   val it = 10 : int
                   > #2 (10, 20);
                   val it = 20 : int
             - a better way
                - we can use a let statement to unpack a tuple (or many other types of values!)
          - look at the count function in lets_count.sml code
             - we declare a new value in the let statement that is a tuple and call the recursive case
             - num will get the first value of the tuple and lst the second
             - we have unpacked the tuple
             - we can then use the values in the if statements below
          - this technique works on any pattern type things, e.g.
             let
                val (x::xs) = some_call_that_returns_a_nonempty_list
             in
                (* use x and xs *)
             end;