Swift函数式编程十(迭代器和序列)

Posted 酒茶白开水

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift函数式编程十(迭代器和序列)相关的知识,希望对你有一定的参考价值。

代码地址

迭代器(Iterators) 和序列(Sequences) 组成了 Swift 中 for 循环的基础部分。

迭代器

迭代器是每次根据请求生成新元素的“过程”,一个迭代器是遵守一下协议的任何类型:

protocol IteratorProtocol  
    typealias Element
    func next() -> Element?

这个协议需要一个关联类型——Element,还有一个产生新元素的next方法,如果新元素存在就返回新元素,否则返回nil。

下面的迭代器从数组的末尾开始生成序列值,一直到0,Element可以由next函数推断出来,所以不必显式指定:

struct ReverseIndexIterator: IteratorProtocol 
    var index: Int
    init<T>(array: [T]) 
        index = array.endIndex - 1
    
    
    mutating func next() -> Int? 
        guard index >= 0 else  return nil 
        
        defer  index -= 1 
        
        return index
    

使用 ReverseIndexIterator 来倒序遍历数组:

let letters = ["A", "B", "C"]
var iterator = ReverseIndexIterator(array: letters)
while let i = iterator.next() 
    print("数组的第\\(i)个元素是\\(letters[i])")

/* 输出:
 数组的第2个元素是C
 数组的第1个元素是B
 数组的第0个元素是A
 */

如果需要另一种方式排序序列值,只需要更新迭代器。

有时迭代器并不需要生成 nil 值,例如定义一个迭代器来生成无数个2的幂值(直到某个最大值,使得 NSDecimalNumber 溢出):

struct PowerIterator: IteratorProtocol 
    var power: NSDecimalNumber = 1
    mutating func next() -> NSDecimalNumber? 
        power = power.multiplying(by: 2)
        
        return power
    

现在可以在2的幂值中搜索一些有趣的值,定义一个 find 函数带有一个NSDecimalNumber -> Bool 类型的predicate参数,并返回复合该条件的最小值:

extension PowerIterator 
    mutating func find(where predicate: (NSDecimalNumber) -> Bool) -> NSDecimalNumber? 
        while let x = next() 
            if predicate(x) 
                return x
            
        
        
        return nil
    

例如计算2的幂值中大于1000的最小值:

var powerIterator = PowerIterator()
let num = powerIterator.find  $0.intValue > 1000 
if let value = num 
    print(value)

/// 输出:1024

下面迭代器生成一组字符串,与文件中以行为单位的内容相对应:

struct FileLinesIterator: IteratorProtocol 
    let lines: [String]
    var currentLine: Int = 0
    
    init(filename: String) throws 
        let contents: String = try String(contentsOfFile: filename)
        lines = contents.components(separatedBy: .newlines)
    
    
    mutating func next() -> String? 
        guard currentLine < lines.endIndex else  return nil 
        
        defer  currentLine += 1 
        
        return lines[currentLine]
    

通过定义这种迭代器,将数据的生成和使用分离开,确保在使用数据时不用去考虑其他问题。

基于迭代器协议,可以定义一些通用的泛型函数,例如下面findPower函数的泛型版本:

extension IteratorProtocol 
    mutating func find(predicate:(Element) -> Bool) -> Element? 
        while let x = next() 
            if predicate(x) 
                return x
            
        
        
        return nil
    

这个find函数适用任何迭代器,由于调用了next方法可能修改迭代器,所以在申明中添加mutating标注。

可以层级式组合迭代器。比如希望限制生产元素个数,可以构建一个迭代器转换器,适用参数limit来限制迭代器G生成元素的个数:

struct LimitIterator<I: IteratorProtocol>: IteratorProtocol 
    var limit = 0
    var iterator: I
    init(limit: Int, iterator: I) 
        self.limit = limit
        self.iterator = iterator
    
    
    mutating func next() -> I.Element? 
        guard limit > 0 else 
            return nil
        
        
        return iterator.next()
    

为每个迭代器引入一个新的类型是非常繁琐的,Swift提供了一个AnyGenerator类,通过传入一个next函数来初始化。

