19 min read

In this article by Keith Elliott, author of, Swift 3 New Features , we are focusing on collection and closure changes in Swift 3. There are several nice additions that will make working with collections even more fun. We will also explore some of the confusing side effects of creating closures in Swift 2.2 and how those have been fixed in Swift 3.

(For more resources related to this topic, see here.)

Collection and sequence type changes

Let’s begin our discussion with Swift 3 changes to Collection and Sequence types. Some of the changes are subtle and others are bound to require a decent amount of refactoring to your custom implementations. Swift provides three main collection types for warehousing your values: arrays, dictionaries, and sets. Arrays allow you to store values in an ordered list. Dictionaries provide unordered the key-value storage for your collections. Finally, sets provide an unordered list of unique values (that is, no duplicates allowed).

Lazy FlatMap for sequence of optionals [SE-0008]

Arrays, sets, and dictionaries are implemented as generic types in Swift. They each implement the new Collection protocol, which implements the Sequence protocol. Along this path from top-level type to Sequence protocol, you will find various other protocols that are also implemented in this inheritance chain. For our discussion on flatMap and lazy flatMap changes, I want to focus in on Sequences.

Sequences contain a group of values that allow the user to visit each value one at a time. In Swift, you might consider using a for-in loop to iterate through your collection. The Sequence protocol provides implementations of many operations that you might want to perform on a list using sequential access, all of which you could override when you adopt the protocol in your custom collections. One such operation is the flatMap function, which returns an array containing the flattened (or rather concatenated) values, resulting from a transforming operation applied to each element of the sequence.

let scores = [0, 5, 6, 8, 9]
    .flatMap{ [$0, $0 * 2] }
print(scores)  // [0, 0, 5, 10, 6, 12, 8, 16, 9, 18]

 

In our preceding example, we take a list of scores and call flatMap with our transforming closure. Each value is converted into a sequence containing the original value and a doubled value. Once the transforming operations are complete, the flatMap method flattens the intermediate sequences into a single sequence.

We can also use the flatMap method with Sequences that contain optional values to accomplish a similar outcome. This time we are omitting values from the sequence we flatten by return nil on the transformation.

let oddSquared = [1, 2, 3, 4, 5, 10].flatMap { n in
    n % 2 == 1 ? n*n : nil
}
print(oddSquared) // [1, 9, 25]

The previous two examples were fairly basic transformations on small sets of values. In a more complex situation, the collections that you need to work with might be very large with expensive transformation operations. Under those parameters, you would not want to perform the flatMap operation or any other costly operation until it was absolutely needed. Luckily, in Swift, we have lazy operations for this very use case. Sequences contain a lazy property that returns a LazySequence that can perform lazy operations on Sequence methods. Using our first example, we can obtain a lazy sequence and call flatMap to get a lazy implementation. Only in the lazy scenario, the operation isn’t completed until scores is used sometime later in code.

let scores = [0, 5, 6, 8, 9]
    .lazy
    .flatMap{ [$0, $0 * 2] } // lazy assignment has not executed

for score in scores{ 
    print(score)
}

The lazy operation works, as we would expect in our preceding test. However, when we use the lazy form of flatMap with our second example that contains optionals, our flatMap executes immediately in Swift 2. While we expected oddSquared variable to hold a ready to run flatMap, delayed until we need it, we instead received an implementation that was identical to the non-lazy version.

let oddSquared = [1, 2, 3, 4, 5, 10]
    .lazy               // lazy assignment but has not executed
    .flatMap { n in
    n % 2 == 1 ? n*n : nil
}

for odd in oddSquared{
    print(odd)
}

Essentially, this was a bug in Swift that has been fixed in Swift 3.

You can read the proposal at the following link https://github.com/apple/swift-evolution/blob/master/proposals/0008-lazy-flatmap-for-optionals.md

Adding the first(where:) method to sequence

A common task for working with collections is to find the first element that matches a condition. An example would be to ask for the first student in an array of students whose test scores contain a 100. You can accomplish this using a predicate to return the filtered sequence that matched the criteria and then just give back the first student in the sequence. However, it would be much easier to just call a single method that could return the item without the two-step approach. This functionality was missing in Swift 2, but was voted in by the community and has been added for this release. In Swift 3, there is a now an extension method on the Sequence protocol to implement first(where:).

["Jack", "Roger", "Rachel", "Joey"].first { (name) -> Bool in
    name.contains("Ro")
}  // =>returns Roger

This first(where:) extension is a nice addition to the language because it ensures that a simple and common task is actually easy to perform in Swift.

You can read the proposal at the following link https://github.com/apple/swift-evolution/blob/master/proposals/0032-sequencetype-find.md.

Add sequence(first: next:) and sequence(state: next:)

