scala currying/partials to build function filter l

2019-07-20 04:53发布

Given the following code:

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

I'd like to replace the ugly doOps method with a more efficient and idiomatic construct. Specifically, I'd like to build a chain of functions that performs only the required transformations based on the specific Config being used. I know that I probably want to create some sort of partially applied function that I can pass the Integer into, but I'm drawing a blank at how to achieve this in an efficient way.

I specifically want to avoid the if statements inside doOps, I want the resulting structure to just be a chain of functions that calls the next one in the chain without checking a conditional first.

The resulting code, I imagine would look something like this:

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条回答
贼婆χ
2楼-- · 2019-07-20 05:04

Similar to Tomasz Nurkiewicz's solution, but using Scalaz's monoid for endomorphisms (functions that have the same input and output type).

The monoid's append operation is compose, and the identity element is the identity function.

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)

The ?? operator returns the right operand when the left operand is true, and the monoid's identity element when false.

Note that the order of composition of the functions is in reverse to the order they are applied.

查看更多
3楼-- · 2019-07-20 05:04

Here is my suggestion. Basically I create a sequence of functions independent from each other. If one of the operations is disabled, I replace it with identity. In the end I foldLeft over that sequence, using num argument as the initial value:

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

}

I placed doOps() inside Config as it fits there nicely.

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

If you are a masochist, foldLeft() can be written like this:

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

If you don't like identity, use Option[Int => Int] and 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
查看更多
劳资没心,怎么记你
4楼-- · 2019-07-20 05:15

You could just add more functionality to the Config case class, like the following. That will allow you to chain the function calls together as you mentioned.

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

A cleaner way to do this "chaining" would be to use foldLeft over a list of partially applied functions, similar to what one of the other answers mentions:

def doOps(num: Integer, config: Config): Integer = {
  List(
    config.addThree(_),
    config.halve(_),
    config.timesFive(_)
  ).foldLeft(num) {
    case(x,f) => f(x)
  }
}
查看更多
神经病院院长
5楼-- · 2019-07-20 05:22

If you want to go toward a more declarative (and extensible) style, you could do this:

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

Now you can simply define your config like this:

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

And finally here's some test in the 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    

Note that buildDoOps takes an instance of Config, which is abstract. In other words, it works with any sub-class of Config (such as MyConfig above) and you won't need to rewrite it when creating another type of config.

Also, buildDoOps returns a function that does just the requested operations, which means we are not needlessly testing against the values in the config everytime we apply the function (but only when constructing it). In fact, given that the function depends only on the state of the configuration, we could (and probably should) simply define a lazy val for it, right into Config (this is the result value below):

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

Then we'd do:

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) )
查看更多
登录 后发表回答