LoginSignup
4
2

More than 3 years have passed since last update.

circeのgeneric encoderはどこからくるの?

Posted at

とある人のとあるツイートが目に入り、twitterのリプライでは語りきれないと思ったのでQittaで説明記事を書いてみる
https://twitter.com/hakukotsu/status/1171469303247335429

Circe

https://circe.github.io/circe/
scalaのJSONライブラリのひとつです
関数型のエッセンスを取り入れていたり、後述するジェネリックプログラミング(とマクロ)を利用したEncoder/Decoderの自動導出が特徴的です

ジェネリックプログラミング

(僕自身この言葉についてあまり詳しくないので正しい説明はではないかもしれません、指摘頂ければ修正します)
プログラミングにおいて"ジェネリック"というとJavaやTypescriptにある型の抽象化機能が思い浮かびますよね、Scalaでは型クラスと呼ばれていますね
今回の文脈であげているジェネリックプログラミングも型を抽象化して扱う、という意味では同じです
ジェネリックスと大きく違うのは、ジェネリックスの場合はサブタイピングによって抽象型を表現します( T <: SuperClass のような)が
ジェネリックプログラミングでは型の特徴を元に抽象化します
例えば、以下の二つはFieldの型と定義順という特徴では同一と見なせます

case class Human(name: String, age: Int)
case class Apartment(address: String, age: Int)

// キー名やclass名は違うがどちらも [String, Int]の順で定義されている

Shapeless

scalaでジェネリックプログラミングを行うためのライブラリです
詳しい扱い方については以下の記事が参考になりました
https://qiita.com/ryoppy/items/476467a4b709f011fb8f

この項目では説明に必要な部分のみ説明させていただきます

HList

型付きの値の列を表した型です
イメージとしては結合可能なTupleといった感じです
終端値として HNil が存在しており、HNil 自身も HList 型です
Generic を使うことで任意の case classHList に変換できます

import shapeless._

Generic[Human].to(Human("moemoe", 12))
// => res1: String :: Int :: shapeless.HNil = moemoe :: 12 :: HNil

Generic[Apartment].to(Apartment("hdmr", 20))
// => res2: String :: Int :: shapeless.HNil = hdmr :: 20 :: HNil

同じ型になりましたね、これで二つの値はひとつの関数で扱うことができます

LabelledGeneric

また LabelledGeneric を使うことで型情報にキーを含んだHListに変換することができます
これもCirceのコード内で使用されています

scala> LabelledGeneric[Human].to(Human("moemoe", 12))
res1: String with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("name")],String] :: Int with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("age")],Int] :: shapeless.HNil = moemoe :: 12 :: HNil

ジェネリックプログラミングについての補足

https://ja.wikipedia.org/wiki/%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AA%E3%83%83%E3%82%AF%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0
wiki調べですがジェネリックスを使った言語機能もジェネリックプログラミングと呼んでも良さそうな感じがします、ただし、今回の文脈でジェネリックプログラミングと称したときはジェネリックスのことは含まない、とさせていただきます

Auto Derivation

https://circe.github.io/circe/codecs/auto-derivation.html
circeは implicit class, implicit parameter, マクロ, ジェネリックプログラミングを使って未定義case classに対してJSON Encoder/Decoderを自動導出してくれます
具体的なフローは

(1). case class 定義
(2). shaplessのジェネリック型(HList)
(3). case classのFieldの値ごとのEncoder取得
(4). 取得したエンコーダとHListに含まれるField名の情報から case class のエンコーダを作成

といったような感じです、さぁ実際にコード追いながら見てみましょう

コードリーディング

EncoderOps

このコードリーディングの出発点です
ある任意の方 A をWrapし、JsonObject型へ変換する asJsonObject 型を持ちます
制限のない型 A に対する implicit class なので全ての型に対して適用することができます
asJsonObjectimplicit parameter Encoder.AsObject[A] を受け取ります

Encoder.AsObject[A]

関数 encodeObject(a: A): JsonObject のみを未定義とした trait です
各型 AJsonObject 型に変換するための責務を持ちます
最終的には定義した case class X に対する Encoder.AsObject[X] の具象型見つけるのが主目的となります(主題の generic encoder とはまさにこいつのことです)
最終的には Encoder.AsObject[X] の具象型はマクロによって生成されます
そのマクロを見つけるために implicit を追いましょう

Encoder.AsObject *companion

