Generic "math functions"

Yes, and I rescind that particular suggestion.

I am mostly brainstorming. Regardless of the final name choice, there will inevitably be lots of people aliasing it according to their own preference, since there is so much precedence in so many different directions. As long as the implementation is available generically in some form, we can all alias it easily.

2 Likes

In favor of the direction of the pitch. In particular, the introduction of a new, separate Math module is an important precedent to set. It would be great to see more domain-specific modules embedded with Swift moving forward.

One more consideration against the Math namespace: it forgoes the ability to use the static member functions directly with type context. For example, when passing a parameter to a function (without importing the Math module), the difference can be as significant as

foo(Double.Math.sin(x))
// versus
foo(.sin(x))
4 Likes

Nice to see this moving forward.

I'm not sure why we'd have member functions and free-floating aliases for everything. That seems like a crutch to comfort developers from certain other languages - in which case, is it unreasonable to ask them to bring it themselves?

As for namespacing the math functions: that's only useful if we expect alternative implementations to be a thing (e.g. reduced precision/ffast-math-style assumptions, perhaps?). That might be better expressed as a kind of FastFloat<T> wrapper though, because it could cover more operations (like equality checking, assuming NaN and -0 don't exist).

Even in the prior case where we want these alternative implementations on Float/Double directly, I think it's still valuable to provide "default" conformances without a namespace. This mirrors the way String provides multiple implementations of Collection, but we removed the explicit String.CharacterView to make it an un-namespaced conformance.

Not a big fan of the Mathable name. Mathematical sounds nicer.

3 Likes

Nice! +1!

The names of these functions are strongly conserved across languages, but they are not universal; we could consider more verbose names inline with Swift usual patterns. sine , cosine , inverseTangent , etc. This holds some appeal especially for the more niche functions (like expm1 ), but the weight of common practice is pretty strong here; almost all languages use essentially the same names for these operations.

I fully agree, with the caveat that in some cases, argument labels can dramatically improve readability without taking away from standard terminology (even if they are formally part of a declaration's name).

The free functions are the natural spelling of these operations. No one expects to see x.sin() any more than we want to see x.adding(1) instead of x + 1.† The static functions are implementation hooks that are necessary to implement the free functions in a generic fashion, with the added benefit that they can be used in situations where importing the free functions may be undesirable.

[†] to some extent, this is a matter of taste, and you can make an argument that you really do want to use x.sin( ), I guess, but sin(x) is explicitly given in the API Design Guidelines as an example of following precedent, so I think the guidance is pretty clear here.

3 Likes

So should we also add a free round function?

It already exists in the Platform module, and would be provided by Math with this proposal. So, yes.

3 Likes

I would push back on verbose naming for mathematical operations. There is a benefit in keeping mathematical notation compact. It is more helpful to be able to visually parse all of an equation at once and see the overall pattern to it, than it is to spell out each constituent operation in a beginner friendly way. For example, in my opinion the following code would become less readable if the mathematical operations were more verbose leading me to have to break up the equations into more pieces spread over more lines.

func getCircleIntersections(p1: CGPoint, p2: CGPoint, r1: CGFloat, r2: CGFloat) -> (CGPoint, CGPoint)? {
    let L = (p2 - p1).length
    let A = (pow(r1,2) - pow(r2,2))/(2*pow(L,2))
    let disc = 2*(pow(r1,2) + pow(r2,2))/pow(L,2) - pow((pow(r1,2)-pow(r2,2)),2)/(pow(L,4)) - 1 //discriminant
    if disc > 0 {
        let B = 0.5*sqrt(disc)
        let v0 = CGVector(dx: p1.x + p2.x, dy: p1.y + p2.y)
        let v1 = CGVector(dx: p2.x - p1.x, dy: p2.y - p1.y)
        let v2 = CGVector(dx: p2.y - p1.y, dy: p1.x - p2.x)
        
        let v3 = 0.5*v0 + A*v1 + B*v2
        let v4 = 0.5*v0 + A*v1 - B*v2
        
        return (CGPoint(x: v3.dx, y: v3.dy), CGPoint(x: v4.dx, y: v4.dy))
    } else {
        return nil
    }
}

When reading dense mathematical code the clarity of an individual exponent operation is the least of my concerns. The closer my code can become to compact handwritten mathematical equations the better.

7 Likes

Thanks, @Nevin — that helps a lot!

I'd expect to see the whole proposed API surface, either in the proposal or in a linked implementation, so that it's clear what the additions will be. For example, on my first read I missed that the list of functions in the section that follows what I quoted (Functions not defined on Mathable) will all be included in the Math module. Are you planning to add one of those for a future version of the proposal?

1 Like

For exponentiation specifically, I stuck with the pow name in NumericAnnex for a few iterations and realized that ** really is just much nicer. I think it would be worthwhile to consider adding that operator and its associated precedence to this proposal. It has strong precedent in many languages.

15 Likes

I don’t think sine(x) and cosine(x) is an improvement over Float.sin(x) and Float.cos(x). In all of high school and college i never once wrote the word “sine” on a piece of paper so it’d be weird to start writing it in code. (By the same argument, never have i ever written “sqrt” on a piece of paper either so I have no problem with .squareRoot().) sin and cos also have the benefit of being 3 characters long each, so code with a lot of trig functions lines up nicely.

The same reasoning goes for log(), i really don’t know what percentage of people even know that it’s short for “logarithm”. For obvious reasons this function should not be available as a top-level function, which is also a pretty strong argument against Swift.sin(_:) and Swift.cos(_:).

I don’t use the other math functions enough to care how they’re spelled.

I’m in favor of ** over pow(a, b). might lose a potential operator candidate for vector dot products though.

6 Likes

I remember when writing this function I thought that even the pow() function was too verbose and wished I could use a Python style ** operator. Here is how it compares.

//Swift, C, Java etc.
let A = (pow(r1,2) - pow(r2,2))/2*pow(L,2)
let disc = 2*(pow(r1,2) + pow(r2,2))/pow(L,2) - pow((pow(r1,2)-pow(r2,2)),2)/pow(L,4) - 1

//Python, Ruby
let A = (r1**2 - r2**2)/2*L**2
let disc = 2*(r1**2 + r2**2)/L**2 - (r1**2-r2**2)**2/L**4 - 1

Finally, in a perfect world where ^ wasn't already used up on bitwise XOR

//Julia, R, MATLAB
let A = (r1^2 - r2^2)/2*L^2
let disc = 2*(r1^2 + r2^2)/L^2 - (r1^2-r2^2)^2/L^4 - 1
4 Likes

"obviously" this should be:

let A = (r1² - r2²)/2*L²
let disc = 2*(r1² + r2²)/L² - (r1² - r2²)²/L⁴ - 1

/ducks

18 Likes

Some random thoughts, most of these are loosely held, and I am not a numerics expert :-)

  • I am in love with this proposal.
  • The name needs to be bikeshed for sure, thanks for including a list of candidates in alternates considered, and please include more options that come up in this thread. Of the options I've seen, I'd +1 Mathematical
  • I agree with your general sense that there are strong terms of art here, and inverseHyperbolicTangent is ridiculous - it doesn't lead to clarity.
  • I think it would be really nice to pull sqrt into the common framework somehow, even if it leads to a duplicate squareRoot member. The later can be deprecated and (e.g.) hidden from code completion. The numeric programing community will just laugh at Swift if they have to use squareRoot but can use exp and atan2 like they expect.
  • Fixing pow w.r.t. pown is great. Thing to consider: should we also eventually support pow on integer/integer arguments? Does this cause a problem?
  • While I think that we should keep the basic terms of art w.r.t. these functions, I personally think that this is an opportunity to fix points of confusion - concretely, for the atan2 example you mention, we can and probably should add argument labels to fix the bug. This won't affect code completion, but will lead to more clear code.
  • It would be very nice to see a full list of the free functions that won't be provided with import Math after the proposal. You point out that some are out of scope because they are already there, but it isn't clear what the state of things is after you're done. copysign and some of the others listed are pretty important.

