この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
cats Effect IOの#delay
と#pure
使い方の誤りについて、たまたま同じ日に同じ指摘を2回したので記事にしてみることにしました。
何が違うのか
IO.delay(とそのエイリアスのIO.apply)は以下のように定義されています。
/**
* Suspends a synchronous side effect in `IO`.
*
* Alias for `IO.delay(body)`.
*/
def apply[A](body: => A): IO[A] =
delay(body)
/**
* Suspends a synchronous side effect in `IO`.
*
* Any exceptions thrown by the effect will be caught and sequenced
* into the `IO`.
*/
def delay[A](body: => A): IO[A] =
Delay(body _)
対してIO.pureは下記の通りです。
/**
* Suspends a pure value in `IO`.
*
* This should ''only'' be used if the value in question has
* "already" been computed! In other words, something like
* `IO.pure(readLine)` is most definitely not the right thing to do!
* However, `IO.pure(42)` is correct and will be more efficient
* (when evaluated) than `IO(42)`, due to avoiding the allocation of
* extra thunks.
*/
def pure[A](a: A): IO[A] = Pure(a)
どちらもAをとってIO[A]を返しますが評価戦略が異なります。delayは非正格(名前渡しパラメータ)ですがpureは正格です。以下のように実行してみると違いが明確です。
import cats.effect.IO
import cats.implicits._
@SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements"))
object IOExample extends App {
//IO.apply
println("[IO.apply]")
val ioWithApply = List(
IO(println("hello")),
IO(println("cats")),
IO(println("world"))
).sequence
ioWithApply.unsafeRunSync()
ioWithApply.unsafeRunSync()
//IO.pure
println("[IO.pure]")
val ioWithPure = List(
IO(println("hello")),
IO.pure(println("cats")),
IO(println("world"))
).sequence
ioWithPure.unsafeRunSync()
ioWithPure.unsafeRunSync()
}
実行結果は下記のようになります。
[IO.apply]
hello
cats
world
hello
cats
world
[IO.pure]
cats
hello
world
hello
world
pureを使った2つ目のパターンでは以下の違いがあります。
- 1回目のunsafeRunSyncではhelloとcatsの出力順序が逆になっている
- 2回目のunsafeRunSyncではcatsは出力されない
これはIOが生成されるときに値が評価されているか、IOの実行時に評価されているか、の違いによるものです。
エラー処理
評価タイミングの違いはエラーハンドリングでは致命的な違いをもたらします。
以下のコードではIOの実行時発生したエラーをhandleErrorWithでハンドリングしようという意図で書かれています。
//エラーハンドリング
def catsNameOrError: String = throw new RuntimeException("Cats Name Error !!")
//IO.apply
println("[IO.apply]")
IO(catsNameOrError)
.handleErrorWith {
case e: Throwable =>
println("Error handled: " + e.getMessage)
IO.pure(e.getMessage)
}.unsafeRunSync()
//IO.pure
println("[IO.pure]")
IO.pure(catsNameOrError).handleErrorWith {
case e: Throwable =>
println("Error handled: " + e.getMessage)
IO.pure(e.getMessage)
}.unsafeRunSync()
しかし下記の実行例の通り実際にはIO.pureを使った方はIOが実行される前に例外が送出されるため補足されない例外はmainへ伝搬します。
[IO.apply]
Error handled: Cats Name Error !!
[IO.pure]
Exception in thread "main" java.lang.RuntimeException: Cats Name Error !!
at IOExample$.catsNameOrError(IOExample.scala:30)
at IOExample$.delayedEndpoint$IOExample$1(IOExample.scala:44)
at IOExample$delayedInit$body.apply(IOExample.scala:5)
at scala.Function0.apply$mcV$sp(Function0.scala:39)
at scala.Function0.apply$mcV$sp$(Function0.scala:39)
at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:17)
at scala.App.$anonfun$main$1$adapted(App.scala:80)
at scala.collection.immutable.List.foreach(List.scala:392)
at scala.App.main(App.scala:80)
at scala.App.main$(App.scala:78)
at IOExample$.main(IOExample.scala:5)
at IOExample.main(IOExample.scala)
まとめ
両者の違いを理解して副作用のある処理を安全に書きましょう。