01110111010110 11110101010101 00101011010011 01010111010101 01001010101010 10101010101010 Programming abstractions and analysis –– recursion 10101011110101 Mikko Kivelä 01010101011101 01010111010110 Department of Computer Science Aalto University 10101101010110 10101110101010 CS-A1120 Programming 2 11101010101101 15 April 2020 01110111010110 10111011010101 11110101010101 Lecture notes based on material created by Tommi 00010101010101 Junttila & Petteri Kaski 01011010101110 10101010100101
Contents (rounds and modules) • 1. Warmup round • I The mystery of the computer 2. Bits and data 3. Combinational logic 4. Sequential logic 5. Programmable computer • II Programming abstractions and analysis 6. Collections and functions 7. Efficiency 8. Recursion 9. Algorithms and representations of information • III Frontiers 10. Concurrency and parallelism 11. Virtualization and scalability 12. Machines that learn?
Recursion Definition: see recursion
Recursion • Recursion is a basic principle for… • replacing iteration in functional programming • defining structural objects such as data types or collections and traverse them • solving computational problems
Palindromes (reminder from O1) • Palindromes are words or sentences that read the same forwards and backwards (not including spaces and punctuation) • E.g., “testset” or “A man, a plan, a canal: Panama” • Can be defined (and checked for) recursively: A. String with zero or one characters is a palindrome B. String is a palindrome if it (1) starts and ends with the same character and (2) the substring between these is a palindrome
Palindromes (reminder from O1) def isPalindrome(s: String): Boolean = { // Helper inner function doing the actual recursive task def actualSearch(t: String): Boolean = { if (t.length <= 1) true else if (t.head != t.last) false else actualSearch(t.substring(1, t.length - 1)) } // Remove spaces and special characters val sPlain = s.filter(_.isLetterOrDigit).map(_.toLower) // Call the actual recursive search function actualSearch(sPlain) } • Animation for a similar code in Programming 1: https://plus.cs.aalto.fi/o1/2019/w12/ch01/
Textual representation of a call stack • A way of visualising what happens when executing a recursive function isPalindrome("ufo tofu"): val sPlain = "ufotofu" // We could omit this return actualSearch("ufotofu") actualSearch("ufotofu"): // tests (t.length <= 1) and (t.head != t.last) both fail return actualSearch("fotof") actualSearch("fotof"): return actualSearch("oto"): actualSearch(“oto”): return actualSearch("t"): actualSearch("t"): return true // test (t.length <= 1) succeeds • Not interesting or obvious rows such as sPlain = “ufotofu” can be left out of this illustration
Expanding • Another way of visualising recursive function calls • Computations in functions without side effects can be illustrated by “expanding” the code: isPalisPalindrome("ufo tofu") = actualSearch("ufo tofu".filter(_.isLetterOrDigit).map(_.toLower)) = actualSearch("ufotofu".map(_.toLower)) = actualSearch("ufotofu") = actualSearch("fotof") = actualSearch("oto") = actualSearch("t") = true • Again, not interesting or obvious rows can be left out
Tail calls and recursion
Recursion and the call stack • The main problem in recursion: there is only limited amount of memory for the call stack • For example, computing factorial recursively: n ! = { when n = 1 1 n × ( n − 1)! when n > 2 def fact(n : Int) : BigInt = { require(n >= 1, "n should be a positive integer") if(n == 1) BigInt(1) else n * fact(n-1) } • Running this will lead to problems: scala> fact(100000) java.lang.StackOverflowError
Recursion and the call stack • Call stack for our recursive factorial function: fact(4): val temp1 = fact(3) fact(3): val temp2 = fact(2) fact(2): val temp3 = fact(1) fact(1): return BigInt(1) // temp3 = 1 return 2*temp3 // 2*1 = 2 // temp2 = 2 return 3*temp2 // 3*2 = 6 // temp1 = 6 return 4*temp1 // 4*6 = 24 • For every call, we need to remember the value of n and calculate fact(n-1) before we can return the result ➔ Call stack will have the depth of the parameter n
Recursion and the call stack • We can compute the result by expanding the function (because it doesn’t have side effects) fact(4) = (4 * fact(3)) = (4 * (3 * fact(2))) = (4 * (3 * (2 * fact(1)))) = (4 * (3 * (2 * 1))) = (4 * (3 * 2)) = (4 * 6) = 24
Tail calls Definition Function call in a method is a tail call if it is the last operation of the function • For example, in the function def fact(n : Int) : BigInt = { require(n >= 1, "n should be a positive integer") if(n == 1) BigInt(1) else n * fact(n-1) } • BigInt(1) is a tail call • fact(n-1) is not a tail call; the last operation in the function is n * fact(n-1)
Tail recursion Definition Function is tail recursive if all of its calls to itself are tail calls • Here we only consider direct tail recursion where the tail calls do not go through some other function (e.g., def f() = …; g() and def g() = …; f() ) • Scala can optimise tail recursive functions such that they are implemented with iteration • This is because the compiler knows that no variables cannot be used after the tail call and the the call frame can be released
Tail recursion • Lets make a tail recursive version of the factorial function (by using tail recursive inner function): def fact(n: Int): BigInt = { require(n >= 1, "n should be a positive integer") def iterate(i: Int, result: BigInt): BigInt = { if(i == 0) result else iterate(i - 1, result * i) } iterate(n, BigInt(1)) } • Note the similarity with the iterative version: def fact(n : Int) : BigInt = { require(n >= 1, "n should be a positive integer") var result = BigInt(1) for(i <- n to 1 by -1) result = result * i result }
Tail recursion • Expansion of the tail recursive fact function: fact(4) = iterate(4, 1) = iterate(3, 4) = iterate(2, 12) = iterate(1, 24) = iterate(0, 24) = 24 • Same with call stack: the call frame of iterate is reused: fact(4): return iterate(4, 1) iterate(4, 1): return iterate(3, 4) iterate(3, 4): return iterate(2, 12) iterate(2, 12): return iterate(1, 24) iterate(0, 24): return 24
@tailrec annotation • The @tailrec annotation in Scala declares that the compiler must optimise tail recursion into iteration import scala.annotation.tailrec ... @tailrec def myFunction(...) { ... } • If the annotated function is not tail recursive, the compiler will issue an error • Only direct tail calls are allowed • It must not be possible to override the function: it must be an inner function or declared as final method For more information, see: http://theyougen.blogspot.com/2010/01/why-overrideable-tail-recursive-methods.html
Recursively defined data structures
Recursive data structures • In addition to functions, data structure can be recursive • They are most naturally manipulated with recursive functions • Next we go through two examples: 1. Linked lists (similar to List in Scala) 2. Symbolic arithmetic expressions (example of more general tree data structure)
Linked lists
Linked lists • We consider lists containing elements of type T • Let define such T -lists recursively: 1. Nil is the empty T -list 2. If l is a T -list and e is an element of type T , then Cons(e, l) is a T -list where e is the first element followed by the elements of the list l Examples: • Nil is an empty Int -list [] • Cons(1,Nil) is an Int -list [1] • Cons(1,Cons(3,Cons(5,Nil))) is an Int -list [1,3,5] • Cons(1,Cons(Nil,Cons(5,Nil))) is not an Int - list
Linked lists • For a list Cons(e, l) • We say that: • e is the head of the list • l is the tail of the list • Empty list does not have head or tail Examples: • In Int -list Cons(5, Nil) the head is 5 and tail Nil • In String -list Cons(“first”, Cons(“second”,Nil)) the head is “ first ” and tail Cons(“second”,Nil)
Linked lists, the first implementation abstract class LinkedList[A] { def isEmpty: Boolean def head: A def tail: LinkedList[A] } class Nil[A]() extends LinkedList[A] { def isEmpty = true def head = throw new java.util.NoSuchElementException("head of empty list") def tail = throw new java.util.NoSuchElementException("tail of empty list") } class Cons[A](val head: A, val tail: LinkedList[A]) extends LinkedList[A] { def isEmpty = false } object Nil { def apply[A]() = new Nil[A]() } object Cons { def apply[A](head : A, tail : LinkedList[A]) = new Cons(head, tail) }
Linked lists, the first implementation • We can now write: val l = Cons(5, Cons(4, Cons(-7, Nil()))) • … and these objects will be created in the memory: l Cons Cons Cons Nil head tail head tail head tail 5 4 − 7
Recommend
More recommend