Refactoring with Functional Programming Style
+ - 0:00:00
Notes for current slide
Notes for next slide

Refactoring with Functional Programming Style

Scala関西Summit 2015/08/01

1 / 53

自己紹介

2 / 53

今日は宣伝に来ました

t2v

3 / 53

Scalaコードレビュー サービス

  • GitHub(or類似サービス)上でのOnlineコードレビュー
  • Slack等チャットツールによるQ&A
  • 月数回程度のOfflineミーティングを相談に応じて
4 / 53

今日の内容

  • 過去の事例から共通する典型的なパターンを紹介
  • 実際のコードに FP Style をどう適用するのか示しつつ
    どんな感じのレビューなのか雰囲気を感じて貰えれば
5 / 53

典型例 その1

巨大なListのループ

6 / 53

最初のコード

case class Product(id: ProductId, name: Name)
def createItem(product: Product): Item = ???
def createCodes(name: Name, item: Item): List[Code] = ???
val products: List[Product] = ??? // サイズが大きいことが想定される
val allItemsBuf: mutable.ListBuffer[Item] = mutable.ListBuffer()
val allCodesBuf: mutable.ListBuffer[Code] = mutable.ListBuffer()
for (product <- products) {
val item = createItem(product)
allItemsBuf += item
allCodesBuf ++= createCodes(product.name, item)
}
val allItems = allItemsBuf.toList
val allCodes = allCodesBuf.toList
7 / 53

これ 進研○ミ でやったところだ! 高階関数使うんだ!

def createItem(product: Product): Item = ???
def createCodes(name: Name, item: Item): List[Code] = ???
val products: List[Product] = ??? // サイズが大きいことが想定される
val allItems = products.map(createItem)
val allCodes = products.flatMap { p =>
createCodes(p.name, createItem(p))
}

やった! mutable が無くなった!
 

8 / 53

シンプルにはなった。けれど……

def createItem(product: Product): Item = ???
def createCodes(name: Name, item: Item): List[Code] = ???
val products: List[Product] = ??? // サイズが大きいことが想定される
val allItems = products.map(createItem)
val allCodes = products.flatMap { p =>
createCodes(p.name, createItem(p))
}

2回全件ループしてしまっている
createItem も2重に実行している

9 / 53

「やっぱり効率考えると普通のループの方が……」

「FP Style って実用には向かないんじゃ……」

10 / 53

ちょっと待って!!

11 / 53

そこで Monoid ですよ

12 / 53

Monoid とは

以下の性質を満たす集合と2項演算の組み合わせ

  • 演算が集合に対し閉じている
    • 基本的に集合は型で表す
    • つまり引数の型と戻り値の型が同じという事
  • 演算が結合法則を満たす
    • a, b, c を集合の要素として、演算を |+| とした時
      (a |+| b) |+| c == a |+| (b |+| c) が成り立つという事
    • 交換法則(可換則) ではない事に注意
  • 単位元が存在する
    • e |+| a == a
    • a == a |+| e
    • 全ての要素に対して上記が成り立つ e が存在するという事
13 / 53

例えば

  • 整数と足し算
    • 整数 + 整数 = 整数 (演算が集合に対し閉じている)
    • (1 + 2) + 3 == 1 + (2 + 3) (結合法則を満たす)
    • 0 + a == a かつ a + 0 == a (単位元 0 が存在する)
  • Listと連結
    • List[Int] ++ List[Int] の結果は List[Int]
    • (List(1) ++ List(2)) ++ List(3) ==
           List(1) ++ (List(2) ++ List(3))
    • Nil ++ List(1) == List(1) かつ List(2) ++ Nil == List(2)
14 / 53

Monoidではない例

  • 整数と割り算
    • 整数÷整数の結果は整数とは限らない(演算が閉じていない)
  • 整数と引き算
    • (1 - 2) - 3 != 1 - (2 - 3)(結合法則を満たさない)
15 / 53

そして Foldable

16 / 53

Foldable とは

畳み込みができる型クラス

※型クラスとは、
クラス定義時とは別に後から実装を提供できるinterfaceのようなもの
17 / 53

みんな大好き foldLeft

scala> List(1, 2, 3).foldLeft(0)(_ + _)
res0: Int = 6 // 0 + 1 + 2 + 3
18 / 53

もし List の要素が Monoid だったら

List(...).foldLeft(単位元)(2項演算)

畳み込みが可能!!

19 / 53

でも常に要素が Monoid とは限らない

20 / 53

そこで foldMap

def foldMap[B: Monoid](f: A => B): B

「要素の型を任意のMonoidへ変換する関数」
を受け取り畳み込みを行う

flatMap が map して flatten するイメージなら
foldMap は map して fold するイメージ
21 / 53

これを使うと最初の例(再掲)も

def createItem(product: Product): Item = ???
def createCodes(name: Name, item: Item): List[Code] = ???
val products: List[Product] = ??? // サイズが大きいことが想定される
val allItems = products.map(createItem)
val allCodes = products.flatMap { p =>
createCodes(p.name, createItem(p))
}
22 / 53

