Swift的一次函數式之旅

本文首發於搜狐產品技術公衆號,如今是同步發佈在個人掘金上,你們看完有問題能夠留留言討論討論。java

本文適合哪些人?

本文針對的是已經有一部分Swift開發的基礎,可是對函數式範式比較感興趣的開發者。固然,若是隻對函數式範式感興趣,我以爲這篇文章也值得一看。react

函數式編程是什麼?

首先來看這個詞語」Functional Programming「,它是什麼?程序員

當須要去查一個專業術語的定義的時候,個人第一反應是來查詢Wikipedia:spring

In computer science, fucnitonal programming is a programming paradigm where programs are constructed by applying and composing fucntions.編程

在這個定義裏,有一個很熟悉的詞——programming paradigm, 通常翻譯爲編程範式,但是我對這個翻譯仍是有些迷糊,因而我又在wikipedia中查找這個詞語的含義:swift

Programming paradigms are a way to classify programming languages based on their features. 編程範式(編程範例)是一種基於語言自身的特性來給編程語言分類的方式。api

同時wikipedia中還總結了常見的編程範式的分類:markdown

  • imperative
    • procedural
    • object-oriented
  • declarative
    • functional
    • logic
    • mathematical
    • reactive

那麼究竟什麼是編程範式呢?咱們知道編程是一門工程學,它的目的是去解決問題,而解決問題能夠有不少的方法,編程範例就是表明着解決問題的不一樣思路。若是說咱們是編程世界的造物主的話,那麼編程範例應該就是咱們創造這個世界的方法論。因此我很是喜歡臺灣那邊對programming paradigm 的翻譯:程式設計法。網絡

爲何我要強調編程範例是什麼東西,並且還分門別類的列舉了出來這些編程範例呢?app

由於編程自己是抽象的,編程範例其實就是咱們如何抽象這個世界的方法,我只是想經過這個具體的定義來講明**函數式自己就是一種方法論。**因此咱們學習的時候不必懼怕它,遇到引用透明,反作用,科裏化,函子,單子,惰性求值等等等等這些概念的時候,畏懼的緣由只是不熟悉而已,就想咱們學習面向對象的時候:繼承,封裝,多態,動態綁定,消息傳遞等等等等,這些概念咱們一開始也不熟悉,因此當咱們熟悉了函數式這些概念的時候,一切天然水到渠成。 ** 在咱們熟悉的面向對象的編程範式中,咱們知道它的思想是:一切皆對象,而在純函數式的編程範式中,能夠說:一切皆函數。在函數式編程中,函數是一等公民,那什麼是一等公民呢?就是它能夠做爲參數,返回值,也能夠賦值給變量,也就是說它的地位實際上是和Int,String, Double等基本類型是同樣的,換言之,要像使用基本類型同樣去使用它!

不一樣的思想就是建立世界的方法論的不一樣之處,這裏我舉個例子,那就是狀態,好比登陸的各類狀態,維護狀態會大大增長系統的複雜性,特別是狀態不少的時候,並且引入狀態這個概念以後,會帶來不少複雜的問題:狀態持久化,環境模型等等等,而若是使用面向對象的編程範例,能夠將**每個狀態都定義爲一個對象,**如C#中的狀態機的實現,而在函數式編程裏呢?**在SICP中提到,狀態是隨着時間改變的,因此狀態是否可使用f(t)來表示呢?**這就是使用函數式的思路來抽象狀態。

固然,我這裏並非說只能使用一種編程範式,我也並不鼓吹函數式就一直是好的,可是掌握函數式可讓咱們在解決問題的時候提供更多的選擇,更有效率的解決問題,事實上,咱們解決問題(創造世界)確定會使用不少種方法論即多種編程範式,通常狀況下,更現代的編程語言都支持多範式編程,這裏用swift裏的RxSwift來舉例:

public class Observable<Element> : ObservableType {
    internal init()
    
    public func subscribe<Observer>(_ observer: Observer) -> Disposable where Element == Observer.Element, Observer : RxSwift.ObserverType

    public func asObservable() -> Observable<Element>
}

// 觀察者
final internal class AnonymousObserver<Element> : ObserverBase<Element> {

    internal typealias EventHandler = (Event<Element>) -> Void

    internal init(_ eventHandler: @escaping EventHandler)

    override internal func onCore(_ event: Event<Element>)
}



extension ObservableType {
    public func flatMap<Source>(_ selector: @escaping (Element) throws -> Source) -> Observable<Source.Element> where Source : RxSwift.ObservableConvertibleType
}

extension ObservableType {
    public func map<Result>(_ transform: @escaping (Element) throws -> Result) -> Observable<Result>
}
複製代碼

