I was playing with calculations to analyse reliability and error propagation with different numeric type when high precision matters. The same formula being applied to different numeric types, I tried to use generics but was not able to cover also the Decimal
type.
Is there a way to have a single generic implementation covering Float
, Double
and Decimal
for simple numeric functions ?
The calculation is based on the Muller sequence that applies a very simple formula recursively. The code for the three considered type is:
func mullerFormulaDouble(y:Double, z:Double ) -> Double {
return 108.0 - ( (815.0-1500.0/z)/y)
}
func mullerFormulaFloat(y:Float, z:Float ) -> Float {
return 108.0 - ( (815.0-1500.0/z)/y)
}
func mullerFormulaDecimal(y:Decimal, z:Decimal ) -> Decimal {
return Decimal(108) - ( (Decimal(815)-Decimal(1500)/z)/y)
}
The generic calculation looks like:
func mullerFormula<T>(y:T, z:T ) -> T { // a constraint is required for T
let a = T(1500)/z
let b = (T(815) - a)/y
return T(108) - b
}
print (mullerFormula(y:4.0, z:4.24))
print (mullerFormula(y:Float(4.0), z:Float(4.24)))
print (mullerFormula(y:Decimal(4), z:Decimal(sign:.plus, exponent: -2, significand:424)))
The code above did not compile, due to an error message about T
having no initializer, which makes sense. Some type constraint is required to make it work. So I used T:Numeric
, but it failed on the division, with the error that it cannot be applied to operands T?
and T
.
A couple of errors later, I ended up with T:FloatingPoint
. It then worked for Double
and Float
, but I can still not use it with Decimal
, because Decimal
does not conform to FloatingPoint
. I didn't find any other suitable common protocol for my type constraint.
Considering that all these three numeric types support initialization from an integer and the four basic operations, I cannot imagine that there is not way to write a generic function to cover them all. Hence my question.
Method 1: Define a BasicNumeric
protocol with the functions you require
Here is one way to do it. Create a protocol
called BasicNumeric
that has the common properties of the types you want to use. Declare that your types implement that protocol, and use that protocol as the constraint in your new function:
protocol BasicNumeric: ExpressibleByIntegerLiteral {
static func +(_: Self, _: Self) -> Self
static func -(_: Self, _: Self) -> Self
static func *(_: Self, _: Self) -> Self
static func /(_: Self, _: Self) -> Self
}
extension Double: BasicNumeric { }
extension Float: BasicNumeric { }
extension Decimal: BasicNumeric { }
func mullerFormula<T: BasicNumeric>(y:T, z:T ) -> T {
let a = 1500/z
let b = (815 - a)/y
return 108 - b
}
print (mullerFormula(y:4.0, z:4.24))
print (mullerFormula(y:Float(4.0), z:Float(4.24)))
print (mullerFormula(y:Decimal(4), z:Decimal(sign:.plus, exponent: -2, significand:424)))
Output:
-7.306603773584911
-7.3066025
-7.30660377358490566037735849056603775
Method 2: Defining Divisible
protocol and using Numeric
protocol
Leo Dabus offered this alternate implementation in the comments.
protocol Divisible {
static func /(_: Self, _: Self) -> Self
}
extension Double: Divisible { }
extension Float: Divisible { }
extension Decimal: Divisible { }
func mullerFormula<T: Numeric & Divisible>(y: T, z: T) -> T {
let a = 1500 / z
let b = (815 - a) / y
return 108 - b
}