Generics in Swift

Type Constraints

Jeremy Jacobson

--

The other day I came across a bug in my RethinkDB driver for Swift. The function that runs the query defines a generic parameter T, which is used to cast the result of the query to a specific datatype (e.g.: Document, Int, String, etc.).

Here’s an example using the driver:

let doc: Document = try r.table("users").get(userId).run()

The function signature of run() looks like this: func run<T>() -> T. Using type inference, the Swift compiler uses the type defined on the left-side of an assignment or after an as (e.g.: foo as String) to determine what type T is. In the example above, T is determined to be the type Document, because doc is defined to be a Document. In the depths of the driver, T is used to unwrap the result of the query and propogate it up to the run() function. In unwrapping, if the result is not of type T, then an exception is thrown. The unwrapping function itself doesn’t throw the exception, however, it returns a value of type T?, meaning if the value was not of type T, nil was returned.

This is where the problem was. I was trying to write a query similar to this one:

let doc: Document? = try r.table("users")
.filter(["email": email])[0]
.defaults(nil)
.run()

I’m trying to find a user with a specific email and if it doesn’t exist, I want to default to nil, so the result of my query should be an optional Document. Internally, since RethinkDB returns query responses in the form of JSON, nulls are converted into NSNull. Whenever the unwrapping function encountered NSNull, it would try to cast it to T and always fail. In the example above, it would try to cast NSNull to Document.

Should be a quick fix: whenever there is an NSNull, just return nil and don’t cast to T. There’s a problem with this though. As mentioned before, the unwrapping function returns T? and when nil is returned, this indicates that the type-casting failed. My next attempt was to cast nil to T, but even if T is an optional, the Swift compiler will not let you cast nil to T.

The answer lies in Generic Type Constraints. When defining a generic type, you can also specify a class or protocol that the type must inherit or implement. By using type constraints, I could write two functions: one that allowed nil values and one that didn’t. Here are the two versions of run:

func run<T>() -> Tfunc run<T: ExpressibleByNilLiteral>() -> T

In the second function, T is constrained to implement ExpressibleByNilLiteral, which means that a value of type T can be assigned to nil. The Optional enum implements this protocol, therefore T was now able to be an optional and nil could be cast to T.

Generics is a very powerful tool that prevents writing duplicate code in most cases. In this case though, I had to write another function every time a function used the generic T. At least I didn’t have to write a function without generics for every possible data type that a query result could return.

If you have any questions or if you like what you’ve read and want to know more things about Swift, please leave a comment here or tweet or PM me on Twitter. Happy Swifting!

--

--