Open pommicket opened 1 year ago
https://github.com/rust-lang/rfcs/pull/2700 chose not to do this for the mathematical constants.
So I think this ACP should address what's changed since then that means it was a bad idea then but a good idea now. And if moving MIN
like that was an RFC, maybe this should be too.
One thing that comes to mind is that use std::f32::consts::*;
seems more plausible than use f32::*;
, should that latter formulation ever be allowed.
I agree with @joshtriplett that it would be nice to be able to use
associated constants. Maybe a lang change to enable that should happen before this?
Relevant tracking issue: https://github.com/rust-lang/rust/issues/68490
I also suggested this there, but I think it's relevant to put here too.
You can kind of emulate a module in this case by using inherent_associated_types
: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=1498d15e059ff14dd69ae46c5a10ee94
Also possible with just an inherent constant, but doesn't result in purely path syntax: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=5b9d620c72fa0a10c66cb750a2515dc5
https://github.com/rust-lang/rfcs/pull/2700 chose not to do this for the mathematical constants.
Thanks I didn't know that.
Seems like it was decided that it could be left to a later RFC. I'd be happy to create it, if people want (although maybe that should be left to someone who has more experience creating RFCs), or wait until (if ever) it's possible to use f32::PI;
.
Personally, I would prefer to have the constants available even without the ability to use associated constants. I still think that the advantage of being able to type out f32::PI
rather than std::f32::consts::PI
is worth the extra difficulty of having to do const PI: f32 = f32::PI;
instead of use std::f32::consts::PI;
in the rare cases where you really need a short name for pi — but we wouldn't necessarily have to deprecate std::f32::consts
, at least until associated types can be used.
As for use std::f32::consts::*;
I'm not sure if that's ever a good idea, but okay that's a reasonable objection.
First off, I do agree withs the points that having to write std::f32::consts::PI
is a pain, and f32::PI
just working would be a very reasonable expectation.
However, the mathematical constants are fundamentally different from the current associated constants, which are all either special values of that type like NAN
or INFINITY
, or otherwise describe properties of the type like MANTISSA_DIGITS
or MAX_EXP
.
The constants in std::{f32,f64}::consts
are all approximations of the mathematical expressions they represent. Best approximations for their types, but rounded values nonetheless. For example, the value of std::f32::consts::PI
is in no way special in the f32
type: std::f32::consts::PI.sin()
does not return 0
, because f32::sin()
is periodic in 2π
, not in 2.0*std::f32::consts::PI
. Similarly, std::f32::consts::E.ln()
returns 0.99999994_f32
, which is the correctly rounded result: Round(ln(Round[e, 2^-22]),2^-24).
IMO, ideally we could do something like this:
let pi = std::math::PI as f32;
That is, the named mathematical constants would have one definition each, independently of the numeric types, with operations to obtain the actual primitive values in whichever type you want. This would naturally allow getting upper and lower bounds for those approximations as well:
use std::math::PI;
let pi_lb = PI.lower_bound().into();
for x in data {
// |x| < π
if x.abs() <= pi_lb {
...
}
}
The above would be equally correct for both f32 and f64, which is difficult to do with just the current definitions.
A couple of other things to consider:
This would add many more items under f32
which is already crowded. Many of these constants are redundant (Why have π/2
, π/4
and π/8
separately?), though admittedly it would also be weird to have some constants as associated items and others only in a separate path. Then again, having all the constants as associated items would raise the bar for ever adding new ones.
While the other names are sufficiently clear, I'm not sure if f32::E
would be. It's a bit of a stretch, but I could imagine that being accidentally used where f32::EPSILON
was intended.
IMO, ideally we could do something like this:
What is PI
here exactly? A generic constant (this exists in C++, but not rust yet...)? A constant of some special new type FloatingConstant
which impls Into<f32>
,Into<f64>
(i hope it would also impl Add<f32>
, etc)?
Anyways this definitely seems interesting but I'm not sure if it would happen in the forseeable future. And for now what might one day happen to PI is not really relevant to whether std::f32::consts::PI
or f32::PI
is better (except that having to switch paths twice is tedious)
The ability to get the lower and upper bound seems cool, but I don't know of any language that has that in its standard library... perhaps it is best left for a crate.
The above would be equally correct for both f32 and f64, which is difficult to do with just the current definitions.
Yeah, that's neat. Although it's worth mentioning the same could be said of NAN, INFINITY (although you can always do 1.0 / 0.0
or 0.0 / 0.0
which is more difficult to do (in a readable and definitely-precise way) for some mathematical constants).
At least with this proposal it would be possible to do:
type MySpecialType = f32; // might change to f64!
// ...
let pi = MySpecialType::PI;
which is not currently possible (admittedly you can do std::f64::consts::PI as MySpecialType
but that's less readable...)
And if typeof
is ever added it will be possible to do
for x in data {
if x.abs() <= (typeof x)::PI {
// ...
}
}
:)
This would add many more items under f32 which is already crowded.
Yeah, that's a good point. I will add it to the "Drawbacks" section of the RFC.
Many of these constants are redundant
Well, it is true that FRAC_PI_3 != PI / 3.0
for f64
, and maybe it might seem weird to have FRAC_PI_3
but not FRAC_PI_2
even though the latter could be perfectly accurately written as PI / 2.0
(tbh I mostly agree with you here but these constants already exist oh well)
I've got another option for you. Put these constants on a trait:
pub trait MathConsts {
const PI: Self;
// etc
}
Only when the trait is in scope, you can access all the constants directly on f32
or f64
:
use std::math::MathConsts;
dbg!(f64::PI);
This approach doesn't clutter editor suggestions or the docs page.
And you can even use type inference when you need to:
for x in data {
if x.abs() <= MathConsts::PI {
// ...
}
}
This would be especially nice since then you would be able to use PI in a generic context.
Unfortunately it would mean that adding a new constant would be a breaking change for any types which impl MathConsts
For what it's worth this trait already exists but with fns instead of consts in the num
crate: https://docs.rs/num/0.4.0/num/traits/trait.FloatConst.html :) — and for now maybe that's better given how limited const evaluation currently is....
I will add this to the "alternatives" section of the RFC though!
Unfortunately it would mean that adding a new constant would be a breaking change for any types which impl MathConsts
I think it would be best to seal this trait so it's only implementable by std types anyways.
Another option is to just create a supertrait for each addition and change the name resolution on an edition boundary.
IMO, ideally we could do something like this:
let pi = std::math::PI as f32;
One way to do that would be to add
// in std::num::consts
pub struct Pi;
And add impl From<Pi> for f32
and impl From<Pi> for f64
, which would also let others implement things like impl From<Pi> for MyFancyBigFloat
.
That way you'd have f32::from(Pi)
, or with Mul
overrides one could also have x * Pi
.
I played around with doing this for Zero
and One
in https://lib.rs/crates/zero-one, as a potential alternative to https://docs.rs/num-traits/latest/num_traits/identities/trait.Zero.html-style approaches. But it could absolutely work for things like π too.
(Dunno if it's something people would want, but I think it does at least mean a simpler approach than adding new traits for every constant of interest.)
One way to do that would be to add
// in std::num::consts pub struct Pi;
And add
impl From<Pi> for f32
andimpl From<Pi> for f64
, which would also let others implement things likeimpl From<Pi> for MyFancyBigFloat
.
This does seem to match mathematical reality most closely in that we cannot express real numbers exactly, so the idea of converting with From has considerable appeal.
If we have a symbolic struct Pi
, then we could also implement for it functions with nice outcomes, like sin and cos. Which makes me wonder how we can get sin(2*pi) etc to work as well.
Which makes me wonder how we can get sin(2*pi) etc to work as well.
I think it would be best to just leave this up to a Rational
type.
let n = Rational::new(2, 1) * Rational::from(Pi);
dbg!(n.sin()); // 0/1
And if typeof is ever added it will be possible to do
for x in data { if x.abs() <= (typeof x)::PI { // ... } }
And you can even use type inference when you need to:
for x in data { if x.abs() <= MathConsts::PI { // ... } }
These are actually a good example of why I mentioned lower bounds; your examples would not be strictly correct for both f32 and f64. I really did mean that it's difficult with just the current definitions: With the rounded values of the constants, it just so happens that f64::PI < π < f32::PI
(but you couldn't know this just from reading the documentation), so if the code is intended to separate out precisely the values greater than π, it needs to be either x < f32::PI
or x <= f64::PI
. Related issue: https://github.com/rust-lang/rust/issues/108769#issuecomment-1465069896
If we have a symbolic
struct Pi
, then we could also implement for it functions with nice outcomes, like sin and cos. Which makes me wonder how we can get sin(2*pi) etc to work as well.
The IEEE-754 standard recommends the functions sinPi, cosPi, tanPi, asinPi, acosPi, atanPi, and atan2Pi, which all measure angles in multiples of π. E.g. sinPi(x) = sin(π*x)
, asinPi(x) = asin(x)/π
. Implementing those for f32 and f64 might be a reasonable thing to do.
A more ambitious (experiment-in-a-crate-first) direction might be something like a wrapper type for multiples of π:
Anyway, @scottmcm's suggestion of having a ZST-type Pi: Into<f32> + Into<f64>
is pretty much what I was tending towards, although I would be wary of defining the arithmetic operators by casting to the other operand's type since Rust doesn't generally do implicit conversions.
This is neat, but all general-purpose programming languages that I know of just have floating-point constants for π, etc., and they seem to get along fine without all this "machinery". In other languages people use math.pi
/Math.PI
/whatever all the time — just having PI
constants for f32
and f64
is good enough for 95% of cases (the remaining 5% can imo be left to crates...)
Using floating-point numbers in general will invariably lead to some amount of mathematical incorrectness unless you're extremely careful. No, that example code isn't correct, but it's correct down to 1 ULP which is good enough for almost everyone (if those values came from a previous calculation with floats, they will be (potentially) off by at least 1 ULP anyways).
This is neat, but all general-purpose programming languages that I know of just have floating-point constants for π, etc., and they seem to get along fine without all this "machinery". In other languages people use math.pi/Math.PI/whatever all the time — just having PI constants for f32 and f64 is good enough for 95% of cases (the remaining 5% can imo be left to crates...)
Well, I wasn't sure either, but I tried with Julia and found out the standard library can do this:
julia> π
π = 3.1415926535897...
julia> π == pi
true
julia> typeof(π)
Irrational{:π}
julia> sin(π)
1.2246467991473532e-16
julia> sinpi(1)
0.0
julia> sin(BigFloat(π))
1.096917440979352076742130626395698021050758236508687951179005716992142688513354e-77
julia> sin(BigFloat(π, precision=20000))
4.502850813633212220480382554741730704191676738588386332317097735049223941670455e-6021
julia> lb = Float32(π, RoundDown)
3.1415925f0
julia> ub = Float32(π, RoundUp)
3.1415927f0
julia> lb < π < ub
true
julia> nextfloat(lb) == ub
true
Now, dynamic typing does afford a lot of internal complexity without getting in the way of ergonomics, and Rust would have a hard time reaching the same.
Using floating-point numbers in general will invariably lead to some amount of mathematical incorrectness unless you're extremely careful. No, that example code isn't correct, but it's correct down to 1 ULP which is good enough for almost everyone (if those values came from a previous calculation with floats, they will be (potentially) off by at least 1 ULP anyways).
Sure, some amount of error is to be expected in many cases, but regardless, the value you have is the value you're using, and you expect the range check to check that value. As in the issue I linked, if the reason for testing the magnitude (supposing the threshold was actually π/2) was that you next pass the angle to tan
, that 1 ULP will become the difference between something very positive and something very negative.
Proposal
Problem statement
Currently, to use mathematical constants such as π, you need to use
std::f32::consts::PI
. This proposal is for adding associated constants to thef32
andf64
types so that the constants could be accessed with justf32::PI
for example.Motivation, use-cases
std::f32::consts::PI
is quite long. It takes a while to type out, and makes code difficult to read if written out in full everywhere.Consider
vs
You can always do something like
use std::f32::consts as f32c;
but it would be nice for a shorter path to be included in the standard library.f32::PI
and to my disappointment it didn't work and I had to do an internet search to find out the correct path.NAN
,INFINITY
, etc. are associated constants, and it feels inconsistent to me forf32::NAN
to exist, but notf32::PI
.Solution sketches
I have implemented this on my fork: https://github.com/rust-lang/rust/compare/master...pommicket:rust:shorter-paths-for-float-consts
Links and related work
half
andfixed
already have these as associated constants on their types.What happens now?
This issue is part of the libs-api team API change proposal process. Once this issue is filed the libs-api team will review open proposals in its weekly meeting. You should receive feedback within a week or two.