I want to make a type that internally stores values in a Complex form. I assumed that I should simply convert any Real input. However, some math constants give an error doing this:
julia> Complex(2.0f0)
2.0f0 + 0.0f0im
julia> Complex(pi/2)
1.5707963267948966 + 0.0im
julia> Complex(pi)
ERROR: MethodError: no method matching Irrational{:π}(::Int64)
...
Stacktrace:
[1] convert(::Type{Irrational{:π}}, ::Int64) at ./number.jl:7
[2] oftype(::Irrational{:π}, ::Int64) at ./essentials.jl:334
[3] zero(::Irrational{:π}) at ./number.jl:262
[4] Complex(::Irrational{:π}) at ./complex.jl:16
[5] top-level scope at none:0
The same happens for ℯ. I caught it only because of a very arbitrary unit test. I’m not saying this behavior is irrational (har har!), but it is arguably not intuitive, so I would love to know if it’s intentional.
For the Real types I’ve thought to try, it does always work to add 0im. Not a big deal, but it feels a bit non-Julian to use addition for what is fundamentally a type conversion. I’m open to equally simple yet general alternatives.
a easy solution would be Complex(1*pi), but i’m checking about the irrational type:
update, i found this:
basically, the Irrational type gives arbitrary precision for math constants, allowing BigFloat(pi) to be pi with more digits, instead of a Float64 aproximation, the blog post explains that with more detail
in this case, i think is preferable to allow the composition, and transform the irrational to a concrete numeric type when an operation is performed, but the problem is that the Complex type asigns the type of the stored numbers to be the same:
as there is no implementation of zero(<:Irrational) (and it doesn’t make sense, zero and one aren’t irrationals), the generation of the Complex{Irrational{…}}` fails. this can be avoided at expense of hiding the transformation from the user, using a sane default (Float64?)
#something like this?
Complex(x::Irrational) = Complex{Float64}(x)
but the point is that the parameter of Complex{T} is the type of both the real and the imaginary parts, so the only way to build a Complex{Irrational{...}} is to do complex(x, x) with x isa Irrational.
If one wants to have complex(pi) with the highest precision possible, complex(big(pi)) is the only solution currently.
OTOH, they are numbers that can be computed to arbitrarily high precision (trivially), so there’s also no particular reason not to implement such methods.
Not sure that’s helpful in the grander scheme of what you’re saying, just thought it might be a useful way to think about it.
Irrationals are represented by singletons. It would be easy to have Irrational{:zero} (after all, Irrational represents a non-terminating turing machine that writes better and better approximations of a real number onto an infinite tape, i.e. is a glorified function pointer), but Complex does not permit real and imaginary parts to have different types. This means that e.g.
julia> Complex(pi, ℯ) |>typeof
Complex{Float64}
Even if we had irrational zero, this would not be enough to represent Complex(pi, true_zero).
To be clear, I’m not personally after some abstract notion of complex irrationals, which I’ve never given any thought to. I was looking for a way to convert to a complex representation built from whatever real type the caller sends. And yes, it’s easy for me to actually do.
My point is the same one made by @giordano on 28 July 2017 in the thread he gave: “I’d say that it’s better to automatically convert complex(pi) to Complex{Float64} (first version of this PR) rather than throwing an error for a simple conversion.”
Justifications of current behavior based on preserving the abstraction of pi to irrationality or arbitrary precision then have to explain why Complex(pi,0) and pi/2 not only work but produce results based on Float64 (which is what the majority of scientific computation expects).
The treatment of pi as a special kind of value is only a mm deep. Indeed, methodswith(typeof(pi)) returns no results in base. Irrational pi should be the use case that you go out of your way to find, as opposed to the current situation of the tail wagging the dog. I’d even go so far as to say it ought to be an explicit import.
complex(pi, 0) promotes the arguments to a common type, that is Float64. pi/2 is a Float64.
This is more useful:
julia> methodswith(AbstractIrrational)
[1] *(x::Bool, y::AbstractIrrational) in Base at irrationals.jl:135
[2] *(x::AbstractIrrational, y::AbstractIrrational) in Base at irrationals.jl:133
[3] +(x::AbstractIrrational, y::AbstractIrrational) in Base at irrationals.jl:133
[4] -(x::AbstractIrrational) in Base at irrationals.jl:131
[5] -(x::AbstractIrrational, y::AbstractIrrational) in Base at irrationals.jl:133
[6] /(x::AbstractIrrational, y::AbstractIrrational) in Base at irrationals.jl:133
[7] <(x::Float32, y::AbstractIrrational) in Base at irrationals.jl:76
[8] <(x::Float64, y::AbstractIrrational) in Base at irrationals.jl:74
[9] <(x::Float16, y::AbstractIrrational) in Base at irrationals.jl:78
[10] <(x::BigFloat, y::AbstractIrrational) in Base at irrationals.jl:82
[11] <(x::Rational{BigInt}, y::AbstractIrrational) in Base at irrationals.jl:117
[12] <(x::AbstractIrrational, y::AbstractIrrational) in Base at irrationals.jl:61
[13] <(x::AbstractIrrational, y::Float64) in Base at irrationals.jl:73
[14] <(x::AbstractIrrational, y::Float32) in Base at irrationals.jl:75
[15] <(x::AbstractIrrational, y::Float16) in Base at irrationals.jl:77
[16] <(x::AbstractIrrational, y::BigFloat) in Base at irrationals.jl:79
[17] <(x::AbstractIrrational, y::Rational{BigInt}) in Base at irrationals.jl:116
[18] <(x::AbstractIrrational, y::Rational{T}) where T in Base at irrationals.jl:99
[19] <(x::Rational{T}, y::AbstractIrrational) where T in Base at irrationals.jl:108
[20] <=(x::AbstractIrrational, y::AbstractIrrational) in Base at irrationals.jl:66
[21] <=(x::AbstractIrrational, y::AbstractFloat) in Base at irrationals.jl:86
[22] <=(x::AbstractFloat, y::AbstractIrrational) in Base at irrationals.jl:87
[23] <=(x::AbstractIrrational, y::Rational) in Base at irrationals.jl:119
[24] <=(x::Rational, y::AbstractIrrational) in Base at irrationals.jl:120
[25] ==(::AbstractIrrational, ::AbstractIrrational) in Base at irrationals.jl:57
[26] ==(x::AbstractIrrational, y::Real) in Base at irrationals.jl:69
[27] ==(x::Real, y::AbstractIrrational) in Base at irrationals.jl:70
[28] ^(::Irrational{:ℯ}, x::AbstractIrrational) in Base.MathConstants at mathconstants.jl:91
[29] ^(x::AbstractIrrational, y::AbstractIrrational) in Base at irrationals.jl:133
[30] big(x::AbstractIrrational) in Base at irrationals.jl:174
[31] isfinite(::AbstractIrrational) in Base at irrationals.jl:122
[32] isinteger(::AbstractIrrational) in Base at irrationals.jl:123
[33] isone(::AbstractIrrational) in Base at irrationals.jl:125
[34] iszero(::AbstractIrrational) in Base at irrationals.jl:124
[35] rationalize(::Type{T}, x::AbstractIrrational) where T in Base at irrationals.jl:91
I see I missed the modest number methods. I find iszero and isone to be quite bizarre in the context of errors for zero(pi) and one(pi)!
More broadly, I fail to see how the “common type” of two arguments is not the type of either one of them. Logical consistency would imply that pi/2 is irrational, no?
I reiterate: if I have to import special functions, or linear algebra for that matter, because they aren’t of interest to many who want to use Julia, then I don’t see why this stuff is part of the base.
Remember that irrationals in Julia are singletons. Rather than “common type” I should have said “the simplest type to which both numbers can be promoted”.
There have been quite some discussions on GitHub about this. It was decided in the end that any time you perform any operation on an Irrational it “collapses” onto a concrete AbstractFloat, either Float64 or BigFloat or whatever. Even though it could be interesting, It’d be very tricky to have an arithmetic for irrationals so that any elementary operations with irrationals gives another irrational, and so.
There is no general reason that should hold. It depends on the operation, eg /(::Int, ::Int) gives a Float64. Even for straightforward conversion (promote_type), you can run into cases where the promoted type is neither argument.
This is slightly different because in 1/1 the arguments are converted to an AbstractFloat, no promotion rule involved here. In the case of complex(pi, 0) the only operations performed is promote, but the promotion rule for AbstractIrrational and Int is to promote to Float64
My general point was that for f(::T1, ::T2, ...)::S, there is no rule that S has to be one of T1, T2, …, or an applicable parametric type with S, regardless of whether f is a constructor or a function.
I appreciate the great, thoughtful replies. The / example isn’t perfect; everyone understands rational/real numbers to generalize integers, while the relationship between Irrational and Float64 is less intuitive. But the larger point is taken.
(Also, why complex(1), Complex(1), float(1), but not Float(1)…never mind, I probably don’t want to know.)
I do still find a language that offers pi and 2pi by default, yet regards them as fundamentally different types of numerical objects, counter to my expectations from decades of both mathematics and computing. I still don’t see the payoff that it provides, aside from modest convenience in showing off VPA. Unlike most of the choices in base Julia, this one feels oddly skewed toward a niche form of computing.
There is probably no use of an Irrational type that doesn’t end in either full symbolic math or bailing out to a float type. Now, I have no problem with reverting to float. I was caught off-guard when that didn’t happen. And there are a bunch of ways to avoid/force it, I know.
I also get that people have thought about these things before, and decisions are made to optimize different goals. Consider this commentary more than criticism. I was relating the experience of one aspiring package author who was trying to follow the use of parameterized types, terminology like Real, and formal-sounding functions like one and zero, but still ended up getting burned in the case of the single input pi. The community can decide for itself whether it finds that concerning, or just the result of an old dog slow to learn new tricks.