These functions compute, respectively, cos(πx), sin(πx), and tan(πx) in a manner that is much more accurate than is possible with the "obvious" expressions like .cos(.pi * x), because π cannot be exactly represented as a FloatingPoint type, so .pi * x always introduces a small relative error, which can become a large relative error in the face of further operations. This makes getting many common cases exactly right significantly easier. E.g.:
[ 1> import Real
[ 2> var a = Double.tan(.pi * 1.5)
a: Double = 5443746451065123 // huh?
[ 3> var b = Double.tan(piTimes: 1.5)
b: Double = -Inf // yay!
[ 4> var c = Float.sin(.pi * 1000000)
c: Float = -0.152986646 // what the ...
[ 5> var d = Float.sin(piTimes: 1000000)
d: Float = 0 // yay!
I have some more to do, and need to commit the tests, but they're basically working and you can play with them now if you grab the branch.
The functionality provided by these operations is something that I'm quite set on adding, but I am less committed to this particular spelling, and am interested in alternatives. Some possibilities:
let a = Double.cospi(x) // C-family style
let b = cos(πx: y) -> Self // too-clever by half
... your suggestion here!
Especially worth keeping in mind is a pattern that provides names for inverse trig-pi functions, which have an implicit scale of π on their result rather than their argument.
I’m so in love with this. I hadn’t thought about it before but I’m realizing I should just store “rotation” in my app as a real from 0..<2, which will be way more human-readable and also less squirmy for common (90º) rotations.
The only thing I’d love to have added is a (tauTimes:) variant because to me τ is even more readable — mmm, 0..<1 rotation value * τ.
Have you considered using a wrapper type, which wraps a Real and represents "pi times" that amount?
There could be a protocol that describes it, that would let you write overloads for cos that use these precise expressions. E.g. cos(PiTimes(1.5)). This would let you also allow you to generalize over degrees, radians (by a factor of π or τ, or none at all), and even gradians
struct MultipleOfPi<T: BinaryFloatingPoint> {
var n: T
fileprivate init(n: T) { self.n = n }
}
struct PiSymbol {}
let pi = PiSymbol()
extension BinaryFloatingPoint {
static func * (lhs: Self, rhs: PiSymbol) -> MultipleOfPi<Self> {
return MultipleOfPi(n: lhs)
}
static func * (lhs: Self, rhs: MultipleOfPi<Self>) -> MultipleOfPi<Self> {
return MultipleOfPi(n: lhs * rhs.n)
}
static func * (lhs: MultipleOfPi<Self>, rhs: Self) -> MultipleOfPi<Self> {
return MultipleOfPi(n: lhs.n * rhs)
}
}
func cos<T>(_ mPi: MultipleOfPi<T>) -> T where T:BinaryFloatingPoint {
fatalError()
}
let x: Float80
let _ = cos(x * pi) // result is Float80
let _ = cos(1.123 * pi) // result is Double
let _ = cos(2 * pi * 1.132) // result is Double
While this looks pretty cool, it may lead to subtle bugs: the visual difference between cos(x * .pi) and cos(x * pi) is not easy to spot, but the first one involves actually multiplying by the imprecise constant (Float/Double/etc).pi and the second one wouldn't.
Yes please! @scanon, would including this in the module have any benefit, or can those of us who prefer tau simply define it in terms of (piTimes:) without any loss of accuracy?
Right, there's never any loss of accuracy when scaling by 0.5 or 2.0 in a BinaryFloatingPoint type (unless underflow or overflow occurs, but in practice that "never" happens when working with angles, and if it does something has already gone wrong).
In order to implement that type well, you need to have bindings for the trig-pi operations (and make them available on all platforms). So we may very well end up exposing something like that, but making these functions available is the first step toward doing so.
It also adds another * operator to the overload set, which may cause "expression too complex" issues for some code that would otherwise want to use it in the short-term.