101.06 - Collections

Other collections

All the collections shown here are immutable.

Lists are linear, access to the first element is faster than accessing the middle of the list. A more evenly balanced access pattern is provided by Vectors:

  • Array of 32 entries
  • If more than 32 entries, it is transformed into an array of 32 pointers (references) to arrays of 32 entries (total: 32 * 32 = 1024 entries), and so on ...
  • Better random access performance

Vectors and Lists are quite analogous and easily interchangeable.

val nums = Vector(1, 2, 3, -88)
val people = Vector("Bob", "James", "Peter")

Instead of: x :: xs we have:

  • x +: xs Create a new vector with leading element x, followed by all elements of xs
  • xs :+ x Create a new vector with trailing element x, preceded by all elements of xs

Vectors and Lists are both sub-classes of Seq which implements Iterable.

  • Iterable
    • Seq
      • List
      • Vector
      • Range
    • Set
    • Map

Array and String are very similar to Seq and support functions like foldLeft, filter or map but they don't share the same hierarchy because they come from Java.

Other collection class is Range:

val r: Range = 1 until 5       // 1, 2, 3, 4
val s: Range = 1 to 5          // 1, 2, 3, 4, 5
val t: Range = 1 to 10 by 3    // 1, 4, 7, 10
val u: Range = 6 to 1 by -2    // 6, 4, 2

More Seq operations:

xs exists p                     // true if an element exists that fulfills p
xs forall p                     // true if all elements fulfill p
xs zip ys                       // creates a list of pairs drawing from xs and ys
xs.unzip                        // splits a list of pairs into two lists
xs.flatMap f                    // applies the collection-valued function to each 
                                // element and concatenates the result
xs.sum
xs.product
xs.max
xs.min

Example:

def isPrime(n: Int): Boolean = (2 until n) forall (x => n % x != 0)

Combinatorial Search and for-expressions

Example: find all the pairs of (1 <= i < n, 1 <= j < i) such that i + j is prime.

