SE-0246: Generic Math(s) Functions


(Erik Little) #21

Just to be clear, I was actually arguing against having a log, and instead just ln and log10. IMO log without specifying a base is an issue pervasive throughout the sciences, I don't really want Swift to keep perpetuating that.


(Steve Canon) #22

To clarify what the options here are, we would not be able to eliminate log due to source and binary stability requirements. We would deprecate and later obsolete it with ln as a replacement.


(Erik Little) #23

Yes that would certainly be source breaking to outright remove it. That being said, I still think it's important that we still seriously consider going with ln and adding a deprecation notice to log.


#24

Wait, what? Currently log is available in Foundation, but not the standard library and certainly not the as-yet-nonexistant Math library.

Does the proposal include changes to other libraries?


(Steve Canon) #25

log is emphatically not in Foundation. log is in the platform module (Darwin/GLibc) and the CoreGraphics module (in the case of CGFloat), which Foundation transitively imports.

The proposal specifically highlights how it would change the existing platform module:

We will update the platform imports to obsolete existing functions covered by the new free functions in the Math module, and also remove the imports of the suffixed <math.h> functions (which were actually never intended to be available in Swift). The Platform module will re-export the Math module, which allows most source code to migrate without any changes necessary.


(Šimon Javora) #26

I realize this is not very important to the meat of the proposal, but there are still references to Mathable in there and a duplicated Future Expansion header. That should probably be cleaned up :wink:.


(Steve Canon) #27

Thanks! Fixed.


(Ben Langmuir) #28

+1 I think this fills an important gap for writing portable numeric code.

I would prefer not to have the "Math" namespace (i.e. Float.Math.sin vs. Float.sin). While I agree with the proposal author that once you have found something in the namespace it is cleaner to find the other operations in the same place, I think it still hinders overall discoverability since I expect to look for either sin() or Double.sin(). However, this does not affect users who import the Math module, so I don't think it's a big issue.

I don't love the name ElementaryFunctions, but I don't have a better suggestion. I'm ambivalent about the Real protocol.d

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes

  • Does this proposal fit well with the feel and direction of Swift?

Yes

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

This proposal mostly follows precedent set by other languages, which I think is the right decision for the reasons outlined in the proposal. In the handful of places where the operation names do not match, I think the change was well-motivated by the proposal and improves over the status quo without unduly affecting users familiar with these functions in other languages.

Unlike several other reviewers, I agree with proposal and prefer that we use the name log over ln, despite learning it as ln in school. Using log matches the overwhelming precedent in programming languages (Rust and Kotlin notwithstanding). It also means that all of the logarithmic functions have "log" in their name, which makes the class of methods more discoverable as a group. I'm not concerned about collision with a logging API, since I'm unlikely to want to log a single floating point value.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

In-depth study


(Xiaodi Wu) #29

I am delighted that, when I belatedly go to update NumericAnnex for the next version of Swift, I can just delete the whole thing. It has always been the hope that this functionality would be exposed in a library that ships with Swift itself. The problem being addressed is significant enough to warrant that sort of addition to the language.

