authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Alain-Michel Chomnoue Nghemning
Verified Expert in Engineering
12 Years of Experience

作为远程办公公司的首席架构师和Java专家, Alain has led software development teams to impact thousands of users’ work.

Expertise

Share

The Scala language offers developers the opportunity to write object-oriented and functional code in a clean and concise syntax (as compared to Java, for example). Case classes, higher-order functions, and type inference are some of the features that Scala developers can leverage to write code that’s easier to maintain and less error-prone.

Unfortunately, Scala代码也不能幸免于样板文件, and developers may struggle to find a way to refactor and reuse such code. For example, some libraries force developers to repeat themselves by calling an API for each subclass of a sealed class.

But that’s only true until developers learn how to leverage macros and quasiquotes to generate the repeated code at compile time.

Use Case: Registering the Same Handler for All Subtypes of a Parent Class

在微服务系统的开发过程中, I wanted to register a single handler for all events derived from a certain class. 为了避免被我使用的框架的细节分散我们的注意力, here’s a simplified definition of its API for registering event handlers:

trait EventProcessor[Event] {
  def addHandler[E <: Event: ClassTag](
      handler: E => Unit
  ): EventProcessor[Event]

  def process(event: Event)
}

拥有任何事件处理器 Event 类型的子类,我们可以注册处理程序 Event with the addHandler method.

Looking at the above signature, a developer might expect a handler registered for a given type to be invoked for events of its subtypes. For example, let’s consider the following class hierarchy of events involved in the User entity lifecycle:

从UserEvent降序的Scala事件层次结构. 有三个直接的后代:UserCreated(有名字和电子邮件), which are both Strings), UserChanged, and UserDeleted. Furthermore, UserChanged有两个自己的后代:NameChanged(有一个名称, 这是一个字符串)和EmailChanged(有电子邮件, which is a string).
A Scala event class hierarchy.

相应的Scala声明如下所示:

sealed trait UserEvent
final case class UserCreated(name: String, email: String) extends UserEvent
sealed trait UserChanged                                  extends UserEvent
final case class NameChanged(name: String)                extends UserChanged
final case class EmailChanged(email: String)              extends UserChanged
case object UserDeleted                                   extends UserEvent

我们可以为每个特定的事件类注册一个处理程序. 但是如果我们想为 all the event classes? 的处理程序 UserEvent class. 我期望对所有事件都调用它.

val handler = new EventHandlerImpl[UserEvent]
val processor = EventProcessor[UserEvent].addHandler[UserEvent](handler)

我注意到在测试期间从未调用处理程序. I dug into the code of Lagom, the framework I was using.

I found that the event processor implementation stored the handlers in a map with the registered class as the key. When an event is emitted, it looks for its class in that map to get the handler to call. 事件处理器是这样实现的:

type Handler[Event] = (_ <: Event) => Unit

private case类EventProcessorImpl[Event]
    handlers: Map[Class[_ <: Event], List[Handler[Event]]] =
      Map[Class[_ <: Event], List[Handler[Event]]]()
)扩展EventProcessor[Event] {

  override def addHandler[E <: Event: ClassTag](
      handler: E => Unit
  ): EventProcessor[Event] = {
    val eventClass =
      implicitly[ClassTag[E]].runtimeClass.asInstanceOf[Class[_ <: Event]]
    val eventHandlers = handler
      .asInstanceOf[Handler[Event]]:: handlers.getOrElse(eventClass, List())
    copy(handlers + (eventClass -> eventHandlers))
  }

  override def process(event: event): Unit = {
    handlers
      .get(event.getClass)
      .foreach(_.foreach(_.asInstanceOf[Event => Unit].apply(event)))
  }
}

