julia
julia copied to clipboard
Fix pi == one(pi)*pi or update docs for one()
Based on the discussion in #37931, I have found that pi == one(pi)*pi is broken. However, it should hold according to the documentation of one() function:
one(x)
one(T::type)
Return a multiplicative identity for `x`: a value such that
`one(x)*x == x*one(x) == x`. Alternatively `one(T)` can
take a type `T`, in which case `one` returns a multiplicative
identity for any `x` of type `T`.
If possible, `one(x)` returns a value of the same type as `x`,
and `one(T)` returns a value of type `T`. However, this may
not be the case for types representing dimensionful quantities
(e.g. time in days), since the multiplicative
identity must be dimensionless. In that case, `one(x)`
should return an identity value of the same precision
(and shape, for matrices) as `x`.
...
Don't know if you prefer fixing the implementation or just updating the documentation to make it consistent. Notice that since one(pi) already returns true, it would be possible to make true * pi === pi.
FYI: Returning 1.0 (or 1 or true) isn't strictly wrong.
That is the multiplicative identity. The problem comes when you multiply it with pi, or even 2 with pi, you want to get pi or 2pi, but that's only possibly in a CAS (maybe e.g. in http://nemocas.org/ ). It was decided that actualy calculations would convert to floats that make pi rational approximation.
I think in this case the docs is at fault. It should say that identity only holds for T being a concrete type.
edit: I meant to distinguish where a T is "concretely" represented by bits but find it hard to point finger at:
julia> isconcretetype(typeof(pi))
true
julia> isbits(pi)
true
julia> sizeof(Int)
8
julia> sizeof(Irrational{:π})
0
One way to frame the problem is to say: multiplicative identity of Irrational{:pi} doesn't exist. (even before we talk about if the identity is of the same type or not)
typeof(pi) is a concrete type.
🤷♂️ Really starting to regret the Irrational type entirely.
While of course it would be nice, I don't really see why this so badly needs to hold --- many mathematical identities are not true e.g. with floating-point numbers.
At some point, I think we had the property that true*x was just x and false*x should always be zero(x). If we had that then this identity would hold. It would be a bit weird though since true*x and false*x would not be of the same type when x is an irrational. Perhaps that would be ok since it would be very amenable to constant propagation and type analysis.
Perhaps a case where a 0-bit integer is useful!
Isn't pi (and friends) already 0-bit integers? (base pi and whatnot)
First, thank you for the responses. I understand this is a minor issue.
I totally agree with @StefanKarpinski that the whole Irrational type is somehow unfortunate. I can demonstrate it by more practical examples:
- Problem with negation (not even
≈helps)
julia> x = big(1.0)
1.0
julia> cos(-big(π)-x) == cos(π+x)
true
julia> cos(-π-x) ≈ cos(π+x)
false
2πmay look like an irrational constant (but is already converted toFloat64), and the order of operations matters
julia> 2π+x == π+π+x
true
julia> 2π+x ≈ π+(π+x)
false
The current documentation contains only the following text, but nothing about Float64 is a fallback type.
AbstractIrrational <: Real
Number type representing an exact irrational value, which is automatically
rounded to the correct precision in arithmetic operations with other numeric
quantities.
What I love about julia is that it is easy to use and intuitive. But the behavior in these examples is not intuitive.
There is no easy fix. pi is already threatened as Float64 in many cases. Thus, the least breaking change would probably be to make pi behave like Float64 constant in v2.0 with a single exception of big(pi) or BigFloat(pi), respectively. This would also solve this issue since pi == one(pi)*pi would compare two floats. However, it would be necessary to deprecate current arithmetic operations with {Float16, Float32, BigFloat} like
big(1.0) + pi # deprecate this
big(1.0) + big(pi) # in favour of this
I belive it would also make user's code less error-prone.
julia> cos(-big(π)-x) == cos(π+x)
true
julia> cos(-π-x) ≈ cos(π+x)
false
The latter says a bit more about how cos is implemented on floats (vs big numbers) rather than about pi.
It is a bit surprising that it's not even approximately true, with cos symmetric and e.g.:
julia> cos(-π-0.1) == cos(π+0.1)
true
Note also that despite:
julia> cos(-big(π)-x) == cos(π+x)
true
Neither side gives the correct value as both are irrational (and the same), there the approximation of is just the same.
You did the right thing with -big(pi), note how careful you would have to be:
julia> -big(pi) ≈ big(-pi)
false
The issue came up again in a discussion, with a suggestion by @nsajko to have a function return irrational numbers instead based on an explicitly specified type, eg
irrational(:π, Float64) = 3.141592653589793
etc.
Yes, this is breaking, but it could be revisited for 2.0.
A better alternative would perhaps be to remove Irrational and instead just have each irrational be a function that takes a T <: AbstractFloat and returns the value of T closest to the given irrational. It seems like the lesson of irrationals is that if you want to do anything with them, convert them to float first. We might as well just have a function give a float to begin with.
Edit: That's literally what you wrote just above, derp. Disregard me, I must have brainfarted and couldn't read.
@jakobnissen Isn't that the same thing though?
We could actually have π == one(π)*π behave as expected even before Julia 2. I think there are basically two ways to a fix (without sacrificing type stability):
-
Make
one(π)return an instance of a singleton typeOne <: Integer. Then multiplication ofIrrationalandOnevalues could return the givenIrrationalvalue without type stability issues. A package namedZerosis already registered and seems to implementOneas required here, so maybe it could be moved toBase: https://juliahub.com/ui/Packages/Zeros/zPOBQ/0.3.0 ? Singleton<:Numbertypes likeZeroorOnewould be useful in many more places, for example: ifZero <: Integerexisted inBase, we could defineimag(::Real)asimag(::Real) = Zero(). EDIT: related discussion in #34003 -
Assuming that it's OK to add some data fields to
struct Irrational, we could haveone(π) isa typeof(π). For example, if we had aBoolexponent as a data member ofIrrational, a zero exponent would indicate that the value of theIrrationalvalue is one. This approach could be taken even further, for example if we had anInt8scaling factor in addition to the exponent, we could also have-π isa typeof(π)andzero(π) isa typeof(π). In such an implementation the scaling factor and exponent could both be encoded within a singleInt8value.
I guess that the first approach would be both easier to implement and more promising, but the second approach also seems OK. EDIT: actually, the second approach would be more powerful, in that it would prevent any information loss due to negation (-), which could fix some of the issues noted in the comments above. In particular, I think we could have cos(-π-x) == cos(π+x) with the second approach. EDIT2: on the other hand, the first approach could also be extended for achieving lossless -(::Irrational) by defining it using a wrapper type Neg: -(x::Irrational) = Neg(x).
Thoughts?
I still think that the whole concept of irrationals with precision resolved by context is a design mistake, so I think that fixing that by introducing yet another type is just compounding this.
Irrational of a given precision should be requested by the user, π (as a value) should be deprecated, and possibly replaced by π(::T) or similar.
I mostly agree, I was just thinking about what can be done before Julia 2.
A new interface irrational(:π, Float64) etc can be added any time without being breaking. That said, it might as well just live in a package.
In the meantime, Irrational as is now could remain unchanged, but its use could be discouraged.
It seems that Irrational is basically AbstractFloat with infinity digits. The difference is that, whereas promote usually takes two arguments and makes them the wider type, instead it demotes Irrational to the (float of) the other argument. In this sense, Irrational has its uses (but pi(::Type) might have been the better choice).
So one candidate resolution (which I will dismiss in my next paragraph) is to change == for Irrational to promote the arguments first (although Integer comparisons could virtually always be false), so that pi == x if oftype(x, pi) == x.
The problem is that this would create an inconsistency with inequalities. Because Float64(pi) < pi is true and this is "more true" than them being ==. So the implementation of a promote-based comparison would create a violation of the incredibly useful and widely-assumed property that two reals (except NaN) should be related by precisely one of <, ==, or >.
Maybe this is the original sin of the Irrational concept? We have other issues, like the current "paradox" that pi - Float64(pi) == 0.0 even though pi > Float64(pi) and the difference would be easily approximated as another Float64 (no pair of IEEE-754 floats in compliant arithmetic have the property that they are != yet their difference is 0). In this light, we could change == and </> to all promote Irrationals beforehand and that would resolve the paradox, at the price of losing the true fact that Float64(pi) < pi (but how useful is this?).
All of this is easily reconciled by the trivial fact that Irrational does not propagate through virtually any non-symbolic calculation (and is usually evaluated before the calculation, rather than during). Existential issues with Irrational aside, I'm not really worried about this issue. T(pi) == one(T(pi)) * T(pi) is true for any T<:AbstractFloat and I think that's good enough for me. Where in practice does one check an (uncasted) Irrational in a == with an arithmetic result?
I think that the easiest solution at this point (while remaining in Julia 1.x) could be just documenting the intended use and limitations of Irrational. Specifically, that it resolves into floating point in some situations, and this may not satisfy mathematical identities because, well, floating point.
Where in practice does one check an (uncasted)
Irrationalin a==with an arithmetic result?
Yes, I agree. We should just document that this is not a symbolic algebra system and using it as such will result in problems. Irrational is a mechanism to access mathematical constants at given precisions conveniently, nothing more.