Scala
JSON
cats
circe

Internal circe-core


Circeとは

Scalaでよく使われるJSONライブラリの一つです。

ScalaにはScalazCatsという関数型プログラミング2大派閥があるのですが、Cats側にいるのがCirceです。

これに則っていることもあって、Circeも外向きのI/Fは関数型を意識した実装になっています。

この記事では、circeがどのようにcatsを利用しているのかに注目して、ライブラリがどういう設計になっているのかを簡単に解説しようと思います。

なお、ここでは書いた時点で最新のリリースであるv0.9.3を対象としています。


想定する読者

関数型の基礎だけなんとなく理解しているぐらいのレベルを想定しています。

circeの中身にまで興味がないという方は下の部分だけ読んでいただければいいと思います。


Circeの基本概念

circe.png

よく見かける図ですが、ScalaのJSONライブラリはString(もしくはArray[Byte])とJson、そして任意の型 Aの間の相互変換を行うものとして設計されていることが多いです。Circeもこれに即して設計されています。主な役割は次の通りです。



  • Parser: StringからJsonに変換するクラスで、構文を解析してASTを構築する役割を担う

  • Printer: JsonからStringに変換する役割を担うクラス。spaceなしの表現にするか、prettyプリントするかといった表記に関する実装を持つ



  • Codec: Encoder/Decoderのこと、これらがcirceにおける抽象化の核となる型クラス



    • Encoder: A => Jsonの変換を行う型クラス


    • Decoder:
      Json => Aの変換を行う型クラス



これらとJson ASTを操作するCursorがcirceの主要機能を構成する6大コンポーネントだと思います。

この記事ではParser, DecoderとCursor周辺を取り上げて内部実装を解説し、最後にcirce-coreを補助するモジュール群の提供する機能を簡単に紹介します。


Parser

それでは早速コードをみていきましょう! まずはParserから。

コード量はかなり少ないですが、ParserとDecoderはcatsや型クラスを使った実装が多く見られるコンポーネントなのではないかと思います。

以降ではコードコメントで添えてある番号に沿って説明します。


Parser.scala

import cats.data.{ NonEmptyList, Validated, ValidatedNel }

import java.io.Serializable

trait Parser extends Serializable {

// (1)
def parse(input: String): Either[ParsingFailure, Json]

// (2)
protected[this] final def finishDecode[A](input: Either[ParsingFailure, Json])(implicit
decoder: Decoder[A]
): Either[Error, A] = input match {
case Right(json) => decoder.decodeJson(json)
case l @ Left(_) => l.asInstanceOf[Either[Error, A]]
}

protected[this] final def finishDecodeAccumulating[A](input: Either[ParsingFailure, Json])(implicit
decoder: Decoder[A]
): ValidatedNel[Error, A] = input match {
case Right(json) => decoder.accumulating(json.hcursor).leftMap { // ここでAccumulatingDecoderに変換した上で処理を移譲している
case NonEmptyList(h, t) => NonEmptyList(h, t)
}
case Left(error) => Validated.invalidNel(error)
}

// (3)
final def decode[A: Decoder](input: String): Either[Error, A] = finishDecode(parse(input))

final def decodeAccumulating[A: Decoder](input: String): ValidatedNel[Error, A] = finishDecodeAccumulating(parse(input))
}



(1) parseメソッド

JSON文字列が構文に沿っていない場合、パースは失敗することになります。

そのため、エラーかJsonどちらかを返す型として Either[ParsingFailure, Json] で表現されています。

ここでは抽象メソッドですが、parseの具象実装はcirce-parserなどの別モジュールにあります。

選択肢はいくつかありますが、基本的にはJawnが使われます。


(2) finishDecodeメソッド、finishDecodeAccumulatingメソッド

関数型の基本である、型クラスを使ったad-hoc polymorphismの使用例になっています。

個人的にはオブジェクト指向のデザインパターンでいうとStrategyパターンに似ていると思います。

Json => Aの変換処理は、Aの型によって異なるので、ここをDecoderという型クラスとして切り出してあるということです。

こういった実装ではScalaでは、型クラスimplicitな引数とするシグネチャがよく使われます。


(3) decodeメソッド、 decodeAccumulatingメソッド

ここがCirceの良いところの1つで、parse時に1つエラーが見つかった時点で処理をやめてエラーを返すいわゆるfail-fastな実装と、処理を続けて起きたエラーをまとめて返すaccumulatingな実装の両方が用意されています。

decodeの方は返り値の型に Either[E,A] が、decodeAccumulatingの方は ValidatedNel[E, A] = Validated[NonEmptyList[E], A] が使われているところに注目してください。

