1. Overview

The underscore (_) is one of the symbols we widely use in Scala. It’s sometimes called syntactic sugar since it makes the code pretty simple and shorter. But, this often results in a lot of confusion and increases the learning the curve.

In this tutorial, we’ll look at the different and most common usages of underscores in Scala.

2. Deprecation Warning

As of Scala 3, some uses of the underscore have been deprecated or removed (such as the syntax of wildcard arguments and vararg splices), with new syntaxes introduced to replace these usages. The features discussed in this article work well in Scala 2, but not all of them are compatible with Scala 3.

3. Pattern Matching and Wildcards

We widely use the underscore as a wildcard and in matching unknown patterns. This, perhaps, is the first usage of underscore we come across when learning Scala. Let’s see some examples.

3.1. Module Import

We use underscore when importing packages to indicate that all or some members of the module should be imported:

// imports all the members of the package junit. (equivalent to wildcard import in java using *)
import org.junit._

// imports all the members of junit except Before.
import org.junit.{Before => _, _}

// imports all the members of junit but renames Before to B4.
import org.junit.{Before => B4, _}

3.2. Existential Types

Also, we use the underscore as a wildcard to match all types in type creators like List, Array, Seq, Option, or Vector:

def getLength(x : List[List[_]]): Int = x.length

assertEquals(getLength(List(List(8), List("str"))), 2)
assertEquals(getLength(List(List(5.00), List("str"))), 2)
assertEquals(getLength(List(List(Array(7)), List("str"))), 2)

With _, we allowed all types of elements in the inner list.

3.3. Matching

Using the match keyword, we can use the underscore to catch all possible cases not handled by any of the defined cases.

For example, given an item price, we want to either buy or sell the item based on certain special prices. If the price is 130, we want to buy, but if it’s 150, we want to sell. For any other price outside these, we need to obtain approval for the item:

def itemTransaction(price: Double): String = {
  price match {
    case 130 => "Buy"
    case 150 => "Sell"
  
    // if price is not any of 130 and 150, this case is executed
    case _ => "Need approval"
  }
}

itemTransaction(130) // Buy
itemTransaction(150) // Sell
itemTransaction(70) // Need approval
itemTransaction(400) // Need approval

We can also read our pattern matching tutorial for more examples.

4. Ignoring Things

The underscore can be used to ignore variables and types that aren’t used anywhere in the code.

4.1. Ignored Parameter

For example, in function execution, we can use the underscore to hide not used parameters:

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

Using the underscore, we ignored whatever value we have in the map anonymous function; we just return Int for each element of the range. We may use the anonymized parameter as a placeholder in function. This makes the code clean; though less explicit:

val prices = Seq(10.00, 23.38, 49.82)
val pricesToInts = prices.map(_.toInt)
assertEquals(pricesToInts, Seq(10, 23, 49))

Here, the mapping is equivalent to:

prices.map(x => x.toInt)

We can also use the underscore to access nested collections:

val items = Seq(("candy", 2, true), ("cola", 7, false), ("apple", 3, false), ("milk", 4, true))
val itemsToBuy = items
  .filter(_._3)  // filter in only available items (true)
  .filter(_._2 > 3)  // filter in only items with price greater than 3
  .map(_._1)  // return only the first element of the tuple; the item name
assertEquals(itemsToBuy, Seq("milk"))

We already saw a hidden import in pattern matching; that can also be referenced as ignoring things. We can ignore a given module and import the rest:

import org.junit.{Before => _, _}

With this, we’ve imported all members of the junit package except (ignoring) Before.

4.2. Ignored Variable

When we don’t care about variables for constructed entries, we can ignore them using the underscore.

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

val text = "a,b"
val Array(a, _) = text.split(",")
assertEquals(a, "a")

The same is applicable if we want only the second one:

val Array(_, b) = text.split(",")
assertEquals(b, "b")

We can extend this to more than two entries:

val text = "a,b,c,d,e"
val Array(a, _*) = text.split(",")
assertEquals(a, "a")

To ignore the rest of the entries after the first, we use the underscore together with *.

We can also ignore randomly using the underscore for any one entry we don’t want:

val Array(a, b, _, d, e) = text.split(",")
assertEquals(a, "a")
assertEquals(b, "b")
assertEquals(d, "d")
assertEquals(e, "e")

