【问题标题】:How to implement a reader monad to access a database如何实现 reader monad 来访问数据库
【发布时间】:2013-10-28 14:32:12
【问题描述】:

我第一次尝试实现一个 reader monad。

我想用一元样式查询数据库。

用例 1:用户与同事具有一对一的关系。伪代码为getUserById(getUserById(id).getColleague())

用例 2:按 id 检索用户列表。伪代码为List(getUserById(id1), getUserById(id2))

这似乎是 monad 的好用例。我的目标是看看我是否可以利用 monad 来改进我的代码

PS : 请提供至少一个不带 scalaz 的答案。

代码如下:

package monad
import com.mongodb.casbah.Imports._

object Monad {
  type UserId = Int
  case class User(id: UserId, name: String, colleagueId: UserId)

  trait Reader[I, A] { self =>
    def run(id: I) : A
    def map[B](f: A => B) : Reader[I, B] = 
      new Reader[I, B] { def run(id: I) = f(self.run(id)) }
    def flatMap[B](f: A => Reader[I, B]) : Reader[I, B] =
      new Reader[I, B] { def run(id: I) = f(self.run(id)).run(id) }
  }

  def coll = MongoClient()("test")("user")

  def DBObject2User(o: DBObject) : User = User(o.as[Double]("id").toInt, o.as[String]("name"), o.as[Double]("colleague").toInt)

  // Strange design, id is not used…
  def User2Colleague(u: User) : Reader[UserId, DBObject] = 
    unit(coll.findOne(MongoDBObject("id" -> u.colleagueId)).get)

  def GetUserById : Reader[UserId, DBObject] = 
    Reader { id: UserId => coll.findOne(MongoDBObject("id" -> id)).get }

  def GetUserById2 : Reader[UserId, User] = GetUserById.map(DBObject2User)

  def unit[A](a: => A) = Reader { id: UserId => a }

  object Reader {
    def apply[I, A](f: I => A) = new Reader[I, A] { def run(i: I) = f(i) }
  }

  def main(args: Array[String]) {
    // I can do
    println(GetUserById2.run(1))

    // Same with for comprehension
    val userReader = for (io <- GetUserById2) yield io
    println(userReader.run(1))

    //Combination to explore one-to-one relation
    val user = GetUserById2.run(1)
    val colleague = GetUserById2.run(user.colleagueId)

    // Same with flatMap
    println(GetUserById2.flatMap(User2Colleague).run(1))

    // Same with for-comprehension but doesn't work
    val io = for {io  <- GetUserById2
                  io2 <- User2Colleague(io).map(DBObject2User)} yield io2
    println(io.run(1))

    //TODO: List[Reader] to Reader[List]
  }
}

这是好方法吗?我有一些疑问,请参阅我的评论Strange design

如何改进我的代码?

标签: scala monads


【解决方案1】:

我已尝试修改您的建议,使该集合成为读者的输入,并在我进行的过程中稍微修改了命名。

package monad
package object reader{
  type UserId = Int
}
package reader {

case class User(id: UserId, name: String, colleagueId: UserId)

import com.mongodb.casbah.Imports._
import com.mongodb.casbah


trait Reader[I, A] {
  self =>
  val run = apply _

  def apply(id:I):A

  def map[B](f: A => B): Reader[I, B] =
    new Reader[I, B] {
      def apply(id: I) = f(self.run(id))
    }

  def flatMap[B](f: A => Reader[I, B]): Reader[I, B] =
    new Reader[I, B] {
      def apply(id: I) = f(self(id)).run(id)
    }
}

object Reader {
  def unit[A](a: => A) = apply {
    id: UserId => a
  }

  def apply[I, A](f: I => A) = new Reader[I, A] {
    def apply(i: I) = f(i)
  }
}

object Users {

  def asUser(o: DBObject): User = User(o.as[Double]("id").toInt, o.as[String]("name"), o.as[Double]("colleague").toInt)

  def colleague(u: User): Reader[MongoCollection, User] =
    Reader{ 
      coll => asUser(coll.findOne(MongoDBObject("id" -> u.colleagueId)).get)
    }

  def getUserById(id:UserId): Reader[MongoCollection, User] =
    Reader {
      coll => asUser(coll.findOne(MongoDBObject("id" -> id)).get)
    }
}

object Client extends App {

  import Users._
  def coll: casbah.MongoCollection = MongoClient()("test")("user")

  // I can do
  println(getUserById(1)(coll))

  // Same with for comprehension
  val userReader = for (user <- getUserById(1)) yield user
  println(userReader(coll))

  //Combination to explore one-to-one relation
  val user = getUserById(1)(coll)
  val otherUser = getUserById(user.colleagueId)(coll)

  // Same with flatMap
  println(getUserById(1).flatMap(colleague)(coll))

  // Same with for-comprehension but doesn't work
  val coworkerReader = for {user <- getUserById(1)
                            coworker <- colleague(user)} yield coworker
  println(coworkerReader(coll))         
}

}