适用AnyIterator可以更简短的定义迭代器,如下重写ReverseIndexIterator:

extension Int 
    func countDown() -> AnyIterator<Int> 
        return AnyIterator 
            var i = self - 1
            guard i >= 0 else 
                return nil
            
            
            defer  i -= 1 
            
            return i
        
    

还可以使用AnyIterator定义对迭代器操作、组合的函数:

func +<I: IteratorProtocol, J: IteratorProtocol>(first: I, second: J) -> AnyIterator<I.Element> where I.Element == J.Element 
    var i = first
    var j = second
    return AnyIterator  i.next() ?? j.next() 

先取第一个迭代器的元素,耗尽之后再从第二个迭代器取元素,直到第二个迭代器耗尽,返回nil。

可以对上面定义的第二个参数进行延迟化处理,返回结果完全相同,只是如果只返回部分元素将会更加高效:

func +<I: IteratorProtocol, J: IteratorProtocol>(first: I, second: @escaping @autoclosure () -> J) -> AnyIterator<I.Element> where I.Element == J.Element 
    var one = first;
    var other: J? = nil
    return AnyIterator 
        if other != nil 
            return other?.next()
         else if let result = one.next() 
            return result
         else 
            other = second()
            return other?.next()
        
    

函数通过将参数标记为 @autoclosure 来接收一个自动闭包。自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。自动闭包能够延迟求值,因为直到调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它能控制代码的执行时机。

序列

迭代器为序列提供了基础类型,迭代器提供“单次触发”机制反复计算下一个元素,不支持反查、重生已经生成过的元素。而SequenceType协议为这些提供了合适的接口:

protocol Sequence 
    associatedtype Iterator: IteratorProtocol
    func makeIterator() -> Iterator
    // ...

每个序列都有一个关联的迭代器类型和一个生成新的迭代器的方法,可以据此使用迭代器来遍历序列。例如定义ReverseIndexIterator序列用以生成数组倒序序列值:

struct ReverseArrayIndices<T>: Sequence 
    let array: [T]
    init(array: [T]) 
        self.array = array
    
    func makeIterator() -> ReverseIndexIterator 
        return ReverseIndexIterator(array: array)
    

使用序列:

var array = ["one", "two", "three"]
let reverseSequence = ReverseArrayIndices(array: array)
var reverseIterator = reverseSequence.makeIterator()
while let i = reverseIterator.next() 
    print("第\\(i)个元素是\\(array[i])")

/*
 输出:
 第2个元素是three
 第1个元素是two
 第0个元素是one
 */

同一个序列可以使用多次,只需要调用makeIterator来生成新的迭代器。Sequence的定义中封装了迭代器的创建过程,使用序列不必担心迭代器的创建过程。这跟面向对象中使用和创建分离的思想一脉相承,具有更高的内聚性。

Swift处理序列有一个特别的语法,就是可以用for循环遍历:

for i in reverseSequence 
    print("第\\(i)个元素是\\(array[i])")

/*
 输出:
 第2个元素是three
 第1个元素是two
 第0个元素是one
 */

其实应该是对数组中的元素更感兴趣,但是ReverseArrayIndices只是生成元素序号,不过序列也有标准的map、filter方法:

public protocol Sequence  
    public func map<T>(_ transform: (Iterator.Element) throws -> T) rethrows -> [T]
    public func filter(_ isIncluded: (Iterator.Element) throws -> Bool) rethrows -> [Iterator.Element]

使用map映射就可以得到相应的元素:

let reverseElements = reverseSequence.map  array[$0] 
for x in reverseElements 
    print("元素是:\\(x)")

/*
 输出:
 元素是:three
 元素是:two
 元素是:one
 */

map、filter这些方法不会返回新的序列,而是通过遍历生成一个新的数组。Sequence协议中有一个内建的reversed方法就返回一个新的数组。

BidirectionalCollection协议中的reversed方法,根据序列值返回内部集合的反向视图。但是在Sequence协议中没有序列值的概念,在计算出全部结果之前,无法给出一个高效的reveresed序列。

延迟化序列

