Guide to Shapless を読む

shapeless guide を通して generic programming を学びたいのでその備忘録。

1,2章はサラッと読んだ。
2章では、Product, Coproduct の構造だけを抽出して HList, Coproduct という型への双方向の変換ができることを学んだ。
『構造だけ抜き出す』というのが垣間見えた気がする。

3 Automatically deriving type class instances を読んでいく。
Product の CsvEncoder を型クラスとして定義し、そのインスタンスの導出を shapeless でできるっぽい?
型クラスは便利である程度抽象化されているけど、結局手で instance を定義しないといけないよね、そこの分離ができないよね、って書いてある。

3.2 Deriving instances for products を読んでいく。
直感的に以下2点が考えられる:
-
HList
の head, tail それぞれの型クラスのインスタンスがあれば、HList
全体のインスタンスを導出できそう。 - case class
A
があればGeneric[A]
が得られる。Generic[A]
はRepr
に構造を保持している。これらを組み合わせて、A
のインスタンスを作れる。
1点目はまあそうかなぁ。
CsvEncoder
のインスタンスとして、要素それぞれのインスタンスがあれば、出力(List)を連結するだけ。
2点目も、割とそのまんま?
Generic[A]
があれば A
を HList
に変換する方法も知っていて、その構造は Repr
が表現している。その HList
をエンコードする方法がわかっていれば A
のインスタンスたり得る。
そんな感じかな?読み進める。

今更だが、shapeless を REPL で動かせるものがあったのでそれで確認している。
Try shapeless with an Ammonite instant REPL
の部分を参照。
Ammonite はこちら。

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"))

HNil
の Encoder。末尾に HNil
があるならこれは必要か。
import shapeless.{HList, ::, HNil}
implicit val hnilEncoder: CsvEncoder[HNil] =
createEncoder(hnil => Nil)

ようやく。最初に言っていたことの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)
}

個別の要素の 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)

implicitly
は初めて見た。implicit value があればそれを呼び出す感じか。
上では、CsvEncoder[String :: Int :: Boolean :: HNil]
の implicit value を呼び出しているのか。
参考:https://stackoverflow.com/questions/3855595/what-is-the-scala-identifier-implicitly

3.2.2 Instances for concrete products を読んでいく。

具体的な case class , ここでは IceCream
の CsvEncoder を Generic
を使って導出している。
IceCream --> HList
を gen.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
}

確かに 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

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

以下のようなコードを考えてみる。
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 する方法を知っていればいいはず。

直接 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 というものらしい。型クラスに制約を追加するようなものみたい。
ここでいうと、genericEncoder[A,R]
が受け取る R
に一致する type Repr
を持つような Generic[A]
を受け取るという制約になるのかな。Generic
の型インスタンスが Repr
を持つことは知っているので、うまくはまるのか。
ここで大事なのは、何か case class 的なもの(A
)とその構造 (R
, HList)を指定して CsvEncoder が自動導出されるようになったということ。

先ほどの 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)))

Aux pattern なるものがあるらしい。ここでも shapeless が例として挙げられている。

3.2.3 So what are the downsides?を読んでいく。

ここまでのコードがコンパイルエラーになった時の話。
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)))
//

もう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 のテクニックが紹介されるらしいけど、基本的には地道にエラーの原因を探るしかないのか?

3.3 Deriving instances for coproducts を読んでいく。

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

ワンライナーで実行したらできた🎉
@ 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

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 なので実際に問題は起きない。

今時点ではエラーが起きる。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

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

よく例として挙げられる、再帰的データ構造
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

::[H,T]
, :+:[H,T]
だともう少し状況はよろしくない。
コンパイラが同じ type constructor を2回見つけて、かつその型パラメータがより複雑になっているなら、その分岐は"発散"しているとみなす。
shapeless では ::[H,T]
, :+:[H,T]
が繰り返し出てくるのでコンパイラがうまく implicit を解決してくれないっぽい。

3.4.2 Lazy を読んでいく。
上記の implicit の発散を防ぐために Shapeless は Lazy
という型を提供している。
Lazyがすること:
- 前述の過剰に防御的な収束する発見方法を防ぎ、コンパイル時の impclit 発散を抑制する。
- 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]]
も導出できる。

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

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

