-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
Type Tags for Extendable Type Information #37790
Comments
This sounds great! |
Could you elaborate on this a little bit? |
Doing some stuff like |
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 That is, 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(x::A) where A <: AbstractArray{T, N} -> AbstractAdjoint{A, T, N} And we have two methods for 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 |
Yeah I see what you mean. It's not sustainable to have different structs 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. |
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 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 |
To do this at compile time, you need a type parameter. E.g. a Boolean type parameter 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 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:
|
@ChrisRackauckas This would also mean that concrete inheritance would also not solve this issue, right? |
If that is the case, then essentially Still, it's unclear why this should be per-object.
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. If the complaint is that
|
Or if you replace the 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 |
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.
@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 However, if we had something that was analogous to That is, you would need a type 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 |
Ah I see! Could we make it so that you can stick 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 |
@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.
Yes, it's the expression problem on inheritance in some sense.
Yup, that's precisely what I am proposing with the
It's the same thing as |
Just as a side-note on syntax, julia> Array{Int}{2}
Array{Int64,2} |
@MasonProtter in your working example above, can you post what the |
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). |
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 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 |
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])) |
What if we throw an error when you try to define this second method 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. |
Good point. |
Wait.... only one package can own the generic function (I don't think that adding a tag to a type makes it "your type", does it?) |
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 So in this case, at least one of the two packages in question is committing type piracy. Unless we are saying that julia> Base.length(x::Array{T, N, Tag{MyTag}}) = 1 |
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 |
So really #37793 is the only blocker, right? Want to create a package with this prototype? Then @ChrisRackauckas can stress-test it and see how it does in his various use cases. |
FWIW, #37793 seems likely to eventually be fixed by ensuring that |
Hmmm. That is unfortunate news for us, because IIUC Mason is using So @MasonProtter we may need to have the restriction that tags cannot be |
I mean that all types should behave like UnionAll currently does. Currently, it gets the answer wrong when presented with a concrete type. |
Hmmm. In that case, we can't use the |
Late to the party. I've certainly seen the problem reported in the OP with wrapper types, but I've sometimes wondered if we could get a lot of mileage by defining sometrait(::MyNewArray) = Awesome()
sometrait(A::AbstractAray) = iswrapper(A) ? sometrait(parent(A)) : Mundane() |
If you take a look at ArrayInterface this is done a lot with Fore example, known_length uses this strategy. |
Re: Tim’s idea, does it happen in practice that a wrapper type is a wrapper with respect to some traits but not others? |
Do you mean f(x::TaggedArray{T, 2, Tag}) where {T, Tag >: Union{Adjoint, Symmetric}} = 3 ? |
Good discussion. I see this as basically moving type parameters from being positional to being like keyword arguments, which is something I've thought about. That would make parameters more meaningful as well as more extensible. A lot of things to think about here. For example, |
Huh, interesting I had definitely tried the angry-face operator there and failed to make it work for me, but I probably just did something silly. Thanks for pointing that out. As I said earlier today on Zulip,
|
I think that this is a step in the right direction for traits in general. For example, an issue brought up on Zulip was that someone wanted to add the table interface (i.e. |
This is an interesting discussion and being able to attach extra tags to types seems like it might be quite useful, but I'm not convinced it solves the problem it's intended to here. What made me skeptical is the appearance of So I'm afraid for the problem of array wrappers, we replace the combinatorial explosion of wrapper combinations with the same explosion of tag combinations. Or can someone give a brief example, how, say, |
@martinholters: good points. Let me rephrase your comments to digest their consequences. You are essentially saying that if tags are commutative (i.e. their order doesn't matter, so we encode them with a If we instead want to keep tags general (so we can have both What we could perhaps attempt in relation to the problem in the OP is to allow defining equivalence classes of types in terms of commutation rules of wrappers (or tags), such as
Then, if we write a dispatch like |
Uhm, I guess there is another aspect in which tags as proposed in the OP are prefereable to wrappers: they don't require unwrapping to access the fields of a type. The interface of a tagged type would not change as a result of the added types. So the above stuff on commutation rules should actually be reformulated in terms of non-commutative tags instead of wrappers so that the same code of the |
But that only holds if the tag (née wrapper) does not interfere with how the internals are to be interpreted. So most algorithms would have to differentiate whether |
Yes, I agree this is a bad fit for |
@JeffBezanson I think at least for the purposes of labelling the fields of struct Foo
a::Vector{Int}
end can only store the un-tagged version. However, I wonder if in all other circumstances, we could get away with having it be 'abstract'? i.e. people can write struct Foo{V <: Vector{Int}}
a::V
end and then that would be compatible with |
One way to deal with the whole trait/multiple inheritance thing would just be to have a macro for appending to union types. Like if you have
A user could bestow a trait on a new thing
|
Question; is this proposal related to the tagged unions implemented in other programming languages, and in some Julia packages? More broadly, in the time since this was first proposed, has anyone written a possible ecosystem or package solution (not in base) that fixes this/provides this feature (like how there are packages fixing the lack of multiple inheritance, traits, etc. in Julia)? |
No. |
To clarify, I'm not asking because of the array--I'm asking because the original example in this thread, |
That addresses the problem of Adjoint, but not Tracked, Symmetric, and every other property someone would want to add. |
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...
The text was updated successfully, but these errors were encountered: