1. Overview

In this tutorial, we’ll learn about implicit classes. We’ll learn about their attributes, limitations, and how they can be used for adding methods to existing classes.

2. Implicit Classes

Implicit classes are available since Scala 2.10 and give us the ability to add additional methods to existing classes. Thanks to that, we can easily add some useful methods to built-in classes like Int or String.

3. A Money Use Case

Let’s imagine that we have a Money case class, that contains amount and currency:

sealed trait Currency
object Currency {
  case object EUR extends Currency
  case object USD extends Currency
  case object GBP extends Currency
}

case class Money(amount: Double, currency: Currency)

We would like to have a convenient way of creating Money using the following syntax:

val amount: Double = 30.5

val euros: Money = amount.euros
val dollars: Money = amount.dollars
val pounds: Money = amount.pounds

4. Creating Money Syntax

To achieve that, we need to provide an implicit class that adds euros, dollars, and pounds methods to the Double class:

object MoneySyntax {
  implicit class RichMoney(val amount: Double) extends AnyVal {
    def euros: Money = Money(amount, Currency.EUR)
    def dollars: Money = Money(amount, Currency.USD)
    def pounds: Money = Money(amount, Currency.GBP)
  }
}

In the above example, we’ve created an implicit class called RichMoney that adds euros, dollars, and pounds methods for any Double. It also extends AnyVal to avoid runtime allocation.

5. Using Money Syntax

Now that we’ve created an implicit class, it’s time to use it and verify if the new syntax works:

import MoneySyntax._

val amount: Double = 30.5
  
amount.dollars shouldBe Money(amount, Currency.USD)
amount.euros shouldBe Money(amount, Currency.EUR)
amount.pounds shouldBe Money(amount, Currency.GBP)

First, we need to make the implicit class RichMoney available in scope by using import MoneySyntax._.

The compiler knows that there is no dollars method available on Double, so it tries to find any available implicit class that adds the dollars method to Double. Thanks to the import, RichMoney is available in the scope and makes the compiler happy.

Likewise, the same happens for the euros and pounds methods.

6. Limitations

Unfortunately, not all classes can be implicit classes because

  • They cannot be defined as top-level objects
  • They cannot take multiple non-implicit arguments in their constructor
  • We cannot use implicit classes with case classes
  • There cannot be any member or object in scope with the same name as the implicit class

6.1. Top-level Objects

They cannot be defined as top-level objects:

implicit class RichMoney(amount: Double) {
  def euros: Money = Money(amount, Currency.EUR)
  def dollars: Money = Money(amount, Currency.USD)
  def pounds: Money = Money(amount, Currency.GBP)
}

This won’t compile because: ‘implicit’ modifier cannot be used for top-level objects.

Therefore we need to move RichMoney into a trait, class, object, or package object:

trait MoneySyntax {
  implicit class RichMoney(amount: Double) {
    def euros: Money = Money(amount, Currency.EUR)
    def dollars: Money = Money(amount, Currency.USD)
    def pounds: Money = Money(amount, Currency.GBP)
  }
}

If we want to avoid RichMoney allocation by extending AnyVal, we’re limited only to objects and package objects, because the ‘value class may not be member of another class’.

object MoneySyntax {
  implicit class RichMoney(val amount: Double) extends AnyVal {
    def euros: Money = Money(amount, Currency.EUR)
    def dollars: Money = Money(amount, Currency.USD)
    def pounds: Money = Money(amount, Currency.GBP)
  }
}

6.2. Non-implicit Arguments

They cannot take multiple non-implicit arguments in their constructor:

object MoneySyntax {
  implicit class RichMoney(amount: Double, secondArg: Boolean) {
    def euros: Money = Money(amount, Currency.EUR)
    def dollars: Money = Money(amount, Currency.USD)
    def pounds: Money = Money(amount, Currency.GBP)
  }
}

The above code won’t compile because ‘implicit class must have a primary constructor with exactly one argument in first parameter list’.

However, having an additional implicit argument is not a problem:

object MoneySyntax {
  implicit class RichMoney(amount: Double)(implicit secondArg: Boolean) {
    def euros: Money = Money(amount, Currency.EUR)
    def dollars: Money = Money(amount, Currency.USD)
    def pounds: Money = Money(amount, Currency.GBP)
  }
}

6.3. Case Classes

We cannot use implicit classes with case classes:

object MoneySyntax {
  implicit case class RichMoney(amount: Double) {
    def euros: Money = Money(amount, Currency.EUR)
    def dollars: Money = Money(amount, Currency.USD)
    def pounds: Money = Money(amount, Currency.GBP)
  }
}

The compiler will fail with the message: ‘illegal combination of modifiers: implicit and case for: class RichMoney’.

6.4. Scope

There cannot be any member or object in scope with the same name as the implicit class:

import MoneySyntax._
val amount: Double = 30.5
val RichMoney = 1

amount.dollars

Therefore if we defined a variable with the same name as the implicit class imported by import MoneySyntax._, it won’t compile.

The same will happen for object:

import MoneySyntax._
val amount: Double = 30.5
object RichMoney

amount.dollars

7. Conclusion

In this short article, we explored implicit classes. We saw how they can be used to create nicer syntax and learned about their limitations.

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

Comments are closed on this article!