追記: Shapeless入門を記事にまとめました
今日は、これまでに学習してきた Generic
を拡張した LabelledGeneric
について学習していく。これによって、型レベルでフィールド名や型名を扱う事ができるようになり、Shapeless
が扱える処理の範囲がさらに広がる。
前回はこちら。
LabelledGeneric
を理解するために、この Chapter の先頭では、
型レベルでフィールド名や型名にアクセスすることを可能にしているこれらの技法について学んでいく:
- literal types
- singleton types
- phantom types
- type tagging
Literal types
まずは Literal types
から学んでいこう。
Scalaにおいては、同じ値が複数の型に所属するという現象はよくあることだ。例えば、値 "Hello"
は次のような型に所属している:
String
AnyRef
Any
面白いことに、 "Hello"
はもう一つの型に所属している。 Singleton type といって、その値のみを持つ型だ。これはコンパニオンオブジェクトを定義するときに見る型に似ている:
object Foo Foo // res3: Foo.type = ...
この Foo.type
型は値 Foo
の型であり、 Foo
はその唯一の値だ。
リテラルの singleton type のことを literal type と呼ぶ。 しかしコンパイラは自動的に singleton type ではない型へと型を拡大しようとするので、
普段この型に触れることはできない。Shapelessは直接 literal type に触れるようにするための narrow
マクロを提供している。このマクロを使うと、リテラルは literal type で型付けされる。
import shapeless.syntax.singleton._ var x = 42.narrow // x: Int(42) = 42
この x
に登場する Int(42)
こそが literal type である。この型は値 42
のみを値に持つので、他の数値を代入することはできない:
x = 43 // compile error
そして Int(42)
は Int
のサブタイプなので、 Int
に対して可能な操作は x
に対しても可能だ:
x + 1 // => 43: Int
Type taggingとphantom type
さて、 literal type がどこで使われているかというと、 Shapeless は case class のフィールド名を型レベルで扱うために literal type を使っている。 これは各フィールドをフィールド名の literal type を使って tagging することで実現している。これがどのように行われるのかを確認しよう。
まずは、ある数値 number
があるとしよう:
val number = 42
この変数は二つの側面を持つ。すなわち、実行時の姿と、コンパイル時の姿だ。
ここであるテクニックを使うことで、 実行時のふるまいを変更することなくコンパイル時の型を修正できる。その方法とは phantom type による tagging(タギング) だ。
Phantom typeとは、実行時の意味論を持たない型のことである:
trait Cherries // 実装なし
そして、ここで asInstanceOf
を使うことで、 number
をタグ付けできる。すると、
コンパイル時には Int
であり Cherries
でもあるが、実行時には Int
として振る舞う値が得られる:
val numCherries = number.asInstanceOf[Int with Cherries] // Cherriesには実装が無いので、動作に影響しない // => numCherries: Int with Cherries = 42
Shapeless はこの技法を利用してフィールド名・サブタイプ = ADTs 間の関係を紐付けている。
asInstanceOf
を使う代わりに、 Shapeless は便利な文法を用意しているので見ていこう。
->>
->>
は、矢印の右辺値を左側の singleton type でタグ付けする。
"foo" ->> 1234
は、Int with KeyTag["foo", Int]
に型付けされる。->>
は、タグ付け専用の構文であって、 値レベルの意味論に影響しない。タグ付けされた値は、元の値と同じように扱うことができる。type FieldType[K, V] = V with KeyTag[K, V]
という型レベルの構文糖がある。- 型レベルでフィールド名がエンコードされるので、
HList
の中から特定のフィールドを取り出すといった用途で使える。
Witness
Witness
を使うことで、 KeyTag
に使われた singleton type の値を取り出せる。
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)
Witness
の中ではおそらく何らかの黒魔術が使われているが、我々が使う分には implicit
と Witness.Aux[K]
を使ってキー K
に対応する Witness
のインスタンスを summon し、値レベルで witness.value
を呼び出せばよい。
Record
Shapeless では、 FieldType[_, _]
で構成された HList
を Record と呼ぶ。
val garfield = ("cat" ->> "Garfield") :: ("orange" ->> true) :: HNil
この仕組みにより、 Shapelessでは HList
と FieldType
との枠組みによって辞書的な構造を型レベルで表現できる。
LabelledGeneric -- Genericの再来
さて、基礎的な道具が揃ったところで新たな道具が登場する。 LabelledGeneric
は、その名が暗示するように、 Generic
を record に対して拡張する。
case class Vec(x: Double, y: Double) val a = Vec(10.0, 20.0) LabelledGeneric[Vec].to(a) // => 10.0 :: 20.0 :: HNil: Double with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("x")],Double] :: Double with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("y")],Double] :: shapeless.HNil
値レベルではただの HList
にすぎないが、型を見ると KeyTag
を用いてフィールド名がタグ付けされている様子が分かる。
嬉しいことに、 LabelledGeneric
は Coproduct に対しても動作する:
import shapeless.LabelledGeneric sealed trait Angle final case class Deg(d: Double) extends Angle final case class Rad(r: Double) extends Angle val d = Deg(120.0) val r = Rad(2.0 * Math.PI) LabelledGeneric[Angle].to(d) // => Inl(Deg(120.0)): Deg with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("Deg")],Deg] :+: Rad with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("Rad")],Rad] :+: shapeless.CNil LabelledGeneric[Angle].to(r) // => Inr(Inl(Rad(6.283185307179586))): Deg with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("Deg")],Deg] :+: Rad with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("Rad")],Rad] :+: shapeless.CNil
Angle
は Deg
か Rad
のどちらかであるという前提付きで、そのどちらの型なのか / 値は何か という情報が Coproduct
としてエンコードされた。
まとめ
この chapter では、以下のことを学んだ:
- Literal typeは、唯一のリテラル値のみを持つ型である。
- 空のtraitなどをmix-inすることで、実行時の振舞いを変更せずにコンパイル時の型だけ操作できる。 (Phantom Type)
k ->> v
はFieldType[K, V]
すなわちV with KeyTag[K, V]
のための構文糖であり、 literal typeK
を持つKeyTag[K, V]
を用いた phantom typing を行う。Witness.Aux[K]
を使うと、なんらかの黒魔術によって、KeyTag
のliteral typeから値を取り出せる。FieldType
からなるHList
を構成できる。これを record と呼び、 名前でフィールドを呼び出すことができる。LabelledGeneric
を使うと、ADTs を record と相互変換できる。LabelledGeneric
は、Coproduct
にも対応している。
ここで Part 1 はおしまい。この Part では、Shapelessを構成する各部品の基礎について学んだ。 Part 2
では、 HList
や Coproduct
を操作する Ops について学んでいく。