操作序列可以将一些简单的变换步骤组合起来,例如下面这段代码:

let _ = (1...10).filter  $0%3 == 0 .map  $0*$0 

将上述代码用一个for循环编写:

var result: [Int] = []
for element in (1...10) 
    if element%3 == 0 
        result.append(element*element)
    

for 循环的命令式版本更复杂,难以理解,不利于扩展。函数式版本更加浅显,可以继续链接其他操作,相互独立的各个操作易读,整体含义也便于理解。

但是命令式版本还是有一个好处的,那就是性能更高,只对序列进行了一次迭代。而函数式版本对序列做了两次迭代(过滤、映射各一次),还生成了一个过渡数组将filter的结果传递给map。

多数情况可读性比性能重要,但还是可以优化,使用LazySequence可以在链式操作的同时一次性计算所有操作之后的结果。这样可以将filter、map合并为一步:

let lazyResult = (1...10).lazy.filter  $0%3 == 0 .map  $0*$0 

lazyResult的类型比较复杂,实际上新元素还没有被计算出来。由于该类型遵守Sequence协议,所以既可以使用for循环迭代也可以将转换为数组:

let _ = Array(lazyResult)
for e in lazyResult 
    print(e)

/*
 输出:
 9
 36
 81
*/

多个方法链接时,使用lazy合并所有循环,性能可以媲美命令式代码。

案例一遍历二叉树

首先定义二叉树:

indirect enum BinarySearchTree<Element: Comparable> 
    case leaf
    case node(BinarySearchTree<Element>, Element, BinarySearchTree<Element>)

扩展二叉树,使其准守Sequence协议:

extension BinarySearchTree: Sequence 
    func makeIterator() -> AnyIterator<Element> 
        switch self 
        case .leaf:
            return AnyIterator  nil 
        case let .node(left, element, right):
            return left.makeIterator() + CollectionOfOne(element).makeIterator() + right.makeIterator()
        
    

使用重载的+运算符来生成二叉树元素的序列。makeIterator遍历访问左右子树、根节点,没有元素返回空迭代器,如果是节点,则递归调用并与该节点存储的值拼接作为结果返回。

注意使用的是延迟化方案的+,如果是非延迟化方案的+,则makeIterator需要访问整棵树再返回结果。

案例二优化QuickCheck的范围收缩

之前在QuickCheck中实现的Smaller协议是这样定义的:

protocol Smaller 
    func small() -> Self?

使用Smaller去判断和收缩测试中的反例,small函数反复调用来生成更小的值,如果这个值依然无法通过测试则说明可能存在一个更好的反例。

定义数组遵守Smaller协议,反复移除第一个元素:

extension Array: Smaller 
    func small() -> Array<Element>? 
        if self.isEmpty  return nil 
        return Array(dropFirst())
    

这确实有助于收缩反例,但计算出所有子数组非常昂贵,一个长度为n的数组有2^n个子数组,生成和计算这些并不是一个好主意。

下面将使用迭代器来生成更小的值,重新定义Smaller协议:

protocol Smaller 
    func small() -> AnyIterator<Self>

不同于前面只一次数组的第一个元素,而是计算出一系列数组,每个数组都移除一个元素,每个数组都比原始数组少一个元素:

extension Array: Smaller 
    func small() -> AnyIterator<Array<Element>> 
        var i = 0
        return AnyIterator 
            guard i < self.endIndex else  return nil 
            var result = self
            result.remove(at: i)
            i += 1
            return result
        
    

可以做的还有很多,而不仅仅是移除元素。测试一下迭代器:

let v = Array([1, 2, 3].small())
print(v)
/*
 输出:
 [[2, 3], [1, 3], [1, 2]]
*/

以上是关于Swift函数式编程十(迭代器和序列)的主要内容,如果未能解决你的问题,请参考以下文章

Swift函数式编程十(迭代器和序列)

Rust编程语言入门之函数式语言特性:-迭代器和闭包

Swift函数式编程十三(函子适用函子单子)

Swift函数式编程十三(函子适用函子单子)

Swift函数式编程十三(函子适用函子单子)

Swift函数式编程十二(表格应用)