它的Observable和Observer都抽象成了類,而且添加了相應的行爲,承擔了相應的職責,這是面向對象範式;它實現了OberveableType協議,而且拓展了該協議,添加了大量的默認實現,這是面向協議範式;它實現了map,和flatMap方法,能夠說Observable是一個函數單子(Monad),同時也提供了大量的操做符可供使用和組合,這是函數式範式;同時,總所周知,Reactive框架是一個響應式的框架,因此它也是響應式範式......

更況且,編程能力不就是抽象能力的體現嗎?因此我認爲掌握函數式是很是必要的!那麼具體來講爲何重要呢?

在1984年的時候,John Hughes 有一篇很著名的論文《Why Functional Programming Matters》, 它解答了咱們的疑問。

爲何函數式編程重要?

一般網絡上的一些文章都會總結它的優勢:它沒有賦值,沒有反作用,沒有控制流等等等等,不一樣的只是它們對於各個關鍵詞諸如引用透明,無反作用的種種解釋,單是這只是列出了不少函數式程序**"沒有"什麼,卻沒有說它「有」什麼,因此這些優勢其實沒有太大的說服力。並且咱們實際上去寫程序的時候,也不可能特地去寫一個缺乏了賦值語句或者特別引用透明**的程序,這也不是衡量質量的尺度,那麼真正重要的是什麼呢?

在這篇論文中提到,模塊化設計是成功的程序化設計的關鍵,這一觀點已經被廣泛接受了,但有一點常常容易被忽略,那就是編寫一個模塊化程序解決問題的時候,程序員首先要把問題分解爲子問題,而後解決這些子問題並把解決方案合併。程序員可以以什麼方式分解問題,直接取決於他能以什麼方式把解決方案粘起來。而函數式範式其實提供給咱們很是重要的粘合劑,它可讓咱們設計一些更小、更簡潔、更通用的模塊,同時使用黏合劑粘合起來。

那麼它提供了哪些黏合劑呢?這篇論文介紹了兩種:

黏合函數:高階函數

The first of the two new kinds of glue enables simple functions to be glued together to make more complex ones.

黏合簡單的函數變爲更復雜的函數。這樣的好處是咱們模塊化的顆粒度是更細的,能夠組合的複雜函數也是更多的。若是非要作一個比喻的話,我以爲就像樂高的基礎組件: 截屏2021-03-18 上午11.13.02.png 這種聚合就是一個泛化的高階函數和一些特化函數的聚合,這樣的高階函數一旦定義,不少操做均可以很容易地編寫出來。

黏合程序:惰性求值

The other new kind of glue that functional languages provide enables whole programs to be glued together.

函數式語言提供的另外一種黏合劑就是可使得程序黏在一塊兒。假設有這麼一個函數:

g(f(input))
複製代碼

傳統上,須要先計算f,而後再計算g,這是經過將f的輸出存儲在臨時文件中實現的,這種方法的問題是臨時文件會佔用太大的空間,會讓程序之間的黏合變得不太現實。而函數式語言提供的這一種解決方案,程序f和g嚴格的同步運行,只有當g視圖讀取輸入時,f才啓動。這種求值方式儘量得少運行,所以被稱爲**"惰性求值"**。它將程序模塊化爲一個產生大量可能解的生成器與一個選取恰當解的選擇器的方案變得可行。

你們若是有時間仍是應該去讀讀這一篇論文,在論文中,它講述了三個實例:牛頓-拉夫森求根法,數值微分,數值積分,以及啓發性搜索,並使用函數式來實現它們,很是的精彩,這裏我就不復述這些實例了。最後我再引用一下該論文的結論:

在本文中,咱們指出模塊化是成功的程序設計的關鍵。以提升生產力爲目標的程序語言,必須良好地支持模塊化程序設計。可是,新的做用域規則和分塊編譯的技巧是不夠的——「模塊化」不只僅意味着「模塊」。咱們分解程序的能力直接取決於將解決方案粘在一塊兒的能力。爲了協助模塊化程序設計,程序語言必須提供優良的黏合劑。函數式程序語言提供了兩種新的黏合劑——高階函數惰性求值

一顆棗樹(例子)

這個例子我參考了Objc.io的《函數式Swift》書籍中關於如何使用函數式的方式來封裝濾鏡的案例。

Core Image是一很強大的圖像處理框架,可是它的API是弱類型的 —— 能夠經過鍵值編碼來配置圖像濾鏡,這樣就致使很容易出錯,因此可使用類型來避免這些緣由致使的運行時錯誤,什麼意思呢?就是說咱們能夠封裝一些基礎的濾鏡**Filter, **而且還能夠實現它們之間的聚合方式。這就是上述論文中介紹的函數式編程提供的黏合劑之一:使簡單的函數能夠聚合起來造成複雜的函數。

首先肯定咱們的濾鏡類型,該函數應該接受一個圖像做爲參數並返回一個新的圖像:

typalias Filter = (CIImage) -> CIImage
複製代碼

在這裏引用一段書中的原話:

咱們應該謹慎地選擇類型。這比其餘任何事情都重要,由於類型將左右開發流程。

而後能夠開始定義函數來構件特定的基礎濾鏡了:

/// sobel提取邊緣濾鏡
func sobel() -> Filter {
    return { image in
        let sobel: [CGFloat] = [-1, 0, 1, -2, 0, 2, -1, 0, 1]
        let weight = CIVector(values: sobel, count: 9)
        guard let filter = CIFilter(name: "CIConvolution3X3",
                                    parameters: [kCIInputWeightsKey: weight,
                                                 kCIInputBiasKey: 0.5,
                                                 kCIInputImageKey: image]) else { fatalError() }
        
        guard let outImage = filter.outputImage else { fatalError() }
        
        return outImage.cropped(to: image.extent)
    }
}

/// 顏色反轉濾鏡
func colorInvert() -> Filter {
    return { image in
        guard let filter = CIFilter(name: "CIColorInvert",
                                    parameters: [kCIInputImageKey: image]) else { fatalError() }
        guard let outImage = filter.outputImage else { fatalError() }
        return outImage.cropped(to: image.extent)
    }
}


/// 顏色變色濾鏡
func colorControls(h: NSNumber, s: NSNumber, b: NSNumber) -> Filter {
    return { image in
        guard let filter = CIFilter(name: "CIColorControls", parameters: [kCIInputImageKey: image, kCIInputSaturationKey: h, kCIInputContrastKey: s, kCIInputBrightnessKey: b]) else { fatalError() }
        
        guard let outImage = filter.outputImage else { fatalError() }
        
        return outImage.cropped(to: image.extent)
    }
}
複製代碼

直接黏合

基礎組件已經有了,接下來就能夠堆積木了。若是有一個濾鏡須要:先提取邊緣 -> 顏色反轉 -> 顏色變色,那麼咱們能夠實現以下:

let newFilter: Filter = { image in
    return colorControls(h: 97, s: 8, b: 85)(colorInvert()(sobel()(image)))
}
複製代碼

上述作法有一些問題:

  • 可讀性差:沒法代碼即註釋,沒法很容易的知道濾鏡的執行順序
  • 不易拓展:API不友好,添加新的濾鏡時,須要考慮順序和括號,很容易出錯

自定義函數黏合

首先咱們解決可讀性差的問題,由於直接使用嵌套調用方法,因此會可讀性差。因此咱們要避免嵌套調用,直接定義combine方法來組合濾鏡:

func compose(filter filter1: @escaping Filter, with filter2: @escaping Filter) -> Filter {
    return { image in
        filter2(filter1(image))
    }
}

// sobel -> invertColor
let newFilter1: Filter = compose(sobel(), colorInvert()) // 左結合的
複製代碼

這是左結合的,因此可讀性是OK的,可是若是有三個濾鏡組合呢?四個濾鏡組合呢?要定義那麼多方法嗎? 巧了,還真有人是這麼幹的: 截屏2021-03-18 下午3.03.41.png 若是你們去看RxSwift的話,就會看見它組合多個Observable的函數: zip , combineLastest ,每個方法簇都提供了支持多個參數的組合方法,但是這就意味着咱們在這個案例也是能夠這樣作的,可是這顯然不是最好的解決方案。

若是使用combine這裏三個濾鏡組合的方案:

let newFilter2: Filter = compose(compose(sobel(), colorInvert()), colorControls(h:97, s:8, b:85)))
複製代碼

可讀性還行,可是仍是在添加新的濾鏡的時候容易出錯,不那麼容易拓展。若是要再組合多個濾鏡,那麼就須要多個combine函數嵌套調用。

自定義操做符黏合

若是對應到數學領域的話,其實這幾個濾鏡的組合不就是四則運算中的 +  嗎?一層一層效果的疊加,固然,確切地說,從效果上和 + 更類似,可是從特性來講更符合減法 - 的,都是向左結合,並且都不知足交換律。

因此咱們能夠自定義操做符來處理濾鏡的結合:

infix operator >>>
func >>>(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
    return { image in
        filter2(filter1(image))
    }
}
複製代碼