public func sequence<T>(first: T, next: @escaping (T) -> T?) -> UnfoldSequence<T, (T?, Bool)>

public func sequence<T, State>(state: State, next: @escaping (inout State) -> T?) -> UnfoldSequence<T, State>

public struct UnfoldSequence<Element, State> : Sequence, IteratorProtocol

These two functions were added as replacements to the C-style for loops that were removed in Swift 3 and to serve as a compliment to the global reduce function that already exists in Swift 2. What’s interesting about the additions is that each function has the capability of generating and working with infinite sized sequences. Let’s examine the first sequence function to get a better understanding of how it works.

/// - Parameter first: The first element to be returned from the sequence.
/// - Parameter next: A closure that accepts the previous sequence element and
///   returns the next element.
/// - Returns: A sequence that starts with `first` and continues with every
///   value returned by passing the previous element to `next`.
///
func sequence<T>(first: T, next: @escaping (T) -> T?) -> UnfoldSequence<T, (T?, Bool)>

The first sequence method returns a sequence that is created from repeated invocations of the next parameter, which holds a closure that will be lazily executed. The return value is an UnfoldSequence that contains the first parameter passed to the sequence method plus the result of applying the next closure on the previous value. The sequence is finite if next eventually returns nil and is infinite if next never returns nil.

let mysequence = sequence(first: 1.1) { $0 < 2 ? $0 + 0.1 : nil }
for x in mysequence{
    print (x)
} // 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0

In the preceding example, we create and assign our sequence using the trailing closure form of sequence(first: next:). Our finite sequence will begin with 1.1 and will call next repeatedly until our next result is greater than 2 at which case next will return nil. We could easily convert this to an infinite sequence by removing our condition that our previous value must not be greater than 2.

/// - Parameter state: The initial state that will be passed to the closure.
/// - Parameter next: A closure that accepts an `inout` state and returns the
///   next element of the sequence.
/// - Returns: A sequence that yields each successive value from `next`.
///
public func sequence<T, State>(state: State, next: (inout State) -> T?) -> UnfoldSequence<T, State>

The second sequence function maintains mutable state that is passed to all lazy calls of next to create and return a sequence. This version of the sequence function uses a passed in closure that allows you to update the mutable state each time the next called. As was the case with our first sequence function, a finite sequence ends when next returns a nil. You can turn an finite sequence into an infinite one by never returning nil when next is called.

Let’s create an example of how this version of the sequence method might be used. Traversing a hierarchy of views with nested views or any list of nested types is a perfect task for using the second version of the sequence function. Let’s create a an Item class that has two properties. A name property and an optional parent property to keep track of the item’s owner. The ultimate owner will not have a parent, meaning the parent property will be nil.

class Item{
    var parent: Item?
    var name: String = ""
}

Next, we create a parent and two nested children items. Child1 parent will be the parent item and child2 parent will be child1.

let parent = Item()
parent.name = "parent"

let child1 = Item()
child1.name = "child1"
child1.parent = parent

let child2 = Item()
child2.name = "child2"
child2.parent = child1

Now, it’s time to create our sequence. The sequence needs two parameters from us: a state parameter and a next closure. I made the state an Item with an initial value of child2. The reason for this is because I want to start at the lowest leaf of my tree and traverse to the ultimate parent. Our example only has three levels, but you could have lots of levels in a more complex example. As for the next parameter, I’m using a closure expression that expects a mutable Item as its state. My closure will also return an optional Item. In the body of our closure, I use our current Item (mutable state parameter) to access the item’s parent. I also updated the state and return the parent.

let itemSeq = sequence(state: child2, next: {
    (next: inout Item)->Item? in
    let parent = next.parent
    next = parent != nil ? parent! : next
    return parent
})


for item in itemSeq{
    print("name: (item.name)")
}

There are some gotchas here that I want to address so that you will better understand how to define your own next closure for this sequence method.

  • The state parameter could really be anything you want it to be. It’s for your benefit in helping you determine the next element of the sequence and to give you relevant information about where you are in the sequence. One idea to improve our example above would be to track how many levels of nesting we have. We could have made our state a tuple that contained an integer counter for the nesting level along with the current item.
  • The next closure needs to be expanded to show the signature. Because of Swift’s expressiveness and conciseness, when it comes to closures, you might be tempted to convert the next closure into a shorter form and omit the signature. Do not do this unless your next closure is extremely simple and you are positive that the compiler will be able to infer your types. Your code will be harder to maintain when you use the short closure format, and you won’t get extra points for style when someone else inherits it.
  • Don’t forget to update your state parameter in the body of your closure. This really is your best chance to know where you are in your sequence. Forgetting to update the state will probably cause you to get unexpected results when you try to step through your sequence.
  • Make a clear decision ahead of the time about whether you are creating a finite or infinite sequence. This decision is evident in how you return from your next closure. An infinite sequence is not bad to have when you are expecting it. However, if you iterate over this sequence using a for-in loop, you could get more than you bargained for, provided you were assuming this loop would end.

