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.
Data Engineering Design Patterns
Looking for a book that defines and solves most common data engineering problems? I'm currently writing
one on that topic and the first chapters are already available in π
Early Release on the O'Reilly platform
I also help solve your data engineering problems π contact@waitingforcode.com π©
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:
- classes
@deprecated("This class shouldn't be used. Use instead NewRule") class SomeRule {}
- methods
@deprecated("This method is deprecated. Use applyNewRule2") def applyNewRule() {}
- fields
class ValidationRule { @deprecated("Use NewValidationThreshold") val ValidationThreshold = 5
- local variables
def main(args: Array[String]): Unit = { @transient val text = "text" }
- parameters
def passParameter(@deprecated("This parameter is deprecated") parameter: String)
- type parameters - I shown this annotation in one of the previous posts about Type specialization in Scala:
class GlobalSpecialization[@specialized T] { def get(item: T) = item }
- expressions
(letter: @unchecked) match { case _: String => "string type" }
- primary constructor - the annotation is placed before it:
class NewClass @deprecated("This constructor is deprecated") (param1: String, param2: String)
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:
- @tailrec - I described it shortly in the Scala for loop post. Like you can read there, @tailrec will throw a compilation error if the annotated recursive function can't be optimized with a loop
- @deprecated - is another common annotation used in Scala. If you annotate an element with it and use it in your code, the compiler will print a warning about the use of a deprecated code:
Warning:scalac: there was one deprecation warning Warning:scalac: there were four deprecation warnings (since 2.11.0) Warning:scalac: there were 5 deprecation warnings in total; re-run with -deprecation for details
- @transient - it denotes the field that should not be serialized.
- @volatile - marks given field as a volatile. Under-the-hood the field is written as Java's volatile variable:
object AnnotationsTest { @volatile var x = 3
That code will be compiled as:private volatile int x; descriptor: I flags: ACC_PRIVATE, ACC_VOLATILE
- @inline - the special demand for the compiler to try really hard to inline the annotated method.
- @throws - defines the exceptions thrown by a method. But it's not a constraint, i.e. the method's caller doesn't need to handle the exception explicitly. This annotation has purely informational purpose. Therefore, the following code will compile without an error:
def main(args: Array[String]): Unit = { trySucceed(null) } @throws[NullPointerException]("Something may go wrong") def trySucceed(parameter: String) = parameter.length
- @unchecked - can be used to annotate the match expressions or type arguments in a match case to suppress the exhaustiveness and unchecked warnings.
- @varargs - instructs the compiler to generate a Java varargs-style forwarder method. By default, such methods are compiled as a sequence in the parameter. It may be useful when you need to use the Scala code using varargs in Java.
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.