EitherはMonadでもあり、Applicativeでもあるため、整合性をとるためにApplicativeな結合を行う際にも、最初のエラーしか拾えない実装になっています。

一方、ValidatedNel[E, A]の方はApplicativeな結合を行うと、NonEmptyListをMonoid結合(=NonEmptyListをconcat)してエラーを蓄積できるように実装されています。

この詳細についてはこちらを参照されると良いと思います。

実はこの部分は、作者も設計に悩んだところのようで、自分もかなりこれらの使い方でハマりました。

decodeAccumulatingメソッドは、一見無条件でエラーを蓄積してくれるように見えるのですが、実際はDecoderをどう実装するかに依存します(Decoderに処理を委譲しているので当然といえば当然)。

どのようにDecoderをかけばエラーが蓄積されるかは、Decoderの項でコードを参照しながら説明します。

一旦ここでは、fail-fastな返り値を持つ実装、accumulatingな返り値を持つ実装両方があるというところだけ覚えておいてください。


Decoder

続いてDecoderの解説をします。

Encoder/Decoderはいわゆるオブジェクトマッパー的な存在で、Circeを利用するに当たってユーザが主に実装しないといけないのがこれらのインスタンスです(ただし、これを自動で導出する補助モジュールがあるので、実際に自分で書かないといけない場面は割と少ないです)。

Parserの項で触れたように、エラーを蓄積するには、自分でDecoderをそうなるように実装してやらないといけません。Decoderの内部実装を見る前に、どのように書けばエラーが蓄積されるのかを示したいと思います。


fail-fastなDecoder、エラーを蓄積するDecoder

例として次のコードを見てください。このコードではFooというcase classに対して2通りの方法でDecoderを定義しています。

import cats.syntax.apply._

import io.circe._
import io.circe.parser._

case class Foo(name: String, age: Int)

object FooDecoder {

// (1)
implicit val decoder: Decoder[Foo] = Decoder.instance {
cursor => for {
name <- cursor.get[String]("name")
age <- cursor.get[Int]("age")
} yield Foo(name, age)
}

// (2)
implicit val accDecoder: Decoder[Foo] = (
Decoder.instance(_.get[String]("name")),
Decoder.instance(_.get[Int]("age"))
).mapN(Foo.apply)

}


(1) decoder

for内包表記を使ってDecoderを実装しています。

これは実質flatMapとmapなので、エラーにもMonadicな結合が適用されます。


(2) accDecoder

こちらは、2つのDecoderインスタンスを生成し、mapNというメソッドで一つにまとめています。

mapNはcatsのメソッドで、Applicativeな結合をします。(厳密にはApplicativeのスーパークラスの型クラスであるSemigroupalという型クラスに属するメソッドです)

これらを使って先ほどのParserの挙動を見てみます。


object Main extends App {
// (1)
println(decode("{}")(FooDecoder.decoder))
// (2)
println(decode("{}")(FooDecoder.accDecoder))
// (3)
println(decodeAccumulating("{}")(FooDecoder.decoder))
// (4)
println(decodeAccumulating("{}")(FooDecoder.accDecoder))
}

結果は次のようになります。

Left(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(name))))

Left(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(name))))
Invalid(NonEmptyList(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(name)))))
Invalid(NonEmptyList(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(name))), DecodingFailure(Attempt to decode value on failed cursor, List(DownField(age)))))

(1)(2)ではdecodeメソッドを使っているので、当然エラーは蓄積されません。最初のnameに関するエラーのみが返されます。

特に注目して欲しいのが(3)と(4)の違いです。

for内包表記で実装したDecoderでは、返り値はValidatedではあるものの、エラーは蓄積されていません。

一方(4)では、nameとageに関するエラー両方が返されます。

つまり、エラーを蓄積しつつデコードを行いたい場合は、


  1. Applicativeな結合で複数のDecoderを結合したDecoderを作ること

  2. decodeAccumulatingを使うこと

この両方が必要になります。


Decoderの実装

それでは踏まえてエラー蓄積の仕組みを中心にDecoderの実装を追って見ましょう!

なおここでもコードを記載してありますが、説明に必要ないメソッドは省略しています。


Decoder.scala

final object Decoder extends TupleDecoders with ProductDecoders with LowPriorityDecoders {

// (1)
final def instance[A](f: HCursor => Result[A]): Decoder[A] = new Decoder[A] {
final def apply(c: HCursor): Result[A] = f(c)
}

implicit final val decoderInstances: SemigroupK[Decoder] with MonadError[Decoder, DecodingFailure] =
// (2)
new SemigroupK[Decoder] with MonadError[Decoder, DecodingFailure] {

override final def product[A, B](fa: Decoder[A], fb: Decoder[B]): Decoder[(A, B)] = fa.product(fb)

// ... その他の型クラスのメソッドの具象実装
}
}

