Alex

Chapter 3

Singly Linked Lists

enum List[+A]:
  case Nil
  case Cons(head: A, tail: List[A])

object List:
  def apply[A](as: A*): List[A] =
    if as.isEmpty then Nil
    else Cons(as.head, apply(as.tail*))

Pattern Matching

def sum(ints: List[Int]): Int = ints match
  case Nil => 0
  case Cons(x, xs) => x + sum(xs)

def product(doubles: List[Double]): Double = doubles match
  case Nil => 1.0
  case Cons(0.0, _) => 0.0
  case Cons(x, xs) => x * product(xs)

Data Sharing in Functional Data Structures

Exercises:

def tail[A](xs: MyList[A]): MyList[A] = xs match
  case Nil => sys.error("Nil list; no tail")
  case Cons(x: A, xs: MyList[A]) => xs

def setHead[A](x: A, xs: MyList[A]): MyList[A] = Cons(x, tail(xs))

@tailrec
def drop[A](as: MyList[A], n: Int): MyList[A] = n match {
  case 0 => as
  case n => as match {
    case Nil => Nil
    case Cons(a, as) => drop(as, n - 1)
  }
}

// technique time!! wrapping multiple arguments in a tuple allows for much nicer pattern matching more similar to Haskell!!1

@tailrec
def drop[A](as: MyList[A], n: Int): MyList[A] = (as, n) match {
  case (_, 0) => as // scrumptious
  case (Nil, _) => Nil
  case (Cons(a, as), n) => drop(as, n - 1)
}

@tailrec
def dropWhile[A](as: MyList[A], f: A => Boolean): MyList[A] = as match {
  case Nil => Nil
  case Cons(a, as) => if f(a) then dropWhile(as, f) else Cons(a, as)
}

// note this is not tail recursive - it will create a stack frame for every element in the list so can StackOverflow
def init[A](as: MyList[A]): MyList[A] = as match {
  case Nil => sys.error("init of empty list")
  case Cons(a, Nil) => Nil
  case Cons(a, rest) => Cons(a, init(rest))
}

More Recursion Over Lists

// value returned doesn't need to be the same type of the list
def foldRight[A, B](as: List[A], acc: B, f: (A, B) => B): B =
  as match
    case Nil => acc
    case Cons(x, xs) => f(x, foldRight(xs, acc, f))

Exercises and Notes From These

// length of a list is pretty trivial
def length[A](as: MyList[A]) = foldRight(as, 0, (_, acc) => acc + 1)

// foldLeft can be tail recursive
@tailrec
def foldLeft[A, B](as: MyList[A], acc: B, f: (B, A) => B): B = as match {
  case Nil => acc
  case Cons(a, as) => foldLeft(as, f(acc, a), f)
}

def sum(xs: MyList[Int]) = foldLeft(xs, 0, _+_)
def product(xs: MyList[Int]) = foldLeft(xs, 1.0, _*_)

// we can define a reversal by foldLeft with Cons, since we work from the left hand side we can build up the
// list in reverse order
def reverse[A](xs: MyList[A]) = foldLeft(xs, Nil, (xs: MyList[A], x: A) => Cons(x, xs))

// we can write foldRight in terms of foldLeft by reversing the list first
def foldRight'[A, B](as: List[A], acc: B, f: (A, B) => B): B = foldLeft(reverse(as), acc, (b, a) => f(a, b))

Intuition of FoldLeft and FoldRight

Right Fold via Left Fold -> Uh Oh

// so we now have this abomination:
def foldRight'[A, B](as: List[A], acc: B, f: (A, B) => B): B =
  foldLeft(as, (b: B) => b, (g, a) => b => g(f(a, b)))(acc)

More Exercises

def append[A](a1: MyList[A], a2: MyList[A]): MyList[A] =
  foldRight(a1, a2, Cons(_, _))

// we can just use our append function to concatenate a list of lists together
def concat[A](as: MyList[MyList[A]]): MyList[A] =
  foldRight(as, Nil, append)

Other Functions on Lists

def addOne(xs: MyList[Int]): MyList[Int] =
  foldRight(xs, Nil: MyList[Int], (i, acc) => Cons(i + 1, acc))

def convertToString(ds: MyList[Double]): MyList[String] =
  foldRight(xs, Nil: MyList[String], (d, acc) => Cons(d.toString, acc))

// we can generalise the logic in the two functions above into `map`
def map[A, B](as: MyList[A], f: A => B): MyList[B] =
  foldRight(as, Nil, (a, acc) => Cons(f(a), acc))

// we can define filter similarly to map except we drop the element if the predicate doesn't match
def filter[A](as: MyList[A], f: A => Boolean): MyList[A] =
  foldRight(as, Nil: MyList[A], (a, acc) => if f(a) then Cons(a, acc) else acc)

