【函數式 Swift】Map、Filter 和 Reduce

說明:本文及所屬系列文章爲圖書《函數式 Swift》的讀書筆記,旨在記錄學習過程、分享學習心得。文中部分代碼摘自原書開源代碼庫 Github: objcio/functional-swift,部份內容摘自原書。如需深刻學習,請購買正版支持原書。(受 @SwiftLanguage 微博啓發,特此說明)javascript


標題中的三個數組操做函數咱們並不陌生,本章將藉助這些 Swift 標準庫中的函數,再次探索函數式思想的應用。html

本章關鍵詞

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

  • 函數式思想
  • 泛型

案例:City Filter

使用 City 結構體描述城市信息(名字、人口數量),並定義一個城市數組:git

struct City {
    let name: String
    let population: Int
}

let paris = City(name: "Paris", population: 2241) // 單位爲「千」
let madrid = City(name: "Madrid", population: 3165)
let amsterdam = City(name: "Amsterdam", population: 827)
let berlin = City(name: "Berlin", population: 3562)

let cities = [paris, madrid, amsterdam, berlin]複製代碼

問題:輸出 cities 數組中全部人口超過百萬的城市信息,並將人口數量單位轉換爲「個」。github

開始解決問題以前,請你們先忘掉標題中 Map、Filter 和 Reduce 等函數,不管以前是否使用過,咱們嘗試從零開始逐步向函數式思想過渡。express

咱們先使用一個簡單的思路來解決這個問題,即,遍歷輸入的城市數組,而後依次判斷每一個城市的人口數量,超過一百萬的城市輸出其信息:swift

func findCityMoreThanOneMillion(_ cities: [City]) -> String {
    var result = "City: Population\n"
    for city in cities {
        if city.population > 1000 {
            result = result + "\(city.name): \(city.population * 1000)\n"
        }
    }
    return result
}

let result = findCityMoreThanOneMillion(cities)
print(result)
// City: Population
// Paris: 2241000
// Madrid: 3165000
// Berlin: 3562000複製代碼

對於一個具體問題來講,咱們的解法並不算差,知足需求、代碼也簡單,可是它只能正常工做於這樣侷限的場景中,顯然,這不符合函數式思想。數組

咱們從上述代碼開始分析,findCityMoreThanOneMillion 函數主要完成了如下三個工做:安全

  1. 過濾:經過 city.population > 1000 過濾出人口超過百萬的城市;
  2. 轉換單位:經過 city.population * 1000 將單位轉換爲「個」;
  3. 拼接結果:使用 var result 將結果拼接起來,並最終返回。

這三步天然的幫咱們將原問題分解成了三個子問題,即:閉包

  1. 數組元素過濾問題(Filter);
  2. 數組元素修改問題(Map);
  3. 數組遍歷與結果拼接問題(Reduce)。

爲了解決原始問題,咱們須要優先解決這三個子問題,很明顯,它們對應了標題中的函數,下面一一討論(爲了匹配原書內容,咱們從 Map 開始)。

Map

案例中,咱們須要將 city.population 的單位轉換爲「個」,本質上就是將一個數值轉換爲另外一個數值,下面編寫一個函數來實現這個功能:

func transformArray(xs: [Int]) -> [Int] {
    var result: [Int] = []
    for x in xs {
        result.append(x * 1000)
    }
    return result
}複製代碼

使用該函數能夠幫助咱們將一個 [Int] 數組中的每一個元素乘以 1000,這樣就能知足咱們從「千」到「個」的單位轉換需求,然而,這個函數存在的問題也很是明顯:

  1. 入參和返回值均固定爲 [Int],擴展性差;
  2. 數值變換方式固定爲 x * 1000,場景侷限。

試想,若是輸入數組可能爲 [Double][Int],須要將單位從「千」轉換爲「萬」、「百萬」或者「千萬」,輸出爲 [Int][Double],就不得不去修改這個函數,或是添加更多類似的函數。

如何解決呢?先來了解一個概念:泛型(Generics),Swift 官方文檔對泛型的定義以下:

Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner.

可見,泛型的目標就是編寫靈活、可複用,而且支持任意類型的函數,避免重複性的代碼。以官方代碼爲例:

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"複製代碼

使用泛型定義的 swapTwoValues 函數可以接受任意類型的入參,不管是 Int 仍是 String 均能正常工做。

回到 transformArray 函數:

引入泛型能夠幫助咱們解決第一個問題,即,入參和返回值均固定爲 [Int],既然入參和返回值能夠是不相關的兩種數組類型,那麼咱們可使用兩個泛型來表示它們,例如 [E][T]

