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.
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 log
.
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?
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.
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 .
Thanks! Fixed.
+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
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 overElementaryFunctions
for two reasons:- It returns
log(abs(gamma(x))
for real types andlog(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
ElementaryFunctions
is pretty well named (and not just a grab bag of functions) as, not only islogGamma
one of the only non-elementary functions of the bunch, it also doesn't actually generalize among all types envisioned as conforming toElementaryFunctions
.
- It returns
-
Agree with
atan2(y:x:)
. However, should this be generic overElementaryFunctions
or justReal
? -
Likewise the same question for
erf
anderfc
: do these belong onElementaryFunctions
? 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
forReal
. -
Like others, I feel that the
T.Math.function()
syntax is unusual and likely unnecessary. I implemented these asT.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) overlogGamma
(camelCase), as other additions to the language in the future (such assinpi
) 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 forReal
types) actuallylog(gamma(x))
.loggamma
is actually precedented in at least one other language, as islngamma
; if we chooseln
overlog
, then naturally we should change the name of this function to match
-
As for
signGamma
: unless it returns a value of typeFloatingPointSign
, it's more aptlysgngamma
(as in the common abbreviation for the signum function): we already use the termsignum
in Swift to indicate such a function which returns-1
,0
(inapplicable in this case), or1
. -
I have no strong preference of
ln
vs.log
but mildly favor limiting code churn. Given thatlog
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.
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
log
.
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.
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
.
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.
Mathable
?
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.
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.
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?
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.
Oops, thinko. Meant to refer to the C++ type generic macro but forgot that they arenāt prefixed. Fair enough.