この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
今更ですがメインのプロジェクトでCats Effectのバージョンを2.x.xに上げたのでAPIの変更点の対応を行っていました。今回はIO#fromFutureが変更されたことについて詳細を調べてみました。
IO#fromFutureの変更点
以下のようにfromFutureがContextShiftを要求するようになって、これまでのインターフェースはdeprecatedになっています。
def fromFuture[A](iof: IO[Future[A]])(implicit cs: ContextShift[IO]): IO[A] =
iof.flatMap(IOFromFuture.apply).guarantee(cs.shift)
@deprecated("Use the variant that takes an implicit ContextShift.", "2.0.0")
private[IO] def fromFuture[A](iof: IO[Future[A]]): IO[A] =
iof.flatMap(IOFromFuture.apply)
何が変わったかというとIO#gurantee(ContextShift)を呼ぶようになったのですが、これにはどういった意味があるのでしょう?
IO#guranteeとは何か
guranteeを見ると IO.unit.bracket(_ => io)(_ => f)
と同等だと説明しています。guranteeはIOの実行が成功、失敗、キャンセルのいずれの場合でもfinalizerを実行するIOを返します。(Java でいうtry-catch-finallyのようなものですが、finalize処理を含めて合成可能なのです。最高だな。)
/**
* Executes the given `finalizer` when the source is finished,
* either in success or in error, or if canceled.
*
* This variant of [[guaranteeCase]] evaluates the given `finalizer`
* regardless of how the source gets terminated:
*
* - normal completion
* - completion in error
* - cancelation
*
* This equivalence always holds:
*
* {{{
* io.guarantee(f) <-> IO.unit.bracket(_ => io)(_ => f)
* }}}
*
* As best practice, it's not a good idea to release resources
* via `guaranteeCase` in polymorphic code. Prefer [[bracket]]
* for the acquisition and release of resources.
*
* @see [[guaranteeCase]] for the version that can discriminate
* between termination conditions
*
* @see [[bracket]] for the more general operation
*/
def guarantee(finalizer: IO[Unit]): IO[A] =
guaranteeCase(_ => finalizer)
再びfromFutureへ
fromFutureはFutureから生成したIOのfinalizerでコンテキストシフトをしています。
ということはgurannteeなしの場合はFutureのあとのIOはFutureが実行されたExecutionContextで実行されていたのか?ということで確かめてみます。
package example
import cats.effect.{ExitCode, IO, IOApp, Resource}
import java.util.concurrent.Executors
import scala.concurrent.{ExecutionContext, Future}
object FromFutureExample extends IOApp {
override def run(args: List[String]): IO[ExitCode] =
//区別しやすくするためにFutureはスレッドプール で実行
Resource
.make(IO(Executors.newFixedThreadPool(1)))(es => IO(es.shutdownNow()))
.map(es => ExecutionContext.fromExecutor(es))
.use { implicit ec =>
for {
_ <- IO(println(
s"threadName of before_future: ${Thread.currentThread().getName}"))
_ <- IO.fromFuture(
IO(Future(println(
s"threadName of future: ${Thread.currentThread().getName}"))))
_ <- IO(
println(
s"threadName of after_future: ${Thread.currentThread().getName}"))
} yield ExitCode.Success
}
}
実行結果は以下のようになります(cats effect 1.4.0で実行)
threadName of before_future: ioapp-compute-0
threadName of future: pool-1-thread-1
threadName of after_future: ioapp-compute-1
次に2.x系と同様にguranteeを追加して実行します。
for式だけ抜粋するとこうなります。
for {
_ <- IO(println(
s"threadName of before_future: ${Thread.currentThread().getName}"))
_ <- IO
.fromFuture(IO(Future(println(
s"threadName of future: ${Thread.currentThread().getName}"))))
.guarantee(contextShift.shift)
_ <- IO(
println(
s"threadName of after_future: ${Thread.currentThread().getName}"))
} yield ExitCode.Success
この実行結果は以下の通り(2.x相当)
threadName of before_future: scala-execution-context-global-11
threadName of future: pool-1-thread-1
threadName of after_future: scala-execution-context-global-11
やはりFutureの処理の前後でExecutionContextの切り替えが強制されるかどうかが違いのようです。
変更の経緯
この変更の経緯はcats-effect #546 で確認できます。破壊的な変更だと分かりながらもExecutionContextが切り替わったことに気付きづらいそれまでのインターフェースよりはいいということで変更されたようです。
まとめ
今更2.x系の変更点でしたが、今回移行してみてインタフェースが変わったこと以上に、これまでExecutionContextが変わったことに気づかず使っていた点にショックを受けました。