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) )
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 theidentity
function.The
??
operator returns the right operand when the left operand istrue
, and the monoid's identity element whenfalse
.Note that the order of composition of the functions is in reverse to the order they are applied.
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 IfoldLeft
over that sequence, usingnum
argument as the initial value:I placed
doOps()
insideConfig
as it fits there nicely.If you are a masochist,
foldLeft()
can be written like this:If you don't like
identity
, useOption[Int => Int]
andflatten
: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.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:If you want to go toward a more declarative (and extensible) style, you could do this:
Now you can simply define your config like this:
And finally here's some test in the REPL:
Note that
buildDoOps
takes an instance ofConfig
, which is abstract. In other words, it works with any sub-class ofConfig
(such asMyConfig
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 alazy val
for it, right intoConfig
(this is theresult
value below):Then we'd do: