初探 Swift Sequences 和 Generators

作者:uraimo,原文鏈接,原文日期:2015-11-12
譯者:CoderAFI;校對:Cee;定稿:numbbbbb

在這篇文章中我們將介紹 Swift 2 自定義序列,并舉例說明有限序列和無限序列的區(qū)別,本文是 Swift and the functional approach 系列其中一篇。

sequences
sequences

你可以訪問 GitHub 或下載 zip 文件來獲取本文示例程序的 playground。

SequenceType 標(biāo)準(zhǔn)協(xié)議在官方文檔中被定義為一種簡單的數(shù)據(jù)類型,該類型可以用 for...in 來循環(huán)遍歷。協(xié)議中最重要的定義是在上半部分:

public protocol SequenceType {
    typealias Generator : GeneratorType
    /// Return a *generator* over the elements of this *sequence*.
    ///
    /// - Complexity: O(1).
    public func generate() -> Self.Generator
    ...
    ...
}

上面的協(xié)議中關(guān)聯(lián)了另一個 GeneratorType 協(xié)議類型(Swift 讓協(xié)議泛型化的獨特方式)。當(dāng)我們要自定義序列的時候,我們同時也要自定義一個實現(xiàn)這個協(xié)議的生成器,保證我們自定義的 SequenceType 在調(diào)用 generate() 方法時能夠返回指定元素類型的生成器。

序列協(xié)議中提供了許多有意思的方法,這些方法很多都已經(jīng)在擴(kuò)展中實現(xiàn)了,例如 mapflatmap(深入了解可以參看 map and flatMap)、filter、reduce、subsequence functions 等。

這些方法讓 SequenceType 協(xié)議的作用遠(yuǎn)遠(yuǎn)大于只進(jìn)行 for each 遍歷。

讓我們來看下 GeneratorType 的定義:

public protocol GeneratorType {
    typealias Element
    /// Advance to the next element and return it, or `nil` if no next
    /// element exists.
    public mutating func next() -> Self.Element?
}

這個簡單的協(xié)議只包含了一個 next() 方法,該方法用來返回生成器管理的下一個元素。至關(guān)重要的一點是,當(dāng)序列遍歷到最后時,生成器應(yīng)該返回 nil。接下來當(dāng)我們構(gòu)造一個無限序列的時候,來看看為什么這里要返回 nil

首先,我們來寫一個簡單的斐波那契數(shù)序列生成器:

class FibonacciGenerator : GeneratorType {
    var last = (0,1)
    var endAt:Int
    var lastIteration = 0

    init(end:Int){
        endAt = end
    }

    func next() -> Int?{
        guard lastIteration<endAt else {
            return nil
        }
        lastIteration++

        let next = last.0
        last = (last.1,last.0+last.1)
        return next
    }
}

為了定義一個有限序列,我們需要一個自定義構(gòu)造函數(shù)來指定一個序列長度。當(dāng)?shù)竭_(dá)這個長度時 next() 方法就返回 nil。這里我們用元組(Tuple)實現(xiàn)起來會節(jié)省很多代碼量,讓我們看下如何使用這個生成器:

var fg = FibonacciGenerator(end:10)

while let fib = fg.next() {
    print(fib)
}

用這種方式我們就可以遍歷生成器中的元素,直到生成器返回 nil

根據(jù)這個生成器實現(xiàn)一個 SequenceType 輕而易舉。

class FibonacciSequence : SequenceType {
    var endAt:Int

    init(end:Int){
        endAt = end
    }

    func generate() -> FibonacciGenerator{
        return FibonacciGenerator(end: endAt)
    }
}

let arr = Array(FibonacciSequence(end:10))

for f in FibonacciSequence(end: 10) {
    print(f)
}

上面的序列正如預(yù)期那樣,可以在 foreach 遍歷中使用,同樣也可以用來生成其他類型的序列,比如數(shù)組。

