可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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) )
回答1:
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
回答2:
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:
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)
}
}
回答4:
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) )