Architectural pattern "Iterator" ("Iterator") in the universe "Swift"

    Iterator is one of the design patterns that programmers most often overlook, because its implementation is usually built directly into the standard programming language tools. However, this is also one of the behavioral patterns described in the book “Gangs of Four”, “GoF”, “Design Patterns” (“Design Patterns: Reusable Object-Oriented Software”) , and to understand its device never hurts, and sometimes it can even help.

    An "iterator" is a method of sequential access to all elements of a composite object (in particular, container types, such as an array or a set).

    Standard language tools


    Create any array :

    let numbersArray = [0, 1, 2]
    

    ... and then "walk" on it cycle :

    for number in numbersArray {
        print(number)
    }
    

    ... seems like a very natural action, especially for modern programming languages ​​such as Swift . However, behind the "scenes" of this simple action is the code that implements the principles of the "Iterator" pattern.

    In Swift, in order to be able to “iterate” a variable using- forcycles, the type of the variable must implement the protocolSequence . Among other things, this protocol requires the type to have associatedtypeIterator, which in turn must implement the requirements of the protocol IteratorProtocol, as well as a factory methodmakeIterator() that returns a specific “iterator” for this type:

    protocol Sequence {
        associatedtype Iterator : IteratorProtocol
        func makeIterator() -> Self.Iterator
        // Another requirements go here…
    }
    

    The protocol, IteratorProtocolin turn, contains only one method next(), which returns the next element in the sequence:

    protocol IteratorProtocol {
        associatedtype Element
        mutating func next() -> Self.Element?
    }
    

    It sounds like a lot of complex code, but in fact it is not. Below we will see this.

    The type Arrayimplements the protocol Sequence(though not directly, but through the chain of inheritance of the protocols : MutableCollectioninherits the requirements Collection, and that inherits the requirements Sequence), so its instances, in particular, can be "iterated" using- forcycles.

    Custom types


    What needs to be done to be able to iterate your own type? As is often the case, the easiest way to show is by example.

    Suppose there is a type representing a bookshelf that stores in itself a certain set of instances of a class, which in turn represents a book:

    struct Book {
        let author: String
        let title: String
    }
    struct Shelf {
        var books: [Book]
    }
    

    To be able to “iterate” an instance of a class Shelf, this class must comply with the protocol requirements Sequence. For this example, it will be sufficient only to implement the method makeIterator(), especially since the other protocol requirements have default implementations . This method should return an instance of the type implementing the protocol IteratorProtocol. Fortunately, in the case of Swift, this is very little very simple code:

    struct ShelfIterator: IteratorProtocol {
        private var books: [Book]
        init(books: [Book]) {
            self.books = books
        }
        mutating func next() -> Book? {
            // TODO: Return next underlying Book element.
        }
    }
    extension Shelf: Sequence {
        func makeIterator() -> ShelfIterator {
            return ShelfIterator(books: books)
        }
    }
    

    The next()type method is ShelfIteratordeclared mutating, because an instance of the type must in one way or another store the state corresponding to the current iteration:

    mutating func next() -> Book? {
        defer {
            if !books.isEmpty { books.removeFirst() }
        }
        return books.first
    }
    

    This implementation always returns the first element in the sequence, or nilif the sequence is empty. The block is deferwrapped with the change code of the iterated collection, which deletes the element of the last iteration step immediately after the method returns.

    Example of use:

    let book1 = Book(author: "Ф. Достоевский",
                     title: "Идиот")
    let book2 = Book(author: "Ф. Достоевский",
                     title: "Братья Карамазовы")
    let book3 = Book(author: "Ф. Достоевский",
                     title: "Бедные люди")
    let shelf = Shelf(books: [book1, book2, book3])
    for book in shelf {
        print("\(book.author) – \(book.title)")
    }
    /*
    Ф. Достоевский – Идиот
    Ф. Достоевский – Братья Карамазовы
    Ф. Достоевский – Бедные люди
    */
    

    Because Since all types used (including the Arrayunderlying ones Shelf) are based on the semantics of values ​​(as opposed to references) , you need not worry about the fact that the value of the original variable will be changed during the iteration. When dealing with types based on link semantics, this point should be kept in mind and taken into account when creating your own iterators.

    Classic functionality


    The classic “iterator” described in the book “Gangs of Four”, in addition to returning the next element of the iterated sequence, can also at any time return the current element during the iteration process, the first element of the iterated sequence and the value of the “flag” indicating whether elements in the iterated sequence relative to the current iteration step.

    Of course, it would be easy to declare a protocol, thus extending the capabilities of the standard IteratorProtocol:

    protocol ClassicIteratorProtocol: IteratorProtocol {
        var currentItem: Element? { get }
        var first: Element? { get }
        var isDone: Bool { get }
    }
    

    The first and current elements are returned as optional. source sequence may be empty.

    Option simple implementation:

    struct ShelfIterator: ClassicIteratorProtocol {
        var currentItem: Book? = nil
        var first: Book?
        var isDone: Bool = false
        private var books: [Book]
        init(books: [Book]) {
            self.books = books
            first = books.first
            currentItem = books.first
        }
        mutating func next() -> Book? {
            currentItem = books.first
            books.removeFirst()
            isDone = books.isEmpty
            return books.first
        }
    }
    

    In the original description of the pattern, the method next()changes the internal state of the iterator to go to the next element and is of type Void, and the current element is returned by the method currentElement(). In the protocol, IteratorProtocolthese two functions are combined into one.

    The need for a method is first()also doubtful, since the iterator does not change the original sequence, and we always have the opportunity to refer to its first element (if present, of course).

    And, since the method next()returns nilwhen the iteration is completed, the method isDone()also becomes useless.

    However, for academic purposes, it is quite possible to come up with a function that could use the full functionality:

    func printShelf(with iterator: inout ShelfIterator) {
        var bookIndex = 0
        while !iterator.isDone {
            bookIndex += 1
            print("\(bookIndex). \(iterator.currentItem!.author) – \(iterator.currentItem!.title)")
            _ = iterator.next()
        }
    }
    var iterator = ShelfIterator(books: shelf.books)
    printShelf(with: &iterator)
    /*
    1. Ф. Достоевский – Идиот
    2. Ф. Достоевский – Братья Карамазовы
    3. Ф. Достоевский – Бедные люди
    */
    

    The parameter is iteratordeclared inoutbecause its internal state changes during the execution of the function. And when a function is called, the iterator instance is not transmitted directly by its own value, but by reference.

    The result of the method call is next()not used, imitating the absence of the return value of the textbook implementation.

    Instead of conclusion


    It seems that is all I wanted to say this time. All beautiful code and deliberate writing it!

    My other articles on design patterns:
    Architectural pattern "Visitor" ("Visitor") in the universe of "iOS" and "Swift"

    Also popular now: