【函數式 Swift】封裝 Core Image

前一章《函數式思想》中,咱們學習了函數式編程思想,即,經過構建一系列簡單、實用的函數,再「裝配」起來解決實際問題。本章藉助一個封裝 Core Image 的案例進一步練習函數式思想的應用。javascript

本章關鍵詞

請帶着如下關鍵詞閱讀本文:java

  • 高階函數
  • 柯里化

案例:封裝 Core Image

本章案例是對 Core Image 中的 CIFilter 進行再次封裝。開始看到這一章時,我就有疑問:爲何會選擇這個不是很經常使用,但已經功能很是全面的 API 做爲封裝對象呢?後來想一想,封裝這種看上去沒什麼可封裝的 API 恰好可讓咱們體會到函數式思想的內涵,咱們甚至能夠不去關注函數的具體實現(下文中,我將會省略部分函數內部的具體實現,僅關注如何封裝高質量函數)。git

若是你不瞭解 CIFilter 的用途,能夠先來看看下面這段代碼和效果:github

let image = UIImage(named: "graynoel")

guard image != nil else { // 1
    fatalError()
}
let originImage = CIImage(image: image!)
let filter = CIFilter(name: "CIGaussianBlur") // 2

guard filter != nil else { // 3
    fatalError()
}
filter!.setValue(originImage, forKey: kCIInputImageKey) // 4
filter!.setValue(5.0, forKey: kCIInputRadiusKey) // 5

guard filter!.outputImage != nil else { // 6
    fatalError()
}
let outputImage = UIImage(ciImage: filter!.outputImage!)複製代碼

代碼邏輯比較簡單:讀入一個 UIImage 對象,轉換爲 CIImage 實例,而後經過 Core Image 中的 CIFilter API 對其進行「高斯模糊」處理。咱們不用去關心實現邏輯,只看看這段代碼自己的問題:編程

  1. 防護型代碼不少:註釋 一、三、6 處;
  2. 使用字符串配置濾鏡類型:註釋 2 處;
  3. 使用鍵值方式配置濾鏡參數:註釋 四、5 處。

這些問題當咱們僅處理少許圖片時彷佛尚可接受,但大量使用時代碼就變得複雜了。swift

如何優化呢?很容易想到的解決辦法就是把須要的濾鏡變化封裝成函數,一個可能的函數封裝方式以下:api

func blur(originImage: CIImage, radius: Double) -> CIImage {
    let outputImage: CIImage
    ...
    return outputImage
}複製代碼

這樣作能夠達到效果,但是並無使用函數式思想,那函數式思想指導下的解決方案是什麼樣呢?ide

藉助前一章《函數式思想》的結論,首先應該選擇一個合適的類型,而合適的類型直接取決於所要實現功能的輸入和輸出,很明顯,輸入、輸出都是 CIImage,那麼,咱們能夠獲得如下函數式定義:函數式編程

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

若是您讀過前一章的內容,看到這句定義後,應該會有一種豁然開朗的感受。此時,咱們的目標發生了變化,再也不是爲了實現某一些具體的濾鏡方法,而是構建濾鏡相關的「工具庫」,爲一切高階函數封裝提供可能。函數

有了這個認識,咱們從新定義 blur 函數,並與上文中的函數進行對比:

// after
func blur(radius: Double) -> Filter {
    return { image in
        let outputImage: CIImage
        ...
        return outputImage
    }
}

// before
func blur(originImage: CIImage, radius: Double) -> CIImage {
    let outputImage: CIImage
    ...
    return outputImage
}複製代碼

基於這個思路,咱們能夠封裝不少相似的函數:

func colorGenerator(color: UIColor) -> Filter {
    return { _ in
        let c = CIColor(color: color)
        let parameters = [kCIInputColorKey: c]
        guard let filter = CIFilter(name: "CIConstantColorGenerator", withInputParameters: parameters) else {
            fatalError()
        }
        guard let outputImage = filter.outputImage else { fatalError() }
        return outputImage
    }
}


func compositeSourceOver(overlay: CIImage) -> Filter {
    return { image in
        let parameters = [
            kCIInputBackgroundImageKey: image,
            kCIInputImageKey: overlay
        ]
        guard let filter = CIFilter(name: "CISourceOverCompositing", withInputParameters: parameters) else {
            fatalError()
        }
        guard let outputImage = filter.outputImage else { fatalError() }
        let cropRect = image.extent
        return outputImage.cropping(to: cropRect)
    }
}複製代碼

獲得這個小型工具庫以後,咱們能夠很是便捷的「裝配」出複雜的濾鏡效果函數:

