-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
promote_op() fixes #16995
promote_op() fixes #16995
Conversation
@JeffBezanson I've found one case where the While more consistent, the latter has the drawback that julia> dim=2
2
julia> S=zeros(Complex,dim,dim)
2×2 Array{Complex,2}:
0+0im 0+0im
0+0im 0+0im
julia> T=zeros(Complex,dim,dim)
2×2 Array{Complex,2}:
0+0im 0+0im
0+0im 0+0im
julia> T[:] = 1
1
julia> z = 2.5 + 1.5im
2.5 + 1.5im
julia> S[1] = z
2.5 + 1.5im
julia> S*T
ERROR: InexactError()
in convert(::Type{Int64}, ::Float64) at ./int.jl:239
in matmul2x2!(::Array{Complex{Int64},2}, ::Char, ::Char, ::Array{Complex,2}, ::Array{Complex,2}) at ./linalg/matmul.jl:662
in generic_matmatmul!(::Array{Complex{Int64},2}, ::Char, ::Char, ::Array{Complex,2}, ::Array{Complex,2}) at ./linalg/matmul.jl:441
in *(::Array{Complex,2}, ::Array{Complex,2}) at ./linalg/matmul.jl:129
in eval(::Module, ::Any) at ./boot.jl:231
in macro expansion at ./REPL.jl:92 [inlined]
in (::Base.REPL.##1#2{Base.REPL.REPLBackend})() at ./event.jl:46 This problem has been discussed at #4796. OTOH, the same problem currently exists with reals: julia> Real[1 2] * Real[1.5; 2.]
ERROR: InexactError()
in convert(::Type{Int64}, ::Float64) at ./int.jl:239
in generic_matvecmul!(::Array{Int64,1}, ::Char, ::Array{Real,2}, ::Array{Real,1}) at ./linalg/matmul.jl:416
in *(::Array{Real,2}, ::Array{Real,1}) at ./linalg/matmul.jl:84
in eval(::Module, ::Any) at ./boot.jl:231
in macro expansion at ./REPL.jl:92 [inlined]
in (::Base.REPL.##1#2{Base.REPL.REPLBackend})() at ./event.jl:46 Instead of the current hack, one solution would be to change |
@@ -165,9 +165,9 @@ m = [1:2;]' | |||
@test @inferred([0,1.2].+reshape([0,-2],1,1,2)) == reshape([0 -2; 1.2 -0.8],2,1,2) | |||
rt = Base.return_types(.+, Tuple{Array{Float64, 3}, Array{Int, 1}}) | |||
@test length(rt) == 1 && rt[1] == Array{Float64, 3} | |||
rt = Base.return_types(broadcast, Tuple{Function, Array{Float64, 3}, Array{Int, 1}}) | |||
rt = Base.return_types(broadcast, Tuple{typeof(+), Array{Float64, 3}, Array{Int, 1}}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is another place where the return type is assumed to be equal to promote_type
on the inputs. It breaks with the promote_op
definition based on typeof(op(one(R), one(S)))
, since the exact function needs to be specified to call it.
I think I've found a better solution to the abstract types problem from my last comment: in |
7da178c
to
4e1ce13
Compare
@@ -10,6 +10,10 @@ promote_rule{s}(::Type{Irrational{s}}, ::Type{Float32}) = Float32 | |||
promote_rule{s,t}(::Type{Irrational{s}}, ::Type{Irrational{t}}) = Float64 | |||
promote_rule{s,T<:Number}(::Type{Irrational{s}}, ::Type{T}) = promote_type(Float64,T) | |||
|
|||
promote_op{S<:Irrational,T<:Irrational}(::Any, ::Type{S}, ::Type{T}) = Float64 | |||
promote_op{S<:Irrational,T<:Number}(::Any, ::Type{S}, ::Type{T}) = promote_type(S, T) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Irrational
looks like a valid case where promote_op
should rely on promote_type
: irrationals by definition cannot be represented correctly perfectly by any other type, and no other type is closed under operations with irrationals. Therefore, the most appropriate type for the result is the same for all operations, and is obtained using promotion rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the most appropriate type for the result is the same for all operations
Only for all operations of a particular, numerical variety. Not for things like converting to string, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course. One issue is that the scope of promote_op
isn't clearly defined: is "op" only for standard operators? For any operation? That's a general fallback anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true, but broadcast
uses it for everything. Since broadcast
doesn't know when promote_op
is the right thing to use, these fallbacks continue to get in the way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would broadcast
use if we removed the fallbacks? What do you think we should do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JeffBezanson commented on #16986 that we might want to have the fallbacks as Union{}
and use an algorithm similar to the one map uses by dispatching on Union{}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the fallbacks returned Union{}
or perhaps Any
, we could dispatch to a version of broadcast
that uses the map
algorithm in that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But which fallbacks? Should we keep the Numbers
methods? Or should we restrict them to the combination of numbers and known operators?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it ever makes sense to have a definition of promote_op
that ignores the function argument --- except one fallback that returns a sentinel value like Any
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I general I fully agree. But for the special case of numbers, the use of one
means that the function argument is taken into account.
Anyway, I only care to the extent that we need a way to get the return type to implement operators on Nullable
(which are the origin of this investigation).
@JeffBezanson Could you tell me which part of this PR (and of additional fixes at #17114) you would accept, and what we should fix? This is blocking operators support for |
I would prefer to remove the definitions that ignore |
I think that in #17114 I managed to leave only |
I don't think we can get rid of the method for numbers which is based on I've just picked further changes by @pabloferz, let's see the result. |
for f in (ceil, floor, round, trunc) | ||
@eval promote_op{Op<:$(typeof(f)),T}(::Op, ::Type{T}, ::Any) = T | ||
@eval promote_op{T}(::typeof($f), ::Type{T}, ::Any) = T |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's going to be a lot of ambiguities with these changes that are going to be too cumbersome to solve otherwise. That's why I had this, but maybe there's another (better) way to handle those.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? It shouldn't make any difference from the previous syntax.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the same reason you added versions for <
, >
, ==
, etc. parametrised with Number
. You'll need versions parametrised with Number
and Nullable
for convert
, ceil
, parse
, etc.
And the definitions I had would make a difference since you'd have methods with a parameter for the first argument that are more specific than the definitions for Number
and Nullable
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you actually tested it? In my tests, the Op<:
form has exactly the same ambiguities.
@JeffBezanson Does that mean we don't need the methods for |
The promote_op method calling promote_type could be restored for |
I don't really like that solution either, since that rule isn't correct in many cases: So, as regards the |
319dfd7
to
ac429cb
Compare
# preserve the most general (abstract) type when possible | ||
return isleaftype(T) ? S : typejoin(S, T) | ||
end | ||
function promote_op{R<:Number,S<:Number}(op, ::Type{R}, ::Type{S}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can write
x = R <: Irrational ? 1.0 : one(R)
y = S <: Irrational ? 1.0 : one(S)
T = typeof(op(x, y))
and remove the ones for Irrational
above? (and restore the comparison ones)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I thought about that possibility too. In general though, I think it's considered more Julian to dispatch on types.
That solution would be quite appealing to avoid ambiguities with the methods for comparison operators, but we still need to take care of ambiguities with the Nullable
method. So I'd rather get rid of the comparison operators method altogether. That fits with @JeffBezanson's request. (But currently it fails the tests, not sure why yet.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I now dispatching in types is more Julian. The problem here, if I understand Jeff right, is that any method that ignores op, that is promote_op(op, ...)
should preferably return Any
, and that we should have as fewer such methods as possible (but also the as fewer as possible of the ones that take op into account). But maybe we should still follow the Julian way everywhere.
The methods that have Number
types as arguments (other than op) are a special case since we use them in other places besides broadcast. The methods with Nullable
types are probably also important on its own.
The comparison methods are failing because there is no I'm not sure why the comparison methods can't be inferred with this definition, that's why I had the comparison ones.isless
for Complex
and other numerical types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why the comparison methods can't be inferred with this definition, that's why I had the comparison ones.
I suspect there's a type-instability somewhere. These tests already caught one in a corner case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found a minimum reproducer: Base.Test.@inferred <(one(Rational{Int32}), one(Float64))
(only happens on 32-bit)
@@ -10,6 +10,13 @@ promote_rule{s}(::Type{Irrational{s}}, ::Type{Float32}) = Float32 | |||
promote_rule{s,t}(::Type{Irrational{s}}, ::Type{Irrational{t}}) = Float64 | |||
promote_rule{s,T<:Number}(::Type{Irrational{s}}, ::Type{T}) = promote_type(Float64,T) | |||
|
|||
promote_op{S<:Irrational,T<:Irrational}(::Any, ::Type{S}, ::Type{T}) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is missing op as first argument
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Will fix, but the CI hang doesn't seem related...
69a31e0
to
7d70ae1
Compare
@@ -198,7 +198,7 @@ end | |||
function ndigits0z(n::Unsigned, b::Int) | |||
d = 0 | |||
if b < 0 | |||
d = ndigits0znb(signed(n), b) | |||
d = ndigits0znb(n, b) | |||
else |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems that CI has been hanging all along because of this (see https://github.com/JuliaLang/julia/blob/master/test/intfuncs.jl#L78)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much for catching it! How did you found out that was the culprit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked in test/choosetests.jl
to see if there was some of the tests missing in the log.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had tried that too, but I guess it didn't do it systematically enough. Adding a timeout on individual tests would indeed make this much clearer...
35cf7de
to
7a0d383
Compare
The new tests cover it already, that's why they were failing. |
AV failure seems to be #16091 |
Yes, I've restarted it hoping we'll be more lucky this time. Good news: the Travis CI build passed! Is there anything left to change? I'd be in favor of merging if it's globally OK, and possibly tweak details in subsequent PRs. |
Thanks for persisting on this! Could you squash some of the commits? In particular would be good to keep the ones that fix general issues separate, and squash together the promote_op changes. |
@@ -198,7 +198,7 @@ end | |||
function ndigits0z(n::Unsigned, b::Int) | |||
d = 0 | |||
if b < 0 | |||
d = ndigits0znb(n, b) | |||
d = ndigits0znb(signed(n), b) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this still right if it wraps to negative?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change actually cancels the one I incorrectly made in a previous commit. So after squashing nothing changes here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess consider that a question w.r.t. master then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so, since after removing signed
the unsigned wrapping triggered a failure. So I guess this means with signed
, the wrapping is correct (since the test works). But you'll have to find somebody who knows this code to get an actual answer. :-)
Need at least 64-bit to store the result of Float64 decomposition. (A very good use case for return type declarations.)
These only affected particular type combinations, with either type instabilities which affected inference of return type or plain failures. Tests are included in the next commit.
Use common promote_op() based on one() for all Number types: this fixes promote_op(+, ::Bool), which returned Int, and promote_op(==, ::Complex, ::Complex), which returned Complex{Bool}. Add fallback definitions returning Any so that broadcast() computes the appropriate return type, instead of using promote_type() which is often wrong. This fixes broadcast() when the return type is different from the input type. Add systematic tests for operators and their return types.
Squashed and rebased, tests still pass! |
promote_op{T}(::Type{T}, ::Any) = (@_pure_meta; T) | ||
promote_op{R,S}(::Any, ::Type{R}, ::Type{S}) = (@_pure_meta; promote_type(R, S)) | ||
promote_op(op, T, S, U, V...) = (@_pure_meta; promote_op(op, T, promote_op(op, S, U, V...))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed this method when squashing since it doesn't make sense to use the output type as an input type now that the fallback to promote_type
has been removed. I've replaced it with promote_op(::Any, ::Any, ::Any...)
above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense 👍. Isn't this just missing NVM I didn't see the splatting above.promote_op(::Any, ::Any) = Any
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, because ::Any...
can also match no argument. This is an annoying detail with current varargs.
@@ -461,7 +461,7 @@ function ^{T<:AbstractFloat}(z::Complex{T}, p::Complex{T}) | |||
if p==2 #square | |||
zr, zi = reim(z) | |||
x = (zr-zi)*(zr+zi) | |||
y = 2zr*zi | |||
y = T(2)*zr*zi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this necessary? It seems like this will be slower e.g. for Complex{BigFloat}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of the type instability it creates for Float16
:
julia> typeof(2*Float16(1.0))
Float32
Maybe better write it as T(2*zr*zi)
?
(I wonder whether that promotion exception for Float16
is really a good idea...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think T(2*zr*zi)
seems like a better way to do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #17259.
I've added systematic tests for
promote_op
and operators on numbers, and they uncovered a few corner cases which either didn't work at all, were type-unstable, or for whichpromote_op
did not match the actual type. See commit messages for details.The first commit comes from #16996 and is needed for all tests to pass, so I moved it here.
Cf. #16988. Fixes #4883.