Authors Top

We’re starting a new Scala area. If you have a few years of experience in the Scala ecosystem, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

1. Introduction

Scala 3 introduced a lot of changes to the core language features. One of the most notable and radical changes is the complete overhaul of metaprogramming.

In this tutorial, we’ll discuss inline, one of the newly introduced metaprogramming features.

2. inline Modifier

Scala 3 introduced the inline modifier to simplify metaprogramming. Most common metaprogramming requirements can be easily implemented using an inline modifier. We can achieve more complex requirements using other low-level metaprogramming concepts like Macros, ReflectAPI, and Run-Time Multi Staging.

3. inline in Scala 2 vs Scala 3

In Scala 2, there is the @inline annotation. However, it’s merely a suggestion, and it doesn’t guarantee the code is inlined. In Scala 3, the inline modifier guarantees that the code is inlined and will give a compilation error if it’s not possible to inline the code.

4. inline Usages

There are multiple ways in which we can use the inline keyword. We can apply inline to variables, functions, match statements, if conditions, and so on. Next, we’ll look at the different ways of using the inline keyword.

4.1. inline def

inline def moves the code from definition-site to call-site. Let’s look at it with an example:

inline def answer(name:String): Unit = println(s"Elementary, my dear $name")
@main def main() = answer("Watson")

The Scala compiler will convert the above code to:

@main def main() = println(s"Elementary, my dear Watson")

This will provide two benefits:

  • avoids a method invocation
  • allows the compiler to apply more optimizations

4.2. inline val

inline val instructs the compiler that the value is a compile-time constant and that the variable can be replaced with the actual value at compile time.

Let’s see it in action:

@main def hello: Unit = 
  if(debugLogEnabled){
    println("Hello world!")
  }
inline val debugLogEnabled = true

During the compilation, the compiler will apply the optimizations in two steps.

Step 1: Replace the if condition with the inline val value. Therefore, it becomes:

@main def hello: Unit = 
  if(true){
    println("Hello world!")
  }

Step 2: Again, the compiler simplifies the if condition, since the value is a constant:

@main def hello: Unit = 
  println("Hello world!")

4.3. Combination of inline val and def

We can use both inline def and val in a single program or function:

inline val debugLogEnabled = true
inline def logTime[T](fn: => T): T = {
  if(debugLogEnabled) {
    val startTime = System.currentTimeMillis
    val execRes = fn
    val endTime = System.currentTimeMillis
    println(s"Took ${endTime-startTime} millis")
    execRes
  } else fn
}

We can now invoke the above method as:

@main def hello: Unit = 
  logTime { myLongRunningMethod }

When we compile the above code, the compiler first inlines the def at the call site (main method, in this case) as:

@main def hello: Unit = 
  if(debugLogEnabled) {
    val startTime = System.currentTimeMillis
    val execRes = myLongRunningMethod
    val endTime = System.currentTimeMillis
    println(s"Took ${endTime-startTime} nanos")
    execRes
  } else myLongRunningMethod

The compiler then will inline the inline val:

@main def hello: Unit = 
  if(true) {
    val startTime = System.currentTimeMillis
    val execRes = myLongRunningMethod
    val endTime = System.currentTimeMillis
    println(s"Took ${endTime-startTime} nanos")
    execRes
  } else myLongRunningMethod

Since the if the condition is a constant value, the compiler again optimizes the block by removing the else block.

4.4. inline if Condition

We can also use the inline modifier for if conditions as well. If the inline keyword is applied to an if condition, it ensures that at compile-time, either the if or else branch is removed, based on the constant optimization. If the compiler can’t do the simplification, it will throw a compilation error.

Let’s see how we can implement it:

inline def hello: Unit = 
  inline if(debugLogEnabled) {
    println("debug is enabled")
  } else { 
    println("debug is disabled")
  }
inline val debugLogEnabled = true

Note that the inline modifier is applied before the if condition. If we remove the inline modifier from the variable debugLogEnabled, then the compiler will not inline the variable and, subsequently, it will not be able to optimize the if block. In this case, the compiler will show a compilation error.

Additionally, we can apply an inline if modifier only for blocks inside an inline method. That means if we remove the inline modifier from the method hello in the above code block, it will not compile.

4.5. inline Match

Similar to inline if, we can apply the inline modifier to match statements as well:

inline def classify(value: Any) = {
  inline value match {
    case int: Int    => "Int"
    case str: String => "String"
  }
}
@main def main = classify("hello")

The compiler will try to reduce the match to a single branch at compile time. If this isn’t possible, we’ll see a compiler error.

Note that an inline match statement can be used only within an inline method.

4.6. inline Parameter

We can also use the inline modifier for method parameters. While optimizing the inline method, the compiler will replace the inline parameter of the method with the value from the call site. Let’s have a look:

inline def show(inline a: Int) = {
  println("Value of a = " + a)
  println("Again value of a = " + a)
}
@main def main = show(Random.nextInt())

In the above code, the compiler will replace the usage of a with Random.nextInt(). Therefore, if we execute the code, we’ll see different values of a for each print statement.

4.7. Transparent inline

Since inline expansions are done at compile-time, the compiler can type-check the values with even more clarity:

inline def sum(a: Int, b: Int) = a + b
val total = sum(10,20)

In the above code, the compiler will calculate the sum of 10 and 20 and assign the value 30 to the variable total. However, by default, the type of the variable total is Int.

Since we know the value at compile-time, we can provide the type as Literal Type 3o. We can instruct the compiler to use the most specialized type if available, by using the keyword transparent. Now, let’s see it in action:

transparent inline def sum(a: Int, b: Int) = a + b
val total: 30 = sum(10, 20) // The type is Literal Value 30 instead of Int

5. Conclusion

In this article, we discussed different ways in which we can use the inline modifier in Scala 3. Inlining allows the Scala compiler to optimize the code more efficiently during the compilation and improve its performance.

As always, the code samples used in this article are available over on GitHub.

Authors Bottom

We’re starting a new Scala area. If you have a few years of experience in the Scala ecosystem, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

Comments are closed on this article!