4.3. Variable Initialization to Its Default Value

When the initial value of a variable is not necessary, we can use the underscore as default:

var x: String = _
x = "real value"
println(x) // real value

This doesn’t work for local variables; local variables must be initialized.

5. Conversions

In several ways, we can use the underscore in conversions.

5.1. Function Reassignment (Eta expansion)

With the underscore, we can convert a method to a function:

def multiplier(a: Int, b: Int): Int = a*b

val times = multiplier _ // reassign multiplier to times
assertEquals(multiplier(8, 13), times(8, 13))

5.2. Variable Argument Sequence

We can convert a sequence to variable arguments using seqName: _* (a special instance of type ascription).

For example, let’s define our own sum function that accepts varargs (of type Int) and sum all:

def sum(args: Int*): Int = {
  args.reduce(_ + _)
}
val sumable = Seq(4, 5, 10, 3)
val sumOfSumable = sum(sumable: _*) // convert the sequence sumable to varargs using sumable: _*
assertEquals(sumOfSumable, 22)

5.3. Partially-Applied Function

By providing only some arguments in a function and leaving the rest to be passed, we can generate a partially-applied function.

The unprovided parameters are replaced by the underscore:

def sum(x:Int,y:Int): Int = x+y
val sumToTen = sum(10,_:Int)
val sumFiveAndTen = sumToTen(5)

assertEquals(sumFiveAndTen, 15)

The use of underscores in a partially-applied function can also be grouped as ignoring things. We can also ignore parameter groups in functions with multiple parameter groups to generate a kind of partially-applied function:

def bar(x:Int,y:Int)(z:String,a:String)(b:Float,c:Float): Int = x
val foo = bar(1,2) _

assertEquals(foo("Some string", "Another string")(3/5, 6/5), 1)

5.4. Assignment Operators (Setters overriding)

Overriding the default setter can also be considered as a kind of conversion using the underscore:

class Product {
  private var a = 0
  def price = a
  def price_=(i: Int): Unit = {
    require(i > 10)
    a = i
  }
}

val product = new Product
product.price = 20
assertEquals(product.price, 20)

try {
  product.price = 7 // will fail because 7 is not greater than 10
  fail("Price must be greater than 10")
} catch {
  case _: IllegalArgumentException => assertNotEquals(product.price, 7)
}

6. Miscellaneous Usages

We have other usages of the underscore that might not fit into any of our groups.

6.1. Joining Letters to Operators/Punctuation

We can’t use punctuations in variable names like we do with alphanumerics. If using punctuations in a variable name makes it more explicit, we can do that by joining the letters to the punctuations with the underscore:

def list_++(list: List[_]): List[_] = List.concat(list, list)
val concatenatedList = list_++(List(2, 5))
assertEquals(concatenatedList, List(2, 5, 2, 5))

6.2. Numeric Literal Separator (Scala 2.13+)

Scala introduced a numeric literal separator using an underscore in Scala 2.13. Let’s see some examples:

var x = 1_000_000 // 1000000
x = 1_00_00_00 // 1000000
x = 1_0000_00 // 1000000

var pi = 3_14e-0_2 // 3.14
pi =  3_14e-02 // 3.14
pi =  314e-0_2 // 3.14
pi =  314e-02 // 3.14

6.3. Higher-Kinded Type

A Higher-Kinded type is a type that abstracts over some type that, in turn, abstracts over another type. In this way, Scala can generalize across type constructors. It’s quite similar to the existential type.

We can define higher-kinded types using the underscore:

trait ObjectContainer[T[_]] { // higher-kinded type parameter
  def checkIfEmpty(collection: T[_]): Boolean
}
object SeqContainer extends ObjectContainer[Seq] {
  override def checkIfEmpty(collection: Seq[_]): Boolean = !collection.nonEmpty
}

var seqIsEmpty = SeqContainer.checkIfEmpty(Seq(7, "7"))
assertTrue(seqIsEmpty == false)
seqIsEmpty = SeqContainer.checkIfEmpty(Seq())
assertTrue(seqIsEmpty == true)

7. Conclusion

In this tutorial, we’ve seen many different ways to use the Scala underscore.

As always, the full source code of the article is available over on GitHub.