Swift Algorithms: Getting Started

Learn about Apple’s open-source Swift Algorithms package, which can be used to simplify complex code and improve its performance. By Ehab Amer.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Working With Product

Next, you’ll learn about Product. The previous methods both created different variations within the same set, but this method is slightly different. It works with two collections — not one — and it gives you all the combinations of items in the first collection with items in the second collection. If that’s confusing, don’t worry; you’ll see an example in a moment.

On your Product playground page, add the following:

func doSomething(year: Int, season: String) {
  print("In \(season) in the year \(year) something happened...")
}

for year in years {
  for season in seasons {
    doSomething(year: year, season: season)
  }
}

In the code above, years is a sequence from 2000 to 2020, seasons has the four seasons of the year, and you’re matching seasons with the years.

Note: years and seasons are both defined in SampleData.swift.

This code is simple to understand. But, it requires you to process all the items there and then within the nested for-loops. What if you want to save processing to later? That’s where using something Swift Algorithms can do much better.

This is what Product does:

let productResults = product(years, seasons)

for (year, season) in productResults {
  doSomething(year: year, season: season)
}

Here you are using product(_:_) from Swift Algorithms to produce a result which can then be iterated whenever you want. In the simple example above, you do the same as the nested for loops, but crucially, you can see you have split the calculation of the product from the usage of the calculation’s results. Neat!

Working With Chunked

Chunked is the next algorithm, and it helps you break a collection down into smaller collections, processing each “chunk” in turn. You can define the way the main collection is broken into chunks however you wish.

To understand this better, try implementing it using sample data from the starter project. marvelMoviesWithYears is an array of (String, Int) tuples containing a movie name and its year of release. The goal is to break this collection down into groups of movies released within the same year.

Start building a solution by adding the following to your Chunked playground page:

var groupedMovies: [[(String, Int)]] = []
var currentYear = marvelMoviesWithYears.first?.1
var currentGroup: [(String, Int)] = []

Here’s what each of these variables are for:

  • groupedMovies: Where you’ll store the final chunks.
  • currentYear: A temporary variable holding the year you’re currently checking.
  • currentGroup: The chunk you’re currently building.

Now, add the following code:

// 1
for (movie, year) in marvelMoviesWithYears {
  if currentYear == year {
    // 2
    currentGroup.append((movie, year))
  } else {
    // 3
    groupedMovies.append(currentGroup)
    currentGroup = [(movie, year)]
    currentYear = year
  }
}

In the code above:

  1. You’re iterating over the entire marvelMoviesWithYears collection.
  2. If the item’s year is the same as the current value you’re comparing against, it belongs to the chunk you’re working on.
  3. Otherwise, that chunk is complete; add it to the final result. In this case, prepare a new chunk with the new movie and use its year as your new comparison value.

After the loop concludes, groupedMovies contains all the chunks you want. Print them to see the results:

for group in groupedMovies {
  print(group)
}
[("Iron Man", 2008), ("The Incredible Hulk", 2008)]
[("Iron Man 2", 2010)]
.
.
[("Avengers: Infinity War", 2018), ("Ant-Man and the Wasp", 2018)]

Swift Algorithms provides this implementation for you, so you don’t need to worry about any of its underlying details. All you need to define is the condition between an item and the one next to it.

Add the following to your playground:

let chunks = marvelMoviesWithYears.chunked { $0.1 == $1.1 }
for group in chunks {
  print(group)
}

This will produce exactly the same result. All you did was specify that the second property in the tuple of the first object equals that of the second object, and the method did all the work for you.

The code above uses Collection.chunked(by:), which takes a closure. There’s another variation, Collection.chunked(on:), that makes things simpler if your expression is a simple equality check. You would use it like this:

let sameChunks = marvelMoviesWithYears.chunked { $0.1 }

In this form, the closure identifies which part of each object to use in the equality comparison.

Working With Chain

The opposite of breaking down a collection into chucks is to chain the chunks back together. The standard library already has Array.joined() for when the items in the array are sequences. So what’s new about chain(_:_:)?

On your Chain playground page, add the following:

let closedRange = 1...10
let closedRange2 = 11...15
let intArray = [11, 12, 13, 14, 15]

The first two variables are two collections of type ClosedRange. The third is a simple integer array. If you try to join closedRange and intArray together, the compiler won’t be very happy:

let joinedLists = [closedRange, intArray].joined() // error

For Array.joined() to work, all the collections in the array need to be of identical type. Either all elements are arrays, or closed ranges or whatever kind of sequence used, and they must match, so only this would work:

let joinedClosedRanges = [closedRange, closedRange2].joined()

for item in joinedClosedRanges {
  print(item)
}

But the new chain(_:_:) doesn’t have this restriction. Although it’s slightly limited by the number of sequences it can concatenate, it can accept different sequence types. Try the following in your playground:

let joinedItems = chain(closedRange, intArray)

for item in joinedItems {
  print(item)
}

As you can see, chain(_:_:) concatenated the two different sequence types successfully. However, the result from chain(_:_:) isn’t a single sequence. It’s a wrapper on top of the two collections that acts as a collection. It conforms to Collection, BidirectionalCollection and RandomAccessCollection if the two underlying sequences conform to them.

Working With Cycle

Sometimes you might want to repeat a sequence a number of times. For example you might want to repeat the numbers 1 through 5, ten times.

Add the following to your Cycle playground page:

var smallList = [1, 2, 3, 4, 5]
let repeatedAndJoined = repeatElement(smallList, count: 10).joined()

repeatedAndJoined.count // 50
for item in repeatedAndJoined {
  print(item)
}

This code repeats a collection of the numbers 1 through 5, ten times with the call to repeatElement(_:count:). Then, it joins them up into one long collection with the call to joined(). Finally, it shows the count which we expect to be 50 and prints out the final collection.

Another way to do the same thing is to use the new Collection.cycled(times:). Add the following:

let cycles = smallList.cycled(times: 10)

cycles.count // 50

for item in cycles {
  print(item)
}

The call to cycled(:) did the same thing! The implementation of Collection.cycled(times:) in the Swift Algorithms package repeats the elements and joins them, just like what you did before.

So what’s different about it? What if you want to have an infinite sequence? What if you want your small collection to repeat indefinitely? The first thing that would come to your mind is how such a sequence would be stored.

This is what’s beautiful about how Swift Algorithms make use of lazy collections. There’s another method called Collection.cycled(). It doesn’t take any number of repetitions and so when you iterate through the resulting collection, it will loop indefinitely.

Add the following to your playground:

let infiniteCycles = smallList.cycled()

If you try to print the values here, it’ll keep writing numbers forever or until Xcode crashes.

This didn’t actually build a collection with a size of infinity! Instead, it created a wrapper above your collection that will keep going over the collection as long as you’re reading values from it.