天天看點

ScalaNote22-函數式進階程式設計

偏函數

先看一個需求,給你一個集合val list = List(1, 2, 3, 4, “abc”) ,請完成如下要求:

将集合list中的所有數字+1,并傳回一個新的集合

要求忽略掉 非數字 的元素,即傳回的 新的集合 形式為 (2, 3, 4, 5)

用之前學過的知識,這個題有兩個解法,第一個是利用filter和map函數

  • filter篩選出Int的元素,注意此時傳回的Any類型
  • 把filter之後的元素再轉換成Int類型
  • map對篩選出來的元素計算+1
val list = List(1, 2, 3, 4, "abc") 
def isInt(x:Any)={
    // 傳回true false
    x.isInstanceOf[Int]
}
def asInt(x:Any)={
    // 傳回true false
    x.asInstanceOf[Int]
}
def andOne(x:Int)={
    x+1
}
// 當然asInt和andOne可以寫在一起,這樣省點事
list.filter(isInt).map(asInt).map(andOne)      
list: List[Any] = List(1, 2, 3, 4, abc)
isInt: (x: Any)Boolean
asInt: (x: Any)Int
andOne: (x: Int)Int
res3: List[Int] = List(2, 3, 4, 5)      

也可以用模式比對寫

// 此時如果x非Int,傳回(),是以還得再filter
def matchF(x:Any)={
    x match{
        case x:Int => x+1
        case _ =>
    }
}
println(list.map(matchF))
println(list.map(matchF).filter(isInt))      
List(2, 3, 4, 5, ())
List(2, 3, 4, 5)





matchF: (x: Any)AnyVal      

上述兩種方法都要進行filter和map兩步操作,偏函數可以一步到位~

  • 在對符合某個條件,而不是所有情況進行邏輯操作時,使用偏函數是一個不錯的選擇
  • 将包在大括号内的一組case語句封裝為函數,我們稱之為偏函數,它隻對會作用于指定類型的參數或指定範圍值的參數實施計算,超出範圍的值會忽略(未必會忽略,這取決于你打算怎樣處理)
  • 偏函數在Scala中是一個特質

直接上代碼吧~

val list = List(1, 2, 3, 4, "abc")
//定義一個偏函數
    //1. PartialFunction[Any,Int] 表示偏函數接收的參數類型是Any,傳回類型是Int
    //2. isDefinedAt(x: Any) 如果傳回true ,就會去調用 apply 建構對象執行個體,如果是false,過濾
    //3. apply 構造器 ,對傳入的值 + 1,并傳回(新的集合)
    //4. isDefinedAt和apply方法是固定的,不可以随便定義方法名稱
val addOne3= new PartialFunction[Any, Int] {
def isDefinedAt(any: Any) = if (any.isInstanceOf[Int]) true else false
def apply(any: Any) = any.asInstanceOf[Int] + 1
}
val list3 = list.collect(addOne3)
println("list3=" + list3)      
list3=List(2, 3, 4, 5)





list: List[Any] = List(1, 2, 3, 4, abc)
addOne3: PartialFunction[Any,Int] = <function1>
list3: List[Int] = List(2, 3, 4, 5)      
  • 使用建構特質的實作類(使用的方式是PartialFunction的匿名子類)
  • PartialFunction 是個特質(看源碼)
  • 建構偏函數時,參數形式[Any, Int]是泛型,第一個表示參數類型,第二個表示傳回參數
  • 當使用偏函數時,會周遊集合的所有元素,編譯器執行流程時先執行isDefinedAt()如果為true ,就會執行 apply, 建構一個新的Int對象傳回
  • 執行isDefinedAt() 為false 就過濾掉這個元素,即不建構新的Int對象
  • map函數不支援偏函數,因為map底層的機制就是所有循環周遊,無法過濾處理原來集合的元素
  • collect函數支援偏函數

