Scalaのオブジェクト同士を比較する際に、==
を使って比較することは出来るが、同値かそうでないかしかわからない。
そこで、具体的に何がどう変わったのかをジェネリックに取得するライブラリを作ってみた。
こんな雰囲気で使える。
$ amm Loading... Welcome to the Ammonite Repl 1.6.7 (Scala 2.12.8 Java 1.8.0_192) If you like Ammonite, please support our development at www.patreon.com/lihaoyi @ import $ivy.`net.petitviolet::generic-diff:0.6.0-RC2`; import net.petitviolet.generic.diff._ import $ivy.$ ; import net.petitviolet.generic.diff._ @ case class Hoge(i: Int, s: String) defined class Hoge @ Hoge(100, "foo") diff Hoge(100, "bar") res: DiffResult[Hoge] = DiffResult(List(FieldSame("i", 100), FieldDiff("s", "foo", "bar")))
関係ないけどAmmonite便利。
case classのフィールドで異なる値があればそれがわかるようになっている
ソースコードはpetitviolet/scala-generic-diffに公開してあってScala2.13.0-RC2まで無駄に対応してみた。
ユースケース
例えば、オブジェクトのフィールドを更新する処理の中で、実際に値が書き換わっていたらDBに保存するが、値が書き換わっていなければDBへの保存処理をスキップする、などが出来る。 あるいはバリデーションで何も更新されていないよと突き返すことも。
classのequals
をoverrideしてidによる同値チェックで実装しているような場合とか。
DDDのEntityでそういう処理をしていることは多そう。
@ case class User(id: String, name: String) { override def equals(obj: scala.Any): Boolean = obj match { case other: User => other.id == this.id case _ => false } def updateName(newName: String) = new User(this.id, newName) } defined class User @ val user = User("user-id", "alice") user: User = User("user-id", "alice")
こういうuser
がいた時に、==
での比較するとこうなる。
@ user == user res: Boolean = true @ user == user.updateName("bob") res: Boolean = true
これに対してdiff
を取ると、
@ user diff user res: DiffResult[User] = DiffResult(List(FieldSame("id", "user-id"), FieldSame("name", "alice"))) @ user diff user.updateName("bob") res: DiffResult[User] = DiffResult(List(FieldSame("id", "user-id"), FieldDiff("name", "alice", "bob")))
このようにフィールドごとに同値判定をして、同じ値かどうかを結果として返却してくれる。
異なるフィールドが存在したかどうかはhasDiff
を参照すれば可能。
@ (user diff user).hasDiff res: Boolean = false @ (user diff user.updateName("bob")).hasDiff res: Boolean = true
技術的な話
ここからはライブラリの実装的な話でトピックは以下。
- ジェネリックプログラミング
- macro
ジェネリックプログラミング
case classのコンストラクタ全体を参照するのにshapelessを使っている。 ジェネリックにフィールド名と値を取得している。
実装はdiff.scalaのこのあたり。
case classをHList
で表現しつつ、LabelledGeneric
でフィールド名と値の組を扱っている。
再帰的にフィールドを参照しつつdiffをとっていくような実装となっていて、アイデアはcirceから学ぶ GenericProgramming入門を パクった 参考にした。
macro
こんな感じにdiff
した結果をフィールド名で参照できるようにするためにmacroを使っている。
@ val diffResult = user diff user.updateName("bob") diffResult: DiffResult[User] = DiffResult(List(FieldSame("id", "user-id"), FieldDiff("name", "alice", "bob"))) @ diffResult.id res: Field = FieldSame("id", "user-id") @ diffResult.name res: Field = FieldDiff("name", "alice", "bob")
ScalaのDynamicsを使ってフィールドを自由に指定することが出来るようにしつつ、内部的にはmacroを使って型安全性を担保している。
ここのアイデアはscalikejdbc/SQLSyntaxSupportFeature.scalaを参考に実装した。
ここでフィールドとして存在しない名前を指定するとコンパイルで死ぬ。
@ diffResult.hoge java.util.NoSuchElementException: field #hoge not found. net.petitviolet.generic.diff.DiffResult.$anonfun$field$2(GenericDiff.scala:29) scala.Option.getOrElse(Option.scala:138) net.petitviolet.generic.diff.DiffResult.field(GenericDiff.scala:29) ammonite.$sess.cmd15$.<init>(cmd15.sc:1) ammonite.$sess.cmd15$.<clinit>(cmd15.sc)
Dynamic
だけどマクロで型安全に実装していてScala便利。
今後の展望
使うのかどうかよく分からないので展望とか今のところ特にない。