A new Model for Collections and Indices [SE-0065]Swift 3 introduces a new model for collections that moves the responsibility of the index traversal from the index to the collection itself. To make this a reality for collections, the Swift team introduced four areas of change:

  • The Index property of a collection can be any type that implements the Comparable protocol
  • Swift removes any distinction between intervals and ranges, leaving just ranges
  • Private index traversal methods are now public
  • Changes to ranges make closed ranges work without the potential for errors

You can read the proposal at the following link https://github.com/apple/swift-evolution/blob/master/proposals/0065-collections-move-indices.md.

Introducing the collection protocol

In Swift 3, Foundation collection types such as Arrays, Sets, and Dictionaries are generic types that implement the newly created Collection protocol. This change was needed in order to support traversal on the collection. If you want to create custom collections of your own, you will need to understand the Collection protocol and where it lives in the collection protocol hierarchy. We are going to cover the important aspects to the new collection model to ease you transition and to get you ready to create custom collection types of your own.

The Collection protocol builds on the Sequence protocol to provide methods for accessing specific elements when using a collection. For example, you can use a collection’s index(_:offsetBy:) method to return an index that is a specified distance away from the reference index.

let numbers = [10, 20, 30, 40, 50, 60]
let twoAheadIndex = numbers.index(numbers.startIndex, offsetBy: 2)
print(numbers[twoAheadIndex]) //=> 30  

In the preceding example, we create the twoAheadIndex constant to hold the position in our numbers collection that is two positions away from our starting index. We simply use this index to retrieve the value from our collection using subscript notation.

Conforming to the Collection Protocol

If you would like to create your own custom collections, you need to adopt the Collection protocol by declaring startIndex and endIndex properties, a subscript to support access to your elements, and the index(after: ) method to facilitate traversing your collection’s indices.

When we are migrating existing types over to Swift 3, the migrator has some known issues with converting custom collections. It’s likely that you can easily resolve the compiler issues by checking the imported types for conformance to the Collection protocol.

Additionally, you need to conform to the Sequence and IndexableBase protocols as the Collection protocol adopts them both.

public protocol Collection : Indexable, Sequence { … }

A simple custom collection could look like the following example. Note that I have defined my Index type to be an Int. In Swift 3, you define the Index to be any type that implements the Comparable protocol.

struct MyCollection<T>: Collection{
    typealias Index = Int
    var startIndex: Index
    var endIndex: Index
    
    var _collection: [T]
    
    subscript(position: Index) -> T{
        return _collection[position]
    }
    
    func index(after i: Index) -> Index {
        return i + 1
    }
    
    init(){
        startIndex = 0
        endIndex = 0
        _collection = []
    }
    
    mutating func add(item: T){
        _collection.append(item)
    }
}

var myCollection: MyCollection<String> = MyCollection()
myCollection.add(item: "Harry")
myCollection.add(item: "William")
myCollection[0]

The Collection protocol has default implementations for most of its methods, the Sequence protocols methods, and the IndexableBase protocols methods. This means you are only required to provide a few things of your own. You can, however, implement as many of the other methods as make sense for your collection.

New Range and Associated Indices Types

Swift 2’s Range<T>, ClosedInterval<T>, and OpenInterval<T> are going away in Swift 3. These types are being replaced with four new types. Two of the new range types support general ranges with bounds that implement the Comparable protocol: Range<T> and ClosedRange<T>. The other two range types conform to RandomAccessCollection. These types support ranges whose bounds implement the Strideable protocol.

Last, ranges are no longer iterable since ranges are now represented as a pair of indices. To keep legacy code working, the Swift team introduced an associated indices type, which is iterable. In addition, three generic types were created to provide a default indices type for each type of collection traversal category. The generics are DefaultIndices<C>, DefaultBidirectionalIndices<C>, and DefaultRandomAccessIndices<C>; each stores its underlying collection for traversal.

Quick Takeaways

I covered a lot of stuff in a just a few pages on collection types in Swift 3. Here are the highlights to keep in mind about the collections and indices.

  • Collections types (built-in and custom) implement the Collection protocol.
  • Iterating over collections has moved to the collection—the index no longer has that ability.
  • You can create your own collections by adopting the Collection protocol. You need to implement:
    • startIndex and endIndex properties
    • The subscript method to support access to your elements
    • The index(after: ) method to facilitate traversing your collection’s indices

Closure changes for Swift 3

