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:ところでこれどう読むの?キルケー?カーク?サーク?