scala currying / partials来构建函数过滤器列表

时间:2013-02-15 17:41:25

标签: scala partials currying

给出以下代码:

case class Config(
  addThree: Boolean = true,
  halve: Boolean = true,
  timesFive: Boolean = true
)


def doOps(num: Integer, config: Config): Integer = {
  var result: Integer = num
  if ( config.addThree ) {
    result += 3
  }
  if ( config.halve ) {
    result /= 2
  }
  if ( config.timesFive ) {
    result *= 5
  }
  result
}                                             

val config = Config(true,false,true)          

println( doOps(20, config) )
println( doOps(10, config) )

我想用更有效和惯用的构造替换丑陋的doOps方法。具体来说,我想构建一系列函数,这些函数仅根据所使用的特定Config执行所需的转换。我知道我可能想创建一些部分应用的函数,我可以将Integer传递给它,但是我在如何以有效的方式实现这一点上画了一个空白。

我特别想避免doOps中的if语句,我希望得到的结构只是一个函数链,它可以在不检查条件的情况下调用链中的下一个函数。

结果代码,我想会看起来像这样:

case class Config(
  addThree: Boolean = true,
  halve: Boolean = true,
  timesFive: Boolean = true
)

def buildDoOps(config: Config) = ???

val config = Config(true,false,true)
def doOps1 = buildDoOps(config)

println( doOps1(20) )
println( doOps1(10) )

4 个答案:

答案 0 :(得分:3)

这是我的建议。基本上我创建了一系列彼此独立的函数。如果其中一个操作被禁用,我将其替换为identity。最后我foldLeft在该序列上,使用num参数作为初始值:

case class Config(
  addThree: Boolean = true,
  halve: Boolean = true,
  timesFive: Boolean = true
) {

  private val funChain = Seq[Int => Int](
    if(addThree) _ + 3 else identity _,
    if(halve) _ / 2 else identity _,
    if(timesFive) _ * 5 else identity _
  )

  def doOps(num: Int) = funChain.foldLeft(num){(acc, f) => f(acc)}

}

我将doOps()置于Config内,因为它非常适合。

Config(true, false, true).doOps(10)  //(10 + 3 ) * 5 = 65

如果你是受虐狂,foldLeft()可以这样写:

def doOps(num: Int) = (num /: funChain){(acc, f) => f(acc)}

如果您不喜欢identity,请使用Option[Int => Int]flatten

private val funChain = Seq[Option[Int => Int]](
    if(addThree) Some(_ + 3) else None,
    if(halve) Some(_ / 2) else None,
    if(timesFive) Some(_ * 5) else None
).flatten

答案 1 :(得分:2)

与Tomasz Nurkiewicz的解决方案类似,但使用Scalaz的monoid用于endomorphisms(具有相同输入和输出类型的函数)。

monoid的追加操作是compose,而identity元素是identity函数。

import scalaz._, Scalaz._

def endo(c: Config): Endo[Int] =
  c.timesFive ?? Endo[Int](_ * 5) |+|
  c.halve ?? Endo[Int](_ / 2) |+|
  c.addThree ?? Endo[Int](_ + 3)

def doOps(n: Int, c: Config) = endo(c)(n)

??运算符在左操作数为true时返回右操作数,在false时返回monoid的标识元素。

请注意,函数的组合顺序与它们的应用顺序相反。

答案 2 :(得分:0)

您可以向Config案例类添加更多功能,如下所示。这将允许您按照提到的方式将函数调用链接在一起。

case class Config(
  doAddThree : Boolean = true,
  doHalve : Boolean = true,
  doTimesFive : Boolean = true
) {
  def addThree(num : Integer) : Integer = if(doAddThree) (num+3) else num
  def halve(num : Integer) : Integer = if(doHalve) (num/2) else num
  def timesFive(num : Integer) : Integer = if(doTimesFive) (num*5) else num
}


def doOps(num: Integer, config: Config): Integer = {
  var result: Integer = num
  result = config.addThree(result)
  result = config.halve(result)
  result = config.timesFive(result)
  result
}                                             

val config = Config(true,false,true)          

def doOps1(num : Integer) = doOps(num, config)

println( doOps1(20) )
println( doOps1(10) )

更简洁的方法是“链接”使用foldLeft覆盖部分应用的函数列表,类似于其他答案之一所提到的:

def doOps(num: Integer, config: Config): Integer = {
  List(
    config.addThree(_),
    config.halve(_),
    config.timesFive(_)
  ).foldLeft(num) {
    case(x,f) => f(x)
  }
}

答案 3 :(得分:0)

如果你想要更具说明性(和可扩展性)的风格,你可以这样做:

import collection.mutable.Buffer

abstract class Config {
  protected def Op( func: Int => Int )( enabled: Boolean) {
    if ( enabled ) {
      _ops += func
    }   
  }
  private lazy val _ops = Buffer[Int => Int]()
  def ops: Seq[Int => Int] = _ops
}

def buildDoOps(config: Config): Int => Int = {
  val funcs = config.ops
  if ( funcs.isEmpty ) identity // Special case so that we don't compose with identity everytime
  else funcs.reverse.reduceLeft(_ andThen _)
}

现在您可以像这样简单地定义配置:

case class MyConfig(
  addThree: Boolean = true,
  halve: Boolean = true,
  timesFive: Boolean = true
) extends Config {
  Op(_ + 3)(addThree)
  Op(_ / 3)(halve)
  Op(_ * 5)(timesFive)
}

最后这里是REPL中的一些测试:

scala> val config = new MyConfig(true,false,true)
config: MyConfig = MyConfig(true,false,true)
scala> val doOps1 = buildDoOps(config)
doOps1: Int => Int = <function1>
scala> println( doOps1(20) )
115
scala> println( doOps1(10) )
65    

请注意buildDoOps采用Config的实例,这是抽象的。换句话说,它适用于Config的任何子类(例如上面的MyConfig),在创建其他类型的配置时,您不需要重写它。

此外,buildDoOps返回一个只执行请求操作的函数,这意味着每次应用函数时我们都不会不必要地测试配置中的值(但仅在构造它时)。实际上,假设函数仅依赖于配置的状态,我们可以(也可能应该)为它定义lazy val,直到Config(这是result价值低于):

abstract class Config {
  protected def Op( func: Int => Int )( enabled: Boolean) {
    if ( enabled ) {
      _ops += func
    }   
  }
  private lazy val _ops = Buffer[Int => Int]()
  def ops: Seq[Int => Int] = _ops
  lazy val result: Int => Int = {
    if ( ops.isEmpty ) identity // Special case so that we don't compose with identity everytime
    else ops.reverse.reduceLeft(_ andThen _)
  }
}    

然后我们会这样做:

case class MyConfig(
  addThree: Boolean = true,
  halve: Boolean = true,
  timesFive: Boolean = true
) extends Config {
  Op(_ + 3)(addThree)
  Op(_ / 3)(halve)
  Op(_ * 5)(timesFive)
}

val config = new MyConfig(true,false,true)
println( config.result(20) )
println( config.result(10) )