2

给定以下代码:

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 方法。具体来说,我想构建一个函数链,仅根据所使用的特定配置执行所需的转换。我知道我可能想创建某种部分应用的函数,我可以将 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

4 回答 4

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
于 2013-02-15T18:00:30.503 回答
2

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

幺半群的追加操作是compose,标识元素是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

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

于 2013-02-15T18:17:18.067 回答
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) )
于 2013-02-15T18:45:39.757 回答
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)
  }
}
于 2013-02-15T18:09:46.470 回答