此時,transformArray 函數的入參和返回值變成了 [E][T],那麼函數內部所要完成的任務就是將 [E] 轉換爲 [T],而轉換過程正好對應了第二個問題,即,數值變換方式固定位 x * 1000,解決它只須要調用方將這個「轉換過程」傳遞給 transformArray 函數便可,也就是說,咱們須要一個形如這樣的函數做爲入參:

typealias Transform = (E) -> (T)
// 因爲沒有定義 E、T 泛型,因此這裏僅做示意複製代碼

而後,將 transformArray 函數改寫以下:

func transformArray<E, T>(xs: [E], transform: Transform) -> [T] {
    var result: [T] = []
    for x in xs {
        result.append(transform(x))
    }
    return result
}複製代碼

這樣就完成了一個相似 Map 的函數,將這個函數加入 Array 中,並將函數名改成 map,就可使用 Array 對象來調用這個方法了:

extension Array {
    func map<T>(transform: (Element) -> T) -> [T] {
        var result: [T] = []
        for x in self {
            result.append(transform(x))
        }
        return result
    }
}
// 其中 Element 使用 Array 中 Element 泛型定義複製代碼

咱們知道,map 函數已經存在於 Swift 標準庫中(基於 Sequence 協議實現),所以並不須要本身來實現,咱們經過 Swift 源碼來學習一下(路徑:swift/stdlib/public/Sequence.swift):

public protocol Sequence {
    ...

    func map<T>(
        _ transform: (Iterator.Element) throws -> T
    ) rethrows -> [T]
}

extension Sequence {
    ...

    public func map<T>(
        _ transform: (Iterator.Element) throws -> T
    ) rethrows -> [T] {
        let initialCapacity = underestimatedCount
        var result = ContiguousArray<T>()
        result.reserveCapacity(initialCapacity)

        var iterator = self.makeIterator()

        // Add elements up to the initial capacity without checking for regrowth.
        for _ in 0..<initialCapacity {
            result.append(try transform(iterator.next()!))
        }
        // Add remaining elements, if any.
        while let element = iterator.next() {
            result.append(try transform(element))
        }
        return Array(result)
    }
}複製代碼

除了一些額外的處理,核心部分與咱們的實現是相同的,使用方法以下:

let arr = [10, 20, 30, 40]
let arrMapped = arr.map { $0 % 3 }
print(arrMapped)
// [1, 2, 0, 1]複製代碼

Filter

有了 Map 的經驗,對於 Filter 的設計就方便多了,咱們參考 transformArray 函數能夠這樣設計 Filter 函數:

  1. 入參和返回值均爲 [T]
  2. 入參的 transform 修改成 isIncluded,類型爲 (T) -> Bool,用於判斷是否應該包含在返回值中。

實現代碼以下:

func filterArray<T>(xs: [T], isIncluded: (T) -> Bool) -> [T] {
    var result: [T] = []
    for x in xs {
        if isIncluded(x) {
            result.append(x)
        }
    }
    return result
}複製代碼

一樣的,filter 函數也已經存在於 Swift 標準庫中,源碼以下(路徑:swift/stdlib/public/Sequence.swift):

public protocol Sequence {
    ...

    func filter(
        _ isIncluded: (Iterator.Element) throws -> Bool
    ) rethrows -> [Iterator.Element]
}

extension Sequence {
    ...

    public func filter(
        _ isIncluded: (Iterator.Element) throws -> Bool
    ) rethrows -> [Iterator.Element] {

        var result = ContiguousArray<Iterator.Element>()
        var iterator = self.makeIterator()

        while let element = iterator.next() {
            if try isIncluded(element) {
                result.append(element)
            }
        }

        return Array(result)
    }
}複製代碼

核心部分實現也是相同的,使用方法以下:

let arr = [10, 20, 30, 40]
let arrFiltered = arr.filter { $0 < 35 }
print(arrFiltered)
// [10, 20, 30]複製代碼

Reduce

Reduce 與 Map 不一樣之處在於,Map 每次將集合中的元素拋給 transform 閉包,而後獲得一個「變形」後的元素,而 Reduce 是將集合中的元素連同當前上下文中的變量一塊兒拋給入參閉包(此處命名爲 combine),以便於該閉包處理,而後返回處理後的結果,所以 combine 的定義相似:

typealias Combine = (T, E) -> (T)
// 因爲沒有定義 E、T 泛型,因此這裏僅做示意複製代碼

所以 reduce 函數能夠定義以下:

func reduceArray<E, T>(xs: [E], initial: T, combine: Combine) -> T {
    var result: T = initial
    for x in xs {
        result = combine(result, x)
    }
    return result
}複製代碼

Swift reduce 函數源碼以下(路徑:swift/stdlib/public/Sequence.swift):

