
cats effect IO.pureとdelayの違い
この記事は公開されてから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)
まとめ
両者の違いを理解して副作用のある処理を安全に書きましょう。







