Generics in Swift

Type Constraints

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>() -> T func 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 .