foldMap で書き直せる

def createItem(product: Product): Item = ???
def createCodes(name: Name, item: Item): List[Code] = ???
import scalaz.std.list._
//import scalaz.std.tuple._
import scalaz.syntax.foldable._
val products: List[Product] = ??? // サイズが大きいことが想定される
val allItems = products.foldMap { p => List(createItem(p)) }
val allCodes = products.foldMap { p =>
createCodes(p.name, createItem(p))
}

でもこれでは 2回ループしてるのは変わらない

23 / 53

Monoid は合成できる

24 / 53

2つの Monoid[A] と Monoid[B] を元に

タプルのモノイド、Monoid[(A, B)] を定義することが可能

def tuple2[A, B](ma: Monoid[A], mb: Monoid[B]): Monoid[(A, B)] = {
new Monoid[(A, B)] {
def zero: (A, B) = (ma.zero, mb.zero)
def append(x: (A, B), y: (A, B)): (A, B) = {
(ma.append(x._1, y._1), mb.append(x._2, y._2))
}
}
}
25 / 53

つまり、以下(再掲)を

def createItem(product: Product): Item = ???
def createCodes(name: Name, item: Item): List[Code] = ???
import scalaz.std.list._
//import scalaz.std.tuple._
import scalaz.syntax.foldable._
val products: List[Product] = ??? // サイズが大きいことが想定される
val allItems = products.foldMap { p => List(createItem(p)) }
val allCodes = products.foldMap { p =>
createCodes(p.name, createItem(p))
}

 

26 / 53

次のように書き換える事ができる

def createItem(product: Product): Item = ???
def createCodes(name: Name, item: Item): List[Code] = ???
import scalaz.std.list._
import scalaz.std.tuple._
import scalaz.syntax.foldable._
val products: List[Product] = ??? // サイズが大きいことが想定される
val (allItems, allCodes) = products.foldMap { p =>
val item = createItem(p)
(List(item), createCodes(p.name, item))
}

めでたくループが1回に!

27 / 53

Monoid と Foldable の
本当の力をお見せしますよ

28 / 53

Monoidの2項演算は結合則を満たす

29 / 53

従って、要素の順序さえ同じであれば、どこから結合しても問題ない

30 / 53

つまり、平衡畳み込みが可能

31 / 53

foldLeft での畳み込み

List(a,b,c,d).fold // append(append(append(a, b), c), d)

平衡畳み込み

List(a,b,c,d).fold // append(append(a, b), append(c, d))
32 / 53

appendの計算量によっては
全体の計算量を大きく減らすことができる

また、スレッドを使った並列計算も可能になる

33 / 53

ここまでのまとめ

  • 巨大な集合に対する状態を持った複雑なループも
    Monoid と Foldable を使うことで
    簡潔に記述できる場合がある
  • Monoid や Foldable の実装を工夫することで、
    パフォーマンスを改善できる場合がある
34 / 53

典型例 その2

Validationと更新

35 / 53

要件

  • CSVのファイルを読み込む
  • 全ての行をvalidateし、
    エラーが1件でもあれば全てのエラーを出力して終了
  • エラーが全行で無ければ各行をEntityに変換し、
    後続処理をする
36 / 53

最初のコード main

val records: List[String] = loadCsv()
val errors: List[String] = validate(records)
if (errors.isEmpty) {
updateRecords(records)
} else {
outputErrors(errors)
}
def validate(records: List[String]): List[String] = {
// 後述
}
def updateRecords(records: List[String]): Unit = {
// 後述
}
37 / 53

最初のコード validate

def validate(records: List[String]): List[String] = {
val errors: mutable.ListBuffer[String] = mutable.ListBuffer()
for (record <- records) {
val columns = record.split(",")
if (columns.size != 4) errors += "less columns"
else {
if (Try(columns(0).toLong).isFailure) errors += "invalid id"
if (columns(1).size > 10) errors += "name too long"
val min = Try(columns(2).toInt)
val max = Try(columns(3).toInt)
if (min.isFailure) errors += "invalid min"
if (max.isFailure) errors += "invalid max"
if (min.isSuccess && max.isSuccess) {
if (min.get > max.get) {
errors += "min is grater than max"
}
}
}
}
errors.toList
}
38 / 53

最初のコード updateRecords

case class ScoreRange(min: Int, max: Int)
case class Entity(id: Long, name: String, scoreRange: ScoreRange)
def updateRecords(records: List[String]): Unit = {
val entities: mutable.ListBuffer[Entity] = mutable.ListBuffer()
for (record <- records) {
val columns = record.split(",")
val id = columns(0).toLong
val name = columns(1)
val scoreRange = ScoreRange(columns(2).toInt, columns(3).toInt)
entities += Entity(id, name, scoreRange)
}
batchUpdate(entities.toList)
}
39 / 53

