swift之函數式編程

函數式編程初探

最近初學swift,和OC比,發現語言更現代,也有了更多的特性。如何寫好swift代碼,也許,熟練使用新特性寫出更優秀的代碼,就是答案。今天先從大的方向談談swift中的編程範式-函數式編程。主要還是讀了大佬帖子,寫寫自己的理解。

什麼是函數式編程

"函數式編程"是一種"編程範式"(programming paradigm),也就是如何編寫程序的方法論。

它屬於"結構化編程"的一種,主要思想是把運算過程儘量寫成一系列嵌套的函數調用。

它使代碼更像自然語言,告訴程序員要幹什麼,而不是怎麼幹,把怎麼幹的細節拆分到各個函數中。調用的地方邏輯清晰,便於debug。

舉例來說,現在有這樣一個數學表達式:

  (1 + 2) * 3 - 4

傳統的過程式編程,可能這樣寫:

var a = 1 + 2;

var b = a * 3;

var c = b - 4;

函數式編程要求使用函數,我們可以把運算過程定義爲不同的函數,然後寫成下面這樣:

var result = subtract(multiply(add(1,2), 3), 4);

這就是函數式編程。

爲什麼使用函數式編程

  1. 代碼簡潔,開發快速 函數式編程大量使用函數,減少了代碼的重複,因此程序比較短,開發速度較快。
  2. 接近自然語言,易於理解 函數式編程的自由度很高,可以寫出很接近自然語言的代碼。我們把程序的邏輯分成了幾個函數,這樣一來,我們的代碼邏輯也會變得幾個小碎片,於是我們讀代碼時要考慮的上下文就少了很多,閱讀代碼也會更容易。 而把代碼邏輯封裝成了函數後,我們就相當於給每個相對獨立的程序邏輯取了個名字,於是代碼成了自解釋的。 前文曾經將表達式(1 + 2) * 3 - 4,寫成函數式語言: 對它進行變形,不難得到另一種寫法:   add(1,2).multiply(3).subtract(4)
subtract(multiply(add(1,2), 3), 4)
  1. 更方便的代碼管理 函數式編程不依賴、也不會改變外界的狀態,只要給定輸入參數,返回的結果必定相同。因此,每一個函數都可以被看做獨立單元,很有利於進行單元測試(unit testing)和除錯(debugging),以及模塊化組合。
  2. 易於"併發編程" 函數式編程不需要考慮"死鎖"(deadlock),因爲它不修改變量,所以根本不存在"鎖"線程的問題。不必擔心一個線程的數據,被另一個線程修改,所以可以很放心地把工作分攤到多個線程,部署"併發編程"(concurrency)。

函數式編程的一些重要概念

函數是"第一等公民"

所謂"第一等公民"(first class),指的是函數與其他數據類型一樣,處於平等地位,可以賦值給其他變量,也可以作爲參數,傳入另一個函數,或者作爲別的函數的返回值。

舉例來說,下面代碼中的print變量就是一個函數,可以作爲另一個函數的參數。

var print = function(i){ console.log(i);};

[1,2,3].forEach(print);

函數(function)和procedure是有區別的

函數(function)這個名詞來自於數學,函數通過一個給定的值,計算出另外一個值,也就是上學時常見的f(x)。

在通常的理解中,下面代碼裏面,f1,f2,f3通常都被叫做函數:

//僞代碼 函數無入參,返回2
def f1(): return 2
//函數有參數x,返回 x+1
def f2(int x): return x+1
//函數無入參,無返回值,打印hello world
def f3(): print("hello world")

複製代碼但實際上,函數(function)和procedure是有區別的: function 通過運算返回一個值,而procedure只執行一段代碼,沒有返回值。 這一點對於後面的理解是非常有幫助的,首先要區分出二者。

再回到上面的代碼中,f1,f2,爲function而f3爲procedure。

Pure Function(純函數)

Pure:純的; 單純的; 純真的; 乾淨的 我們將滿足下面兩個條件的函數稱作Pure Function:

  • 函數不會產生side effect(no side effect)
  • 函數滿足referential transparency這個條件 (原諒我不會翻譯這兩個名詞)
  1. Side effect 函數調用後不會對外部狀態產生影響,比如下面這段代碼中sum函數是no side effect的: 產生side effect的函數長成什麼樣呢?其實經常會寫這樣的函數: int sum = 0 def plus(a,b){ sum = a + b return sum } plus函數除了計算參數之和以外,還改變了外部變量sum的值,我們plus這個函數產生了side effect。 常見的Side effect