A closure in Swift is a block of code that can be used in a function call as a parameter or assigned to a variable to execute their functionality at a later time. Closures are a core feature to Swift and are familiar to developers that are new to Swift as they remind you of lambda functions in other programming languages. For Swift 3, there were two notable changes that I will highlight in this section. The first change deals with inout captures. The second is a change that makes non-escaping closures the default.

Limiting inout Capture of @noescape Closures]In Swift 2, capturing inout parameters in an escaping closure was difficult for developers to understand. Closures are used everywhere in Swift especially in the standard library and with collections. Some closures are assigned to variables and then passed to functions as arguments. If the function that contains the closure parameter returns from its call and the passed in closure is used later, then you have an escaping closure. On the other hand, if the closure is only used within the function to which it is passed and not used later, then you have a nonescaping closure. The distinction is important here because of the mutating nature of inout parameters.

When we pass an inout parameter to a closure, there is a possibility that we will not get the result we expect due to how the inout parameter is stored. The inout parameter is captured as a shadow copy and is only written back to the original if the value changes. This works fine most of the time. However, when the closure is called at a later time (that is, when it escapes), we don’t get the result we expect. Our shadow copy can’t write back to the original. Let’s look at an example.

var seed = 10
let simpleAdderClosure = { (inout seed: Int)->Int in
    seed += 1
    return seed * 10
}

var result = simpleAdderClosure(&seed)  //=> 110
print(seed) // => 11

In the preceding example, we get what we expect. We created a closure to increment our passed in inout parameter and then return the new parameter multiplied by 10. When we check the value of seed after the closure is called, we see that the value has increased to 11.

In our second example, we modify our closure to return a function instead of just an Int value. We move our logic to the closure that we are defining as our return value.

let modifiedClosure = { (inout seed: Int)-> (Int)->Int in
    return { (Int)-> Int in
        seed += 1
        return seed * 10
    }
}

print(seed)  //=> 11
var resultFn = modifiedClosure(&seed)
var result = resultFn(1)
print(seed) // => 11

This time when we execute the modifiedClosure with our seed value, we get a function as the result. After executing this intermediate function, we check our seed value and see that the value is unchanged; even though, we are still incrementing the seed value.

These two slight differences in syntax when using inout parameters generate different results. Without knowledge of how shadow copy works, it would be hard understand the difference in results. Ultimately, this is just another situation where you receive more harm than good by allowing this feature to remain in the language.

You can read the proposal at the following link https://github.com/apple/swift-evolution/blob/master/proposals/0035-limit-inout-capture.md.

Resolution

In Swift 3, the compiler now limits inout parameter usage with closures to non-escaping (@noescape). You will receive an error if the compiler detects that your closure escapes when it contains inout parameters.

Making non-escaping closures the default [SE-0103]

You can read the proposal at https://github.com/apple/swift-evolution/blob/master/proposals/0103-make-noescape-default.md.

In previous versions of Swift, the default behavior of function parameters whose type was a closure was to allow escaping. This made sense as most of the Objective-C blocks (closures in Swift) imported into Swift were escaping. The delegation pattern in Objective-C, as implemented as blocks, was composed of delegate blocks that escaped. So, why would the Swift team want to change the default to non-escaping as the default?

The Swift team believes you can write better functional algorithms with non-escaping closures. An additional supporting factor is the change to require non-escaping closures when using inout parameters with the closure [SE-0035]. All things considered, this change will likely have little impact on your code. When the compiler detects that you are attempting to create an escaping closure, you will get an error warning that you are possibly creating an escaping closure. You can easily correct the error by adding @escaping or via the fixit that accompanies the error.


In Swift 2.2:
var callbacks:[String : ()->String] = [:]
func myEscapingFunction(name:String, callback:()->String){
    callbacks[name] = callback
}
myEscapingFunction("cb1", callback: {"just another cb"})
for cb in callbacks{
    print("name: (cb.0) value: (cb.1())") 
}
In Swift 3: 
var callbacks:[String : ()->String] = [:]
func myEscapingFunction(name:String, callback: @escaping ()->String){
    callbacks[name] = callback
}
myEscapingFunction(name:"cb1", callback: {"just another cb"})
for cb in callbacks{
    print("name: (cb.0) value: (cb.1())") 
}

Summary

In this article, we covered changes to collections and closures. You learned about the new Collection protocol that forms the base of the new collection model and how to adopt the protocol in our own custom collections. The new collection model made a significant change in moving collection traversal from the index to the collection itself. The new collection model changes are necessary in order to support Objective-C interactivity and to provide a mechanism to iterate over the collections items using the collection itself. As for closures, we also explored the motivation for the language moving to non-escaping closures as the default. You also learned how to properly use inout parameters with closures in Swift 3.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here