何が悪いのか

  • validateが入り組みすぎており単体テストができない
  • validate と updateRecords で同じ変換処理を行っている
  • updateRecords が validate を通ったという暗黙の前提に依存しきっている
  • 2重でrecordsループを回している
40 / 53

そこで Validation ですよ

41 / 53

Validation[+E, +A] とは

Scala 標準の Either もしくは Try のように、

でできており、

  • エラーを蓄積する合成が可能になっている
  • また成功時の値をそのまま保持する事ができる
42 / 53

したがって、Validation を使うことで

  • 最小のvalidationロジックを組み立てる事で全体のvalidationロジックを記述する事が可能になり、Testabilityがあがる
  • validation成功時の値を持つことで、後続処理で2重に変換ロジックを行う必要が無くなる

という事が実現できる

43 / 53

ValidationNel

エラーを蓄積する際に、NonEmptyList をよく使うので

type ValidationNel[E, A] = Validation[NonEmptyList[E], A]

というエイリアスが切られている

44 / 53

この ValidationNel で以下(再掲)の main を書き直すと

case class ScoreRange(min: Int, max: Int)
case class Entity(id: Long, name: String, scoreRange: ScoreRange)
val records: List[String] = loadCsv()
val errors: List[String] = validate(records)
if (errors.isEmpty) {
updateRecords(records)
} else {
outputErrors(errors)
}
def validate(records: List[String]): List[String] = {
// 後述
}
def updateRecords(records: List[String]): Unit = {
// 後述
}
45 / 53

次のようになる

case class ScoreRange(min: Int, max: Int)
case class Entity(id: Long, name: String, scoreRange: ScoreRange)
val records: List[String] = loadCsv()
val validated: ValidationNel[String, List[Entity]] = validate(records)
validated match {
case Success(entities) => batchUpdate(entities)
case Failure(errors) => outputErrors(errors.list)
}
def validate(records: List[String]): ValidationNel[String, List[Entity]] = {
// 後述
}
// 直接 List[Entity] 扱えるので updateRecords は必要なくなる
46 / 53

validation は以下の様に(長いので分割)

def validate(records: List[String]): ValidationNel[String, List[Entity]] = {
import scalaz.Validation.FlatMap._
records.foldMap { record =>
for {
columns <- validateColumn(record)
entity <- validateEntity(columns)
} yield List(entity)
}
}
def validateColumn(record: String): ValidationNel[String, Array[String]] = {
val columns = record.split(",")
if (columns.size == 4) columns.successNel
else "less columns".failureNel
}
def validateEntity(col: Array[String]): ValidationNel[String, Entity] = {
import scalaz.syntax.apply._
(validateId(col(0)) |@|
validateName(col(1)) |@|
validateScoreRange(col(2), col(3)))(Entity)
}
47 / 53
def validateId(id: String): ValidationNel[String, Long] = {
Validation.fromTryCatchNonFatal(id.toLong).leftMap(_ => NonEmptyList("invalid id"))
}
def validateName(name: String): ValidationNel[String, String] = {
if (name.size <= 10) name.successNel
else "name too long".failureNel
}
def validateScoreNum(num: String, column: String): ValidationNel[String, Int] = {
Validation.fromTryCatchNonFatal(num.toInt).leftMap(_ => NonEmptyList(s"invalid $column"))
}
def validateMinMax(min: String, max: String): ValidationNel[String, (Int, Int)] = {
import scalaz.syntax.apply._
(validateScoreNum(min, "min") |@| validateScoreNum(max, "max"))((n, x) => (n, x))
}
def validateScoreRangeConstraint(min: Int, max: Int): ValidationNel[String, ScoreRange] = {
if (min <= max) ScoreRange(min, max).successNel
else "min is grater than max".failureNel
}
def validateScoreRange(min: String, max: String): ValidationNel[String, ScoreRange] = {
import scalaz.Validation.FlatMap._
validateMinMax(min, max).flatMap { case (n, x) => validateScoreRangeConstraint(n, x) }
}
48 / 53

ここまでのまとめ

  • 入り組んだvalidationや変換ロジックはValidationを使うことで小さいロジックから合成できるようになり、Testabilityを上げる事ができる
  • Monoid も Validation もComposability(合成可能性)が肝になっている
49 / 53

総括

こんな感じで、Functional Programming Style を生かした Scala のコーディングについてレビューしたりチャットで質問に回答したりしています。

50 / 53
  • チームメンバーにScalaのスペシャリストが居ない
  • もっと業務コードに密接に絡んだ質問がしたい

などなどありましたら是非Scalaコードレビューサービスの利用をご検討ください。

51 / 53

お申し込み

52 / 53

質問とか

53 / 53

自己紹介

2 / 53
Paused

Help

Keyboard shortcuts

, , Pg Up, k Go to previous slide
, , Pg Dn, Space, j Go to next slide
Home Go to first slide
End Go to last slide
Number + Return Go to specific slide
b / m / f Toggle blackout / mirrored / fullscreen mode
c Clone slideshow
p Toggle presenter mode
t Restart the presentation timer
?, h Toggle this help
Esc Back to slideshow