📜 ⬆️ ⬇️

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 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: 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 some kind of array :

let numbersArray = [0, 1, 2] 

... and then "walk" through it:

 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 for cycles, the type of the variable must implement the Sequence protocol . Among other things, this protocol requires the type to have a associatedtype Iterator , which in turn must implement the requirements of the IteratorProtocol protocol, as well as the factory method makeIterator() , which returns a specific “iterator” for this type:

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

The IteratorProtocol protocol, in 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 Array type implements the Sequence protocol (though not directly, but through the protocol inheritance chain: MutableCollection inherits Collection requirements, and that is Sequence requirements), so its instances, in particular, can be “iterated” using for cycles.

Custom types


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

Suppose there is a type that represents a bookshelf that stores 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 the Shelf class, this class must comply with the requirements of the Sequence protocol. For this example, it will be enough just 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 IteratorProtocol protocol. 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() method of the ShelfIterator type ShelfIterator declared 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 nil if the sequence is empty. The defer block defer change code of the iterated collection, which deletes the element of the last iteration step immediately after the method returns.

Usage example:

 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)") } /* Ф. Достоевский – Идиот Ф. Достоевский – Братья Карамазовы Ф. Достоевский – Бедные люди */ 

Since all types used (including Array , the underlying Shelf ) are based on the semantics of values ​​(as opposed to references) , you can not worry that the value of the original variable will be changed during the iteration process. 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 return the current element during the iteration at any time, the first element of the iterated sequence and the value of the “flag” indicating elements in an iterable 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 next() method 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 currentElement() method. In the IteratorProtocol protocol, these two functions are combined into one.

The need for the first() method is 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 next() method returns nil , when the iteration is finished, the isDone() method 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 iterator parameter is declared inout , because 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 calling the next() method is 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 about design patterns:
Architectural pattern "Visitor" ("Visitor") in the universe of "iOS" and "Swift"

Source: https://habr.com/ru/post/437614/