Scala の IO モナドライブラリ Cats Effect に含まれる 7つの型クラスのまとめ
はじめに
Cats Effect の解説では、IO モナドを中心に語られることが多いが、この記事では型クラスの観点で広く浅くまとめてみる。
Cats Effect のバージョンは 1.2.0 とした。
7つの型クラス
Cats Effect の7つの型クラスは、Cats 本体の MonadError と Defer から始まる、下図のような継承ツリーを構成する(実装必須メソッドだけ記入した)。
構成
(MonadError)
\ (Defer) * MonadError と Defer は cats 本体
Bracket /
----------- /
bracketCase /
\ /
Sync LiftIO
------- --------
suspend liftIO
delay /
\ /
Async
---------
async
asyncF
/ \
Concurrent Effect
---------- --------
start runAsync
racePair /
cancelable /
\ /
ConcurrentEffect
---------------
runCancelable
以降、個別に見ていく。各型クラスのサブタイトルはこの図から適当に訳した。
Bracket 〜 安全なリソース確保と解放
trait Bracket[F[_], E] extends MonadError[F, E]
F[_] が文脈で、E はエラー型。MonadError より上の継承ツリーは最後の補足に書いておいた。
主なメソッド
def bracketCase[A, B](acquire: F[A])(use: A => F[B])(release: (A, ExitCase[E]) => F[Unit]): F[B]
def bracket [A, B](acquire: F[A])(use: A => F[B])(release: A => F[Unit]): F[B]
def uncancelable[A](fa: F[A]): F[A]
def guarantee [A](fa: F[A])(finalizer: F[Unit]) : F[A]
def guaranteeCase[A](fa: F[A])(finalizer: ExitCase[E] => F[Unit]): F[A]
- bracket/bracketCase: acquire、use、release の3段階による安全なリソース操作を提供。
-
uncancelable: キャンセルできない操作の提供。意味的には
bracketに、acquire のみを与えたもの。 -
guarantee/guaranteeCase:
try {} finally {}に相当。uncancelableとは逆に、bracketに、use、release のみを与えて実現。
主な法則
F.guarantee (fa)(f) <-> F.bracket (F.unit)(_ => fa)( _ => f)
F.guaranteeCase(fa)(f) <-> F.bracketCase(F.unit)(_ => fa)((_, e) => f(e))
メモ
-
ExitCaseは以下の種類- Completed: 普通に終わった場合
- Error(e): エラーが発生した場合
-
Canceled:
Fiber#cancelなどでタスクがキャンセルされた場合
-
bracket/guaranteeは、内部でExitCodeを無視する実装になっている。 - スコープに
Bracket[F]があれば、Kleisliのインスタンスも自動的に提供される。
LiftIO 〜 IO[A] から F[A] への変換
trait LiftIO[F[_]]
主要メソッド
def liftIO[A](ioa: IO[A]): F[A]
-
liftIO:
IO[A]からF[A]への変換
メモ
- スコープに
LiftIO[F]があれば、EitherT、Kleisli、OptionT、StateT、WriterT、IotTのLiftIOインスタンスも自動的に提供される。 - ※ Cats Effect 公式ドキュメントの LiftIO 解説サンプルでは
IOの unsafe 系メソッドを使っているが、実戦では、どのレイヤーがIO[_]でどのレイヤーがF[_]かなど、境界を工夫するとよいかもしれない。
Sync 〜 Bracket × Defer + 同期サスペンド
trait Sync[F[_]] extends Bracket[F, Throwable] with Defer[F]
エラー型を Throwableに固定した Bracket と、Cats 本体の Defer を継承。
主なメソッド
def suspend[A](thunk: => F[A]): F[A]
def delay[A] (thunk: => A): F[A] = suspend(pure(thunk))
...
-
suspend: by-name で
F[A]を受け取って、by-name じゃないF[A]を返すが、評価自体は遅延される。Defer#deferのエイリアスでもある。名前からなんとなく「一時停止(pause)」的な操作を連想してしまうが、そうではない1。 -
delay:
suspendとApplicative#pureを用いて実装されている。
主な法則
F を IO とすると、下記の関係がなりたつ。
Sync[IO].delay(thunk) <-> IO(thunk) <-> IO.delay(thunk)
Sync[IO].suspned(thunk) <-> IO(thunk).flatten <-> IO.suspend(thunk)
Async 〜 LiftIO × Sync + 非同期サスペンド
trait Async[F[_]] extends Sync[F] with LiftIO[F] {
主なメソッド
ぱっと見わかりにくいかもしれないが、Either[Throwable, A] => Unit はコールバック関数で、 Async[F] のインスタンスから async/asyncF に渡されてくる。
def async[A] (k: (Either[Throwable, A] => Unit) => Unit): F[A]
def asyncF[A](k: (Either[Throwable, A] => Unit) => F[Unit]): F[A]
def never[A]: F[A]
...
-
async: ジェネレータ関数
k2から非同期タスクF[A]を作るビルダ。kの実装としては、渡されたコールバックに適当なEither[Throwable, A]値を渡したりしつつ、最後にUnitを返せばよい。コールバックの呼び方としては、kの中で別スレッド(Future.onCompleteなど)に書いても良いし、なんなら後述のneverのように呼ばない(終わらない)という実装もある。 -
asyncF:
asyncと似ているが、kの結果型がFの文脈に入ったものになる。 -
never: 何もせず、しかし終わらないメソッド。
F.async(_ => ())と同じ。
主な法則
Async.async、Async.asyncF と Sync#delay との間に、以下の関係がある。
F.async(k) <-> F.asyncF(cb => F.delay(k(cb)))
主なコンパニオンオブジェクトの要素
-
shift 関数: 与えられた
ExecutionContextでのシフト。IO.shiftのジェネリック版。 -
memoize 関数:
F[A] => F[F[A]]となるメモ化関数。
メモ
-
Async[F]がスコープにあれば、EitherT、OptionT、StateT、WriterT、Kleisli、IorTのインスタンスも自動的に得られる。
Concurrent 〜 Async + 並行なスタートとキャンセル
trait Concurrent[F[_]] extends Async[F]
Fiber を利用した並行スタート/キャンセルを Async に付与したもの。
主なメソッド
def start[A](fa: F[A]): F[Fiber[F, A]]
def cancelable[A](k: (Either[Throwable, A] => Unit) => CancelToken[F]): F[A]
def race [A, B](fa: F[A], fb: F[B]): F[Either[A, B]]
def racePair[A, B](fa: F[A], fb: F[B]): F[Either[(A, Fiber[F, B]), (Fiber[F, A], B)]]
-
start: タスク
faを開始してFiberインスタンスを得る。このFiberを介してcancelしたりjoinしたりできる。 -
cancelable:
CancelToken[F]を返すジェネレータ関数kから、キャンセラブルなタスクF[A]を作る。CancelToken[F]はF[Unit]のことで、Fの文脈で実行中のタスクをキャンセルするアクション。 -
race/racePair: 文脈
F上の2つのタスクを実行して結果をEitherで得る。raceは早いもの勝ちになるが、racePairでは早く終わったタスクの結果と共に、終わっていない方のFiberも返す。
主な法則
Bracket#uncancelable、Concurrent#cancelable、Async#async の間に以下の関係がある
F.uncancelable(F.cancelable { cb => f(cb); token }) <-> F.async(f)
主なコンパニオンオブジェクトの要素
-
timeout/timeoutTo 関数:
F.raceを応用して、タスクにタイムアウト付ける関数 -
cancelableF 関数:
F.cancelableと似ているが、ジェネレータ関数の結果型が、CancelToken[F]ではなくF[CancelToken[F]]となるもの
メモ
- スコープ内で
FがConcurrentならば、EitherT、OptionT、Kleisli、WriterT、IorTのインスタンスも自動的に得られる。 -
Fiberの簡略コードは以下のようなもの。
trait Fiber[F[_], A] {
def join: F[A]
def cancel: CancelToken[F]
}
Effect 〜 Async + 遅延非同期評価
trait Effect[F[_]] extends Async[F]
主なメソッド
def runAsync[A](fa: F[A])(cb: Either[Throwable, A] => IO[Unit]): SyncIO[Unit]
def toIO[A](fa: F[A]): IO[A]
-
runAsync:
IO#unsafeRunAsyncのジェネリックで安全な版(safe and generic version)。下のようにIO#unsafeRunAsyncと比較すると、「ジェネリックで安全な版」の意味がわかりやすいかもしれない。
def unsafeRunAsync (cb: Either[Throwable,A]=>Unit ): Unit // IO
def runAsync[A](fa: F[A])(cb: Either[Throwable,A]=>IO[Unit]): SyncIO[Unit]// Effect[F]
-
toIO:
liftIOと逆に、F[A]からIO[A]に変換する。
主な法則
F.toIO(liftIO(ioa)) <-> ioa
メモ
- スコープ内で
FがEffectならば、EitherT、WriterTのインスタンスも自動的に得られる。 -
SyncIOは非同期計算のないIO。
ConcurrentEffect 〜 Concurrent × Effect + キャンセルと並行評価
trait ConcurrentEffect[F[_]] extends Concurrent[F] with Effect[F]
主なメソッド
def runCancelable[A](fa: F[A])(cb: Either[Throwable, A] => IO[Unit]): SyncIO[CancelToken[F]]
-
runCancelable:
Concurrent#cancelableと、Effect#runAsyncを兼ね備えたようなもので、並べて比較すると下のようになる。
def cancelable[A] (k: (Either[Throwable,A]=> Unit) => CancelToken[F]): F[A]
def runAsync[A] (fa: F[A])(cb: Either[Throwable,A]=> IO[Unit]): SyncIO[Unit]
def runCancelable[A](fa: F[A])(cb: Either[Throwable,A]=> IO[Unit]): SyncIO[CancelToken[F]]
メモ
- スコープ内で
FがConcurrentEffectならば、EitherT、WriterTのConcurrentEffectインスタンスも自動的に得られる。
補足
Bracket は MonadError を継承しているが、MonadError 自体は下のようなツリーになっている。なので LiftIO 以外は、以下の cats core の型クラス群の性質を持っていることになる。
Invariant
---------
imap
\
Functor Semigroupal
------ ----------
map product
\ /
Apply
------
ap
/ \
FlatMap Applicative
------- ----------
flatMap pure
\ / ApplicativeError
Monad -----------------
\ raiseError
\ handleErrorWith
\ /
MonadError