Scalaで型クラスをメタプログラミングできるライブラリMagnoliaを使ってみる

MagnoliaはScala界隈ではおなじみのJon Prettyさん作のライブラリで、簡単に言うとケースクラスに対する型クラスのインスタンスを生成するためのマクロを簡単に実装できるライブラリです。Play-JSONReadsWritesFormatなどのマクロなどを簡単に作れるものというとわかりやすいでしょうか。Scala eXchange 2017のとあるセッションでちょっとだけ紹介されていて面白そうだったので軽く触ってみました。

github.com

わかりやすい例としてケースクラスの内容を文字列表現に変換するShowという型クラスを作ってみます(ScalaのケースクラスはtoStringするだけで十分わかりやすい出力を得ることができるので敢えて作る必要はないのですがMagnoliaのドキュメントにも書かれているので練習ということで…)。

まずは以下のトレイトを定義します。

trait Show[T]{
  def show(value: T): String
}

Derivationというオブジェクトを以下のような感じで実装します。

import language.experimental.macros, magnolia._

object ShowDerivation {
  type Typeclass[T] = Show[T]

  def combine[T](ctx: CaseClass[Show, T]): Show[T] = new Show[T] {
    def show(value: T): String = {
      ctx.parameters.map { p =>
        s"${p.label}=${p.typeclass.show(p.dereference(value))}"
      }.mkString("{", ",", "}")
    }
  }

  def dispatch[T](ctx: SealedTrait[Show, T]): Show[T] = ???
  
  implicit def gen[T]: Show[T] = macro Magnolia.gen[T]
}

見た目でなんとなくわかると思いますが、combineメソッドにケースクラスに対するshowメソッドの実装が記述されています。decodeメソッドはsealedトレイトを扱わない場合は実装しなくても大丈夫です。今回はMagnoliaでどんなことができるかを見るだけなので???にしておきます。

定義したShowDerivationオブジェクトを実際に使ってみましょう。以下のようなケースクラスと関数を用意しておきます。

// ケースクラス
case class Address(zipCode: String, country: String, prefecture: String)
case class Person(name: String, address: Address)

// Showを使ってケースクラスの内容を出力する関数
def printCaseClass[T](value: T)(implicit s: Show[T]): Unit = {
  println(s.show(value))
}

この関数を呼び出してみます。

// 基本的な型に対するインスタンスは自前で実装しておく
implicit val showString = new Show[String]{
  override def show(value: String) = value
}

// ケースクラスに対するインスタンスはgenマクロで生成可能
implicit val showAddress = ShowDerivation.gen[Address]
implicit val showPerson = ShowDerivation.gen[Person]

// ケースクラスのインスタンスを生成してメソッド呼び出し
val person = Person("takezoe", Address("150-0013", "Japan", "Tokyo"))
printCaseClass(person)

出力は以下のようになります。

{name=takezoe,address={zipCode=150-0013,country=Japan,prefecture=Tokyo}}

このようにMagnoliaを使うと自前でマクロを書くのに比べると簡単に型クラスのインスタンスを生成するマクロを定義することができます。GitHubリポジトリにはサンプルもいくつか含まれているので参考になると思います。

性質的にアプリケーションコードではなくライブラリやフレームワーク側で使うものだと思いますので利用頻度はさほど高くないかもしれませんが目の付け所が面白いライブラリですね。