val n = 7                  // upper limit
(1 until n).flatMap(i => (1 until i).map(j => (i, j))
  .filter(pair => isPrime(pair._1 + pair._2))

Introducing for-expressions:

case class Person(name: String, age: Int)

for (p <- persons if p.age > 20) yield p.name   // list of names of all persons 
                                                // older than 20

This is equivalent to:

persons filter (p => p.age > 20) map (p => p.name)

Rewriting the original example using for-expressions:

val n= 7
for {
  i <- 1 until n
  j <- 1 until i
  if (isPrime(i + j))
} yield (i, j)

Another example:

def scalarProduct(xs: List[Int], ys: List[Int]): Int = 
  (for ((x, y) <- xs zip ys) yield x * y).sum

Combinatorial search example

Sets are close to Seqs

val fruit = Set("apple", "orange", "banana")
val s = (1 to 6).toSet

s.map(_ + 2)
fruit filter (_.startsWith = "app")
s.nonEmpty

Differences between Sets and Seqs:

  • Sets are unordered
  • Sets don't contain duplicates
  • The fundamental operation in a Set is contains: s contains 5

The eight queens problem:

  • place a queen in each row
  • once we placed k-1 queens we must place a k-th queen so that it is not in "check" with any others

Recursive algorithm:

  • Suppose we have generated all the solutions of placing k-1 queens on a board of size n
  • Each solution is represented by a list of size k-1 containing the columns (between 0 and n - 1)
  • The column for queen k - 1 comes in the beginning of the list, before the column for queen k - 2
  • The solution set is a set of lists with one element for each solution
  • To place the k-th queen we generate all possible extensions of each solution precede by the new queen
def queens(n: Int): Set[List[Int]] = {
  def placeQueens(k: Int): Set[List[Int]] = 
    if (k == 0) Set(List())
    else 
      for {
        queens <- placeQueens(k-1)
        col <- 0 until n
        if (isSafe(col, queens))
      } yield col :: queens

  def isSafe(col: Int, queens: List[Int]): Boolean = {
    val row = queens.length
    val queensWithRow = (row - 1 to 0 by -1) zip queens  // List of pairs (r, c)
    queensWithRow forall {
      case (r, c) => (col != c) && (math.abs(col - c) != (row - r))
    }
  } 

  placeQueens(n)
}

Maps

Maps associate keys to values.

val romanNumerals = Map("I" -> 1, "V" -> 5, "X" -> 10)
val capitalOfCountry = Map("US" -> "Washington", "Switzerland" -> "Bern")

Maps are Iterables. Class Map[Key, Value] extends Iterable[(Key, Value)], so they support all operations that other collections do.

val countryOfCapital = capitalOfCountry map {
  case (x, y) => (y, x)       // reverse map
}

The syntax key -> value is just another way of writing the pair (key, value).

Class Map[Key, Value] also extends the function type Key => Value, so maps can be used everywhere functions.

capitalOfCountry("US")                    // function call
capitalOfCountry("Andorra")               // java.util.NoSuchElementException

capitalOfCountry get "US"                 // Some("Washington")
capitalOfCountry get "Andorra"            // None

The result of get is an Option.

Option is a standard type in Scala:

trait Option[+A]
case class Some[+A](value: A) extends Option[A]
object None extends Option[Nothing]

Options can be decomposed using pattern matching:

def showCapital(country: String) = capitalOfCountry.get(country) match {
  case Some(capital) => capital
  case None => "missing data"
}

showCapital("US")                // Washington
showCapital("Andorra")           // missing data

Collections can be sorted and grouped (like in SQL):

val fruit = List("apple", "pear", "orange", "pineapple")
fruit sortWith (_.length < _.length)   // List(pear, apple, orange, pineapple)
fruit.sorted                           // List(apple, orange, pear, pineapple)
fruit groupBy (_.head)                 // Map( p -> List(pear, pineapple),
                                       //      o -> List(orange),
                                       //      a -> List(apple))
                                       // (_.head) is a "discriminator" function

Example: modeling polynomials. Polynomials can be represented as maps from exponents to coefficients. For instance:

$$ x^3 - 2x + 5 $$

can be represented as:

Map(0 -> 5, 1 -> -2, 3 -> 1)

We can encapsulate this in a class:

class Poly(val terms: Map[Int, Double]) {

  def +(other: Poly): Poly = new Poly(terms ++ (other.terms map adjust))

  def adjust(term: (Int, Double)): (Int, Double) = {
    val (exp, coeff) = term
    terms get exp match {
      case Some(exp1, coeff1) => exp -> (coeff + coeff1)
      case None => exp -> coeff
    }
  }

  override def toString =
    (for ((exp, coeff) <- terms.toList.sorted.reverse) 
      (yield coeff + "x^" + exp).mkString " + "
}

val p1 = new Poly(Map(1 -> 2.0, 3 -> 4.0, 5 -> 6.2))
val p2 = new Poly(Map(0 -> 3.0, 3 -> 7.0))
p1 + p2

Maps can have default values:

val cap1 = capitalOfCountry withDefaultValue "<unknown>"
cap1("Andorra")                 // <unknown>

Rewriting the Polynomials example with default values:

class Poly(val terms0: Map[Int, Double]) {
  def this(bindings: (Int, Double)*) = this(bindings.toMap)

  val terms = terms0 withDefaultValue 0.0

  def +(other: Poly): Poly = new Poly(terms ++ (other.terms map adjust))

  def adjust(term: (Int, Double)): (Int, Double) = {
    val (exp, coeff) = term
    exp -> (coeff + terms(exp))
  }

  override def toString =
    (for ((exp, coeff) <- terms.toList.sorted.reverse) 
      (yield coeff + "x^" + exp).mkString " + "
}

val p1 = Poly(1 -> 2.0, 3 -> 4.0, 5 -> 6.2)
val p2 = Poly(0 -> 3.0, 3 -> 7.0)
p1 + p2

Notice the second constructor, using a varargs syntax **(Int, Double)* that simplifies the construction of Poly** objects.

The + operation can also be implemented using foldLeft

Putting the pieces together

Example: generate all phone number mnemonics based on a dictionary.

We will implement a function translate(phoneNumber) that produces all phrases of words that can serve as mnemonics for the phone number.

val words = ...                       // dictionary, one word per line

val mnem = Map('2' -> "ABC", '3' -> "DEF", '4' -> "GHI",
               '5' -> "JKL", '6' -> "MNO", '7' -> "PQRS",
               '8' -> "TUV", '9' -> "WXYZ")

// invert the map to get a map of characters to digits
val charCode: Map[Char, Char] = 
  for ((digit, str) <- mnem; ltr <- str) yield ltr -> digit

// map a word to its code, e.g. "Java" -> "5282"
def wordCode(word: String): String = word.toUpperCase map charCode

// reverse map from digit strings to the words that represent them
// missing numbers will map to an empty sequence "1111" -> List()
val wordsForNum: Map[String, Seq[String]] = words groupBy wordCode

//finally, encoding the number
def encode(number: String): Set[List[String]] = 
  if (number.isEmpty) Set(List())
  else {
    for {
      split <- 1 to number.length
      word <- wordsForNum(number take split)
      rest <- encode(number drop split)
    } yield word :: rest
  }.toSet

encode("4169535740")