- Proposal: SE-0246
- Author: Stephen Canon
- Review Manager: John McCall
- Status: Accepted with modifications (2019-03-28)
- Implementation: apple/swift#23140
- Previous Revision: 1 2
- Decision Notes: (review) (acceptance)
This proposal is accepted, but currently not implemented in Swift due to source breaking consequences relating to typechecker performance and shadowing rules. These are expected to be resolved in a future release of Swift, at which point this proposal can be implemented.
In the mean-time, users can make use of the Swift Numerics package, which also provides this functionality.
This proposal introduces two new protocols to the standard library: ElementaryFunctions
and Real
. These protocols combine to provide "basic math functions" in generic contexts
for floating-point and SIMD types, and provide a path to extend that functionality to
planned complex types in the future.
BinaryFloatingPoint
(and the protocols it refines) provides a powerful set of
abstractions for writing numerical code, but it does not include the transcendental
operations defined by the C math library, which are instead imported by the platform
overlay as a set of overloaded concrete free functions.
There are two deficiencies with the current approach. First, what you need to import
to get this functions varies depending on platform, forcing the familiar but awkward
#if
dance:
#if canImport(Darwin)
import Darwin
#elseif canImport(GlibC)
...
This shouldn't be required for functionality that is intended to be available on all platforms.
Second, these bindings are overloaded for the various concrete BinaryFloatingPoint
types, but there's no way to use them generically. Suppose we want to implement the
"sigmoid" function generically:
func sigmoid<T>(_ x: T) -> T where T: FloatingPoint {
return 1/(1 + exp(-x))
}
This doesn't work, because exp
is not available on the FloatingPoint
protocol.
Currently, you might work around this limitation by doing something like:
func sigmoid<T>(_ x: T) -> T where T: FloatingPoint {
return 1/(1 + T(exp(-Double(x))))
}
but that's messy, inefficient if T is less precise than Double, and inaccurate if T is more precise than Double. We can and should do better in Swift.
With the changes in this proposal, the full implementation would become:
import Math
func sigmoid<T>(_ x: T) -> T where T: Real {
return 1/(1 + exp(-x))
}
There are four pieces of this proposal: first, we introduce the protocol
ElementaryFunctions
:
public protocol ElementaryFunctions {
/// The cosine of `x`.
static func cos(_ x: Self) -> Self
/// The sine of `x`.
static func sin(_ x: Self) -> Self
/// The tangent of `x`.
static func tan(_ x: Self) -> Self
...
}
Conformance to this protocol means that the elementary functions are available as static functions:
(swift) Float.exp(1)
// r0 : Float = 2.7182817
(For the full set of functions provided, see Detailed Design below). All of the standard
library FloatingPoint
types conform to ElementaryFunctions
; a future Complex
type would also conform. SIMD
types do not conform themselves, but the operations
are defined on them when their scalar type conforms to the protocol.
The second piece of the proposal is the protocol Real
:
public protocol Real: FloatingPoint, ElementaryFunctions {
/// `atan(y/x)` with quadrant fixup.
///
/// There is an infinite family of angles whose tangent is `y/x`.
/// `atan2` selects the representative that is the angle between
/// the vector `(x, y)` and the real axis in the range [-π, π].
static func atan2(y: Self, x: Self) -> Self
/// The error function evaluated at `x`.
static func erf(_ x: Self) -> Self
/// The complimentary error function evaluated at `x`.
static func erfc(_ x: Self) -> Self
/// sqrt(x*x + y*y) computed without undue overflow or underflow.
///
/// Returns a numerically precise result even if one or both of x*x or
/// y*y overflow or underflow.
static func hypot(_ x: Self, _ y: Self) -> Self
...
}
This protocol does not add much API surface, but it is what most users will
write generic code against. The benefit of this protocol is that it allows
us to avoid multiple constraints for most simple generic functions, and to
adopt a clearer naming scheme for the protocol that most users see, while
also giving ElementaryFunctions
a more precise name, suitable for
sophisticated uses.
The third piece of the proposal is the introduction of generic free function implementations of math operations:
(swift) exp(1.0)
// r0 : Float = 2.7182817
Finally, we will update the platform imports to obsolete existing functions
covered by the new free functions, and also remove the
imports of the suffixed <math.h> functions (which were actually never
intended to be available in Swift). Updates will only be necessary with functions like atan2(y: x:)
where we are adding argument labels or logGamma( )
where we have new
function names. In these cases we will deprecate the old functions instead
of obsoleting them to allow users time to migrate.
The full API provided by ElementaryFunctions
is as follows:
/// The square root of `x`.
///
/// For real types, if `x` is negative the result is `.nan`. For complex
/// types there is a branch cut on the negative real axis.
static func sqrt(_ x: Self) -> Self
/// The cosine of `x`, interpreted as an angle in radians.
static func cos(_ x: Self) -> Self
/// The sine of `x`, interpreted as an angle in radians.
static func sin(_ x: Self) -> Self
/// The tangent of `x`, interpreted as an angle in radians.
static func tan(_ x: Self) -> Self
/// The inverse cosine of `x` in radians.
static func acos(_ x: Self) -> Self
/// The inverse sine of `x` in radians.
static func asin(_ x: Self) -> Self
/// The inverse tangent of `x` in radians.
static func atan(_ x: Self) -> Self
/// The hyperbolic cosine of `x`.
static func cosh(_ x: Self) -> Self
/// The hyperbolic sine of `x`.
static func sinh(_ x: Self) -> Self
/// The hyperbolic tangent of `x`.
static func tanh(_ x: Self) -> Self
/// The inverse hyperbolic cosine of `x`.
static func acosh(_ x: Self) -> Self
/// The inverse hyperbolic sine of `x`.
static func asinh(_ x: Self) -> Self
/// The inverse hyperbolic tangent of `x`.
static func atanh(_ x: Self) -> Self
/// The exponential function applied to `x`, or `e**x`.
static func exp(_ x: Self) -> Self
/// Two raised to to power `x`.
static func exp2(_ x: Self) -> Self
/// Ten raised to to power `x`.
static func exp10(_ x: Self) -> Self
/// `exp(x) - 1` evaluated so as to preserve accuracy close to zero.
static func expm1(_ x: Self) -> Self
/// The natural logarithm of `x`.
static func log(_ x: Self) -> Self
/// The base-two logarithm of `x`.
static func log2(_ x: Self) -> Self
/// The base-ten logarithm of `x`.
static func log10(_ x: Self) -> Self
/// `log(1 + x)` evaluated so as to preserve accuracy close to zero.
static func log1p(_ x: Self) -> Self
/// `x**y` interpreted as `exp(y * log(x))`
///
/// For real types, if `x` is negative the result is NaN, even if `y` has
/// an integral value. For complex types, there is a branch cut on the
/// negative real axis.
static func pow(_ x: Self, _ y: Self) -> Self
/// `x` raised to the `n`th power.
///
/// The product of `n` copies of `x`.
static func pow(_ x: Self, _ n: Int) -> Self
/// The `n`th root of `x`.
///
/// For real types, if `x` is negative and `n` is even, the result is NaN.
/// For complex types, there is a branch cut along the negative real axis.
static func root(_ x: Self, _ n: Int) -> Self
Real
builds on this set by adding the following additional operations that are either
difficult to implement for complex types or only make sense for real types:
/// `atan(y/x)` with quadrant fixup.
///
/// There is an infinite family of angles whose tangent is `y/x`. `atan2`
/// selects the representative that is the angle between the vector `(x, y)`
/// and the real axis in the range [-π, π].
static func atan2(y: Self, x: Self) -> Self
/// The error function evaluated at `x`.
static func erf(_ x: Self) -> Self
/// The complimentary error function evaluated at `x`.
static func erfc(_ x: Self) -> Self
/// sqrt(x*x + y*y) computed without undue overflow or underflow.
///
/// Returns a numerically precise result even if one or both of x*x or
/// y*y overflow or underflow.
static func hypot(_ x: Self, _ y: Self) -> Self
/// The gamma function evaluated at `x`.
///
/// For integral `x`, `gamma(x)` is `(x-1)` factorial.
static func gamma(_ x: Self) -> Self
/// `log(gamma(x))` computed without undue overflow.
///
/// `log(abs(gamma(x)))` is returned. To recover the sign of `gamma(x)`,
/// use `signGamma(x)`.
static func logGamma(_ x: Self) -> Self
/// The sign of `gamma(x)`.
///
/// This function is typically used in conjunction with `logGamma(x)`, which
/// computes `log(abs(gamma(x)))`, to recover the sign information that is
/// lost to the absolute value.
///
/// `gamma(x)` has a simple pole at each non-positive integer and an
/// essential singularity at infinity; we arbitrarily choose to return
/// `.plus` for the sign in those cases. For all other values, `signGamma(x)`
/// is `.plus` if `x >= 0` or `trunc(x)` is odd, and `.minus` otherwise.
static func signGamma(_ x: Self) -> FloatingPointSign
These functions directly follow the math library names used in most other languages, as there is not a good reason to break with existing precedent. The changes worth noting are as follows:
-
exp10
does not exist in most C math libraries. It is a generally useful function, corresponding tolog10
. We'll fall back on implementing it aspow(10, x)
on platforms that don't have it in the system library. -
There are two functions named
pow
with different signatures. One implements the IEEE 754powr
function (nan ifx
is negative), the other restricts the exponent to have typeInt
, and implements the IEEE 754pown
function. -
The function
root
does not exist in most math libraries; it computes the nth root of x. For now this is implemented in terms ofpow
, but we may adopt other implementations for better speed or accuracy in the future. -
Argument labels have been added to
atan2(y:x:)
. This is the only math.h function whose argument order regularly causes bugs, so it would be good to clarify here. -
logGamma
is introduced instead of the existinglgamma
, and returns a single value instead of a tuple. The sign is still available via a newsignGamma
function, but requires a separate function call. The motivation for this approach is two-fold: first, the more common use case is to want only the first value, so returning a tuple creates noise:let (result, _) = lgamma(x)
Second, there's an outstanding bug that results from the C interfaces being re-exported in Swift where
lgamma
is ambiguous; it can be either the platform shim returning(T, Int)
, or the C library function returningDouble
; we want to deprecate the first and make the second unavailable. Simultaneously introducing yet another function with the same name would create a bit of a mess.
The following functions recommended by IEEE 754 are not provided at this point
(because implementations are not widely available), but are planned for future expansion,
possibly with implementation directly in Swift: cospi
, sinpi
, tanpi
, acospi
, asinpi
,
atanpi
, exp2m1
, exp10m1
, log2p1
, log10p1
, compound
(these are the names
used by IEEE 754; Swift can use different names if we like).
The following functions are exported by <math.h>, but will not be defined on ElementaryFunctions:
frexp
, ilogb
, ldexp
, logb
, modf
, scalbn
, scalbln
, fabs
, ceil
,
floor
, nearbyint
, rint
, lrint
, llrint
, round
, lround
, llround
, trunc
, fmod
,
remainder
, remquo
, copysign
, nan
, nextafter
, nexttoward
, fdim
, fmin
, fmax
,
fma
.
Most of these are not defined on ElementaryFunctions because they are inherently bound to the
semantics of FloatingPoint
or BinaryFloatingPoint
, and so cannot be defined for
types such as Complex or Decimal. Equivalents to many of them are already defined on
[Binary]FloatingPoint
anyway--in those cases free functions are defined as generic over FloatingPoint
or BinaryFloatingPoint
:
@available(swift, introduced: 5.1)
@_alwaysEmitIntoClient
public func ceil<T>(_ x: T) -> T where T: FloatingPoint {
return x.rounded(.up)
}
@available(swift, introduced: 5.1)
@_alwaysEmitIntoClient
public func floor<T>(_ x: T) -> T where T: FloatingPoint {
return x.rounded(.down)
}
@available(swift, introduced: 5.1)
@_alwaysEmitIntoClient
public func round<T>(_ x: T) -> T where T: FloatingPoint {
return x.rounded()
}
@available(swift, introduced: 5.1)
@_alwaysEmitIntoClient
public func trunc<T>(_ x: T) -> T where T: FloatingPoint {
return x.rounded(.towardZero)
}
@available(swift, introduced: 5.1)
@_alwaysEmitIntoClient
public func fma<T>(_ x: T, _ y: T, _ z: T) -> T where T: FloatingPoint {
return z.addingProduct(x, y)
}
@available(swift, introduced: 5.1)
@_alwaysEmitIntoClient
public func remainder<T>(_ x: T, _ y: T) -> T where T: FloatingPoint {
return x.remainder(dividingBy: y)
}
@available(swift, introduced: 5.1)
@_alwaysEmitIntoClient
public func fmod<T>(_ x: T, _ y: T) -> T where T: FloatingPoint {
return x.truncatingRemainder(dividingBy: y)
}
These definitions replace the definitions existing in the platform module.
A few of the other functions (nearbyint
, rint
) are fundamentally tied to the
C language notion of dynamic floating-point rounding-modes, which is not modeled
by Swift (and which we do not have plans to support--even if Swift adds rounding-mode
control, we should avoid the C fenv model). These are deprecated.
The remainder will not be moved into the Math module at this point, as they can
be written more naturally in terms of the FloatingPoint
API. We intend to
deprecate them.
This is an additive change, but it entails some changes for platform modules; the existing platform implementations provided by the Darwin or GLibc should be deprecated and made to redirect people to the new operations.
For the standard library, this is an additive change. We'll need to continue to support the old platform hooks to provide binary stability, but will mark them deprecated or obsoleted.
This is an additive change.
-
The name
ElementaryFunctions
is a marked improvement on the earlierMathable
, but is still imperfect. As discussed above, the introduction of theReal
protocol mostly renders this issue moot; most code will be constrained to that instead. -
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 (likeexpm1
), but the weight of common practice is pretty strong here; almost all languages use essentially the same names for these operations. Another alternative would be to break these up intoTrigonometricFunctions
,HyperbolicFunctions
,ExponentialFunctions
, etc, but I don't think that actually buys us very much. -
We may also want to add
log(_ base: T, _ x: T) -> T
at some future point as a supplement to the existinglog
,log2
, andlog10
functions. Python and Julia both provide a similar interface. Doing this correctly requires a building block that the C math library doesn't provide (an extra-preciselog
orlog2
that returns a head-tail representation of the result); without this building block rounding errors creep in even for exact cases:>>> from math import log >>> log(3**20, 3) 19.999999999999996
Julia includes a warning about this in their documentation that basically says "Use log2 or log10 instead if base is 2 or 10". We could take that approach, but base 2 and 10 cover 99% of uses, so I would rather wait to provide this function until we have time to do it correctly.
-
We could spell
log
(the natural logarithm)ln
. This would resolve some ambiguity for users with a background in certain fields, at the cost of diverging from the vast majority of programming languages. Rust and Kotlin do spell it this way, so we wouldn't be completely alone. It would also avoid using a function name that potentially conflicts (visually or syntactically) with an obvious name for logging facilities. However, depending on font,ln
is easily confused within
, and it breaks the similarity with the otherlog
functions. As an assistance, we will addln
in theMath
module but mark it unavailable, referring users tolog
. -
We could put the free functions into a separate module instead of the standard library. Having them in a separate module helps avoid adding stuff to the global namespace unless you're actually using it, which is generally nice, and the precedent from other languages is pretty strong here:
#include <cmath>
,import math
, etc. However, the general utility of having these functions available in the global namespace is felt to outweigh this. -
We could define an operator like
^
or**
for one or both definitions ofpow
. I have opted to keep new operators out of this proposal, in the interests of focusing on the functions and their implementation hooks. I would consider such an operator to be an additive change to be considered in a separate proposal. -
Add the constants
pi
ande
toT.Math
. There's a bit of a question about how to handle these with hypothetical arbitrary-precision types, but that's not a great reason to keep them out for the concrete types we already have. Plus we already havepi
on FloatingPoint, so someone implementing such a type already needs to make a decision about how to handle it. There's a second question of how to handle these withComplex
orSIMD
types; one solution would be to only define them for types conforming toReal
.
-
A number of functions (
atan2
,erf
,erfc
,gamma
,logGamma
) have been moved fromElementaryFunctions
ontoReal
. The rationale for this is threefold:atan2
never made much sense for non-real arguments. Implementations oferf
,erfc
,gamma
andlogGamma
are not available on all platforms. Finally, those four functions are not actually "elementary", so the naming is more accurate following this change. -
hypot
has been added toReal
. We would like to have a more general solution for efficiently rescaling computations in the future, buthypot
is a tool that people can use today. -
I have dropped the
.Math
pseudo-namespace associatedtype from the protocols. In earlier versions of the proposal, the static functions were spelledFloat.Math.sin(x)
. This was motivated by a desire to avoid "magic" underscore-prefixed implementation trampolines, while still grouping these functions together under a single.Math
in autocompletion lists. Whatever stylistic benefits this might have were judged to be not worth the extra layer of machinery that would be fixed into the ABI even if we get a "real" namespace mechanism at some future point. These functions now appear simply asFloat.sin(x)
.