Posted at

Futureでの副作用に気をつけないといけないお話。特にRepoとかの永続化時


qiita.scala


type ProcessR[T] = EitherT[Future, Errors, T]

override def add(
e: AccountingEvent
)(implicit op: DBOperation): ProcessR[AccountingEvent] = ProcessR.success {
val added = exec { implicit s =>
val uid = applyUpdateAndReturnGeneratedKey(insert.into(~~~~~)) <- DBへ永続化している
e.copy(uid = uid)
}
added
}


こんな感じのRepoのaddがあったとして、

insertした結果のjeneratedKeyを戻り値の中に含めているので、副作用ではないからと油断していると痛い目に会う


①副作用を残してしまう例


qiita.scala

 j <- ProcessR.success{

Seq(journal1,journal2,journal3).foldLeft(Seq.empty[Journal]){case(js, j) =>
eventRepo.add(event)
js :+ j
}
}
} yield {
(j, errors) ==> ここの段階でeventRepo.addの中身の処理が終わっている確証はない。
}


②副作用を残さない例


qiita.scala

 j <- ProcessR.success{

Seq(journal1,journal2,journal3).foldLeft((Seq.empty[Journal], Seq.empty[ProcessR[AccountingEvent]])){case((js, es), j) =>
val event = eventRepo.add(event)
(js :+ j, es :+ event)
}
}
} yield {
(j._1, errors) ==> ここの段階でeventRepo.addの中身の処理は確実に終わっている
}

foldLeftを使っているから、必ず1処理ごとに完了してから次の処理が行われるはずなのに


なぜこの現象が起こるのか?

①の例だとeventRepo.addの結果をfoldLeftの戻りにbindしていない。

なのでeventRepo.addの処理はFutureで走っているので、別スレッドで評価が開始される。

全部回りきる頃には、Seq[Journal]と言う実態が完成している。

が、裏側ではまだeventRepo.addが頑張って永続化をしている。


qiita.scala

for{

j <- Seq(journal1,journal2,journal3)
} yield {
ここに来るのはjが完全に評価されたタイミング
}

となるので、jにeventRepo.addの結果を含めていないとyieldにきても副作用が残ってしまっている。

②の例だと、


qiita.scala

val event = eventRepo.add(event)


と結果をeventにbindしている。

そしてfoldLeftの結果にも含めているので、


qiita.scala

      } yield{

(j._1, errors)
}

に来る頃には、必ずjの処理が終わっていることが確約される。


最後に

もしかしたら間違えて理解している説もあるので、そうなら優しく指摘してください(マサカリはだめ)