Swift numeric types are needlessly complicated to extend

Extensions in Swift are one of the most powerful features of the language in my opinion. However, they become needlessly complicated when trying to extend numeric types.
For example, the following code defines a clean property that returns a string representation of a number, removing the decimal point and trailing 0 if need be:

import Foundation

extension Double {
	var clean: String {
		return self.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(self)
	}
}

let test1 = 1.0
let test2 = 1.2

print(test1.clean) //outputs 1
print(test2.clean) //outputs 1.2

Obviously, this code is not specific to the Double type, but rather any floating point value. It would be nice to make this property generic. However, changing the extension to FloatingPoint yields the following errors:

no exact matches in call to initializer
argument type 'Self' does not conform to expected type 'CVarArg'
found candidate with type '(Self) -> String'
found candidate with type '(Self, Int, Bool) -> String'
candidate expects value of type 'Self' for parameter #1
add missing conformance to 'CVarArg' to protocol 'Self'

If you attempt to add conformance to CVarArg, more errors popup. Similarly, multiplication tends to result in lack of SIMD conformance between elements, especially when an intermediary step converts to a Double or Float:

extension FloatingPoint {
	var clean: Self {
		let temp = pow(clean, 2)
		return temp * 3.0
	}
}
cannot convert value of type 'Self' to expected argument type 'Decimal'
no '*' candidates produce the expected contextual result type 'Self'

I may just not be using this functionality right, but it seems like a fairly common thing to want to do.

The problem that you're having is that you are using C functions (or functions which wrap C functions), which are not generic.

In the first case, the issue is String(format:), which takes a printf-style format string (which includes specific type identifiers and is not suited for generics). @Michael_Ilseman has created a draft PR for swift-numerics showing how generic FloatingPoint formatting could work, and I think we'd both agree that it would be a really nice thing to add.

In the second case, pow is not generic, and FloatingPoint does not include it. The generic constraint you're looking for is ElementaryFunctions in the swift-numerics library, which does include it. This is supposed to become part of the standard library at some point, but currently isn't due to issues with type-checker performance.

8 Likes

Yeah I understand why the issues are occurring, it's just frustrating that they are at all. Making an extension to both Float and Double shouldn't require duplicating code

Writing generic extensions does require you to think carefully about the constraints you have on the type. In this case, the String initializers you're attempting to use don't accept arbitrary FloatingPoint arguments. Your first clean definition does compile with a couple additional constraints on Self, though:

extension FloatingPoint where Self: CVarArg, Self: LosslessStringConvertible {
    var clean: String {
        return self.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(self)
    }
}

let test1: Float = 1.0
let test2: Double = 1.2

print(test1.clean) //outputs 1
print(test2.clean) //outputs 1.2

Now, I'm not certain this definition is actually correct, in that I don't know whether it's guaranteed that any type which conforms to both CVarArg and FloatingPoint can be passed to a %f format specifier, but it appears to work for Double and Float, at least.

I appreciate this example, it does work and will suffice for the moment, I just think the language itself should make this a better experience.

This sentence suggests that you want to add clean to FloatingPoint numbers which at the same time have a string representation. CustomStringConvertible should be the additional constraint you're looking for since it gives you the string representation description:

extension FloatingPoint where Self: CustomStringConvertible {
  var clean: String {
    var string = description
    if string.hasSuffix(".0") { string.removeLast(2) }
    return string
  }
}
2 Likes

That's a fair criticism, although, again, if we had generic FloatingPoint formatting and you (or any other programmer) knew that it existed, the first example would "just work". It's a matter of completing that implementation (if you're interested, perhaps reach out to the author and ask if there's any way you could help with it).

The second example requires knowing that not all FloatingPoints necessarily support pow. Is that intuitive? Maybe not, but we do have a generic constraint to express "FloatingPoint which supports pow", and that is ElementaryFunctions.

So, there are some pieces that are still missing/WIP, and I get that that's frustrating, but it's not that bad, IMHO.

1 Like

I wonder whether we could add unavailable methods to some protocol extensions to help provide diagnostic hints that you might need an additional constraint to get expected operations. For instance, pow could be unavailable on FloatingPoint with a message telling you it requires ElementaryFunctions. @scanon would that be something interesting to try with the numerics protocols?

13 Likes