的处理程序 UserEvent 类,但每当派生事件(如 UserCreated ,处理器在注册表中找不到它的类.

Thus Begins the Boilerplate Code

The solution is to register the same handler for each concrete event class. We can do it like this:

val handler = new EventHandlerImpl[UserEvent]  
val processor = EventProcessor[UserEvent]  
  .addHandler[UserCreated](handler)  
  .addHandler[NameChanged](handler)  
  .addHandler [EmailChanged](处理器)  
  .addHandler[UserDeleted.type](handler)

Now the code works! But it’s repetitive.

It’s also difficult to maintain, 因为我们需要在每次引入新事件类型时修改它. We might also have other places in our codebase where we are forced to list all the concrete types. 我们还需要确保修改这些地方.

This is disappointing, as UserEvent is a sealed class, meaning that all its direct subclasses are known at compile time. 如果我们可以利用这些信息来避免样板文件会怎么样?

Macros to the Rescue

Normally, Scala functions return a value based on the parameters we pass to them at run time. You can think of Scala macros 作为生成一些代码的特殊函数 compile 是时候将它们的调用替换为.

While the macro interface might seem to take values as parameters, its implementation will actually capture the abstract syntax tree (AST)—the internal representation of source code structure that the compiler uses—of those parameters. 然后使用AST生成一个新的AST. 最后,新的AST在编译时替换宏调用.

Let’s look at a macro declaration that will generate event handler registration for all the known subclasses of a given class:

def addHandlers[Event](
      处理器:EventProcessor(事件),
      handler: Event => Unit
  ): EventProcessor[Event] =宏setEventHandlers_impl[Event]  
  
  
defseteventhandlers_impl[事件:c.WeakTypeTag](c: Context)(
      processor: c.Expr[EventProcessor[Event]],
      handler: c.Expr[Event => Unit]
  ): c.Expr[EventProcessor[Event]] = {

  // implementation here
}

Notice that for each parameter (including type parameter and return type), the implementation method has a corresponding AST expression as a parameter. For example, c.Expr[EventProcessor[Event]] matches EventProcessor[Event]. The parameter c: Context wraps the compilation context. 我们可以使用它来获取编译时可用的所有信息.

在本例中,我们希望检索密封类的子类:

import c.universe._  
  
val symbol = weakTypeOf[Event].typeSymbol

def subclasses(symbol: symbol): List[symbol] = {  
  val children = symbol.asClass.knownDirectSubclasses.toList  
  symbol :: children.flatMap(subclasses(_))  
}  
  
Val children =子类(symbol)

Note the recursive call to the subclasses 方法以确保也处理间接子类.

现在我们有了要注册的事件类列表, 我们可以为Scala宏将要生成的代码构建AST.

生成Scala代码:ast或准引号?

要构建AST,既可以操作AST类,也可以使用Scala quasiquotes. Using AST classes can produce code that is difficult to read and maintain. In contrast, quasiquotes dramatically reduce the complexity of the code by allowing us to use a syntax that is very similar to the generated code.

为了说明简单性的好处,让我们以简单表达式为例 a + 2. 用AST类生成它是这样的:

val exp = Apply(Select(Ident(TermName("a")), TermName("$plus")), List(Literal(Constant(2))))

We can achieve the same with quasiquotes with a more concise and readable syntax:

val exp = q"a + 2"

为了保持宏的直接性,我们将使用准引号.

让我们创建AST并将其作为宏函数的结果返回:

val calls = children.foldLeft(q"$processor")((current, ref) =>
  q"$current.addHandler[$ref]($handler)"
)
c.Expr [EventProcessor[活动]](调用)

The code above starts with the processor expression received as a parameter, and for each Event 类,它生成对 addHandler 方法,并将子类和处理程序函数作为参数.

现在我们可以调用宏了 UserEvent class and it will generate the code to register the handler for all the subclasses:

val handler = new EventHandlerImpl[UserEvent]  
val processor = EventProcessorMacro.addhandler (EventProcessor UserEvent,处理程序)

That will generate this code:

com.example.event.processor.EventProcessor
.apply[com.example.event.handler.UserEvent]()
.addHandler[UserEvent](handler)
.addHandler[UserCreated](handler)
.addHandler[UserChanged](handler)
.addHandler[NameChanged](handler)
.addHandler [EmailChanged](处理器)
.addHandler[UserDeleted](handler)

The code of the complete project compiles correctly and the test cases demonstrate that the handler is indeed registered for each subclass of UserEvent. Now we can be more confident in the capacity of our code to handle new event types.

Repetitive Code? Get Scala Macros to Write It

Even though Scala 有一个简洁的语法,通常有助于避免样板文件, developers can still find situations where code becomes repetitive and cannot be easily refactored for reuse. Scala宏可以与准引号一起使用来克服这类问题, 保持Scala代码的整洁和可维护.

还有一些很受欢迎的图书馆,比如 Macwire,它利用Scala宏帮助开发人员生成代码. I strongly encourage every Scala developer to learn more about this language feature, as it can be a valuable asset in your tool set.

Understanding the basics

  • What is Scala used for?

    Scala lets programmers write object-oriented and functional code in a clean and concise syntax. Its output can be executed either in a Java virtual machine (JVM) or in a JavaScript runtime.

  • What are Scala macros?

    Scala macros are special functions that replace their own invocations with source code that they generate at compile time.

  • What are syntax trees?

    An abstract syntax tree (or AST) is the representation of source code structure that a compiler uses internally.

  • What are sealed classes?

    密封类是其子类在编译时已知的类. In Scala, they cannot be extended by classes outside of the source file in which they are defined.

  • Scala中的类和对象是什么?

    In Scala, classes define common structure and behavior; objects are instances of a class. Scala also uses the object keyword to define singletons (classes with a single instance).

聘请Toptal这方面的专家.
Hire Now
Alain-Michel Chomnoue Nghemning

Alain-Michel Chomnoue Nghemning

Verified Expert in Engineering
12 Years of Experience

阿比让,拉古内斯大区,Côte科特迪瓦

Member since November 11, 2020

About the author

作为远程办公公司的首席架构师和Java专家, Alain has led software development teams to impact thousands of users’ work.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 privacy policy.

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 privacy policy.

Toptal Developers

Join the Toptal® community.