4 Working with types and implicits を読んでいく。

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つの型を参照している。

以下の関数はどんな型を返すだろうか?
import shapeless.Generic
def getRepr[A](value: A)(implicit gen: Generic[A]) =
gen.to(value)
gen が受け取った Generic インスタンスに寄る。A
の generic な構造に応じた HList
や Coproduct
を返すと思われる。
実際そうなる。
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

ここで見ているのが dependent typing だそう。
つまり、getRepr
が返す方は、型パラメータ A
に依存して( Repr
を経由して)決まっている。ある型が別の型によって決まっているという感じかな。
A
が入力、Repr
が出力と考えると良いらしい。

4.2 Dependently typed functions を読んでいく。

そのほかの Dependent type の例を見ていく。
shapeless には Last
という型クラスがある。HList
の最後の要素を返すものらしい。
単純化した実装は以下:
package shapeless.ops.hlist
trait Last[L <: HList] {
type Out
def apply(in: L): Out
}
ここでも L
が入力、Out
が出力になっている。

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

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]
// ^

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)
//

さらに例として、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
}

Second.apply
で Aux[L, inst.Out]
を返している点に注意。
単に Second[L]
を返してしまうと、コンパイラが type Out
の情報を消してしまう。
Second
trait 自体には、L
と Out
に何か関係があるような定義はされていないので。
inst
で解決された型の情報を保つために必要らしい。
shapeless が提供する the
も使える。
この例のような、dependent type を扱う際には、独自の summoer を定義するか the
を使うかなどする必要がある。

以下の implicit def で、少なくとも2つの要素を持った HList
の Second
インスタンスを自動で導出できる。
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つ目の要素を返すといった具体的な処理は定義していないので、上のようなインスタンスが必要。

なので、要素が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
を実装して振る舞いを確認してみた。

4.3 Chaining dependent functions を読んでいく。

先ほどの Last
, Second
のような関数( apply があるという意味で)を chain させる例を見ていく。
以下は、型 A
の HList
の最後の要素を返そうとしているような関数。だが、コンパイルエラーになる。
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 を使うところ。

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)

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
である」という関係性をコンパイラが認識できるようになる。
このスタイルでコーディングしていくと言っている。
別の例を見てみる。

ただ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
がそのまま表示されていることがヒントっぽい。

gen
が過剰に制約を課せられていることが問題のよう。
コンパイラは Repr
の長さまでは確定できない模様。というか、同時に複数の制約を解決できず、ここでは、Repr
を見つけつつ、かつその長さまでチェックできないっぽい?
Nothing
が現れる時も注意が必要で、共変な型パラメータを統合しようとして失敗した際に出てくるらしい。
implicit 解決を2つのステップに分解:
-
A
に適したRepr
を持つGeneric
を見つける。 - 先頭の型が
H
であるRepr
を提供する。
=:=
を使った例が以下。ev: (Head :: Tail) =:= Repr
なので、Head :: Tail
と Repr
が型として一致するときだけ 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
は、HList
を Head
とTail
に分解する型クラスとのことです。
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かな。

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

LabelledGeneric
という Generic
の変種があり、フィールド名や型名などにアクセスできる?らしい。
これを理解するために少し理論的なこと学ぶ。
keyword: literal types
, singleton types
, phantom types
, type tagging

5.1 Literal types を読んでいく

"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 な型に拡大するので、普通はお目にかからないっぽい。

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

以下はエラーになる
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

5.2 Type tagging and phantom types を読んでいく。

shapeless では、フィールド名の literal type による "tagging" をして、フィールドの型にtagをつけるっぽい。
val number = 42
number
は runtime, compile-time いずれにおいても単に Int
として扱われる。
runtime での振る舞いを変えずに、compile-time での型を変える方法として、phantom type を用いた tagging がある。

余談:phantom type は scala with cats でも取り上げられていた。runtime において参照されないような型(パラメータ)のこと。

以下は、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 を提供している。

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 でタグづけされている様子が見て取れる。

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
と同じっぽい。

