petitviolet_blog

@petitviolet blog

いい感じにオブジェクトのdiffを取るライブラリ作った

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

技術的な話

ここからはライブラリの実装的な話でトピックは以下。

ジェネリックプログラミング

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")

ScalaDynamicsを使ってフィールドを自由に指定することが出来るようにしつつ、内部的には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便利。

今後の展望

使うのかどうかよく分からないので展望とか今のところ特にない。

2018年度の振り返り

社会人4年目の振り返り。
昨年度のはこれ。

petitviolet.hatenablog.com

エンジニアとして

相変わらずサーバサイドでScalaを書くことがほとんど。
GraphQLを実戦投入出来たし、FPとかDDDらへんも深めることが出来た感覚。

GraphQLについてはGraphQLナイトとScala関西で発表も出来た。

petitviolet.hatenablog.com

FPとかDDDっぽい話で行くとこのへん。

fringeneer.hatenablog.com

Kubernetesは実戦投入したこともあってクラウドネイティブという流れにのってトークしてみたり。

petitviolet.hatenablog.com

スキルとしては新しい何かを身につけたというよりは今までのを総合的にアウトプット出来た一年間だったような感じでもある。
新規事業を作るぞ!みたいなところだとそれなりのバリューを発揮できるくらいの力はついてきたと思う多分。

仕事

今年度は1つのプロダクト開発に従事していた。
プロダクトとしては1つだったが、チーム内では広告配信サーバと管理画面のバックエンドAPIとバッチらへんをがっつり設計から実装までやった。 会社としてMicrosoft Azureをまともに使ったのは初めてだったのもあって共同開催の勉強会をするなどしていた。(自分は発表してないけど)

fringe81.connpass.com

Kubernetesも実戦投入出来たし多少の知見は溜まったものの、もっとでかい規模感でやりたいという気持ち。 他には少しAndroidな仕事を久しぶりにするなどもしていた。アプリ作るわけじゃなくてピュアJavaSDK改修してたというくらい。

あとインターンとかの採用関連っぽいところの仕事もそこそこ占めていた感覚。
サマーインターンのお題作ったりとかそういうの。

そして年始いきなり病欠してしまったというのが辛い出来事だった。

twitter.com

プライベート

子どもが少し大きくなって歩き始めたり、家を買うなど。まだリフォーム中だけど。
人生順調である。

来年度に向けて

なんと30歳になるので体調管理頑張りつつもっとスキルの幅を広げていきたいという気持ち。 ちなみに今も風邪をひいていて、年々免疫とか体力とかが落ちてきているというのを実感している。。

そして、自分で書いた小さいライブラリを公開して自分で使ったりはしているものの、OSSにコントリビュートは全くできていないので頑張りたい所存。

ポートフォリオサイトをGatsbyで作り直した

自分のポートフォリオ的なWebサイトとしてhttps://www.petitviolet.netを作っている。

www.petitviolet.net

もともとはHTMLべた書き + Bootstrapで作っていたが、GatsbyJSを使って作り直してみた。

www.gatsbyjs.org

たいして更新することもないので静的サイトジェネレータを使ってジェネレートするほどのことはないと断言できる。
が、たまにはフロントっぽいこともやるかというくらいの理由。
GatsbyJSを選択したのはなんとなく。HugoとかでもよかったけどJS触っておきたかっただけ。

技術っぽい話

技術的な要素を並べてみる。

マルチクラウドな感じですね。
ソースコードは特に隠すものもないのでGitHubにあげてある。

github.com

TypeScriptはとにかく型があるので良い。
普段Scala書いててコンパイラに全部やってもらっているので生JavaScriptを書く技術はなかったし助かった。
そしてCSS慣れてないしやる気もないけど、コンポーネントごとにCSS書けるのは体験が良かった。

ホスティングには、もともとGAEにデプロイしていたのでそれを踏襲することに。
当初の思いとしてはGKEでやりたかったけどお金かかるし例えばNginxとかにぶち込んだりするのもめんどくさいしでやめた。
そもそも静的なファイルをKubernetesで運用する理由がなさすぎてモチベーションわかなかった。
S3、Github Pages、Firebase、Netlifyあたりも候補になるけど単にGAEにも触れておきたい気持ちで選択。

ヘッダとかの画像はCDN通して配信できるようにしたかったのでやった。
普段、公開用の画像とかはS3使ってたのでそれを踏襲してCloud Front使うことにした。
今まではS3のURLを直接使ってたけどせっかくだから独自ドメインで配信することにして、AWSのCertificate ManagerとGoogle Domainsの組み合わせでCDN用のドメインとその証明書を用意したのだけど、証明書の発行にめっちゃ時間かかって困った。

twitter.com

リプライでやり直したらすぐ通ったよ!的なことを教えてもらったのでやったら1分で検証されて発行されて無事httpsでアクセスできるように。
GAEでhttps勝手にやってくれるしそれ以外のリソースについてもCDNhttps配信できるようになったのでまあいい感じでしょう多分。

高速化

CDN配信、画像をwebp化、静的ファイルのキャッシュ長め、といったあたりをやったら速くなった。

f:id:petitviolet:20190224232639p:plainf:id:petitviolet:20190224232646p:plain
PageSpeed Insight

そもそも静的コンテンツなので速くて当たり前ということで。

感想とか

持っててどれくらいの意味があるかわからないポートフォリオをアップデートしたけど、持ってないより持ってたほうがエンジニア感出るかな、メンテナンスしてないよりしてたほうが良いかな、と思ってやった。
いずれは、はてブロやQiita、SpeakerDeckらへんの更新を自動でフェッチして反映するような仕組みとか作れたらいいかもしれない。