Skip to content

Instantly share code, notes, and snippets.

@ryoppy
Last active March 27, 2016 02:24
Show Gist options
  • Save ryoppy/15e9dc7d72fd6058edf1 to your computer and use it in GitHub Desktop.
Save ryoppy/15e9dc7d72fd6058edf1 to your computer and use it in GitHub Desktop.

Genericを使った型クラスインスタンスの導出

@ryoppy516


アジェンダ

  1. HListについて
  2. Genericについて
  3. JsonEncoderを書いてみる
  4. circeの紹介
  5. Natについて

1. HListについて

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を使ったループというか再帰が頻繁に出てきます。


3. Genericについて

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じゃなくても使えるみたいです。


3. JsonEncoderを書いてみる

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も取得できます。


おさらい。

  1. case classをGenericでHListに変換。
  2. HListをループ。
  3. ループ中に型クラスインスタンスを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を少し紹介してみます。


4. 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


5. Natについて

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)

便利ですね..?


以上です。ご清聴ありがとうございました。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment