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: ジェネレータ関数
k
2から非同期タスク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