Zenn
Open139

Guide to Shapless を読む

RikiyaOtaRikiyaOta

1,2章はサラッと読んだ。

2章では、Product, Coproduct の構造だけを抽出して HList, Coproduct という型への双方向の変換ができることを学んだ。

『構造だけ抜き出す』というのが垣間見えた気がする。

RikiyaOtaRikiyaOta

3 Automatically deriving type class instances を読んでいく。

Product の CsvEncoder を型クラスとして定義し、そのインスタンスの導出を shapeless でできるっぽい?

型クラスは便利である程度抽象化されているけど、結局手で instance を定義しないといけないよね、そこの分離ができないよね、って書いてある。

RikiyaOtaRikiyaOta

3.2 Deriving instances for products を読んでいく。

直感的に以下2点が考えられる:

  1. HList の head, tail それぞれの型クラスのインスタンスがあれば、HList 全体のインスタンスを導出できそう。
  2. case class A があれば Generic[A]が得られる。Generic[A]Repr に構造を保持している。これらを組み合わせて、A のインスタンスを作れる。

1点目はまあそうかなぁ。
CsvEncoder のインスタンスとして、要素それぞれのインスタンスがあれば、出力(List)を連結するだけ。

2点目も、割とそのまんま?
Generic[A] があれば AHList に変換する方法も知っていて、その構造は Repr が表現している。その HList をエンコードする方法がわかっていれば Aのインスタンスたり得る。

そんな感じかな?読み進める。

RikiyaOtaRikiyaOta

String, Int, Boolean という要素ごとの Encoder を実装。これは書かないと仕方ないのか?

def createEncoder[A](func: A => List[String]): CsvEncoder[A] =
  new CsvEncoder[A] {
    def encode(value: A): List[String] = func(value)
  }

implicit val stringEncoder: CsvEncoder[String] =
  createEncoder(str => List(str))

implicit val intEncoder: CsvEncoder[Int] =
  createEncoder(num => List(num.toString))

implicit val booleanEncoder: CsvEncoder[Boolean] =
  createEncoder(bool => List(if(bool) "yes" else "no"))
RikiyaOtaRikiyaOta

HNil の Encoder。末尾に HNil があるならこれは必要か。

import shapeless.{HList, ::, HNil}

implicit val hnilEncoder: CsvEncoder[HNil] =
  createEncoder(hnil => Nil)
RikiyaOtaRikiyaOta

ようやく。最初に言っていたことの1点目に対応する。自然な実装。

implicit def hlistEncoder[H, T <: HList](
  implicit
  hEncoder: CsvEncoder[H],
  tEncoder: CsvEncoder[T]
): CsvEncoder[H :: T] =
  createEncoder {
    case h :: t =>
      hEncoder.encode(h) ++ tEncoder.encode(t)
  }
RikiyaOtaRikiyaOta

個別の要素の encoder は知っていて、implicit def hListNecoder があるので、以下の形で String :: Int :: Boolean :: HNil という HList の Encoder が自動導出できる。

val reprEncoder: CsvEncoder[String :: Int :: Boolean :: HNil] =
  implicitly

reprEncoder.encode("abc" :: 123 :: true :: HNil)
// res9: List[String] = List(abc, 123, yes)
RikiyaOtaRikiyaOta

具体的な case class , ここでは IceCream の CsvEncoder を Generic を使って導出している。

IceCream --> HListgen.to でしている。HList --> List[String]enc.encode でしている。組み合わせている点がポイント。

implicit val iceCreamEncoder: CsvEncoder[IceCream] = {
  val gen = Generic[IceCream]
  val enc = CsvEncoder[gen.Repr]
  createEncoder(iceCream => enc.encode(gen.to(iceCream)))
}

最初に言っていたことの2点目。
先ほど作った CsvEncoder[HList] の導出の implicit def も使っている。

なお、多分先に 3.1.2 で出てきていた companion object を作っておく必要がある。apply が必要。ここの apply は単に summon してるだけ。

object CsvEncoder {
  // "Summoner" method
  def apply[A](implicit enc: CsvEncoder[A]): CsvEncoder[A] =
    enc

  // "Constructor" method
  def instance[A](func: A => List[String]): CsvEncoder[A] =
    new CsvEncoder[A] {
      def encode(value: A): List[String] =
        func(value)
    }

  // Globally visible type class instances
}
RikiyaOtaRikiyaOta

確かに csv encode できる。

List(
  IceCream(name = "Sundae", numCherries = 1, inCone = false),
  IceCream(name = "Cornetto", numCherries = 0, inCone = true),
  IceCream(name = "Banana Split", numCherries = 0, inCone = false)
)


