BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Streamlining Code with Unnamed Patterns/Variables: a Comparative Study of Java, Kotlin, and Scala

Streamlining Code with Unnamed Patterns/Variables: a Comparative Study of Java, Kotlin, and Scala

Key Takeaways

  • Java's JEP 443: Enhances code readability by allowing the omission of unnecessary components in pattern matching and unused variables.
  • Kotlin's unused variables: Simplifies code by denoting unused parameters in functions, lambdas, or destructuring declarations.
  • Scala's unused variables: Used as a wildcard to ignore unused variables and conversions, improving code conciseness.
  • Underscore as Syntactic Sugar: A common feature in many languages, including Java, Kotlin, and Scala, that simplifies code.
  • Enhanced Code Readability and Maintainability: The underscore character improves code readability and maintainability.
  • Future Language Evolution: Expect further enhancements and innovative uses of the underscore as languages evolve.

In the world of programming, the underscore (`_`) is a character with a wide range of uses. It’s often referred to as syntactic sugar, as it simplifies the code and makes it more concise.

This article will explore the use of underscores in three popular programming languages: Java, Kotlin, and Scala.

Java: Unnamed Patterns and Variables with JEP 443

Java, the ever-evolving language, has taken another significant step towards enhancing its code readability and maintainability with the introduction of JEP 443. This proposal, titled "Unnamed Patterns and Variables (Preview)," has been completed from the targeted status for JDK 21.

The JEP aims to enhance the language with unnamed patterns, which match a record component without stating the component’s name or type, and unnamed variables, which you can but not use.

Both of these are denoted by the underscore character, as in r instanceof _(int x, int y) and r instanceof _.

Unnamed Patterns

Unnamed patterns are designed to streamline data processing, particularly when working with record classes. They allow developers to omit the type and name of a record component in pattern matching, which can significantly improve code readability.

Consider the following code snippet:

if (r instanceof ColoredPoint(Point p, Color c)) {
    // ...
}

If the Color c component is not needed in the if block, it can be laborious and unclear to include it in the pattern. With JEP 443, developers can simply omit unnecessary components, resulting in cleaner, more readable code:

if (r instanceof ColoredPoint(Point p, _)) {
    // ...
}

This feature is particularly useful in nested pattern-matching scenarios where only some components of a record class are required. For example, consider a record class ColoredPoint that contains a Point and a Color. If you only need the x coordinate of the Point, you can use an unnamed pattern to omit the y and Color components:

if (r instanceof ColoredPoint(Point(int x, _), _)) {
    // ...
}

Unnamed Variables

Unnamed variables are useful when a variable must be declared, but its value is not used. This is common in loops, try-with-resources statements, catch blocks, and lambda expressions.

For instance, consider the following loop:

for (Order order : orders) {
    if (total < limit) total++;
}

In this case, the order variable is not used within the loop. With JEP 443, developers can replace the unused variable with an underscore, making the code more concise and clear:

for (_ : orders) {
    if (total < limit) total++;
}

Unnamed variables can also be beneficial in switch statements where the same action is executed for multiple cases, and the variables are not used. For example:

switch (b) {
    case Box(RedBall _), Box(BlueBall _) -> processBox(b);
    case Box(GreenBall _) -> stopProcessing();
    case Box(_) -> pickAnotherBox();
}

In this example, the first two cases use unnamed pattern variables because their right-hand sides do not use the box’s component. The third case uses the unnamed pattern to match a box with a null component.

Enabling the Preview Feature

Unnamed patterns and variables are a preview feature, disabled by default. To use it, developers must enable the preview feature to compile this code, as shown in the following command:

javac --release 21 --enable-preview Main.java

The same flag is also required to run the program:

java --enable-preview Main

However, one can directly run this using the source code launcher. In that case, the command line would be:

java --source 21 --enable-preview Main.java

The jshell option is also available but requires enabling the preview feature as well:

jshell --enable-preview

Kotlin: Underscore for Unused Parameters

In Kotlin, the underscore character (_) is used to denote unused parameters in a function, lambda, or destructuring declaration. This feature allows developers to omit names for such parameters, leading to cleaner and more concise code.

