The Art of Recursion: Problem Set 10

Similar documents
Computer Science 210 Data Structures Siena College Fall Topic Notes: Trees

Summer 2017 Discussion 10: July 25, Introduction. 2 Primitives and Define

The fringe of a binary tree are the values in left-to-right order. For example, the fringe of the following tree:

SCHEME 7. 1 Introduction. 2 Primitives COMPUTER SCIENCE 61A. October 29, 2015

SCHEME 8. 1 Introduction. 2 Primitives COMPUTER SCIENCE 61A. March 23, 2017

CMPSCI 250: Introduction to Computation. Lecture #14: Induction and Recursion (Still More Induction) David Mix Barrington 14 March 2013

Recursion. Comp Sci 1575 Data Structures. Introduction. Simple examples. The call stack. Types of recursion. Recursive programming

Class 6: Efficiency in Scheme

Programming Paradigms

CS422 - Programming Language Design

SCHEME The Scheme Interpreter. 2 Primitives COMPUTER SCIENCE 61A. October 29th, 2012

Project Compiler. CS031 TA Help Session November 28, 2011

CSCC24 Functional Programming Scheme Part 2

Trees. Chapter 6. strings. 3 Both position and Enumerator are similar in concept to C++ iterators, although the details are quite different.

CS 3410 Ch 7 Recursion

1 Lower bound on runtime of comparison sorts

CS 171: Introduction to Computer Science II. Binary Search Trees

More About Recursive Data Types

V Advanced Data Structures

Lecture Notes on Binary Decision Diagrams

Spring 2018 Discussion 7: March 21, Introduction. 2 Primitives

Lecture 12: Dynamic Programming Part 1 10:00 AM, Feb 21, 2018

Fall 2017 Discussion 7: October 25, 2017 Solutions. 1 Introduction. 2 Primitives

Tail Recursion. ;; a recursive program for factorial (define fact (lambda (m) ;; m is non-negative (if (= m 0) 1 (* m (fact (- m 1))))))

Section 05: Solutions

Programming Languages 3. Definition and Proof by Induction

V Advanced Data Structures

Fall 2018 Discussion 8: October 24, 2018 Solutions. 1 Introduction. 2 Primitives

List Functions, and Higher-Order Functions

CS24 Week 8 Lecture 1

Declarative concurrency. March 3, 2014

CSE100. Advanced Data Structures. Lecture 12. (Based on Paul Kube course materials)

Recursion versus Iteration

Part I Basic Concepts 1

Typed Scheme: Scheme with Static Types

BST Deletion. First, we need to find the value which is easy because we can just use the method we developed for BST_Search.

Compilers CS S-08 Code Generation

CS61A Notes 02b Fake Plastic Trees. 2. (cons ((1 a) (2 o)) (3 g)) 3. (list ((1 a) (2 o)) (3 g)) 4. (append ((1 a) (2 o)) (3 g))

HIGHER-ORDER FUNCTIONS

EECE.3170: Microprocessor Systems Design I Summer 2017 Homework 4 Solution

Last week. Another example. More recursive examples. How about these functions? Recursive programs. CSC148 Intro. to Computer Science

Hints for Exercise 4: Recursion

CS450 - Structure of Higher Level Languages

Recursion and Structural Induction

Haskell: From Basic to Advanced. Part 3 A Deeper Look into Laziness

CS2 Algorithms and Data Structures Note 6