上面偏函數也有其他簡化形式,後面遇到我應該也不會寫了吧。。。直接把代碼Copy過來~

def f2: PartialFunction[Any, Int] = {
  case i: Int => i + 1 // case語句可以自動轉換為偏函數
}
List(1, 2, 3, 4,"ABC").collect(f2)      
f2: PartialFunction[Any,Int]
res10: List[Int] = List(2, 3, 4, 5)      
List(1, 2, 3, 4,"ABC").collect{ case i: Int => i + 1 
                                case i:String=>i*2}      
res13: List[Any] = List(2, 3, 4, 5, ABCABC)      

作為參數的函數

這個其實在map中用過,簡單看個例子:

//說明
def plus(x: Int) = 3 + x
//說明
println(Array(1, 2, 3, 4).map(plus(_)).mkString(","))
println(Array(1, 2, 3, 4).map(plus).mkString(","))      
4,5,6,7
4,5,6,7





plus: (x: Int)Int      
  • ​map(plus(_)) 中的plus(_) 就是将plus這個函數當做一個參數傳給了map,_這裡代表從集合中周遊出來的一個元素​

  • ​plus(_) 這裡也可以寫成 plus 表示對 Array(1,2,3,4) 周遊,将每次周遊的元素傳給plus的 x!​

匿名函數

沒有名字的函數就是匿名函數,可以通過函數表達式來設定匿名函數!

val triple = (x: Double) => 3 * x
println(triple(3))
println(triple.getClass)      
9.0
class $line39.$read$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$anonfun$1





triple: Double => Double = <function1>      
  • (x: Double) => 3 * x 就是匿名函數
  • (x: Double) 是形參清單, => 是規定文法表示後面是函數體, 3 * x 就是函數體,如果有多行,可以 {} 換行寫
  • triple 是指向匿名函數的變量

再看個例子:

val f1 = (n1: Int, n2: Int ) => {
println("biubiubiu")
n1 + n2
}
println(f1.getClass)
println("f1:" + f1)
println(f1(10, 30))      
class $line41.$read$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$anonfun$1
f1:<function2>
biubiubiu
40





f1: (Int, Int) => Int = <function2>      

高階函數

能夠接受函數作為參數的函數,叫做高階函數 (higher-order function)。可使應用程式更加健壯

//test 就是一個高階函數,它可以接收f: Double => Double ,f的參數和傳回值都為Double
def test(f: Double => Double, n1: Double) = {
f(n1)
}
//sum 是接收一個Double,傳回一個Double
def sum(d: Double): Double = {
d + d
}
val res = test(sum, 6.0)
println("res=" + res)      
res=12.0





test: (f: Double => Double, n1: Double)Double
sum: (d: Double)Double
res: Double = 12.0      

上面的例子是把函數作為參數,我們也可以定義一個函數,使之結果傳回為一個函數

// 相當于把x固定了傳回的新函數中,y是參數
def minusxy(x: Int) = {
 (y: Int) => x-y //匿名函數
}
//這一步相當于先傳回個函數(3-y),然後y=5
val result3 = minusxy(3)(5) 
// println(result3)      
minusxy: (x: Int)Int => Int
result3: Int = -2      

參數類型推斷

據說這裡的應用很多,我暫時沒遇到過。參數推斷省去類型資訊(在某些情況下[需要有應用場景],參數類型是可以推斷出來的,如list=(1,2,3) list.map(),map中函數參數類型是可以推斷的,因為list中元素都是Int),同時也可以進行相應的簡寫。

參數類型推斷寫法說明:

  • 參數類型是可以推斷時,可以省略參數類型
  • 當傳入的函數,隻有單個參數時,可以省去括号
  • 如果變量隻在=>右邊隻出現一次,可以用_來代替
