Meta programming is a popular technique from 1970’s and 1980’s which used languages like LISP to enable applications to process code for artificial intelligence based applications. When a programming language is it’s own meta-language then it is called as reflection. Reflection is one of the important feature for any programming language to facilitate meta programming. Meta programming moves computations from run-time to compile-time thereby enabling self-modifying code. Hence, a program is designed in such a way that it can read, analyse or transform other programs or itself while it is running. This style of programming falls under Generic programming paradigm where the programming language itself is a first-class datatype.
This metaprogramming is exercised in various programming languages for various purposes. In Scala it is used as macro systems, muti-stage programming (runtime staging) etc.
Meta programming in Scala
Meta programming in Scala introduces fundamental features like:
- Macros: built on two fundamental operations: quotation (via. as ‘{…}) and splicing (via. as ${ … }). Along with
inline
, these two abstractions allow to construct program code pro-grammatically. - inline: A new modifier which guarantees definition will be inlined at point of use. It reduces overhead of function call and values access.
- Compile-time ops: helper functions that provides support for compile time operations like
constValue
andconstValueOpt
- Runtime Staging: To make code generation depend on run time data, staging lets code construct new code at runtime.
- Reflection
- TASTy Inspection: Typed Abstract Syntax Tree allows to load files and analyse their content in tree structure.
Adding these new meta programming capabilities adds enormous benefits and privileges for eliminating the boilerplate code and improving the overall performance of applications. With meta programming developers in scala can leverage their applications performance and remove all redundant & boilerplate code with use of these features.
Trade-Offs: Macros over Functions
Execution Time: With macros we can make execution comparatively faster. As during processing a macro is expanded and replaced by its definition each time its used. On the other hand function definition occurs only once irrespective of number of times its called. Macros might increase code s lines of code but don’t have overhead associated with function calls.
Clean Code with Macros
Repeated Code: Even though Scala syntax is concise that avoids boilerplate code that occurs in other JVM programming language. But still there are scenarios where developers might end up writing repetitive code and which can’t be refactored further for reuse. With Scala macros we can keep code clean and maintainable.
Meta programming Features Snippets
Inlined Method Example
inline def repeat(s: String, count: Int): String =
inline count match
case 0 => ""
case _ => s + repeat(s, count-1)
Macros Example
import scala.quoted.*
// Note the quote '{...} and the argument types
private def failImpl[T](
predicate: Expr[Boolean], message: Expr[String],
block: Expr[T], beforeAfter: Expr[String])(
using Quotes): Expr[String] =
'{ throw InvariantFailure(
s"""FAILURE! predicate "${${showExpr(predicate)}}" """
+ s"""failed ${$beforeAfter} evaluation of block:"""
+ s""" "${${showExpr(block)}}". Message = "${$message}". """)
}
private def showExpr[T](expr: Expr[T])(using Quotes): Expr[String] =
val code: String = expr.show
Expr(code)
TASTy Inspection Example
<Sample .tasty file>
import scala.quoted.*
import scala.tasty.inspector.*
class MyInspector extends Inspector:
def inspect(using Quotes)(tastys: List[Tasty[quotes.type]]): Unit =
import quotes.reflect.*
for tasty <- tastys do
val tree = tasty.ast
// Your code here
Consumer of above .tasty file
object Test:
def main(args: Array[String]): Unit =
val tastyFiles = List("sample.tasty")
TastyInspector.inspectTastyFiles(tastyFiles)(new MyInspector)
Compile-time ops Example
/* constValue - function to produce constant value represented by a type, or
a compile time error if the type is not a constant type. constValueOpt is
same as constValue, however it returns Option[T] to handle where a value is
not present. */
import scala.compiletime.constValue
import scala.compiletime.ops.int.S
transparent inline def toIntConst[N]: Int =
inline constValue[N] match
case 0 => 0
case _: S[n1] => 1 + toIntConst[n1]
inline val constTwo = toIntConst[2]
Meta programming Applications
Program transformation systems can be helpful to build:
- Test coverage and profiling tools
- Code generation & completion tools
- Automated Refactoring tools
- Language migration tools
- Tools to re-architecture/re-shape applications
- Build Domain Specific Languages via. Metaprogramming
- Project Templates
- Dynamic Dispatch
- Reflection
- Aspect Oriented Programming (to resolve issues w.r.t logging, transaction management etc.)
- GUI code generation
- Compilers & Interpreters implementation
- Frameworks
- ORM in dynamic language
Conclusion
In statically typed languages like Java and Scala, meta programming is more constrained and is very less common. But it’s still useful for solving many advance real-time design problems. With more effort to separate compile-time versus runtime manipulation. Also, it gives more flexibility and configuration at runtime. Overall program can become more expressive and flexible with these techniques.


