Sets vs Dictionaries smackdown in #swiftlang

Traditional Cocoa has a bad dictionary habit. From user info to font options to AV settings, NSDictionary has long acted as the Cocoa workhorse for passing data. Dictionaries are flexible, easy to use, and a minefield of potential disasters.

In this post, I’m going to discuss an alternative approach, one that’s far more Swift-y. It’s not yet a completely turn-key solution but I think its one that showcases a much better mindset for how APIs should be working in a post-Swift world.

Working with Dictionary-based Settings

Here is some code I pulled out of a random project of mine. (If you’re familiar with my other writings, it’s from my Movie Maker class.) This Objective-C code builds an option dictionary, which is used to construct a AV pixel buffer:

 NSDictionary *options = @{
    (id) kCVPixelBufferCGImageCompatibilityKey : 
        @YES,
    (id) kCVPixelBufferCGBitmapContextCompatibilityKey : 
        @YES,
 };

This example casts the option keys to (id) and uses Objective-C literal notation to transform Booleans to NSNumber instances. The Swift version of this is much simpler. The compiler is smart enough to associate  values with the declared dictionary type.

let myOptions: [NSString: NSObject] = [
    kCVPixelBufferCGImageCompatibilityKey: true,
    kCVPixelBufferCGBitmapContextCompatibilityKey: true
]

Even in Swift, this is far from an ideal way to pass values to APIs.

Characteristics of Setting Dictionaries

Settings dictionaries like the examples you just saw exhibit common characteristics, which are worth examining carefully.

  • They have a fixed set of legal keys. There are about a dozen legal pixel buffer attribute keys in AVFoundation. This collection rarely  changes and those keys are tied to a well-established task.
  • The requested values associated with each key instance are of known types. These types are more nuanced than just NSObject, for example “The number of pixels padding the right of the image (type CFNumber).”
  • Type safety does matter for the passed values, even though this cannot be enforced through an NSDictionary. Both compatibility keys in the preceding example should be Boolean, not Int or String or Array or even NSNumber.
  • Valid entries appears just once in the dictionary, as keys are hashed and new entries overwrite older ones.

In Swift, the characteristics in the above bullets list are far more typical of sets and enumerations than dictionaries. Here are some reasons why.

  • An enumeration expresses a full range of possible options for a given type. Most Cocoa APIs similar to this example have fixed, unchanging keys.
  • Enumerations enable you to associate typed values with individual cases. Cocoa APIs document the types they expect to be passed for each key.
  • Like dictionaries, sets restrict membership to avoid multiple instances.

For these reasons, I think these settings collections are better expressed in Swift as sets of enumerations instead of an [NSString: NSObject] dictionary.

Transitioning from Keys

Stepping back a second, consider the current state of the art. AVFoundation defines a series of keys like the following. (This is not a complete set of the pixel buffer keys, by the way)

const CFStringRef kCVPixelBufferPixelFormatTypeKey;
const CFStringRef kCVPixelBufferExtendedPixelsTopKey;
const CFStringRef kCVPixelBufferExtendedPixelsRightKey;
const CFStringRef kCVPixelBufferCGBitmapContextCompatibilityKey;
const CFStringRef kCVPixelBufferCGImageCompatibilityKey;
const CFStringRef

Each of these items is a  constant string and each string is used to index dictionaries. Callers build dictionaries using these keys, passing arbitrary objects as values.

In Swift, you can re-architect this key-based system into a simple enumeration, specifying the valid type associated with each key case. Here’s what this looks like for the five cases listed above. The  associated types are pulled from the existing pixel buffer attribute key docs.

enum CVPixelBufferOptions {
 case CGImageCompatibility(Bool)
 case CGBitmapContextCompatibility(Bool)
 case ExtendedPixelsRight(Int)
 case ExtendedPixelsBottom(Int)
 case PixelFormatTypes([PixelFormatType])
 // ... etc ...
}

Re-designing options this way produces an extensible enumeration with strict value typing for each possible case. This approach offers you a major type safety victory compared to weak dictionary typing.

In addition, the individual enumeration cases are also clearer, more succinct, and communicate use better than exhaustively long Cocoa constants. Compare these cases with, for example,  kCVPixelBufferCGBitmapContextCompatibilityKey. The Cocoa names take responsibility for mentioning their use as a constant (k), their related class (CVPixelBuffer), and their usage role (key), all of which can be dropped here.

Building Settings Collections

With this redesign, your call constructing a set should look like the following example. I say should because this example will not yet compile.

