これまでも出かかってきてたけど、未だ取り扱っていなかった話題としてモナド変換子という概念がある。 幸いなことに、Haskell の良書でオンライン版も公開されている本がもう 1冊あるので、これを参考にしてみる。

モナド変換子 

Real World Haskell 曰く:

もし標準の State モナドに何らかの方法でエラー処理を追加することができれば理想的だ。 一から手書きで独自のモナドを作るのは当然避けたい。mtl ライブラリに入っている標準のモナド同士を組み合わせることはできない。 だけども、このライブラリはモナド変換子というものを提供して、同じことを実現できる。

モナド変換子は通常のモナドに似ているが、孤立して使える実体ではなく、 基盤となる別のモナドの振る舞いを変更するものだ。

Dependency injection 再び 

6日目 にみた Reader データ型 (Function1) を DI に使うという考えをもう一度見てみよう。

scala> :paste
// Entering paste mode (ctrl-D to finish)
case class User(id: Long, parentId: Long, name: String, email: String)
trait UserRepo {
  def get(id: Long): User
  def find(name: String): User
}

// Exiting paste mode, now interpreting.

defined class User
defined trait UserRepo

Jason Arhart さんの Scrap Your Cake Pattern Boilerplate: Dependency Injection Using the Reader MonadConfig オブジェクトを作ることで Reader データ型を複数のサービスのサポートに一般化している:

scala> import java.net.URI
import java.net.URI

scala> :paste
// Entering paste mode (ctrl-D to finish)
trait HttpService {
  def get(uri: URI): String
}
trait Config {
  def userRepo: UserRepo
  def httpService: HttpService
}

// Exiting paste mode, now interpreting.

defined trait HttpService
defined trait Config

これを使うには Config => A 型のミニ・プログラムを作って、それらを合成する。

ここで、Option を使って失敗という概念もエンコードしたいとする。

ReaderT としての Kleisli 

昨日見た Kleisli データ型を ReaderT、つまり Reader データ型のモナド変換子版として使って、それを Option の上に積み上げることができる:

scala> import cats._, cats.data._, cats.implicits._
import cats._
import cats.data._
import cats.implicits._

scala> :paste
// Entering paste mode (ctrl-D to finish)
type ReaderTOption[A, B] = Kleisli[Option, A, B]
object ReaderTOption {
  def ro[A, B](f: A => Option[B]): ReaderTOption[A, B] = Kleisli(f)
}

// Exiting paste mode, now interpreting.

defined type alias ReaderTOption
defined object ReaderTOption

Config を変更して httpService をオプショナルにする:

scala> :paste
// Entering paste mode (ctrl-D to finish)
trait UserRepo {
  def get(id: Long): Option[User]
  def find(name: String): Option[User]
}
trait Config {
  def userRepo: UserRepo
  def httpService: Option[HttpService]
}

// Exiting paste mode, now interpreting.

defined trait UserRepo
defined trait Config

次に、「プリミティブ」なリーダーが ReaderTOption[Config, A] を返すように書き換える:

scala> :paste
// Entering paste mode (ctrl-D to finish)
trait Users {
  def getUser(id: Long): ReaderTOption[Config, User] =
    ReaderTOption.ro {
      case config => config.userRepo.get(id)
    }
  def findUser(name: String): ReaderTOption[Config, User] =
    ReaderTOption.ro {
      case config => config.userRepo.find(name)
    }
}
trait Https {
  def getHttp(uri: URI): ReaderTOption[Config, String] =
    ReaderTOption.ro {
      case config => config.httpService map {_.get(uri)}
    }
}

// Exiting paste mode, now interpreting.

defined trait Users
defined trait Https

これらのミニ・プログラムを合成して複合プログラムを書くことができる:

scala> :paste
// Entering paste mode (ctrl-D to finish)
trait Program extends Users with Https {
  def userSearch(id: Long): ReaderTOption[Config, String] =
    for {
      u <- getUser(id)
      r <- getHttp(new URI(s"http://www.google.com/?q=${u.name}"))
    } yield r
}
object Main extends Program {
  def run(config: Config): Option[String] =
    userSearch(2).run(config)
}
val dummyConfig: Config = new Config {
  val testUsers = List(User(0, 0, "Vito", "vito@example.com"),
    User(1, 0, "Michael", "michael@example.com"),
    User(2, 0, "Fredo", "fredo@example.com"))
  def userRepo: UserRepo = new UserRepo {
    def get(id: Long): Option[User] =
      testUsers find { _.id === id }
    def find(name: String): Option[User] =
      testUsers find { _.name === name }
  }
  def httpService: Option[HttpService] = None
}

// Exiting paste mode, now interpreting.

defined trait Program
defined object Main
dummyConfig: Config = $anon$1@7acb071

上の ReaderTOption データ型は、Reader の設定の読み込む能力と、 Option の失敗を表現できる能力を組み合わせたものとなっている。

複数のモナド変換子を積み上げる 

RWH:

普通のモナドにモナド変換子を積み上げると、別のモナドになる。 これは組み合わされたモナドの上にさらにモナド変換子を積み上げて、新しいモナドを作ることができる可能性を示唆する。 実際に、これはよく行われていることだ。

