Usages of Underscore (_) in Scala



Underscore (_) character is reserved in Scala. We use Underscore a lot in Scala, which simplifies the code. However, this can cause confusion and make learning more challenging.

1. Pattern Matching and Wildcards

We often use the underscore as a wildcard and for matching unknown patterns when learning Scala.

(a) Module Import

We use underscore when importing packages to import some or all members of the module.

For example,

// Import all members of the scala.util package.
import scala.util._

// Import all members of scala.util except for Try.
import scala.util.{Try => _, _}

// Import all members of scala.util but rename Try to Attempt.
import scala.util.{Try => Attempt, _}

(b) Existential Types

We use underscore as wildcard to match all types in type creators. For example, like List, Array, Seq, Option, and Vector.

For example,

def processLists(lists: List[List[_]]): Int = lists.length

assertEquals(processLists(List(List(1, 2, 3), List("a", "b")), 2)
assertEquals(processLists(List(List(4.0, 5.0), List("x", "y")), 2)
assertEquals(processLists(List(List(Array(6), Array(7)), List("p", "q")), 2)

You can get all types of elements in inner list by using underscore(_).

(c) Matching

With the 'match' keyword, we can use the underscore to catch all unhandled cases.

For example, if you want to classify students based on test scores. Then you can use pattern matching for this.

def classifyStudent(score: Int): String = {
   score match {
      case x if x >= 90 => "Excellent"
      case x if x >= 70 && x <= 89 => "Good"
      case _ => "Needs Improvement"
   }
}

classifyStudent(95) // Excellent
classifyStudent(80) // Good
classifyStudent(60) // Needs Improvement
classifyStudent(105) // Excellent

2. Ignoring Things

We can use the underscore to ignore unused variables and types in the code.

(a) Ignored Parameter

We can use underscore (_) to hide unused parameters in function execution.

For example,

val ints = (1 to 5).map(_ => "Int")
assertEquals(ints, Vector("Int", "Int", "Int", "Int", "Int"))

By using the underscore, we ignored the values in the map anonymous function. It returns Int for each range element. The anonymized parameter acts as a placeholder in the function. Code cleaner but less explicit:

val numbers = Seq(7.56, 12.34, 9.99)
val intParts = numbers.map(_.toInt)
assertEquals(intParts, Seq(7, 12, 9))

So, mapping is equivalent to:

numbers.map(x => x.toInt)

We can access nested collections using underscores. For example,

val products = Seq(("book", 15, true), ("pen", 2, false), ("laptop", 900, true), ("notebook", 5, true))
val popularItems = products
   .filter(_._3)  // Filter in only available items (true).
   .filter(_._2 > 10)  // Filter in only items with a price greater than 10.
   .map(_._1)  // Return only the first element of the tuple: the product name.
assertEquals(popularItems, Seq("book", "laptop"))

We saw how to hide imports in pattern matching which is similar to ignoring things. We can ignore a specific module and import the rest. For example,

import java.util.{List => _, _}
In this example, we import all members from the java.util package except for the List class, which is excluded using 'List => _.

(b) Ignored Variable

We can ignore variables for constructed entries when we don't need them by using the underscore.

For example, we only want the first element in a split string:

val input = "apple,banana,carrot"
val Array(first, _*) = input.split(",")
assertEquals(first, "apple")

The same applies if we only want the second one:

val input = "apple,banana,carrot"
val Array(_, second) = input.split(",")
assertEquals(second, "banana")

And can be more than two:

val input = "apple,banana,carrot,dates,eggplant"
val Array(first, _*) = input.split(",")
assertEquals(first, "apple")

To ignore the entries after the first one, we use the underscore along with *.

We can also randomly ignore any specific entry we do not want using the underscore:

val input = "apple,banana,carrot,dates,eggplant"
val Array(a, b, _, d, e) = input.split(",")
assertEquals(a, "apple")
assertEquals(b, "banana")
assertEquals(d, "dates")
assertEquals(e, "eggplant")

(c) Variable Initialization to its Default Value

We can use the underscore as a default when the initial value of a variable is not needed:

var count: Int = _
count = 42
println(count) // 42

This does not apply to local variables, they need to be initialized.

3. Conversion

You can use underscore (_) in many ways in conversion.

(a) Function Reassignment (Eta expansion)

You can convert method into function. For example,

def add(a: Int, b: Int): Int = a + b

val sum = add _ // reassign add to sum
assertEquals(add(5, 7), sum(5, 7))

(b) Variable Argument Sequence

We can convert sequence into variable arguments using 'seqName: _*' with type ascription.

For example,

def product(args: Int*): Int = {
   args.fold(1)(_ * _)
}

val factors = Seq(2, 3, 4)
val productOfFactors = product(factors: _*) // Convert the sequence factors to varargs using factors: _*
assertEquals(productOfFactors, 24)

(c) Partially-Applied Function

We can create a partially-applied function by providing only some arguments and leaving the rest to be passed later.

If parameters are not provided then these are replaced by underscore(_). For example,

def multiply(x: Int, y: Int): Int = x * y

val doubleWithFive = multiply(5, _: Int)
val result = doubleWithFive(8)

assertEquals(result, 40)

You use underscore in partially-applied functions to ignore things, like parameter groups in functions. For example,

def add(x: Int, y: Int)(z: Int, a: Int)(b: Int, c: Int): Int = x + y + z + a + b + c

val partialAdd = add(1, 2) _
val result = partialAdd(3, 4)(5, 6)

assertEquals(result, 21)

(d) Assignment Operators (Setters overriding)

Overriding the default setter can be seen as a form of conversion using the underscore.

For example,

class Temperature {
   private var value = 0
   def degrees = value
   def degrees_=(newValue: Int): Unit = {
      require(newValue >= -20 && newValue <= 40, "Temperature must be between -20 and 40 degrees")
      value = newValue
   }
}

val temp = new Temperature
temp.degrees = 25
assertEquals(temp.degrees, 25)

try {
   temp.degrees = 50 // This will fail because 50 is outside the valid temperature range.
   fail("Invalid temperature setting")
} catch {
   case e: IllegalArgumentException => assertNotEquals(temp.degrees, 50)
}

4. Other Usages of Underscore (_) in Scala

These are some other usages of underscore(_) in Scala.

(a) Joining Letters to Punctuation/Operators

Punctuation characters can not be used in variable names like alphanumeric characters. If using punctuation in variable names improves clarity. Then you can do so by joining letters and punctuation with an underscore.

For example,

def multiplyElements(list: List[_]): List[_] = list.map(_ * 2)
val multipliedList = multiplyElements(List(3, 6, 9))
assertEquals(multipliedList, List(6, 12, 18))

(b) Numeric Literal Separator

Scala introduced a numeric literal separator with an underscore. For example,

var population = 1_2_3_4_5_6_7_8_9 // 123456789
population = 12_34_567_8_9 // 123456789
population = 123_4_567_89 // 123456789

var price = 2_5_0.5 // 250.5
price = 25_0.5 // 250.5
price = 25_05 // 2505

(c) Higher-Kinded Type

A Higher-Kinded type is a type that abstracts over another type. Scala can generalize type constructors, similar to existential types. For example,

trait OptionContainer[T[_]] { 
   // higher-kinded type parameter
   def checkIfEmpty(option: T[_]): Boolean
}

object ListContainer extends OptionContainer[List] {
   override def checkIfEmpty(option: List[_]): Boolean = option.isEmpty
}

var listIsEmpty = ListContainer.checkIfEmpty(List(1, 2, 3))
assertTrue(listIsEmpty == false)

listIsEmpty = ListContainer.checkIfEmpty(List())
assertTrue(listIsEmpty == true)

We have summarized all other uses of underscore(_) in the following table.

Concept Example
Existential types def foo(l: List[Option[_]]) = ...
Higher kinded type parameters case class A[K[_],T](a: K[T])
Ignored variables val _ = 5
Ignored parameters List(1, 2, 3) foreach { _ => println("Hi") }
Ignored names of self types trait MySeq { _: Seq[_] => }
Wildcard patterns Some(5) match { case Some(_) => println("Yes") }
Wildcard patterns in interpolations "abc" match { case s"a$_c" => }
Sequence wildcard in patterns C(1, 2, 3) match { case C(vs @ _*) => vs.foreach(f(_)) }
Wildcard imports import java.util._
Hiding imports import java.util.{ArrayList => _, _}
Joining letters to operators def bang_!(x: Int) = 5
Assignment operators def foo_=(x: Int) { ... }
Placeholder syntax List(1, 2, 3) map (_ + 2)
Method values List(1, 2, 3) foreach println _
Converting call-by-name parameters to functions def toFunction(callByName: => Int): () => Int = callByName _
Default initializer var x: String = _
Advertisements