COP 3223H meeting -*- Outline -*- * flat recursion over (Lisp) lists (Little Schemer ch. 2-4, EOPL 1.2.1) advertisement: this may seem to be just for a special kind of lists at first, but also applies to languages (grammars), and other recursive data (like windows, files/directories, ...) why? recursion is a fundamental way to process larger amounts of data Recursion is also used extensively in grammars for programming languages (and other data); matching recursion produces a "well-structured" program, that is easy to write and understand ** inductive definition of lists ------------------------------------------ INDUCTIVE DEFINITION OF LISP LISTS def: Let t be a type of data. Then a list(t) is - either - or Grammar: ------------------------------------------ ... Nil() ... Cons(v, lst) where v:t and lst:list(t) ... ::= Nil() | Cons( , ) ** correspondence between grammar and classes in Python ------------------------------------------ CORRESPONDENCE BETWEEN GRAMMARS AND CLASSES Grammars and Sentences|Classes and Objects ======================|=================== Nonterminal Sentence Alternative Example Grammar: ::= Nil() | Cons( , ) Corresponding Classes: import abc class LispList(abc.ABC): pass class Nil(LispList): def __init__(self): pass ... class Cons(LispList): def __init__(self, hd, tl) self.car = hd self.cdr = tl ... Corresponding instances: ------------------------------------------ ... class ... instance (or object) ... subclass ... Nil() Cons(1, Nil()) Cons(1, Cons(2, Nil())) ------------------------------------------ PICTURES OF LISTS Pictures of Lists: lst = Cons(1, Cons(2, Nil())) lst [ * ] / v [Cons | * | *-]-> [Cons | * | *-]-> [Nil] | | v v 1 2 ------------------------------------------ *** the details of LispList See the LispList.py file in ../../docs/ or in the "Following the Grammar with Python" handout for details of LispList ------------------------------------------ # $Id: LispList.py,v 1.4 2017/02/05 22:21:24 leavens Exp $ import abc class LispList(abc.ABC): pass class Nil(LispList): def __init__(self): """Initialize this empty list""" pass def __eq__(self, lst): """Return True just when lst is also an instance of Nil.""" return isinstance(lst, Nil) def __repr__(self): """Return a string representing this Nil instance.""" return "Nil()" def __str__(self): """Return a string showing the elements of self.""" return "[]" def isEmpty(self): """Return whether this list is empty.""" return True class Cons(LispList): def __init__(self, hd, tl): """Initialize this Cons with head hd and tail tl.""" self.car = hd self.cdr = tl def __eq__(self, lst): """Return True just when self is structurally equivalent to lst.""" return isinstance(lst, Cons) and lst.first() == self.first() \ and lst.tail() == self.tail() def __repr__(self): """Return a string representing this list.""" return "Cons(" + repr(self.first()) + ", " + repr(self.tail()) + ")" def elements_str(self): """Return a string of the elements of self, separated by commas.""" if self.tail().isEmpty(): return str(self.first()) else: return str(self.first()) + ", " + self.tail().elements_str() def __str__(self): """Return a string showing the elements of self.""" return "[" + self.elements_str() + "]" def isEmpty(self): """Return whether this list is empty.""" return False def first(self): """Return the first element of this list.""" return self.car def tail(self): """Return the rest of this list.""" return self.cdr ------------------------------------------ Q: How does the __eq__() method work? it's a simultaneous recursion Play with LispLists in the interpreter Q: What are equations for .isEmpty()? Nil().isEmpty() == True Cons(e, lst).isEmpty() == False Q: What equation relates Cons, .first() and .tail()? if not lst.isEmpty(): Cons(lst.first(), lst.tail()) == lst *** consequences of LispList's design ------------------------------------------ LispList Methods Methods that work on all LispList objects: .isEmpty() __eq__ (i.e., == ) __repr__ (i.e., repr()) __str__ (i.e., str()) Other methods that work on Cons objects: .first() .tail() ------------------------------------------ ------------------------------------------ CONSEQUENCES - Must test with - In a Cons must use - When building a LispList, with Cons, ------------------------------------------ ... .isEmpty() to decide between Nil and Cons ... .first() to get the first element .tail() to work on the rest of the list ... can only add an element at the head/front So LispLists are like stacks, can only access the top element. pragmatics: - Cons executes in constant time and space - .first() and .tail() are fast ** tips for avoiding taking to long to write recursive functions warning: be systematic! I'm trying to give you another way to think; the usual imperative way (try something and debug) wastes lots of time (hours for 4 line program). You won't get the right answer quickly by tinkering and debugging. ------------------------------------------ TIPS FOR WRITING EXPLICIT RECURSIONS 1. Understand the problem a. What does it do? b. What's the type? c. What are the grammars for the arguments? d. What are examples for each case in the interesting grammars? e. What are related simpler examples? 2. Write an outline that follows the grammar 3. Fill in the outline using the examples - generalize from the examples - create new examples if needed 4. Use one function per nonterminal 5. Debug by testing larger and larger expressions ------------------------------------------ *** tips by example ------------------------------------------ AN EXAMPLE RECURSIVE PROBLEM Write a function, sumSquares(lst), with type: LispList(int) -> int that takes a LispList of numbers and returns the sum of the squares of those numbers. a. What does it do? b. What's the type? c. What's the grammar for LispList(int)? d. What are some examples? e. What are some related simpler examples? ------------------------------------------ ... it returns the sum of the squares of the elements in the list. ... LispList(int) -> int ... sumSquares(Nil()) == 0 sumSquares(Cons(5, Cons(10, Nil()))) == 25 + 100 == 125 sumSquares(Cons(3, Cons(2, Cons(2, Nil())))) == 17 ... sumSquares(Cons(2, Cons(2, Nil()))) == 8 sumSquares(Cons(2, Nil())) == 4 *** follow the grammar ------------------------------------------ FOLLOW THE GRAMMAR! 2. Use an outline that matches the grammar for a (recursive) argument's data type. ::= Nil() | Cons(, ) ------------------------------------------ def sumSquares(lon): """type: LispList(int) -> int""" if lon.isEmpty(): return ____ else: return ____________ sumSquares(lon.tail()) *** fill in by generalizing examples ------------------------------------------ FILL IN BY GENERALIZING FROM EXAMPLES 3. Fill in the outline by generalizing the examples a. What's returned in the base case(s)? sumSquares(Nil()) == b. How do we get the answer from - recursive call's answer, and - other information? sumSquares(Cons(3, Cons(2, Cons(2, Nil())))) == 17 _______________________________ __ lon sumSquares(lon) sumSquares(Cons(2, Cons(2, Nil()))) == 8 ______________________ _ lon.tail() sumSquares(lon.tail()) ------------------------------------------ ... 0 Use the related simpler example to ask how, generalize *** when debugging, work bottom up ------------------------------------------ MORE TIPS 4. When debugging, work bottom up from subexpressions lon = Cons(3, Cons(2, Cons(2, Nil()))) lon.tail() lon.first() lon.first()**2 ... If you need to use a recursive call when debugging, use its value instead lon.first()**2 + 9 ------------------------------------------ Don't try things randomly when debugging, use a strategy! ** deriving recursive programs from examples *** example ------------------------------------------ DERIVATION FROM RELATED EXAMPLES Write a function sum2lists(ls1, ls2) that takes two LispLists of the same length and returns a list of the sums of the corresponding elements of each list. Examples: ls342 = Cons(3, Cons(4, Cons(2, Nil()))) ls597 = Cons(5, Cons(9, Cons(7, Nil()))) assert sum2lists(ls342, ls597) \ == Cons(8, Cons(13, Cons(9, Nil()))) assert sum2lists(ls342.tail(), ls597.tail()) \ == Cons(13, Cons(9, Nil())) assert sum2lists(Cons(2, Nil()), Cons(7, Nil())) \ == Cons(9, Nil()) assert sum2lists(Nil(), Nil() == Nil() ------------------------------------------ Q: What's a related simpler example for the second example? the third one Q: What's the type? type: (LispList(int),LispList(int)) -> LispList(int) Q: What are the cases? What's the outline? Q: What is the answer for the base cases? Q: How to decompose? Where is the recursion? Q: How do we get from the answer for the recursion to what we want? look at the related example, generalize. Point out: - basis - the "one step" - the "rest of the journey" - combining the one step with the rest of the journey *** trace ------------------------------------------ HOW IT WORKS sum2lists(ls342, ls597) == if ls342.isEmpty(): return Nil() else: return Cons(ls342.first() + ls597.first(), \ sum2lists(ls342.tail(), ls597.tail())) == Cons(ls342.first() + ls597.first(), \ sum2lists(ls342.tail(), ls597.tail())) == Cons(3 + 5, sum2lists(Cons(4, Cons(2, Nil())), \ Cons(9, Cons(7, Nil())))) == Cons(8, sum2lists(Cons(4, Cons(2, Nil())), \ Cons(9, Cons(7, Nil())))) == Cons(8, Cons(4 + 9, sum2lists(Cons(2, Nil())) Cons(7, Nil())) == Cons(8, Cons(13, Cons(2+7, sum2lists(Nil(), Nil())))) == Cons(8, Cons(13, Cons(9, sum2lists(Nil(), Nil())))) == Cons(8, Cons(13, Cons(9, Nil()))) ------------------------------------------ go over the first substitution carefully, then faster *** try it ------------------------------------------ FOR YOU TO DO Write a function xerox(lst), of type LispList(int) -> LispList(int) that duplicates each element in lst. For example: xerox(Cons(3, Cons(9, Cons(2, Nil())))) \ == Cons(3, Cons(3, Cons(9, Cons(9, Cons(2, Cons(2, Nil())))))) xerox(Cons(9, Cons(2, Nil()))) \ == Cons(9, Cons(9, Cons(2, Cons(2, Nil())))) xerox(Nil()) == Nil() ------------------------------------------ Q: What's a related example to the first example? the second one Q: What's the type? LispList(int) -> LispList(int) Q: How do you make [3, 3, 9, 9, 2, 2] from [3, 9, 2] and [9, 9, 2, 2]? use (lambda lst: Cons(3, lst)) twice ** more complex cases *** give yourself examples to cover all cases ------------------------------------------ GIVE YOURSELF EXAMPLES TO COVER ALL CASES Write a function insertBefore(where, what, lst) that takes a LispList(t) and two t values where and what and returns a copy of lst with what inserted just before the first occurrence of where, if any. Examples: insertBefore(7, 3, Cons(7, Cons(7, Nil()))) \ == Cons(3, Cons(7, Cons(7, Nil()))) insertBefore(5, 2, Cons(1, Cons(2, Cons(3, Nil())))) \ == Cons(1, Cons(5, Cons(2, Cons(3, Nil())))) insertBefore(5, 2, Cons(3, Nil()) == Cons(3, Nil()) ------------------------------------------ Q: What examples are we missing? The base case related simpler examples Q: Which argument has an inductively-specified type? so the outline, which gives partial credit, is 2 cases, with a test for the first also in the 2nd case, will recurse on cdr if need be because that's where the grammar is recursive in the second case there are also 2 cases but those are not the context-free cases. finish writing this, by working with the examples... summary: it's the input data that determines the overall structure. *** example for you ------------------------------------------ FOR YOU TO DO (IN PAIRS) Write a function insertBeforeAll(where, what, lst) that takes a LispList(t) and two t values where and what and returns a copy of lst with what inserted just before the first occurrence of where, if any. nil = Nil() insertBeforeAll(9, 5, nil) == nil insertBeforeAll(6, 7, Cons(6, Cons(1, Cons(2, nil)))) insertBeforeAll(6, 7, Cons(5, Cons(6, Cons(1, Cons(2, nil))))) Any other examples you want? What's the type? What's the structure of the code? Write it! ------------------------------------------ Q: How does insertBeforeAll differ from insertBefore?