tag はcompile-timeにのみ存在し、runtime では参照できない。
runtime で使える値に変換するために、shapeless は Witness
地おう型クラスを提供している。
Witness
と FieldType
を組み合わせることで、タグに使われているフィールド名を取得できる。
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
と呼ぶ。

5.2.1 Records and LabelledGeneric を読んでいく。

先ほど見た通り、Record
はタグづけされた要素からなる HList
である。
val garfield = ("cat" ->> "Garfield") :: ("orange" ->> true) :: HNil
garfield
の型は以下のようなものである。
// FieldType["cat", String] ::
// FieldType["orange", Boolean] ::
// HNil
LabelledGeneric
は、product, coproduct をそれらのフィールド名でタグ付するものとのこと。(ただし、名前は String ではなくSymbol である)
record については深掘りせず、LabelledGeneric
の例を見ていく。

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

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

エンコーダーと、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))

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でタグ付された要素になっている。(ややこしい)

5.3.1 Instances for HLists を読んでいく。

ここまで、String, Int, Boolean や、List, Option についてのエンコーディングを実装した。
HList
のエンコーディングを実装する。
JsonObject
を扱いたいので、それ用の振る舞いを用意する。JsonObjectEncoder
が JsonEncoder
を継承していることは注意。
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)
}

HNil
のエンコーダーインスタンス。JsonObject(Nil)
の Nil
は、List
の Nil
なこと(一応)注意。
implicit val hnilEncoder: JsonObjectEncoder[HNil] =
createObjectEncoder(hnil => JsonObject(Nil))

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 を発散させないようにする必要があった。

上では 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
は厳密にはそうでない?)

Witness
を使い、compile-time でのみ存在しているタグの情報を runtime で扱えるように抜き出す。
↓ここで勉強した。
↓LabelledGeneric
ではフィールド名が String
でなく Symbol
でタグ付されていたことを思い出す。
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
???
}

以上を踏まえて、全体の実装は以下になる。
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
の定義なども思い出しながら。

5.3.2 Instances for concrete products を読んでいく。

ようやく 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
とかはあるけど。

これで Json Encoding ができるようになった。挙動を見てみる。
JsonEncoder[IceCream].encode(iceCream)
// res14: JsonValue = JsonObject(List((name,JsonString(Sundae)), (numCherries,JsonNumber(1.0)), (inCone,JsonBoolean(false))))
↓手元で動かした方がなんかわかりやすいかも。

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

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

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

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 での動作は速いものになると書いてある。では、コンパイル時間はどうなの?と思うが、実際どうなんだろう。

6 Working with HLists and Coproducts を読んでいく。
shapeless.ops
パッケージを見ていき、もう少し shapless を使ったコーディングの例を見ていく感じかな。

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 についてはこの辺りを思い出す。

6.1 Simple ops examples を読んでいく。

shapeless.ops.hlist.Init
と shapeless.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 の最後の要素を返すようなインスタンスになっている。
再帰的な定義になっているっぽい?

もちろん以下のように導出できる。

拡張メソッドも使える(タイポした)。

要素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
//

6.2 Creating a custom op (the “lemma” pattern) を読んでいく。
lemma は補題とかの意味。数学でしか見たことない単語。読み飛ばしていたけど4.4 で出ていたらしい。ちょっと振り返る。

Chapter4 では Aux を使って dependent type を chain させることを学んでいた。
HList
の head, tail を分解して制約をかけるような型クラスとして IsHCons
も学んだ(忘れてた)。
これらのパーツを組み合わせて(chainさせて)新しい型クラスを定義していくようなパターンを Lemma pattern と呼んでいるっぽい。数学の定理や命題の証明で、使いやすい補題をいくつか示しておくパターンに確かに似ている。

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

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
の情報をコンパイラーが消してしまうことを思い出す。

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
の末尾の要素を落とした HListM
を抜き出す。 -
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))
は上の処理の実装。そのまま。

確かに動く。