// This does not compile yet
let bufferOptions: Set<CVPixelBufferOptions> = 
    [.CGImageCompatibility(true), 
     .CGBitmapContextCompatibility(true)]

Swift cannot compile this because the CVPixelBufferOptions options are not yet Hashable. At this point, all you can build is an array, which does not guarantee unique membership:

// This compiles
let bufferOptions: [CVPixelBufferOptions] =
    [.CGImageCompatibility(true),
     .CGBitmapContextCompatibility(true)]

Arrays are all well and lovely but they lack the unique options feature that dictionaries offer and that is driving this redesign.

Differentiating Values

The Hashable protocol enables Swift to differentiate instances which are basically the same thing from instances which are different things. Both sets and dictionaries use hashing to ensure that members and keys are unique. Without hashing, they cannot provide these assurances.

When establishing settings collections, you want to build sets that won’t construct an example like the following, where multiple members with conflicting settings are present at the same time:

[.CGImageCompatibility(true),
 .CGImageCompatibility(false)] // which one?!

These are clearly two distinct enumeration instances due to the different associated values. In this example, you’ll want a set to discard all identical options that follow the first member added to the set, leaving just the “true” case. (Dictionaries follow the opposite rule. Subsequent additions replace existing members instead of being discarded.)

You achieve this by implementing hashing, which enables you to compare enumeration cases.

Implementing Hash Values

For this specific use-case, you need to create a hashing function that considers  case and only case, and not associated values. There is currently no native construct in Swift that offers this functionality, so you need to build this on your own.

Swift’s Hashable protocol conforms to Equatable, so your implementation must address both sets of requirements. For Hashable, you must return a hash value. For Equatable, you must implement ==.

public var hashValue: Int { get } // hashable
public func ==(lhs: Self, rhs: Self) -> Bool // equatable

Basic enumerations, e.g. enum MyEnum {case A, B, C} provide raw values that tell you which item you’re working with. These are numbered from zero, and are super handy to use. Unfortunately, enumerations with associated values don’t provide raw value support, making this exercise a lot harder than it might otherwise be. So you have to build hash values by hand, which kind of sucks.

Here’s an extension of CVPixelBufferOptions, which manually adds hash values for each case. Ugh.

extension CVPixelBufferOptions: 
    Hashable, Equatable {
    public var hashValue: Int {
        switch self {
          case .CGImageCompatibility: return 1
          case .CGBitmapContextCompatibility: return 2
          case .ExtendedPixelsRight: return 3
          case .ExtendedPixelsBottom: return 4
          case .PixelFormatTypes: return 5
       }
    }
}

public func ==(lhs: CVPixelBufferOptions, 
    rhs: CVPixelBufferOptions) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

On the (slightly) bright side, these hash values have absolutely no meaning and are never exposed to API consumers, so if you need to stick in extra values, you can do so. That said, this approach is ugly and hacky and feels very un-Swift.

Once you add these features, however, everything starts to work. You can build sets of settings, with the assurance that each setting appears just once and their associated values are well typed.

Final Thoughts

The approach described in this post, clunky hashing support and all, is far better than the Cocoa NSDictionary approach. Type safety, enumerations, and set, all provide a better solution for what otherwise feels like an archaic API dinosaur.

What Swift really needs here, though, is something closer to option sets with associated values. At a minimum, adding raw value support to all enumeration cases (not just basic ones that lack associated or intrinsic values) would be a major step forward.

Thanks, Erik Little