固然還有一個小問題,就是若是有三個濾鏡組合的話,會報錯,由於咱們沒有指定它組合的方式(左結合,仍是右結合)因此這裏咱們讓它繼承加法的優先級,由於它和加法同樣都是左結合的:

infix operator >>>: AdditionPrecedence // 讓它繼承+操做符的優先級, 左結合
func >>>(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
    return { image in
        filter2(filter1(image))
    }
}
複製代碼

那接下來咱們愉快地使用它吧:

let filter = sobel() >>> colorInvert() >>> colorControls(h: 97, s: 8, b: 85)
let outputImage = filter(inputImage)
imageView.image = UIImage(ciImage: outputImage)
複製代碼

函數式Swift.001.jpeg

那麼這裏來總結一下這一波過程,假設需求是存在的:

咱們定義了不少基礎濾鏡層(Filter),接下來確定須要組合基礎濾鏡爲咱們實際需求須要的濾鏡,有的濾鏡多是有三個基礎濾鏡組合的,有的須要五個基礎濾鏡組合,固然極限狀況下,可能還有須要十個濾鏡組合的。

因此咱們須要定義不一樣濾鏡組合的**黏合函數,**咱們一共經歷了三個組合方案的變遷:

  1. 直接組合
  2. 定義compose函數
  3. 自定義操做符

固然,諸君也可使用更好的組合方案,若是能夠但願留個言,共同探討探討。

還有一顆也是棗樹(例子)

接下來這個例子,是一個咱們使用Objective-C編程的時候常常會遇到的問題,需求以下:第二行數據必須等待第一行請求結束以後才能夠開始請求。 截屏2021-03-31 上午9.58.51.png

那麼開始吧!

首先咱們來看最容易的實現方案:

@objc func syncData() {
        self.statusLabel.text = "正在同步火影忍者數據"
        
        WebAPI.requestNaruto { (firstResult) in
            if case .success(let result) = firstResult {
                self.sectionOne = result.map { $0 as? String ?? "" }
                DispatchQueue.main.async {
                    self.tableView.reloadSections([0], with: .automatic)
                    
                    self.statusLabel.text = "正在同步海賊王數據"
                    WebAPI.requestOnePiece { (secondResult) in
                        if case Result.success(let result) = secondResult {
                            self.sectionTwo = result.map { $0 as? String ?? "" }
                            DispatchQueue.main.async {
                                self.statusLabel.text = "同步海賊王數據成功"
                                self.tableView.reloadSections([1], with: .automatic)
                            }
                        }
                    }
                }
            }
        }
    }
複製代碼

熟悉嗎?固然熟悉,直接在第一個請求的callback中直接進行第二個請求,可是請注意,這和OC寫的有區別嗎?咱們這樣和寫和簡單的人肉翻譯機有區別嗎?咱們寫的是Swift這個多範式的編程語言嗎?

回到例子,咱們就事論事,我以爲這樣寫會有幾個問題:

  1. 數據修改和UI修改耦合在了一塊兒
  2. 多重嵌套
  3. 違背了OCP(Open Closed Principle)法則:應該對修改閉合,對拓展開放
  4. 醜!

解決數據和UI耦合

從重要性的角度,我以爲應該先解決第4個問題,可是出於節奏,咱們仍是從第一個問題開始解決吧~

@objc func syncDataThere() {
        // 嵌套函數
        func updateStatus(text: String, reload: (isReload: Bool, section: Int)) {
            DispatchQueue.main.async {
                self.statusLabel.text = text
                if reload.isReload { self.tableView.reloadSections([reload.section], with: .automatic) }
            }
        }
        
        updateStatus(text: "正在同步火影忍者數據", reload: (false, 0))
        
        requestNaruto {
            updateStatus(text: "正在同步海賊王數據", reload: (true, 0))
            self.requestOnePiece {
                updateStatus(text: "同步數據成功", reload: (true, 1))
            }
        }
    }
複製代碼

這裏我把網絡請求和數據處理都封裝到了網絡請求中,並且使用了swift的特性:嵌套函數,剝離了一部分重複代碼,這樣整個請求就變得很是清晰明瞭了,並且數據和UI就隔離開來了,並無耦合在一塊兒。

但是嵌套的問題仍是存在,如何解決呢?

解決多重嵌套

還記得我介紹的第一棵棗樹嗎?我使用了自定義操做符來解決了函數調用的嵌套,這裏其實也是同樣的思路,可是要更復雜些。

這裏我還須要重複引用一下《函數式Swift》中的那句話:

咱們應該謹慎地選擇類型。這比其餘任何事情都重要,由於類型將左右開發流程。

第一步抽象

