Underscore role in Scala

Versions: Scala 2.12.1

Scala has a lot of syntactic sugar. Despite the fact that most of times it pretty simply understandable, it occurs often that it increases the learning curve. One of symbols widely used in Scala and hard to understand at first contact is the underscore.

In this new post about Scala features we'll focus on the underscore. Despite the fact that it's hard to categorize its use, this post is divided in only 3 parts. The first section shows underscore's use in ignoring things. The second section explains its role in the conversions. The final one is devoted to miscellaneous use cases grouped in a common list. Each of the sections contains a short explanation and some learning tests.

Ignoring things

The underscore can be used to ignore things as: not used variables or not used types. Let's start by showing how to ignore the former ones. For instance, if we split some text and we want to get only the first of so constructed entries, we could write the code as it:

it("should ignore not used variables") {
  val text = "a,b"
  val Array(firstLetter, _) = text.split(",")

  firstLetter shouldEqual "a"
}

Second ignore use case can be hiding not used parameters in function execution, as for example:

it("should ignore parameter in the mapping function") {
    val numbers = 1 to 10

    val numberSeq = numbers.map(_ => "Number")

    numberSeq should have size 10
    numberSeq should contain only "Number"
}

Regarding to the previous point, sometimes we may want to use the anonymized parameters in the functions (aka placeholders). It means that we don't assign them a name, somehow like in Java's anonymous classes. With that we gain some "place" but in the other side we make the code less explicit. Thus the parameters anonymization should be used really only in the case of short operations (first case below) and not in the case of long and chained constructions (second case below) that are harder to understand:

it("should anonymize parameter in a short function") {
    val lettersUpperCase = Seq("A", "B", "C")

    val lettersLowerCase = lettersUpperCase.map(_.toLowerCase())

    lettersLowerCase should have size 3
    lettersLowerCase should contain allOf("a", "b", "c")
}
it("should anonymize parameter in a lot of chained functions") {
    val lettersWithNumbers = Seq(("A", 1, true), ("B", 2, true), ("C", 3, true), ("D", 4, false))

    val lettersEvenAndTrue = lettersWithNumbers
      .filter(_._2%2 == 0)
      .filter(_._3)
      .map(_._1)

    lettersEvenAndTrue should have size 1
    lettersEvenAndTrue should contain only "B"
}

The underscore can also be employed to ignore the types in pattern matching. For instance, if we're only interested on matching given type without taking its value into account, we can simply write:

it("should ignore type in the pattern matching") {
    val lettersSet = Set("A", "B", "C")

    val collectionType = lettersSet match {
      case _: Set[_] => "set"
      case _: List[_] => "list"
      case _ => "unknown"
    }

    collectionType shouldEqual "set"
}

Among these all ignoring purposes we can also mention the use of underscore with hidden imports:

import java.util.{HashSet => _, _}

Above will import all members of java.util package except the HashSet.

Conversions

The underscore can also be used in conversion. The first conversion concerns varargs that can be constructed from a sequence with seqName: _* expression:

it("should construct varargs from sequence") {
  val letters = Seq("A", "B", "C", "D")
  def concatenateLetters(letters: String*): String = {
    letters.mkString(",")
  }

  val concatenatedLetters = concatenateLetters(letters: _*)

  concatenatedLetters shouldEqual "A,B,C,D"
}

Another conversion use case concerns methods. Scala's method can be transformed to a function (aka Eta expansion of method into method value) with the help of an underscore symbol:

it("should convert method to a function") {
  def concatenateLetters(letter1: String, letter2: String): String = s"$letter1,$letter2"
  val concatenationFunction = concatenateLetters _

  val aAndBConcatenated = concatenationFunction("A", "B")

  aAndBConcatenated shouldEqual "A,B"
}

The underscore can be also helpful to define partially applied functions in Scala:

it("should be used in partially applied functions") {
  def add(number1: Int, number2: Int, number3: Int): Int = {
    number1 + number2 + number3
  }
  val addTo1: (Int, Int) => Int = add(_, _, 1)

  val add1To1To1Result = addTo1(1, 1)

  add1To1To1Result shouldEqual 3
}

Miscellaneous

This part covers miscellaneous uses of the underscore, listed below for better readability:

The underscore symbol and Scala is a long story. As shown in this post, it's used in a lot of places for different purposes. The most of them are reserved to ignoring stuff (imports, types, cases in pattern matching). Beside it, the underscore is also employed in conversions to varargs or partially applied function. The last part shown some remaining use cases of the underscore, such as: in the type system, custom setters or universality.