func colorOverlay(color: UIColor) -> Filter {
    return { image in
        let overlay = colorGenerator(color: color)(image)
        return compositeSourceOver(overlay: overlay)(image)
    }
}複製代碼

咱們來用一下試試:

let image = UIImage(named: "graynoel")
let originImage = CIImage(image: image!)!

// 高斯模糊
let radius = 5.0
let blurredImage = blur(radius: radius)(originImage)

// 對 blurredImage 再疊加顏色濾鏡、合成濾鏡
let overlayColor = UIColor.red.withAlphaComponent(0.2)
let outputImage = colorOverlay(color: overlayColor)(blurredImage)複製代碼

效果以下:

效果已經達成!爲了方便,對於上面這種將兩種濾鏡組合應用的狀況,能夠直接嵌套的來寫:

let outputImage = colorOverlay(color: overlayColor)(blur(radius: radius)(originImage))複製代碼

複雜的括號嵌套已經讓代碼失去了可讀性,如何優化呢?先來了解一個概念:柯里化(Currying),摘錄一段維基中的解釋:

In functional programming languages, and many others, it provides a way of automatically managing how arguments are passed to functions and exceptions.

簡單來講,就是把接受多個參數的方法進行一些變形,使其更加靈活的方法。咱們借用王巍(@onevcat)在其《100個Swift開發必備Tip》一書中柯里化 (CURRYING) 章節的例子進行介紹:

// 數字 +1 的函數
func addOne(num: Int) -> Int {
    return num + 1
}複製代碼

若是咱們還須要 +二、+三、…… 的函數,這樣的寫法就沒法擴展了,所以能夠藉助柯里化,用以下方式進行定義:

func addNum(adder: Int) -> (Int) -> Int {
    return { num in
        return num + adder
    }
}複製代碼

能夠看到,函數返回值變成了 (Int) -> Int,這不正是一等函數的應用嗎!咱們再使用時,就能夠採用以下方法:

// 方式 1
let addTwo = addNum(adder: 2)
addTwo(1) // 3

// 方式 2
addNum(adder: 2)(3) // 5複製代碼

方式 1 中的 addTwo 就是咱們基於 addNum 函數「裝配」出的高階函數,而方式 2 的寫法則更簡潔。

讓咱們再回到濾鏡組合應用的問題,藉助柯里化,咱們定義如下函數:

func composeFilters(filter1: @escaping Filter, _ filter2: @escaping Filter) -> Filter {
    return { image in
        filter2(filter1(image))
    }
}

// 調用
let composeTwoFilters = composeFilters(filter1: blur(radius: radius), colorOverlay(color: overlayColor))
let outputImage = composeTwoFilters(originImage)複製代碼

composeFilters 函數能夠將兩個 Filter 進行疊加,生成一個新的 Filter,而後傳入 originImage 便可獲得最終結果。

原書章節還講解了引入運算符的方式進行代碼改寫,思路相似,本文再也不展開。


思考

高階函數

經過兩章的學習,咱們對高階函數(Higher-order function)已經有了一些瞭解,下面簡單總結一下。首先,仍是先看看維基對高階函數的定義:

In mathematics and computer science, a higher-order function (also functional, functional form or functor) is a function that does at least one of the following:

  • takes one or more functions as arguments (i.e., procedural parameters),
  • returns a function as its result.

高階函數知足兩個條件:

  1. 以一個或多個函數做爲參數;
  2. 將一個函數做爲返回值。

對比前文的代碼,composeFilters 函數就是一個高階函數,它以兩個 Filter 函數做爲參數,組合後的濾鏡函數做爲返回值。

高階函數應該與函數式思想相結合,函數式思想幫助咱們獲得一系列「小型」函數,而後「裝配」成複雜的高階函數,對功能進行擴展。一樣的,高階函數仍然能夠做爲「小型」函數,繼續「裝配」更高階的函數,這種「裝配」的便捷性和擴展性就得益於函數式編程方法。

柯里化

柯里化是本章新引入的概念,經過前文代碼示例能夠看出,藉助柯里化,咱們能夠開發出用於「量產函數」的函數,就如同開發了一個函數的模板,能夠生成各類相似功能的函數,而且避免寫出不少重複代碼,也方便了後續維護。

可是,對於柯里化的應用是須要進行設計的,反覆嵌套或連接過多括號會嚴重影響代碼的可讀性,使用時,要根據具體問題適度柯里化,提升開發效率和代碼擴展性的同時,避免濫用致使的可讀性降低。


參考資料

  1. Github: objcio/functional-swift
  2. Currying
  3. 柯里化 (CURRYING)
  4. Higher-order function

本文屬於《函數式 Swift》讀書筆記系列,同步更新於 huizhao.win,歡迎關注!

相關文章
相關標籤/搜索