How to create inline methods in Scala 3

In this article I’ll take a look at creating Scala 3 inline methods.

Background

First, from the Scala documentation: “inline is a new soft modifier that guarantees that a definition will be inlined at the point of use.” This is different than the Scala 2 inline annotation, which notes, “An annotation for methods that the optimizer should inline ... If inlining is not possible, for example because the method is not final, an optimizer warning will be issued.”

Given that background, let’s see how the Scala 3 inline keyword works on methods and functions.

Creating a Scala 3 inline method

About the simplest inline example you can create looks like this:

class Writer(var prefix: String):
    inline def print(s: String): Unit =
        if (prefix != null) println(s"${prefix} ${s}")

In this example, the Writer class has a print method that I’ve defined to be an inline method. The main point of interest here is that this method checks to see if prefix is null. If it’s null, nothing happens, and if it’s not null, the println statement is run. That’s what you need to keep an eye on as we go through this exercise.

Creating a “point of use”

As the documentation states, this definition “will be inlined at the point of use,” so what we need now is a “point of use.” To create a point of use, I’ll create a little test App:

object InlineTest extends App:
    val w = Writer("Hello,")
    w.print("world")

On the surface there’s nothing too exciting here, I just create a new Writer instance and then call its print method. Because the prefix parameter is set, when I compile this code with dotc and run it dotr, it results in this output:

Hello, world

Nothing too exciting yet, but now let’s look behind the scenes.

Decompiling the class files

One way to see what’s going on here is to disassemble the Scala/JVM .class files with javap -c. You can do that, but my degree is in aerospace engineering, not computer science, so I tend to prefer other tools.

You might be able to decompile the class files with IntelliJ IDEA, but I don’t know for sure, I didn’t test that. I initially decompiled the code with the Java Decompiler Project, and then ended up using JAD.

I’m amazed that JAD still works, and when I decompiled the InlineTest$.class file, I found this:

    private InlineTest$()
    {
        Writer Writer_this;
        MODULE$ = this;
        App.$init$(this);
        w = new Writer("Hello,");
        Writer_this = w();
        if(Writer_this.prefix() == null) goto _L2; else goto _L1
_L1:
        Predef$.MODULE$.println((new StringBuilder()).append("").append(Writer_this.prefix()).append(" ").append("world").toString());
        BoxedUnit.UNIT;
          goto _L3
_L2:
        BoxedUnit.UNIT;
_L3:
        JVM INSTR pop ;
    }

The key part of what you’re looking at is that all of the print function code in the Writer class ends up in the InlineTest source code. Remember that “if null” test was in the print method in Writer class, and now this code is showing up in the InlineTest class.

Similarly, if I create a second Writer instance named w2 like this:

object InlineTest extends App:
    val w = Writer("Hello,")
    w.print("world")
    val w2 = Writer("Hello,")   //<-- NEW INSTANCE
    w2.print("scala world")

and then go through the same process, I see this code in the JAD output:

    private InlineTest$()
    {
        Writer Writer_this;
        MODULE$ = this;
        App.$init$(this);
        w = new Writer("Hello,");
        Writer_this = w();
        if(Writer_this.prefix() == null) goto _L2; else goto _L1
_L1:
        Predef$.MODULE$.println((new StringBuilder()).append("").append(Writer_this.prefix()).append(" ").append("world").toString());
        BoxedUnit.UNIT;
          goto _L3
_L2:
        BoxedUnit.UNIT;
_L3:
        JVM INSTR pop ;
        Writer Writer_this;
        
        // MY COMMENT: THIS IS THE CODE FOR THE SECOND INSTANCE
        // ----------------------------------------------------
        w2 = new Writer(null);
        Writer_this = w2();
        if(Writer_this.prefix() == null) goto _L5; else goto _L4
_L4:
        Predef$.MODULE$.println((new StringBuilder()).append("").append(Writer_this.prefix()).append(" ").append("scala world").toString());
        BoxedUnit.UNIT;
          goto _L6
_L5:
        BoxedUnit.UNIT;
_L6:
        JVM INSTR pop ;
    }

Lesson learned

So, lesson learned: Each time I use the print method of the Writer class, the logic from the print method is copied wherever I call it. (Technically this is more like “lesson verified,” but it’s still cool to see how it works.)

The Writer##print method

It’s also important to note that when I decompile the Writer class, I see that it has no print method(!). As far as JAD can see, this is what the Writer class looks like:

public class Writer {
    public Writer(String prefix) {
        this.prefix = prefix;
        super();
    }

    public String prefix() {
        return prefix;
    }

    public void prefix_$eq(String x$1) {
        prefix = x$1;
    }

    private String prefix;
}

The print method is not in this class — it’s been inlined everywhere it’s used.

The same steps without the inline keyword

In case you’re not comfortable with what this decompiled code normally looks like, let’s remove the inline keyword from the print method:

class Writer(var prefix: String):
    // REMOVED `inline` HERE
    def print(s: String): Unit =
        if (prefix != null) println(s"${prefix} ${s}")

and then go back to our initial App:

object InlineTest extends App:
    val w = Writer("Hello,")
    w.print("world")

Now when I compile the code with dotc and then decompile the InlineTest$.class file with JAD, I see this output:

public final class InlineTest$ implements App, Serializable {

    private final Writer w = new Writer("Hello,");

    private InlineTest$()
    {
        App.$init$(this);
        w().print("world");
    }
    
    // more code ...
}

That’s an amazing difference, isn’t it? This is what you expect to see with “regular” Scala code — i.e., non-inlined code — the InlineTest class knows nothing about the internals of the Writer class, it just executes its print method.

Now if you decompile the Writer.class file you’ll see that its print method is as you’d expect it to be:

public void print(String s)
{
    if(prefix() != null)
        Predef$.MODULE$.println((new StringBuilder()).append("").append(prefix()).append(" ").append(s).toString());
}

So there’s a huge difference between a “normal” method and an inlined method.

Summary

I’m fairly new to using inline, but the obvious change here is that wherever the print function is called, that call is eliminated and its code is inlined at that point.

Rather than trying to figure out the pros and cons of inlining methods, I google’d “benefits of inline methods,” and this C++ web page provides a list of the pros and cons. The pros of this technique (in C++) start with the elimination of the function call overhead, and then what duplicating that code at every point of use brings to the table. The cons are also related to the elimination of function calls and the duplication of code.

When I add the word “Scala” to that query I find that a user by the name of oxbow_lakes on this Stack Overflow page further states, “Never @inline anything whose implementation might reasonably change and which is going to be a public part of a library.”

In general, everything I have read about inlining methods makes it sound like a performance technique, where you are trading code duplication for better performance. As with any performance-related technique, make sure you test your code to make sure you’re getting the performance improvement you expect. And as I learn more, I’ll update this article.

See also

Besides needing to look into this for my work on the 2nd Edition of the Scala Cookbook, I was inspired to look into this by Heiko Seeberger’s post about inline. In that post he shows a more real-world example of how this can be useful, by limiting how other long-running code can be run as soon as possible.

In addition to his blog post, the Dotty inline documentation shows a logging-related example, and also provides many more details on how this works, including a discussion of “transparent inline” methods. See that documentation for many more details.

On a personal note, I don’t know if the Scala Cookbook V2 will have a recipe like this, but that’s one thing about writing a book like this: you look at hundreds of things and then have to figure out what should be in the book, and what doesn’t make it in the end.