val list = List(1, 2, 3, 4)
//匿名函數寫法
println(list.map((x:Int)=>x + 1)) //(2,3,4,5)
//list中都是Int,可以簡寫
println(list.map((x)=>x + 1)) //(2,3,4,5)
println(list.map(x=>x + 1)) //(2,3,4,5)
//x在右邊隻出現一次,直接_簡寫
println(list.map( _ + 1)) //(2,3,4,5)
// reduce好像預設是reduceLeft?從左邊開始計算,把元素一次傳進去  
println(list.reduce((n1:Int ,n2:Int) => n1 + n2)) //10
println(list.reduce((n1 ,n2) => n1 + n2)) //10
println(list.reduce( _ + _)) //10      
List(2, 3, 4, 5)
List(2, 3, 4, 5)
List(2, 3, 4, 5)
List(2, 3, 4, 5)
10
10
10





list: List[Int] = List(1, 2, 3, 4)      

閉包

前面高階函數中,傳回值為函數的case已經涉及到了閉包。閉包就是一個函數和與其相關的引用環境組合的一個整體(實體)。看個例子:

//1.用等價了解方式改寫 2.對象屬性了解
def minusxy(x: Int) = (y: Int) => x - y
//f函數就是閉包.這玩意感覺就是友善後面使用時,不需要重複把x=20傳進去
val f = minusxy(20) 
println("f(1)=" + f(1)) // 19
println("f(2)=" + f(2)) // 18      
f(1)=19
f(2)=18





minusxy: (x: Int)Int => Int
f: Int => Int = <function1>      

再來看個實際case:

  • 編寫一個函數 makeSuffix(suffix: String) 可以接收一個檔案字尾名(比如.jpg),并傳回一個閉包
  • 調用閉包,可以傳入一個檔案名,如果該檔案名沒有指定的字尾(比如.jpg) ,則傳回 檔案名.jpg , 如果已經有.jpg字尾,則傳回原檔案名
  • 要求使用閉包的方式完成
  • 字尾名可以使用String.endsWith(xx),這個函數類似截取最後length(xx)個字元串和xx比較,傳回true false
def makeSuffix(suffix: String) ={
    (fileName:String)=> if( fileName.endsWith(suffix)) fileName else fileName+suffix
}
val f = makeSuffix(".jpg")
println(f("dog.jpg")) // dog.jpg
println(f("cat")) // cat.jpg      
dog.jpg
cat.jpg





makeSuffix: (suffix: String)String => String
f: String => String = <function1>      

函數柯裡化

韓老師講的一個例子很懵逼,有點繞,但是如果隻看函數柯裡化的形式,似乎又不是很複雜。

  • 函數程式設計中,接受多個參數的函數都可以轉化為接受單個參數的函數,這個轉化過程就叫柯裡化
  • 柯裡化就是證明了函數隻需要一個參數而已。其實我們剛才的學習過程中,已經涉及到了柯裡化操作

上面閉包和高階函數的case就用到所謂的函數柯裡化

def mulCurry1(x: Int,y:Int) = x * y
def mulCurry2(x: Int)(y:Int) = x * y
println(mulCurry1(10,8))
println(mulCurry2(10)(8))      
80
80





mulCurry1: (x: Int, y: Int)Int
mulCurry2: (x: Int)(y: Int)Int      

控制抽象

控制抽象學着更是蒙蔽。。。先看定義:

控制抽象是函數且滿足如下條件

  • 參數是函數
  • 函數參數沒有輸入值也沒有傳回值
var x = 10
def until(condition: => Boolean)(block: => Unit): Unit = {
//類似while循環,遞歸
if (!condition) {
block
until(condition)(block)
}
//      println("x=" + x)
//      println(condition)
//      block
//      println("x=" + x)
}
// x==0是condition,後面的代碼塊是block參數
//如果condition不滿足,則執行block,并且遞歸調用until
until(x == 0){
x -= 1
println("x=" + x)
}      
x=9
x=8
x=7
x=6
x=5
x=4
x=3
x=2
x=1
x=0





x: Int = 0
until: (condition: => Boolean)(block: => Unit)Unit