At first glance, the collection types defined by the Swift standard library look similar to the ones you find in other programming languages. But if you dig a little deeper, you start to see the differences.
You can ask a dictionary for its keys and values through its keys
and values
properties. Take a look at this example. We create a dictionary of type [String: Int]
with three elements.
let dictionary = ["One": 1, "Two": 2, "Three": 3]
We can ask dictionary
for its keys through its keys
property. Quick question. What's the type of keys
? If you guessed [String]
, then you guessed wrong.
let keys = dictionary.keys
The keys
constant is of type LazyMapCollection<Dictionary<String, Int>, String>
. What's that about? We need to dig into the Swift source code to find the answer.
Swift Has the Answer
We can find out more about the keys
property by pressing Command and clicking keys
in the playground. This takes us to the interface of the Dictionary
structure.
/// A collection containing just the keys of the dictionary.
///
/// When iterated over, keys appear in this collection in the same order as they
/// occur in the dictionary's key-value pairs. Each key in the keys
/// collection has a unique value.
///
/// let countryCodes = ["BR": "Brazil", "GH": "Ghana", "JP": "Japan"]
/// for k in countryCodes.keys {
/// print(k)
/// }
/// // Prints "BR"
/// // Prints "JP"
/// // Prints "GH"
public var keys: LazyMapCollection<[Key : Value], Key> { get }
While this gives us more information about the keys
property, including a code example, it doesn't help us on our quest. Did you know that Apple open-sourced Swift? Seriously. That should help us find the answer to our question.
If you want to follow along, visit the repository on GitHub, clone it, and open it in a text editor, such as Atom, Sublime Text, or Visual Studio Code.
git clone https://github.com/apple/swift.git
The file we're interested in lives in the standard library folder, stdlib. Navigate to stdlib > public > core and look for a file named HashedCollections.swift.gyb. Don't worry about the gyb extension. That's a different story for another day.
Look for the keys
property in this file. The implementation of this computed property looks like this.
public var keys: LazyMapCollection<Dictionary, Key> {
return self.lazy.map { $0.key }
}
This brings us closer to the answer. The value of keys
is the result of a map(_:)
operation on the value of the collection's lazy
property. That's a step in the right direction.
To continue our journey, we need to learn more about the lazy
property. Return to your playground, press Command, and click the keys
property. This takes you once more to the interface of the Dictionary
structure.
From the jump bar at the top, select the lazy
property.
This is what the interface of the lazy
computed property looks like. The comments are particularly interesting.
/// A sequence containing the same elements as this sequence,
/// but on which some operations, such as `map` and `filter`, are
/// implemented lazily.
///
/// - SeeAlso: `LazySequenceProtocol`, `LazySequence`
public var lazy: LazySequence<Dictionary<Key, Value>> { get }
The value of lazy
is the sequence itself. The only difference is that an operation is performed on the elements of the sequence. The lazy
sequence is of type LazySequence<Dictionary<Key, Value>>
.
We're almost there. Press Command once again and click LazySequence
to bring up its interface. The first few lines are most interesting to us.
/// A sequence containing the same elements as a `Base` sequence, but
/// on which some operations such as `map` and `filter` are
/// implemented lazily.
///
/// - See also: `LazySequenceProtocol`
public struct LazySequence<Base : Sequence> : LazySequenceProtocol, _SequenceWrapper {
...
What's important to remember is that a LazySequence
contains the elements of the sequence with an operation performed on each sequence. Notice that LazySequence
conforms to the LazySequenceProtocol
. Press Command and click LazySequenceProtocol
for the final clue.
Scroll to the first lines of the comments that precede the LazySequenceProtocol
definition. These comments tell us what's going on.
/// A sequence on which normally-eager operations such as `map` and
/// `filter` are implemented lazily.
///
/// Lazy sequences can be used to avoid needless storage allocation
/// and computation, because they use an underlying sequence for
/// storage and compute their elements on demand. For example,
///
/// [1, 2, 3].lazy.map { $0 * 2 }
///
/// is a sequence containing { `2`, `4`, `6` }. Each time an element
/// of the lazy sequence is accessed, an element of the underlying
/// array is accessed and transformed by the closure.
///
/// Sequence operations taking closure arguments, such as `map` and
/// `filter`, are normally eager: they use the closure immediately and
/// return a new array. Using the `lazy` property gives the standard
/// library explicit permission to store the closure and the sequence
/// in the result, and defer computation until it is needed.
///
...
public protocol LazySequenceProtocol : Sequence {
...
The map(_:)
operation is usually an eager operation. This means that the closure we pass to the map(_:)
function is immediately performed on every element of the sequence. This isn't true for types conforming to the LazySequenceProtocol
. To optimize performance and memory usage, the values of the keys
and values
properties of a dictionary are computed lazily.
This means that, when you ask a dictionary for its keys, it returns a LazyMapCollection
object. The elements of this collection are computed on demand. This is only possible because the map(_:)
operation is performed lazily instead of eagerly. Remember the implementation of the keys
property of the Dictionary
structure we explored earlier.
public var keys: LazyMapCollection<Dictionary, Key> {
return self.lazy.map { $0.key }
}
The closure passed to the map(_:)
function is stored by the standard library. This is important since the closure is not immediately executed. It's executed whenever an element of the keys
collection is accessed.
Exploring the Swift Standard Library
Exploring the source code of the Swift standard library can take you down a rabbit hole, but I hope you agree that it teaches you quite a bit along the way. I really enjoy digging through the source code of open source projects and exploring the Swift project is no different.
If you want to become a better developer, then such journeys are great opportunities to learn and extend your boundaries as a developer. It's fine if you don't understand every detail. The goal is to leave your comfort zone and learn as you go.