Reader データ型 

すごいHaskellたのしく学ぼう 曰く:

第11章では、関数を作る型、(->) r も、Functor のインスタンスであることを見ました。

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

scala> val f = (_: Int) * 2
f: Int => Int = <function1>

scala> val g = (_: Int) + 10
g: Int => Int = <function1>

scala> (g map f)(8)
res0: Int = 36

それから、関数はアプリカティブファンクターであることも見ましたね。これにより、関数が将来返すであろう値を、すでに持っているかのように演算できるようになりました。

scala> val h = (f |@| g) map {_ + _}
h: Int => Int = <function1>

scala> h(3)
res1: Int = 19

関数の型 (->) r はファンクターであり、アプリカティブファンクターであるばかりでなく、モナドでもあります。これまでに登場したモナド値と同様、関数もまた文脈を持った値だとみなすことができるのです。関数にとっての文脈とは、値がまだ手元になく、値が欲しければその関数を別の何かに適用しないといけない、というものです。

この例題も実装してみよう:

scala> val addStuff: Int => Int = for {
         a <- (_: Int) * 2
         b <- (_: Int) + 10
       } yield a + b
addStuff: Int => Int = <function1>

scala> addStuff(3)
res2: Int = 19

(*2)(+10) はどちらも 3 に適用されます。実は、return (a+b) も同じく 3 に適用されるんですが、引数を無視して常に a+b を返しています。そいういうわけで、関数モナドは Reader モナドとも呼ばれたりします。すべての関数が共通の情報を「読む」からです。

Reader モナドは値が既にあるかのようなフリをさせてくれる。恐らくこれは1つのパラメータを受け取る関数でしか使えない。

DI: Dependency injection 

2012年3月9日にあった nescala 2012 で Rúnar (@runarorama) さんが Dead-Simple Dependency Injection というトークを行った。そこで提示されたアイディアの一つは Reader モナドを dependency injection に使うというものだった。同年の 12月に YOW 2012 でそのトークを長くした Lambda: The Ultimate Dependency Injection Framework も行われた。 翌 2013年に Jason Arhart さんが書いた Scrap Your Cake Pattern Boilerplate: Dependency Injection Using the Reader Monad に基づいた例をここでは使うことにする。

まず、ユーザを表す case class と、ユーザを取得するためのデータストアを抽象化した trait があるとする。

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

次に、UserRepo trait の全ての演算に対してプリミティブ・リーダーを定義する:

scala> :paste
// Entering paste mode (ctrl-D to finish)
trait Users {
  def getUser(id: Long): UserRepo => User = {
    case repo => repo.get(id)
  }
  def findUser(name: String): UserRepo => User = {
    case repo => repo.find(name)
  }
}

// Exiting paste mode, now interpreting.

defined trait Users

(ボイラープレートをぶち壊せとか言いつつ) これはボイラープレートっぽい。一応、次。

プリミティブ・リーダーを合成することで、アプリケーションを含む他のリーダーを作ることができる。

scala> :paste
// Entering paste mode (ctrl-D to finish)
object UserInfo extends Users {
  def userInfo(name: String): UserRepo => Map[String, String] =
    for {
      user <- findUser(name)
      boss <- getUser(user.parentId)
    } yield Map(
      "name" -> s"${user.name}",
      "email" -> s"${user.email}",
      "boss_name" -> s"${boss.name}"
    )
}
trait Program {
  def app: UserRepo => String =
    for {
      fredo <- UserInfo.userInfo("Fredo")
    } yield fredo.toString
}

// Exiting paste mode, now interpreting.

defined object UserInfo
defined trait Program

この app を実行するためには、UserRepo の実装を提供する何かが必要だ:

scala> :paste
// Entering paste mode (ctrl-D to finish)
val testUsers = List(User(0, 0, "Vito", "vito@example.com"),
  User(1, 0, "Michael", "michael@example.com"),
  User(2, 0, "Fredo", "fredo@example.com"))
object Main extends Program {
  def run: String = app(mkUserRepo)
  def mkUserRepo: UserRepo = new UserRepo {
    def get(id: Long): User = (testUsers find { _.id === id }).get
    def find(name: String): User = (testUsers find { _.name === name }).get
  }
}
Main.run

// Exiting paste mode, now interpreting.

testUsers: List[User] = List(User(0,0,Vito,vito@example.com), User(1,0,Michael,michael@example.com), User(2,0,Fredo,fredo@example.com))
defined object Main
res3: String = Map(name -> Fredo, email -> fredo@example.com, boss_name -> Vito)

ボスの名前が表示された。

for 内包表記の代わりに actM を使ってみる:

scala> :paste
// Entering paste mode (ctrl-D to finish)
object UserInfo extends Users {
  import example.MonadSyntax._
  def userInfo(name: String): UserRepo => Map[String, String] =
    actM[UserRepo => ?, Map[String, String]] {
      val user = findUser(name).next
      val boss = getUser(user.parentId).next
      Map(
        "name" -> s"${user.name}",
        "email" -> s"${user.email}",
        "boss_name" -> s"${boss.name}"
      )
    }
}
trait Program {
  import example.MonadSyntax._
  def app: UserRepo => String =
    actM[UserRepo => ?, String] {
      val fredo = UserInfo.userInfo("Fredo").next
      fredo.toString
    }
}
object Main extends Program {
  def run: String = app(mkUserRepo)
  def mkUserRepo: UserRepo = new UserRepo {
    def get(id: Long): User = (testUsers find { _.id === id }).get
    def find(name: String): User = (testUsers find { _.name === name }).get
  }
}
Main.run

// Exiting paste mode, now interpreting.

defined object UserInfo
defined trait Program
defined object Main
res4: String = Map(name -> Fredo, email -> fredo@example.com, boss_name -> Vito)

actM ブロックの中は for バージョンよりも自然な形に見えるけども、 型注釈が必要なせいで、多分こっちの方が使いづらいと思う。

今日はここまで。

Contents