1. Overview

Scala 3 introduced a lot of new features to the language. One of the most notable changes is the redesign of the implicit feature. As part of that, a new keyword, extension, is introduced to implement the extension method, which was earlier done using the implicit keyword.

In this tutorial, we’ll have a detailed look at the extension methods in Scala 3.

2. Extension Method

An extension method allows defining new methods to third-party types, without making any changes to the original type. This type is also sometimes known as the Pimp My Library pattern. This helps us to create utility methods for favorite types, and access them like they are part of the type itself.

We can still use the implicit keyword to create extension methods in Scala 3, the same way it’s done in Scala 2. However, in future releases, Scala 3 will remove the implicit keyword completely.

3. Defining an Extension Method

Now, let’s see how we can define an extension method. In our case, we’ll see how we can add additional methods to Scala’s String type and access them like any other String method:

object StringExtensions {
  extension (str: String) {
    def toSnakeCase = {
      str.replaceAll("([A-Z])", "_" + "$1").toLowerCase
    }
  }
}

Once we import the StringExtensions, we can use the method toSnakeCase() just like any other String method:

import StringExtensions._
val str = "helloWorldScala3"
val snakeCaseResult = str.toSnakeCase
snakeCaseResult shouldBe expected

4. Defining Multiple Extension Methods

We can define multiple extension methods for a specific type in the same class/object:

object StringExtensions {
  extension (str: String) {
    def toSnakeCase = {
      str.replaceAll("([A-Z])", "_" + "$1").toLowerCase
    }
    def isNumber = {
      str.matches("[0-9]+")
    }
  }
}

Both the methods isNumber() and toSnakeCase() are defined within the same class StringExtensions.

5. Generic Extension Methods

We can also extend generic types by adding type parameters to an extension.

5.1. Generic Extensions Without Restriction

We can define extension methods for any generic type. For example, let’s extend the List type and add an extension method:

object GenericExtensions {
  extension [T](list: List[T]) {
    def getSecond = if(list.isEmpty) None else list.tail.headOption
  }
}

Now, we can invoke the method getSecond() for List[String], List[Int], and so on:

import GenericExtensions._
val intList: List[Int] = List(1,2,3,4)
val strList: List[String] = List("s1","s2")
val intSec = intList.getSecond
intSec shouldBe Some(2)
val strSec = strList.getSecond
strSec shouldBe Some("s2")

5.2. Extension Method With Type Constraints

We can define extension methods that have type constraints:

extension [T: Numeric](a: T) {
  def add(b:T): T = {
    val numeric = summon[Numeric[T]]
    numeric.plus(a, b)
  }
}

The method add() is now available for any subtypes of Numeric like Int, Float, and Double:

1.add(2)
1.2f.add(2.2f)

5.3. Extension Methods With using Parameter

Instead of using a type constraint, we can also pass the type as a using clause. Let’s rewrite the previous method using the using clause:

extension [T](a: T)(using numeric: Numeric[T]) {
  def add2(b:T): T = {
    numeric.plus(a, b)
  }
}

Now, we can invoke the method add2() in the same way as we do for add():

1.add2(2)

6. Conclusion

In this article, we looked at how we can define extension methods in different ways. As always, the sample code used in this tutorial is available over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.