diff -r a623dd1f2898 -r e03a0100ec46 progs/lecture4.scala --- a/progs/lecture4.scala Mon Nov 06 21:49:55 2023 +0000 +++ b/progs/lecture4.scala Fri Dec 08 00:54:36 2023 +0000 @@ -1,38 +1,227 @@ // Scala Lecture 4 //================= +// pattern-matching // tail-recursion // polymorphic types -// implicits + + + +// Pattern Matching +//================== + +// A powerful tool which has even landed in Java during +// the last few years (https://inside.java/2021/06/13/podcast-017/). +// ...Scala already has it for many years and the concept is +// older than your friendly lecturer, that is stone old ;o) -import scala.annotation.tailrec +// The general schema: +// +// expression match { +// case pattern1 => expression1 +// case pattern2 => expression2 +// ... +// case patternN => expressionN +// } + + +// recall +def len(xs: List[Int]) : Int = { + if (xs == Nil) 0 + else 1 + len(xs.tail) +} -def fact(n: BigInt): BigInt = - if (n == 0) 1 else n * fact(n - 1) - - -def factT(n: BigInt, acc: BigInt): BigInt = - if (n == 0) acc else factT(n - 1, n * acc) +def len(xs: List[Int]) : Int = xs match { + case Nil => 0 + case _::xs => 1 + len(xs) +} + +len(Nil) +len(List(1,2,3,4)) + + +List(1,2,3,4).map(x => x * x) + +def my_map_int(lst: List[Int], f: Int => Int) : List[Int] = + lst match { + case Nil => Nil + case foo::xs => f(foo) :: my_map_int(xs, f) + } + +def my_map_option(opt: Option[Int], f: Int => Int) : Option[Int] = + opt match { + case None => None + case Some(x) => { + Some(f(x)) + } + } + +my_map_option(None, x => x * x) +my_map_option(Some(8), x => x * x) -println(factT(1000000), 1)) +// you can also have cases combined +def season(month: String) : String = month match { + case "March" | "April" | "May" => "It's spring" + case "June" | "July" | "August" => "It's summer" + case "September" | "October" | "November" => "It's autumn" + case "December" => "It's winter" + case "January" | "February" => "It's unfortunately winter" + case _ => "Wrong month" +} + +// pattern-match on integers + +def fib(n: Int) : Int = n match { + case 0 | 1 => 1 + case _ => fib(n - 1) + fib(n - 2) +} + +fib(10) + +// pattern-match on results + +// Silly: fizz buzz +def fizz_buzz(n: Int) : String = (n % 3, n % 5) match { + case (0, 0) => "fizz buzz" + case (0, _) => "fizz" + case (_, 0) => "buzz" + case _ => n.toString +} + +for (n <- 1 to 20) + println(fizz_buzz(n)) + +// guards in pattern-matching + +def foo(xs: List[Int]) : String = xs match { + case Nil => s"this list is empty" + case x :: xs if x % 2 == 0 + => s"the first elemnt is even" + case x if len(x) == + => s"this list has exactly two elements" + case x :: y :: rest if x == y + => s"this has two elemnts that are the same" + case hd :: tl => s"this list is standard $hd::$tl" +} + +foo(Nil) +foo(List(1,2,3)) +foo(List(1,1)) +foo(List(1,1,2,3)) +foo(List(2,2,2,3)) + + + + +abstract class Colour +case object Red extends Colour +case object Green extends Colour +case object Blue extends Colour +case object Yellow extends Colour + + +def fav_colour(c: Colour) : Boolean = c match { + case Green => true + case Red => true + case _ => false +} + +fav_colour(Blue) -def foo[A](args: List[A]) = ??? +// ... a tiny bit more useful: Roman Numerals + +sealed abstract class RomanDigit +case object I extends RomanDigit +case object V extends RomanDigit +case object X extends RomanDigit +case object L extends RomanDigit +case object C extends RomanDigit +case object D extends RomanDigit +case object M extends RomanDigit + +type RomanNumeral = List[RomanDigit] + +List(I, M,C,D,X,X,V,I,I, A) + +/* +I -> 1 +II -> 2 +III -> 3 +IV -> 4 +V -> 5 +VI -> 6 +VII -> 7 +VIII -> 8 +IX -> 9 +X -> 10 +*/ -foo(List("1","2","3","4")) +def RomanNumeral2Int(rs: RomanNumeral): Int = rs match { + case Nil => 0 + case M::r => 1000 + RomanNumeral2Int(r) + case C::M::r => 900 + RomanNumeral2Int(r) + case D::r => 500 + RomanNumeral2Int(r) + case C::D::r => 400 + RomanNumeral2Int(r) + case C::r => 100 + RomanNumeral2Int(r) + case X::C::r => 90 + RomanNumeral2Int(r) + case L::r => 50 + RomanNumeral2Int(r) + case X::L::r => 40 + RomanNumeral2Int(r) + case X::r => 10 + RomanNumeral2Int(r) + case I::X::r => 9 + RomanNumeral2Int(r) + case V::r => 5 + RomanNumeral2Int(r) + case I::V::r => 4 + RomanNumeral2Int(r) + case I::r => 1 + RomanNumeral2Int(r) +} + +RomanNumeral2Int(List(I,V)) // 4 +RomanNumeral2Int(List(I,I,I,I)) // 4 (invalid Roman number) +RomanNumeral2Int(List(V,I)) // 6 +RomanNumeral2Int(List(I,X)) // 9 +RomanNumeral2Int(List(M,C,M,L,X,X,I,X)) // 1979 +RomanNumeral2Int(List(M,M,X,V,I,I)) // 2017 + +abstract class Tree +case class Leaf(x: Int) +case class Branch(tl: Tree, tr: Tree) -// from knight1.scala -def first(xs: List[Pos], f: Pos => Option[Path]) : Option[Path] = ??? +abstract class Rexp +case object ZERO extends Rexp // matches nothing +case object ONE extends Rexp // matches the empty string +case class CHAR(c: Char) extends Rexp // matches a character c +case class ALT(r1: Rexp, r2: Rexp) extends Rexp // alternative +case class SEQ(r1: Rexp, r2: Rexp) extends Rexp // sequence +case class STAR(r: Rexp) extends Rexp // star -// should be -def first[A, B](xs: List[A], f: A => Option[B]) : Option[B] = ??? +def depth(r: Rexp) : Int = r match { + case ZERO => 1 + case ONE => 1 + case CHAR(_) => 1 + case ALT(r1, r2) => 1 + List(depth(r1), depth(r2)).max + case SEQ(r1, r2) => 1 + List(depth(r1), depth(r2)).max + case STAR(r1) => 1 + depth(r1) +} + + +// Trees (example of an Algebraic Datatype) + + +abstract class Tree +case class Leaf(x: Int) extends Tree +case class Node(s: String, left: Tree, right: Tree) extends Tree + +val lf = Leaf(20) +val tr = Node("foo", Leaf(10), Leaf(23)) + +val lst : List[Tree] = List(lf, tr) + // expressions (essentially trees) -abstract class Exp +sealed abstract class Exp case class N(n: Int) extends Exp // for numbers case class Plus(e1: Exp, e2: Exp) extends Exp case class Times(e1: Exp, e2: Exp) extends Exp @@ -44,6 +233,7 @@ } val e = Plus(N(9), Times(N(3), N(4))) +println(e.toString) println(string(e)) def eval(e: Exp) : Int = e match { @@ -59,7 +249,7 @@ // e * 0, 0 * e => 0 // e * 1, 1 * e => e // -// (....0 ....) +// (....9 ....) def simp(e: Exp) : Exp = e match { case N(n) => N(n) @@ -83,6 +273,11 @@ println(string(simp(e2))) + + + + + // Tokens and Reverse Polish Notation abstract class Token case class T(n: Int) extends Token @@ -116,6 +311,53 @@ comp("1 2 + 4 * 5 + 3 +".split(" ").toList.map(proc), Nil) +// Tail recursion +//================ + +def fact(n: BigInt): BigInt = + if (n == 0) 1 else n * fact(n - 1) + + +fact(10) +fact(1000) +fact(100000) + + +def factT(n: BigInt, acc: BigInt): BigInt = + if (n == 0) acc else factT(n - 1, n * acc) + + +factT(10, 1) +println(factT(100000, 1)) + + +// there is a flag for ensuring a function is tail recursive +import scala.annotation.tailrec + +@tailrec +def factT(n: BigInt, acc: BigInt): BigInt = + if (n == 0) acc else factT(n - 1, n * acc) + +factT(100000, 1) + +// for tail-recursive functions the Scala compiler +// generates loop-like code, which does not need +// to allocate stack-space in each recursive +// call; Scala can do this only for tail-recursive +// functions + +// Moral: Whenever a recursive function is resource-critical +// (i.e. works with a large recursion depth), then you need to +// write it in tail-recursive fashion. +// +// Unfortuantely, Scala because of current limitations in +// the JVM is not as clever as other functional languages. It can +// only optimise "self-tail calls". This excludes the cases of +// multiple functions making tail calls to each other. Well, +// nothing is perfect. + + + // Polymorphic Types //=================== @@ -158,9 +400,6 @@ map(List(1, 2, 3, 4), (x: Int) => x.toString) -// from knight1.scala -def first(xs: List[Pos], f: Pos => Option[Path]) : Option[Path] = ??? - // should be def first[A, B](xs: List[A], f: A => Option[B]) : Option[B] = ??? @@ -278,55 +517,6 @@ // Source.fromFile(name)(encoding) -// Tail recursion -//================ - -def fact(n: Int): Int = - if (n == 0) 1 else n * fact(n - 1) - - -fact(10) -fact(1000) -fact(100000) - -def factB(n: BigInt): BigInt = - if (n == 0) 1 else n * factB(n - 1) - -def factT(n: BigInt, acc: BigInt): BigInt = - if (n == 0) acc else factT(n - 1, n * acc) - - -factB(1000) - - -factT(10, 1) -println(factT(500000, 1)) - - -// there is a flag for ensuring a function is tail recursive -import scala.annotation.tailrec - -@tailrec -def factT(n: BigInt, acc: BigInt): BigInt = - if (n == 0) acc else factT(n - 1, n * acc) - -factT(100000, 1) - -// for tail-recursive functions the Scala compiler -// generates loop-like code, which does not need -// to allocate stack-space in each recursive -// call; Scala can do this only for tail-recursive -// functions - -// Moral: Whenever a recursive function is resource-critical -// (i.e. works with a large recursion depth), then you need to -// write it in tail-recursive fashion. -// -// Unfortuantely, Scala because of current limitations in -// the JVM is not as clever as other functional languages. It can -// only optimise "self-tail calls". This excludes the cases of -// multiple functions making tail calls to each other. Well, -// nothing is perfect.