Search code examples
swiftgenericsfloating-pointdecimalnsdecimalnumber

Generic calculation functions that support also Decimal


In short

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 ?

More details

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.


Solution

  • 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
    }