10

So I have a method that has 3 different types of arguments that could come in:

Int32, Int and Double. So the idea was to use generics to minimize the interface

func resetProgressBarChunks<T:Numeric>(originalIterationCount: T) {
    guard let iCount = originalIterationCount as? Double else {return}

But what I have realized, is at runtime, the Int32 and Int arguments will actually fail that guard let. It makes sense, it was just wishful thinking on my part.

But if I try to simply cast a Numeric into a double, the compiler will bark:

func resetProgressBarChunks<T:Numeric>(originalIterationCount: T) {
    guard let iCount = Double(originalIterationCount) else {return}

Cannot invoke initializer for type 'Double' with an argument of type '(T)'

Which I suppose also makes sense, because there is no initializer for Double that takes a Generic.

So it looks like I'm about to be forced to write 3 methods with different parameter types. The Int32 and Int parameter types would just cast into a Double and then call the Double method. Is this really the best way? I really was hoping I could leverage Numeric somehow

President James K. Polk
  • 40,516
  • 21
  • 95
  • 125
A O
  • 5,516
  • 3
  • 33
  • 68
  • 3
    "Is this really the best way?" Basically yes. You solve the problem by _overloading_, not by a generic. Look at how `+` works in the Swift standard library! There is not one `+` for all numbers; there is a `+` for every separate numeric type. – matt Dec 14 '18 at 00:20

2 Answers2

10

... because there is no initializer for Double that takes a Generic.

That is not entirely true. There is no initializer taking a Numeric argument. But there are generic initializers taking BinaryInteger and BinaryFloatingPoint arguments, so that two overloads are sufficient:

func resetProgressBarChunks<T: BinaryInteger>(originalIterationCount: T) {
    let iCount = Double(originalIterationCount)
    // ...
}

func resetProgressBarChunks<T: BinaryFloatingPoint>(originalIterationCount: T) {
    let iCount = Double(originalIterationCount)
    // ...
}

This covers Double, Int, Int32 arguments as well as Float and all other fixed-size integer types.

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • 1
    This is clearly the better answer because the function can accept any number type without needing an switch case for each number type. – Peter Schorn Apr 08 '20 at 12:14
7

Just for purposes of syntactical illustration, here's an example of making this a generic and arriving at a Double for all three types:

func f<T:Numeric>(_ i: T) {
    var d = 0.0
    switch i {
    case let ii as Int:
        d = Double(ii)
    case let ii as Int32:
        d = Double(ii)
    case let ii as Double:
        d = ii
    default:
        fatalError("oops")
    }
    print(d)
}

But whether this is better than overloading is a matter of opinion. In my view, overloading is far better, because with the generic we are letting a bunch of unwanted types in the door. The Numeric contract is a lie. A triple set of overloads for Double, Int, and Int32 would turn the compiler into a source of truth.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 2
    This is a really bad idea because more types could be added that conform to `Numeric` (either from the standard library or from third parties). It would be better to add a concrete overload for every known type that conforms to `Numeric`. Then, you won't get a `fatalError` when trying to pass in a type that's not handled in the switch statement. This should not be the accepted answer. – Peter Schorn May 23 '21 at 08:08