10 Comments

  • How would use on the impelementor’s side look? If I needed to unpack a single item from the set, how would I do so?

    • Irritatingly, it looks like Swift.Set doesn’t have the member(_:) method that NSSet does, which would make that fairly easy. So you need to write it as:

      extension Set {
      func member(elem: Element) -> Element? { return indexOf(elem).map { self[$0] } }
      }

      Then you can switch on, e.g., options.member(.CGImageCompatibility (true)); the “true” parameter there is a dummy. Kind of clunky, though. To tell the truth, I’m not really sure why this suggestion is supposed to be better than a struct with a field for each option.

      • I’d probably do the implementor side this way:

        enum FooOptions {
        case OptionA(Bool)
        case OptionB(Bool)
        case OptionC(Int)
        }

        // The first argument to reduce is the default value for whatever option it is:

        let a = options.reduce(false) { if case let .OptionA(x) = $1 { return x } else { return $0 } }
        let b = options.reduce(false) { if case let .OptionB(x) = $1 { return x } else { return $0 } }
        let c = options.reduce(12) { if case let .OptionC(x) = $1 { return x } else { return $0 } }

        // Use a, b, c…

      • The big problem with an option struct (and the default-parameters way mentioned below by @has) is of compatibility. As you add and remove options, those structs lose binary compatibility, which makes such a technique infeasible for Apple or maintaining your own APIs.

        Also not a fan of yours nor gregomni’s suggestion of doing the option enum set lookup with linear search. Yeah, it’s not a big deal for few options, but it still bothers me.

        • “As you add and remove options, those structs lose binary compatibility”

          Swift can’t even do ABIs yet. Until it does, such a debate is completely useless. And an ABI can’t do structs then there’s no reason to assume it would fare any better on classes and enums.

          You might also give some thought to what the phrase “public interface” means. Generally it doesn’t mean “something its author changes each day on a whim”. (Unless you’re a Rubyist.) It’s not the language’s job to indulge unruly cowboys, but to allow competent, responsible grown-ups to interact safely and efficiently with each other. (Unless it’s Ruby, again.)

          If you want to discuss something useful, you could give some thought to what degree of flexibility and forwards/backwards compatibility would be reasonable and realistic to expect of a modern ABI design. The Swift team is still working on completing and improving the language and are receptive to constructive feedback.

  • Still a code smell. Parameterized enums are intended for constructing tagged unions (aka sum types), not for inventing ingeniously unorthodox ways to perform tasks for which that the language itself already provides perfectly capable, established, and familiar features.

    To pass options to a function, just use optional parameters: that’s what they’re there for, and Swift has excellent support for them. (Cocoa’s ‘options dictionary’ anti-pattern is really just a kludge-around for the absence of named parameters in [Obj-]C.)


    func foo(arg1: Int, _ arg2: Int, x: Int = 0, y: Str = "") {
    // ...
    }

    foo(1, 2, y: "bar")

    Alternatively, if the options data is all closely related and wants to be easily shared across multiple functions, group it in a struct and pass that as brentdax says. (Again, that’s what they’re there for.) The only mild annoyance (for the developer, not the user) is that Swift sucks at generating default initializers, so you’ll have to add your own boilerplate `init` that takes optional arguments:


    struct FooOptions {
    let x: Int
    let y: String

    init(x: Int = 0, y: String = "") {
    self.x = x
    self.y = y
    }
    }

    func foo(arg1: Int, _ arg2: Int, options: FooOptions = FooOptions()) {
    // ...
    }

    let opts = FooOptions(y:"bar")
    foo(1, 2, options: opts)

    It’d be nice if tuple types could also do optional/default properties, allowing a lightweight expression that combines the best of both (minimal syntax + convenient grouping), but as with smarter default initializer generation that’s the sort of thing worth filing a feature request on if it’s important to you.

    • The problem with both these suggestions is that the parameters (to foo() in the first case or FooOptions.init in the second) are ordered. Go look at CVPixelBuffer and you’ll find that there are 15 possible options (and this is by no means the largest number of possible options in various Cocoa APIs). So if you want to use 3 of those options, you either need to look up the correct order to put them in every single time, or you need somewhere around 15! (15*14*13… 1,307,675,368,000) options initializers to provide the caller with all possible parameter orderings.

      Optional function parameters are great, in ones and twos, but they don’t really solve the problem of fifteens and twenties.

      • Except that ordered parameters isn’t the problem you make it out to be†. Seriously, if you’ve got 15+ options to deal with, you’re not going to be keeping all of those in your head anyway. You’re going to recall the most commonly used ones – which a good API design will already present in a logical order – and rely on tooling like autocomplete to assist in constructing the full expression.

        If you really must insist on unordered options, do the old C trick of defining a struct whose slots are all optional vars, then just set them as needed.


        struct FooOptions {
        var x: Int?
        var y: String?
        ...
        }

        var opts = FooOptions()
        opts.y = "baz"

        That all said, if your API has got 15-20 options to it you should be giving its entire design a very long hard sniff, as I suspect you’ll find its UX woes extend way further than just a single function signature.

        † Protip: Pointing to spectacularly gnarly C APIs and resorting to fantasy math is hardly the best way to build a good strong argument [sic]. (Unless, of course, your argument is that C utterly stinks at enabling any sort of powerful, concise self-expression; in which case you wouldn’t be trying to mimic its design strategies anyway.)

  • You might try and use the initial AVFoundation CFStringRef’s as rawValues instead of arbitrary integers. As these can be cast as String, they are still equatable, and as a benefit you can use these hashValues to be backward-compatible, whenever you do need to pass an NSDictionary. Anyway, great article.

    • Except for the fact that hashValue needs to be Int. Never mind 🙂

Leave a Reply