読者です 読者をやめる 読者になる 読者になる

by shigemk2

当面は技術的なことしか書かない

まとめ Refactoring in Scala #ScalaMatsuri #sm_a

Scala

@gakuzzzz

Tech to Value

Professional Null Cleanerと呼ばれるなど

値の型について

" DDDではValue Object " Scalaの表現力

case classとかtraitとか定義してる例があって、ビジネスロジックが定義されている

case class Board
case class User

このコードには問題がある。

  • Map[Long, User] は意味情報としてたいして情報が読めない(何を意味しているのか分からない)
  • 引数を間違えてもコンパイルエラーにならない(実行時例外を投げる可能性が高い)

ゆえに、怖いコードである

最初のアプローチ

type aliasを使う 型の別名

package object models {
  type UserId = Long
  type GroupId = Long
  .....
}

型の別名を使うことで、何の情報なのかコードを見て意味通りにすることが出来る

  • Map[UserId, User]→Clear semantics
  • 物理表現の変更が簡単(全部Longだと型を別のに修正しようとするときに大変)
type UserId = String

でも、引数ミスをコンパイルエラーで弾けない

次のアプローチ

Tagged Type

型にラベルをつける仕組み Scalaz/shapelessなどで提供 型引数をふたつとる型は中置記法 中置記法のための@@

type Tagged[A, T] = {type Tag = T, type Self = A}
Map[Id @@ User, User]

Tagged Typeをつかうと、@@などが使える

  • コードの意味も明白になる
  • ミスしてもコンパイルエラーになる
  • バイトコードとしてのキャストになるのでwrap/unwrapもない

注意

AnyVal(Javaだとプリミティブになるやつ)と使うとBoxing/Unboxingが起きる

次のアプローチ その2

  • Value Class
  • メモリ割り当てを削減したいときに使える
class UserId(val value: Long) extends AnyVal

http://docs.scala-lang.org/ja/overviews/core/value-classes.html

  • Map[UserId, User]になるので、意味も明白かつコンパイルエラーも弾きやすい
  • コンパイル時にinline展開してくれるので、wrap/unwrapのコストもかからない

" 間違えてメモリ割り当てされる場合がある * 値クラスが別の型として扱われるとき * 値クラスが配列に代入するとき * パターンマッチングで実行時の型検査をおこなうとき

Tagged TypeとValue Classどちらを使うべきか

自分のプロジェクトで状況に合わせてつかうべき

  • Tagged Type コンテナタイプのキャストをサポート(Value Classだとめんどい)
  • Value Class メソッド定義の追加が簡単(Tagged Typeだとめんどい)

UserId/BoardIdとかいろいろ書くのめんどくさい

次のアプローチ

  • Phantom Types
    • 値に使用されない型引数を使って制約を表現する手法 " コンパイルすると幽霊のように消える

https://www.google.co.jp/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=phantom%20types

まとめ

  • 実際の物理表現と意味表現を分離(エイリアス)
  • 間違いの検出しやすく(Tagged Typeと値クラス)
  • ジェネリクスでボイラーテンプレートをなくす(幽霊型)

  • 新しい問題 WebフレームワークとかRDB操作ライブラリなどの外部のライブラリは自分たちのオレオレ定義の型の情報を知らない
  • レイヤー境界で型変換する必要があるんだけど
  • これを手書きしてるとしんどい

これの改善が次の課題

メジャーなフレームワークは境界に型変換する機能がある

  • Play(Formatter)/Skinny/Slick(ColumnType)/ScalikeJDBC
  • 今使っているライブラリにこの機能がない場合はPRを投げるかそのプロジェクトから逃げよう
    • たとえばLongからId型への変換が自動的にフレームワーク側で行われる
  • IdとかNameとかはドメインのオブジェクト(層の責務/レイヤーわけに失敗してる)

次のアプローチ

Iso(あいそもーふぃーずむ)とPrismを使おう

Iso

  • Isomorphism ふたつの型が完全に相互変換可能なことをあらわす
    • from(to(a)) == a
    • to(from(b)) == b

Prism

  • Isoに似ているが、片方の変換が必ず成功する(失敗する可能性)とは限らない
    • from(to(a)) == a
    • to(from(b)) == b

http://the.igreque.info/posts/2015-06-09-lens-prism.html

  • Prismは生成に制約があるような値型で利用する
  • IsoはPrismのサブクラスにすることが出来る

IsoであればPrismになることが出来る

  • Iso/Prismはシンプルなやつなので、Monocleやshapelessなどのライブラリで提供されている
  • 自分でつくってもいいし、こういったライブラリを使ってもいいでしょう
  • IsoはScalazでも導入されている

  • Iso/Prismはレイヤー/アーキの表現に依存しないのがアドバンテージ。

  • ドメイン層の責務を破る心配はない

  • Prismを使って変換するのがよい

  • Iso/Prismだけを提供すれば全てのライブラリに対応できる
  • でも手書きするとボイラープレートになってしんどい

そこでimplicit macroを使用することでIsoの定義を簡略化できる implicit macroのドキュメントの具体例がIsoそのもの Scala 2.11/2.12はmacroはexperimentalな機能なので長いプロジェクトではちょっと考えるべき

まとめ

  • レイヤー境界ではフレームワーク/ライブラリに応じた変換処理
  • Iso/Prismを使ってコードの依存性を下げる DRYに出来る
  • マクロを使って定義のボイラーテンプレートをなくせる

資料

http://gakuzzzz.github.io/slides/refactoring_in_scala/

宣伝
  • Tech to Valueではこんな感じのコードレビューをオンラインでサービスとしてやっている

その他

幽霊型 http://akabe.github.io/2015/12/PhantomTypeTricks/

質疑

  • TypeAlias こんかいやりたかったのはPublicなやつであって、Path依存型の話になると面倒
  • TypeProjectionをつかうとさらに読みづらい

まとめ

  • DDD
  • Iso/Prism/Lens