[API review] trig-pi functions

acosInHalfTurns

1 Like
sin(degrees: x)  // or sin(deg:)
sin(radians: x)  // or sin(rad:)
sin(pis: x)      // or sin(pi:)

Do we currently have the last one in C, BTW?

asinInDegrees(y) // or asinDegrees, not too far from asinDeg
asinInRadians(y) // or asinRadians, not too far from asinRad
asinInPis(y)     // or asinPis, not to far from asinPi
1 Like

Yes, C23 has sinpi (Apple's and a few other libcs have exposed it as an extension for a decade or so--we called it __sinpi in Apple's libm since C hadn't standardized a name for it yet. I don't think any mainstream libcs have a sin-in-degrees).

1 Like

+1. i reached the same conclusion after trying to “translate” countless MongoDB APIs as well.

A slight variation of the above:

enum TrigUnits {
    case radians
    case degrees
    case pi
    case arcminute
    case arcsecond
    case grad
    case turn
    case custom(Double)
}
func sin(_ v: Double, _ units: TrigUnits = .radians) -> Double
func asin(_ v: Double, _ units: TrigUnits = .radians) -> Double

usage:

sin(42)            // same as sin(42, .radians)
asin(0.2)          // same as asin(42, .radians)
sin(42, .degrees)
asin(0.2, .degrees)
sin(42, .pi)       // sinpi
asin(0.2, .pi)     // asinpi

This is heading quickly to being a full-fledged Angle type, which is interesting and worth doing well, but these are the implementation hooks that we use to build that, rather than the Angle type itself.

2 Likes

I see your point. However, if to have only "case pi" in the above enum and two separate (sets of) functions, one for "sin(x) asin(x)" (& co) and another for "sin(x, .pi), asin(x, .pi)" the resulting ASM is exactly the same as what it is in your proposal, so basically we are talking about the visual aesthetics of:

  static func asinOverPi(_ x: Self) -> Self
  static func sin(piTimes x: Self) -> Self

vs:

  static func asin(_ x: Self, _ pi: Pi) -> Self
  static func sin(_ x: Self, _ pi: Pi) -> Self

To get to the use sites:

  asinOverPi(x)    vs   asin(x, .pi)
  sin(piTimes: x)  vs   sin(x, .pi)

The trick is that API suggests to a user that they should also write:

func myFunction(_ x: Double, _ units: TrigUnits = .radians) -> Double {
  return asin(/* some expression involving x */, units)
}

and then all of a sudden they're dynamically switching over units unless everything gets inlined (which is also a problem, in a different way).

But that won't happen if we have "pi" in that enum as the only option (in which case it could be named "Pi"). To handle both cases users will have to write:

enum Pi { case pi }
...
func myFunction(_ x: Double, _ pi: Pi) -> Double {
    asin(/* some expression involving x */, .pi)
}
func myFunction(_ x: Double) -> Double {
    asin(/* some expression involving x */)
}

which is no different (†) compared to the "asinOverPi(x)" of your proposal other than the visual aesthetics.

(†) there could be a difference in compilation time though.

1 Like

Ah, I was still thinking about your earlier units suggestion. I agree that there's no runtime cost here, but asin(-1/3, .pi) has the wrong sense. It suggests (at least to me, if I don't stop and think about it) that we're computing asin(-π/3) rather than asin(-1/3)/π

4 Likes

This is basically the right answer, but I'd take out the reference to π in the last one. sin(cycle: x) or somesuch is perhaps more appropriate. It would be off by a factor of 2 from the π version, but it may be that it's closer to what most code is intending anyway. Having to think about how many πs you want is harder to quickly reason about than how many cycles you are intending. cos(cycle: 1/2) // = -1. But I know many are perhaps more comfortable thinking in terms of half-cycles (π). Cycles may be friendlier to less mathy coders who just know they want some thing to twirl around 3 times. Just my 2¢.

(For most trig calculations, even though people like the number π, it is besides the point and need not be explicit if you have functions internally handling the scaling factor.)

I agree that sin(piTimes x: Self) -> Self reads much nicer than the reverse asinOverPi(_ x: Self) -> Self.
Perhaps asinOverPi(of x: Self) -> Self adds that little bit of extra clarity. Makes it different enough to emphasise that it's asin(x) / π and not asin(x/π).

Also thinking if this part of the function docstring is clear enough on that not so minor detail:
/// The arcsine (inverse sine) of x, scaled by 1/π.
perhaps a little verbose but slightly more clear?
/// The arcsine (inverse sine) of x, the result of which is scaled by 1/π.

1 Like

Are the atanOverPi family of methods necessary? tan(piTimes:) et al make sense because they allow you to use “friendly” (and exact) numbers as the argument, and they can also provide more precision for pathological inputs (such as very large values). But the over methods would be returning a value. Unless moving division by pi into the function allows for more precision, I'm not sure what the utility is versus just having the user divide the result by pi.