Scalaのジェネリックライブラリshapelessの基本

Scalaにはジェネリックプログラミングとか言うものを行うshapelessというライブラリがある。
その基本。

shapelessがやりたいこと

そもそもジェネリックプログラミングとはなんぞやというと、名前の通りジェネリック=総称的なプログラミングやりたくない?ということ。
まあこれでも意味が不明なのでつまりどういうことかと言うと以下のようなこと。

// domainモデル的な
case class Person(name: String, age: Long)

// DTO的な
case class PersonRow(name: String, age: Long)

// コイツラほぼ同じ様なもんだし簡単に詰め替えられたら楽じゃない?
val p = PersonRow("Alice", 20)
p.mapTo[Person] // => Person("Alice", 20)

簡単に言えばstructural typingのように構造としての型が同じなら同じものとしてみなしてグチャグチャしよう、というのがshapelessのやりたいこと。
しかも全て型安全に。

上のは例だが、こういうことがいくらでもできる。

じゃあどうやるのか

ジェネリックプログラミングをやるためにshapelessはすべてのオブジェクトをHListというものに変換する。
HListとはタプルのように異なる要素の型を何個も詰め込めて、リストのように長さが可変なものである。

"Alice" :: 20 :: HNil
// => res0: String :: Int :: shapeless.HNil = Alice :: 20 :: HNil

HNilというのはリストのNilのように長さ0のHListである。
HListは::で繋げばいくらでも伸ばせる。
そして全ての型の要素を保持できる。

これがどう役に立つのかというとPersonの例で出したものと見比べて見るとわかりやすい。

Person("Alice", 20)
PersonRow("Alice", 20)
"Alice" :: 20 :: HNil
// 。。。ほぼ一緒じゃない?

要するにHListはcase classをより抽象化したものとみなせる。

shapelessはcase classをHListに変換する機構を備えている。
そのためユーザは自前のcase classをHListに変換したあとに、shapelessが用意しているHListへの操作をごちゃごちゃやったあとにまた好きなcase classに変換し直せばよい。
shapelessはHListに対する豊富な機能を備えているのでほとんどなんでもできると言って良い。

実際に変換だけやってみよう。

shapelessを用いたcase classの変換

上で述べたHListとcase classはGenericという型クラスを用いれば変換できるようになっている。
Genericは変換対象を表すHListの型を内部的に保持している。
実際に使ってみよう。

case class Person(name: String, age: Long)
case class PersonRow(name: String, age: Long)

val person = Person("Alice", 20)

// Person用のGenericインスタンスを生成
val pgen = Generic[Person]
// pgen: shapeless.Generic[Person]{type Repr = String :: Long :: shapeless.HNil} = anon$macro$3$1@4ebb16f0
// 上のようにPersonを表すHListの方はString :: Long :: shapeless.HNil

// toメソッドはPersonインスタンスをHListに変換する
val hlist = pgen.to(person)
// hlist: pgen.Repr = Alice :: 20 :: HNil

val prgen = Generic[PersonRow]
// prgen: shapeless.Generic[PersonRow]{type Repr = String :: Long :: shapeless.HNil} = anon$macro$3$1@1267cf34
// 当然PersonとPersonRowを表すHListの型は同じ

// PersonRowと同じとみなせるHListはfromメソッドで再びcase classに戻せる
prgen.from(hlist)
// res1: PersonRow = PersonRow(Alice,20)

まあこれで最初に出した例みたいなことはできた。
上の例ではPersonと同じHListで表せるcase classは全て変換できるが、逆に違うHListのものはコンパイルエラーになる。
実行時エラーではなくコンパイルエラー、すごい。

あとは色々やればmapToメソッドは実装できるがそれはまた今度。

fp-tsとoption Polyfunctionでpartially applied typeを改善する fp-tsとSemigroup
View Comments
There are currently no comments.