Make a function handle Float and Double

I'm quite puzzled by how complicated it is to make this simple function handle both Double and Float in Swift. Is it even possible?

func srgb2linear(_ S: Float) -> Float {
  if S <= 0.04045 {
    return S / 12.92
  } else {
    return pow((S + 0.055) / 1.055, 2.4)
  }
}

The Swift 4 Documentation says that what I need is a FloatingPoint generic, to represent both Float and Double classes, like:

func srgb2linear<T: FloatingPoint>(_ S: T) -> T

However when I try to do this, it doesn't compile with the following errors:

Error: binary operator '<=' cannot be applied to operands of type 'T' and 'Double'
Error: binary operator '/' cannot be applied to operands of type 'T' and 'Double'
Error: binary operator '+' cannot be applied to operands of type 'T' and 'Double'

How is it possible that for a generic representing floating point numbers such operators are not implemented? And if not like this, how can I write this function in Swift?

Now I tried with BinaryFloatingPoint, which by SE-0067 shouldn't even be needed, as BFP "adds some additional operations that only make sense for a fixed radix", but even BFP cannot handle the simple pow function.

Is it even possible to make this simplest function work on Double and Float in Swift?

You need to use BinaryFloatingPoint because FloatingPoint itself doesn't conform to ExpressibleByFloatLiteral. That's why you see these errors: Swift defaults to making your literal values of type Double.

pow isn't generic, but you can write your own extension to BinaryFloatingPoint that calls the right pow. It's not pretty, though. The Swift standard library doesn't actually offer floating-point math functions [yet]. You're relying on the C standard library here (although as an implementation detail pow specifically is actually intercepted by Swift and calls through to the LLVM intrinsic).

2 Likes

First problem: All of your float literals like 0.04045 are inferred to be Double. You need to stipulate that whatever type T is, it's a type that can be initialized from a float literal:

func srgb2linear<T: FloatingPoint & ExpressibleByFloatLiteral>(_ s: T) -> T {
	if s <= 0.04045 {
		return s / 12.92
	} else {
		return pow((s + 0.055) / 1.055, 2.4)
	}
}

Now the literals are correctly being used to make instances of T.

Second issue: pow is not generic. It only exists as two concrete overloads, over types (Float, Float) -> Float and (Double, Double) -> Double. So I'll make a new protocol for Double and Float, and have it define a generic interface to access the respective overloads of pow:

import Darwin

protocol DoubleOrFloat: FloatingPoint, ExpressibleByFloatLiteral {
	func toThePower(of exponent: Self) -> Self
}

extension Float: DoubleOrFloat {
	func toThePower(of exponent: Float) -> Float { return pow(self, exponent) }
}

extension Double: DoubleOrFloat {
	func toThePower(of exponent: Double) -> Double { return pow(self, exponent) }
}

func srgb2linear<T: DoubleOrFloat>(_ s: T) -> T {
	if s <= 0.04045 {
		return s / 12.92
	} else {
		return ((s + 0.055) / 1.055).toThePower(of: 2.4)
	}
}

The main issue here is that Swift has really horrible handling of basic math like pow, sin, etc. I would recommend NumericAnnex, which I suspect will eventually inform a future addition to the standard library.

4 Likes

Thanks for the solution. Personally I think that at this point it's just cleaner to write a non-DRY solution with two functions, one being a wrapper.

1 Like

The FloatingPoint protocol *does* work with integer literals though, so once you take care of pow then you can write:

func srgb2linear<T: FloatingPoint> (_ s: T) -> T {
  if s <= (4045/100_000) {  // compare to 0.04045
    return s * (100/1292)   // divide by 12.92
  } else {
    return pow((s + 55/1000) * (1000/1055), 24/10)
//  return pow((s + 0.055) / 1.055, 2.4)
  }
}