Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MyList - step 4

Add behavior: foreach, map, and filter. These are the bread-and-butter operations of functional collections. We’ll implement them recursively on our immutable, singly linked list.

  • foreach visits each element and runs a side-effecting function (like printing).
  • map transforms every element and returns a new list of the results.
  • filter keeps only the elements that satisfy a predicate.

Exercise: add methods to MyList

trait MyList[A]:
  ...
  def foreach(f: A => Unit): Unit
  def map[B](f: A => B): MyList[B]
  def filter(p: A => Boolean): MyList[A]

Hints:

  • Use structural recursion: handle Empty as the base case; for Cons, do work on head, then recurse on tail.
  • Keep everything immutable. Construct new Cons nodes rather than mutating.

Solution: foreach

Empty

override def foreach(f: A => Unit): Unit = ()

Doing nothing for the empty list makes sense and returns Unit (written as ()).

Cons

override def foreach(f: A => Unit): Unit =
  f(head)
  tail.foreach(f)

Apply f to head, then recurse into the tail.

Solution: map

Empty

override def map[B](f: A => B): MyList[B] = Empty[B]()

Mapping over an empty list yields an empty list of the new type B.

Note: If Empty were declared as case object Empty extends MyList[Nothing], we could avoid specifying [B] here thanks to variance. We keep it simple for now and stay with EmptyB.

Cons

override def map[B](f: A => B): MyList[B] =
  Cons[B](f(head), tail.map(f))

Transform the head, then recursively map the tail, and rebuild a new list. You can omit [B] in Cons because the type is inferred from the return type: Cons(f(head), tail.map(f))

Solution: filter

Empty

override def filter(p: A => Boolean): MyList[A] = this

Filtering an empty list is still empty.

Cons

override def filter(p: A => Boolean): MyList[A] =
  if p(head) then Cons(head, tail.filter(p))
  else tail.filter(p)

Keep head if it matches the predicate p; otherwise drop it and continue with the tail.

Try it out

@main
def main(): Unit =
  val myList: MyList[Int] = MyList(1, 2, 3)

  myList.foreach(x => println(x + 2))         // prints 3, 4, 5
  println(myList.map(x => x * 2))             // MyList(2, 4, 6)
  println(myList.filter(x => x < 2))          // MyList(1)

Key ideas

  • Recursion mirrors the list’s structure: Empty is the base case; Cons does work and recurses on tail.
  • Immutability makes it natural to “return a new list” rather than modify in place.