Lambdaカクテル

京都在住Webエンジニアの日記です

Shapelessの勉強(その4)

今日は、これまでに学習してきた Genericを拡張した LabelledGenericについて学習していく。これによって、型レベルでフィールド名や型名を扱う事ができるようになり、Shapeless が扱える処理の範囲がさらに広がる。

前回はこちら。

blog.3qe.us

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の中ではおそらく何らかの黒魔術が使われているが、我々が使う分には implicitWitness.Aux[K]を使ってキー Kに対応する Witnessのインスタンスを summon し、値レベルで witness.valueを呼び出せばよい。

Record

Shapeless では、 FieldType[_, _]で構成された HListを Record と呼ぶ。

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

この仕組みにより、 Shapelessでは HListFieldTypeとの枠組みによって辞書的な構造を型レベルで表現できる。

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

AngleDegRadのどちらかであるという前提付きで、そのどちらの型なのか / 値は何か という情報が Coproductとしてエンコードされた。

まとめ

この chapter では、以下のことを学んだ:

  • Literal typeは、唯一のリテラル値のみを持つ型である。
  • 空のtraitなどをmix-inすることで、実行時の振舞いを変更せずにコンパイル時の型だけ操作できる。 (Phantom Type)
  • k ->> vFieldType[K, V]すなわち V with KeyTag[K, V]のための構文糖であり、 literal type Kを持つ 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 では、 HListCoproductを操作する Ops について学んでいく。

Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?