Annotations in Scala

Versions: Scala 2.12.1

When I was working with Java and Spring framework, the annotations were my daily friend. When I have started to work with Scala, I haven't seen them a lot. It was quite surprising at the beginning. But with every new written line of code, I have started to see them more and more. After that time it's a good moment to summarize the experience and focus on the Scala annotations.

New ebook 🔥

Learn 84 ways to solve common data engineering problems with cloud services.

👉 I want my Early Access edition

This post is divided into 3 parts. In the 2 first parts, I will explain the annotations and show how to write a custom one. In the last section, I will focus on the annotations already present in the language.

Annotations definition

In a nutshell, Scala annotation is a meta-information associated to an element. The built-in annotations can have an effect on the compiled code and can even make the compilation process fail. I will give you some examples of such annotations in the second section.

Among the annotable elements, you can find:

Custom annotation

Any Scala's annotation extends scala.annotation.Annotation abstract class, either directly or indirectly because this class has 2 children traits, ClassfileAnnotation or StaticAnnotation. You should extend one of them if you want to access your annotation directly from the reflection API. Using the Annotation class directly won't make it accessible. You can see that in the following test:

import scala.reflect.runtime.universe._


class CustomAnnotationTest extends FlatSpec with Matchers {

  "not StaticAnnotation" should "not be accessible through the reflection" in {
    typeOf[Helper].typeSymbol.annotations should have size 0
  }

  "StaticAnnotation" should "be accessible through the reflection" in {
    val mappedAnnotations = typeOf[HelperReflective].typeSymbol.annotations.map(annotation => annotation.toString)

    mappedAnnotations should have size 1
    mappedAnnotations.head shouldEqual "annot.todo_reflective(scala.collection.Seq.apply[String](\"implement me\", \"think about another name\"))"
  }

  "ClassfileAnnotation" should "be accessible through the reflection" in {
    val mappedAnnotations = typeOf[HelperClassfile].typeSymbol.annotations.map(annotation => annotation.toString)

    mappedAnnotations should have size 1
    mappedAnnotations.head shouldEqual "annot.todo_classfile"
  }

}

class todo(todos: Seq[String]) extends Annotation {}
class todo_reflective(todos: Seq[String]) extends StaticAnnotation {}
class todo_classfile extends ClassfileAnnotation {}

@todo(Seq("implement me", "think about another name"))
class Helper {}

@todo_reflective(Seq("implement me", "think about another name"))
class HelperReflective {}

@todo_classfile
class HelperClassfile

What is then the difference between StaticAnnotation and ClassfileAnnotation ? ClassfileAnnotation was intended to be retained at runtime. However, if you try to compile the code using it, the compiler will prevent you that it's not possible:

Warning:(37, 7) Implementation restriction: subclassing Classfile does not
make your annotation visible at runtime.  If that is what
you want, you must write the annotation class in Java.

Except that intention, the difference are the restrictions. ClassfileAnnotation, like Java annotation, accepts only the constant expressions as the parameters. If you try to define them differently, for instance like this, you will get a compilation error:

class todo_reflective_classfile(todos: Seq[String]) extends ClassfileAnnotation {}

@todo_reflective_classfile(todos = Seq("implement me"))
class HelperReflectiveClassfile

And the error will be:

Error:(33, 39) annotation argument needs to be a constant; found: scala.collection.Seq.apply[String]("implement me")
@todo_reflective_classfile(todos = Seq("implement me"))

Just one more thing to precise before approaching TypeConstraint trait. The ClassfileAnnotation is the subclass of the StaticAnnotation.

Annotations examples

Among the annotations implemented in Scala you will find:

Throughout these examples you can find some main use cases of the annotations. The former one is to forbid some illegal behavior at the compilation time. It's, for instance, the case of @tailrec. The second use case is communication. Some of the annotations can be printed at the compile time to signal the behavior that can lead to problems in the future. Also, you can define some custom annotations that you will use at runtime. However, remember that the use of reflection to read them may be slow down the code execution because it's not a direct call of the reference.

To summarize, the annotations in Scala exist but they operate mainly at the compile time. The compiler can use them to, for instance, disallow not optimized code execution (e.g. @tailrec) or to prevent the programmer against the poor quality of his code. You can also define your own annotations by extending one of the basic traits. But be careful because depending on the used one, you will or will not be able to work with them at the runtime.