7

There’s very little up-to-date guidance on how to make generators in Swift (or iterators as they’re apparently called in Swift), especially if you are new to the language. Why are there so many generator types like AnyIterator and UnfoldSequence? Why doesn’t the following code, which should yield from a sequence of either individual Ints or Arrays of Ints, work?

func chain(_ segments: Any...) -> AnyIterator<Int>{
    return AnyIterator<Int> {
        for segment in segments {
            switch segment {
            case let segment as Int:
                return segment
            case let segment as [Int]:
                for i in segment {
                    return i
                }
            default:
                return nil
            }
        }
        return nil
    }
}

let G = chain(array1, 42, array2)
while let g = G.next() {
    print(g)
}

The way I understand it, AnyIterator is supposed to take the closure in the {}s and turn it into the .next() method in the returned generator, but it doesn’t seem to be working. Or should I be using UnfoldSequence like in this question instead. I’m very confused.

Community
  • 1
  • 1
taylor swift
  • 2,039
  • 1
  • 15
  • 29

2 Answers2

4

Yes, the next() method of AnyIterator calls the given closure. And in your code, that closure returns the same first element on each call, because it does not remember what elements have been returned already.

If Swift had a yield statement like Python or C# then things would be easier: you could yield segment or yield i and are done.

But – unfortunately? – Swift has no yield statement, which means that the closure must explicitly manage some state in order to resume the iteration with the next element on each call.

One possibility would be to maintain two indices, one for the current segment and one for the current element within a segment if that is an array:

func chain(_ segments: Any...) -> AnyIterator<Int> {
    var currentSegment = 0 // index of current segment
    var currentElement = 0 // index of current element within current segment
    return AnyIterator<Int> {
        while currentSegment < segments.count {
            let next = segments[currentSegment]
            switch next {
            case let value as Int:
                currentSegment += 1
                return value
            case let segment as [Int]:
                if currentElement < segment.count {
                    let val = segment[currentElement]
                    currentElement += 1
                    return val
                }
                currentSegment += 1
                currentElement = 0
            default:
                return nil
            }
        }
        return nil
    }
}

This can be generalized to arbitrarily nested arrays:

func chain(_ segments: Any...) -> AnyIterator<Int> {
    var stack: [(Any, Int)] = [(segments, 0)]
    return AnyIterator<Int> {
        while let (next, idx) = stack.popLast() {
            switch next {
            case let value as Int:
                return value
            case let segments as [Any]:
                if idx < segments.count {
                    stack.append((segments, idx + 1))
                    stack.append((segments[idx], 0))
                }
            default:
                return nil
            }
        }
        return nil
    }
}

The still-to-be-processed arrays are on the stack together with their current index. The arrays themselves are not modified, so that the copy is cheap.

Example:

let G = chain([1, 2, [3]], 4, [5, 6, [], 7])
while let g = G.next() {
    print(g)
}
// 1 2 3 4 5 6 7

See also Implementing recursive generator for simple tree structure in Swift for more approaches to recursively enumerate a tree-like structure.

Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • so this creates a copy of the array? isn’t that something generators are used to avoid? – taylor swift Nov 18 '16 at 13:56
  • @taylorswift: How large is your array? Does it matter? – But as I said, it surely can be improved, it was meant to explain the general idea. – You could for example (in the first method) store the index of the current segment and the index of the current element within the current segment if that is an array. – Martin R Nov 18 '16 at 13:59
  • often we want to use generators to build a joined or flattened array out of many pieces “at once”, to bypass slow extend methods in the high level language. If the array gets copied we are better off just using repeated array concatenation? Or is that exactly what you’re supposed to do in Swift and i am thinking too much like a python programmer – taylor swift Nov 18 '16 at 14:06
  • the idea here with these kinds of generators is the generator does all the work of locating the items that are supposed to go into the array, and then the copy operations to actually build the array can be done in one pass – taylor swift Nov 18 '16 at 14:09
  • @taylorswift: The first method was indeed not optimal. I have replaced it with a method which keeps only two indices. – Martin R Nov 18 '16 at 14:14
  • i suppose the new question should be then, is using a generator any faster than just repeatedly appending/extending a Swift array – taylor swift Nov 18 '16 at 14:55
  • 1
    @taylorswift: If you want to create a flattened "real array" with all elements then I *assume* that there will be no difference. If you just want to *iterate* over all elements, without the need to them in an array, then a generator or (lazy) sequence would be faster, at least if you have many objects. – But yes, that would be a different question. – Martin R Nov 18 '16 at 14:59
  • ... (cross-post ellipsis) the _"if Swift had a yield"_ statement was the reason of me wanting to (attempt to) use the `sequence(state:,next:)` to try to somewhat emulate that in the context of the example is this Q&A. Anyhow, the `sequence(state:, next:)` (without a `state` closure :( ) function can be used as a [slightly modified alternative](https://gist.github.com/dfrib/3eda3e2540b31486d5728b2fa574e8af) to the simple stack solution of yours above. – dfrib Nov 19 '16 at 21:12
  • @dfri: I assume that you chose to reverse the arrays because removing the *last* element is faster than removing the first? An ArraySlice might be an alternative. I have also thought of just keeping a stack of indices (as a generalization of the first method). – Martin R Nov 19 '16 at 21:32
  • @MartinR exactly, the `insert(...)` methods are generally `O(n)` whereas appending is amortized `O(1)` (with an extra single initial `reversed` on array top level). And partly for the semantics of a stack. But if performance is really an issue, working with indices will probably prove to be most performant. – dfrib Nov 19 '16 at 21:38
0

You can also check out this custom implementation of an iterator as seen in https://stackoverflow.com/a/67215766/5867877

class ArrayIterator<T>{

private var array : [T] = []
private var stepSize: Int = 10
private var head: Int = 0

var hasNext: Bool {
    get {
        return head < array.count
    }
}
class func from(array: [T], stepSize size: Int = 10, startingAt startIndex: Int = 0) -> ArrayIterator<T>{
    
    let a = ArrayIterator<T>()
    a.array = array
    a.stepSize = size
    a.head = startIndex
    return a
}

func next() -> Array<T>? {
    guard head < array.count else {
        return nil
    }
    
    defer {
        head = head + stepSize
    }
    
    guard stepSize < array.count else {
        return array
    }
    
    if let _ = array[safe: (head + stepSize - 1)] {
        return Array(array[head..<head + stepSize])
        
    } else {
        let remaider = (head + stepSize - 1) % array.count
        return Array(array[head..<(head + stepSize - 1 - remaider)])
    }
}
Victor Benetatos
  • 396
  • 3
  • 12