Changes to the Swift standard library in 2.0 betas 2..<5

So, I was waiting for enough library changes across a few betas to justify posting something, and then all of a sudden lots of stuff changed! So let’s start with:

Flat Map

Missed from my 2.0b1 changes post (shame on me) was a third kind of flatMap.

1.2 brought us flatMap for collections – given an array, and a function that transforms elements into arrays, it produces a new array with every element transformed, and then those arrays “flattened” into a single array. For example, suppose you had a function, extractLinks, that took a filename and returned an array of links found in that file:

func extractLinks(markdownFile: String) -> [NSURL]

If you had an array of filename strings (say from a directory listing), and you wanted an array of the links in all the files, you could use this function with flatMap to produce a single array (unlike map, which would generate an array of arrays):

let nestedArrays: [NSURL] = markdownFiles.flatMap(extractLinks)

There was also a flatMap for optionals. Given an optional, it takes a function that takes the possible value inside the optional, and applies a function that also returns an optional, and “flattens” the result of mapping an optional to another optional. For example, Array.first returns the first element of a collection, or nil if the array is empty and has no such element. Int.init(String) takes a string, and if it is a representation of an integer, returns that integer, or nil if it isn’t. To take the first element of an array, and turn it into an optional integer, you could use flatMap (unlike map, which will return an optional of an optional of an integer):

let a = ["1","2","3"]
let i: Int? = a.first.flatMap { Int($0) }

So what’s the new third kind of flatMap? It’s a mixture of the two – for when you have an array, and want to transform it with a function that returns an optionals, discarding any transforms that return nil. This is logical if you think of optionals as like collections with either zero or one element – flatMap would discard the empty results, and flatten the single-element collections into an array of elements.

For example, if you have an array of strings, and you want to transform it into integers, discarding any non-numeric strings:

let strings = ["1","2","elephant","3"]
let ints: [Int] = strings.flatMap { Int($0) }
// ints = [1,2,3]

If you’ve been following this blog for a while, you’ll recognize this is a function we’ve defined before as mapSome.

By the way, all these examples have been pulled from the book on Swift that Chris Eidhof and I have been writing. The chapter on optionals has just been added to the preview.

First-class Initializers

In beta 2 came the ability to use initializers, partially-applied methods, and enum cases, as first-class functions. This gives you the ability to pass them directly into higher-order functions without needing a closure expression:

// equivalent to [1,2,3].map { Optional.Some($0) }
let optionals = [1,2,3].map(Optional.Some)

// equivalent to [1,2,3].map { String($0) }
let strings = [1,2,3].map(String.init)

// equivalent to [1,2,3].map { 1.advancedBy($0) }
let plusones = [1,2,3].map(1.advancedBy)

Given this, you might wonder why I wrote strings.flatMap { Int($0) } in the earlier section, instead of strings.flatMap(Int.init). But this new ability is constrained by the fact that overload resolution works slightly differently for function invocation versus function assignment. If you try the latter version, you will get an error because there is no method for Int.init that takes just a single string as an argument. When you call Int("1"), you are really calling Int("1", radix: 10), with the compiler filling out the default argument for you. But it won’t do that when passing Int.init in to map.

String.init on integers works though, I guess because the overloading is less ambiguous? On the other hand, arrayOfCustomStructs.map(String.init) won’t compile, probably because there is ambiguity between String.init(T) and String.init(reflecting:T) (the debug version). So for now, you’ll have to stick with writing array.map { String($0) }, like an animal.

One other thing to be careful of: this new terse syntax doesn’t work for when you want to apply a method to the self argument. The following will compile, but won’t do what might be intended – reverse each of the arrays:

let reversedArrays = [[1,2,3],[4,5,6]].map(Array.reverse)

Instead, this will produce an array of functions – because Array.reverse returns a function that returns a function where the method will be called on the argument pased.

So instead, you would have to write this:

let reversedArrays = [[1,2,3],[4,5,6]].map { Array.reverse($0)() }
// which is just a long winded way of writing this…
let reversedArrays = [[1,2,3],[4,5,6]].map { $0.reverse() }
// but bear with me...

You could always write a version of map that takes curried functions and does the above for you:

extension CollectionType {
    func map<T>(@noescape transform: (Self.Generator.Element) -> () -> T) -> [T] {
        return self.map { transform($0)() }
    }
}

after defining which, .map(Array.reverse) will do what you probably want.1

This can also be nice to do for other functions. For example, if you defined a similar version of sort:

