# Complexifying pi

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

1 Like

To add to @longemen3000’s explanation: the thing that does work is

``````julia> Complex{Float64}(pi)
3.141592653589793 + 0.0im
``````

because now we can infer to what precision you want to have it.

4 Likes

The question is, should those types compose? Should there be a `Complex{Irrational{...}}` type?

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:

``````julia> x = Complex(1,BigFloat(1))
1.0 + 1.0im

julia> typeof(x)
Complex{BigFloat}

julia> typeof(x.re)
BigFloat
``````

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)
``````

``````julia> complex(pi, pi) |> typeof
Complex{Irrational{:π}}
``````

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.

3 Likes

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.

1 Like

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)`.

2 Likes

These things were already discussed in the issue I linked above, including the irrational zero.

Actually, there are a couple of ways to define a complex irrational number which retains its irrational features:

``````julia> Complex{Real}(pi, 0)
π = 3.1415926535897... + 0im

[...]

julia> Base.complex(x::T) where {T<:Irrational} = Complex{Union{T, Int}}(x, 0)

julia> complex(pi)
π = 3.1415926535897... + 0im
``````

but they aren’t super efficient (at least, they weren’t two years ago) because of type instability, see
https://github.com/JuliaLang/julia/pull/22928

2 Likes

Thanks to @longemen3000 and @giordano for the pointers to the discussions.

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.

1 Like

`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
``````
1 Like

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.

1 Like

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.

``````julia> promote_type(AbstractIrrational, Int)
Float64
``````

is actually a nice example of how this can fail.

Regarding the original question: one solution I did not see above is

``````julia> Complex(float(pi))
3.141592653589793 + 0.0im
``````

which could actually be a fallback for the single-argument `Complex` constructor.

1 Like
1 Like

I am not sure we are talking about the same thing — the PR is about something else, using `float` was mentioned in a comment and was generally approved.

The PR had initially the method you suggested.

Quoting verbatim from the comment you linked:

He suggested to manually do `complex(float(pi))` and keep `complex(pi)` erroring

1 Like

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.

4 Likes