In Kotlin, developers can place an underscore instead of its name if the lambda parameter is unused. This is particularly useful when working with functions that require a lambda with multiple parameters but only some of the parameters are needed.

Consider the following Kotlin code snippet:

mapOf(1 to "one", 2 to "two", 3 to "three")
   .forEach { (_, value) -> println("$value!") }

In this example, the forEach function requires a lambda that takes two parameters: a key and a value. However, we’re only interested in the value, so we replace the key parameter with an underscore.

Let’s consider another code snippet:

var name: String by Delegates.observable("no name") {
    kProperty, oldValue, newValue -> println("$oldValue")
}

In this instance, if the kProperty and newValue parameters are not used within the lambda, including them can be laborious and unclear. With the underscore feature, developers can simply replace the unused parameters with underscores:

var name: String by Delegates.observable("no name") {
    _, oldValue, _ -> println("$oldValue")
}

This feature is also useful in destructuring declarations where you want to skip some of the components:

val (w, x, y, z) = listOf(1, 2, 3, 4)
print(x + z) // 'w' and 'y' remain unused

With the underscore feature, developers can replace the unused components with underscores:

val (_, x, _, z) = listOf(1, 2, 3, 4)
print(x + z)

This feature is not unique to Kotlin. Other languages like Haskell use the underscore character as a wildcard in pattern matching. For C#, `_` in lambdas is just an idiom without special treatment in the language. The same semantic may be applied in future versions of Java.

Scala: The Versatility of Underscore

In Scala, the underscore (`_`) is a versatile character with a wide range of uses. However, this can sometimes lead to confusion and increase the learning curve for new Scala developers. In this section, we’ll explore the different and most common usages of underscores in Scala.

Pattern Matching and Wildcards

The underscore is widely used as a wildcard and in matching unknown patterns. This is perhaps the first usage of underscore that Scala developers would encounter.

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, _}

Existential Types

The underscore is also used as a wildcard to match all types in type creators such as List, Array, Seq, Option, or Vector.

// Using underscore in List
val list: List[_] = List(1, "two", true)
println(list)

// Using underscore in Array
val array: Array[_] = Array(1, "two", true)
println(array.mkString("Array(", ", ", ")"))

// Using underscore in Seq
val seq: Seq[_] = Seq(1, "two", true)
println(seq)

// Using underscore in Option
val opt: Option[_] = Some("Hello")
println(opt)

// Using underscore in Vector
val vector: Vector[_] = Vector(1, "two", true)
println(vector)

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

Matching

Using the match keyword, developers can use the underscore to catch all possible cases not handled by any of the defined cases. For example, given an item price, the decision to buy or sell the item is made based on certain special prices. If the price is 130, the item is to buy, but if it’s 150, it is to sell. For any other price outside these, approval needs to be obtained:

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"
 }
}

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

Ignoring Things

The underscore can ignore variables and types not used anywhere in the code.

Ignored Parameter

For example, in function execution, developers can use the underscore to hide unused parameters:

val ints = (1 to 4).map(_ => "Hello")
println(ints) // Vector(Hello, Hello, Hello, Hello)

Developers can also use the underscore to access nested collections:

val books = Seq(("Moby Dick", "Melville", 1851), ("The Great Gatsby", "Fitzgerald", 1925), ("1984", "Orwell", 1949), ("Brave New World", "Huxley", 1932))

val recentBooks = books
 .filter(_._3 > 1900)  // filter in only books published after 1900
 .filter(_._2.startsWith("F"))  // filter in only books whose author's name starts with 'F'
 .map(_._1)  
// return only the first element of the tuple; the book title

println(recentBooks) // List(The Great Gatsby)

In this example, the underscore is used to refer to the elements of the tuples in the list. The filter function selects only the books that satisfy the given conditions, and then the map function transforms the tuples to just their first element (book title). The result is a sequence with book titles that meet the criteria.

Ignored Variable

When a developer encounters details that aren’t necessary or relevant, they can utilize the underscore to ignore them.

For example, a developer wants only the first element in a split string:

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

The same principle applies if a developer only wants to consider the second element in a construct.

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

