Scala関西Summit 2015/08/01
最初のコード
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.toListval allCodes = allCodesBuf.toList
これ 進研○ミ でやったところだ! 高階関数使うんだ!
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))}
シンプルにはなった。けれど……
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))}
「やっぱり効率考えると普通のループの方が……」
「FP Style って実用には向かないんじゃ……」
以下の性質を満たす集合と2項演算の組み合わせ
a
, b
, c
を集合の要素として、演算を |+|
とした時(a |+| b) |+| c == a |+| (b |+| c)
が成り立つという事e |+| a == a
a == a |+| e
(1 + 2) + 3 == 1 + (2 + 3)
(結合法則を満たす)0 + a == a
かつ a + 0 == a
(単位元 0 が存在する)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)
(1 - 2) - 3 != 1 - (2 - 3)
(結合法則を満たさない)畳み込みができる型クラス
scala> List(1, 2, 3).foldLeft(0)(_ + _)res0: Int = 6 // 0 + 1 + 2 + 3
List(...).foldLeft(単位元)(2項演算)
畳み込みが可能!!
def foldMap[B: Monoid](f: A => B): B
「要素の型を任意のMonoidへ変換する関数」
を受け取り畳み込みを行う
これを使うと最初の例(再掲)も
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))}
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回ループしてるのは変わらない
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)) } }}
つまり、以下(再掲)を
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))}
次のように書き換える事ができる
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))}
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))
appendの計算量によっては
全体の計算量を大きく減らすことができる
また、スレッドを使った並列計算も可能になる
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 = { // 後述}
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}
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)}
Scala 標準の Either もしくは Try のように、
でできており、
したがって、Validation を使うことで
という事が実現できる
エラーを蓄積する際に、NonEmptyList をよく使うので
type ValidationNel[E, A] = Validation[NonEmptyList[E], A]
というエイリアスが切られている
この 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 = { // 後述}
次のようになる
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 は必要なくなる
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)}
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) }}
こんな感じで、Functional Programming Style を生かした Scala のコーディングについてレビューしたりチャットで質問に回答したりしています。
などなどありましたら是非Scalaコードレビューサービスの利用をご検討ください。
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 |