這裏有兩個類型須要抽象,第一是執行單個語句的函數(這裏是更新UI),第二個是對應網絡請求的函數

infix operator ->> AdditionPrecedence
typealias Action = () -> Void
typealias Request = (@escaping Action) -> Void
複製代碼

第二步抽象

那麼如何將原來的函數拆解爲使用類型表示的函數呢?

func syncDataF() {
    ......
	requestNaruto {
    	updateStatus(text: "正在同步海賊王數據", reload: (true, 0))
        self.requestOnePiece {
        	updateStatus(text: "同步數據成功", reload: (true, 1))
        }
	}
)
複製代碼

咱們由上往下,那麼抽象的過程應該就是

  • (Request, Action) -> Request

第一個請求 和 回調中的第一個Action,可是第一個請求尚未結束,因此返回的仍是Request

  • (Request, Request) -> Request

處理了第一個Action的第一請求 + 第二個請求, 可是請求仍是沒有結束,因此返回的仍是Request

  • (Request, Action) -> Action

第二個請求加上最後須要處理的Action , 完畢!

因此結果以下:

@objc func syncDataFour() {
	func updateStatus(text: String, reload: (isReload: Bool, section: Int)) {
     	DispatchQueue.main.async {
        	self.statusLabel.text = text
            if reload.isReload { 
                self.tableView.reloadSections([reload.section], with: .automatic) 
            }
        }
    }
    updateStatus(text: "正在同步火影忍者數據", reload: (false, 0))
    // 咱們來拆解一下函數:要把函數抽象出來,這一點很是的重要
    // (Request, Action) -> Request
    // (Request, Request) -> Request
    // (Request, Action) -> Action
    // 經過這樣的拆解方式就能夠開始定義方法了
    let task: Action =
     	requestNaruto
            ->> { updateStatus(text: "正在同步海賊王數據", reload: (true, 0)) }
            ->> requestOnePiece
            ->> { updateStatus(text: "同步數據成功", reload: (true, 1)) }
    task()
}
複製代碼

結果呢?我解決了嵌套的問題,很好,很完美,但是也很天真。

解決OCP問題

即便咱們使用了自定義操做符,也沒有解決OCP問題,由於若是咱們要添加請求的話,咱們仍是須要修改原來的方法,依然違背了OCP法則。

那麼怎麼解決呢?

嗯嗯,具體的,請各位本身去試驗吧!

我在文章尾部添加了相應的引用信息,這個例子是基於2016年的國內的Swift大會中翁陽的分享《Swift, 改善既有代碼的設計》,若是有時間,但願你們能夠去看看這個分享。

在分享中,他使用了面向協議的思路解決了OCP問題,很抽象,很精彩。

總結

很開心諸位看到了這裏,我以爲這篇文章的能量密度應該不會浪費大家的時間。

在這邊文章中,我首先是追問了函數式編程,以及編程範式的定義,只是想告訴你們:函數式編程之因此複雜只是由於咱們不熟悉,同時它也應該是咱們必須的工具。

而後我介紹了《Why Functional Programming Matters》這篇論文,它說明了爲何函數式編程重要,提到函數式範式的兩大武器:高階函數和惰性求值。

最後我使用了兩顆棗樹來給你們看一看Swift語言結合函數式的思想能夠有哪些奇妙的化學反應。

那麼這一次Swift的一次函數式之旅就結束了。可是仍是想補充幾句,每年的WWDC其實Swift都更新了不少的內容,Swift自己也一直在增長新的特性,一直在穩健的迭代着,若是咱們仍是使用Objective-C的思惟去寫Swift的話,其實自己是落後於語言發展的。

最後引用王安石的《遊褒禪山記》中的一段話:

而世之奇偉、瑰怪,很是之觀,常在於險遠,而人之所罕至焉,故非有志者不能至也。

與君共勉!

引用

  1. wikipedia. "Functional programming".(en.wikipedia.org/wiki/Functi…)
  2. wikipedia. "Programming paradigm". (en.wikipedia.org/wiki/Progra…)
  3. John Hughes. "Why Functional Programming Matters".(PDF) (www.cs.rice.edu/~javaplt/41…)
  4. objc. "Functional Swift".(eBook)(objccn.io/products/fu…)
  5. 翁陽. "Swift, 改善既有代碼的設計".(Video)(www.youtube.com/watch?v=z4r…)
  6. 包函卿. "Swift函數式實踐".(Video)(www.youtube.com/watch?v=lf9…)
  7. ScottWlaschin. "The Functional ToolKit".(Video)(www.bilibili.com/video/BV1ex…)
相關文章
相關標籤/搜索