Open ChrisRackauckas opened 3 years ago
This sounds great!
The one thing to really think about though is ambiguity handling...
Could you elaborate on this a little bit?
Doing some stuff like mul!(::IsAdjoint,..)
would cause a lot of dispatch ambiguities if in Array{Float64,1}{Adjoint}
the Adjoint
has equal precedence to the other types. In this sense, I almost thing that a separate rule needs to be applied, like "tags supersede type parameters" in order to break ambiguities in a nice way. One of the type gods could probably point to some literature on an idea like this with its trade-offs.
Just to copy my thoughts so they don't get lost in the Slack-hole:
I feel like the idea to have "something that extends the idea of type parameters" a-la Array{Float64,1}{Adjoint}
is just re-discovering concrete inheritance.
That is, Array{Float64,1}{Adjoint}
are types that are distinct from Array{Float64,1}
, but have the same memory layout and will be accepted by all the same methods. This is essentially what people are talking about when they discuss inheriting from concrete types.
My understanding is that concrete inheritance is looked at with a lot of skepticism and scorn but it does feel like our abstract array ecosystem is hitting the limits of simple type composition and it may be time to revisit concrete inheritance and see if we can do it sanely.
Instead of concrete inheritance, is it possible to accomplish this with only abstract multiple inheritance (a la #5)?
E.g. for the tracker example, something like this:
abstract type AbstractArray{T, N}
end
abstract type AbstractAdjoint{A, T, N} <: AbstractArray{T, N}
end
struct Adjoint{A, T, N} <: AbstractAdjoint{A, T, N}
data::A
end
abstract type AbstractTrackedArray{B, T, N} <: AbstractArray{T, N}
end
struct TrackedArray{B, T, N} <: AbstractTrackedArray{B, T, N}
data::B
sensitivities
end
struct TrackedAdjointArray{B, T, N} <: AbstractTrackedArray{B, T, N}, AbstractAdjoint{B, T, N} # requires abstract multiple inheritance
data::B
sensitivities
end
We have a method adjoint
with the following signature:
adjoint(x::A) where A <: AbstractArray{T, N} -> AbstractAdjoint{A, T, N}
And we have two methods for track
with the following signatures:
track(x::B) where B <: AbstractArray{T, N} -> TrackedArray{B, T, N}
track(x::B) where B <: AbstractAdjoint{A, T, N} -> TrackedArray{B, T, N}
That will hit this issue that we want something to be Tracked
, Adjoint
, and Symmetric
. I think the tagging needs to be "from the outside" instead of being defined at type definition time.
Yeah I see what you mean. It's not sustainable to have different structs struct TrackedAdjointSymmetricArray
for each possible combination.
Also we don't currently have abstract multiple inheritance, so that would be another barrier.
I agree that the tag approach is probably more sustainable.
They extend the way things dispatch, but only do so at compile time for a given type. isadjoint(::Array) can't be a thing, unless you have a type parameter for it.
Why is this? Trait doesn't have to happe all at compile time, much like dispatch doesn't have to happen at compile time. It's generally even just a way to compute a property from an object for dispatch. Why can't isadjoint(::Array)
be a thing?
Is this tag supposed to be per object or per type. When and how is it attached? If it's attached to the type then it seems to be exactly what traits are fore. If it's attached to the object, then this is essentially something that is used for dispatch that can be changed at runtime so it cannot be statically inferred. I don't think that's desireable effect.
If I understand correctly, the information is attached to instances, not types.
julia> f(x::IsSymmetric) = 1
julia> f(x::IsNotSymmetric) = 2
julia> x = [3.0 4.0; 4.0 3.0]
julia> f(x)
2
julia> tag!(x, IsSymmetric)
julia> f(x)
1
Why can't
isadjoint(::Array)
be a thing?
To do this at compile time, you need a type parameter. E.g. a Boolean type parameter Adjointness
:
struct Array{T, N, Adjointness}
data
end
Then we define:
isadjoint(x::Array{T, N, Adjointness} = Adjointness
To do it at run time, we need a field in the Array
struct:
struct Array{T, N} <: AbstractArray{T, N}
data
adjointness::Bool
Then we define:
isadjoint(x::Array{T, N} = x.adjointness
In either case, we need to decide ahead of time which properties (adjointness, symmetricness, etc) we will support, and we have to hardcode those as either type parameters or fields.
As Chris says:
So, we could in theory do Array{T,1,Adjoint}, but now you can see the issue: it's an extendability problem since these tags need to all be pre-specified in the type definition!
That will hit this issue that we want something to be
Tracked
,Adjoint
, andSymmetric
. I think the tagging needs to be "from the outside" instead of being defined at type definition time.
@ChrisRackauckas This would also mean that concrete inheritance would also not solve this issue, right?
If I understand correctly, the information is attached to instances, not types.
If that is the case, then essentially f(x)
will almost never be statically dispatched. It'll be really hard for type inference to know if the type tag is changed. Is this the desired effect? This is roughly equivalent to putting values into type domain, which is a standard form of dispatch abuse for exactly this reason.
Still, it's unclear why this should be per-object.
To do this at compile time, you need a type parameter. E.g. a Boolean type parameter Adjointness:
Hmmm, no? What you need, if this property is not defined/determined by yourself, is a way to delegate this check to the part that should determine this, i.e. TrackedArray{A}
should implement an isadjoint
to call that on A
instead. If there's nothing else to delegate to, a explicit implementation should be provided or the fallback is used and there's no way around this, no matter what you do.
If the complaint is that TrackedArray
has to implement isadjoint
and unknown number of other properties and you would rather like a way to transfer all properties to the wrapped object without enumerating over all of them, well,
isadjoint
as sth like trait(..., ::IsAdjoint)
and simply implement trait(::TrackedArray, ::Any) = trait(<delegate to the wrapped array>)
Or if you replace the trait
function I have above with tag
, you already get an explicit version of what this is doing without the tag mutation part of the API that kills inference. In another word, instead of doing f(a)
you do f(a, tag(a, IsAdjoint()))
and that's exactly how all traits works with various different spelling. The tag
function can rely on the type or rely on the object which may affect how inference friendly it is but at least it is not guaranteed to kill inference.... And note that even though IsAdjoint()
is explicitly used here, this is on the consumer of the information which has to know about it and will not be dealing with unknown number of traits.
And such a system is already there. Improving it/them and adopting a standard one are certainly things to be worked on. @vtjnash had even made a comment of replacing most complex dispatch with it. Changing the syntax so that this is done automatically without additional argument necessary can potentially be on the table once there's a clear winner/one that's widely adopted.
Another way to look at this is that methods are already a way to attach compiler friendly info to arbitrary object/types (i.e. methods are mappings from object/type to an output). They also already allow very flexible manipulate/inheritance which seems to cover the need here. Attaching other generic metadata to object could have other use and it is basically what WeakKeyDict
are for, but I highly doubt it's the right solution here.
If I understand correctly, the information is attached to instances, not types.
No, I don't think that's correct @DilumAluthge. We want the information to be a part of the type. Basically, Chris is asking for an un-ordered set of extra parameters we can stick on the end of types to attach additional meaning.
So, we could in theory do
Array{T,1,Adjoint}
, but now you can see the issue: it's an extendability problem since these tags need to all be pre-specified in the type definition! What if new packages add new tag verbs? SOL, redefine/pirate the type if you want to use it! This is why people use wrapper types: you can always extend, but of course you get the dispatch problem.
@ChrisRackauckas Can't we do something like
using LinearAlgebra: Adjoint
struct MyArray{T, N, Tag <: Tuple}
data::Array{T, N}
end
MyArray(x::Array{T, N}) where {T,N} = MyArray{T, N, Tuple{}}(x)
function Base.adjoint(x::MyArray{T, N, Tag}) where {T, N, Tag}
AdjointTag = Adjoint in Tag.parameters ? Tuple{(T for T in Tag.parameters if T != Adjoint)...} : Tuple{Adjoint, Tag.parameters...}
MyArray{T, N, AdjointTag}(x.data)
end
now we have
julia> MyArray([1,2,3])
MyArray{Int64,1,Tuple{}}([1, 2, 3])
julia> MyArray([1,2,3])'
MyArray{Int64,1,Tuple{Adjoint}}([1, 2, 3])
julia> (MyArray([1,2,3])')'
MyArray{Int64,1,Tuple{}}([1, 2, 3])
In principal, other packages should be able to add their own tags as they please without any baking in ahead of time. Of course, the biggest downside to this approach that I see would be that basically all Tag
functions would need to be @generated
for performance which is a total nightmare.
However, if we had something that was analogous to Tuple
which could have a variable number of parameters and was un-ordered, then perhaps we could make it work.
That is, you would need a type Tag
such that
julia> Tag{Int, Float64} === Tag{Float64, Int}
true
and you would need to be able to write methods like
f(x::Array{T, N, Tag{Adjoint}}) = ...
and have it apply to a x::Array{T, N, Tag{Sorted, Adjoint}}
(and thus causing a new host of ambiguities)
If I understand correctly, the information is attached to instances, not types.
No, I don't think that's correct @DilumAluthge. We want the information to be a part of the type. Basically, Chris is asking for an un-ordered set of extra parameters we can stick on the end of types to attach additional meaning.
Ah I see!
Could we make it so that you can stick Tag
on the end of any type, without the definition of the type needing to include tags?
E.g. suppose I have some structs
struct Foo
end
struct Bar{A, B}
end
Can we make it so I can stick tags on these types, even though they weren't written with tags in mind?
E.g. I'd want to be able to write methods like this:
f(x::Foo{Tag{Adjoint}}) = ...
g(x::Bar{A, B, Tag{Adjoint}}) = ...
Even though the definitions of Foo
and Bar
don't mention tags.
@MasonProtter that would be an interesting implementation for this, yes. And indeed the big deal here would be some kind of rule or machinery to reduce the ambiguities. I think you do have to give the tags precedence, which would make it be like how the wrapper type dispatches always take control, unless there's no definition and then it would get the dispatch of the non-tagged version.
@ChrisRackauckas This would also mean that concrete inheritance would also not solve this issue, right?
Yes, it's the expression problem on inheritance in some sense.
Could we make it so that you can stick Tag on the end of any type, without the definition of the type needing to include tags?
Yup, that's precisely what I am proposing with the Array{T,N}{Adjoint}
.
Still, it's unclear why this should be per-object.
It's the same thing as Adjoint{Array{T,N}}
: not every Array
is the Adjoint
of an Array
. The type has to change when you do such an operation, since otherwise you can't change the dispatches. Traits are functions on the type information, but if you can't distinguish the types, you can't distinguish the trait outcome. Tagging can be inferred in any case you can infer the wrapper type: it's basically just a way to get Array{T,N,Adjoint}
instead of Adjoint{Array{T,N}}
, or as @MasonProtter showcases, Array{T,N,Tag{Adjoint}}
.
Just as a side-note on syntax, Array{T, N}{Adjoint}
won't work at least in 1.0 because we curry type parameters. T{U}{V}
is the same as T{U, V}
:
julia> Array{Int}{2}
Array{Int64,2}
@MasonProtter in your working example above, can you post what the @generated
version of your Base.adjoint
function would look like?
Yeah, I'm not wedded to the syntax at all: I just needed a syntax to express the idea that it's not really a type parameter because I want to be able to do this even if the user didn't give me a type parameter for this piece of information (though you show a way such that opting in could be one parameter for all tags at least).
And indeed the big deal here would be some kind of rule or machinery to reduce the ambiguities. I think you do have to give the tags precedence, which would make it be like how the wrapper type dispatches always take control, unless there's no definition and then it would get the dispatch of the non-tagged version.
Hm, actually thinking about this again, I assumed you wanted the tags to be unordered as that would be more expressive, but now I realize that unordered tags would also cause far more dispatch ambiguity problems. Consider
f(::Vector{Int, Tag{Sorted}}) = 1
f(::Vector{Int, Tag{Adjoint}}) = 2
If we make it so that Tag{Sorted, Adjoint} === Tag{Adjoint, Sorted}
, what does this return?
f(Vector{Int, Tag{Sorted,Adjoint}}([1, 2, 3]))
This has all the ambiguities of extra type params PLUS all the extra ambiguities of multiple inheritance (because this would basically be a route to multiple inheritance).
Meanwhile, ordered tags (at least with them having higher precedence than other type params) wouldn't cause a new class of ambiguities.
If tags are ordered, then are you envisioning that
f(Vector{Int, Tag{Sorted, Adjoint}}([1, 2, 3]))
Would dispatch to the
f(::Vector{Int, Tag{Sorted}}) = 1
method, because Sorted
appears earlier in the tag list and thus has higher precedence?
Now, if we have
f(::Vector{Int, Tag{Sorted}}) = 1
f(::Vector{Int, Tag{Adjoint}}) = 2
f(::Vector{Int, Tag{Sorted, Adjoint}}) = 3
Then
f(Vector{Int, Tag{Sorted, Adjoint}}([1, 2, 3]))
Would instead dispatch to
f(::Vector{Int, Tag{Sorted, Adjoint}}) = 3
Right?
But then what does this call dispatch to?
f(Vector{Int, Tag{Adjoint, Sorted}}([1, 2, 3]))
Consider
f(::Vector{Int, Tag{Sorted}}) = 1 f(::Vector{Int, Tag{Adjoint}}) = 2
If we make it so that
Tag{Sorted, Adjoint} === Tag{Adjoint, Sorted}
, what does this return?
What if we throw an error when you try to define this second method f(::Vector{Int, Tag{Adjoint}}) = 2
?
Something like this:
julia> f(::Vector{Int, Tag{Sorted}}) = 1
Generic function `f` with 1 method(s)
julia> f(::Vector{Int, Tag{Adjoint}}) = 2
ERROR: Because the method `f(::Vector{Int, Tag{Sorted}})` has already been defined, you are not allowed to define the method `f(::Vector{Int, Tag{Adjoint}})` unless you first define the method `f(::Vector{Int, Tag{Sorted, Adjoint}})`. Please note that `Tag{Sorted, Adjoint} === Tag{Adjoint, Sorted}`.
julia> f(::Vector{Int, Tag{Sorted, Adjoint}}) = 3
Generic function `f` with 2 method(s)
julia> f(::Vector{Int, Tag{Adjoint}}) = 2
Generic function `f` with 3 method(s)
To me, this seems better than making tags ordered.
That can't work because then if two different packages separately defined tags, they could break eachother.
That can't work because then if two different packages separately defined tags, they could break eachother.
Good point.
Wait.... only one package can own the generic function f
, right? So the other package is committing type piracy.
(I don't think that adding a tag to a type makes it "your type", does it?)
@MasonProtter in your working example above, can you post what the
@generated
version of yourBase.adjoint
function would look like?
Basically the same:
@generated function Base.adjoint(x::MyArray{T, N, Tag}) where {T, N, Tag}
AdjointTag = Adjoint in Tag.parameters ? Tuple{(T for T in Tag.parameters if T != Adjoint)...} : Tuple{Adjoint, Tag.parameters...}
:(MyArray{T, N, $AdjointTag}(x.data))
end
it's just bad to have a proliferation of generated functions like this.
E.g. if my package does not own the type Foo{A, B}
, then it also doesn't own the type Foo{A, B, Tag{MyTag}}
, even if the tag MyTag
is owned by my package.
So in this case, at least one of the two packages in question is committing type piracy.
Unless we are saying that Foo{A, B, Tag{MyTag}}
is now "my type". E.g. is it type piracy if I define this method in my package:
julia> Base.length(x::Array{T, N, Tag{MyTag}}) = 1
Wait.... only one package can own the generic function
f
, right? So the other package is committing type piracy.(I don't think that adding a tag to a type makes it "your type", does it?)
If that's piracy, then there's no point in doing this. It would make it impossible for a package to safely define their own tags and overload existing functions, in which case why care about any of this?
Think of the tag as like a type parameter. If I own MyType
, then I also own Array{MyType}
.
Wait.... only one package can own the generic function
f
, right? So the other package is committing type piracy. (I don't think that adding a tag to a type makes it "your type", does it?)If that's piracy, then there's no point in doing this. It would make it impossible for a package to safely define their own tags and overload existing functions. Think of the tag as like a type parameter. If I own
MyType
, then I also ownArray{MyType}
.
I see. That makes things harder... we can't throw an error because that would allow packages to break each other.
But having sorted tags seems weird... you could have f(::Foo{Tag{Sorted, Adjoint}})
and f(::Foo{Tag{Adjoint, Sorted}})
be completely different methods.
But having sorted tags seems weird... you could have
f(::Foo{Tag{Sorted, Adjoint}})
andf(::Foo{Tag{Adjoint, Sorted}})
be completely different methods.
it is weird and unfortunate, but it's also at least not worse than the current situation with wrappers where Sorted{Adjoint{Vector}}
and Adjoint{Sorted{Vector}}
are also completely different types.
But having sorted tags seems weird... you could have
f(::Foo{Tag{Sorted, Adjoint}})
andf(::Foo{Tag{Adjoint, Sorted}})
be completely different methods.
Although, maybe that's not a big deal.
What if we have a convenience macro:
@tag function f(x::Vector{Int, Tag{A, B, C}})
...
end
And in this case, that macro would autogenerate all of the following methods, with the same body:
f(x::Vector{Int, Tag{A, B, C}})
f(x::Vector{Int, Tag{A, C, B}})
f(x::Vector{Int, Tag{B, A, C}})
f(x::Vector{Int, Tag{B, C, A}})
f(x::Vector{Int, Tag{C, A, B}})
f(x::Vector{Int, Tag{C, B, A}})
It's sounding like this is all doable in a third party package at least as a proof-of-concept. TaggedArrays.jl
or something.
The main thing missing from the prototype that you've put together in this thread is what the dispatch function actually looks like. E.g. what is the code for
f(::Vector{Int, Tag{Sorted}}) = 1
IIUC, you can't just do this
f(::Vector{Int, Tuple{Sorted}}) = 1
Because this will accept Vector{Int, Tuple{Sorted}}
but not Vector{Int, Tuple{Sorted, Adjoint}}
.
If you can figure that out, I definitely think you have everything you need for a TypeTaggedArrays.jl
package.
i really need the approach of a sorted type parameter, that works like a set on the type system T[a,b,c} -> T{Set((a,b,c)}
i basically had a crude implementation of this on a package using:
f(::Vector{Int, Tag{Sorted}}) = 1 f(::Vector{Int, Tag{Adjoint}}) = 2`
and a generated function: https://github.com/longemen3000/ThermoState.jl/blob/master/src/state_type.jl
where
T{a,b,c} = T{c,b,a}
If I am understanding correctly, what Mason is suggesting is that Tag{a,b,c}
would NOT be equal to Tag{c,b,a}
.
Now I feel silly. "If only there was an unordered type with a variable number of parameters that supports things like subtyping!"
It's a union. We want a union. Unions even have the added benefit of when they get bigger, they get less specific.
using LinearAlgebra: LinearAlgebra, Adjoint, Symmetric
struct TaggedArray{T, N, Tag}
data::Array{T, N}
end
TaggedArray(x::Array{T, N}) where {T,N} = TaggedArray{T, N, Union{}}(x)
Base.adjoint(x:: TaggedArray{T, N, Tag}) where {T, N, Tag} = TaggedArray{T, N, Union{Adjoint, Tag}}(x.data)
# Base.adjoint(x:: TaggedArray{T, N, Tag}) where {T, N, Tag, Adjoint <: Tag} = # get rid of the adjoint
LinearAlgebra.Symmetric(x::TaggedArray{T, 2, Tag}) where {T, Tag} = TaggedArray{T, 2, Union{Tag, Symmetric}}(x.data)
f(x::TaggedArray{T, 2, Tag}) where {T, Tag, Adjoint <: Tag} = 1
f(x::TaggedArray{T, 2, Tag}) where {T, Tag, Symmetric <: Tag} = 2
f(x::TaggedArray{T, 2, Tag}) where {T, Tag, Union{Adjoint, Symmetric} <: Tag} = 3
Unfortunately, the final line there does not work currently, giving
julia> f(x::TaggedArray{T, 2, Tag}) where {T, Tag, Union{Adjoint, Symmetric} <: Tag} = 2
ERROR: syntax: invalid type parameter name "Union{Adjoint, Symmetric}" around REPL[8]:1
Stacktrace:
[1] top-level scope at REPL[8]:1
but we can work around it:
const AdjointSymmetric = Union{Adjoint, Symmetric}
f(x::TaggedArray{T, 2, Tag}) where {T, Tag, AdjointSymmetric <: Tag} = 3
For completeness, you need an additional using LinearAlgebra
at the top of your code.
# Base.adjoint(x:: TaggedArray{T, N, Tag}) where {T, N, Tag, Adjoint <: Tag} = # get rid of the adjoint
I think there's a "union subtraction" function somewhere in Base.
@MasonProtter Instead of this:
Base.adjoint(x:: TaggedArray{T, N, Tag}) where {T, N, Tag} = TaggedArray{T, N, Union{Adjoint, Tag}}(x)
Did you mean this?
Base.adjoint(x:: TaggedArray{T, N, Tag}) where {T, N, Tag} = TaggedArray{T, N, Union{Adjoint, Tag}}(x.data)
Turns out my idea doesn't work:
julia> f(TaggedArray(rand(2,2))')
1
julia> f(TaggedArray(rand(2,2)) |> Symmetric)
1
π
It's because I need to do the where Tag
part first and that kills it I guess.
This works:
using LinearAlgebra: LinearAlgebra, Adjoint, Symmetric
struct TaggedArray{T, N, Tag}
data::Array{T, N}
end
TaggedArray(x::Array{T, N}) where {T,N} = TaggedArray{T, N, Union{}}(x)
Base.adjoint(x:: TaggedArray{T, N, Tag}) where {T, N, Tag} = TaggedArray{T, N, Union{Adjoint, Tag}}(x.data)
# Base.adjoint(x:: TaggedArray{T, N, Tag}) where {T, N, Tag, Adjoint <: Tag} = # get rid of the adjoint
LinearAlgebra.Symmetric(x::TaggedArray{T, 2, Tag}) where {T, Tag} = TaggedArray{T, 2, Union{Tag, Symmetric}}(x.data)
f(x::TaggedArray{T, 2, Adjoint}) where {T} = 1
f(x::TaggedArray{T, 2, Union{Adjoint, Other}}) where {T, Other} = 1
f(x::TaggedArray{T, 2, Symmetric}) where {T} = 2
f(x::TaggedArray{T, 2, Union{Symmetric, Other}}) where {T, Other} = 2
f(x::TaggedArray{T, 2, Union{Symmetric, Adjoint}}) where {T} = 3
f(x::TaggedArray{T, 2, Union{Symmetric, Adjoint, Other}}) where {T, Other} = 3
At the repl:
julia> f(TaggedArray(rand(2,2))')
1
julia> f(TaggedArray(rand(2,2)) |> Symmetric)
2
julia> f(TaggedArray(rand(2,2))' |> Symmetric)
3
julia> f(TaggedArray(rand(2,2)) |> Symmetric |> adjoint)
3
π
If you only define these methods for f
:
f(x::TaggedArray{T, 2, Adjoint}) where {T} = 1
f(x::TaggedArray{T, 2, Union{Adjoint, Other}}) where {T, Other} = 1
f(x::TaggedArray{T, 2, Symmetric}) where {T} = 2
f(x::TaggedArray{T, 2, Union{Symmetric, Other}}) where {T, Other} = 2
Then you get this:
julia> f(TaggedArray(rand(2,2))')
1
julia> f(TaggedArray(rand(2,2)) |> Symmetric)
2
julia> f(TaggedArray(rand(2,2))' |> Symmetric)
ERROR: MethodError: no method matching f(::TaggedArray{Float64, 2, Union{Adjoint, Symmetric}})
Closest candidates are:
f(::TaggedArray{T, 2, Union{Symmetric, Other}}) where {T, Other} at REPL[13]:1
f(::TaggedArray{T, 2, Union{Adjoint, Other}}) where {T, Other} at REPL[11]:1
f(::TaggedArray{T, 2, Adjoint}) where T at REPL[10]:1
...
Stacktrace:
[1] top-level scope
@ REPL[16]:1
julia> f(TaggedArray(rand(2,2)) |> Symmetric |> adjoint)
ERROR: MethodError: no method matching f(::TaggedArray{Float64, 2, Union{Adjoint, Symmetric}})
Closest candidates are:
f(::TaggedArray{T, 2, Union{Symmetric, Other}}) where {T, Other} at REPL[13]:1
f(::TaggedArray{T, 2, Union{Adjoint, Other}}) where {T, Other} at REPL[11]:1
f(::TaggedArray{T, 2, Adjoint}) where T at REPL[10]:1
...
Stacktrace:
[1] top-level scope
@ REPL[17]:1
Is this the correct behavior?
Nope, that's unintentional. I wonder why the Other
trick isn't working π
This is with defining only the first four methods for f
. The Other
trick works for Foo
but not for Symmetric
:
julia> struct Foo end
julia> a = TaggedArray{Float64, 2, Union{Adjoint, Foo}}([1.0 2.0; 3.0 4.0])
TaggedArray{Float64, 2, Union{Foo, Adjoint}}([1.0 2.0; 3.0 4.0])
julia> f(a)
1
julia> b = TaggedArray{Float64, 2, Union{Adjoint, Symmetric}}([1.0 2.0; 3.0 4.0])
TaggedArray{Float64, 2, Union{Adjoint, Symmetric}}([1.0 2.0; 3.0 4.0])
julia> f(b)
ERROR: MethodError: no method matching f(::TaggedArray{Float64, 2, Union{Adjoint, Symmetric}})
Closest candidates are:
f(::TaggedArray{T, 2, Union{Symmetric, Other}}) where {T, Other} at REPL[9]:1
f(::TaggedArray{T, 2, Union{Adjoint, Other}}) where {T, Other} at REPL[7]:1
f(::TaggedArray{T, 2, Adjoint}) where T at REPL[6]:1
...
Stacktrace:
[1] top-level scope
@ REPL[28]:1
Maybe it's a bug due to Symmetric
being a UnionAll
?
If I only define the first two methods of f
:
julia> struct Foo end
julia> a = TaggedArray{Float64, 2, Union{Adjoint, Foo}}([1.0 2.0; 3.0 4.0])
TaggedArray{Float64, 2, Union{Foo, Adjoint}}([1.0 2.0; 3.0 4.0])
julia> f(a)
1
julia> b = TaggedArray{Float64, 2, Union{Adjoint, Symmetric}}([1.0 2.0; 3.0 4.0])
TaggedArray{Float64, 2, Union{Adjoint, Symmetric}}([1.0 2.0; 3.0 4.0])
julia> f(b)
ERROR: MethodError: no method matching f(::TaggedArray{Float64, 2, Union{Adjoint, Symmetric}})
Closest candidates are:
f(::TaggedArray{T, 2, Union{Adjoint, Other}}) where {T, Other} at REPL[7]:1
f(::TaggedArray{T, 2, Adjoint}) where T at REPL[6]:1
Stacktrace:
[1] top-level scope
@ REPL[12]:1
MWE:
julia> g(::Ref{Union{Int, Other}}) where {Other} = 1
f (generic function with 1 method)
julia> g(Ref{Union{Int, Float64}}(1))
1
julia> g(Ref{Union{Int, Array}}(1))
ERROR: MethodError: no method matching f(::Base.RefValue{Union{Int64, Array}})
Closest candidates are:
f(::Ref{Union{Int64, Other}}) where Other at REPL[1]:1
Stacktrace:
[1] top-level scope at REPL[3]:1
Seems like a bug with UnionAll
to me for sure. I gotta go to bed, but I can open an issue in the morning if nobody beats me to it.
Wrapper types are a mess. The classic example is that
TrackedArray{Adjoint{Array}}
is no longer seen as anAdjoint
and Tracker.jl needed special overloads for handling adjoint arrays. Two deep you can start to special case: three deep is a nightmare. Why?The issue is that wrapper types aren't exactly new types, they are extended pieces of information. A lot of information stays the same, like
length
, but only some changes. In the parlance of Julia, this is the kind of behavior that you normally have with type parameters, whereArray{T,1}
shares dispatches withArray{T,2}
but then overrides what changes when you change dimensions. So, we could in theory doArray{T,1,Adjoint}
, but now you can see the issue: it's an extendability problem since these tags need to all be pre-specified in the type definition! What if new packages add new tag verbs? SOL, redefine/pirate the type if you want to use it! This is why people use wrapper types: you can always extend, but of course you get the dispatch problem.What about traits you say? They extend the way things dispatch, but only do so at compile time for a given type.
isadjoint(::Array)
can't be a thing, unless you have a type parameter for it.So you need some extra information. That's why I am proposing type tags. A type tag is an extension like
Array{Float64,1}{Adjoint}
, where the tag adds information to an existing type. It could be like,tag(A,IsAdjoint())
. By default, any tags would have the default behavior unless tagged, soArray{Float64,1}
would havetag(Adjoint) == DefaultTag()
. Then dispatches could be written on the tags, allowing for extension while keeping dispatches.The one thing to really think about though is ambiguity handling...