Debugging Generics in Swift

A friend of mine was having a problem with a generic function they’d written. I’ve simplified it quite a bit to post, but the essence was:

func foo<T>(_ n: T) -> T where T: Numeric {
  return n * (4 as! T)
}

They could call it with an Int just fine, but when calling with a Double, They’d trap:

let i: Int = 5
let d: Double = 5.5
foo(i) //> 20
foo(d) //> 🛑 SIGABRT 

Those steeped in the myths and traditions of generics might see the issue right away. But I didn’t. At least not in its original form. And the compiler wasn’t giving me anything. I had to dig deeper.

Thankfully, with generics, that’s easy to do. They are, at the end of the day, just syntactic sugar around defining polymorphic functions. Our generic function above is functionally equivalent to writing out the same function over and over, each time replacing its Ts with one of the set of all the types that meet its requirements (in this case, all types that implement Numeric).

We can do this manually (at least over the small number of types that are interesting to us). And doing so not only gives us a clearer view into our function’s execution, it also gives the compiler an easier path for diagnostics reporting. Both are huge wins whenever we’re trying to debug.

Just look at what happens when we rewrite our function with the types from our example:

func foo(_ n: Int) -> Int {
  return n * (4 as! Int)
}

func foo(_ n: Double) -> Double {
  return n * (4 as! Double)
  // ⚠️ Cast from 'Int' to unrelated type 
  // 'Double' always fails
}

We probably don’t even need the compiler’s warning to see the problem: we can’t downcast from an Int to a Double — the one isn’t down from the other in any type hierarchy.1

Unwinding our generic function like this also makes solution more obvious. Double (like Numeric… and Int, obviously) implements ExpressibleByIntegerLiteral. So we don’t even need the cast:

func foo(_ n: Int) -> Int {
  return n * 4
}

func foo(_ n: Double) -> Double {
  return n * 4
}

Or, expressed generically:

func foo<T>(_ n: T) -> T where T: Numeric {
  return n * 4
}

1: And it traps because of the as!. Curious why? Ole Begemann has you covered with the magnificent post as, as?, and as!.↩︎