Just to be clear, I was actually arguing against having a
log, and instead just
log without specifying a base is an issue pervasive throughout the sciences, I don't really want Swift to keep perpetuating that.
Just to be clear, I was actually arguing against having a
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.
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
Wait, what? Currently
log is available in
Foundation, but not the standard library and certainly not the as-yet-nonexistant
Does the proposal include changes to other libraries?
log is emphatically not in
log is in the
platform module (
GLibc) and the
CoreGraphics module (in the case of
Foundation transitively imports.
The proposal specifically highlights how it would change the existing
We will update the platform imports to obsolete existing functions covered by the new free functions in the
Mathmodule, 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.
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 .
+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.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
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
- Is the problem being addressed significant enough to warrant a change to Swift?
- Does this proposal fit well with the feel and direction of Swift?
- 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
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?
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:
logGammashould not be generic over
ElementaryFunctionsfor two reasons:
- 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.
- It is not an elementary function. Which is not really a problem on its own. But: it proves the point that
ElementaryFunctionsis pretty well named (and not just a grab bag of functions) as, not only is
logGammaone of the only non-elementary functions of the bunch, it also doesn't actually generalize among all types envisioned as conforming to
- It returns
atan2(y:x:). However, should this be generic over
Likewise the same question for
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.
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.
Because we are deliberately choosing to adhere to existing names unless they are problematic, I have a mild preference for
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
loggammais actually precedented in at least one other language, as is
lngamma; if we choose
log, then naturally we should change the name of this function to match
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
signumin Swift to indicate such a function which returns
0(inapplicable in this case), or
I have no strong preference of
logbut mildly favor limiting code churn. Given that
loghas 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.
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 , so it will not confuse me anymore, nor bother me all that much if Swift chooses
This is mostly analogous to
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
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.
I agree with @xwu that
atan2 are only really useful with real-number inputs.
• • •
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.
ln was not nearly so nice. It is simultaneous too compact, and too similar to
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
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
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:
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
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.
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
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
So I think
logGamma is fine, and improves clarity.
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.
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.
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.
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?
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.
Oops, thinko. Meant to refer to the C++ type generic macro but forgot that they aren’t prefixed. Fair enough.