// we can then define flatMap by calling our expanding function `f` on each element and appending the resulting list to the accumulator
def flatMap[A, B](as: MyList[A], f: A => MyList[B]): MyList[B] =
  foldRight(as, Nil, (a, acc) => append(f(a), acc))

// using flatMap to implement filter
def filterPrime[A](as: MyList[A], f: A => Boolean): MyList[A] =
  flatMap(as, a => if f(a) then MyList(a) else Nil)

def addLists(xs: MyList[Int], ys: MyList[Int]): MyList[Int] = (xs, ys) match {
  case (xs, Nil) => Nil
  case (Nil, ys) => Nil
  case (Cons(x, xs), Cons(y, ys)) => Cons(x + y, addLists(xs, ys))
}

// we can generalise addLists to take an arbitrary function
def combine[A, B, C](as: MyList[A], bs: MyList[B], f: (a: A, b: B) => C): MyList[C] = (as, bs) match {
  case (as, Nil) => Nil
  case (Nil, bs) => Nil
  case (Cons(a, as), Cons(b, bs)) => Cons(f(a, b), combine(as, bs, f))
}
// we build an accumulator up in a tail recursive fashion 
def zipWithTailRec[A, B, C](a: MyList[A], b: MyList[B], f: (A, B) => C): MyList[C] =
  @tailrec
  def loop(a: MyList[A], b: MyList[B], acc: MyList[C]): MyList[C] =
    (a, b) match
      case (Nil, _) => acc
      case (_, Nil) => acc
      case (Cons(h1, t1), Cons(h2, t2)) => loop(t1, t2, Cons(f(h1, h2), acc))
  reverse(loop(a, b, Nil))

Standard Library List

@tailrec
def hasSubsequence[A](sup: MyList[A], sub: MyList[A]): Boolean =
  // we define a helper function to determine whether a list starts with a given prefix
  @tailrec
  def startsWith(l: MyList[A], prefix: MyList[A]): Boolean = (l, prefix) match
    case (_, Nil) => true
    case (Cons(h, t), Cons(h2, t2)) if h == h2 => startsWith(t, t2)
    case _ => false

  // then we just wander down the sup list until we hit various conditions (startsWith is true, Nil, otherwise check for tail subsequence)
  sup match
    case Nil => sub == Nil
    case _ if startsWith(sup, sub) => true // guard condition
    case Cons(h, t) => hasSubsequence(t, sub)

ADTs and Trees

Binary Tree

enum Tree[+A]:
  case Leaf(value: A)
  case Branch(left: Tree[A], right: Tree[A])
def size: Int = this match {
  case Leaf(_) => 1
  case Branch(l, r) => 1 + l.size + r.size
}

Extension Methods

extension (t: Tree[Int]) def firstPositive: Int = t match
  case Leaf(i) => i
  case Branch(l,r) =>
    val lpos = l.firstPositive
    if lpos > 0 then lpos else r.firstPositive

Tree Functions Exercises

def maximum(t: Tree[Int]): Int = t match {
  case Leaf(v) => v
  case Branch(l, r) => maximum(l).max(maximum(r))
}

def depth[A](t: Tree[A]): Int = t match {
  case Leaf(v) => 1
  case Branch(l, r) => 1 + depth(l).max(depth(r))
}

def map[A, B](f: A => B, t: Tree[A]): Tree[B] = t match {
  case Leaf(x) => Leaf(f(x))
  case Branch(l, r) => Branch(map(f, l), map(f, r))
}

// we can define a fold algorithm that generalises the logic in the above functions
def fold[A, B](t: Tree[A], f: A => B, g: (B, B) => B): B = t match {
  case Leaf(v) => f(v)
  case Branch(l, r) => g(fold(l, f, g), fold(r, f, g))
}

def foldSize[A](t: Tree[A]): Int = fold(t, _ => 1, 1 + _ + _)

def foldMaximum(t: Tree[Int]): Int = fold(t, a => a, _.max(_))

def foldDepth[A](t: Tree[A]): Int = fold(t, _ => 1, 1 + _.max(_))

def foldMap[A, B](t: Tree[A], f: A => B): Tree[B] = 
  fold(t, Leaf(f(_)), Branch(_, _))

Aside - Enums vs Sealed Traits

// we can define our List as a sealed trait rather than an enum:
sealed trait List[+A]:
  import List.{Cons, Nil}

  def size: Int = this match
    case Cons(_, t1) => 1 + t1.size
    case Nil => 0

object List:
  case class Cons[+A](head: A, tail: List[A]) extends List[A]
  case object Nil extends List[Nothing]