CS52 - Spring 2016 - Class 21

Example code in this lecture

   lazy_lists.sml
   basic_odometer.sml
   odometer.sml
   producer.sml

Lecture notes

  • admin
       - assignment 8

  • lazy list recap
       - what is a lazy list (look at datatype declaration in lazy_lists.sml code)?
          - generic list datatype
          - each entry is a tuple containing
             - a value
             - a *function* from () to the rest of the list

       - functions we defined last time:
          - lazyFirst
             - uses pattern matching to extra x and return it

          - lazyRest
             - uses pattern matching to extract xs
             - since xs is a function, we have to call it with () to get the rest of the list

          - countUp
             - generates a lazy list of numbers starting at k up to infinity!
             - for example:
                > val naturals = countUp 0 ()
                val naturals = LazyCons (0,fn) : int lazy_list
          
          - lazyFind
             - finds a value in a lazy list
             - I've changed the version from last time to use the option type
                - returns NONE if it doesn't find it
                - or some with the value if it finds it

          - lazyCopyFirstN

  • look at lazyMap in lazy_lists.sml code
       - what does it do?
          - same idea as the general map: apply the function to each element
       
       - key difference is that it processes and returns a lazy list
          - we use the same trick with composition we did on lazyCopyFirstN in the recursive step

  • look at lazyFilter in lazy_lists.sml code
       - what is its type signature?
          ('a -> bool) -> 'a lazy_list -> 'a lazy_list
       - what does it do?
          - applies the function to each item in the lazy_list
          - returns a new lazy list containing all of the elements where the function returns true

          > val first100 = lazyCopyFirstN 100 (countUp 0);
          val first100 = LazyCons (0,fn) : int lazy_list
          > lazyFilter (fn x => x mod 2 = 0) first100;
          val it = LazyCons (0,fn) : int lazy_list
          

  • one of the problems with lazy lists is you can't easily see what's in them
       - write a function called lazyToList that takes as input a lazy_list and returns a normal list, i.e.
          val lazyToList = fn : 'a lazy_list -> 'a list
       
       - look at lazyToList in lazy_lists.sml code

       > lazyToList (lazyFilter (fn x => x mod 2 = 0) first100);
       val it = [0,2,4,6,8,10,12,14,16,18,20,22,...] : int list

  • one other function (that we won't go over in class, but you should be able to write): lazyIndex in lazy_lists.sml code
       - returns the value at an input index in the list
          > lazyIndex 100 naturals;
          val it = 100 : int

  • roomer problem (from assignment 9)
       Baker, Cooper, Fletcher, Miller, and Smith live on different floors of an apartment house that contains only five floors. Baker does not live on the top floor. Fletcher does not live on either the top or the bottom floor. Miller lives on a higher floor than does Cooper. Smith does not live on a floor adjacent to Fletcher’s. Fletcher does not live on a floor adjacent to Cooper’s. Where does everyone live?

  • solving this problem through exhaustive search
       - if you sat down for a few minutes, you could probably solve this problem
          - what if the problem now had 20 roommates and many more constraints?
       
       - we'd like to solve this problem by exhaustive search:
          1) define the state space, i.e. a list of all possible states
             - in this case, a state is a possible assignment of a person to a floor
          2) write a test if a particular state is a solution
          3) search!
             - explore all possible states (i.e. configurations)
             - test if each state is a solution
             - keep track of all solutions found

          - this isn't an elegant solution, but it's effective in many cases!

       - exhaustive search in sml using lazy lists:
          1) generate the state space
             - define a representation (in sml) for the state
             - define a lazy list representing *all* the possible states for a problem
          2) write a test if a particular state is a solution
             - define a function that takes a state and returns true if it's a solution, false otherwise
             
          3) search!
             - filter the list of states with the test function to get the list of possible solutions

  • step 1: defining the state space
       - we want to define a lazy list of all possible states/configurations
       - how could we represent the possible states/configurations for our roomers problem?
          - 5 roommates
          - 5 floors
          - for each person, state what floor they're staying on
             - we can represent this as a list of 5 numbers where each of the numbers is a number 1-5.

          - for example:
             - Baker = first entry in the list
             - Cooper = second entry in the list
             - ...

             - what would [3, 1, 2, 4, 5] represent?
                - Baker on third floor
                - Cooper on the first
                - Fletcher on the second
                - Miller on the fourth
                - Smith on the fifth

       - now we just need a way of creating a lazy list of all possible configurations
          - how many configurations are there?
             - 5^5, non-unique (i.e. with multiple people on a floor)
             - 5! = 5*4*3*2*1 = 120, unique (i.e. one person per floor)

  • look at the odometerNextState function in basic_odometer.sml code
       - what is the type signature of this function?
          - int list -> int list
       - what does this function do?
          - what would this function give back on the following inputs?
             - [1, 1, 1, 1, 1]
             - [1, 2, 1, 1, 1]
             - [5, 4, 3, 2, 1]
             - [5, 5, 5, 5, 5]

          - the function increments the "odometer" by 1 (though with the low order bit to the left, instead of the right)
             - if the first number is less then 5, it increments it
             - if it's 5, it changes it to 1 and then tries to increment the rest of the list

             > odometerNextState [1,2,1,1,1];
             val it = [2,2,1,1,1] : int list
             > odometerNextState [1, 1, 1, 1, 1];
             val it = [2,1,1,1,1] : int list
             > odometerNextState [1, 2, 1, 1, 1];
             val it = [2,2,1,1,1] : int list
             > odometerNextState [5, 4, 3, 2, 1];
             val it = [1,5,3,2,1] : int list
             > odometerNextState [5, 5, 5, 5, 5];
             val it = [1,1,1,1,1] : int list

       - this function provides a transition from any list of the numbers 1-5 to another list!

       - how does this help us?
          - we'd like to make a lazy list using this function

  • an aside
       - last time we defined the countUp function
       fun countUp k = LazyCons(k, fn () => countUp (k+1))

       - this creates a lazy list of numbers starting at k, e.g. countUp 0 =
          0 -> 1 -> 2 -> 3 -> ...

       - Here's an even more esoteric way of writing it:

       fun addOne x = x+1;

       fun countUp k = LazyCons(k, fn () => countUp (addOne k));

  • creating the state space, i.e. the lazy list of possible configurations
       - we'd like to crate a lazy list that looks something like:
          [1, 1, 1, 1, 1] -> [2, 1, 1, 1, 1] -> [3, 1, 1, 1, 1] -> ... -> [5, 5, 5, 5, 5] -> [1, 1, 1, 1, 1] -> ...

       - we can create a function like countUp, but replace (k+1)/(addOne K) with our next state function
          fun countUp k = LazyCons(k, fn () => countUp (addOne k))

          fun countUp k = LazyCons(k, fn () => countUp (odometerNextState k))

       - look at the odometerList function in basic_odometer.sml code
          - we can create a new list of states by starting it somewhere (anywhere, in fact)
             > val od = odometerList [1, 1, 1, 1, 1];
             val od = LazyCons ([1,1,1,1,1],fn) : int list lazy_list

             - its a lazy list storing int lists

          - we can ask questions about this lazy list just like our int lazy list
             > lazyRest od;
             val it = LazyCons ([2,1,1,1,1],fn) : int list lazy_list
             > lazyToList (lazyCopyFirstN 10 od);
             val it =
              [[1,1,1,1,1],[2,1,1,1,1],[3,1,1,1,1],[4,1,1,1,1],[5,1,1,1,1],[1,2,1,1,1],
              [2,2,1,1,1],[3,2,1,1,1],[4,2,1,1,1],[5,2,1,1,1]] : int list list
             > lazyIndex 10 od;
             val it = [1,3,1,1,1] : int list

             - what index will [5, 5, 5, 5, 5] be at?
                - 5^5-1 (remember, we start counting at 0)

                > lazyIndex (5*5*5*5*5-1) od;
                val it = [5,5,5,5,5] : int list

             - what will be at index 5^5?
                > lazyIndex (5*5*5*5*5) od;
                val it = [1,1,1,1,1] : int list

                - it wraps around... it's an infinite list!

  • a non-infinite state space
       - remember our goal is to use lazyFilter to find the solutions
       - can we use it on our current list?
          - No! It's infinite and it will never finish

       - How can we detect that we're at [5, 5, 5, 5, 5] and are about to wrap around, specifically, what happens in this function call that doesn't happen in *any* other calls to odometerNextState?
          - we get to the base case

       - Idea: use the option type to indicate the end of the list
          - change the base case to return NONE instead of []
          - change the recursive case to check if NONE was returned and, if so, return NONE as well

       - look at odometerNextState in odometer.sml code
          - base case return NONE
          - recursive case, use the case statement on the recursive call
             - if NONE, return NONE
             - otherwise, return the normal answer
          - only need to do this in the case where x = 5, otherwise, we know we're not wrapping around
       
          - what is it's type signature?
             int list -> int list option

       - do we need to change our odometerList function?
          - Yes!
          - The odometerNextState returns an int list option (instead of just an int list)
          - Need to differentiate between NONE and SOME
       
       - look at odometerList function in odometer.sml code
          - if we get NONE, we end the list with LazyNil
          - otherwise, we do the recursive case as normal by pattern matching against the option type with SOME

       - now our odometer is finite!
          > lazyIndex (5*5*5*5*5 - 1) finiteOdometer;
          val it = [5,5,5,5,5] : int list
          > lazyIndex (5*5*5*5*5) finiteOdometer;

          uncaught exception LazyEmpty
           raised at: lazy_lists.sml:41.43-41.52

             - this is because we tried to call index on LazyNil
             - we can debate whether we should do this or use the option type

  • step 2: finding a solution
       - the only thing left is to define a function that checks to see if a given state/configuration is a solution
       
       - what would this function look like? what would be it's type?
          - int list -> bool
          - take in a state (i.e. a list of length 5) and return true if it meets the problem requirements, false otherwise
             
       - This is a pattern we will use A LOT
       - Rough outline
          def isSolution [b, c, f, m, s] =
             (* check all the conditions
              * better to use boolean connective, e.g. andalso than if-then-else *)
           | isSolution _ = false;

          - include a catch all at the end to keep SML from yelling at you :)

       - step 3: I've done this for the roomers problem (and you will in assignment 9)
          > lazyFilter isRoomersSolution finiteOdometer;

             - and I get back a list of the solutions!

          - I can print out all of the solutions using:

          > lazyToList (lazyFilter isRoomersSolution finiteOdometer);

  • generalizing beyond roomers
       - To generate the finiteOdometer, what are the pieces of information we needed that are problem specific:
          1) the starting state
             - e.g. for roomers, [1, 1, 1, 1, 1]
          
          2) the next state function
             - e.g. odometerNextState

          3) the solution checker
             - a function from state to bool, e.g. int list -> bool

       - the first two of these define the state space that we're searching over
       
       - look at the producer function in producer.sml code
          - looks very similar to odometerList
          - what's different?
             - has another parameter
          - what is the second parameter?
             - a nextState function!
          - what is the type signature?
             - odometerNextState was: int list -> int list option
                - int list was the state representation
             - and therefore, odometerList was
                int list option -> int list lazy_list

             - here, we can use any "state" representation:
                ('a -> 'a option) -> 'a option -> 'a lazy_list

       - using the producer function we can generate many different kinds of problem lazy_lists
          - What would the following do?
             > producer (fn k => SOME(k+1)) (SOME 0)

             - creates the natural numbers!

          - How would I create the finite odometer?
             > val finiteOdometer = producer odometerNextState (SOME [1, 1, 1, 1, 1]);
             val finiteOdometer = LazyCons ([1,1,1,1,1],fn) : int list lazy_list