trait Decoder[A] extends Serializable { self =>
// (1)
def apply(c: HCursor): Decoder.Result[A]

// (3)
final def product[B](fb: Decoder[B]): Decoder[(A, B)] = new Decoder[(A, B)] {
final def apply(c: HCursor): Decoder.Result[(A, B)] = Decoder.resultInstance.product(self(c), fb(c))
override def decodeAccumulating(c: HCursor): AccumulatingDecoder.Result[(A, B)] =
AccumulatingDecoder.resultInstance.product(self.decodeAccumulating(c), fb.decodeAccumulating(c))
}

// (3)
private[circe] def decodeAccumulating(c: HCursor): AccumulatingDecoder.Result[A] = apply(c) match {
case Right(a) => Validated.valid(a)
case Left(e) => Validated.invalidNel(e)
}
}



(1) applyメソッド、instancesメソッド

DecoderのメインのメソッドであるapplyはHCursorをとってEither[DecodingFailure, A]を返すメソッドです。

instancesメソッドはこのファクトリメソッドという立ち位置で、自前でDecoderを実装する際は、このメソッドを利用する場合がほとんどです。


(2) decoderInstances

これらがDecoderに対する各種型クラスの実装です。

先程のmapNの呼び出しでDecoderをApplicativeに結合する際の具象実装がこのproductです。


(3) productメソッド, decodeAccumulatingメソッド

(2)のproductが呼び出しているのが、こちらのDecoder#productです。

ポイントは返しているDecoderインスタンスでdecodeAccumulatingに特殊な実装を入れている点です。

通常のDecoderであれば、次のdecodeAccumulatingのようにEitherをValidatedに変換するだけの処理なのですが、ここではさらにそれをoverrideしてAccumulatingDecoderのresultInstanceに属するproductを呼び出しています。


AccumulatingDecoderの実装

Decoderで使われていたresultInstanceの定義を中心にAccumulatingDecoderのコードを見てみます。

(ここも説明に不要なコードは削っています)


AccumulatingDecoder.scala

package io.circe

import cats.{ ApplicativeError, Semigroup, SemigroupK }
import cats.data.{ NonEmptyList, Validated, ValidatedNel }
import java.io.Serializable

sealed trait AccumulatingDecoder[A] extends Serializable { self =>

// (1)
def apply(c: HCursor): ValidatedNel[DecodingFailure, A]

// ... その他のメソッド
}

final object AccumulatingDecoder {

// (2)
final val failureNelInstance: Semigroup[NonEmptyList[DecodingFailure]] =
NonEmptyList.catsDataSemigroupForNonEmptyList[DecodingFailure]

final val resultInstance: ApplicativeError[Result, NonEmptyList[DecodingFailure]] =
Validated.catsDataApplicativeErrorForValidated[NonEmptyList[DecodingFailure]](failureNelInstance)

// .. その他のメソッド
}



(1) applyメソッド

AccumulatingDecoderのapplyはHCursorをとってValidatedNelを返すメソッドになっています。


(2) resultInstance, failureNelInstance

これらがDecoder側でoverrideされていたdecodeAccumulatingの本体です。

catsでValidatedに対して組み込みで用意されているApplicativeの実装を持ってきています。

ここでは内部までは紹介しませんが、E側の値がMonoid結合される実装になっています。

つまり、DecoderをApplicativeに結合すると、decodeAccumulatingがエラーを蓄積する実装にoverrideされたDecoderインスタンスが作られるということです。

以上がcirceのエラー蓄積の仕組みでした。

この辺りは特にcatsを活用した実装になっているので、参考になりますね!


Cursorの実装

CursorはJSON ASTを辿るための仕組みです。

CirceのCursorは今JSON AST中のどこを指しているかと、探索した履歴を保持しています。

主要機能だけ抜き出すと、次のような実装になっています。


ACursor.scala


// (1)
abstract class ACursor(private val lastCursor: HCursor, private val lastOp: CursorOp) extends Serializable {

// (2)
def focus: Option[Json]

// (1)
final def history: List[CursorOp] = {
var next = this
val builder = List.newBuilder[CursorOp]

while (next.ne(null)) {
if (next.lastOp.ne(null)) {
builder += next.lastOp
}
next = next.lastCursor
}

builder.result()
}

// (3)
final def as[A](implicit d: Decoder[A]): Decoder.Result[A] = d.tryDecode(this)

// (3)
final def get[A](k: String)(implicit d: Decoder[A]): Decoder.Result[A] = downField(k).as[A]

}



(1) コンストラクタ, historyメソッド

Cursorでは一つ前のCursorの状態と、現在のCursorに移動するのに行われたオペレーションが格納されています。ちょっと標準ライブラリの単方向Listの実装に似ていますね!

