1. 程式人生 > >Swift 的函式式 API

Swift 的函式式 API

在過去的時間裡,人們對於設計 API 總結了很多通用的模式和最佳實踐方案。一般情況下,我們總是可以從蘋果的 Foundation、Cocoa、Cocoa Touch 和很多其他框架中總結出一些開發中的範例。毫無疑問,對於“特定情境下的 API 應該如何設計”這個問題,不同的人總是有著不同的意見,對於這個問題有很大的討論空間。不過對於很多 Objective-C 的開發者來說,對於那些常用的模式早已習以為常。

隨著 Swift 的出現,設計 API 引起了更多的問題。絕大多數情況下,我們只能繼續做著手頭的工作,然後把現有的方法翻譯成 Swift 版本。不過,這對於 Swift 來說並不公平,因為和 Objective-C 相比,Swift 添加了很多新的特性。引用 Swift 創始人

Chris Lattner 的一段話:

Swift 引入了泛型和函數語言程式設計的思想,極大地擴充套件了設計的空間。

在這篇文章裡,我們將會圍繞 Core Image 進行 API 封裝,以此為例,探索如何在 API 設計中使用這些新的工具。 Core Image 是一個功能強大的影象處理框架,但是它的 API 有時有點笨重。 Core Image 的 API 是弱型別的 - 它通過鍵值對 (key-value) 設定影象濾鏡。這樣在設定引數的型別和名字時很容易失誤,會導致執行時錯誤。新的 API 將會十分的安全和模組化,通過使用型別而不是鍵值對來規避這樣的執行時錯誤。

目標

我們的目標是構建一個 API ,讓我們可以簡單安全的組裝自定義濾鏡。舉個例子,在文章的結尾,我們可以這樣寫:

let myFilter = blur(blurRadius) >|> colorOverlay(overlayColor)
let result = myFilter(image)

上面構建了一個自定義的濾鏡,先模糊影象,然後再新增一個顏色蒙版。為了達到這個目標,我們將充分利用 Swift 函式是一等公民這一特性。專案原始碼可以在 Github 上的這個示例專案中下載。

Filter 型別

CIFilterCore Image 中的一個核心類,用來建立影象濾鏡。當例項化一個 CIFilter 物件之後,你 (幾乎) 總是通過 kCIInputImageKey 來輸入影象,然後通過 kCIOutputImageKey

獲取返回的影象,返回的結果可以作為下一個濾鏡的引數輸入。

在我們即將開發的 API 裡,我們會把這些鍵值對 (key-value) 對應的真實內容抽離出來,為使用者提供一個安全的強型別 API。我們定義了自己的濾鏡型別 Filter,它是一個可以傳入圖片作為引數的函式,並且返回一個新的圖片。

typealias Filter = CIImage -> CIImage

這裡我們用 typealias 關鍵字,為 CIImage -> CIImage型別定義了我們自己的名字,這個型別是一個函式,它的引數是一個 CIImage ,返回值也是 CIImage 。這是我們後面開發需要的基礎型別。

如果你不太熟悉函數語言程式設計,你可能對於把一個函式型別命名為 Filter 感覺有點奇怪,通常來說,我們會用這樣的命名來定義一個類。如果我們很想以某種方式來表現這個型別的函式式的特性,我們可以把它命名成 FilterFunction 或者一些其他的類似的名字。但是,我們有意識的選擇了 Filter 這個名字,因為在函數語言程式設計的核心哲學裡,函式就是值,函式和結構體、整數、多元組、或者類,並沒有任何區別。一開始我也不是很適應,不過一段時間之後發現,這樣做確實很有意義。

構建濾鏡

現在我們已經定義了 Filter 型別,接下來可以定義函式來構建特定的濾鏡了。這些函式需要引數來設定特定的濾鏡,並且返回一個型別為 Filter 的值。這些函式大概是這個樣子:

func myFilter(/* parameters */) -> Filter

注意返回的值 Filter 本身就是一個函式,在後面有利於我們將多個濾鏡組合起來,以達到理想的處理效果。

為了讓後面的開發更輕鬆一點,我們擴充套件了 CIFilter 類,添加了一個 convenience 的初始化方法,以及一個用來獲取輸出影象的計算屬性:

typealias Parameters = Dictionary<String, AnyObject>

extension CIFilter {

    convenience init(name: String, parameters: Parameters) {
        self.init(name: name)
        setDefaults()
        for (key, value : AnyObject) in parameters {
            setValue(value, forKey: key)
        }
    }

    var outputImage: CIImage { return self.valueForKey(kCIOutputImageKey) as CIImage }

}

這個 convenience 初始化方法有兩個引數,第一個引數是濾鏡的名字,第二個引數是一個字典。字典中的鍵值對將會被設定成新濾鏡的引數。我們 convenience 初始化方法先呼叫了指定的初始化方法,這符合 Swift 的開發規範。

