Zero(π) and one(π) are false and true?

That can also be argued, I just think all numeric types need to have one and zero defined, at least a lot of code relies on that to be generic, now if it’s misleading then maybe you don’t want it defined… [Is it helpful, or hurtful, to have it, and then approximate? I’m not sure what would happen with just one and zero dropped and irrationals otherwise kept, more or less complaining?] I just meant in the math sense 1 (the returned type sort of) is that identity. Of course the functions are all about types, either you would simply write 1 and 0…

Yes, in retrospect that would have been much better. Cf

3 Likes

I would argue that, since zero and one is apparently defined as being the additive and multiplicative identity, having one(π) * π != π is quite clearly a bug.
The alternative is to either define a one or zero type for irrationals, or preferrably, to not have added this buggy definition in the first place.

2 Likes

The problem here seems to be the definition of != or == which is not the same as the mathematical concept of =.

We also have the following result due to floating point arithmetic. Due to finite precision, we know the underlying numerical system has imperfections.

julia> 0.1 + 0.2 == 0.3
false

julia> 0.3 - 0.1 - 0.2
-2.7755575615628914e-17

If we are trying to apply mathematical concepts, then often , \approx[tab], is probably a better suited for that purpose.

julia> 0.1 + 0.2 ≈ 0.3
true

julia> one(π) * π ≈ π
true
2 Likes

I don’t think this is the same. 0.3 isn’t defined as “the floating point sum of 0.1 and 0.2”. It just happens that they are equal in some number systems (but not Float64), and has many other properties as well.

one, on the other hand has only one primary property, which is identical to its definition, and that is

one(x) * x == x

When this fails, it’s seems a bit pointless.

6 Likes

Why do we even need one(x) to begin with? Why not just one * x?

julia> import Base.*

julia> one::typeof(one) * x = x
* (generic function with 338 methods)

julia> one * π
π = 3.1415926535897...
1 Like

It took my a while seeing you were defining a function… with:

one::typeof(one) * x = x  # seems you don't need: import Base.*

julia> π * one
ERROR: MethodError: no method matching *(::Irrational{:π}, ::typeof(one))

so as a replacement for 1-ary function, since you define a binary operator/function, you would also need just in case someone rather does above for it to work:

x * one::typeof(one) = x  # or this way, still looks odd: *(x, one::typeof(one)) = x

It’s too bad you can’t define:

julia> 1::typeof(one) * x = x
ERROR: syntax: "1" is not a valid function argument name around REPL[575]:1

except that’s in a sense possible, already such done for some literal powers, and you could define 2π that way, and 1π just in case someone would type that in, and I guess -π, -2π etc.

A couple years ago I suggested something similar, namely singleton types for the concepts of additive and multiplicative identities. It didn’t gain any traction but I still think it would be a good idea when suitable concrete types aren’t known or don’t exist.

1 Like

It’s used also for things like initializing arrays.

2 Likes

InitialValues.jl takes only the function, not the operand type. eg

julia> InitialValue(*) * 3
3
2 Likes

You can see something similar with the unit imaginary im. It’s defined as Complex(false, true), and we have

julia> zero(im)
Complex(false,false)

julia> one(im)
Complex(true,false)

This works nicely because Complex is a parametric type, so if you multiply im by some number of your favorite type T, you’ll get a Complex{T} because Bool multiplied by T generally results in T:

julia> typeof(2im)
Complex{Int64}

julia> typeof(2.0im)
ComplexF64 (alias for Complex{Float64})

julia> typeof(BigFloat(2)im)
Complex{BigFloat}

I’ve never encountered any loss of precision or unexpected type problems with Complex.

The behavior of Irrational, on the other hand, is not so nice. Specifically,

julia> typeof(2π)
Float64

This is especially troublesome when you have a fancy mathematical formula that you want to work with another number type. Suppose we define

function f(x)
    2π * x
end

Well, f(BigFloat(2)) returns a BigFloat, as expected. But the accuracy of this result is only about 5e-16 — because is a Float64.

julia> f(BigFloat(2)) ≈ 4*BigFloat(π)
false

julia> f(BigFloat(2)) - 4*BigFloat(π)
-4.898587[...]e-16

This is very sneaky and very annoying if you’re trying to work with different number types.

What I end up doing is first converting π to the type of x:

function g(x)
    let π=oftype(x, π)
        2π * x
    end
end

With this modification, we have exact agreement:

julia> g(BigFloat(2)) - 4*BigFloat(π)
0.0

This works with all the usual float types and now with Symbolics.jl. It doesn’t work with integer types; if needed, you could use something like let π = float(typeof(x))(π) instead. Again, this is annoying and inelegant, but at least it’s a workaround when you know that there’s something to work around.

6 Likes