1. Reader データ型

Reader データ型 

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

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

import cats._, cats.syntax.all._

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

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

(g map f)(8)
// res0: Int = 36

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

{
  val h = (f, g) mapN {_ + _}

  h(3)
}
// res1: Int = 19

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

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

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

  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 があるとする。

case class User(id: Long, parentId: Long, name: String, email: String)

trait UserRepo {
  def get(id: Long): User
  def find(name: String): User
}

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

trait Users {
  def getUser(id: Long): UserRepo => User = {
    case repo => repo.get(id)
  }
  def findUser(name: String): UserRepo => User = {
    case repo => repo.find(name)
  }
}

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

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

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
}

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

val testUsers = List(User(0, 0, "Vito", "vito@example.com"),
  User(1, 0, "Michael", "michael@example.com"),
  User(2, 0, "Fredo", "fredo@example.com"))
// testUsers: List[User] = List(
//   User(id = 0L, parentId = 0L, name = "Vito", email = "vito@example.com"),
//   User(id = 1L, parentId = 0L, name = "Michael", email = "michael@example.com"),
//   User(id = 2L, parentId = 0L, name = "Fredo", email = "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
// res3: String = "Map(name -> Fredo, email -> fredo@example.com, boss_name -> Vito)"

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

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

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
// res5: String = "Map(name -> Fredo, email -> fredo@example.com, boss_name -> Vito)"

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

今日はここまで。