Swift 相比原先的 Objective-C 最重要的優點之一,就是對函數語言程式設計提供了更好的支援。 Swift 提供了更多的語法糖和一些新特性來增強函數語言程式設計的能力,本文就在這方面進行一些討論。
Swift 概覽
對程式語言有了一些經驗的程式設計師,尤其是那些對多種不同型別的程式語言都有經驗的開發者, 在學習新的語言的時候更加得心應手。原因在於程式語言本身也是有各種正規化的, 把握住這些特點就可以比較容易的上手了。
在入手一門新的語言的時候,一般關注的內容有:
- 原生資料結構
- 運算子
- 分支控制
- 如果是物件導向的程式語言,其物件導向的實現是怎樣的
- 如果是函數語言程式設計語言,其面向函數語言程式設計的實現是怎樣的
通過這幾個點,其實只要閱讀 Swift 文件的第一章,你就可以對這個語言有一個大概的印象。 比如對於資料結構,Swift 和其他的程式語言大體一樣,有 Int, Float, Array, Dictionary 等, 運算子也基本與 C 語言一致等。 本文主要集中於對 Swift 函數語言程式設計方面的特點進行一些盤點,因此在這裡假設大家對 Swift 的基本語法已經有所瞭解。
對於一種程式設計正規化,要掌握它也要抓住一些要點。對於支援函數語言程式設計的語言,其一般的特點可能包含以下幾種:
- 支援遞迴
- 函式本身是語言 First Class 的組成要素,且支援高階函式和閉包
- 函式呼叫盡可能沒有副作用 (Side Effect) 的條件
接下來我們來逐個盤點這些內容。
遞迴
Swift 是支援遞迴的,事實上現在不支援遞迴的程式語言已經很難找到了。在 Swift 裡寫一個遞迴呼叫和其他程式語言並沒有什麼區別:
1 2 3 4 5 6 7 8 9 |
func fib(n: Int) -> Int { if n <= 1 { return 1 } else { return fib(n-1) + fib(n-2) } } fib(6) // output 13 |
關於 Swift 的遞迴沒有什麼好說的。作為一個常識,我們知道遞迴是需要消耗棧空間的。 在函數語言程式設計語言中,遞迴是一個非常常用的方法,然而使用不慎很容易導致棧溢位的問題。 如果將程式碼改寫為非遞迴實現,又可能會導致程式碼的可讀性變差,因此有一個技巧是使用“尾遞迴”, 然後讓編譯器來優化程式碼。
一個 Common Lisp 的尾遞迴的例子是
1 2 3 4 5 6 7 |
(defun fib(n) (fib-iter 1 0 n)) (defun fib-iter(a b count) (if (= count 0) b (fib-iter (+ a b) a (- count 1)))) |
我們可以把我們上述的 Swift 程式碼也改寫成相同形式
1 2 3 4 5 6 7 8 9 10 11 12 |
func fibiter(a: Int, b: Int, count: Int) -> Int { if count==0 { return b } else { return fibiter(a + b, a, count-1) } } func fib(n: Int) -> Int { return fibiter(1, 1, n); } |
我們可以 Playground 裡觀察是否使用尾遞迴時的迭代結果變化。
值得注意的是,這裡出現了一個 Swift 的問題。雖然 Swift 支援巢狀函式,但是當我們將fibiter
作為一個高階函式包含在fib
函式之內的時候卻發生了 EXC_BAD_ACCESS 報錯, 並不清楚這是語言限制還是 Bug。
Swift 的高階函式和閉包
在 Objective-C 時代,使用 block 來實現高階函式或者閉包已經是非常成熟的技術了。 Swift 相比 Objective-C 的提高在於為函數語言程式設計新增了諸多語法上的方便。
首先是高階函式的支援,可以在函式內定義函式,下面就是一個很簡潔的例子。
1 2 3 4 5 6 7 8 9 10 |
func greetingGenerator(object:String) -> (greeting:String) -> String { func sayGreeting(greeting:String) -> String { return greeting + ", " + object } return sayGreeting } let sayToWorld = greetingGenerator("world") sayToWorld(greeting: "Hello") // "Hello, World" sayToWorld(greeting: " 你好 ") // " 你好, World" |
如果使用 block 實現上述功能,可讀性就不會有這麼好。而且 block 的語法本身也比較怪異, 之前沒少被人吐槽。Swift 從這個角度來看比較方便。事實上,在 Swift 裡可以將函式當做物件賦值, 這和很多函數語言程式設計語言是一樣的。
作為一盤大雜燴,Swift 的函式系統也很有 JavaScript 的影子在裡面。比如可以向下面這樣定義函式:
1 2 3 4 5 6 |
let add = { (a:Int, b:Int) -> Int in return a+b } add(1, 2) // 3 |
等號之後被賦予變數add
的是一個閉包表示式,因此更準確的說, 這是將一個閉包賦值給常量了。注意在閉包表示式中,in
關鍵字之前是閉包的形式定義,之後是具體程式碼實現。 Swift 中的閉包跟匿名函式沒有什麼區別。 如果你將它賦值給物件,就跟 JavaScript 中相同的實踐是一樣的了。幸好 Swift 作為 C 系列的語言, 其分支語句 if 等本身是有作用域的,因此不會出現下列 JavaScript 的坑:
1 2 3 4 5 6 7 8 |
if (someNum>0) { function a(){ alert("one") }; } else { function a(){ alert("two") }; } a() // will always alert "two" in most of browsers |
Swift 的閉包表示式和函式都可以作為函式的引數,從下面的程式碼我們可以看出閉包和函式的一致性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func function() { println("this is a function") } let closure = { () -> () in println("this is a closure") } func run(somethingCanRun:()-> ()) { somethingCanRun() } run(function) run(closure) |
類似於 Ruby,Swift 作為函式引數的閉包做了一點語法糖。 在 Ruby 中使用 Block 的時候,我們可以這樣寫:
1 |
(1...5).map {|x| x*2} // => [2, 4, 6, 8] |
在 Swift 當中我們可以得到幾乎一樣的表示式。
1 2 |
var a = Array(1..5).map {x in x*2} // a = [2, 4, 6, 8] |
也就是說, 如果一個函式的最後一個引數是閉包,那麼它在語法上可以放在函式呼叫的外面。 閉包還可以用$0
、$1
等分別來表示第 0、第 1 個引數等。 基本的運算子也可以看做函式。 下面的幾種方式都可以實現逆序倒排的功能。
1 2 3 4 5 |
let thingsToSort = Array(1..5) var reversed1 = sort(thingsToSort) { a, b in a<b} var reversed2 = sort(thingsToSort) { $0 < $1} var reversed3 = sort(thingsToSort, <) // operator as a function // all the above are [5, 4, 3, 2, 1] |
總體來說,Swift 在新增方便函式操作、新增相關語法糖方面走的很遠,基本上整合了目前各種語言中比較方便的特性。 實用性較好。
Side Effects
在電腦科學中,函式副作用指當呼叫函式時,除了返回函式值之外,還對主呼叫函式產生附加的影響。例如修改全域性變數 (函式外的變數) 或修改引數 (wiki)。 函式副作用會給程式帶來一些不必要的麻煩。
為了減少函式副作用,很多函數語言程式設計語言都力求達到所謂的“純函式”。 純函式是指函式與外界交換資料的唯一渠道是引數和返回值, 而不會受到函式的外部變數的干擾。 乍看起來這似乎跟閉包的概念相牴觸,因為閉包本身的一個重要特點就是可以訪問到函式定義時的上下文環境。
事實上,為了在這種情況下支援純函式,一些程式語言如 Clojure 等提供的資料結構都是不可變 (或者說 Persist) 的。 因此其實也就沒有我們傳統意義上的所認為的“變數”的概念。比如說,在 Python 中,字串str
就是一類不可變的資料結構。 你不能在原來的字串上進行修改,每次想要進行類似的操作,其實都是生成了一個新的str
物件。 然而 Python 中的連結串列結構則是可變的。且看下面的程式碼,在 Python 中對a
字串進行修改並不會影響b
, 但是同樣的操作作用於連結串列就會產生不一樣的結果:
1 2 3 4 5 |
a = "hello, " b = a a += "world" print a # hello, world print b # hello, |
Swift 的資料結構的 Persist 性質跟 Python 有點類似。需要注意的是,Swift 有變數和常量兩種概念, 變數使用var
宣告,常量使用let
宣告,使用var
宣告的時候,Swift 中的字串的行為跟 Python 相似, 因此修改字串可以被理解為生成了一個新的字串並修改了指標。同樣, 使用var
宣告的陣列和字典也都是可變的。
在 Swift 中使用let
宣告的物件不能被賦值,基本資料結果也會變得不可變,但是情況更復雜一點。
1 2 3 4 5 6 |
let aDict = ["k1":"v1"] let anArray = [1, 2, 3, 4] aDict["k1"] = "newVal" // !! will fail !! anArray.append(5) // !! will fail !! anArray[0] = 5 // anArray = [5, 2, 3, 4] now ! |
從上面的程式碼中可以看出,使用let
宣告的字典是完全不可變的,但是陣列雖然不可以改變長度, 卻可以改變陣列元素的值!Swift 的文件中指出這裡其實是將 Array 理解為定長陣列從而方便編譯優化, 來獲得更好的訪問效能。
綜上所述,物件是否可變的關係其實略有複雜的,可以總結為:
- 使用
var
和let
,Int
和String
型別都是不可變的,但是var
時可以對變數重新賦值 - 使用
let
宣告的常量不可以被重新賦值 - 使用
let
宣告的Dictionary
是完全不可變的 - 使用
let
宣告的Array
長度不可變,但是可以修改元素的值 - 使用
let
宣告的類物件是可變的
綜上所述,即使是使用let
宣告的物件也有可能可變,因此在多執行緒情況下就無法達到“無副作用”的要求了。
此外 Swift 的函式雖然沒有指標,但是仍通過引數來修改變數的。只要在函式的引數定義中加入inout
關鍵字即可。 這個特性很有 C 的風格。
個人覺得在支援通過元組來實現多返回值的情況下,這個特性不但顯得雞肋,也是一個導致程式產生“副作用”的特性。 Swift 支援這樣的特性,恐怕更多的是為了相容 Objective-C 以及方便在兩個語言之間搭建 Bridge。
1 2 3 4 5 |
func inc(inout a:Int) { a += 1 } var num = 1 inc(&num) // num = 2 now! |
綜上所述,使用 Swift 自帶的資料結構並不能很好的實現“無副作用”的“純函式式”程式設計, 它並沒有比 Python、Ruby 這類語言走的更遠。幸好作為一種關注度很高的語言, 已經有開發者為其實現了一套完全滿足不可變要求的資料結構和庫:Swiftz。 堅持使用let
和 Swiftz 提供的資料結構來操作,就可以實現“純函式式”程式設計。
總結
在我看來,Swift 雖然實現了很多其他語言的亮點特性,但是總體實現來說並不是很整齊。 它在函數語言程式設計方面新增了很多特性,但在控制副作用方面僅能達到平均水準。 有些特性看起來像是為了相容原來的 Objective-C 才加入的。
Swift 寫起來相對比 Objective-C 更方便一點,脫離 Xcode 這樣的 IDE 來寫也是應該是可以的。 目前 Swift 只支援集中少量的原生資料結構而沒有標準庫,更不具備跨平臺特性,這是一個缺點。 在仔細閱讀了文件之後發現 Swift 本身的語法細節還是很多的,就比如switch
分置語句的用法就有很多內容。 入門學習的容易程度並沒有原來想象的那麼好。我個人並不覺得這門語言會對其他平臺的開發者有很大吸引力。
Swift 是一門很強大的語言,在其穩定版本釋出之後我認為我會從 Objective-C 專向 Swift 來進行程式設計, 它在未來很可能成為 iOS 和 Mac 開發的首選。