計算屬性 outputImage 可以方便地從濾鏡物件中獲取到輸出的影象。它查詢 kCIOutputImageKey 對應的值並且將其轉換成一個 CIImage 物件。通過提供這個屬性, API 的使用者不再需要對返回的結果手動進行型別轉換了。

模糊

有了這些東西,現在我們就可以定義屬於自己的簡單濾鏡了。高斯模糊濾鏡只需要一個模糊半徑作為引數,我們可以非常容易的完成一個模糊濾鏡:

func blur(radius: Double) -> Filter {
    return { image in
        let parameters : Parameters = [kCIInputRadiusKey: radius, kCIInputImageKey: image]
        let filter = CIFilter(name:"CIGaussianBlur", parameters:parameters)
        return filter.outputImage
    }
}

就是這麼簡單,這個模糊函式返回了一個函式,新的函式的引數是一個型別為 CIImage 的圖片,返回值 (filter.outputImage) 是一個新的圖片 。這個模糊函式的格式是 CIImage -> CIImage ,滿足我們前面定義的 Filter 型別的格式。

這個例子只是對 Core Image 中已有濾鏡的一個簡單的封裝,我們可以多次重複同樣的模式,建立屬於我們自己的濾鏡函式。

顏色蒙版

現在讓我們定義一個顏色濾鏡,可以在現有的圖片上面加上一層顏色蒙版。 Core Image 預設沒有提供這個濾鏡,不過我們可以通過已有的濾鏡組裝一個。

我們使用兩個模組來完成這個工作,一個是顏色生成濾鏡 (CIConstantColorGenerator),另一個是資源合成濾鏡 (CISourceOverCompositing)。讓我們先定義一個生成一個常量顏色面板的濾鏡:

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

這段程式碼看起來和前面的模糊濾鏡差不多,不過有一個較為明顯的差異:顏色生成濾鏡不會檢測輸入的圖片。所以在函式裡我們不需要給傳入的圖片引數命名,我們使用了一個匿名引數 _ 來強調這個 filter 的圖片引數是被忽略的。

接下來,我們來定義合成濾鏡:

func compositeSourceOver(overlay: CIImage) -> Filter {
    return { image in
        let parameters : Parameters = [ 
            kCIInputBackgroundImageKey: image, 
            kCIInputImageKey: overlay
        ]
        let filter = CIFilter(name:"CISourceOverCompositing", parameters: parameters)
        return filter.outputImage.imageByCroppingToRect(image.extent())
    }
}

在這裡我們將輸出影象裁剪到和輸入大小一樣。這並不是嚴格需要的,要取決於我們想讓濾鏡如何工作。不過,在後面我們的例子中我們可以看出來這是一個明智之舉。

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

我們再一次返回了一個引數為圖片的函式,colorOverlay 在一開始先呼叫了 colorGenerator 濾鏡。colorGenerator 濾鏡需要一個顏色作為引數,並且返回一個濾鏡。因此 colorGenerator(color)Filter 型別的。但是 Filter 型別本身是一個 CIImageCIImage 轉換的函式,我們可以在 colorGenerator(color) 後面加上一個型別為 CIImage 的引數,這樣可以得到一個型別為 CIImage 的蒙版圖片。這就是在定義 overlay 的時候發生的事情:我們用 colorGenerator 函式建立了一個濾鏡,然後把圖片作為一個引數傳給了這個濾鏡,從而得到了一張新的圖片。返回值 compositeSourceOver(overlay)(image) 和這個基本相似,它由一個濾鏡 compositeSourceOver(overlay) 和一個圖片引數 image 組成。

組合濾鏡

現在我們已經定義了一個模糊濾鏡和一個顏色濾鏡,我們在使用的時候可以把它們組合在一起:我們先將圖片做模糊處理,然後再在上面放一個紅色的蒙層。讓我們先載入一張圖片:

let url = NSURL(string: "http://tinyurl.com/m74sldb");
let image = CIImage(contentsOfURL: url)

現在我們可以把濾鏡組合起來,同時應用到一張圖片上:

let blurRadius = 5.0
let overlayColor = UIColor.redColor().colorWithAlphaComponent(0.2)
let blurredImage = blur(blurRadius)(image)
let overlaidImage = colorOverlay(overlayColor)(blurredImage)

我們又一次的通過濾鏡組裝了圖片。比如在倒數第二行,我們先得到了模糊濾鏡 blur(blurRadius) ,然後再把這個濾鏡應用到圖片上。

函式組裝

不過,我們可以做的比上面的更好。我們可以簡單的把兩行濾鏡的呼叫組合在一起變成一行,這是我腦海中想到的第一個能改進的地方:

let result = colorOverlay(overlayColor)(blur(blurRadius)(image))

不過,這些圓括號讓這行程式碼完全不具有可讀性,更好的方式是定義一個函式來完成這項任務:

