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
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.
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
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.
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...
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.
It’s used also for things like initializing arrays.
InitialValues.jl takes only the function, not the operand type. eg
julia> InitialValue(*) * 3
3
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 2π
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.