The principle can indeed be extended to more than two entries. For instance, consider the following example:

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

In this example, a developer splits the text into an array of elements. However, they are only interested in the first element, 'a'. The underscore with an asterisk  (_*) ignores the rest of the entries in the array, focusing only on the required element.

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

The underscore can also be used to ignore randomly:

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

Variable Initialization to Its Default Value

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

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

However, this doesn’t work for local variables; local variables must be initialized.

Conversions

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

Function Reassignment (Eta expansion)

With the underscore, a method can be converted to a function. This can be useful to pass around a function as a first-class value.

def greet(prefix: String, name: String): String = s"$prefix $name"

// Eta expansion to turn greet into a function
val greeting = greet _

println(greeting("Hello", "John"))

Variable Argument Sequence

A sequence can be converted to variable arguments using `seqName: _*` (a special instance of type ascription).

def multiply(numbers: Int*): Int = {
 numbers.reduce(_ * _)
}

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

println(product) // Should print: 24

Partially-Applied Function

By providing only a portion of the required arguments in a function and leaving the remainder to be passed later, a developer can create what’s known as a partially-applied function. The underscore substitutes for the parameters that have not yet been provided.

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

println(sumFiveAndTen, 15)

The use of underscores in a partially-applied function can also be grouped as ignoring things. A developer can ignore entire groups of parameters in functions with multiple parameter groups, creating a special 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) _

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

Assignment Operators (Setters overriding)

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

class User {
 private var pass = ""
 def password = pass
 def password_=(str: String): Unit = {
   require(str.nonEmpty, "Password cannot be empty")
   require(str.length >= 6, "Password length must be at least 6 characters")
   pass = str
 }
}

val user = new User
user.password = "Secr3tC0de"
println(user.password) // should print: "Secr3tC0de"

try {
 user.password = "123" // will fail because it's less than 6 characters
 println("Password should be at least 6 characters")
} catch {
 case _: IllegalArgumentException => println("Invalid password")
}

Higher-Kinded Type

A Higher-Kinded type is one 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. It can be defined higher-kinded types using the underscore:

trait Wrapper[F[_]] {
 def wrap[A](value: A): F[A]
}

object OptionWrapper extends Wrapper[Option] {
 override def wrap[A](value: A): Option[A] = Option(value)
}

val wrappedInt = OptionWrapper.wrap(5)
println(wrappedInt)

val wrappedString = OptionWrapper.wrap("Hello")
println(wrappedString)

In the above example, Wrapper is a trait with a higher-kinded type parameter F[_]. It provides a method wrap that wraps a value into the given type. OptionWrapper is an object extending this trait for the Option type. The underscore in F[_] represents any type, making Wrapper generic across all types of Option.

These are some examples of Scala being a powerful tool that can be used in various ways to simplify and improve the readability of your code. It’s a feature that aligns well with Scala’s philosophy of being a concise and expressive language that promotes readable and maintainable code.

Conclusion

The introduction of unnamed patterns and variables in Java through JEP 443 marks a significant milestone in the language’s evolution. This feature, which allows developers to streamline their code by omitting unnecessary components and replacing unused variables, brings Java closer to the expressiveness and versatility of languages like Kotlin and Scala.

However, it’s important to note that while this is a substantial step forward, Java’s journey in this area is still incomplete. Languages like Kotlin and Scala have long embraced similar concepts, using them in various ways to enhance code readability, maintainability, and conciseness. These languages have demonstrated the power of such concepts in making code more efficient and easier to understand.

In comparison, Java’s current use of unnamed patterns and variables, although beneficial, is still somewhat limited. The potential for Java to further leverage these concepts is vast. Future updates to the language could incorporate more advanced uses of unnamed patterns and variables, drawing inspiration from how these concepts are utilized in languages like Kotlin and Scala.

Nonetheless, adopting unnamed patterns and variables in Java is a significant step towards enhancing the language’s expressiveness and readability. As Java continues to evolve and grow, we expect to see further innovative uses of these concepts, leading to more efficient and maintainable code. The journey is ongoing, and it’s an exciting time to be a part of the Java community.

Happy coding!

About the Author

Rate this Article

Adoption
Style

BT