Sequence Hacking in Swift (III): Building Custom Sequences for Fun and Profit - Digital Leaves
1998
post-template-default,single,single-post,postid-1998,single-format-standard,ajax_fade,page_not_loaded,,select-theme-ver-3.7,wpb-js-composer js-comp-ver-5.0.1,vc_responsive

Sequence Hacking in Swift (III): Building Custom Sequences for Fun and Profit

Hi there! This is the third and final episode of the “Sequence Hacking in Swift” series. In the first article, we learned what a sequence was and how to use its basic functions (map, filter, reduce…). In the following post, we learned about extending sequences and performing custom operations on them. Today, we are going to focus on creating our own custom sequences.

Why?

The first question you may ask yourself when building a custom sequence is “why?”. Before actually implementing a custom sequence, ask yourself if the behavior you need can be obtained by using or extending one of the existing sequence types (like collection, array, set, etc…). Sometimes, that is enough, but other times, it may be really convenient to create your own. Some of the reasons might be:

  • Increased performance or special use case: you want to build a highly efficient sequence for a specific task, maybe using optimized algorithms for iterating through a series of elements (that might be inefficient if managed through a class API) or employing specific techniques to do the task faster or consuming less resources.
  • Reusability: you may want to build a custom sequence for a concrete task or series of tasks that you perform a lot in your applications. Sometimes, creating a custom sequence that encapsulates a concrete behavior can be a nice way of having a ready-to-add library for your projects. I certainly have several of those in my repertoire.
  • Specific requirements: you may need some specific requirements for concrete applications, like keeping the elements of a sequence in memory for efficiency, or retrieving/caching them beforehand and then iterating through them. You may need to free some resources after extracting a item from the iteration… there are lots of specific conditions that may need you to implement a custom sequence.
  • Legacy functionality: you may need to access legacy functions or functionality outside the scope of the modern Cocoa frameworks for Swift (i.e: old C-style cryptographic stuff, or old Carbon libraries) that need some specific treatment when iterating through the elements of the sequence.

Whatever the reason, once you decide that the right solution for you is creating your custom sequence, you need to understand how to implement the basic functionality of your custom class.

The Basics

You might recall from the previous article that a Sequence is basically an Iterator (a class following the IteratorProtocol protocol) with a makeIterator() function, an associated Iterator.Element type (that defines the type of elements that this Sequence contains) and a set of  associated functions like map, reduce, flatMap, filter, etc.

public protocol Sequence {
  associatedtype Iterator : IteratorProtocol
  public func makeIterator() -> Self.Iterator

  public func map<T>(_ transform: (Self.Iterator.Element) throws -> T) rethrows -> [T]
  public func filter(_ isIncluded: (Self.Iterator.Element) throws -> Bool) rethrows -> [Self.Iterator.Element]
  public func forEach(_ body: (Self.Iterator.Element) throws -> Swift.Void) rethrows
  // ... other methods associated to Sequence
}

So in order to build a Sequence, we just need to build a class that implements this IteratorProtocol to act as the iterator for the elements of the sequence, and create the makeIterator() class. The iterator, in turn, is basically a “next()” function that retrieves the next element in the sequence, or nil if there are no more elements in it.

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