func composeFilters(filter1: Filter, filter2: Filter) -> Filter {
    return { img in filter2(filter1(img)) }
}

composeFilters 函式的兩個引數都是 Filter ,並且返回了一個新的 Filter 濾鏡。組裝後的濾鏡需要一個 CIImage 型別的引數,並且會把這個引數分別傳給 filter1filter2 。現在我們可以用 composeFilters 來定義我們自己的組合濾鏡:

let myFilter = composeFilters(blur(blurRadius), colorOverlay(overlayColor))
let result = myFilter(image)

我們還可以更進一步的定義一個濾鏡運算子,讓程式碼更具有可讀性,

infix operator >|> { associativity left }

func >|> (filter1: Filter, filter2: Filter) -> Filter {
    return { img in filter2(filter1(img)) }
}

運算子通過 infix 關鍵字定義,表明運算子具有 兩個引數。associativity left 表明這個運算滿足左結合律,即:f1 >|> f2 >|> f3 等價於 (f1 >|> f2) >|> f3。通過使這個運算滿足左結合律,再加上運算內先應用了左側的濾鏡,所以在使用的時候濾鏡順序是從左往右的,就像 Unix 管道一樣。

剩餘的部分是一個函式,內容和 composeFilters 基本相同,只不過函式名變成了 >|>

接下來我們把這個組合濾鏡運算器應用到前面的例子中:

let myFilter = blur(blurRadius) >|> colorOverlay(overlayColor)
let result = myFilter(image)

運算子讓程式碼變得更易於閱讀和理解濾鏡使用的順序,呼叫濾鏡的時候也更加的方便。就好比是 1 + 2 + 3 + 4 要比 add(add(add(1, 2), 3), 4) 更加清晰,更加容易理解。

自定義運算子

很多 Objective-C 的開發者對於自定義運算子持有懷疑態度。在 Swift 剛釋出的時候,這是一個並沒有很受歡迎的特性。很多人在 C++ 中遭遇過自定義運算子過度使用 (甚至濫用) 的情況,有些是個人經歷過的,有些是聽到別人談起的。

你可能對於前面定義的運算子 >|> 持有同樣的懷疑態度,畢竟如果每個人都定義自己的運算子,那程式碼豈不是很難理解了?值得慶幸的是在函數語言程式設計裡有很多的操作,為這些操作定義一個運算子並不是一件很罕見的事情。

我們定義的濾鏡組合運算子是一個函式組合的例子,這是一個在函數語言程式設計中廣泛使用的概念。在數學裡,兩個函式 fg 的組合有時候寫做 f ∘ g,這樣定義了一種全新的函式,將輸入的 x 對映到 f(g(x)) 上。這恰好就是我們的 >|> 所做的工作 (除了函式的逆向呼叫)。

泛型

仔細想想,其實我們並沒有必要去定義一個用來專門組裝濾鏡的運算子,我們可以用一個泛型的運算子來組裝函式。目前我們的 >|> 是這樣的:

func >|> (filter1: Filter, filter2: Filter) -> Filter

這樣定義之後,我們傳入的引數只能是 Filter 型別的濾鏡。

但是,我們可以利用 Swift 的通用特性來定義一個泛型的函式組合運算子:

func >|> <A, B, C>(lhs: A -> B, rhs: B -> C) -> A -> C {
    return { x in rhs(lhs(x)) }
}

這個一開始可能很難理解 -- 至少對我來說是這樣。但是分開的看了各個部分之後,一切都變得清晰起來。

首先,我們來看一下函式名後面的尖括號。尖括號定義了這個函式適用的泛型型別。在這個例子裡我們定義了三個型別:A、B 和 C。因為我們並沒有指定這些型別,所以它們可以代表任何東西。

接下來讓我們來看看函式的引數:第一個引數:lhs (left-hand side 的縮寫),是一個型別為 A -> B 的函式。這代表一個函式的引數為 A,返回值的型別為 B。第二個引數:rhs (right-hand side 的縮寫),是一個型別為 B -> C 的函式。引數命名為 lhs 和 rhs,因為它們分別對應操作符左邊和右邊的值。

重寫了沒有 Filter 的濾鏡組合運算子之後,我們很快就發現其實前面實現的組合運算子只是泛型函式中的一個特殊情況:

func >|> (filter1: CIImage -> CIImage, filter2: CIImage -> CIImage) -> CIImage -> CIImage

把我們腦海中的泛型型別 A、B、C 都換成 CIImage,這樣可以清晰的理解用通用運算子的來替換濾鏡組合運算子是多麼的有用。

結論

至此,我們成功的用函式式 API 封裝了 Core Image。希望這個例子能夠很好的說明,對於 Objective-C 的開發者來說,在我們所熟知的 API 的設計模式之外有一片完全不同的世界。有了 Swift,我們現在可以動手探索那些全新的領域,並且將它們充分地利用起來。