Reader monad有什么好处?

时间:2014-03-08 15:01:42

标签: scala functional-programming monads

我读过关于Reader monad的blog post

该帖子真正非常好并详细解释了该主题,但我没有得到为什么在这种情况下我应该使用Reader monad。

帖子说:假设有一个函数query: String => Connection => ResultSet

def query(sql:String) = conn:Connection => conn.createStatement.executeQuery(sql)

我们可以按如下方式运行一些查询:

def doSomeQueries(conn: Connection) = {
  val rs1 = query("SELECT COUNT(*) FROM Foo")(conn)
  val rs2 = query("SELECT COUNT(*) FROM Bar")(conn)
  rs1.getInt(1) + rs2.getInt(1)
} 

到目前为止一直很好,但帖子建议使用Reader monad代替:

class Reader[E, A](run: E => A) {

  def map[B](f: A => B):Reader[E, B] =
    new Reader(е=> f(run(е)))

  def flatMap[B](f:A => Reader[E, B]): Reader[E, B] =
    new Reader(е => f(run(е)).run(е))  
}

val query(sql:String): Reader[Connection, ResultSet] =
  new Reader(conn => conn.createStatement.executeQuery(sql))

def doSomeQueries(conn: Connection) = for {
  rs1 <- query("SELECT COUNT(*) FROM Foo")
  rs2 <- query("SELECT COUNT(*) FROM Bar")
} yield rs1.getInt(1) + rs2.getInt(1)

好的,我知道我不需要明确地通过调用connection。那么什么呢? 为什么 Reader monad的解决方案比前一个好?

更新:修正了def查询中的拼写错误:= should be =&gt; 此注释仅存在,因为SO坚持编辑必须至少6个字符长。所以我们走了。

2 个答案:

答案 0 :(得分:9)

最重要的原因是读者monad允许您构建复杂的计算组合。请考虑非读者示例中的以下行:

val rs1 = query("SELECT COUNT(*) FROM Foo")(conn)

我们手动传递conn的事实意味着这条线本身并没有真正意义 - 它只能在doSomeQueries方法的上下文中被理解和推理这给了我们conn

通常这很好 - 显然没有关于定义和使用局部变量的错误(至少在val意义上)。但是,有时候,通过独立的,可组合的部分构建计算会更方便(或者出于其他原因),而读者monad可以帮助解决这个问题。

在第二个示例中考虑query("SELECT COUNT(*) FROM Foo")。假设我们知道query是什么,这是一个完全自包含的表达式 - 没有像conn这样的变量需要被某些封闭范围绑定。这意味着您可以更自信地重复使用和重构,并且当您推理它时,您没有那么多东西可以保留在您的头脑中。

同样,这不是必要 - 这在很大程度上取决于风格。如果你决定尝试一下(我建议你这样做),你可能很快就会发展出偏好和直觉,让你的代码更容易理解,哪些代码不可理解。

另一个优点是,您可以使用ReaderT(或通过将Reader添加到其他堆栈中)来组合不同类型的“效果”。不过,这套问题可能值得回答。

最后一点:您可能希望doSomeQueries看起来像这样:

def doSomeQueries: Reader[Connection, Int] = for {
  rs1 <- query("SELECT COUNT(*) FROM Foo")
  rs2 <- query("SELECT COUNT(*) FROM Bar")
} yield rs1.getInt(1) + rs2.getInt(1)

或者,如果这真的是行的结尾:

def doSomeQueries(conn: Connection) = (
  for {
    rs1 <- query("SELECT COUNT(*) FROM Foo")
    rs2 <- query("SELECT COUNT(*) FROM Bar")
  } yield rs1.getInt(1) + rs2.getInt(1)
).run(conn)

在您当前的版本中,您实际上并未使用conn

答案 1 :(得分:2)

为了找出使用ReaderMonad的一般的好处,我推荐Travis Brown的优秀答案 - ReaderMonad的优势在于其组合性和monad提供的其他附加功能(例如ReaderT等)。如果你用monadic风格编写其他代码,你可以从中获得最大的收益。

你还明确地询问了什么是不必明确地传递connection。我会在这里试着回答你的这部分问题。

首先,少读几字或少读字已经是一个改进。整个代码库越复杂,我就越感激。当我读了一个很长的方法(当然不是我写的;))当它的逻辑与愚蠢的论证传递交织在一起时,我发现它更容易。

其次,ReaderMonad为您提供了保证,connection始终是同一个对象。大多数情况下你都想要那样。在你的第一个例子中,它很容易调用

query("SELECT COUNT(*) FROM Bar")(anotherConnectionWhereverItCameFrom)

无论是出于目的还是出于错误。当我阅读一个很长的方法并看到使用的ReaderMonad时,我知道只会使用一个connection。在该方法的第219行中,没有一些“战术解决方案”引起的令人讨厌的意外。

请注意,没有ReaderMonad也可以实现这些好处,即使它在该领域做得很好。例如,你可以写:

class Query(val connection: Connection) {
  def apply(sql:String) = connection.createStatement.executeQuery(sql)
}

def doSomeQueries(query: Query) = {
  val rs1 = query("SELECT COUNT(*) FROM Foo")
  val rs2 = query("SELECT COUNT(*) FROM Bar")
  rs1.getInt(1) + rs2.getInt(1)
}
doSomeQueries(new Query(connection))

它既没有可编组性也没有monad的其他优点,但是会实现ReaderMonad的目标,即不明确地传递参数(connection)。