historyメソッドではこれを辿ってCursorOpのListにします。


(2) focusメソッド

文字通り、Cursorが現在指しているJson ASTの部分木を返すメソッドです。

具象実装は今回は省略しますが、HCursorの方にあります。


(3) getメソッド、asメソッド

getメソッドは引数で指定したフィールドにフォーカスを移し、その結果得られるJson ASTの部分木を型Aに変換して取得するメソッドです。

ポイントとしては、getやasが引数にAについてのDecoderをとるようになっていることです。

Decoderのapply、instanceメソッドは、HCursorを引数とするメソッドでした。

つまり、ここにDecoder[A]を引数として取れるようにしてあることによって、JSON ASTの部分木に対するDecoderを使いまわして、より大きなASTに対するDecoderを実装することができるようになります!

次のような例を見てください。

Barというcase classの中にFooというcase classが入れ子になっています。

特に注目してもらいたいポイントは、barDecoderを実装する際のfooの取得にfooDecoderの実装が使いまわせていることです。

import io.circe._

import io.circe.parser._

case class Foo(name: String, age: Int)
case class Bar(address: String, foo: Foo)

object Instances {

implicit val fooDecoder: Decoder[Foo] = Decoder.instance {
cursor => for {
name <- cursor.get[String]("name")
age <- cursor.get[Int]("age")
} yield Foo(name, age)
}

implicit val barDecoder: Decoder[Bar] = Decoder.instance {
cursor => for {
address <- cursor.get[String]("address")
foo <- cursor.get[Foo]("foo") // ここでfooDecoderが使いまわせる => name, ageを別個に取得する必要がない!
} yield Bar(address, foo)
}
}

このように、小さい部品に対する実装を大きな部品の実装に使いまわせるところが、型クラスで実装していく醍醐味だと思います。


circe-coreを補助するモジュールたち

最後にCirceの補助モジュールの機能を簡単に紹介しようと思います。

これらで全てではありませんが、Circeには、ここまでで紹介したcoreの機能を便利に使えるようにするためのモジュールがいくつか用意されています。

その中でも特にgenericopticsを紹介します。


generic

これがCirceの目玉機能と言っても過言ではないでしょう!

ここまでの説明ではDecoderを明示的に自分で実装していましたが、case classであれば、genericモジュールを使ってこれを自動的に導出させることができます。


GenericAuto.scala

case class Foo(name: String, age: Int)

import io.circe.generic.auto._
println(decode[Foo]("{}"))
println(decodeAccumulating[Foo]("{}"))


import io.circe.generic.auto._を行うことにより、case class FooのDecoderが自動的に導出されます。

因みに、これを実行すると次のようになり、自動導出されたDecoderでdecodeAccumulatingを呼び出した場合にもエラーが蓄積されていることがわかります。

Left(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(name))))

Invalid(NonEmptyList(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(name))), DecodingFailure(Attempt to decode value on failed cursor, List(DownField(age)))))

つまり、circe-genericを使うとio.circe.generic.auto._をimportするだけで、簡単にfail-fastなDecoderも、エラーを蓄積するDecoderも使うことができるということです!


optics

opticsはJSON ASTに対しておなじみのドットアクセスを提供するモジュールです。

公式ドキュメントからそのまま引っ張ってきたものですが、次のようにネストの深いJSONがあったときに、coreモジュールだけだと、次のようにcursorを移動しながらアクセスするコードになります。

val json: Json = parse("""

{
"order": {
"customer": {
"name": "Custy McCustomer",
"contactDetails": {
"address": "1 Fake Street, London, England",
"phone": "0123-456-789"
}
},
"items": [{
"id": 123,
"description": "banana",
"quantity": 1
}, {
"id": 456,
"description": "apple",
"quantity": 2
}],
"total": 123.45
}
}
"""
).getOrElse(Json.Null)


CursorAccess.scala

val phoneNum: Option[String] = json.hcursor.

downField("order").
downField("customer").
downField("contactDetails").
get[String]("phone").
toOption

opticsを使うとこれを次のように書くことができるようになり、スッキリかけるようになります!


OpticsAccess.scala

import io.circe.optics.JsonPath._

// import io.circe.optics.JsonPath._

val _phoneNum = root.order.customer.contactDetails.phone.string
// _phoneNum: monocle.Optional[io.circe.Json,String] = monocle.POptional$$anon$2@303363b8

val phoneNum: Option[String] = _phoneNum.getOption(json)


終わりに

本記事では、circe-coreの内部実装の説明と、それを補助するモジュールの簡単な機能紹介をしました。

便利なライブラリなのでcats派の方はぜひ使って見てください!