例のコードのimportでは Encoder.AsObject[A] のimplicit値は見つかりません
implicitの探査は対象 implicit parameter 型のコンパニオンオブジェンクトに及び、そのスーパークラスにてやっと見つかります
importedAsObjectEncoder[A]Exported[AsObject[A]] を受け取り AsObject[A] を返す中継者として存在しておりimplicitの探査は Exported[AsObject[A]] へとバトンタッチされます
Exported[AsObject[A]] はimportされている io.circe.generic.auto._ より AutoDerivation classにたどり着き、exportEncoder[A] 関数により Exported[Encoder.AsObject[A]] を得ることができます
exportEncoder[A] の実装はマクロになります
このとき、ExportMacros.exportEncoder には型引数 DerivedAsObjectEncoder を渡していることを記憶しておいてください、実はこの DerivedAsObjectEncoder の匿名サブクラスが Encoder.AsObject[A] の具象型となります

ExportMacros.exportEncoder[DerivedAsObjectEncoder, A]

マクロ実装です、E[_] A ふたつの型情報を受け取り、E[A] 型のimplicitパラメータを引っ張ってくる実装です、対象のimplicitパラメータが存在していない場合は Context#typecheck 関数にてEmptyTreeが帰り、エラーになります(詳細にいうと、型 A に対するジェネリック型 R が見つからない場合にエラーになります)

shapeless.lazilyについてはimplicitをうまく解決してくれるためのWrapperのようなもの、だと認識していただければ十分です(僕も詳しくは理解してないので説明できない、というのが本音ですが)

実際に取ってくる E[A] 型について今回のコードでは型引数 E には DerivedAsObjectEncoder を渡しているので DerivedAsObjectEncoder[A] 型になります
DerivedAsObjectEncoder[A] のimplicit値はコンパニオンオブジェクト DerivedAsObjectEncoder にて定義されています

DerivedAsObjectEncoder *companion

implicit def deriveEncoder では2つのimplicitパラメータを受け取ります
LabelledGeneric.Aux[A, R] は対象の型 A にたいするジェネリック型(HList) R が存在する場合に取得できる型です、つまり AR を紐付けます
AR の関係性の例は

// A =
case class Human(name: String, age: Int)
//の場合
// R =
String with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("name")],String] :: Int with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("age")],Int] :: shapeless.HNil
// となります

LabelledGeneric[A] のサモナーで取ってきたインスタンスと同じ型ですね

Lazy[ReprAsObjectEncoder[R]]
Lazy は implicit解決のためのもの、今回のコードをコンパイラを意識しなければ ReprAsObjectEncoder[R] だと思って頂いて大丈夫だと思います
ReprAsObjectEncoder[R] コンパニオンオブジェクト ReprAsObjectEncoder より取得されます

関数bodyでは DerivedAsObjectEncoder の匿名サブクラスを作成しています
DerivedAsObjectEncoder#encodeObject の実装では2つのimplicitパラメータからJsonObjectを作成しています、具体的にいうとジェネリック型 R へ変換後、それのEncoderを介してJsonObjectを生成しています

ReprAsObjectEncoder * companion

deriveReprAsObjectEncoder はジェネリック型 R のEncoderを取得するマクロ関数です

Deriver.{ deriveEncoder[R] | constructEncoder[R]}

マクロ実装
RHList もしくは CoproductType である必要があります、CoproductType について説明はしてませんが HList と同じくShapelessの型で直和を表します、 sealed trait のジェネリック型となります
今回は型 RHList であるという前提で進めます
$instanceTypeRE[R] を表す構文木を生成しており、 RE 型は継承時に型引数としている ReprAsObjectEncoder 型です
$encodeMethodName継承先で定義しておりencodeObject を表す構文木となります(これは ReprAsObjectEncoder のインスタンスを作るときのメソッド名として扱われます)
$fullEncodeMethodArgsa: R という構文木と継承時にoverrideされていれば追加のメソッド引数の構文木を返します、今回はoverrideされていないので encodeMethodArgs はNil、つまりこの関数で返される構文木は a: R のみとなります
$instanceDefs $instanceImpl については hlistEncoderParts メソッドで作成するため、後述とさせて頂きます

最終的に関数内では

// Rは任意のジェネリック型(HList)
new ReprAsObjectEncoder[R] {
  ..$instanceDefs
  final def encodeObject(a: R): _root_.io.circe.JsonObject = $instanceImpl
}: ReprAsObjectEncoder[R]

