2013-02-15 36 views
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)) 

回答

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 
+1

非常有趣的構造。這可能是不重要的(基於scala/jvm如何最終優化代碼),但似乎執行路徑包括執行'identity'函數而不是完全跳過執行(就好像Seq從未包含該步驟一樣) 。我明確地想避免使用'if'語句,我想知道'identity'的額外遞歸是如何影響實際執行路徑的。我想'Seq'可以首先被過濾掉,以在'foldLeft'之前刪除'identity'調用,但我不確定這是否實際上最終會變得多餘。 – 2013-02-15 18:32:25

+0

@ConnieDobbs:使用Option [Int => Int]'和'flatten'來查看'funChain'的第二個版本,並避免'identity'。 – 2013-02-15 18:33:40

+1

感謝Thomasz,您添加了最新編輯的'Option' /'flatten'組合,我認爲避免了額外的'identity'函數調用的任何(可能/理論上的)低效率,同時(IMO)實際上提高了可讀性。 – 2013-02-15 21:16:06

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

類似托馬斯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返回正確的操作。

請注意,功能的組成順序與其應用的順序相反。

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