9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Cats Effect の7つの型クラス

Last updated at Posted at 2019-02-01

Scala の IO モナドライブラリ Cats Effect に含まれる 7つの型クラスのまとめ

はじめに

Cats Effect の解説では、IO モナドを中心に語られることが多いが、この記事では型クラスの観点で広く浅くまとめてみる。

Cats Effect のバージョンは 1.2.0 とした。

7つの型クラス

Cats Effect の7つの型クラスは、Cats 本体の MonadErrorDefer から始まる、下図のような継承ツリーを構成する(実装必須メソッドだけ記入した)。

構成

(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: acquireuserelease の3段階による安全なリソース操作を提供。
  • uncancelable: キャンセルできない操作の提供。意味的には bracket に、acquire のみを与えたもの。
  • guarantee/guaranteeCase: try {} finally {} に相当。uncancelable とは逆に、bracket に、userelease のみを与えて実現。

主な法則

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 のインスタンスも自動的に提供される。

Gist

LiftIO 〜 IO[A] から F[A] への変換

trait LiftIO[F[_]]

主要メソッド

def liftIO[A](ioa: IO[A]): F[A]
  • liftIO: IO[A] から F[A] への変換

メモ

  • スコープに LiftIO[F] があれば、EitherTKleisliOptionTStateTWriterTIotTLiftIO インスタンスも自動的に提供される。
  • Cats Effect 公式ドキュメントの LiftIO 解説サンプルでは IO の unsafe 系メソッドを使っているが、実戦では、どのレイヤーがIO[_]でどのレイヤーがF[_]かなど、境界を工夫するとよいかもしれない。

Gist

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-nameF[A] を受け取って、by-name じゃないF[A] を返すが、評価自体は遅延される。Defer#defer のエイリアスでもある。名前からなんとなく「一時停止(pause)」的な操作を連想してしまうが、そうではない1
  • delay: suspendApplicative#pure を用いて実装されている。

主な法則

FIO とすると、下記の関係がなりたつ。

Sync[IO].delay(thunk)   <-> IO(thunk)         <-> IO.delay(thunk)
Sync[IO].suspned(thunk) <-> IO(thunk).flatten <-> IO.suspend(thunk)

Gist

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.asyncAsync.asyncFSync#delay との間に、以下の関係がある。

F.async(k) <-> F.asyncF(cb => F.delay(k(cb)))

主なコンパニオンオブジェクトの要素

  • shift 関数: 与えられた ExecutionContext でのシフト。IO.shift のジェネリック版。
  • memoize 関数: F[A] => F[F[A]]となるメモ化関数。

メモ

  • Async[F] がスコープにあれば、EitherTOptionTStateTWriterTKleisliIorTのインスタンスも自動的に得られる。

Gist

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#uncancelableConcurrent#cancelableAsync#async の間に以下の関係がある

F.uncancelable(F.cancelable { cb => f(cb); token }) <-> F.async(f)

主なコンパニオンオブジェクトの要素

  • timeout/timeoutTo 関数: F.race を応用して、タスクにタイムアウト付ける関数
  • cancelableF 関数: F.cancelable と似ているが、ジェネレータ関数の結果型が、CancelToken[F] ではなく F[CancelToken[F]]となるもの

メモ

  • スコープ内で FConcurrent ならば、EitherTOptionTKleisliWriterTIorT のインスタンスも自動的に得られる。
  • Fiber の簡略コードは以下のようなもの。
trait Fiber[F[_], A] {
  def join: F[A]
  def cancel: CancelToken[F]
}

Gist

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

メモ

  • スコープ内で FEffect ならば、EitherTWriterT のインスタンスも自動的に得られる。
  • SyncIO は非同期計算のない IO

Gist

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]]

メモ

  • スコープ内で FConcurrentEffect ならば、EitherTWriterTConcurrentEffect インスタンスも自動的に得られる。

Gist

補足

BracketMonadError を継承しているが、MonadError 自体は下のようなツリーになっている。なので LiftIO 以外は、以下の cats core の型クラス群の性質を持っていることになる。

Invariant
---------
 imap
   \
     Functor      Semigroupal
     ------       ----------
       map         product
         \       /
             Apply
            ------
              ap
           /  \
    FlatMap       Applicative
    -------       ----------
    flatMap         pure
        \       /         ApplicativeError
            Monad           -----------------
             \                raiseError
               \            handleErrorWith
                 \        /
                   MonadError
  1. suspend のネーミングについては議論があったらしい。Scalaz/Cats の Free monad のトランポリン実装や、Scalaz の Task と合わせたネーミングなのだという。

  2. Concurrent#cancelableF のコメントで generator function と書かれていたので「ジェネレータ関数」とした。"inject"される関数という表現もよくある。

9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?