Generic Constraints with Optionals

Introduction

Recently, I’ve been spending a lot of time polishing and testing a collection of Swift frameworks and releasing them on github. You can see the website here.

The lessons learned from this experience were unbelievable. There is way more to Swift that I could have ever imagined. The fun part is, each kit in the project comes with its own design problems, and hence requires different approaches and tapping into more amazing Swift features.

For this post, I’ll be going over a design challenge I faced when writing Storez.

Conformance

So, Storez is a a very flexible Key/Value store for Swift that allows end-developers to easily define their keys in a type-safe manner (with custom behaviors). The biggest challenge faced when doing that is the “type-safe” part. How do you ensure that?

Well, let’s take the UserDefaultsStore as an example. It uses NSUserDefaults as an underlying persistence store. NSUserDefaults doesn’t support every single type of object, only a certain subset. How do we make sure that objects to be stored can be serialized to those types?

Protocol conformance is the key! By defining the following protocol:

// inherits from class, since we need to cast it to AnyObject
protocol UserDefaultsSerializable: class {}

We can then “tag” the classes which are supported by the store:

extension NSNumber: UserDefaultsSerializable {}
extension NSDate: UserDefaultsSerializable {}
extension NSData: UserDefaultsSerializable {}
...

Now, we write two simple get and set functions to access the store:

func get<V: SerializableType>(key: String) -> V? {
    return defaults.objectForKey(key) as? V
}

func set(key: String, value: SerializableType?) {

    defaults.setObject(value, forKey: entry.key)
    defaults.synchronize()
}

The above code is a complete example of providing a safe API on top of NSUserDefaults… But, adding custom type support would be nice, won’t it?

Convertibles

This time, we use “conformance” to provide a set or rules for custom objects that want to store themselves in the key-value store. Here is the one written in Storez:

/** Other types can conform to this protocol to add  support.
    It simply requires the class to convert to and from one of
    the supported types.
*/
public protocol UserDefaultsConvertible {

    typealias UnderlyingType: UserDefaultsSerializable

    static func decode(value: UnderlyingType) -> Self?
    var encode: UnderlyingType? { get }
}

This is really interesting, isn’t it? First, we provide a generic type requirement, which is furthermore constrained to a UserDefaultsSerializable type. Then, the conforming type needs to provide two function implementations, one for decoding (de-serializing), and one for encoding (serializing).

Note, even an enum can easily be a conforming type! Since it is a pure Swift protocol, there are absolutely no limitations to which type can add support.

One last caveat remains .. What about non-optionals? If you look closely at the get function, it always returns an optional. What if we wanted to support non optional values, that return some default value when the persisted value doesn’t exist? This presents its own set of challenges, since we now have to provide two separate flows for option, and non-optional types.

Read on…

Get Under Optional Skin

Let’s first see how we provide the two get variations:

public func get<E: EntryType where E.ValueType: SerializableType>(entry: E) -> E.ValueType {
    return _get(entry.key) ?? entry.defaultValue
}

public func get<E: EntryType, V: SerializableType where E.ValueType == V?>(entry: E) -> V? {
    return _get(entry.key)
}

So, what do we have here… The first method is the “nonnull” variant which always returns a non-optional value, while the second method simply returns a nullable value.

The key difference between these two methods is the way we check to make sure that the value conforms to UserDefaultsSerializable type. In the first method, it’s pretty straight forward. We immediately assert that the ValueType of the EntryType is a conforming type.

For optionals, it was whole journey to reach this final, succulent form. When you look at it now, it feels very natural and straight-forward! It basically says: Define two generic types, one is the EntryType and another is a value type that conforms to UserDefaultsSerializable. Then, simply make sure that the EntryType’s ValueType is an optional of the conforming value type!

Conclusion

The power of Swift is just mind blowing. I’ve worked quite a bit with C++ generics, and they are much more flexible, but no where as safe and well defined as Swift’s.