以下のように拡張メソッドを定義するとちょっと便利。
implicit class PenultimateOps[A](a: A) {
def penultimate(implicit inst: Penultimate[A]): inst.Out =
inst.apply(a)
}
bigList.penultimate
// res7: Boolean = true

Penultimate
は HList
に対して定義していた。なのでもちろん、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))
}
確かに動く。すごい。

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

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

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

6.3.1 The type class を読んでいく。

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)
}

6.3.2 Step 1. Removing fields を読んでいく。

3つのステップで実装する:
-
A
を generic な表現(HList
)に変換する。 - 1の
HList
のうち、B
に存在するフィールドだけ残すようフィルターする。 - その
HList
をB
に変換する。
step2 では、Intersection
という型クラスを利用する。

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

ちゃんと動作する。
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 が解決できないので以下は失敗する。

6.3.3 Step 2. Reordering fields を読んでいく。
genericMigration
をどんどん拡張していく感じ。まさに Lemma を積み上げている。

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))))
}

並び替えも削除もできるようになっている。すごい。
フィールドの追加がまだ動かない。

6.3.4 Step 3. Adding new fields を読んでいく。

フィールドを追加するにあたり、フィールドのデフォルト値を計算する仕組みが必要。
Cats の Monoid
を使うことにする。
Monoid
についてはここでは説明しない。
ここでは Monoid#empty
だけ必要みたい。combine
は使わない?

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

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

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

↓これはどんなインスタンスだろう?
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
は↓この辺で使った。リテラルでなく、型を使ってタグ付けする際のユーティリティ。

最終的に以下のステップで目的を達成する。
- use LabelledGeneric to convert A to its generic representation;
- use Intersection to calculate an HList of fields common to A and B;
- calculate the types of fields that appear in B but not in A;
- use Monoid to calculate a default value of the type from step 3;
- append the common fields from step 2 to the new field from step 4;
- use Align to reorder the fields from step 5 in the same order as B;
- use LabelledGeneric to convert the output of step 6 to B.
AにはなくてBには存在するフィールドがある場合、そのデフォルト値をstep4で Monoidを使って計算する感じ。

最終的に以下。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)))))
}

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

Prepend
は以下。
名前の通り、先頭にくっつける感じか。
Prepend.Aux[Added, Common, Unaligned]
は、くっつけた結果を Unaligned
としている。
最終的にAlign[Unaligned, BRepr]
で並び替えるから、くっつける方向は別に大事ではないのか。機能的には。非機能は知らない。

Note that this code doesn’t use every type class at the value level.
確かに、diff
はランタイムの実装で使っていない。その代わり、Added
を Monoid インスタンスの召喚に使っている。面白い。

確かに全部動くようになっている。
改めて抑えるべきことは、IceCreamV1
や IceCreamV2a
のペアに対して Migration[A,B]
のインスタンスを個別に定義しているのではなく、implicit def genericMigration
という1つの暗黙のインスタンス定義をしているだけで、全ての case class の組み合わせについて動作するコードを書くことができている。
generic programing の強力さが現れている。すごい。

6.4 Record ops を読んでいく。

record については前に出た。タグ付された HList を扱い、Map のような操作を可能にするデータ構造。
record ops は以下で import する。
import shapeless.record._

6.4.1 Selecting fields を読んでいく。

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

6.4.2 Updating and removing fields を読んでいく。

-
updated
メソッド、Updater
型クラス -
remove
メソッド、Remover
型クラス
を見ていく。

あまり言うことはない気がする。

6.4.3 Converting to a regular Map を読んでいく。
toMap
で record ==> Map の変換ができる。

6.4.4 Other operations を読んでいく。
他にも色々あるからソースコード見てね、という話。
Map 相当の操作ができそうということを知っていればよさそう?

Chapter 7 Functional operations on HLists を読んでいく。

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

7.1 Motivation: mapping over an HList を読んでいく

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

7.2 Polymorphic functions を読んでいく。

Poly
という型が提供されている。polymorphic function を表現する型で、parameter の型に応じて結果の型が決まるようなものを表す。
以下では、ガチの Poly
は少し省略して説明のために簡素にした API を作って説明するっぽい?

7.2.1 How Poly works を読んでいく。