extension CollectionType {
    public func sort(isOrderedBefore: Self.Generator.Element -> Self.Generator.Element -> Bool) -> [Self.Generator.Element] {
        return self.sort { isOrderedBefore($0)($1) }
    }
}

then, after also overloading caseInsensitiveCompare to have a version that returns a boolean in Swift’s isOrderedBefore style, you could do the same with methods that compare:

import Foundation

extension String {
    func caseInsensitiveCompare(other: String) -> Bool {
        return self.caseInsensitiveCompare(other) == .OrderedAscending
    }
}

["Hello","hallo","Hullo","Hallo",].sort(String.caseInsensitiveCompare)
// returns ["hallo", "Hallo", "Hello", "Hullo"]

RangeReplaceableType: all your ExtensibleCollectionTypes are belong to us

ExtensibleCollectionType is gone. It used to provide append and extend, which are now amongst the features provided by RangeReplaceableCollectionType.

RangeReplaceableCollectionType is a great example of the power of protocol extensions. You implement one uber-flexible method, replaceRange, which takes a range to replace and a collection to replace with, and from that comes a whole bunch of derived methods for free:

  • append and extend: replace endIndex..<endIndex (i.e. nothing, at the end) with the
    new element/elements.
  • removeAtIndex and removeRange: replace i...i or subRange with an empty collection.
  • splice and insertAtIndex: replace atIndex..<atIndex (i.e. don't replace any elements but insert at that point) with a new element/elements.
  • removeAll: replace startIndex..<endIndex with an empty collection.

The two remaining functions from ExtensibleCollectionType – an empty initializer, and reserveCapacity, have also moved into RangeReplaceableCollectionType.

As always, if a specific collection type can use knowledge about its implementation to perform these functions more efficiently, it can provide custom versions that will take priority over the default protocol extention ones.

You get to be Sliceable. And you get to be Sliceable. Everybody gets to be Sliceable!

Things being Sliceable (i.e. having a subscript that takes a range, and returns a new collection of just that range) was incredibly useful. But it was also limiting, because you needed to rely on things implementing Sliceable.

But you could make anything sliceable if you needed it to be – by writing a wrapper view that took a subrange and implemented CollectionType to present only that subrange:

// note, won't compile in beta 4 because Sliceable is gone!
public struct SubSliceView<Base: CollectionType>: Sliceable {
    let collection: Base
    let bounds: Range<Base.Index>

    public var startIndex: Base.Index { return bounds.startIndex }
    public var endIndex: Base.Index { return bounds.endIndex }

    public subscript(idx: Base.Index) -> C.Generator.Element
    { return collection[idx] }

    typealias SubSlice = SubSliceView<Base>
    public subscript(bounds: Range<Base.Index>) -> SubSliceView<Base> {
        return SubSliceView(collection: collection, bounds: bounds)
    }
}

// AnyRandomAccessCollection is an example of a type that wasn't sliceable,
// but you could make it one by doing this:
extension AnyRandomAccessCollection: Sliceable {
    public subscript(bounds: Range<Index>) -> SubSliceView<AnyRandomAccessCollection> {
        return SubSliceView(collection: self, bounds: bounds)
    }
}

let a = [1,2,3]
let r = AnyRandomAccessCollection(a)
// dropFirst relies on things being sliceable:
print(dropFirst(dropFirst(r)).first)

As of beta 4, the new Slice type does pretty much this exact thing. Including the way the slice's start index is not zero based – if you create a slice starting at index 2, the startIndex of that slice will be 2. Another reason to avoid using indices directly in favour of things like for...in or higher-order functions like map. Index-based operations are so much easier to screw up, and your assumptions (e.g. every integer-indexed collection starts at zero) can easily be invalid.

The Sliceable protocol has been subsumed into CollectionType, and the default implementation for the ranged subscript is to return a Slice view onto the collection. So now every collection supports a slicing subscript.

CollectionType in turn now conforms to Indexable, which is the new home for startIndex, endIndex, and index-based subscript. Note, Indexable does not to conform to SequenceType – these two things are now brought together by CollectionType.

There is still a good reason for collections to implement their own custom slicing code – they can often do it more efficiently.

For example, UnsafeBufferPointer didn't used to be sliceable, but now is, with the default implementation. If you slice it, you'll get back a Slice:

[1,2,3].withUnsafeBufferPointer { buf -> Void in
    let slice = buf[2..<4]
    sizeofValue(slice)  // returns 32 bytes
}

sizeof returns 32, because the type has to contain both the base buffer (which is 16 bytes – the base pointer address and the length), plus the start and end of the subrange (another 16 bytes).2

But alternatively, UnsafeBufferPointer could use itself as its subslice, like so:

extension UnsafeBufferPointer {
    subscript(subRange: Range<Int>) -> UnsafeBufferPointer {
        return UnsafeBufferPointer(start: self.baseAddress + subRange.startIndex,
                                   count: subRange.count)
    }
}

If you do this, you’ll see that UnsafeBufferPointer‘s slice is now another UnsafeBufferPointer, and the memory requirement drops in half:

[1,2,3].withUnsafeBufferPointer { buf -> Void in
    let slice = buf[2..<4]
    sizeofValue(slice)  // returns 16 bytes
}

This approach – making Self your subslice – is taken by String slices, but not by Array, which has a subslice type of ArraySlice.

This brings up another gotcha you might hit when writing generic methods on collections using slices – slices aren’t necessarily of the same type as the thing you are slicing. They are sometimes, but not always.

Even more strangely, the elements contained in the subslice aren’t guaranteed to be the same as the elements of the original collection! Ideally they would be, but this constraint can’t be expressed in Swift right now.

For example, suppose you wanted to overload reduce with a version that didn’t need an initial element – instead it used the first element of the collection, and returned an optional in case the collection was empty. You might try and implement it like this:

extension CollectionType { 
    /// Return the result of repeatedly calling `combine` with an
    /// accumulated value initialized to the first element, and each 
    /// subsequent element of `self`, in turn, i.e. return
    /// `combine(combine(...combine(combine(self[0], self[1]),
    /// self[2]),...self[count-2]), self[count-1])`.
    /// Return `nil` in case of an empty collection.
    func reduce(@noescape combine: (Generator.Element, Generator.Element) -> Generator.Element) -> Generator.Element? {
        return first.map {
            dropFirst(self).reduce($0, combine: combine)
        }
    }
}

This will not compile (and beta 4 will lead you up the garden path with a spurious error for why). The problem being, the elements of the collection returned by dropFirst aren’t guaranteed to be the same as those of the collection. You would probably be right to be annoyed if they weren’t – but the type system doesn’t guarantee it.

The fix would be to constrain the extension to require it:

extension CollectionType where Generator.Element == SubSequence.Generator.Element {
    func reduce(@noescape combine: (Generator.Element, Generator.Element) -> Generator.Element) -> Generator.Element? {
        return first.map {
            dropFirst(self).reduce($0, combine: combine)
        }
    }
}

// so now you can do this:
[1,2,3,4].reduce(+)  // returns 10

Grab Bag

There are a few other changes to the standard library:

  • Reflectable became _Reflectable and MirrorType became _MirrorType
  • QuickLookObject has been renamed PlaygroundQuickLookObject
  • _MirrorType and PlaygroundQuickLookObject acquired documenting comments.
  • _MirrorDisposition is now gone from the visible library, though _MirrorType still refers to it for now.
  • And the reflect function is gone: Xcode tells you to use Mirror.init(reflecting:) instead.
  • RawOptionSetType was removed
  • The gradual disappearing of the _ protocols continues. _UnsignedIntegerType is now gone (its methods – toUIntMax and init(_: UIntMax) moved into UnsignedIntegerType)
  • Views onto other things (such as FilterCollection, a lazily filtered viewonto a collection) have dropped their View suffix.
  • Free functions continue to disappear into protocol extensions.
  • As do unnecessarily non-default methods, such as generate for collections that now use the default indexing generator implementation.
  • LazyRandomAccessCollection.reverse now returns a random-access reversed collection, not a bi-directional one.
  • The Zip2 type has been renamed Zip2Sequence – but you already switched to creating it using the zip function, right? Though perhaps the protocol extension wolves are circling its campfire.
  • SinkType and SinkOf are gone. SinkType was used in two places – UTF encoding methods, where you now pass a closure, and UnsafeMutablePointer, where you could now write (ptr++).memory = x instead, I guess.

Beta 3 didn’t introduce many functional changes, but did do a lot of renaming, changing the placeholder names for generic types to something more descriptive than T, U etc:

  • Sequence and collection-like things now use Element
  • Pointer-like things now use Memory
  • Intervals use Bound
  • Views onto other types use Base for the thing they are viewing
  • Unmanaged things are Instances.
  • Optionals still wrap a T though.

  1. We’re not quite out of gotcha territory yet though. After defining that map, try and guess what ["foo","bar","baz"].map(String.hasPrefix("b")) will do… 
  2. On 64-bit platforms, anyway. Cut all these numbers in half for 32-bit. 

2 thoughts on “Changes to the Swift standard library in 2.0 betas 2..<5

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s