1. Overview

In this tutorial, we’ll see how we can use annotations in Scala for meta-programming.

2. Annotations

Annotations are a programming language feature that allows us to add different behaviors. Some common behaviors include extra compile-time checks, code generations, or just meta information. We more frequently see annotated classes, but we can also annotate variables, types, and expressions as well:

@deprecated("Use D", "1.0") class C { ... } // Class annotation
@transient @volatile var m: Int             // Variable annotation
String @local                               // Type annotation
(e: @unchecked) match { ... }               // Expression annotation

2.1. Annotations to Ensure Correctness

Scala supports two types of annotations. The first type is an annotation that will add more compile-time checks. This type of annotation can actually cause compilation to fail if a given condition is met. A common example is the @tailrec annotation, which ensures that a method is tail-recursive. Let’s start by defining two recursive methods to compute a list length, one tail-recursive and the other not:

def recursiveLength(list: List[String]): Long = list match {
  case Nil => 0
  case head :: tail => 1 + recursiveLength(tail)

def tailRecursiveLength(list: List[String], accumulator: Long): Long = list match {
  case Nil => accumulator
  case head :: tail => tailRecursiveLength(tail, accumulator + 1)

Both methods work just fine. But due to a subtle bug, the first function is not tail-recursive. Scala annotations can help us here. By adding the @tailrec annotation, the compiler would fail to compile the code:

scala> @tailrec
     | def recursiveLength(list: List[String]): Long = list match {
     |         case Nil => 0
     |         case head :: tail => 1 + recursiveLength(tail)
     |     }
4 |        case head :: tail => 1 + recursiveLength(tail)
  |                                 ^^^^^^^^^^^^^^^^^^^^^
  |                 Cannot rewrite recursive call: it is not in tail position

By creating our own annotations we can add many more compile-time checks as we need.

2.2. Annotations Affecting Code Generation

The second type of annotation is one that generates code. For instance, the @inline scala annotation changes the way the method bytecode is generated to improve performance:

final def sum(x: Int, y: Int): Int = 
  x + y

2.3. User-Defined Annotations

A developer can also define new annotations in the codebase. For that, we need to create a new class that will extend from scala.annotation.StaticAnnotation or scala.annotation.ClassfileAnnotation. The first will be visible to the compiler, but not in the class file nor at runtime. The latter will be visibly stored in the annotated class, but not retained at runtime.

scala> class MyAnnotation extends scala.annotation.ClassfileAnnotation

@MyAnnotation class Bar

3. Conclusion

In this article, we looked into annotations, one of the most common meta-programming techniques. This language feature allows us to extend the compiler capabilities or to generate code from a simple annotation placed in our codebase.

Comments are closed on this article!