```scala
writeCsv(iceCreams)
// res11: String =
// Sundae,1,no
// Cornetto,0,yes
// Banana Split,0,no
RikiyaOtaRikiyaOta

ここでは IceCream のインスタンスを結局手で書いているのであまり嬉しくないはず。
もっと広い対象の case class の CsvEncoder を自動導出できるようにしたい。

RikiyaOtaRikiyaOta

以下のようなコードを考えてみる。

implicit def genericEncoder[A](
  implicit
  gen: Generic[A],
  enc: CsvEncoder[???]
): CsvEncoder[A] = createEncoder(a => enc.encode(gen.to(a)))

enc: CsvEncoder[???] が何を知っていればいいか?
A に対応する HList を csv encode する方法を知っていればいいはず。

RikiyaOtaRikiyaOta

直接 CsvEncoder[gen.Repr] と書くことはできないらしい。なので、新しく型パラメータ R を定義し、type Repr を指すようにしている。

implicit def genericEncoder[A, R](
  implicit
  gen: Generic[A] { type Repr = R },
  enc: CsvEncoder[R]
): CsvEncoder[A] =
  createEncoder(a => enc.encode(gen.to(a)))

gen: Generic[A] { type Repr = R } という書き方は僕は見慣れなかったが、Type refinement というものらしい。型クラスに制約を追加するようなものみたい。

参考:https://medium.com/@markgrechanik/type-refinements-in-scala-making-your-types-more-sophisticated-than-your-friends-23600b2305d2

ここでいうと、genericEncoder[A,R] が受け取る R に一致する type Repr を持つような Generic[A] を受け取るという制約になるのかな。Generic の型インスタンスが Reprを持つことは知っているので、うまくはまるのか。

ここで大事なのは、何か case class 的なもの(A)とその構造 (R, HList)を指定して CsvEncoder が自動導出されるようになったということ。

RikiyaOtaRikiyaOta

先ほどの type refinement は冗長なので、Generic.Aux なるものが定義されている。

package shapeless

object Generic {
  type Aux[A, R] = Generic[A] { type Repr = R }
}

書き直すと以下、

implicit def genericEncoder[A, R](
  implicit
  gen: Generic.Aux[A, R],
  env: CsvEncoder[R]
): CsvEncoder[A] =
  createEncoder(a => env.encode(gen.to(a)))
RikiyaOtaRikiyaOta

ここまでのコードがコンパイルエラーになった時の話。

1つのケースとして、case class でないクラス(i.e. ADT でない)を渡した場合。Generic[A] が導出できずにエラーになる。

class Foo(bar: String, baz: Int)

writeCsv(List(new Foo("abc", 123)))
// <console>:26: error: could not find implicit value for parameter encoder: CsvEncoder[Foo]
//        writeCsv(List(new Foo("abc", 123)))
//
RikiyaOtaRikiyaOta

もう1つ。HList の CsvEncoder が導出できずにエラー。

以下の例では、Date に対する csv encoder が定義されていないので、HList 全体の CsvEncoder が導出できずにエラーになる。

import java.util.Date

case class Booking(room: String, date: Date)

writeCsv(List(Booking("Lecture hall", new Date())))
// <console>:28: error: could not find implicit value for parameter encoder: CsvEncoder[Booking]
//        writeCsv(List(Booking("Lecture hall", new Date())))
//                ^

ただし、メッセージがわかりにくい。Section 3.5 で debug のテクニックが紹介されるらしいけど、基本的には地道にエラーの原因を探るしかないのか?

RikiyaOtaRikiyaOta

Coproduct のエンコーダーを自動導出する。

sealed trait Shape
final case class Rectangle(width: Double, height: Double) extends Shape
final case class Circle(radius: Double) extends Shape

余談:↑これを Annmonite で実行するとエラーになってしまう。sealed trait の継承ができないっぽい。

@ final case class Rectangle(width: Double, height: Double) extends Shape
cmd32.sc:1: illegal inheritance from sealed trait Shape
final case class Rectangle(width: Double, height: Double) extends Shape
                                                                  ^
Compilation Failed
RikiyaOtaRikiyaOta

ワンライナーで実行したらできた🎉

@ sealed trait Shape; final case class Rectangle(width: Double, height: Double) extends Shape; final case class Circle(radius: Double) extends Shape;
defined trait Shape
defined class Rectangle
defined class Circle
RikiyaOtaRikiyaOta

Shapeの generic な構造は Rectangle :+: Circle :+: CNil と表現されることは前に見た。

Generic[Shape]
res36: Generic[Shape]{type Repr = ammonite.$sess.cmd32.Circle :+: ammonite.$sess.cmd32.Rectangle :+: shapeless.CNil} = shapeless.Generic$$anon$1@76928be0

:+:CNil に対してのエンコーディングを実装する。

import shapeless.{Coproduct, :+:, CNil, Inl, Inr}

implicit val cnilEncoder: CsvEncoder[CNil] =
  createEncoder(cnil => throw new Exception("Inconceivable!"))

implicit def coproductEncoder[H, T <: Coproduct](
  implicit
  hEncoder: CsvEncoder[H],
  tEncoder: CsvEncoder[T]
): CsvEncoder[H :+: T] = createEncoder {
  case Inl(h) => hEncoder.encode(h)
  case Inr(t) => tEncoder.encode(t)
}
  • :+: の左あるいは右の場合のエンコーディングを実装する必要がある。
  • CNil の値は作れないので、単に例外を投げている。ここは dead code なので実際に問題は起きない。
RikiyaOtaRikiyaOta

今時点ではエラーが起きる。Double の CsvEncoder が定義されていないので、HList の CsvEncoder が導出できずにいる。やはりエラーメッセージからだけではわかりずらい。

val shapes: List[Shape] = List(
  Rectangle(3.0, 4.0),
  Circle(1.0)
)

writeCsv(shapes)
// <console>:26: error: could not find implicit value for parameter encoder: CsvEncoder[Shape]
//        writeCsv(shapes)
//                ^

CsvEncoder[Double] を実装すれば無事に Shape も csv encode できるようになる。

implicit val doubleEncoder: CsvEncoder[Double] =
  createEncoder(d => List(d.toString))

writeCsv(shapes)
// res7: String =
// 3.0,4.0
// 1.0
RikiyaOtaRikiyaOta

3.4 Deriving instances for recursive typesを読んでいく。

RikiyaOtaRikiyaOta

よく例として挙げられる、再帰的データ構造

sealed trait Tree[A]
case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]
case class Leaf[A](value: A) extends Tree[A]

これの CsvEncoder を作ろうとするとエラーになる。

コンパイラは implicit を解決するためのプロセスとして、"収束" するかどうかを決めていく。
1つの方法として、無限ループを回避する方法がある。もしコンパイラーが2回同じ型に遭遇したら、その分岐を諦めて次に移る。

CsvEncoder[Tree[Int]] を辿っていく様子。

CsvEncoder[Tree[Int]]                          // 1
CsvEncoder[Branch[Int] :+: Leaf[Int] :+: CNil] // 2
CsvEncoder[Branch[Int]]                        // 3
CsvEncoder[Tree[Int] :: Tree[Int] :: HNil]     // 4
CsvEncoder[Tree[Int]]                          // 5 uh oh
RikiyaOtaRikiyaOta

::[H,T] , :+:[H,T] だともう少し状況はよろしくない。

コンパイラが同じ type constructor を2回見つけて、かつその型パラメータがより複雑になっているなら、その分岐は"発散"しているとみなす。

shapeless では ::[H,T] , :+:[H,T] が繰り返し出てくるのでコンパイラがうまく implicit を解決してくれないっぽい。

RikiyaOtaRikiyaOta

3.4.2 Lazy を読んでいく。

上記の implicit の発散を防ぐために Shapeless は Lazy という型を提供している。
Lazyがすること:

  1. 前述の過剰に防御的な収束する発見方法を防ぎ、コンパイル時の impclit 発散を抑制する。
  2. implicit parameter の評価をランタイムまで遅らせ、自己参照する implicit を利用できるようにする。

経験的に、HList , Coproduct の先頭や、Repr パラメータを Lazy でラップしておくのが良いらしい。

implicit def hlistEncoder[H, T <: HList](
  implicit
  hEncoder: Lazy[CsvEncoder[H]], // wrap in Lazy
  tEncoder: CsvEncoder[T]
): CsvEncoder[H :: T] = createEncoder {
  case h :: t =>
    hEncoder.value.encode(h) ++ tEncoder.encode(t)
}

implicit def coproductEncoder[H, T <: Coproduct](
  implicit
  hEncoder: Lazy[CsvEncoder[H]], // wrap in Lazy
  tEncoder: CsvEncoder[T]
): CsvEncoder[H :+: T] = createEncoder {
  case Inl(h) => hEncoder.value.encode(h)
  case Inr(t) => tEncoder.encode(t)
}

implicit def genericEncoder[A, R](
  implicit
  gen: Generic.Aux[A, R],
  rEncoder: Lazy[CsvEncoder[R]] // wrap in Lazy
): CsvEncoder[A] = createEncoder { value =>
  rEncoder.value.encode(gen.to(value))
}

これで先ほどの CsvEncoder[Tree[Int]]も導出できる。

RikiyaOtaRikiyaOta

3.5 Debugging implicit resolutionは本当に単に debug 手法なのでサラッと読む。

scala.reflect のreify は知らないので少し調べる。
「scala expression を受け取り、ASTを返す。型注釈付きで。」と書いてあるな。だからこれも、implicit の解決の際にコンパイラがどのように推論をしているかを AST として観察することができるのか。

RikiyaOtaRikiyaOta

ここまで、product, coproduct の CsvEncoder が自動で導出される様子を学ぶことができた。

RikiyaOtaRikiyaOta

4.1 Dependent types を読んでいく。

dependent types という概念を学ぶっぽい。

trait Generic[A] {
  type Repr
  def to(value: A): Repr
  def from(value: Repr): A
}

単純化された Generic の定義。
Generic インスタンスは A , Repr という2つの型を参照している。

RikiyaOtaRikiyaOta

以下の関数はどんな型を返すだろうか?

import shapeless.Generic

def getRepr[A](value: A)(implicit gen: Generic[A]) =
  gen.to(value)

gen が受け取った Generic インスタンスに寄る。A の generic な構造に応じた HListCoproductを返すと思われる。

実際そうなる。

case class Vec(x: Int, y: Int)
case class Rect(origin: Vec, size: Vec)

getRepr(Vec(1, 2))
// res1: Int :: Int :: shapeless.HNil = 1 :: 2 :: HNil

getRepr(Rect(Vec(0, 0), Vec(5, 5)))
// res2: Vec :: Vec :: shapeless.HNil = Vec(0,0) :: Vec(5,5) :: HNil
RikiyaOtaRikiyaOta

ここで見ているのが dependent typing だそう。

つまり、getRepr が返す方は、型パラメータ A に依存して( Reprを経由して)決まっている。ある型が別の型によって決まっているという感じかな。

A が入力、Reprが出力と考えると良いらしい。

RikiyaOtaRikiyaOta

そのほかの Dependent type の例を見ていく。

shapeless には Last という型クラスがある。HList の最後の要素を返すものらしい。
単純化した実装は以下:

package shapeless.ops.hlist

trait Last[L <: HList] {
  type Out
  def apply(in: L): Out
}

ここでも L が入力、Out が出力になっている。

RikiyaOtaRikiyaOta

Last インスタンスを2つ定義。

val last1 = Last[String :: Int :: HNil]
// last1: shapeless.ops.hlist.Last[String :: Int :: shapeless.HNil]{type Out = Int} = shapeless.ops.hlist$Last$$anon$34@12389dd9

val last2 = Last[Int :: String :: HNil]
// last2: shapeless.ops.hlist.Last[Int :: String :: shapeless.HNil]{type Out = String} = shapeless.ops.hlist$Last$$anon$34@6cb2b0cb

以下のように振る舞う:

last1("foo" :: 123 :: HNil)
// res1: last1.Out = 123

last2(321 :: "bar" :: HNil)
// res2: last2.Out = bar
RikiyaOtaRikiyaOta

Last に渡される HList は最低1つの要素を持たなければエラーになる。implicit パラメータが見つからない。

Last[HNil]
// <console>:15: error: Implicit not found: shapeless.Ops.Last[shapeless.HNil]. shapeless.HNil is empty, so there is no last element.
//        Last[HNil]
//            ^
RikiyaOtaRikiyaOta

HListの型が合っているかもチェックする。(これは流石に自明な気がする. def apply(in: L): Out なので、L のところで型が合わないだけ)

last1(321 :: "bar" :: HNil)
// <console>:16: error: type mismatch;
//  found   : Int :: String :: shapeless.HNil
//  required: String :: Int :: shapeless.HNil
//        last1(321 :: "bar" :: HNil)
//
RikiyaOtaRikiyaOta

さらに例として、HList の2つ目の要素を返す Second を実装してみる。

trait Second[L <: HList] {
  type Out
  def apply(value: L): Out
}

object Second {
  type Aux[L <: HList, O] = Second[L] { type Out = O }

  def apply[L <: HList](implicit inst: Second[L]): Aux[L, inst.Out] =
    inst
}
RikiyaOtaRikiyaOta

Second.applyAux[L, inst.Out] を返している点に注意。

単に Second[L] を返してしまうと、コンパイラが type Out の情報を消してしまう。
Second trait 自体には、LOut に何か関係があるような定義はされていないので。

inst で解決された型の情報を保つために必要らしい。

shapeless が提供する the も使える。
この例のような、dependent type を扱う際には、独自の summoer を定義するか the を使うかなどする必要がある。

RikiyaOtaRikiyaOta

以下の implicit def で、少なくとも2つの要素を持った HListSecond インスタンスを自動で導出できる。

import Second._

implicit def hlistSecond[A, B, Rest <: HList]: Aux[A :: B :: Rest, B] =
  new Second[A :: B :: Rest] {
    type Out = B
    def apply(value: A :: B :: Rest): B =
      value.tail.head
  }

改めて見返すと、Second trait は2つ目の要素を返すといった具体的な処理は定義していないので、上のようなインスタンスが必要。

RikiyaOtaRikiyaOta

なので、要素が1つの HList では implicit parameter を解決できない。

Second[String :: HNil]
// <console>:26: error: could not find implicit value for parameter inst: Second[String :: shapeless.HNil]
//        Second[String :: HNil]
//

dependdently typed function の例として、Second を実装して振る舞いを確認してみた。

RikiyaOtaRikiyaOta

先ほどの Last, Second のような関数( apply があるという意味で)を chain させる例を見ていく。

以下は、型 AHList の最後の要素を返そうとしているような関数。だが、コンパイルエラーになる。

def lastField[A](input: A)(
  implicit
  gen: Generic[A],
  last: Last[gen.Repr]
): last.Out = last.apply(gen.to(input))
// <console>:28: error: illegal dependent method type: parameter may only be referenced in a subsequent parameter section
//          gen: Generic[A],
//  

同じパラメータリストの中で Last[gen.Repr] を定義しているのでそうなる。Aux pattern を使うところ。

RikiyaOtaRikiyaOta

Rect(Vec(1, 2), Vec(3, 4))HList として見た時の最後の要素を取得できていることがわかる。

def lastField[A, Repr <: HList](input: A)(
  implicit
  gen: Generic.Aux[A, Repr],
  last: Last[Repr]
): last.Out = last.apply(gen.to(input))

lastField(Rect(Vec(1, 2), Vec(3, 4)))
// res14: Vec = Vec(3,4)
RikiyaOtaRikiyaOta
By encoding all the free variables as type parameters, we enable the compiler to unify them with appropriate types.

最初のエラーになったlastField[A] では、gen.Repr は呼び出し時のAによって決まるという意味で『free variables』と言っているっぽい。

明示的に型パラメータ Repr を導入して gen: Generic.Aux[A, Repr] と定義することで、「 Generic[A] の表現は Repr である」という関係性をコンパイラが認識できるようになる。

このスタイルでコーディングしていくと言っている。
別の例を見てみる。

RikiyaOtaRikiyaOta

ただ1つのフィールドを持つ case class の Generic を呼び出すような以下の関数を考えてみる。
エラーなくコンパイルできる。

def getWrappedValue[A, H](input: A)(
  implicit
  gen: Generic.Aux[A, H :: HNil]
): H = gen.to(input).head

しかし、呼び出すとエラーになる。

case class Wrapper(value: Int)

getWrappedValue(Wrapper(42))
// <console>:30: error: could not find implicit value for parameter gen: shapeless.Generic.Aux[Wrapper,H :: shapeless.HNil]
//        getWrappedValue(Wrapper(42))
//                       ^

型パラメータ H がそのまま表示されていることがヒントっぽい。

RikiyaOtaRikiyaOta

gen が過剰に制約を課せられていることが問題のよう。
コンパイラは Repr の長さまでは確定できない模様。というか、同時に複数の制約を解決できず、ここでは、Repr を見つけつつ、かつその長さまでチェックできないっぽい?
Nothing が現れる時も注意が必要で、共変な型パラメータを統合しようとして失敗した際に出てくるらしい。

implicit 解決を2つのステップに分解:

  1. A に適した Repr を持つ Generic を見つける。
  2. 先頭の型が H である Repr を提供する。

=:= を使った例が以下。ev: (Head :: Tail) =:= Repr なので、Head :: TailRepr が型として一致するときだけ ev が解決されることになる。

だが、これもコンパイルエラーになる。

def getWrappedValue[A, Repr <: HList, Head, Tail <: HList](input: A)(
  implicit
  gen: Generic.Aux[A, Repr],
  ev: (Head :: Tail) =:= Repr
): Head = gen.to(input).head
// <console>:30: error: could not find implicit value for parameter c: shapeless.ops.hlist.IsHCons[gen.Repr]
//        ): Head = gen.to(input).head
//

.head メソッドが IsHCons という型の implicit parameter が必要とのこと。
なので、それを parameter に追加してあげればいい。

IsHCons は、HListHeadTailに分解する型クラスとのことです。

import shapeless.ops.hlist.IsHCons

def getWrappedValue[A, Repr <: HList, Head](in: A)(
  implicit
  gen: Generic.Aux[A, Repr],
  isHCons: IsHCons.Aux[Repr, Head, HNil]
): Head = gen.to(in).head

IsHCons.Aux[Repr, Head, HNil] によって、Repr は空でない HList であり、head の型は Head、 tail の型は HNil であるべしと要求している。つまり Repr は要素が1つの HList であり、その要素の型は Head である。

shapeless は IsHCons のような便利な型クラスを多く提供しているとのこと。

ここでは、Generic.Aux, IsHCons.Aux を chain させている様子を学ぶことができればOKかな。

RikiyaOtaRikiyaOta

5 Accessing names during implicit derivation を読んでいく。

RikiyaOtaRikiyaOta

LabelledGeneric という Genericの変種があり、フィールド名や型名などにアクセスできる?らしい。

これを理解するために少し理論的なこと学ぶ。

keyword: literal types, singleton types, phantom types, type tagging

RikiyaOtaRikiyaOta

"hello" は String, AnyRef, Any といった型を持つが、もう1つもつ。

singleton type と呼ばれるもので、ただ1つの値しか持たない型のことを言う。
singleton object が似たようなもので、object Foo と宣言した場合、Foo.type とは、Fooをただ1つの値としてもつ型となる。

literal value に適用されたsingleton type のことを literal types と呼ぶ。
scala に昔から存在するものだが、コンパイラーがデフォルトでは literal を non-singleton な型に拡大するので、普通はお目にかからないっぽい。

RikiyaOtaRikiyaOta

shalepess の narrow は、literal expression を singleton type で型付された式に変えてくれる:

import shapeless.syntax.singleton._

var x = 42.narrow
// x: Int(42) = 42

ここの Int(42) が、「42 だけを持つ Int の subtype」を表している。

なお、上のコードを Ammonite 上で実行すると謎のエラーが出た。val だと動いた。謎。
↓こちら、自分の REPL で動かした結果。

@ val x = 42.narrow
x: 42 = 42

@ var x = 42.narrow
java.lang.AssertionError: assertion failed:
  mkAttributedQualifier(Unit, <none>)
     while compiling: cmd4.sc
        during phase: fields
     library version: version 2.13.10
    compiler version: version 2.13.10
  reconstructed args: -nowarn -Xmaxwarns 0 -Yresolve-term-conflict:object

  last tree to typer: Function(value $anonfun)
       tree position: line 8 of cmd4.sc
            tree tpe: () => Int(42)
              symbol: value $anonfun
   symbol definition: val $anonfun: <notype> (a TermSymbol)
      symbol package: ammonite.$sess
       symbol owners: value $anonfun -> method $main -> object cmd4
           call site: constructor cmd4 in object cmd4 in package $sess

== Source file context for tree position ==

     5           .ReplBridge
     6           .value
     7           .Internal
     8           .print(x, "x", _root_.scala.None)
     9     ) }
    10   override def toString = "cmd4"
    11
RikiyaOtaRikiyaOta

以下はエラーになる

math.sqrt(4).narrow
// <console>:17: error: Expression scala.math.`package`.sqrt(4.0) does not evaluate to a constant or a stable reference value
//        math.sqrt(4.0).narrow
//                 ^
// <console>:17: error: value narrow is not a member of Double
//        math.sqrt(4.0).narrow
//                       ^

narrow は複合式には使えず、コンパイル時に確定しているリテラルに対してだけ、シングルトン型を付与するように設計されているとのこと。なので以下は(もちろん)書ける:

(1.2).narrow
res11: 1.2 = 1.2
RikiyaOtaRikiyaOta

shapeless では、フィールド名の literal type による "tagging" をして、フィールドの型にtagをつけるっぽい。

val number = 42

numberは runtime, compile-time いずれにおいても単に Int として扱われる。
runtime での振る舞いを変えずに、compile-time での型を変える方法として、phantom type を用いた tagging がある。

RikiyaOtaRikiyaOta

以下は、Cherries という Phantom type を使って number 変数の型を Int with Cherries にキャストしている例。異なる Int 同士を区別するためのテクニックらしい。runtime では変わらず Int として計算される。

trait Cherries

val numCherries = number.asInstanceOf[Int with Cherries]
// numCherries: Int with Cherries = 42

shapeless では、フィールド名の singleton type を使って ADT (とそのサブタイプ)のフィールドをタグ付する。そのための2つの syntax を提供している。

RikiyaOtaRikiyaOta

1つ目が ->>。矢印の右側の式を、左側のリテラル(の singleton type)でタグ付する。

import shapeless.labelled.{KeyTag, FieldType}
import shapeless.syntax.singleton._

val someNumber = 123

val numCherries = "numCherries" ->> someNumber
// numCherries: Int with shapeless.labelled.KeyTag[String("numCherries"),Int] = 123

KeyTag["numCherries", Int] という Phantom type でタグづけされている様子が見て取れる。

RikiyaOtaRikiyaOta

2つ目は、literal ではなく type でタグ付するもの。

import shapeless.labelled.field

field[Cherries](123)
// res11: shapeless.labelled.FieldType[Cherries,Int] = 123

FieldType は以下で定義されるエイリアス。

type FieldType[K, V] = V with KeyTag[K, V]

なのでよく見たら、先ほどの "numCherries" ->> someNumber と同じっぽい。

RikiyaOtaRikiyaOta

tag はcompile-timeにのみ存在し、runtime では参照できない。
runtime で使える値に変換するために、shapeless は Witness 地おう型クラスを提供している。

WitnessFieldType を組み合わせることで、タグに使われているフィールド名を取得できる。

import shapeless.Witness

val numCherries = "numCherries" ->> 123
// numCherries: Int with shapeless.labelled.KeyTag[String("numCherries"),Int] = 123

// Get the tag from a tagged value:
def getFieldName[K, V](value: FieldType[K, V])
    (implicit witness: Witness.Aux[K]): K =
  witness.value

getFieldName(numCherries)
// res13: String = numCherries

タグ付されていない値として取得することもできる。

// Get the untagged type of a tagged value:
def getFieldValue[K, V](value: FieldType[K, V]): V =
  value

getFieldValue(numCherries)
// res15: Int = 123

tagging された要素からなる HList は、Mapに似たデータ構造となる。
shapeless ではこのデータ構造を records と呼ぶ。

RikiyaOtaRikiyaOta

先ほど見た通り、Record はタグづけされた要素からなる HList である。

val garfield = ("cat" ->> "Garfield") :: ("orange" ->> true) :: HNil

garfieldの型は以下のようなものである。

// FieldType["cat",    String]  ::
// FieldType["orange", Boolean] ::
// HNil

LabelledGeneric は、product, coproduct をそれらのフィールド名でタグ付するものとのこと。(ただし、名前は String ではなくSymbol である)

record については深掘りせず、LabelledGeneric の例を見ていく。

RikiyaOtaRikiyaOta

5.3 Deriving product instances with LabelledGeneric を読んでいく。

RikiyaOtaRikiyaOta

LabelledGeneric の例として、 JSON encode を見ていく。

まず JSON data type を定義していく。

sealed trait JsonValue
case class JsonObject(fields: List[(String, JsonValue)]) extends JsonValue
case class JsonArray(items: List[JsonValue]) extends JsonValue
case class JsonString(value: String) extends JsonValue
case class JsonNumber(value: Double) extends JsonValue
case class JsonBoolean(value: Boolean) extends JsonValue
case object JsonNull extends JsonValue
RikiyaOtaRikiyaOta

エンコーダーと、primitive type に対するエンコーダーインスタンスを実装していく。

trait JsonEncoder[A] {
  def encode(value: A): JsonValue
}

object JsonEncoder {
  def apply[A](implicit enc: JsonEncoder[A]): JsonEncoder[A] = enc
}

def createEncoder[A](func: A => JsonValue): JsonEncoder[A] =
  new JsonEncoder[A] {
    def encode(value: A): JsonValue = func(value)
  }

implicit val stringEncoder: JsonEncoder[String] =
  createEncoder(str => JsonString(str))

implicit val doubleEncoder: JsonEncoder[Double] =
  createEncoder(num => JsonNumber(num))

implicit val intEncoder: JsonEncoder[Int] =
  createEncoder(num => JsonNumber(num))

implicit val booleanEncoder: JsonEncoder[Boolean] =
  createEncoder(bool => JsonBoolean(bool))

list, option に対してのエンコーダーは上の primitive のインスタンスから解決できるよう implicit defで定義しておく。

implicit def listEncoder[A]
    (implicit enc: JsonEncoder[A]): JsonEncoder[List[A]] =
  createEncoder(list => JsonArray(list.map(enc.encode)))

implicit def optionEncoder[A]
    (implicit enc: JsonEncoder[A]): JsonEncoder[Option[A]] =
  createEncoder(opt => opt.map(enc.encode).getOrElse(JsonNull))
RikiyaOtaRikiyaOta

JSON エンコードした際に、フィールド名がアウトプットにいい感じに入っていると嬉しい。
具体的には、フィールド名が Map のキーのようなものとして出力されると嬉しい。

この時に LabelledGeneric を使う。

val gen = LabelledGeneric[IceCream].to(iceCream)
// gen: String with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("name")],String] :: Int with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("numCherries")],Int] :: Boolean with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("inCone")],Boolean] :: shapeless.HNil = Sundae :: 1 :: false :: HNil

理解しやすいように書くと以下のような HList になる:

// String  with KeyTag[Symbol with Tagged["name"], String]     ::
// Int     with KeyTag[Symbol with Tagged["numCherries"], Int] ::
// Boolean with KeyTag[Symbol with Tagged["inCone"], Boolean]  ::
// HNil

各要素を見ると、タグ付けされた Symbolでタグ付された要素になっている。(ややこしい)

RikiyaOtaRikiyaOta

ここまで、String, Int, Boolean や、List, Option についてのエンコーディングを実装した。
HList のエンコーディングを実装する。

JsonObject を扱いたいので、それ用の振る舞いを用意する。JsonObjectEncoderJsonEncoder を継承していることは注意。

trait JsonObjectEncoder[A] extends JsonEncoder[A] {
  def encode(value: A): JsonObject
}

def createObjectEncoder[A](fn: A => JsonObject): JsonObjectEncoder[A] =
  new JsonObjectEncoder[A] {
    def encode(value: A): JsonObject =
      fn(value)
  }
RikiyaOtaRikiyaOta

HNil のエンコーダーインスタンス。JsonObject(Nil)Nil は、ListNil なこと(一応)注意。

implicit val hnilEncoder: JsonObjectEncoder[HNil] =
  createObjectEncoder(hnil => JsonObject(Nil))
RikiyaOtaRikiyaOta

hlistEncoder を1つずつ実装してく。まず以下のような定義になることが考えられる

※書籍だと、return type が JsonEncoder[H :: T] になってるけど、たぶん JsonObjectEncoder[H :: T] が正しそう。あとで Pull Request 送る?

implicit def hlistObjectEncoder[H, T <: HList](
  implicit
  hEncoder: Lazy[JsonEncoder[H]],
  tEncoder: JsonObjectEncoder[T]
): JsonObjectEncoder[H :: T] = ???

Lazy はここで勉強した。Coproductなんかは implicit resolution を発散させないようにする必要があった。
https://zenn.dev/link/comments/8edf4d8d054da1

RikiyaOtaRikiyaOta

上では H :: T を使っているので、HList からフィールド名が読み取れない。
LabelledGeneric を使うと以下のようになる:

implicit def hlistObjectEncoder[K, H, T <: HList](
  implicit
  hEncoder: Lazy[JsonEncoder[H]],
  tEncoder: JsonObjectEncoder[T]
): JsonObjectEncoder[FieldType[K, H] :: T] = ???

型パラメータKが増えて、return type でFieldType[K, H] :: Tを使っていることがポイント。
HList の少なくとも head がタグづけされた型であることを宣言している( T は厳密にはそうでない?)

RikiyaOtaRikiyaOta

Witness を使い、compile-time でのみ存在しているタグの情報を runtime で扱えるように抜き出す。

↓ここで勉強した。
https://zenn.dev/link/comments/8d6a83003d8e72

LabelledGeneric ではフィールド名が String でなく Symbol でタグ付されていたことを思い出す。
https://zenn.dev/link/comments/8487506f12b32f

implicit def hlistObjectEncoder[K <: Symbol, H, T <: HList](
  implicit
  witness: Witness.Aux[K],
  hEncoder: Lazy[JsonEncoder[H]],
  tEncoder: JsonObjectEncoder[T]
): JsonObjectEncoder[FieldType[K, H] :: T] = {
  val fieldName: String = witness.value.name
  ???
}
RikiyaOtaRikiyaOta

以上を踏まえて、全体の実装は以下になる。

implicit def hlistObjectEncoder[K <: Symbol, H, T <: HList](
  implicit
  witness: Witness.Aux[K],
  hEncoder: Lazy[JsonEncoder[H]],
  tEncoder: JsonObjectEncoder[T]
): JsonObjectEncoder[FieldType[K, H] :: T] = {
  val fieldName: String = witness.value.name
  createObjectEncoder { hlist =>
    val head = hEncoder.value.encode(hlist.head)
    val tail = tEncoder.encode(hlist.tail)
    JsonObject((fieldName, head) :: tail.fields)
  }
}

JsonObject の定義なども思い出しながら。
https://zenn.dev/link/comments/d2270f569e4942

RikiyaOtaRikiyaOta

ようやく generic instance を導出する実装に入る。
Generic を使っていた時の実装とほぼ同じ。LabelledGeneric を使うことを除いて。

implicit def genericObjectEncoder[A, H](
  implicit
  generic: LabelledGeneric.Aux[A, H],
  hEncoder: Lazy[JsonObjectEncoder[H]]
): JsonEncoder[A] =
  createObjectEncoder { value =>
    hEncoder.value.encode(generic.to(value))
  }

↓ここで csv の generic encoder を定義していたが、これと同じような実装であることがわかる。Lazy とかはあるけど。
https://zenn.dev/link/comments/b26b544ac03b2b

RikiyaOtaRikiyaOta

これで Json Encoding ができるようになった。挙動を見てみる。

JsonEncoder[IceCream].encode(iceCream)
// res14: JsonValue = JsonObject(List((name,JsonString(Sundae)), (numCherries,JsonNumber(1.0)), (inCone,JsonBoolean(false))))

↓手元で動かした方がなんかわかりやすいかも。

RikiyaOtaRikiyaOta

ここでも、IceCream という個別の case class に対する json encoder を明示的には定義していないことを思い出す。

IceCream という case class から導かれる Generic (上では LabelledGeneric)の encoder を定義していただけで、コンパイラーがうまいこと自動で導出してくれる。generic programming の嬉しいところがここと理解している。

RikiyaOtaRikiyaOta

5.4 Deriving coproduct instances with LabelledGeneric を読んでいく。

RikiyaOtaRikiyaOta

まあこれも実装は同様なので省略。Inl, Inr にパターンマッチさせるようにするだけ。

RikiyaOtaRikiyaOta

5.5 Summary にて。

The key take home point from this chapter is that none of this code uses runtime reflection. Everything is implemented with types, implicits, and a small set of macros that are internal to shapeless. The code we’re generating is consequently very fast and reliable at runtime.

ここまで見てきたような shapless を使ったコーディングは全て compile-time での動作するようなもので、runtime での動作は速いものになると書いてある。では、コンパイル時間はどうなの?と思うが、実際どうなんだろう。

RikiyaOtaRikiyaOta

6 Working with HLists and Coproducts を読んでいく。

shapeless.ops パッケージを見ていき、もう少し shapless を使ったコーディングの例を見ていく感じかな。

RikiyaOtaRikiyaOta

3つの基本tけいなパッケージがある。

  • shapeless.ops.hlist
    • HList の型クラスが定義されている。
    • shapeless.syntax.hlist に拡張メソッドが定義されている。
  • shapeless.ops.coproduct
    • Coproduct の型クラスが定義されている
    • shapeless.syntax.coproduct に拡張メソッドが定義されている。
  • shapeless.ops.record
    • shapless record の型クラスが定義されている。
    • shapeless.record を import して拡張メソッドが使える(定義は shapeless.syntax.record

↓ record についてはこの辺りを思い出す。
https://zenn.dev/link/comments/8d6a83003d8e72

RikiyaOtaRikiyaOta

shapeless.ops.hlist.Initshapeless.ops.hlist.Last に基づいた拡張メソッド init , last がある。

それぞれ、最後以外の要素、最後の要素を返すようなメソッドである。

以下は Last の定義の概略:

trait Last[L <: HList] {
  type Out
  def apply(in: L): Out
}

object Last {
  type Aux[L <: HList, O] = Last[L] { type Out = O }
  implicit def pair[H]: Aux[H :: HNil, H] = ???
  implicit def list[H, T <: HList]
    (implicit last: Last[T]): Aux[H :: T, last.Out] = ???
}

pair で、要素1つの HList のインスタンスを導出できるようにしている感じかな?
list では2つ以上の要素の HList のインスタンスを導出する。Aux の2つ目のパラメータが last.Out になっており、last: Last[T] なので、やはり tail の最後の要素を返すようなインスタンスになっている。

再帰的な定義になっているっぽい?

RikiyaOtaRikiyaOta

要素0だとコンパイルエラーになる。

HNil.last
// <console>:16: error: Implicit not found: shapeless.Ops.Last[shapeless.HNil.type]. shapeless.HNil.type is empty, so there is no last element.
//        HNil.last
//
RikiyaOtaRikiyaOta

6.2 Creating a custom op (the “lemma” pattern) を読んでいく。

lemma は補題とかの意味。数学でしか見たことない単語。読み飛ばしていたけど4.4 で出ていたらしい。ちょっと振り返る。

RikiyaOtaRikiyaOta

Chapter4 では Aux を使って dependent type を chain させることを学んでいた。

HList の head, tail を分解して制約をかけるような型クラスとして IsHCons も学んだ(忘れてた)。

これらのパーツを組み合わせて(chainさせて)新しい型クラスを定義していくようなパターンを Lemma pattern と呼んでいるっぽい。数学の定理や命題の証明で、使いやすい補題をいくつか示しておくパターンに確かに似ている。

RikiyaOtaRikiyaOta

Penultimate という、「最後から2番目の要素を取り出す」型クラスを作り上げながら練習していく。

RikiyaOtaRikiyaOta

apply, Aux はいつも通り。

import shapeless._

trait Penultimate[L] {
  type Out
  def apply(l: L): Out
}

object Penultimate {
  type Aux[L, O] = Penultimate[L] { type Out = O }

  def apply[L](implicit p: Penultimate[L]): Aux[L, p.Out] = p
}

apply では Aux を return type に使わないと、type Out の情報をコンパイラーが消してしまうことを思い出す。

RikiyaOtaRikiyaOta

1つインスタンスを定義すれば良い(すごい)。

import shapeless.ops.hlist

implicit def hlistPenultimate[L <: HList, M <: HList, O](
  implicit
  init: hlist.Init.Aux[L, M],
  last: hlist.Last.Aux[M, O]
): Penultimate.Aux[L, O] =
  new Penultimate[L] {
    type Out = O
    def apply(l: L): O =
      last.apply(init.apply(l))
  }
  • L が入力の HList 全体。
  • init: hlist.Init.Aux[L, M] でまず、L の末尾の要素を落とした HList M を抜き出す。
  • last: hlist.Last.Aux[M, O] でその M の末尾の要素 O を抜き出す。
  • Penultimate.Aux[L, O] を返せば、L の最後から2番目の要素 O を返す型クラスを表現できている。
  • def apply(l: L): O = last.apply(init.apply(l)) は上の処理の実装。そのまま。
RikiyaOtaRikiyaOta

以下のように拡張メソッドを定義するとちょっと便利。

implicit class PenultimateOps[A](a: A) {
  def penultimate(implicit inst: Penultimate[A]): inst.Out =
    inst.apply(a)
}

bigList.penultimate
// res7: Boolean = true
RikiyaOtaRikiyaOta

PenultimateHList に対して定義していた。なのでもちろん、Generic を間にかませば、全ての product type (i.e case class) に対してインスタンスを定義できる。

implicit def genericPenultimate[A, R, O](
  implicit
  generic: Generic.Aux[A, R],
  penultimate: Penultimate.Aux[R, O]
): Penultimate.Aux[A, O] =
  new Penultimate[A] {
    type Out = O
    def apply(a: A): O =
      penultimate.apply(generic.to(a))
  }

確かに動く。すごい。

RikiyaOtaRikiyaOta

Penultimate という型クラスを別の型クラスと定義したことで、使いたいところで手軽に使えるツールになっている。shapeless はこういった感じの便利な "ops" を多く提供してくれているっぽい。

RikiyaOtaRikiyaOta

6.3 Case study: case class migrations を読んでいく。

case class のフィールドを消したり並べ替えたり足したりして、別の case class に "migration" する処理を組み立てる例を見ていく。

RikiyaOtaRikiyaOta
case class IceCreamV1(name: String, numCherries: Int, inCone: Boolean)

// Remove fields:
case class IceCreamV2a(name: String, inCone: Boolean)

// Reorder fields:
case class IceCreamV2b(name: String, inCone: Boolean, numCherries: Int)

// Insert fields (provided we can determine a default value):
case class IceCreamV2c(
  name: String, inCone: Boolean, numCherries: Int, numWaffles: Int)

以下のように使えることを目指す。

IceCreamV1("Sundae", 1, false).migrateTo[IceCreamV2a]

(型の変換、詰め替えのボイラープレートをたくさん書かなくて済むメリットがありそう)

RikiyaOtaRikiyaOta

Migration 型クラスは、ある型クラスから目的の型クラスへの移行を表す。type member を用意する必要はないので、Aux pattern は使わない(移行の過程で現れる HList などをここで表現する必要はないという理解)。

trait Migration[A, B] {
  def apply(a: A): B
}

拡張メソッドも用意しておく。

implicit class MigrationOps[A](a: A) {
  def migrateTo[B](implicit migration: Migration[A, B]): B =
    migration.apply(a)
}
RikiyaOtaRikiyaOta

3つのステップで実装する:

  1. A を generic な表現(HList)に変換する。
  2. 1の HListのうち、Bに存在するフィールドだけ残すようフィルターする。
  3. その HListBに変換する。

step2 では、Intersection という型クラスを利用する。

RikiyaOtaRikiyaOta

割と実装はシンプル。

import shapeless._
import shapeless.ops.hlist

implicit def genericMigration[A, B, ARepr <: HList, BRepr <: HList](
  implicit
  aGen  : LabelledGeneric.Aux[A, ARepr],
  bGen  : LabelledGeneric.Aux[B, BRepr],
  inter : hlist.Intersection.Aux[ARepr, BRepr, BRepr]
): Migration[A, B] = new Migration[A, B] {
  def apply(a: A): B =
    bGen.from(inter.apply(aGen.to(a)))
}

https://github.com/milessabin/shapeless/blob/shapeless-2.3.2/core/src/main/scala/shapeless/ops/hlists.scala#L1297-L1352

Intersection.Aux は3つの型パラメータをとる。
1つ目・2つ目は、入力になる型パラメータ。交差をとる対象。3つ目は出力になる型パラメータ。ここでは、Aが持つフィールドのうち、Bが持っていないフィールドを削除して最終的にBを出力したいので上のような実装になる。

RikiyaOtaRikiyaOta

ちゃんと動作する。

This means implicit resolution will only succeed if B has an exact subset of the fields of A, specified with the exact same names in the same order

Intersection では順序が変わってるとかだと implicit parameter が解決できないので以下は失敗する。

RikiyaOtaRikiyaOta

6.3.3 Step 2. Reordering fields を読んでいく。

genericMigration をどんどん拡張していく感じ。まさに Lemma を積み上げている。

RikiyaOtaRikiyaOta

Align という、HList を並び替える型クラスを使う。Align の並び替え元の HList を表す Unaligned を型パラメータに加えた。Intersection.Aux の出力で指定することで、先ほどあった順序の制約を回避している(先ほどは BRepr という具体的な HList を指定したので、ゆるくした感じかな)。

implicit def genericMigration[
  A, B,
  ARepr <: HList, BRepr <: HList,
  Unaligned <: HList
](
  implicit
  aGen    : LabelledGeneric.Aux[A, ARepr],
  bGen    : LabelledGeneric.Aux[B, BRepr],
  inter   : hlist.Intersection.Aux[ARepr, BRepr, Unaligned],
  align   : hlist.Align[Unaligned, BRepr]
): Migration[A, B] = new Migration[A, B] {
  def apply(a: A): B =
    bGen.from(align.apply(inter.apply(aGen.to(a))))
}
RikiyaOtaRikiyaOta

並び替えも削除もできるようになっている。すごい。

フィールドの追加がまだ動かない。

RikiyaOtaRikiyaOta

Cats では Monoid のインスタンスを多く定義してくれている。HNil, :: に対してのインスタンスを定義したい。

RikiyaOtaRikiyaOta

手元の Ammonite 環境で cats を使えるように調べるのが面倒だったので、自分で Monoid instance を定義した。

RikiyaOtaRikiyaOta

HNil の monoid

implicit val hnilMonoid: Monoid[HNil] =
  createMonoid[HNil](HNil)((x, y) => HNil)
RikiyaOtaRikiyaOta

↓これはどんなインスタンスだろう?

implicit def emptyHList[K <: Symbol, H, T <: HList](
  implicit
  hMonoid: Lazy[Monoid[H]],
  tMonoid: Monoid[T]
): Monoid[FieldType[K, H] :: T] =
  createMonoid(field[K](hMonoid.value.empty) :: tMonoid.empty) {
    (x, y) =>
      field[K](hMonoid.value.combine(x.head, y.head)) ::
        tMonoid.combine(x.tail, y.tail)
  }

empty が、HList 各要素の empty からなる Monoid instance っぽい。combine は単に :: である。(結局、combine は使っているのか)

なお、Monoid[FieldType[K, H] :: T]なので、各フィールドがタグ付されているものとして扱っている。

field は↓この辺で使った。リテラルでなく、型を使ってタグ付けする際のユーティリティ。
https://zenn.dev/link/comments/48680c73cb6178

RikiyaOtaRikiyaOta

最終的に以下のステップで目的を達成する。

  1. use LabelledGeneric to convert A to its generic representation;
  2. use Intersection to calculate an HList of fields common to A and B;
  3. calculate the types of fields that appear in B but not in A;
  4. use Monoid to calculate a default value of the type from step 3;
  5. append the common fields from step 2 to the new field from step 4;
  6. use Align to reorder the fields from step 5 in the same order as B;
  7. use LabelledGeneric to convert the output of step 6 to B.

AにはなくてBには存在するフィールドがある場合、そのデフォルト値をstep4で Monoidを使って計算する感じ。

RikiyaOtaRikiyaOta

最終的に以下。Diff, Prepend がしれっと出てくるが雰囲気で理解してくれ。

implicit def genericMigration[
  A, B, ARepr <: HList, BRepr <: HList,
  Common <: HList, Added <: HList, Unaligned <: HList
](
  implicit
  aGen    : LabelledGeneric.Aux[A, ARepr],
  bGen    : LabelledGeneric.Aux[B, BRepr],
  inter   : hlist.Intersection.Aux[ARepr, BRepr, Common],
  diff    : hlist.Diff.Aux[BRepr, Common, Added],
  monoid  : Monoid[Added],
  prepend : hlist.Prepend.Aux[Added, Common, Unaligned],
  align   : hlist.Align[Unaligned, BRepr]
): Migration[A, B] =
  new Migration[A, B] {
    def apply(a: A): B =
      bGen.from(align(prepend(monoid.empty, inter(aGen.to(a)))))
  }
RikiyaOtaRikiyaOta

流石に補足入れる。Diffの定義は以下。

https://github.com/milessabin/shapeless/blob/shapeless-2.3.2/core/src/main/scala/shapeless/ops/hlists.scala#L1354-L1394

HList の subtraction (引き算) を表す型クラスなので、Diff.Aux[BRepr, Common, Added]は、ARepr, BRepr の共通部分 Common には含まれない BRepr の要素を Added として抜き出すことを表現している。つまり、A には無いが B には存在するフィールドを表す。追加すべきもの。

なのでそのあとで Monoid[Added] を要請している。

RikiyaOtaRikiyaOta

Note that this code doesn’t use every type class at the value level.

確かに、diff はランタイムの実装で使っていない。その代わり、Added を Monoid インスタンスの召喚に使っている。面白い。

RikiyaOtaRikiyaOta

確かに全部動くようになっている。

改めて抑えるべきことは、IceCreamV1IceCreamV2a のペアに対して Migration[A,B]のインスタンスを個別に定義しているのではなく、implicit def genericMigration という1つの暗黙のインスタンス定義をしているだけで、全ての case class の組み合わせについて動作するコードを書くことができている。

generic programing の強力さが現れている。すごい。

RikiyaOtaRikiyaOta

Seletor 型クラスによって、get 拡張メソッドでアクセスできる。

RikiyaOtaRikiyaOta
  • updated メソッド、Updater 型クラス
  • remove メソッド、Remover 型クラス

を見ていく。

RikiyaOtaRikiyaOta

6.4.3 Converting to a regular Map を読んでいく。

toMap で record ==> Map の変換ができる。

RikiyaOtaRikiyaOta

型クラスをベースに functional に HList を操作していく手法を見ていく。

まず、shapeless が polymorphic function をどう表現するか?を見ていく。
HListは名前の通り、各要素が異なる具体的な型を持つから、普通の List#map などとは事情が異なることは容易に想像できる)

RikiyaOtaRikiyaOta

Figure 7.2 のように、polymorphic function を通じて HList 全体を変換できる仕組みが欲しいという話。

通常の scala function ではできないので、別の仕組みが必要。

RikiyaOtaRikiyaOta

Poly という型が提供されている。polymorphic function を表現する型で、parameter の型に応じて結果の型が決まるようなものを表す。

以下では、ガチの Poly は少し省略して説明のために簡素にした API を作って説明するっぽい?

作成者以外のコメントは許可されていません