其實我們沒有必要單獨定義一種生成器類型,我們可以用 anyGenerator 工具方法和 AnyGenerator<T> 類來降低序列定義的耦合性:

class CompactFibonacciSequence : SequenceType {
    var endAt:Int

    init(end:Int){
        endAt = end
    }

    func generate() -> AnyGenerator<Int> {
        var last = (0,1)
        var lastIteration = 0

        return anyGenerator({
            guard lastIteration<self.endAt else {
                return nil
            }
            lastIteration++

            let next = last.0
            last = (last.1,last.0+last.1)
            return next
        })
    }
}

這種定義方式跟上面序列的最終效果是一樣的。唯一的區(qū)別就是 generate 方法返回了 AnyGenerator<Int> 類型。它已經(jīng)不是我們開始的時候定義的簡單生成器類型

這種做法在這里看起來可能沒太大用處,但是在很多情況下,相較于讓一個生成器嵌入一個序列集合中,用一個簡單 anyGenerator() 方法來生成的序列更具擴(kuò)展性。

例如,我們用 Lucas 序列的前 10 個數(shù)來創(chuàng)建一個序列。Lucas 序列與斐波那契序列非常相似,不同之處是斐波那契序列以 0,1 開頭而 Lucas 序列以 2,1 開頭,所以當(dāng)然最終會生成截然不同的序列,例如:2,1,3,4,7,11,18,29...下面我們只定義一個生成器,并用它來初始化一個數(shù)組。

var last = (2,1)
var c = 0

let lucas = anyGenerator{
    ()->Int? in
    guard c<10 else {
        return nil
    }

    c++
    let next = last.0
    last = (last.1,last.0+last.1)
    return next
}

let a = Array(lucas) //[2, 1, 3, 4, 7, 11, 18, 29, 47, 76]

看起來不錯,我們刪除了一些無用的代碼,我們也可以擴(kuò)展我們的算法,讓它返回一個黃金分割比,讓我們試試:

import Darwin

let Phi = (sqrt(5)+1.0)/2
let phi = 1/Phi

func luc(n:Int)->Int {
    return Int(pow(Phi, Double(n))+pow(-phi,Double(n)))
}

c = 0
var compactLucas = anyGenerator{ c<10 ? luc(c++): nil }

let a2 = Array(compactLucas) //[2, 1, 3, 4, 7, 11, 18, 29, 47, 76]

這樣確實行得通嗎?當(dāng)然,你可以下載 playground打包的 zip 文件來驗證。

為了嘗試 SequenceType 提供的一些特性方法。我們構(gòu)建一個只返回偶數(shù)的 Lucas 數(shù)字序列:

c = 0
var evenCompactLucas = anyGenerator{ c<10 ? luc(c++): nil }.filter({$0 % 2 == 0})
let a3 = Array(evenCompactLucas) //[2, 4, 18, 76]

注意,這里我們其實是重新定義了 AnyGenerator,由于前面的序列是有限的,當(dāng)遍歷到最后時,就會產(chǎn)生另一個有限的序列。這從另一方面也可以說明,我們很容易就能改變原有序列,返回一組新的數(shù)據(jù)集。我們也可以用 map 方法來做一些更直接的轉(zhuǎn)換。

無限序列

現(xiàn)在,我們移除 nil 的返回值限制,這樣就能根據(jù) Lucas 算法生成一個無限序列。

c = 0
var infiniteLucas = anyGenerator{luc(c++)}

可見,將一個有限序列轉(zhuǎn)換成無限序列是非常容易的?,F(xiàn)在我們生成了一個沒有數(shù)量限制的新序列。但是我們需要另外一種方式來限制序列元素數(shù),從而讓無限序列元素數(shù)更可控。

幸運的是 SequenceType 協(xié)議提供了一個方法來解決這個問題:

let a4 = Array(infiniteLucas.prefix(10)) //[2, 1, 3, 4, 7, 11, 18, 29, 47, 76]