I will name some residual concerns:

  • logGamma should not be generic over ElementaryFunctions for two reasons:

    1. It returns log(abs(gamma(x)) for real types and log(gamma(x)) for complex types; that is, its semantics differ among conforming types. When this has occurred in the design of our numeric protocols--I'm thinking specifically of / differing between integers and floating-point types--we have excluded such functions from protocols because the semantics don't generalize.
    2. It is not an elementary function. Which is not really a problem on its own. But: it proves the point that ElementaryFunctions is pretty well named (and not just a grab bag of functions) as, not only is logGamma one of the only non-elementary functions of the bunch, it also doesn't actually generalize among all types envisioned as conforming to ElementaryFunctions.
  • Agree with atan2(y:x:). However, should this be generic over ElementaryFunctions or just Real?

  • Likewise the same question for erf and erfc: do these belong on ElementaryFunctions? I mention this for practical reasons (my brain hurts thinking about implementing their complex generalization), and also because they, well, aren't elementary functions.

  • Please restore hypot for Real.

  • Like others, I feel that the T.Math.function() syntax is unusual and likely unnecessary. I implemented these as T.function() in NumericAnnex and felt that it did not unduly pollute the namespace. None of these functions would be particularly shocking to find on conforming types or out of place.


Minor points:

  • Because we are deliberately choosing to adhere to existing names unless they are problematic, I have a mild preference for loggamma (lowercase) over logGamma (camelCase), as other additions to the language in the future (such as sinpi) would be expected to follow existing lowercase convention. Using lowercase will also emphasize that the function is really its own thing with its own term of art, and not (at least for Real types) actually log(gamma(x)).

    • loggamma is actually precedented in at least one other language, as is lngamma; if we choose ln over log, then naturally we should change the name of this function to match
  • As for signGamma: unless it returns a value of type FloatingPointSign, it's more aptly sgngamma (as in the common abbreviation for the signum function): we already use the term signum in Swift to indicate such a function which returns -1, 0 (inapplicable in this case), or 1.

  • I have no strong preference of ln vs. log but mildly favor limiting code churn. Given that log has always meant natural log in Swift--and in most other languages--there isn't any evidence of confusion. Ultimately, though, either result is fine to me.


(Jeremy David Giesbrecht) #30

I tripped over this myself over and over again when I started programming, because in the outside, paper‐and‐pencil‐based world it never (in my experience in Canada and Germany) meant a natural logarithm (base e) and always meant a common logarithm (base 10).

However, I have since acquired the habit of checking the documentation before using any felled tree that shows up in any discipline :stuck_out_tongue:, so it will not confuse me anymore, nor bother me all that much if Swift chooses log.


(Steve Canon) #31

This is mostly analogous to log and sqrt; they return NaN for negative real numbers, but return normal results for the same values interpreted as complex numbers. It's slightly different in that logGamma attempts to make sense of negative real inputs for a mix of practical and historical reasons, but I perceive it to be more similar than different.

Flipping this around, can you think of any bugs or difficulties for user code that would be caused by this? I am hard-pressed to come up with any.

Erf and erfc are not elementary, but have natural analytic extensions to the complex plane. They are ... difficult to implement for complex numbers, but I think we could leverage the MIT-licensed libcerf here, so no one should be stuck implementing them for basic types. That said, we could certainly scope them down to Real.

We'll keep it around in platform, but I intend to propose a more general pattern for the functionality of hypot in the next few months, so I'd prefer not to include it for now. For those unfamiliar, hypot computes sqrt(x^2 + y^2) without undue overflow or underflow--if you evaluated the expression naively, you'd simply get zero or infinity for a huge subset of inputs; hypot returns a meaningful finite result instead. This is useful! But we should be have the building blocks to evaluate similar expressions without undue overflow or underflow as well, and we should also have a sum-of-squares function that returns a (result, scale) pair that it can be used in other contexts.

Agreed.

It does.


#32

I agree with @xwu that erf, erfc, and atan2 are only really useful with real-number inputs.

• • •

As for log vs. ln, I was originally ambivalent, but I just tried using a playground where I made both spellings available. When writing code, both were equally convenient, and in real life on pen-and-paper I’m comfortable using either one.

However, when I started reading over the code I had just written in the playground, I found that log was significantly more readable. It was easy to spot, it jumped out as a real pronounceable word, and it wasn’t confusable with anything else.

Conversely, ln was not nearly so nice. It is simultaneous too compact, and too similar to in or In or 1n. It just sort of blends in and makes equations appear denser and less comprehensible.

So I’m going to cast my lot on the side of retaining the log spelling, and also for logGamma.


(Xiaodi Wu) #33

I think this is the part that gives me pause.

That said, I too am hard-pressed to come up with concrete examples of difficulties, but then again, these days I'm not doing much with the gamma function in the first place.

Neat. That said, hypot(x, y) is so much nicer when you want exactly that than pretty much any other notation, such that even when it's trivially composed from more general building blocks, I'd still want that syntax around.

OK, rewinding to something you wrote at the top:

This is actually one of the reasons why, in NumericAnnex, I did not implement global functions. Instead, users would write Float.log(x) or Complex.log(x). Of course, where type inference allows its omission, they could write .log(x). However, where context makes it ambiguous, the user would have to specify, for the benefit of the reader as much as the compiler: Complex.log(-2.0).

It's hard to justify this design pervasively when complex types aren't being vended by the library, but it is a consideration.

I do wonder, if for all our distaste for C-like prefixes and suffixes, distinguishing csqrt from sqrt, etc., might be beneficial. To put it concretely in terms of what I would suggest for this proposal: have the global functions generic over only Real. If a user wants to write code generic over ElementaryFunctions, they can still use the non-global static functions explicitly. When a Complex type is added, the c-prefixed versions could be vended along with it.

This issue had escaped my attention for a minute on my initial review, but now that you bring it up I think it deserves attention. Those who have not tried to use the complex versions of elementary functions would likely be surprised at how different some of the results can be:

Complex.cbrt(-8.0) != -2.0

Given that a complex number can have a zero imaginary component, having a generic function that gives wildly different results (and not just result types) for what looks like the same input is a setup for pitfalls I'd rather not see created de novo in Swift.


(Svein Halvor Halvorsen) #34

Mathable?


(Steve Canon) #35

I think that this would be pretty unfortunate. exp( ) is a perfectly meaningful function for complex values (and even in more general settings) and the customary spelling of this operation is exp(x), not cexp(x). We'd be deliberately obfuscating this ... for what purpose, exactly? Making the language more approachable for novices is good, but not when it comes at the expense of clarity for everyone else.

Late reply to one of your earlier comments:

I would probably not use sinpi either; unlike, e.g. sin, this does not name a standard mathematical function, so I think we have more flexibility here. IEEE 754 uses sinPi, which we could follow, or we could do something wilder like sin(πTimes:). There are a number of options, and I don't know what we'll end up doing, but I don't think we should feel constrained to sinpi specifically.

So I think logGamma is fine, and improves clarity.

log1p and expm1 are the boundary cases in this reasoning for me. I can imagine wanting to give them clearer names, as again they are not standard mathematical function names, but these are just enshrined enough (and all the "clever" names I can see are just a little bit too clever) that I think we should follow precedent on them.


(Xiaodi Wu) #36

For the reason that the principal value is (can be) different between the real and complex functions. I think there is reason to be careful about this.

Scoping this proposal so that the global functions are generic over the reals doesn’t preclude later vending a broader overload of the same name, but I think that would be best decided when users actually have a complex type they can play with.

Fair point.


(Steve Canon) #37

Mathematics has been aware of this distinction for well over a century, and nonetheless considers these functions to be "the same" in a deep sense. That's an awful lot of learned experience to throw away because the behavior is transiently surprising to novices.


(Xiaodi Wu) #38

Few of us will ever rise above the level of novice mathematician, certainly not I. Some guardrails here are appropriate.

Is the sense among the C community that their c-prefixed functions are a mistake?


(Steve Canon) #39

The c-prefixed functions in C are necessary because C doesn't have overloading, so there was never an option to do anything else. Looking at languages that do support both overloading and complex numbers, C++, Julia, and Python all use exp( ) and log( ) for the complex operations. Julia in particular has spent a lot of time thinking about naming these operations (in the context of matrices, where they removed the dedicated matrix exponential function expm in favor of spelling it exp( ) as well--of particular note, some of the core Julia team members initially had the same response that you are having: "it forces people to disambiguate, which is good", but ultimately decided that enabling meaningful generic code was more valuable). Obviously Julia is much more math-centric than Swift is, so we shouldn't necessarily blindly follow them down this path, but the points that using this approach enables writing generic code for things like ODE solvers are hard to ignore.


(Xiaodi Wu) #40

Oops, thinko. Meant to refer to the C++ type generic macro but forgot that they aren’t prefixed. Fair enough.