Swift: Enumerating Collections


I don’t particularly like how Swift prints collections by default. Back in ObjC-World, NSLog does a better job rendering complex collections than println. It’s not that the println results are awful, especially for small collections, but when you’re working with real (read “nontrivial”) data Swift doesn’t really pull its weight.

Here’s an example. When you println this dictionary:

let simpleDictionary = ["Apples": 5, "Oranges": 6, "Bananas" : 10, "Kiwis": 3]

You get:

[Oranges: 6, Apples: 5, Bananas: 10, Kiwis: 3]

Which is just fine. But when you println this dictionary:

let info = NSProcessInfo.processInfo().environment

You get a big ugly mess.

[SIMULATOR_PLATFORM_RUNTIME_OVERLAY_ROOT: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/CoreSimulator/RuntimeOverlay, SIMULATOR_CAPABILITIES: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/CoreSimulator/Profiles/DeviceTypes/iPhone 6.simdevicetype/Contents/Resources/capabilities.plist, XPC_SERVICES_UNAVAILABLE: 1, SIMULATOR_MAINSCREEN_WIDTH: 750, SIMULATOR_DEVICE_NAME: iPhone4Simulator, SIMULATOR_RUNTIME_BUILD_VERSION: 12F69, __CFPREFERENCES_AVOID_DAEMON: 1, SIMULATOR_MAINSCREEN_PITCH: 326.000000, DYLD_FALLBACK_FRAMEWORK_PATH: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks, PLAYGROUND_NAME: MyPlayground,...etc...]

Screen Shot 2015-05-12 at 4.00.11 PM

The information runs into each other in a single blob. When you deal with complex dictionaries, you want to be able to scan keys and locate values. Using println() doesn’t really help with that.

NSLog is called in Swift with NSLog("%@", info). It separates keys and values to produce more readable output.

let info = NSProcessInfo.processInfo().environment
let array = Array<NSObject>(info.keys)
let set = Set(info.keys)
for item in [info, array, set] as [NSObject] {
    NSLog("%@", item)

Still not great, as you see in the following examples, with the extraneous quotes and semicolons, but improved over println().

2015-05-12 16:02:57.654 MyPlayground[98181:7113808] {
"CFFIXED_USER_HOME" = "/var/folders/fq/5xg19svj49x06svs3002k5qw0000gn/T/com.apple.dt.Xcode.pg/containers/com.apple.dt.playground.stub.iOS_Simulator.MyPlayground-7DC14B8C-1EDC-4579-BB59-AA463AD6C17A";
"DYLD_FALLBACK_FRAMEWORK_PATH" = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks";
"DYLD_FALLBACK_LIBRARY_PATH" = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib";
"DYLD_INSERT_LIBRARIES" = "/var/folders/fq/5xg19svj49x06svs3002k5qw0000gn/T/com.apple.dt.Xcode.pg/auxiliarymodules/7DC14B8C-1EDC-4579-BB59-AA463AD6C17A/MyPlayground_Sources.framework/MyPlayground_Sources";
"DYLD_ROOT_PATH" = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk";
HOME = "/var/folders/fq/5xg19svj49x06svs3002k5qw0000gn/T/com.apple.dt.Xcode.pg/containers/com.apple.dt.playground.stub.iOS_Simulator.MyPlayground-7DC14B8C-1EDC-4579-BB59-AA463AD6C17A";
"IPHONE_SHARED_RESOURCES_DIRECTORY" = "/Users/ericasadun/Library/Developer/CoreSimulator/Devices/44E43CC5-880F-4A93-B5FE-99220D916C74/data";
"IPHONE_SIMULATOR_DEVICE" = iPhone;...etc...

You can do a lot better with dictionaries, arrays, and sets, than direct println() and NSLog().  The simplest approach is provided by dump(). This function sends structured collection output to the console.

let info = NSProcessInfo.processInfo().environment
let array = ArraySlice(info.keys)
let set = Set(info.keys)
for item in [info, array, set] as [Any] {dump(item)}

The dump() function result involves compromises, as you see in the following example.

▿ 38 key/value pairs
 ▿ [0]: (2 elements)
 - .1: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/CoreSimulator/RuntimeOverlay
 ▿ [1]: (2 elements)
 - .1: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/CoreSimulator/Profiles/DeviceTypes/iPhone 6.simdevicetype/Contents/Resources/capabilities.plist
 ▿ [2]: (2 elements)
 - .1: 1
 ▿ [3]: (2 elements)
 - .1: 750

Dump does not present keys and values on the same line but it does recurse down the collection in its default form. Your sets will print with indices (which makes no sense for an ordered collection but so long as you expect this it’s not a big problem). Dump also involves significantly more overhead than other printing mechanisms since it relies on Swift reflection. (Thanks, Lily Ballard)

Dump  offers options that let you control exactly how your information is presented, and how deep recursion should go. It’s a valuable and powerful logging tool, whose output can be redirected to an arbitrary output stream.

/// Dump an object's contents using its mirror to the specified output stream.
func dump<T, TargetStream : OutputStreamType>(x: T, inout targetStream: TargetStream, name: String? = default, indent: Int = default, maxDepth: Int = default, maxItems: Int = default) -> T
/// Dump an object's contents using its mirror to standard output.
func dump<T>(x: T, name: String? = default, indent: Int = default, maxDepth: Int = default, maxItems: Int = default) -> T

Often dump() is overkill. When all you want is a line-by-line dump of your dictionary, just use a for loop.

for (key, value) in info {println("\(key): \(value)")}

This approach also works for arrays and sets:

for value in array {println(value)}
for value in set {println(value)}

If you want an index for the array (sets are unordered), use enumeration:

for (index, value) in enumerate(array) {println("\(index): \(value)")}

Mapping also offers a great solution for printing one element per line.

map(info){println("\($0): \($1)")}

Of course, you can rubegoldberg up a solution for one-per-line logging. Here’s one example.

println(array.reduce("", combine:{"\($0)\n\($1)"}))

(Not only deeply ugly, but it leaves an extra space at the top.)

Update via @AirspeedVelocity:

println("\n".join(map(dict) { k,v in "\(k): \(v)"}))

To conclude, the standard print() is fine only if the collections are fairly small.I find myself using array.map{println($0)} and dump(collection) as my primary go-to’s for line-by-line collection logging. They’re indispensible when working with hefty collections.

Mastering dump() doesn’t just give you power over collections, the function is amazing for any kind of structure work. More about that in another post.

Comments are closed.