使用这种方法,我认为代码更容易测试,因为您可以传递依赖项(MongoCollection),同时仅操作签名中的值和函数。阅读更多http://blog.originate.com/blog/2013/10/21/reader-monad-for-dependency-injection/(我不是作者,但解释清楚)

【讨论】:

  • getUserById 有效,因为闭包捕获了 Scala 中的外部上下文(如果 id 不是最终字段,则在 Java 8 中无效)。确认?
  • 我必须修改apply 方法以使其编译:def apply(id: I) = run(id)。有什么解释吗?
  • 您对 getUserById 的看法是正确的,但这与您的 User2Colleague 完全相同,后者是一个引用 u 参数的闭包。
  • 对应用程序感到抱歉,它的工作方式相反(根据应用程序定义运行),我在复制粘贴到 stackoverflow 之前盲目地反转它......
【解决方案2】:

阅读器 monad 和 monad 转换器一步一步 + 使用 mongoDB 的示例,很好地描述了here

【讨论】:

    【解决方案3】:

    您的用法还不错,我觉得令人困惑的是,通常认为读取器的配置输入(如数据库连接数据)在这种情况下是被检查用户的 id。

    首先我将 IO[I,A] 名称更改为 Reader[I,A] 只是为了使用众所周知的名称进行此类操作[*]

    至于User2Colleague(u: User) : IO[UserId, DBObject] 方法,丢弃阅读器输入以提供包含在monad 中的常量值并不罕见:这正是您的unit 方法所做的!

    其实我会把它改成

    def User2Colleague(u: User) : Reader[UserId, DBObject] = 
      unit(coll.findOne(MongoDBObject("id" -> u.colleagueId)).get)
    

    甚至更符合您的客户端代码使用情况

    def User2Colleague(u: User): DBObject = coll.findOne(MongoDBObject("id" -> u.colleagueId)).get
    ...
    def main(args: Array[String]) {
      val mng = new Mongo()
    
      val io = for {
        io <- mng
        io2 <- unit(DBObject2User(io))
        io3 <- unit(User2Colleague(io2))
      } yield (DBObject2User(io3))
    
      println(io.run(1))  
    

    我的建议是尽可能编写您的代码(即没有单子效应),除非需要。这意味着在常规函数中进行映射,然后仅在需要时使用 unit 进行包装(以便在 for 理解中进行组合类型检查)

    [*] 通常的 IO monad 没有输入类型,它只有一个输出类型,通常用于执行用户 I/O(控制台、打印、电子邮件、发送火箭等),即任何有边的东西程序本身的外部效果。

    【讨论】:

    • 我刚刚编辑了我的代码以添加 IO 伴随对象以简化代码。
    • 但是现在你有两种方法可以在 monad 中放置一个值,并且它们不会相互延迟,这意味着代码重复。我会尝试将unit[A] 概括为unit[I,A] 并让IO.apply 调用它,或者反过来。
    • 我已经编辑让 unit 调用 IO.apply 以删除代码重复
    猜你喜欢
    • 2018-10-17
    • 1970-01-01
    • 2017-06-22
    • 2023-04-05
    • 2020-05-24
    • 2014-11-14
    • 1970-01-01
    • 2018-03-26
    • 1970-01-01
    相关资源
    最近更新 更多