Thank you so much for driving this forward. I'm thrilled that you're aiming to get this into 5.1!!

-Chris

13 Likes

to add to this, usually when i’m using math functions, i’m copying down a formula from wikipedia or some academic paper. i have no idea how the formula actually works or what it means mathematically,, my goal is just to accurately reproduce it in code. If the wikipedia article says “atanh” and i see Float.atanh in code completion, i can be pretty sure that’s the function i want. If the function is called Float.inverseHyperbolicTangent, and the article says “atanh”, i am not going to find the right function.

11 Likes

While I think cos, sqrt, and other common operations should probably stay as they are, I wonder if there's opportunity to improve on the C names in a few other areas. For example, why is lgamma not called lnGamma? Similarly, expm1 is less clear to me than expMinusOne or even expSub1, and it has always bothered me that log is not ln.

There's definitely an argument for prior art, but I think there are also a few functions where the C name is non-obvious from a mathematical context, and in those cases I think Swift has room to improve.

9 Likes

Float , Double and Float80 (when defined) all conform to Mathable , as do SIMD vector types when the underlying Scalar type is Mathable .

If Mathable refines Numeric, how can SIMD vector types conform to it without also conforming to Numeric which includes ExpressibleByIntegerLiteral?

Might be a silly question, but can‘t we make these functions more readable for non-mathematics of us? I never liked the short forms of all these operator functions. When I encounter something like erf my first impression would be, what the heck?!
Swift already names things like random instead of rand, which is more readable and better understandable.

1 Like

That’s an excellent point, but the good news is that the pitch is just slightly out of sync with where the implementation has gone, so this isn’t a problem.

Specifically, the “Mathable” protocol (or whatever name we use) now only provides the math functions, and we introduce a new “Real” protocol that is Mathable & FloatingPoint (much like @xwu’s approach). Most people would write code directly against this protocol most of the time, but in SIMD contexts you can use “SIMD where Scalar: Real”, and with e.g. a Complex type, you would have the Mathable conformance—and you can also write against Mathable & Numeric. This works pretty nicely, and makes the naming of Mathable less important because most users won’t use it as a constraint.

4 Likes

I think we could clarify some of these (lgamma might be logGamma, for instance) but we don’t want to improve readability for the people who don’t use the functions at the cost of making it worse for people who use them all the time. ‘erf’ is the name of the function—you can call it ‘errorFunction’ but I don’t think that’s much better for non-statisticians (the naming guidelines might lead us to just ‘error(x)’, but that’s even worse—it sounds like it’s reporting an error), and it is much worse for statisticians.

10 Likes