状態遷移を表す StateTReaderTOption の上に積んでみる。

scala> :paste
// Entering paste mode (ctrl-D to finish)
type StateTReaderTOption[C, S, A] = StateT[({type l[X] = ReaderTOption[C, X]})#l, S, A]
object StateTReaderTOption {
  def state[C, S, A](f: S => (S, A)): StateTReaderTOption[C, S, A] =
    StateT[({type l[X] = ReaderTOption[C, X]})#l, S, A] {
      s: S => Monad[({type l[X] = ReaderTOption[C, X]})#l].pure(f(s))
    }
  def get[C, S]: StateTReaderTOption[C, S, S] =
    state { s => (s, s) }
  def put[C, S](s: S): StateTReaderTOption[C, S, Unit] =
    state { _ => (s, ()) }
  def ro[C, S, A](f: C => Option[A]): StateTReaderTOption[C, S, A] =
    StateT[({type l[X] = ReaderTOption[C, X]})#l, S, A] {
      s: S =>
        ReaderTOption.ro[C, (S, A)]{
          c: C => f(c) map {(s, _)}
        }
    }
}

// Exiting paste mode, now interpreting.

defined type alias StateTReaderTOption
defined object StateTReaderTOption

これは分かりづらいので、分解してみよう。 結局の所 State データ型は S => (S, A) をラッピングするものだから、state のパラメータ名はそれに合わせた。 次に、ReaderTOption のカインドを * -> * (ただ 1つのパラメータを受け取る型コンストラクタ) に変える。

同様に、このデータ型を ReaderTOption として使う方法が必要なので、それは ro に渡される C => Option[A] として表した。

これで Stack を実装することができる。今回は String を使ってみよう。

scala> type Stack = List[String]
defined type alias Stack

scala> val pop = StateTReaderTOption.state[Config, Stack, String] {
         case x :: xs => (xs, x)
         case _       => ???
       }
pop: StateTReaderTOption[Config,Stack,String] = cats.data.StateT@5ee60d0a

poppushgetpush プリミティブを使って書くこともできる:

scala> import StateTReaderTOption.{get, put}
import StateTReaderTOption.{get, put}

scala> val pop: StateTReaderTOption[Config, Stack, String] =
         for {
           s <- get[Config, Stack]
           (x :: xs) = s
           _ <- put(xs)
         } yield x
pop: StateTReaderTOption[Config,Stack,String] = cats.data.StateT@6e0fc271

scala> def push(x: String): StateTReaderTOption[Config, Stack, Unit] =
         for {
           xs <- get[Config, Stack]
           r <- put(x :: xs)
         } yield r
push: (x: String)StateTReaderTOption[Config,Stack,Unit]

ついでに stackManip も移植する:

scala> def stackManip: StateTReaderTOption[Config, Stack, String] =
         for {
           _ <- push("Fredo")
           a <- pop
           b <- pop
         } yield(b)
stackManip: StateTReaderTOption[Config,Stack,String]

実行してみよう。

scala> stackManip.run(List("Hyman Roth")).run(dummyConfig)
res0: Option[(Stack, String)] = Some((List(),Hyman Roth))

とりあえず State 版と同じ機能までたどりつけた。 次に、UsersStateTReaderTOption.ro を使うように書き換える:

scala> :paste
// Entering paste mode (ctrl-D to finish)
trait Users {
  def getUser[S](id: Long): StateTReaderTOption[Config, S, User] =
    StateTReaderTOption.ro[Config, S, User] {
      case config => config.userRepo.get(id)
    }
  def findUser[S](name: String): StateTReaderTOption[Config, S, User] =
    StateTReaderTOption.ro[Config, S, User] {
      case config => config.userRepo.find(name)
    }
}

// Exiting paste mode, now interpreting.

defined trait Users

これを使ってリードオンリーの設定を使ったスタックの操作ができるようになった:

scala> :paste
// Entering paste mode (ctrl-D to finish)
trait Program extends Users {
  def stackManip: StateTReaderTOption[Config, Stack, Unit] =
    for {
      u <- getUser(2)
      a <- push(u.name)
    } yield(a)
}
object Main extends Program {
  def run(s: Stack, config: Config): Option[(Stack, Unit)] =
    stackManip.run(s).run(config)
}

// Exiting paste mode, now interpreting.

defined trait Program
defined object Main

このプログラムはこのように実行できる:

scala> Main.run(List("Hyman Roth"), dummyConfig)
res1: Option[(Stack, Unit)] = Some((List(Fredo, Hyman Roth),()))

これで StateTReaderT、それと Option を同時に動かすことができた。 僕が使い方を良く分かってないせいかもしれないが、StateTReaderTOption に関して statero のようなモナド・コンストラクタを書き出すのは頭をひねる難問だった。

プリミティブなモナド値さえ構築できてしまえば、実際の使う側のコード (stackManip などは) 比較的クリーンだと言える。 Cake パターンは確かに回避してるけども、コード中に積み上げられたモナド型である StateTReaderTOption が散らばっている設計になっている。

最終目的として getUser(id: Long)push などを同時に使いたいというだけの話なら、 8日目に見た自由モナドを使うことで、これらをコマンドとして持つ DSL を構築することも代替案として考えられる。

Contents