Scala metaprogramming, reflection beyond mirrors by Kalin Rudnicki

March 17, 2026

In Scala metaprogramming, reflection beyond mirrors, Kalin Rudnicki made a bold argument: if you want serious metaprogramming in Scala 3, you should go beyond mirrors and embrace macros.

The talk was both a technical deep dive and a manifesto. Its core message was simple: Scala 3’s macro system is extremely powerful — but most libraries are still leaning on mirrors, and that comes with major trade-offs in safety, control, and generated code quality.

A comprehensive overview: Scala metaprogramming, reflection beyond mirrors

The Two Faces of Scala 3 Metaprogramming

Kalin framed Scala 3 metaprogramming around two approaches:

  • Mirrors
  • Macros

Mirrors are the “standard” derivation mechanism many Scala developers encounter first. They expose structural information about types — class names, field names, field types — at the type level.

That sounds promising. But Kalin’s verdict was blunt: Mirrors are terrible.

Why? Because they make it look like you are doing compile-time metaprogramming, while in practice the compiler often fails to fully optimize what you wrote. When that happens, derivation logic leaks into runtime code.

The result is code that may still work — but is far from ideal.

 

Why Mirrors Fall Short

According to Kalin, mirrors come with three core problems:

  1. They are unpleasant to work with
    Everything is encoded at the type level, which makes even simple tasks awkward without layers of helpers.
  1. They encourage unsafe casting
    Working with mirrors often leads to repeated use of asInstanceOf, even for trivial operations. Once a codebase starts depending on those casts, it becomes easy to hide mistakes behind “just cast it.”
  1. They can generate poor runtime code
    This was a key point of the talk. Kalin showed examples from popular libraries like Circe and zio-json, arguing that mirror-based derivation can result in generated code that still carries around derivation machinery at runtime.

That defeats much of the promise of compile-time specialization.

His summary was harsh but clear: mirror-based derivation often produces bloated, low-quality generated code.

 

Why Macros Are Better

Macros, in contrast, give the developer:

  • type safety
  • full control
  • better composability
  • cleaner generated code

Where mirrors ask the compiler to “figure out what you mean,” macros let you describe precisely what code should be generated.

That means if you want your derived Monoid, JsonCodec, or HTTP route definitions to look a certain way, you can make them look exactly that way.

And Kalin’s claim was not just theoretical: he demonstrated examples where the generated code was nearly indistinguishable from what a careful human would write by hand.

That, for him, is the real standard: If the generated code is clean, readable, and efficient, metaprogramming is doing its job.

 

Compile-Time Power Without Runtime Leakage

One of the strongest examples in the talk was deriving a Monoid instance for a Person case class.

The generated code:

  • cached type class instances in lazy vals
  • avoided asInstanceOf
  • was simple and direct
  • looked like hand-written Scala

This was the contrast Kalin wanted the audience to see:

  • mirrors often generate code that still drags derivation logic into runtime
  • macros generate only the specialized code you actually want

He argued that this makes macros a much better fit for performance-sensitive and correctness-focused systems.

 

The “Quotes Reflect” Problem

Of course, Kalin did not pretend Scala 3 macros are easy out of the box.

A major pain point is the low-level reflection API built around:

quotes.reflect.*

 

This API is extremely powerful, but also deeply awkward to work with. It involves dependent types, reflection internals, and a style of programming that quickly becomes hard to structure across files and helper layers.

His point was not that Scala macros are bad — quite the opposite.

The point was that the core macro system is amazing, but the standard library ergonomics are still rough.

 

Oxygen Meta: Building Better Macro Tools

To deal with those rough edges, Kalin introduced his own helper libraries:

  • oxygen-quoted
  • oxygen-meta

These libraries sit on top of Scala’s raw macro APIs and provide higher-level abstractions for common metaprogramming patterns, especially around:

  • product types
  • sum types
  • field access
  • deriving instances
  • turning values into expressions and expressions back into values

The goal is to make advanced macro programming feel more like normal Scala code, instead of forcing developers to write everything directly against low-level compiler structures.

This is important because Scala 3’s macro ecosystem is still evolving. The compiler team has signaled that higher-level abstractions may eventually be informed by what works in third-party libraries — so experiments like these matter.

 

Beyond Type Classes: Deriving APIs from Traits

The talk then expanded beyond classic type class derivation.

Kalin showed how the same macro techniques can be applied to service traits. For example, if you define an API as a Scala trait with method signatures and annotations, macros can derive:

  • HTTP endpoints
  • schema information
  • clients

That means you can define a service interface once and generate:

  • a server-side representation
  • a client-side representation
  • associated schemas and codecs

without duplicating the same knowledge across several layers of the stack.

This is one of the most interesting implications of the talk: metaprogramming is not only about generic derivation of type classes like Show or Monoid — it can also be about eliminating boilerplate in application architecture.

 

Why This Matters for Functional Programming

Although the talk was very Scala-specific, it also had a clear FP angle.

Kalin pointed out that a lot of metaprogramming in the wild is surprisingly anti-functional:

  • mutable internals
  • runtime reflection
  • global dependency injection patterns
  • Java-style bean assumptions

By contrast, Scala macros let you keep metaprogramming within a more functional model:

  • explicit transformations
  • type-safe derivation
  • pure compile-time generation
  • fewer runtime surprises

In that sense, macros fit functional programming much better than many traditional reflection-heavy systems.

 

The Core Takeaways

The talk’s main lessons were:

  1. Mirrors are not enough
    They are convenient at first glance, but they often hide complexity and generate poor runtime code.
  1. Macros give full control
    If you care about the structure and quality of your generated code, macros are the better tool.
  1. Scala 3 macros are powerful — but the ecosystem is still growing
    The raw APIs are strong, but helper libraries are still needed to make advanced use cases comfortable.
  1. Metaprogramming should be judged by the code it generates
    Not just by how clever the metaprogramming code looks, but by whether the resulting runtime code is clean, efficient, and safe.

 

Final Impression

“Reflection beyond mirrors” was ultimately a call to take Scala metaprogramming more seriously.

Kalin’s message was not that mirrors are imperfect. It was that relying on them too heavily keeps Scala developers from using one of the language’s most impressive capabilities: precise, type-safe, compile-time code generation with macros.

For Scala developers working with derivation, codecs, APIs, schemas, or boilerplate-heavy abstractions, the takeaway was clear: If you want high-quality generated code, don’t stop at mirrors.

 

Additional Resources

Check out more from the MeetUp Func Prog Sweden. Func Prog Sweden is the community for anyone interested in functional programming. At the MeetUps the community explore different functional languages like Erlang, Elixir, Haskell, Scala, Clojure, OCaml, F# and more.