Case Classを変換したいことがある。具体的には今作ってる年表作成ツールで,ドメインモデルであるdomain.Timeline
をテンプレート用のDTOであるapplication.dto.Timeline
に変換したいのだが,domain.timeline
はそのフィールドにSeq[domain.Event]
を保持していて,これもSeq[application.dto.Event]
に変換したい,というもの。
最も素朴にやるならば,普通に変換するメソッドを書くのだろうが,こういうボイラープレートがたくさんできてしまう。
def toDTO( timeline: Timeline ): windymelt.timeline.application.dto.Timeline = { DTOTimeline( timeline.id, timeline.title, toDTO(timeline.editor), timeline.events.map(toDTO) ) } def toDTO(event: Event): windymelt.timeline.application.dto.Event = { DTOEvent( event.id, event.name, event.description.getOrElse(""), event.occurredAt ) } def toDTO(user: User): windymelt.timeline.application.dto.User = { DTOUser(user.id, user.name) }
もう完全に型を合わせるためのボイラープレートだ。型を合わせる…………?型変換…………?一般的なケースクラスを別のケースクラスに変換するような,汎用的なメソッドが書けたら…………!?
JSONライブラリ
そういえばJSONライブラリもcase classのserialization時に似たようなことをしている。Case classを受け取り,JSONのASTを生成し,最終的に文字列に変換したりする。この時,具体的なケースクラスの型はなんでもよく,case classでさえあれば大抵の場合にうまく動作する。
これを支えているのはScalaの強力な型機能なのだが,実際はどうなっているのか知らないので見ておく。
手頃なJSONライブラリとしてcirceというのがあるのでひとまずこれの実装を見てみる。*1
circeでは,case classのserializeはio.circe.generic.auto
が担っているそうなので確認する。
io.circe.generic.auto
はAutoDerivation
を継承している。
コードを読むとそこには………!
package io.circe.generic import io.circe.{ Decoder, Encoder } import io.circe.export.Exported import io.circe.generic.decoding.DerivedDecoder import io.circe.generic.encoding.DerivedAsObjectEncoder import io.circe.generic.util.macros.ExportMacros import scala.language.experimental.macros /** * Fully automatic codec derivation. * * Extending this trait provides [[io.circe.Decoder]] and [[io.circe.Encoder]] * instances for case classes (if all members have instances), "incomplete" case classes, sealed * trait hierarchies, etc. */ trait AutoDerivation { implicit def exportDecoder[A]: Exported[Decoder[A]] = macro ExportMacros.exportDecoder[DerivedDecoder, A] implicit def exportEncoder[A]: Exported[Encoder.AsObject[A]] = macro ExportMacros.exportEncoder[DerivedAsObjectEncoder, A] }
exportEncoder
に注目。すべてのA
についてExported[Endocer.AsObject[A]]
を提供するという型変換的なことをやっていますね。
なにやらExportMacros
ってやつを使ってますが,マクロを読んだ(理解したとは言ってない)最終的にnew _root_.io.circe.export.Exported($t: _root_.io.circe.Encoder.AsObject[$A])
というのを生成するようです。
Exported
ってなんやねんという感じですが実装はcase class Exported[+T](instance: T) extends AnyVal
だったのでただのコンテナっぽい。おそらくマクロ中でいい感じのJSONオブジェクトに変換するする君を探してそれを継承させる,といったことをやっているのだと思います。
具体的処理
ところで具体的な処理はio.circe.Encoder
にズラリと書かれています。
具体的には以下のようなもの。
implicit final val encodeByte: Encoder[Byte] = new Encoder[Byte] { final def apply(a: Byte): Json = Json.fromInt(a.toInt) }
Encoder
がJSONオブジェクトへの変換をやるわけですが,具体的処理であるapply
はインスタンス化するときに実装してもらうようになっています。implicit val
なので自動変換されて便利ですね。
ところでProductはどうなってんの?と思うわけですが,それはProductEncoders
に実装されています。といっても具体的に実装されているのではなく,自動生成されていそうです。
Productは最大22-arityまで作成できるので,人間にはメンテ不可能というのが事情のようです。
自分でも実装してみようと思ったけどパワーが足りなかった。
まとめ
ライブラリは型パワーとマクロでJSONのエンコードをやっている。serializeしなくていいなら型パワーだけで型変換とかできそう。ライブラリはshapelessとかcatsを使っている。
*1:ところでこれどう読むの?キルケー?カーク?サーク?