SCHEME 10 COMPUTER SCIENCE 61A. July 26, Warm Up: Conditional Expressions. 1. What does Scheme print? scm> (if (or #t (/ 1 0)) 1 (/ 1 0))

Programming Languages. Thunks, Laziness, Streams, Memoization. Adapted from Dan Grossman s PL class, U. of Washington

Binary Trees Due Sunday March 16, 2014

Recursion(int day){return Recursion(day += 1);} Comp Sci 1575 Data Structures. Recursive design. Convert loops to recursion

Programming Languages. Streams Wrapup, Memoization, Type Systems, and Some Monty Python

Intro. Scheme Basics. scm> 5 5. scm>

Recurrences and Memoization: The Fibonacci Sequence

CMSC 341 Lecture 10 Binary Search Trees

CS Introduction to Data Structures How to Parse Arithmetic Expressions

Continuations and Continuation-Passing Style

Computer Science E-119 Fall Problem Set 4. Due prior to lecture on Wednesday, November 28

Computing Fundamentals Advanced functions & Recursion

Discussion 11. Streams

Binary Trees. BSTs. For example: Jargon: Data Structures & Algorithms. root node. level: internal node. edge.

CMSC 341 Leftist Heaps

Advanced Programming Handout 6. Purely Functional Data Structures: A Case Study in Functional Programming

Assignment 11: functions, calling conventions, and the stack

Chapter 5: Recursion

These notes are intended exclusively for the personal usage of the students of CS352 at Cal Poly Pomona. Any other usage is prohibited without

Recitation 9. Prelim Review

INTRODUCTION TO HASKELL

Binary Trees

Streams and Evalutation Strategies

The Worker/Wrapper Transformation

Lecture 8: Iterators and More Mutation

Homework 3: Recursion Due: 11:59 PM, Sep 25, 2018

Functional Programming in Haskell for A level teachers

CIS 194: Homework 6. Due Monday, February 25. Fibonacci numbers

CSC324 Functional Programming Efficiency Issues, Parameter Lists

Think of drawing/diagramming editors. ECE450 Software Engineering II. The problem. The Composite pattern

INTERPRETERS 8. 1 Calculator COMPUTER SCIENCE 61A. November 3, 2016

Functional Programming. Overview. Topics. Definition n-th Fibonacci Number. Graph

Summer Final Exam Review Session August 5, 2009

CSE413: Programming Languages and Implementation Racket structs Implementing languages with interpreters Implementing closures

Functional Programming for Imperative Programmers

15-122: Principles of Imperative Computation, Fall 2015

Principles of Programming Languages Topic: Functional Programming Professor L. Thorne McCarty Spring 2003

DRAWING AND UNDERSTANDING RECURSIVE FUNCTIONS *

Recursion. Tjark Weber. Functional Programming 1. Based on notes by Sven-Olof Nyström. Tjark Weber (UU) Recursion 1 / 37

Chapter 5: Recursion. Objectives

Computer Science 210 Data Structures Siena College Fall Topic Notes: Priority Queues and Heaps

Forward recursion. CS 321 Programming Languages. Functions calls and the stack. Functions calls and the stack

Tree traversals and binary trees

Data structures. Priority queues, binary heaps. Dr. Alex Gerdes DIT961 - VT 2018

Exercise 1 ( = 24 points)

CSE 2123 Recursion. Jeremy Morris

CS61A Summer 2010 George Wang, Jonathan Kotker, Seshadri Mahalingam, Eric Tzeng, Steven Tang

A brief tour of history

9/10/2016. Time for Some Detailed Examples. ECE 120: Introduction to Computing. Let s See How This Loop Works. One Statement/Step at a Time

Parsing Scheme (+ (* 2 3) 1) * 1

CSE 332: Data Structures & Parallelism Lecture 7: Dictionaries; Binary Search Trees. Ruth Anderson Winter 2019

Lecture 6 CS2110 Spring 2013 RECURSION

1. O(log n), because at worst you will only need to downheap the height of the heap.

CIS 194: Homework 3. Due Wednesday, February 11, Interpreters. Meet SImPL

Transcription:

The Art of Recursion: Problem Set Due Tuesday, November Tail recursion A recursive function is tail recursive if every recursive call is in tail position that is, the result of the recursive call is immediately the result of the whole function; there s no extra work that needs to be done after the recursive call returns. For example, fact = fact n = n * fact (n-) is not tail recursive, because after the recursive call to fact (n-) completes, fact n isn t done: a multiplication by n still has to take place. We can make a tail-recursive variant of the factorial function by adding an extra accumulating parameter which accumulates the result as we descend through the recursive calls; when n reaches we return the accumulated value. facttr r = r facttr r n = facttr (n*r) (n-). How can the original fact be defined in terms of facttr?. Write a tail-recursive version of reverse, where reverse [] = [] reverse (x:xs) = reverse xs [x] Note that I don t care whether you derive it in some principled way from the implementation of reverse. I am just looking for any function which computes the same thing as reverse and is implemented using tail recursion.. Write a tail-recursive version of the usual Fibonacci function. (Again, it doesn t matter how you come up with it; any tailrecursive Fibonacci function will do.) The important point about tail recursion is that any tail-recursive function can easily be implemented iteratively. Since at every recursive call control simply passes to the recursive invocation and does not need to return, it can be implemented just by jumping to the top of a loop. And, as usual, in the iterative version we have one mutable variable corresponding to each argument to the tail-recursive function.

the art of recursion: problem set 4. Turn the definitions of facttr and your tail-recursive Fibonacci function into iterative, imperative programs (using whatever language you like, or pseudocode). Continuation-passing style If we can convert any recursive function into an equivalent tailrecursive function, we can then compile any recursive function into iterative code. However, it is not clear whether this is always possible. The tail-recursive variants seen above have been rather ad-hoc. What we need are more principled methods for deriving tail-recursive variants of arbitrary recursive functions. One such principled method is continuation passing style (or CPS for short). The idea is that instead of returning a result directly, we take an extra argument, called a continuation, which specifies what to do next : we pass the result to this function instead of returning it. For example, here is the usual factorial function again: fact = fact n = n * fact (n-) And here is factorial written using continuation-passing style: factcps k = k factcps k n = factcps (k. (\r -> n*r)) (n-) In the case that n is, we know the answer should be, but instead of returning directly, we pass it to the continuation k. In the general case, when the argument to fact is n, we call fact with n- and specify what needs to happen to the result: first, it needs to be multiplied by n, and then that result should be passed to k. CPS can be tricky to wrap your head around at first; study the above example until you are sure you understand what it is doing! 5. How can we define the original fact in terms of factcps? 6. Write a tail-recursive version of map using CPS, where map :: (a -> b) -> [a] -> [b] map _ [] = [] map f (x:xs) = f x : map f xs 7. Write a tail-recursive version of the naive Fibonacci function using CPS. (The point is that the naive version can mechanically be Once you understand how it works... made tail-recursive using CPS, without having any special flashes of insight about efficiency, memoization, etc..)

the art of recursion: problem set Dissecting call trees CPS is a general method for turning any recursive function into a tail-recursive variant. However, in order to take a tail-recursive function using CPS and compile it to iterative code, we have to figure out how to compile lambdas. It can be done, and in fact, many compilers for functional languages work this way. However, in some cases we can do something easier. Consider expanding out a recursive function call into a complete call tree, defined as the tree with one node for each recursive invocation of the function, where the children of a given node are all the recursive calls made by that node. We label each node with the argument(s) to that recursive call, and, just to help keep track of what is going on, we also decorate internal nodes with whatever computation needs to happen with the recursive results, and leaves with their values. For example, the figure to the right shows the call tree for fact 5 (where fact is the standard implementation of the factorial function), and the figure below shows the call tree for the standard naïve implementation of the Fibonacci function, also at the argument 5. If you want to know more, look up closure conversion. For example, see http://matt.might.net/articles/ closure-conversion. For sufficiently large values of easy. 5 4 5 4 5 4 Observe that evaluating a recursive function is equivalent to doing a traversal of its call tree. So, to implement it iteratively (i.e. tailrecursively), we just need to keep track of enough information during the traversal so that we know where we are in the tree and what to do next at each step of the iteration. As a simple first example, consider the factorial function fact. At each step of the traversal, there will be a current node, but we

the art of recursion: problem set 4 need to remember whether we have just recursed down the tree to reach the current node, or whether we have just returned up the tree after completing a recursive subcall. If we re going down, we need to know what the input to the current node is; if we re coming back up, we need to know what the output from the child node was. We can keep track of this information with the Direction type: data Direction i o where Down :: i -> Direction i o Up :: o -> Direction i o In fact, Direction is not specific to fact; this same pattern applies to any recursive call tree. We may also need to keep track of some extra information about the nodes we ve visited; this part is specific to each recursive function. In the case of fact, we need to remember what number to multiply by once we re done with the recursive calls. We ll use the FactNode type for this, which consists of just a single constructor storing an Integer. 4 data FactNode where -- what to multiply by after finishing the recursion FN :: Integer -> FactNode 4 In this case we could just use Integer directly instead of going to the trouble of making a new FactNode type; the point is to make it easier to see how to generalize to more complex examples. At any given point in the traversal we need to remember information about all the nodes above us, so we will actually use a stack of FactNodes (which we can represent in Haskell using a list). Finally, fact s input is the same type as its output; to better distinguish them (and to prevent confusion when you later generalize to examples where the input and output types are not the same), let s create two different type synonyms: type FactIn = Integer type FactOut = Integer Now we re ready to write the traversal itself. The factt function takes a Direction (indicating whether we re going down or up the tree, and containing either an input or an output) and a stack of node information, and ultimately produces the result of the entire fact computation. factt :: Direction FactIn FactOut -> [FactNode] -> FactOut First, if we re traveling down and see an input of, we ve reached the base case of the recursion (i.e. a leaf of the call tree), so we can immediately return and begin traveling back up the stack. factt (Down ) stack = factt (Up ) stack

the art of recursion: problem set 5 Otherwise, if we re traveling down and see an input of n, we continue traveling down with an input of n-, but push another node onto the stack in order to remember that when we get back to this node we need to multiply by n. factt (Down n) stack = factt (Down (n-)) (FN n : stack) When traveling up the tree, we look at the top of the stack, multiply by the number we find there, and keep traveling up (popping the stack). factt (Up res) (FN n : stack) = factt (Up (n * res)) stack When we run out of nodes on the stack, that means we re back at the top of the tree, i.e. we re done with the traversal, so we return the final result. factt (Up res) [] = res And that s it. Of course this version is quite a bit more complicated than the original! But as you can easily verify, factt is completely tail-recursive; and unlike the CPS version, it is completely first-order, that is, it does not require passing any functions as arguments. 5 It is therefore quite straightforward to translate factt into a loop in an imperative language: we just need a variable for the Direction (in practice we might use a boolean flag to indicate the direction, another variable to store the input, and a third to store the output) and another variable storing the stack of integers. Let s do one more example, this time the naïve fib function. This is a bit more complex than fact because we have something more interesting to remember at each node. In particular, since each nonleaf node has two children (i.e. makes two recursive calls), we have to remember which of the two subtrees we are currently traversing. Furthermore, when we are working on the left subtree, we have to also remember what the input to the recursive call at the root of the right subtree will be; when working on the right subtree, we have to also remember what the output from the left subtree was. The FibNode type encapsulates this idea: FibL indicates we are currently traversing the left subtree, and stores the input for the right; FibR means we are currently traversing the right subtree, and stores the output from the left. 5 The CPS version and this version are actually related: in a sense we ve simply recorded the different possible types of continuations as a data structure. type FibIn = Integer type FibOut = Integer data FibNode where FibL :: FibIn -> FibNode FibR :: FibOut -> FibNode

the art of recursion: problem set 6 And now for the traversal. First, when we reach a leaf node (that is, we see a or input when traveling down), we turn around and go back up the tree with the answer. fibt :: Direction FibIn FibOut -> [FibNode] -> Integer fibt (Down ) stack = fibt (Up ) stack fibt (Down ) stack = fibt (Up ) stack In general, when traveling down and we see input n, we go down the left subtree with input n-; we remember that we went down the left subtree, and that the input to the right subtree will be n-, by pushing FibL (n-) on the stack. fibt (Down n) stack = fibt (Down (n-)) (FibL (n-) : stack) When traveling up, if we see that we have just completed traversing a left subtree, we turn around and go down the right subtree, using the stored input. We replace the FibL node with a FibR node to mark the fact that we re working on the right subtree now, and to store the result res from the left subtree. fibt (Up res) (FibL n : stack) = fibt (Down n) (FibR res : stack) If we re coming up and see that we just completed a right subtree, then we add the stored result from the left subtree with the current result and continue up. fibt (Up res) (FibR res : stack) = fibt (Up (res res)) Finally, when there are no nodes left on the stack, we re done. stack fibt (Up res) [] = res 8. How can we define the original fib in terms of fibt? 9. Recall the h function, defined by h :: Integer -> Integer h = h n odd n = h (n div ) even n = h (n div ) h ((n div ) - ) Implement a tail-recursive version of h.. Consider the usual type of binary trees with data at internal nodes, and the standard fold over such trees. Implement a tailrecursive version of foldbtree. data BTree a = Empty Branch (BTree a) a (BTree a) foldbtree :: c -> (c -> a -> c -> c) -> BTree a -> c foldbtree e _ Empty = e foldbtree e b (Branch l a r) = b (foldbtree e b l) a (foldbtree e b r)