Learn myself some Scala 3, episode 1: metaprogramming – inline

Martin Odersky’s keynote talk at Scala Days 2019 made me realize, that I should pay a lot more attention to Scala 3 (a.k.a. Dotty) than I did so far. For me it now seems like Scala 3 will be a huge improvement over Scala 2, in particular because it significantly increases clearness and feature orthogonality. Just take a look at how the somewhat magic – and therefore hard to understand – realm of implicits has transitioned to given clauses which state the dependency on some “contextual” value and delegates which allow us to define these values – just beautiful!

I have been travelling the Scala universe for long enough to take the timeline Martin presented – Scala 3 being released in 2020 – with a grain of salt, but nevertheless I think it is time to start learning myself some Scala 3 right away. And what could be better for learning something than writing about it? Hence I decided to write this blog post and hopefully some follow-up ones.

For this blog post I picked metaprogramming, which is a fairly advanced feature. Yet I will keep it super simple which is possible thanks to a new feature in Scala 3.

Some years ago, when I was learning Scala 2 macros, I created Scala Logging, a convenient and fast logging library wrapping SLF4J, which has been improved and maintained by others for the past years. It is simple yet useful, because it allows developers to just call the logging methods like debug, which get expanded to the check-enabled-idiom. So let us see, how this can be done in Scala 3.

To make it crystal clear, the following is what we want to achieve. We want to be able to just call the debug method, without having to first check isDebugEnabled:

1
logger.debug(hello())

This should be compiled to the check-enabled-idiom, i.e. we want the hello method to be evaluated only if isDebugEnabled is true:

1
if (logger.isDebugEnabled) logger.debug(hello())

Scala 3 there comes with the new inline keyword which is sort of an enabler for various metaprogramming features like macros. But even on its own it is quite powerful as we will see.

An inline method is guaranteed to be inlined by the Scala 3 compiler, which means that the method body will be expanded into any call site, i.e. any code which calls the respecive method. Hence we can define a Logger class which wraps an underlying Logger from Log4j 2 and has an inlined debug method which takes the log message as a by-name parameter and applies the check-enabled-idiom:

1
2
3
4
5
6
import org.apache.logging.log4j.{ Logger => Underlying }

final class Logger(underlying: Underlying) {
inline def debug(message: => String): Unit =
if (underlying.isDebugEnabled) underlying.debug(message)
}

Now let us add some code using the above:

1
2
3
4
5
6
7
8
9
10
11
import org.apache.logging.log4j.LogManager

def main(args: Array[String]): Unit = {
val logger = new Logger(LogManager.getLogger(getClass))
logger.debug(hello())
}

private def hello() = {
println("********** hello() was called! **********")
"Hello!"
}

When we run this code, we see that the hello method only gets called, when the log level is set to DEBUG. This becomes clear by inspecting the code which gets generated by the Scala 3 compiler for the main method – we can use javap for this purpose:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
1: invokeinterface #42, 1 // InterfaceMethod rocks/heikoseeberger/log4dotty/Logging.logger:()Lrocks/heikoseeberger/log4dotty/Logger;
6: astore_2
7: aload_2
8: invokevirtual #48 // Method rocks/heikoseeberger/log4dotty/Logger.inline$underlying:()Lorg/apache/logging/log4j/Logger;
11: invokeinterface #54, 1 // InterfaceMethod org/apache/logging/log4j/Logger.isDebugEnabled:()Z
16: ifeq 38
19: aload_2
20: invokevirtual #48 // Method rocks/heikoseeberger/log4dotty/Logger.inline$underlying:()Lorg/apache/logging/log4j/Logger;
23: aload_0
24: invokespecial #58 // Method hello:()Ljava/lang/String;
27: invokeinterface #62, 2 // InterfaceMethod org/apache/logging/log4j/Logger.debug:(Ljava/lang/String;)V
32: getstatic #68 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit;
35: goto 41
38: getstatic #68 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit;
...

As we can see, hello only gets called (pos. 24), if the check whether the DEBUG log level is enabled (pos. 11) has been true; else “nothing” happens, i.e. Unit is returned.

To sum it up, all we need to implement a convenient check-enabled logging library like Scala Logging in Scala 3, is the new inline feature – no further metaprogramming facilities like macros, which are also available in Scala 3, are necessary.

The full source code – in a little more polished fashion – can be found at github.com/hseeberger/log4dotty.