record4s
ScalaでExtensible Recordsを行うためのライブラリ。平たくいうと、Scalaでも構造的型付け(structural typing)が便利にできるようにするライブラリ
手軽
型安全
拡張可能
なのが売り。
スライド
環境
ここではScala 3.5.1
% scala-cli --dep "com.github.tarao::record4s:0.13.0"
レコードを作る
import com.github.tarao.record4s.%する
%(foo = "bar")すると% { val foo: String = "bar" }というレコード型の値を作れる
わざわざcase class Foo(foo: String)みたいな定義をしなくて良い
ネストもできる
レコードのフィールドを得る
val r = %(foo = "bar")のとき、r.fooを呼ぶと"bar"を得られる
TypeScriptのObject型みたいなイメージ
同上のとき、r.barを呼ぼうとするとコンパイルエラーになる
Map型との違い
コンパイルタイミングで値が存在することが保証されている
% { val foo: String = "bar" }型はその構造によってのみ比較される
例えば、以下の関数を定義できる
code:scala
def getFooA(r: %{ val foo: A }): A = r.foo getFoo(%(foo = 42, bar = 666))
// => 42
rはfoo: Aフィールドを持っていれば何でも良いので、%(foo = 42, bar = 666)を渡してもコンパイルできるし、動く
レコードに足す
val r = %(foo = "bar")のとき
r + ("hoge" -> 42)は%{ val foo = "bar"; val hoge = 42 }になる
r + (hoge = 42)とも書ける(こっちのが素直そう)
複数追加もできる: r + (hoge = 42, fizz = "buzz")
レコードを結合する
val r = %(foo = "bar")のとき
val s = %(hoge = 42)のとき
r ++ sすると結合されたレコードが得られる
ちなみにr + sするとr ++ %(s = %(hoge = 42))になる(結合されるかわりにsというフィールドに入る)
レコードを更新する
r + (foo = 666)などして結合の構文を利用すると、衝突したときには上書きされた新たなレコードが返る
レコードを引く
レコードの比較
同一のフィールド、同一の値を持つレコード型は、順序に関係なく等しい
code:scala
val r = %(foo = "bar", hoge = 42)
val s = %(hoge = 42, foo = "bar")
r == s // => true
同一のフィールドを持っていれば、他のフィールドを持っていても型の包含関係が成り立つ
code:scala
val r = %(foo = "bar", hoge = 42)
def useOnlyFoo(rr: %{ val foo: Int }) = ???
useOnlyFoo(r) // コンパイルできる
Mapped / Utility Types in record4s
TypeScriptのMapped TypesやUtility Typesをやってみたい
RecordLikeを使うと便利にジェネリックな操作ができそう
Type Projectionの制限により、直接型レベルでフィールドを変換するといったことはできなそう
型レベルではなく、値レベルでusing rl: RecordLike[R]のようなコードが必要になる
特定の%のフィールド名を型レベルで取り出すことができる
code:keyOf.scala
import com.github.tarao.record4s.{%, RecordLike}
def printFieldR <: %(r: R)( // 第一引数は%であればなんでもいい using rl: RecordLikeR // RecordLikeが存在することを期待する(これがフィールド名の情報を供給する) )(
// rl.ElemLabelsは("field1", "field2", ...)になっているため、
// Tuple.Unionで"field1" | "field2" | ... に変換する
fieldName: Tuple.Unionrl.ElemLabels // ここはrのフィールドになっていないとコンパイルできなくなる ): Unit = {
println(r.selectDynamic(fieldName.toString()))
}