for var f in infiniteLucas.prefix(10){
    print(f)
}

這種方式將會從當(dāng)前序列篩選出 10 個元素并添加到一個新的序列中,而且新序列使用起來跟前面的無限序列是一樣的。

讓我們進(jìn)一步來看一下 filter 方法的用法,看看怎么樣用它來獲取 Lucas 偶數(shù)。

var onlyEvenLucas = infiniteLucas.filter({$0 % 2 == 0})
for var f in onlyEvenLucas.prefix(10){
    print(f)
}

然而,上面的代碼并不會像預(yù)期那樣工作。

如果是在 playground 運行,在聲明 onlyEventLucas 時你會看到高亮報錯。如果是在應(yīng)用程序中寫了這段代碼,你的應(yīng)用程序會崩潰。

要了解問題的原因,必須要了解 filter 函數(shù)的工作原理。 當(dāng)對一個序列進(jìn)行 filter 操作時,我們
必須要把序列的所有元素都獲取到,但是如果沒有 nil 限制,序列元素是無限的,就無法確認(rèn)元素的遍歷操作什么時候結(jié)束。

讓我們在每次從生成器獲取元素時都打印一段文本,來更形象的看下原因:

class InfiniteSequence :SequenceType {
    func generate() -> AnyGenerator<Int> {
        var i = 0
        return anyGenerator({
            print("# Returning "+String(i))
            return i++
        })
    }
}

var fs = InfiniteSequence().filter({$0 % 2 == 0}).generate()

for i in 1...5 {
    print(fs.next())
}

如果你運行這段代碼,會發(fā)現(xiàn)在 InfiniteSequence 上的過濾處理一直在運行,直到一段時間后處理器無法再繼續(xù)運行,程序就崩潰了。

幸運的是,解決上面的問題也很容易。我們只需要延遲計算(Lazily evaluate)這個無限的 Lucas 序列:

var onlyEvenLucas = infiniteLucas.lazy.filter({$0 % 2 == 0})
for var f in onlyEvenLucas.prefix(10){
    print(f)
}

使用無限序列的 .lazy 屬性就能獲取一個新的 LazySequenceType 類型,該類型會讓 map、flatmap、reduce 或者 filter 這些方法延遲執(zhí)行,也就是說真正的計算會等到例如 next 這樣的終端操作(Terminal Operation)(其他語言是這么叫的)執(zhí)行時才會被執(zhí)行。

讓無限序列支持延遲計算是一個必要步驟,默認(rèn)情況下 Swift 的序列不能延遲計算(該特性是在 Swift 1.0 發(fā)布的)。具體你可以通過官方文檔來詳細(xì)了解如何自定義一個 LazySequence(大多數(shù)情況可能是解決問題的最好辦法),我也會就該內(nèi)容進(jìn)行講解,敬請期待。

本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請訪問 http://swift.gg

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,569評論 19 139
  • 我們在學(xué)習(xí)web前端的路程起步時總是疑問,我們?nèi)绾胃玫谋闅v元素呢?迭代器和生成器是什么?今天為大家?guī)吓c精彩的E...
    儂姝沁兒閱讀 3,520評論 0 6
  • 你不知道JS:異步 第四章:生成器(Generators) 在第二章,我們明確了采用回調(diào)表示異步流的兩個關(guān)鍵缺點:...
    purple_force閱讀 1,044評論 0 2
  • 來不及彷徨, 來不及懷念, 2015已完美落幕。 我只能重新走上舞臺拉開帷幕, 微笑著說,你好,2016! 我還年...
    高安藝閱讀 158評論 0 0
  • sqlite3SQL語句的特點 不區(qū)分大小寫(比如數(shù)據(jù)庫認(rèn)為user和UsEr是一樣的)每條語句都必須以分號 “...
    光明程輝閱讀 4,332評論 4 23

友情鏈接更多精彩內(nèi)容