1. Overview

A Domain-Specific Language (DSL) is a programming language specialized in solving problems in a particular and finite domain.

Internal DSLs are hosted and save us the need to write our tools to interpret the language. But the host language defines the limits of our DSL. With rigid languages like Java or C#, the best we can do are libraries with fluent interfaces.

Scala’s flexible syntax makes it an ideal host for DSLs.  In this tutorial, we’ll write a DSL in which we will be able to write 20 seconds instead of:

new FiniteDuration(20, SECONDS)

2. A Word of Caution

The implementation of scala.concurrent.duration is compatible with pre-2.10 versions of Scala and uses implicit conversions. We are going to use implicit classes instead.

Both approaches are equivalent, but implicit classes are less tedious and safer.

3. But How Does It Work?

To write our syntactic sugar, we’ll need to do at least two things: Create a wrapper class and then making it implicit. After that, the language itself gives us a lot of sugar on its own.

3.1. Wrapper Class

Let’s now write our spoonful of sugar, a wrapper class:

package com.baeldung.scala

import scala.concurrent.duration.{FiniteDuration, MILLISECONDS, MINUTES, SECONDS}

class DurationSugar(time: Long) {
  def milliseconds: FiniteDuration = new FiniteDuration(time, MILLISECONDS)

  def seconds: FiniteDuration = new FiniteDuration(time, SECONDS)

  def minutes: FiniteDuration = new FiniteDuration(time, MINUTES)
}

Now we can write:

(new DurationSugar(20)).seconds

This doesn’t look like an improvement. But we now have a module to host methods for more conversions like minutes and hours.

Note, by the way, that we needed to import explicitly the members of scala.concurrent.duration instead of all the member packages.

3.2. Using Implicit Classes

DurationSugar takes only one argument, and we could mark it as implicit. Unfortunately, the compiler will complain that implicit classes can’t be top-level definitions. We should also think about how we would like to use our language.

Ideally, a single import should bring everything we need into the scope. Our goal is to be able to write:

import com.baeldung.scala.durationsugar._

object Main {
  println(20 seconds)
}

We can achieve this by moving our class to a package object and making it implicit; this will allow us to write 20.seconds():

package object durationsugar {

  implicit class DurationSugar(time: Long) {
    def milliseconds: FiniteDuration = new FiniteDuration(time, MILLISECONDS)

    def seconds: FiniteDuration = new FiniteDuration(time, SECONDS)

    def minutes: FiniteDuration = new FiniteDuration(time, MINUTES)
  }
}

The name of the implicit class should not clash with any object, method, or member in the user scope. They can’t be case classes because of this, but since we’ll never instantiate them explicitly, we should err on the side of longer names to avoid name clashes with the code of our users.

We need to be careful to not declare any implicit class with more than one argument in the constructor. Scala will allow it, but it won’t use it during the implicit lookup, making it the same as a standard class.

3.3. The Rest Is Free

Scala doesn’t require anything else from us, and we get the rest of the sugar for free.

We can omit the parentheses from any calls to parameterless methods, enabling us to write:

"20.seconds" should "equal the object created with the native scala sugar" in {
  20.seconds shouldBe new FiniteDuration(20, SECONDS)
}

And we can omit the dot, except when the lack of semi-colon confuses the compiler, allowing us to write 20 seconds.

However, we should usually avoid the dotless syntax, since the compiler can get confused under certain circumstances when more code follows one of our methods. And the compiler error message can be confusing:

Compiler Error

4. Making It Sweeter

We can use implicit classes to do more than fancy constructors; with them, we can extend classes outside our control with new methods:

implicit class DurationOps(duration: FiniteDuration) {
  def ++(other: FiniteDuration): FiniteDuration =
    (duration.unit, other.unit) match {
      case (a, b) if a == b =>
        new FiniteDuration(duration.length + other.length, duration.unit)
      case (MILLISECONDS, _) =>
        new FiniteDuration(duration.length + other.toMillis, MILLISECONDS)
      case (_, MILLISECONDS) =>
        new FiniteDuration(duration.toMillis + other.length, MILLISECONDS)
      case (SECONDS, _) =>
        new FiniteDuration(duration.length + other.toSeconds, SECONDS)
      case (_, SECONDS) =>
        new FiniteDuration(duration.toSeconds + other.length, SECONDS)
    }
}

We have to use ++ because the class we are wrapping already has a method named +.

Since we do not have a different instance for MILLISECONDS and SECONDS. We have to use Pattern Matching to differentiate between the units and convert to the smaller one before adding them.

And thanks to Scala’s flexible naming rules, we can enhance classes with methods that look like operators:

"20.seconds ++ 30.seconds" should "be equal to 50.seconds" in {
  20.seconds ++ 30.seconds shouldBe 50.seconds
}

"20.seconds ++ 1.minutes" should "be equal to 80.seconds" in {
  20.seconds ++ 1.minutes shouldBe 80.seconds
}

We should never forget that Scala has no distinction between methods and operators. It merely allows us to use a rich character set in method names and then use the same flexible syntax rules to write our code to look like special operations on the types.

5. Conclusion

Scala’s implicit classes are an efficient way to add functionality to existing types. They enable us to create hosted mini-languages that go beyond the fluent interfaces standard in Java.

We needed only three features, but Scala’s DSL capabilities do not end there. Other features useful for DSL creation are companion objects, apply methods, higher-order functions, and lambda functions. We also have the option to replace parentheses with curly braces to call methods that accept only one argument.

As always, the full source code of the article 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.