We can implement our custom Sequence with either a class or a struct. Usually a class is used, but it’s up to you to choose a struct if this entity will make more sense as a struct than as a class. Let’s start with a basic example to understand how this works, a simple “word iterator” that is a Sequence of characters (if that sounds familiar, it is because this is basically a Swift string. A Swift string is, actually, nothing more than a sequence of characters. This is probably a very dumb example, but will help you understand the basic concepts for creating your custom Sequences.

A basic word sequence

When building a custom sequence, it makes sense to think bottom-to-top, from the elements to the sequence. Think about the elements, its nature (type), how are they distributed in the sequence and how can I iterate from one to the other. If the sequence is finite, when exactly does it end?

Our iterator will start with a String, in the String.startIndex, and then iterate by advancing this index one by one:

class WordIterator: IteratorProtocol {
   var word: String
   var index: String.Index
 
   init(_ word: String) {
     self.word = word
     index = self.word.startIndex
   }
 
   func next() -> Character? {
      if (index < self.word.endIndex) {
         let character = self.word[index]
         index = self.word.index(after: index)
         return character
      } else { return nil }
   }
}

Notice how we return nil when we get past the end index of the string. Notice also how, given that our Sequence is a String, our Iterator deals with Characters. This is defined by the exact nature of your Sequence. For instance, if you build a Sequence that represent the binary numbers (returing 2, 4, 8, 16, 32, etc), your IteratorProtocol subclass will return integers, so the Iterator.Element type will be Int.

Now, we can build our Sequence by returning an instance of WordIterator in our makeIterator() method:

class Word: Sequence {
   var word: String
   init(_ word: String) { self.word = word }
 
   func makeIterator() -> WordIterator {
      return WordIterator(self.word)
   }
}

Easy as pie, right? For such simple sequences, we can always build an inline class and return it in the makeIterator() method directly. So, we could get rid of the WordIterator class and just encapsulate all the functionality in a self-contained class:

class Word: Sequence {
   var word: String
 
   init(_ word: String) { self.word = word }

   func makeIterator() -> AnyIterator<Character> {
     var index = word.startIndex
 
     return AnyIterator {
       if (index < self.word.endIndex) {
         let character = self.word[index]
         index = self.word.index(after: index)
         return character
       } else { return nil }
     }
   }
}

Here, the AnyIterator<Character> is a “placeholder” iterator class that’s part of the basic Swift SDK, and uses generics to forward the “next()” method to the element type contained. So in this case, we just build a implicit String -> Character iterator that does exactly what our WordIterator did, and return a AnyIterator<Character> as the result of makeIterator() to clearly hint the compiler that we are returning an iterator containing characters from our String sequence.

Now, we can use our cool custom Sequence to iterate through any string, yay!

let word = Word("Supercalifragilisticexpialidocious")
for character in word {
   print("\(character)")
}
// prints:
S
u
p
e
r
c
a
l
...

Hacking Sequences: the never ending emoji sequence.

Now it’s time to break the rules we have just learned to have some fun with sequences. If you think about it, there’s nowhere in the Sequence description or its protocol API specifying that a Sequence must be finite, so let’s take that approach to build a never-ending emoji sequence for fun.

For doing this, we’ll take advantage of the fact that we can encapsulate both the IteratorProtocol and the Sequence functionality in an entity that implements both. If we declare a class or struct as implementing Sequence and IteratorProtocol, we will just need to specify a “next()” method, and we won’t need to define the makeIterator() method: it will gladly return itself as a default behavior, due to Swift type inference and the way in which the method is defined in “Sequence”. Let’s see our EmojiSequence struct definition (yes, we are using a struct here to illustrate how you can use structs for custom Sequences when they are as simple as in this case).

struct EmojiSequence: Sequence, IteratorProtocol {
   fileprivate let emojis = "😀😃😄😁😆😅😂🤣☺️😊😇🙂🙃😉😌😍😘😗😙😚😋😜😝😛🤑🤗🤓😎🤡🤠😏😒😞😔😟😕🙁☹️😣😖😫😩😤😠😡😶😐😑😯😦😧😮😲😵😳😱😨😰😢😥🤤😭😓😪😴🙄🤔🤥😬🤐🤢🤧😷🤒🤕"
   fileprivate var emojisNumber: UInt32 { return UInt32(emojis.characters.count) }
 
   fileprivate func randomEmojiIndex() -> String.Index {
      let rndEmojiNumber = arc4random_uniform(emojisNumber)
      return emojis.index(emojis.startIndex, offsetBy: String.IndexDistance(rndEmojiNumber))
   }
 
   func next() -> Character? {
      return emojis.characters[randomEmojiIndex()]
   }
}

The idea is simple, the “next()” method will call the randomEmojiIndex() method to generate a random string index, thus pointing to a random character of our emoji string with each call, and then we will just return the character pointed by this index.

The funny thing about this sequence is that it never ends (because next() never returns nil), so if you do something as simple as this:

let myEmojis = EmojiSequence()
for i in myEmojis { print(i) }
// prints:
😄
🙁
😛
🤔
🤢
😠
🤐
...

Your application will enter an infinite loop. You can always set a safety variable, and return nil after a concrete (or random) number of emojis served, but then it won’t be that fun, now would it?

Where to go from here

In this final episode of the “Sequence Hacking in Swift”, we have learned how to implement our custom sequence types, either structs or classes. We also learned not to limit ourselves to the strict definition of a Sequence to get some fun results. Next, I want to write about generics in Swift and how to get the most of them. But that is another story for another day. Do you have any comments? Some funny sequences of your own you want to share with us? Let us know in the comments!

Ignacio Nieto is the owner of the Digital Leaves blog, he loves building successful products that improve people’s lives. He has expertise in iOS, Full Stack JS, Android, RESTful backends, OAuth and cryptography.

No Comments

Post a Comment

Join the Digital Tips List
Want to become a freelance iOS developer or improve your skills? Subscribe to the Digital Tips list for weekly tips, useful tutorials and more to help you grow as a developer.
I hate spam, and I promise I'll keep your email address safe.