CS-A1120 Programming 2
Department of Computer Science
Aalto University
isPalindrome// Checks if String s is a palindrome. // Assumes s is only lower case and does // not contain any spaces or punctuation. def isPalindrome(s: String): Boolean = // empty or one character? if s.length <= 1 then true // start and end differ? else if s.head != s.last then false // substring palindrome? else isPalindrome(s.substring(1, s.length - 1)) end isPalindrome
One or more base cases, and the function applied to a smaller problem instance
It is common that the recursive step is only part of what we want to do, in which case it is useful to define it as an inner function.
// Checks if String s with punctuation and spaces removed // is a palindrome. Ignores case. def isPalindrome(s: String): Boolean = // Helper inner function that encapsulates the recursion. // Assumes that the string contains only lower case chars // and no spaces or punctuation. def actualRecursion(t: String): Boolean = if t.length <= 1 then true else if t.head != t.last then false else actualRecursion(t.substring(1, t.length - 1)) end actualRecursion // Remove spaces and punctuation, transform to lower case val sPlain = s.filter(_.isLetterOrDigit).map(_.toLower) // Run the actual recursion and return the result actualRecursion(sPlain) end isPalindrome
Thread.currentThread.getStackTrace will get us the stack trace as an array
// Calls itself recursively n times. // Returns a string of the stack trace as base case def rec(n: Int): String = if n == 1 then Thread.currentThread.getStackTrace.mkString("\n") else rec(n-1)+"" // Note: +"" to create need to keep state
scala> rec(4) val res0: String = java.base/java.lang.Thread .getStackTrace(Thread.java:2451) rs$line$1$.rec(rs$line$1:4) rs$line$1$.rec(rs$line$1:5) rs$line$1$.rec(rs$line$1:5) rs$line$1$.rec(rs$line$1:5) rs$line$2$.<clinit>(rs$line$2:1) rs$line$2.res0(rs$line$2) java.base/jdk.internal.reflect.DirectMethodHandleAccessor .invoke(DirectMethodHandleAccessor.java:103) java.base/java.lang.reflect.Method.invoke(Method.java:580) dotty.tools.repl.Rendering.$anonfun$4(Rendering.scala:119) scala.Option.flatMap(Option.scala:283) dotty.tools.repl.Rendering.valueOf(Rendering.scala:119) // .. (Plus many more lines left out.)
isPalindrome recursion using manual call stack
// Checks if String s with punctuation and spaces removed // is a palindrome. Ignores case. def isPalindrome(s: String): Boolean = // Helper inner function that encapsulates the recursion. // Assumes that the string contains only lower case chars // and no spaces or punctuation. def actualRecursion(t: String): Boolean = if t.length <= 1 then true else if t.head != t.last then false else actualRecursion(t.substring(1, t.length - 1)) end actualRecursion // Remove spaces and punctuation, transform to lower case val sPlain = s.filter(_.isLetterOrDigit).map(_.toLower) // Run the actual recursion and return the result actualRecursion(sPlain) end isPalindrome
(This is the same isPalindrome as before.)
"ufo tofu") to get an idea of function behaviour.Call stack:
isPalindrome("ufo tofu"): return actualRecursion("ufotofu") actualRecursion("ufotofu"): return actualRecursion("fotof") actualRecursion("fotof"): return actualRecursion("oto"): actualRecursion("oto"): return actualRecursion("t"): actualRecursion("t"): return true
s.filter(_.isLetterOrDigit).map(_.toLower) is hidden
isPalindrome, until we find a base case:isPalindrome("ufo tofu") = actualRecursion("ufo tofu".filter(_.isLetterOrDigit).map(_.toLower)) = actualRecursion("ufotofu".map(_.toLower)) = actualRecursion("ufotofu") = actualRecursion("fotof") = actualRecursion("oto") = actualRecursion("t") = true
Example: factorial function
def fact(n : Int): BigInt = require(n >= 1, "n should be a positive integer") if(n == 1) then BigInt(1) else n * fact(n-1) end fact
scala> fact(10) val res0: BigInt = 3628800
scala> fact(1000000) java.lang.StackOverflowError at fact(<console>:2) at fact(<console>:4) ...
What is going on here?
StackOverflowError
def fact(n : Int): BigInt = require(n >= 1, "n should be a positive integer") if(n == 1) then BigInt(1) else n * fact(n-1) end fact
fact(4) = (4 * fact(3)) = (4 * (3 * fact(2))) = (4 * (3 * (2 * fact(1)))) = (4 * (3 * (2 * 1))) = (4 * (3 * 2)) = (4 * 6) = 24
Scala cannot simplify in this case, and also cannot multiply values until returning from the recursion (when both values to multiply are known).
In this case, memory use is \(\mathcal{\Omega}(n)\).
def fact(n : Int) : BigInt = require(n >= 1, "n should be a positive integer") var result = BigInt(1) for i <- 2 to n do result = result * i end for result end fact
* is the last operation.
// Calls itself recursively n times. // Returns a string of the stack trace as base case def rec(n: Int): String = if n == 1 then Thread.currentThread.getStackTrace.mkString("\n") else rec(n-1)+"" // Note: +"" to create need to keep state
rec(n-1)+"" makes sure the function is not tail-recursive (+ is the tail call)
// Calls itself recursively n times. // Returns a string of the stack trace as base case def rec(n: Int): String = if n == 1 then Thread.currentThread.getStackTrace.mkString("\n") else rec(n-1) // Note: recursion is now a tail call!
rec:
scala> rec(4) val res1: String = java.base/java.lang.Thread .getStackTrace(Thread.java:2451) rs$line$3$.rec(rs$line$3:4) rs$line$4$.<clinit>(rs$line$4:1) rs$line$4.res1(rs$line$4) java.base/jdk.internal.reflect.DirectMethodHandleAccessor .invoke(DirectMethodHandleAccessor.java:103) java.base/java.lang.reflect.Method.invoke(Method.java:580) dotty.tools.repl.Rendering.$anonfun$4(Rendering.scala:119) scala.Option.flatMap(Option.scala:283) dotty.tools.repl.Rendering.valueOf(Rendering.scala:119) dotty.tools.repl.Rendering.renderVal(Rendering.scala:159) dotty.tools.repl.ReplDriver.$anonfun$7(ReplDriver.scala:421) scala.runtime.function.JProcedure1.apply(JProcedure1.java:15) scala.runtime.function.JProcedure1.apply(JProcedure1.java:10) scala.collection.immutable.List.foreach(List.scala:334) dotty.tools.repl.ReplDriver .extractAndFormatMembers$1(ReplDriver.scala:421) // ... Plus many more lines left out.
rec! The compiler has detected the tail recursive structure and removed the need for multiple stack frames.
A tail-recursive version of factorial performs the multiplication before the call:
import scala.annotation.tailrec def fact(n : Int) : BigInt = require(n >= 1, "n should be a positive integer") // Inner recursive function using two parameters: // - i keep track of the recursion 'level // - result is the value of (i-1)! @tailrec def iterate(i: Int, result: BigInt): BigInt = if i > n then result else iterate(i+1, result*i) end iterate iterate(2, BigInt(1)) end fact
The annotation @tailrec is not strictly necessary, but will help Scala know that you intend the function to be tail recursive, and the compiler will warn you in case this is not possible.
List in Scala)
T-List as a list containing elements of type T:
Nil : which denotes an empty T-list, orCons(e,l) : where e is an element of type T and l is a T-List
e the head, and l the tail of the list Cons(e,l)
Nil is an empty list []Cons(1,Nil) is the Int-List [1]Cons("c",Cons("a",Cons("b",Nil))) is the String-List ["c","a","b"]
But, as always, not one data structure for all needs. We implement linked lists to learn.
T list is
Nil or,T and the tail another T listabstract class LinkedList[T]: // List empty (Nil) // or not (Cons)? def isEmpty: Boolean // Data def head: T // Rest of the list def tail: LinkedList[T] end LinkedList
T is a type parameter Nil and Cons:
case class Nil[T]() extends LinkedList[T]: // Nil is empty def isEmpty = true // head is undefined in Nil def head = throw new java.util .NoSuchElementException("head of empty list") // tail is undefined in Nil def tail = throw new java.util .NoSuchElementException("tail of empty list") end Nil
case class Cons[T](val head: T, val tail: LinkedList[T]) extends LinkedList[T]: // Cons is not empty def isEmpty = false // Rest is created automatically // in the case class end Cons
Now we can build lists like
scala> val l = Cons(5, Cons(4, Cons(-7, Nil()))) val l: Cons[Int] = Cons(5,Cons(4,Cons(-7,Nil()))) scala> l.head val res7: Int = 5 scala> l.tail val res8: LinkedList[Int] = Cons(4,Cons(-7,Nil()))
Notice the recursive structure?
Prepending an element is easy
scala> val m = Cons(3,l) val m: Cons[Int] = Cons(3,Cons(5,Cons(4,Cons(-7,Nil())))) scala> m.head val res9: Int = 3 scala> m.tail == l val res10: Boolean = true
Note that the previous list, l, is not copied!
What are some useful methods to have in a Linked list class?
What would we like to be able to do with it?
lengthIt is always useful to find the number of elements in a sequence.
abstract class LinkedList[T]: // Previous code left out ... def length: Int = if isEmpty then 0 else 1 + tail.length end LinkedList
Is length tail recursive?
No. Evident by expansion:
Cons(1, Cons(2, Cons(3, Nil()))).length = 1 + Cons(2, Cons(3, Nil())).length = 1 + (1 + Cons(3, Nil()).length) = 1 + (1 + (1 + Nil().length)) = 1 + (1 + (1 + 0)) = 1 + (1 + 1) = 1 + 2 = 3
lengthA tail recursive version:
def length: Int = // Tail-recursive inner function, accumulating the result @tailrec def inner(remaining: LinkedList[A], result:Int): Int = if remaining.isEmpty then result else inner(remaining.tail, result+1) end inner inner(this, 0) end length
Expansion:
Cons(1, Cons(2, Cons(3, Nil()))).length = inner(Cons(1, Cons(2, Cons(3, Nil()))), 0) = inner(Cons(2, Cons(3, Nil())), 1) = inner(Cons(3, Nil()), 2) = inner(Nil(), 3) = 3
contains
Check if some element e is in the list
abstract class LinkedList[T]: // Previous code left out ... def contains(e: T): Boolean = if isEmpty then false else if head == e then true else tail.contains(e) end contains end LinkedList
abstract class LinkedList[T]: // Previous code left out ... @tailrec final def contains(e: T): Boolean = if isEmpty then false else if head == e then true else tail.contains(e) end contains end LinkedList
Note: @tailrec methods must be declared final.
ee? Else, check if the tail contains eQuestions:
contains tail recursive?contains?https://presemo.aalto.fi/prog2
contains - alternative implementationWe can do the same using pattern matching as well:
abstract class LinkedList[T]: // Previous code left out ... @tailrec final def contains(e: T): Boolean = this match case Nil() => false case Cons(h, t) => if (h == e) then true else t.contains(e) end match end contains end LinkedList
reverseHow could we go about reversing the order of a list, e.g. 1,2,3 → 3,2,1 ?
abstract class LinkedList[T]: // Previous code left out ... def reverse: LinkedList[T] = @tailrec def inner(remaining: LinkedList[T], result: LinkedList[T]): LinkedList[T] = remaining match case Nil() => result case Cons(h, t) => inner(t, Cons(h, result)) end match end inner inner(this, Nil()) end reverse end LinkedList
Another type of recursively defined data structure is the symbolic arithmetic expression.
Assume that the operations we want to be able to use are addition, subtraction, multiplication, and negation. Then we have the following recursive definition of an expression:
We use an abstract base class and case classes for the different types of expressions:
abstract class Expr: // More here soon.. end Expr /** Variable expression, like "x" or "y". */ case class Var(name: String) extends Expr: override def toString = name end Var /** Constant expression like "2.1" or "-1.0" */ case class Num(value: Double) extends Expr: override def toString = value.toString end Num /** Expression formed by multiplying two expressions, like "x * (y+3)" */ case class Multiply(left: Expr, right: Expr) extends Expr: override def toString = "(" + left + "*" + right + ")" end Multiply /** Expression formed by adding two expressions, like "x + (y*3)" */ case class Add(left: Expr, right: Expr) extends Expr: override def toString = "(" + left + " + " + right + ")" end Add /** Expression formed by subtracting an expression from another, "x - (y*3)" */ case class Subtract(left: Expr, right: Expr) extends Expr: override def toString = "(" + left + " - " + right + ")" end Subtract /** Negation of an expression, like "-((3*y)+z)" */ case class Negate(p: Expr) extends Expr: override def toString = "-" + p end Negate
scala> val e = Negate(Add(Multiply(Num(2.0), | Var("x")), | Var("y"))) | val e: Negate = -((2.0*x) + y)
toString methods in the case classes it even gets pretty-printed.Negate, because this is the outermost expression.
We can make the expressions easier to write by overriding operators in Expr:
abstract class Expr: /* Overriding these operators enable us to construct expressions with infix notation */ def +(other: Expr) = Add(this, other) def -(other: Expr) = Subtract(this, other) def *(other: Expr) = Multiply(this, other) def unary_- = Negate(this) end Expr // Case-classes as before here...
We can now do:
scala> -( Num(2.0) * Var("x") + Var("y")) val res: Negate = -((2.0*x) + y)
It would be useful to have a method to evaluate an expression. That is, given an assignment of values to all variables in an Expr return the numerical value.
Num is the valueVar is the value in the given map
abstract class Expr: // Previous code left out ... /** Custom exception for when a variable isn't assigned. */ class VariableNotAssignedException(message: String) extends java.lang.RuntimeException(message) def evaluate(p: Map[String, Double]): Double = this match case Var(n) => p.get(n) match { case Some(v) => v case None => throw new VariableNotAssignedException(n) } case Num(v) => v case Multiply(l, r) => l.evaluate(p) * r.evaluate(p) case Add(l, r) => l.evaluate(p) + r.evaluate(p) case Subtract(l, r) => l.evaluate(p) - r.evaluate(p) case Negate(t) => -t.evaluate(p) end evaluate end Expr
Evaluate -((2.0*x) + y), given \(x = 4.5\), \(y = 1.2\):
scala> val e1 = -( Num(2.0) * Var("x") + Var("y")) val e1: Negate = -((2.0*x) + y) scala> val p = Map("x" -> 4.5, "y" -> 1.2) val p: scala.collection.immutable.Map ... scala> e1.evaluate(p)
Evaluate -((2.0*x) + y), given \(x = 4.5\), \(y = 1.2\)
val e1 = -( Num(2.0) * Var("x") + Var("y")); val p = Map("x" -> 4.5, "y"->1.2))
Given as input a set \(W = \{w_1,w_2,...,w_n\}\) of \(n\) integers (the steps) and an integer \(t\) (the target), is there a subset \(S \subseteq W\) with \(\sum_{z\in S}z = t\)?
(The subset sum problem, see notes)
"consistently arrange \(x\) 'pieces' in \(y\) 'locations/steps' to reach a target"
"consistently arrange \(x\) 'pieces' in \(y\) 'locations/steps' to reach a target"
We can solve these problems by searching for a solution. Recursion is a natural way to implement exhaustive search, that considers every possible configuration one step at a time until it finds a solution (or every combination has been tried, and there are no solution).
Note: These kind of problems can be very hard to solve in general. Potentially \(\mathcal{O}(x^y)\).
Candidate solutions are built recursively and backtracking is done if we notice that the solution is not going to work.
General idea:
bsolve(config):
if config is consistent then
if config is complete then
return Success(config)
else:
for every possible action given config do
s = bsolve(config updated by action)
if s is Success then return s
else:
return Fail
end bsolve
config is a nxn Sudoku grid
var, Array, et c)