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>() -> 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!