前置き
さて、前置きですが、モナドと書きましたが実際にはモナドではなく、モナドのようなものです。
ここで言うモナドというものは、Scalaにおけるfor
式で扱えるように、以下のメソッドを実装している型のことを指します。
- map/flatMap
(つまり、for
式で使える型、ifフィルターとyieldではないfor式を除く)
あと、記事中の英語は適当です。
はじめに
どこかで見た便利な書き方としてfor
式のネストした形式を紹介します。(具体的な呼び名は知らない。勝手にネストfor
式とか言っておく。)
Scalaでのエラーハンドリングはキモくなりがちです。
Either
、Try
、Option
の入り混ざったものを一つのfor
式で扱うのは困難で、
最初にfor
式中で使用したモナドに、以降も制限されてしまいます。
scala> {
| for {
| result <- Future { Try("hanage") }
| hanage <- result
| } yield hanage
| }
<console>:22: error: type mismatch;
found : scala.util.Try[String]
required: scala.concurrent.Future[?]
hanage <- result
^
さて、この全部の失敗した結果をまとめてハンドリングしたいが、Scala標準ライブラリだけでやりたいです。
(関数型ライブラリなどをプロダクト開発に使おうとすると、扱える人が極端に限定されるので、個人的にはなるべく扱いたくない。)
ネストfor式を使おう
標準のfor
式の構文は以下のようになっており、seq
はジェネレーター、定義、フィルターを連続した形で指定する。連続した要素はセミコロンで区切られます。
for ( seq ) yield expr
このexpr
の部分にfor
式をネストした形で記述するのがfor
式のネストですね。(そのまま)
for ( seq ) yield {
for ( seq ) yield {
...
}
}
この各for
式は、当然のように異なるfor
式なので、for
式一つにつきモナドを使用することができますね。
つまり、複数のモナドを一つ(に見せかけた)のfor
式で扱いたい場合に使用することができます。
以下に実際にFuture
とTry
をまとめてハンドリングしたネストfor式の例を、、
scala> {
| for {
| result <- Future { throw new Exception; Failure(new Exception) }
| } yield for {
| string <- result
| } yield string
| } onComplete {
| case Success(Failure(_)) => println("Future is Success, but hanage is Failure")
| case Success(_) => println("Success All")
| case Failure(e) => println(s"Future is Failure, because of $e")
| }
Future is Failure, because of java.lang.Exception
scala> {
| for {
| result <- Future { Failure(new Exception) }
| } yield for {
| string <- result
| } yield string
| } onComplete {
| case Success(Failure(_)) => println("Future is Success, but hanage is Failure")
| case Success(_) => println("Success All")
| case Failure(e) => println(s"Future is Failure, because of $e")
| }
scala> Future is Success, but hanage is Failure
scala> {
| for {
| result <- Future { Success(new Exception) }
| } yield for {
| string <- result
| } yield string
| } onComplete {
| case Success(Failure(_)) => println("Future is Success, but hanage is Failure")
| case Success(_) => println("Success All")
| case Failure(e) => println(s"Future is Failure, because of $e")
| }
Success All
Futureによって、ネストされたfor式は別スレッドで実行されます。
全ての処理が完了した時の処理をonCompleteに記述します。
onCompleteに渡すのはPartialFunctionなので、エラーハンドリングは別の関数として定義しておいて受け渡すと例外処理ロジックのノイズが軽減されます。
val errorHandler: Try[Try[String]] => Unit = {
case Success(Failure(_)) => println("Future is Success, but hanage is Failure")
case Success(_) => println("Success All")
case Failure(e) => println(s"Future is Failure, because of $e")
}
{
for {
result <- Future { throw new Exception; Failure(new Exception) }
} yield for {
string <- result
} yield string
} onComplete errorHandler
当然FutureとTryだけでなく、そのほかの全てのモナドで同様のことが可能です。
ただ、モナドの種類だけ結果の値がネストしてしまうので(同一のモナドの結果はflatMapによって潰される)、
なるべくモナドの種類は統一するように心がけよう。