This is a proposal to add the inverses of log(self, base: f64) -> f64 and log10(self) -> f64 to the standard lib, namely:
expf(self, base: f64) -> f64
exp10(self) -> f64
I was struck to find that there was no exp10 function in the stdlib. After all, the pairs exp/ln and exp2log2 exists, and so does log10 - so by symmetry so should exp10. Is there an explanation for this API asymmetry? I couldn't find any.
See comparison of currently required syntax (unless I'm missing something) and the possible syntax under this proposal
let x = 3.0_f64;
// All the following expressions should be ~3.0
// Because we are chaining an operation and its inverse
let y = x.log10().exp10(); // proposal
let z = 10_f64.powf(x.log10()); // current syntax required
let a = x.log(2.3).expf(2.3); // proposal
let b = 2.3_f64.powf(x.log(2.3)); // current syntax required
let c = x.log2().exp2(); // this is already supported
let d = x.ln().exp(); // also already supported
I feel that from symmetry this proposal is a no-brainer. It's more concise, obvious, clean to read, gets rid of the _f64 explicit type annotation that's unnecessary in symmetric mode.
For every float method there should be the inverse available, using predictable syntax. Right now that requirement is not satisfied as far as I'm aware.
Note: When I first wrote the code, I didn't include the _f64 in 10_f64.powf(x.log10()); causing compiler error. This proposal would eliminate this type of bug. One more reason.
Unfortunately, because exp() is already defined and Rust doesn't support overloading, the inverse of log(base: f64) can't be exp(base: f64), hence my idea to name it expf - but other names would be just as good. Maybe exp_base or something would be ok as well.
The lack of exp10 seems like an oversight. That would be nice to have.
I am not so convinced by a variable-base exponential function. While I agree that needing the _f64 in 2.3_f64.powf(x) is a pain (and just 2.3.powf(x) looks a bit weird too with the two periods), adding a new function that is just the same with argument order swapped feels like it could just add more confusion. Consider that you can also write f64::powf(2.3, x).
For comparison, the exponential functions included on the list of recommended operations in the IEEE754-2008 (or 2019) standards are B^x and B^x - 1 for each B in [e, 2, 10]: exp, expm1, exp2, exp2m1, exp10, exp10m1, and the listed logarithms are their inverses: log, logp1, log2, log2p1, log10, log10p1.
Out of those, Rust is missing exp10 and the +/-1 variants for bases 2 and 10.
Notably there is no variable-base logarithm there at all, and the natural logarithm is just called "log" (in Rust it is f64::ln, and f64::log takes any base).
As an aside, the reason for the ±1 variants (which Rust should add) is the loss of precision inherent in IEEE 754 when you do (B^x) - 1 if x is close to 0. For example, if x equals 1.0e-40_f64, then to the limits of f64, exp(x) - 1.0_f64 is 0.0, while expm1(x) is 1e-40. Similar applies in the other direction - log1p(1.0e-40_f64) is 1e-40, while log(1.0_f64 + 1.0e-40_f64) is 0.0.
Most of the time, this doesn't matter - but when it does matter, it really matters.
One possible reason is that C99 doesn't have an arbitrary-base logarithm function, and I would bet log(x)/log(2) is going to round incorrectly in common cases. Whereas pow exists, so pow(2, x) and pow(10, x) work fine (as 2 and 10 are exact), so the only necessary special case is exp(x) since pow(e, x) is inaccurate due to e not being representable. (And exp2 is just because the floating-point numbers use power-of-two scale, so it can be done more efficiently than general pow.)