/// You rarely need to use iterators directly, because a `for`-`in` loop is the
/// more idiomatic approach to traversing a sequence in Swift. Some
/// algorithms, however, may call for direct iterator use.
///
/// One example is the `reduce1(_:)` method. Similar to the `reduce(_:_:)`
/// method defined in the standard library, which takes an initial value and a
/// combining closure, `reduce1(_:)` uses the first element of the sequence as
/// the initial value.
///
/// Here's an implementation of the `reduce1(_:)` method. The sequence's
/// iterator is used directly to retrieve the initial value before looping
/// over the rest of the sequence.
///
/// extension Sequence {
/// func reduce1(
/// _ nextPartialResult: (Iterator.Element, Iterator.Element) -> Iterator.Element
/// ) -> Iterator.Element?
/// {
/// var i = makeIterator()
/// guard var accumulated = i.next() else {
/// return nil
/// }
///
/// while let element = i.next() {
/// accumulated = nextPartialResult(accumulated, element)
/// }
/// return accumulated
/// }
/// }複製代碼

reduce 函數與上面兩個函數不太相同,Apple 將其實現以另外一個 reduce1 函數放在了註釋中,緣由應該如註釋所說,for-in loop 方式更加經常使用,但咱們仍然能夠正常使用 reduce 函數,方法以下:

let arr = [10, 20, 30, 40]
let arrReduced = arr.reduce(output) { result, x in
    return result + "\(x) "
}
print(arrReduced)
// Arr contains 10 20 30 40複製代碼

函數式解決方案

在準備好了 Map、Filter 和 Reduce 工具庫以後,咱們再來解決 City Filter 問題:

let result =
    cities.filter { $0.population > 1000 }
        .map { $0.cityByScalingPopulation() }
        .reduce("City: Population") { result, c in
            return result + "\n" + "\(c.name): \(c.population)"
        }
print(result)
// City: Population
// Paris: 2241000
// Madrid: 3165000
// Berlin: 3562000

extension City {
    func cityByScalingPopulation() -> City {
        return City(name: name, population: population * 1000)
    }
}複製代碼

藉助 Map、Filter 和 Reduce 等方法,能夠方便的使用鏈式語法對原數組進行處理,並獲得最終結果。


思考

函數式思想

當咱們討論函數式思想時,咱們到底在說什麼?

簡單說,函數式思想是經過構建一系列簡單、實用的函數,再「裝配」起來解決實際問題,對於這句話的理解,我想至少有三點:

  1. 目標轉換:基於函數式思想解決問題時,目標再也不「急功近利」直接解決具體問題,而是庖丁解牛,把具體問題分解成爲小規模的,甚至是互不相干的子模塊,攻克這些子模塊纔是更高優先級的工做;
  2. 函數設計:分解出的每一個子模塊,實際上也就對應了一個、或一組可以獨立工做的函數,良好的函數設計不只有助於咱們解決當前問題,更能爲咱們構建一個優秀的工具庫,去解決不少其餘問題;
  3. 問題解決:有時具體問題的解決好像已經被咱們遺忘了,在解決了子問題、構建了工具庫後,簡單「裝配」就能輕鬆解決原始問題。藉助函數式思想,咱們也更容易發現問題之間的共同點,從而快速解決,換句話說,解決問題成爲了函數式思想下的「副產品」

泛型

Swift 中對於泛型的應用很是普遍,使用泛型可以使咱們事半功倍,一個函數能夠「瞬間」支持幾乎全部類型,更重要的是,由於 Swift 語言的「類型安全」特性,使得這一切都安全可靠。

泛型之因此安全,是由於它仍然處於編譯器的類型控制下,而 Swift 中的 Any 類型就不那麼安全了,表面上看二者都能表示任意類型,但使用 Any 類型可以避開編譯器的檢查,從而可能形成錯誤,來看下面的例子:

func exchange<T>(_ income: T) -> T {
    return "Money: \(income)" // error
}

func exchangeAny(_ income: Any) -> Any {
    return "Money: \(income)"
}複製代碼

一樣的函數體,使用泛型的 exchange 會提示錯誤:error: cannot convert return expression of type 'String' to return type 'T',而使用 AnyexchangeAny 則不提示任何錯誤。若是咱們不清楚 exchangeAny 的返回值類型,而直接調用,則可能致使運行時錯誤,是很是危險的。所以,善用泛型可以讓咱們在「無須犧牲類型安全就可以在編譯器的幫助下寫出靈活的函數」。

更多關於泛型的討論請參閱原書,或官方文檔。


參考資料

  1. Github: objcio/functional-swift
  2. The Swift Programming Language: Generics
  3. The Swift Programming Language (Source Code)

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

相關文章
相關標籤/搜索