アジェンダ
- HListについて
- Genericについて
- JsonEncoderを書いてみる
- circeの紹介
- Natについて
IntとStringを持つ等、n個の型を持ったリストが定義できます。
val xs: Int :: String :: HNil = 1 :: "a" :: HNil
scalaのListと同じようにHeadとTailを持った構造で、Listにあるようなメソッドは大体持っています。
map/filter/flatMap/length/etc...
HListをループしてみます。
HListはn個の型を持つので普通にforで書くと型を維持できません。
なので、HListをループする簡単な例として、リストから最後の値を取得するLastを書いてみます。
trait Last[L <: HList] {
type Out
def apply(l: L): Out
}
object Last {
// どちらも同じLastのインスタンスを取得するimplicit defです。
// HListをHeadとTailで分けて、Tailでさらに自身のimplicitを取得してループしています。
implicit def hlistLast[H, T <: HList](implicit lt : Last[T]): Last[H :: T] =
new Last[H :: T] {
type Out = lt.Out
def apply(l : H :: T): Out = lt(l.tail)
}
// TailがHNilになったらHeadの要素を返してループがとまります。
implicit def hnilLast[H]: Last[H :: HNil] =
new Last[H :: HNil] {
type Out = H
def apply(l : H :: HNil): Out = l.head
}
}
implicitly[Last[Int :: String :: HNil]].apply(1 :: "a" :: HNil) // "a"
shapelessでは、このようなimplicitを使ったループというか再帰が頻繁に出てきます。
case classをHListに相互変換できるものです。(sealed traitならCoproductですが今回は一切触れてません)
scala> case class Foo(a: Int, b: String)
defined class Foo
scala> Generic[Foo].to(Foo(1, "a"))
res11: shapeless.::[Int,shapeless.::[String,shapeless.HNil]] = 1 :: a :: HNil
scala> Generic[Foo].from(1 :: "a" :: HNil)
res12: Foo = Foo(1,a)
お察しのとおり?macroが使われています。コンストラクタの型をとってきてパターンマッチを作ってるようです。
new Generic[Foo] {
def to(foo: Foo) = foo match {
case Foo(a, b) => a :: b :: HNil
}
普通のclassでもコンストラクタが全部publicならGeneric使えたり、条件が揃えばcase classじゃなくても使えるみたいです。
trait JsonEncoder[A] {
def encode(a: A): JValue // json4sのJValue
}
object JsonEncoder {
implicit def jInt = new JsonEncoder[Int] {
def encode(a: Int): JValue = JInt(a)
}
implicit def jString = new JsonEncoder[String] {
def encode(a: String): JValue = JString(a)
}
}
処理の流れ
- Genericでcase classをHListにし、
- ループして型クラスのインスタンス取得して、
- encodeメソッド呼んでJValueにしています。
例
- case class Foo(a: Int, String)
- → Int :: String :: HNil
- → JsonEncoder[Int], JsonEncode[String]
- → JArray(JInt, JString)
implicitを3つ定義します。
まず1つめは、Genericでcase classをHListにするimplicitです。
object JsonEncoder {
...
// GenericでA(case class)をHListに変換したものをLで受け取ります。そのLでさらにJsonEncoderを取得しています。
implicit def forCaseClass[A, L <: HList]
(implicit gen: Generic.Aux[A, L], encoder: JsonEncoder[L]): JsonEncoder[A] = {
new JsonEncoder[A] {
def encode(a: A): JValue = encoder.encode(gen.to(a))
}
}
2つに、ループして各要素の型にマッチするJsonEncoderを取得します。(IntかStringしか定義してないのでどちらか)
// さっきのLastと同じようにループします。
// HListの先頭HでJsonEncoderを取得し、残りのTでもJsonEncoderを所得してループしています。
// encodeメソッドでは、取得したインスタンスのencodeを読んで、++で繋いで一つのJValueにしています。
implicit def hlistEncoder[H, T <: HList]
(implicit encoderHead: JsonEncoder[H], encoderTail: JsonEncoder[T]): JsonEncoder[H :: T] = {
new JsonEncoder[H :: T] {
def encode(a: H :: T): JValue = encoderHead.encode(a.head) ++ encoderTail.encode(a.tail)
}
}
3つめ。JsonEncoder[HNil]のimplicit defです。
// HNilになったら再帰が止まります。
implicit def hnilEncoder: JsonEncoder[HNil] = {
new JsonEncoder[HNil] {
def encode(a: HNil): JValue = JNothing
}
}
使ってみます。
scala> implicitly[JsonEncoder[Foo]].encode(Foo(1, "a"))
res18: org.json4s.JsonAST.JValue = JArray(List(JInt(1), JString(a)))
case classがネストしてるとimplicitが相互に参照してエラーになるためLazyを付ける必要があります。
implicit def hlistEncoder[H, T <: HList]
(implicit encoderHead: Lazy[JsonEncoder[H]], encoderTail: JsonEncoder[T]): JsonEncoder[H :: T] = {
keyを取得してないのでjsonとして成り立ってませんが、ちゃんと書くときはLabelledGenericを使うとkeyも取得できます。
おさらい。
- case classをGenericでHListに変換。
- HListをループ。
- ループ中に型クラスインスタンスをimplicitで取得。
というのが、型クラスインスタンス導出の流れでした。 Genericを使えばcase classを丸ごと何か処理できるので便利ですね!
途中に出てきたGeneric.Auxに触れてなかったので説明しておきます。
Auxパターン
implicitパラメータで取得したインスタンスの持ってる型から、implicitパラメータを取得するときに使います。
trait Foo[In] {
type Out
}
object Foo {
type Aux[In, Out0] = Foo[In] { type Out = Out0 }
implicit def foo1 = new Foo[Int] {
type Out = String
}
// Foo.AuxはFooのaliasなので、Foo[A]のインスタンスを取得できます。
// Foo[Int]の場合OutはStringなのでBが自動的にStringに決まります。
def apply[A, B](a: A)(implicit f1: Foo.Aux[A, B], m: Monoid[B]) = m
}
Foo(1).zero // ""
shapelessではAuxの定義が頻繁に出てくるので覚えておくといいと思います。
実際にshapelessを使ったjsonライブラリはあるのでそれを見るとより理解が深まるかと思います。
- spray-json-shapeless
- circe (サーシィ)
自動導出に関しては、どちらもやってることは説明した例と同じです。
circeを少し紹介してみます。
circeはJsonライブラリ。Argonautをforkしたもので、CatsやShapelessを使っている。Jawnという速いJsonパーサを使っているとのこと。
ちょっと使ってみます。
Encode
import io.circe._
import io.circe.syntax._
import io.circe.generic.auto._
scala> case class User(id: Int, name: String)
//defined class User
scala> User(1, "taro").asJson
//res79: io.circe.Json =
//{
// "id" : 1,
// "name" : "taro"
//}
denocde
scala> parser.decode[User]("""{"id":1, "name":"taro"}""")
//res95: cats.data.Xor[io.circe.Error,User] = Right(User(1,taro))
失敗すると
scala> parser.decode[User]("""{"id":1}""")
//res96: cats.data.Xor[io.circe.Error,User] = Left(io.circe.DecodingFailure: Attempt to decode value on failed cursor: El(DownField(name),false))
cursor
jsonを色々たどれたり、set/deleteしたりできるもの。
scala> val userJson: Json = User(1, "taro").asJson
scala> userJson.hcursor.get[Int]("id")
//res21: io.circe.Decoder.Result[Int] = Right(1)
Incomplete
case classとjsonがマッチしてなくても、後から一部渡せるというもの。
// まずは普通にデコード
scala> parser.decode[User]("""{"id":1,"name":"taro"}""")
res85: cats.data.Xor[io.circe.Error,User] = Right(User(1,taro))
// idは後から渡すように
scala> parser.decode[Int => User]("""{"name":"taro"}""")
res86: cats.data.Xor[io.circe.Error,Int => User] = Right(<function1>)
scala> .map(_(1))
res87: cats.data.Xor[io.circe.Error,User] = Right(User(1,taro))
関数の引数をHListにしたりとか、コード読むと面白いので興味あれば読んでみるといいと思います。
IncompleteDerivedDecoders.scala
Natは、型レベルの自然数を表すものです。
0は、_0 という特殊な型があります。
1は、Succ[_0] という型で表します。
2は、Succ[Succ[_0]] という型で表します。
3は、Succ[Succ[Succ[_0]]] という感じでSuccのネストで数を表します。
Natの演算
足し算(Sum)の例です。
import ops.nat._
scala> val sum = Sum[Succ[Succ[_0]], Succ[_0]] // 2 + 1
sum: shapeless.ops.nat.Sum[shapeless.Succ[shapeless.Succ[shapeless._0]],shapeless.Succ[shapeless._0]]{type Out = shapeless.Succ[shapeless.Succ[shapeless.Succ[shapeless._0]]]} = shapeless.ops.nat$Sum$$anon$5@15f6b98e
scala> Nat.toInt[sum.Out]
res21: Int = 3
他にも引き算(Diff)や、かけ算(Prod)、割り算(Div)、MinやMax、比較のLT/GTなどあります。
何が悲しくてこんなの使うの...?
Natの演算は、コンパイル時の演算です!
なのでNatを使えばフィボナッチ数の計算やクイックソート等がコンパイル時にできます!()
数値を渡すところは大体?Natが使われていて、HListのapplyでも使われてます。HListの長さ以上の数を渡すとコンパイルエラーになります。
scala> val xs = 1 :: "a" :: HNil
xs: shapeless.::[Int,shapeless.::[String,shapeless.HNil]] = 1 :: a :: HNil
scala> xs(0)
res122: Int = 1
scala> xs(1)
res123: String = a
scala> xs(2)
<console>:54: error: Implicit not found: shapeless.Ops.At[shapeless.::[Int,shapeless.::[String,shapeless.HNil]], nat_$macro$68.N]. You requested to access an element at the position nat_$macro$68.N, but the HList shapeless.::[Int,shapeless.::[String,shapeless.HNil]] is too short.
xs(2)
便利ですね..?
以上です。ご清聴ありがとうございました。