【问题标题】:scala currying/partials to build function filter listscala currying/partials 构建函数过滤器列表
【发布时间】:2013-02-16 03:37:43
【问题描述】:

给定以下代码:

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) )

【问题讨论】:

    标签: scala partials currying


    【解决方案1】:

    这是我的建议。基本上,我创建了一系列相互独立的函数。如果其中一项操作被禁用,我将其替换为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
    

    【讨论】:

    • 非常有趣的结构。这可能无关紧要(基于 scala/jvm 最终如何优化代码),但似乎生成的执行路径涉及执行 identity 函数,而不是完全跳过执行(好像 Seq 从未包含该步骤) .我明确地想避免if 语句,我想知道identity 的额外递归如何影响实际的执行路径。我猜想Seq 可以首先被过滤以删除foldLeft 之前的identity 调用,但我不确定这是否真的会成为多余的。
    • @ConnieDobbs:使用Option[Int => Int]flatten 查看funChain 的第二个版本并避免使用identity
    • 感谢 Thomasz,您在最新编辑中添加的 flatten 组合我认为避免了额外的 identity 函数调用的任何(可能/理论上的)低效率,而(IMO)实际上提高了可读性。
    • 您可能想在答案中添加一件事,Thomasz:在我们不使用identity 的情况下,可能值得在Some(_) 中添加最后一个Some(_),以便如果所有选项都是false,则它不仅仅是一个空的Seq,(它不会返回未掺杂的原始字符串)。
    • @ConnieDobbs:实际上foldLeft() 超过空序列是安全的,因此如果funChain 为空,则返回初始值(num)。这实际上是有道理的 - 没有转换,什么都不做。
    【解决方案2】:

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

    幺半群的追加操作是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 时返回幺半群的标识元素。

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

    【讨论】:

      【解决方案3】:

      您可以向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)
        }
      }
      

      【讨论】:

        【解决方案4】:

        如果您想采用更具声明性(和可扩展性)的样式,您可以这样做:

        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) )
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-02-09
          • 1970-01-01
          • 2021-12-25
          • 1970-01-01
          相关资源
          最近更新 更多