def sum(a,b): return a+b
  • 改變外部變量的值(上面的例子中plus函數)
  • 像磁盤中寫入數據
  • 將頁面上的一個按鈕設置爲可點擊,或者不可點擊 前面提到function和procedure的不同點,在side effect這個角度來講,pure funcion不會產生side effect,procedure通常會產生side effect。 滿足Referential Transparency的函數可以將可以將用函數計算的結果替換表達式本身,而不影響程序的邏輯。 給定指定的參數,在任何時候返回的值都是相同的。不受其他外部條件影響。 兩者說的意思是一樣的,只是表達的角度是不同的 舉個滿足RT的例子 下面這段代碼中的f()是滿足RT的函數,按照上面的解釋,我們可以將f()的結果也就是2替換掉f(),不會影響程序本身的邏輯: def f(): return 2 print(2 + f()) print(2) 或者這樣替換: def f(): return 2 print(f() + 2) print(2) 從另一個角度說,f()這個函數無論在什麼時候調用,返回的值都是一樣的,不會發生改變(沒有外部條件影響) 舉個不滿足RT的例子 int counter = 0 def f(x){ counter += 1 return x + counter } 這個例子中,f(x)這個函數不滿足RT 下面的代碼中,當我們用f(1)的計算結果一次替換代碼中f(1)本身時,程序的邏輯是錯誤的: //原始的代碼執行結果是:3 f(1) + f(1) //把f(1)的結果1替換進來,下面函數執行的結果是:2 f(1) + 1 //同樣,得到2 1 + f(1) //得到2 1 + 1 我們不能用執行的結果替換函數本身, 換個角度,下面兩行代碼執行的結果也不同 f(1) + f(1) 2 * f(1) 雖然入參都爲1,但f(1)在不同時候調用得到的結果不同,因此f不滿足RT這個條件 回到pure function,理解了side effect 和 referential transparency的含義,我們再來重溫pure function的定義,就很好理解了:
def f(): return 2

print(f() + f())
print(2)
  • No side effect
  • Referential transparency 滿足這兩個條件的函數,稱之爲pure function

引用透明

引用透明(Referential transparency),指的是函數的運行不依賴於外部變量或"狀態",只依賴於輸入的參數,任何時候只要參數相同,引用函數所得到的返回值總是相同的。

有了前面的第三點和第四點,這點是很顯然的。其他類型的語言,函數的返回值往往與系統狀態有關,不同的狀態之下,返回值是不一樣的。這就叫"引用不透明",很不利於觀察和理解程序的行爲。

swift中函數式編程的應用

高階函數

先說兩個概念型的名詞:

高階函數(high order func),指可以將其他函數作爲參數或者返回結果的函數。

一級函數(first class func),指可以出現在任何其他構件(比如變量)地方的函數。

map 

map { (Element) -> Element in
    對 element 進行處理
}

一般用在集合類型,對集合裏的元素進行遍歷,函數體裏實現對每一個元素的操作。

var arr = [1,3,2,4]
let mapres = arr.map {
    "NO." + String($0)
}

// 運行結果:["NO.1", "NO.3", "NO.2", "NO.4"]

reduce

reduce(Result) { (Result, Element) -> Result in
    基於 Result 對當前的 Element 進行操作,並返回新的 Result
}

一般用在集合類型,對集合裏的元素進行疊加處理,函數體裏傳兩個參數,第一個是之前的疊加結果,第二個是當前元素,返回值是對當前元素疊加後的結果。

// 對數組裏的元素:奇數相加,偶數相乘
var arr = [1,3,2,4]
let reduceRes = arr.reduce((0,1)) { (a:(Int,Int), t:Int) -> (Int,Int) in
    if t % 2 == 1 {
        return (a.0 + t, a.1)
    } else {
        return (a.0, a.1 * t)
    }
}
// 運行結果:(4,8)

filter

filter { (Element) -> Bool
    對元素的篩選條件,返回 Bool
}

一般用在集合類型,對集合裏的元素進行篩選。函數體裏實現篩選條件,返回 true 的元素通過篩選。

var arr = [1,3,2,4]
let filterRes = arr.filter {
    $0 % 2 == 0
}
// 運行結果:[2,4]

flatMap

首先先看下 Swift 源碼裏對集合數組的map和flatmap的實現:

// Sequence.swift
extension Sequence {
    public func map<T>(_ transform: (Element) -> T) -> [T] {}
}

// SequenceAlgorithms.swift.gyb
extension Sequence {
    public func flatMap<T>(_ transform: (Element) -> T?) -> [T] {}
    public func flatMap<S : Sequence>(_ transform: (Element) -> S) -> [S.Element] {}
}

前面我們已經知道,map是一種遍歷,而上面的代碼又顯示出來,flatmap有兩種重載的函數:

其中一種與map非常相似,差別只在閉包裏的返回值變成了可選類型。 另一種稍微有點不同,閉包傳入的是數組,最後返回的是數組的元素組成的集合。

// map
let arr = [1,2,nil,4,nil,5]
let arrRes = arr.map { $0 } // 結果爲:[Optional(1), Optional(2), nil, Optional(4), nil, Optional(5)]

// flatmap
let brr = [1,2,nil,4,nil,5]
let brrRes = brr.flatmap { $0 } // 結果爲:[1, 2, 4, 5]

let crr = [[1,2,4],[5,3,2]]
let ccRes = crr.flatmap { $0 } // 結果爲:[1, 2, 4, 5, 3, 2]
let cdRes = crr.flatmap { c in
    c.map { $0 * $0 }
} // 結果爲[1, 4, 16, 25, 9, 4]

// 使用 map 實現的上面平鋪功能
let ceRes = Array(crr.map{ $0 }.joined()) // 同 ccRes
let cfRes = Array(crr.map{ $0 }.joined()).map{ $0 * $0 } // 同 cdRes

簡單理解爲,flatMap可以將多維數組平鋪,也還以過濾掉一維數組中的nil元素。

map和flatMap不只在數組中可以使用,對於 Optional 類型也是可以進行操作的。先看下面這個例子:

let a: Date? = Date()
let formatter = DateFormatter()
formatter.dateStyle = .medium

let c = a.map(formatter.string(from:))
let d = a == nil ? nil : formatter.string(from: a!)
c 和 d 是兩種不同的寫法,c 寫法是不是更優雅一些?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章