となります
( _root_ は絶対パス指定なだけで、仮にルートpackageで定義するのであれば _root_.io.circe.JsonObject io.circe.JsonObject は同一です)

Deriver.hlistEncoderParts

encodeObject の関数bodyと ReprAsObjectEncoder に定義する instanceDefs の構文木を返す関数です
引数 members は型 R を元に作られた独自型 Members で大本となった case classA のFieldの一覧を表す型です

Members#fold

変数名とかコードの流れからList#foldだと勘違いしそうですが独自定義関数です
各Fieldに対するEncoderインスタンスを取得し、それらを instanceDefs という構文木のリストとして返すことと HList 分解用のパタンマッチ構文、そして JsonObject.fromIterable にわたすためのfieldに対応したエンコード適用Fieldの構文木を返します

引数について
namePrefix はEncoderインスタンスを変数に束縛する際のヘンス名です(理解する上で重要なものではないので無視しても構いません)
resolver はある型 T からエンコーダ Encoder[T] を導出するための無名関数です、詳しくは説明しないですが、implicitで導出しています(今更ですがプリミティブ型、及び基本的な型 Option, Either, List, etc... についてはライブラリ側でEncoderが定義されています)
init f はゼロ値と畳込み関数です、役割としては通常の List#fold と同じイメージで大丈夫です
戻り値は通常の List#fold と違い、畳み込み結果と一緒に instanceDefs が帰ります

hlistEncoderParts に帰ってくる値としては
instanceDefs は実際には以下のような構文になります

// case class Humanの場合
private[this] val circeGenericEncoderForName = io.circe.Encoder.encodeString
private[this] val circeGenericEncoderForAge = io.circe.Encoder.encodeInt

pattern については

// case class Humanの場合
_root_.shapeless.::(circeGenericHListBindingForName, _root_.shapeless.::(circeGenericHListBindingForAge, _root_.shapeless.HNil))

fields については

("name", circeGenericEncoderForName(circeGenericHListBindingForName)), ("age", circeGenericEncoderForAge(circeGenericHListBindingForAge))

となります

以上の構文木を組み合わせ hlistEncoderParts の戻り値としては

a match {
  case _root_.shapeless.::(circeGenericHListBindingForName, _root_.shapeless.::(circeGenericHListBindingForAge, _root_.shapeless.HNil)) =>
     _root_.io.circe.JsonObject.fromIterable(_root_.scala.collection.immutable.Vector(("name", circeGenericEncoderForName(circeGenericHListBindingForName)), ("age", circeGenericEncoderForAge(circeGenericHListBindingForAge))))
}

読みやすくすると

a match {
  case circeGenericHListBindingForName :: circeGenericHListBindingForAge :: HNil =>
     JsonObject.fromIterable(Vector(("name", circeGenericEncoderForName(circeGenericHListBindingForName)), ("age", circeGenericEncoderForAge(circeGenericHListBindingForAge))))
}

となります
(instanceDefs についてはそのまま返してます)

これを constructEncoder の構文木に組み込むと

```scala
// Rは任意のジェネリック型(HList)
new ReprAsObjectEncoder[R] {
  private[this] val circeGenericEncoderForName = io.circe.Encoder.encodeString
  private[this] val circeGenericEncoderForAge = io.circe.Encoder.encodeInt

  final def encodeObject(a: R): _root_.io.circe.JsonObject = a match {
    case _root_.shapeless.::(circeGenericHListBindingForName, _root_.shapeless.::(circeGenericHListBindingForAge, _root_.shapeless.HNil)) =>
       _root_.io.circe.JsonObject.fromIterable(_root_.scala.collection.immutable.Vector(("name", circeGenericEncoderForName(circeGenericHListBindingForName)), ("age", circeGenericEncoderForAge(circeGenericHListBindingForAge))))
  }
}: ReprAsObjectEncoder[R]

コンパイルの通る形になりました
これで任意のジェネリック型 R に対する ReprAsObjectEncoder[R] ができたので
R に対する具象型 ADerivedAsObjectEncoder[A]、すなわちスーパークラスである Encoder.AsObject[A] が得られるということになります


今回はEncoderについて書きましたがDecoderについても同様にコードを追っていけば理解できると思います、scalaに置いて難しめの機能/ライブラリを使っていますが、その分コードを読むことでscalaの世界への理解が広まる良いコードでした
今回はShapelessの話ではないので省きましたが Generics.Aux[A, R] がなぜ A に対するGenerics型(HList) R を導出できるかなども調べれば面白いかもしれませんね

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2