In this blog we will understand the basics of Macros and reflection. The main motive of the blog is to lay a foundation for better understanding of the the ‘Async library’.
The ‘MAGIC’ of macros ->
Macros can be called ‘special functions’ that are called by the compiler during compilation. These functions are special as using macros we can access the compiler API’s which provide us with the privileged to manage the AST(Abstract Syntax Tree) generated by the compiler. AST is a data structure used by the Scala compiler to store the information about the compiled code. Macros give programmer the power to :-
a). Inspecting type of an object, including generic type (Traversing, inspecting the AST)
b). Creating new objects.(Appending the AST with new child)
c). Access the member function of the object(Accessing the child nodes in AST)
In Scala macros are a way to implement compile time reflection. One advantage that macros possess is that they are based on the same API which is also used for Scala runtime reflection and is provided in ‘scala.reflect.api’ package. Hence macros also has the ability to visit and manipulate generic code which is a feature of run time reflection.
Stated simply, macros(The reflection cousin) can be decomposed into two parts:
a). Introspection: a program can examine itself.
b). Intercession: a program can modify its state/meaning.
Now let us programmatically understand how micro are implemented. For better Understanding I have split the example into following steps.
Step 1 ‘Declaring a macro definition’ -> (Fabricating the body)
To use macro in the project add following dependency to your ‘build.sbt’ :-
Macro definition follows the following prototype :
‘m’ is the name of the method
‘x’ is the parameter of ‘m’
‘R’ is the return type of ‘m’
‘macro’ is the keyword
‘implref’ is another method which provide the implementation for macro
Macro definition for our example :-
The above method takes two integers and returns its sum, the implementation of the addition operation is provided in addImpl method. Here ‘add’ is our macro whose implementation is provided in next step.
Step 2 ‘Implimenting a macro’ -> (Putting in the soal)
Macro implementation is the method which defines the functionality of macro. Though the macro implementation is a bit different from the normal method in a way that the macro implementation work on AST (Abstract Syntax Tree) and is called at the compile time with AST of the parameters rather than the parameter itself and also returns an AST of its return value.
Let us continue with the above example and provide functionality for the ‘add’ macro.
c is a context parameter that contains information collected by the compiler at compile time
‘num1’, ‘num2’ are the AST of the two operands to be added
‘addImpl’ has an AST of type integer as its return type.
The type ‘c.Expr[Int]’ means the parameters are AST’s of the expression with their type being integer.
Macros implementation method returns an AST and this is achieved using the ‘reify’ and ‘splice’ method.
a). reify() – The ‘reify’ method is in itself a macro which turns the code enclosed in its scope into its corresponding AST and type. In our example, ‘reify’ returns the AST of sum of the values of ‘num1’ and ‘num2′(values are obtained using splice) with the type of AST being integer.
b). splice() – The ‘splice’ method is a programmatic antonym of ‘reify’ as it turns the AST into a value with corresponding type. It can only be used inside the scope of ‘reify’. In our example, using ‘splice’ we convert ‘num1’ parameter which is an AST of type integer into a value of type integer.
Step 3 ‘The compiler magic’ -> (sparking in life)
In this step we will learn the sequence of events that occur when the compiler encounters a macro
Firstly, while compiling the code if compiler detects a macro, the corresponding macro implementation is evoked and AST of the parameters of the macro are send as the argument to macro implementation(using reflection).
Call to macro implementation transforms in following way :-
is converted to
Here AST< Expr > represents the abstract syntax tree for the expression ‘Expr’, this notation is used only for the blog actually the AST are made by ‘scala.reflect.api.Trees’. In reality the AST of the parameter ‘2’ would be as follows :
Secondly, the return value of macro implementation which in itself is an AST gets inlined (attached) to the AST of the main program and is type checked in turn. In other words, the macro declaration acts as the part of main AST to which the AST of the returned value of macro implementation is attached.
A very important note is that the call to macro can not be present in same class in which the macro and its implementation are present. In this example the call to the macro is made in the test case(using scalatest) i.e. :
With this an overview to macros and reflection comes to an end. We will now be focussing on ‘async’, ‘await’ of the Scala Async library.
Macro Implementation Git Code : https://github.com/knoldus/macros-example
Happy Coding …..