5

我有一个要过滤的列表中的结果。

用户可以为行上的任何属性提供特定限制(例如,我只想查看 x == 1 的行)。如果他们没有指定限制,那么当然不会使用该谓词。当然,最简单的形式是:

list.filter(_.x == 1)

有许多可能的简单谓词,我正在使用将用户搜索词(例如 Option[Int])转换为谓词函数或 Identity(返回 true 的函数)的代码即时构建一个新的谓词函数。代码看起来像这样(缩短,为清楚起见添加了显式类型):

case class ResultRow(x: Int, y: Int)

object Main extends App {
  // Predicate functions for the specific attributes, along with debug output
  val xMatches = (r: ResultRow, i: Int) => { Console println "match x"; r.x == i }
  val yMatches = (r: ResultRow, i: Int) => { Console println "match y"; r.y == i }
  val Identity = (r : ResultRow) => { Console println "identity"; true }

  def makePredicate(a: Option[Int], b: Option[Int]) : ResultRow => Boolean = {
    // The Identity entry is just in case all the optional params are None 
    // (otherwise, flatten would cause reduce to puke)
    val expr = List(Some(Identity), 
                    a.map(i => xMatches(_: ResultRow, i)),
                    b.map(i => yMatches(_: ResultRow, i))
                   ).flatten

    // Reduce the function list into a single function. 
    // Identity only ever appears on the left...
    expr.reduceLeft((a, b) => (a, b) match {
      case (Identity, f) => f
      case (f, f2) => (r: ResultRow) => f(r) && f2(r)
    })
  }

  val rows = List(ResultRow(1, 2), ResultRow(3, 100))

  Console println rows.filter(makePredicate(Some(1), None))
  Console println rows.filter(makePredicate(None, None))
  Console println rows.filter(makePredicate(None, Some(100)))
  Console println rows.filter(makePredicate(Some(3), Some(100)))
}

这完美地工作。运行时,它会正确过滤,并且调试输出证明调用了最少数量的函数来正确过滤列表:

match x
match x
List(ResultRow(1,2))
identity
identity
List(ResultRow(1,2), ResultRow(3,100))
match y
match y
List(ResultRow(3,100))
match x
match x
match y
List(ResultRow(3,100))

实际上,我对结果如此之好感到非常满意。

但是,我不禁认为有一种更实用的方法可以做到这一点(例如 Monoids 和 Functors 和广义求和)......但我不知道如何让它工作。

我尝试遵循一个 scalaz 示例,该示例表明我需要创建一个隐式零和半群,但我无法让 Zero[ResultRow => Boolean] 进行类型检查。

4

2 回答 2

4

您可以使用以下方法稍微简化代码(无需迁移到 Scalaz)forall

def makePredicate(a: Option[Int], b: Option[Int]): ResultRow => Boolean = {
  val expr = List(
    a.map(i => xMatches(_: ResultRow, i)),
    b.map(i => yMatches(_: ResultRow, i))
  ).flatten

  (r: ResultRow) => expr.forall(_(r))
}

请注意,这也消除了包含Some(Identity)在列表中的需要。

如果您有很多行,我建议使用zipxMatches函数与用户输入相匹配,如下所示:

val expr = List(a, b) zip List(xMatches, yMatches) flatMap {
  case (maybePred, matcher) => maybePred.map(i => matcher(_: ResultRow, i))
}

两行实际上不再简洁或易读,而是四行或五行。


要回答您关于 Scalaz 的问题,问题是 有两个可能的幺半群Boolean,而 Scalaz 不会为您选择一个 - 相反,您必须使用 Haskellnewtype包装器之类的东西标记布尔值,以指示您要使用哪个幺半群(在 Scalaz 7 中——在 6 中,方法有点不同)。

一旦你指定了你想要Boolean的幺半群,那个幺半群 forFunction1就会启动,并且没有什么可做的——你不需要Identity明确定义零。例如:

import scalaz._, Scalaz._

def makePredicate(a: Option[Int], b: Option[Int]): ResultRow => Boolean =
  List(a, b).zip(List(xMatches, yMatches)).flatMap {
    case (maybePred, matcher) =>
      maybePred.map(i => matcher(_: ResultRow, i).conjunction)
  }.suml

在这里,我们刚刚取了ResultRow => Boolean @@ Conjunction函数的总和。

于 2012-12-22T12:32:14.613 回答
3

我非常喜欢的一种简化是使用 Function1[A, Boolean] 的库 pimp 简化这种谓词管道,它将标准布尔表达式提升为谓词。这是我的一个子集:

  implicit def toRichPredicate[A](f: Function1[A, Boolean]) = new RichPredicate(f)
  def tautology[A] = (x:A)=>true
  def falsehood[A] = (x:A)=>false

  class RichPredicate[A](f: Function1[A, Boolean]) extends Function1[A, Boolean] {
    def apply(v: A) = f(v)

    def &&(g: Function1[A, Boolean]): Function1[A, Boolean] = {
      (x: A) => f(x) && g(x)
    }

    def ||(g: Function1[A, Boolean]): Function1[A, Boolean] = {
      (x: A) => f(x) || g(x)
    }

    def unary_! : Function1[A, Boolean] = {
      (x: A) => !f(x)
    }
  }

我发现这完全可以重复使用。有了这样的东西,你的减少就变成了

list.flatten.foldLeft(tautology)(&&)

这很简单。它还为更深层次的功能优势指明了方向,因为带有重言式和 && 的谓词显然形成了一个幺半群,所以这一切都归结为调用 Scalaz 或 Haskell 中的一些高阶 typeclass-y goodness。这在这两种情况下也变得有点棘手,因为在其他情况下,您可能会在由 falsehood 和 || 形成的谓词上使用幺半群,因此需要重载实例解析。

于 2012-12-22T13:17:02.877 回答