-
-
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
Add parenttype
method
#42923
base: master
Are you sure you want to change the base?
Add parenttype
method
#42923
Conversation
Can you describe how this would/should be used and why this is useful? I couldn't really understand this from a brief look at ArrayInterface. |
A simple example is @chriselrod and @ChrisRackauckas probably use it frequently too, in case they want to vouch for this. |
Type wrappers are hard to work with if you wish to understand the memory layout of an array. """
contiguous_axis(::Type{T}) -> StaticInt{N}
Returns the axis of an array of type `T` containing contiguous data.
If no axis is contiguous, it returns a `StaticInt{-1}`.
If unknown, it returns `nothing`.
"""
contiguous_axis(x) = contiguous_axis(typeof(x))
contiguous_axis(::Type{<:StrideIndex{N,R,C}}) where {N,R,C} = static(C)
contiguous_axis(::Type{<:StrideIndex{N,R,nothing}}) where {N,R} = nothing
function contiguous_axis(::Type{T}) where {T}
if parent_type(T) <: T
return nothing
else
return contiguous_axis(parent_type(T))
end
end
contiguous_axis(::Type{<:Array}) = One()
contiguous_axis(::Type{<:BitArray}) = One()
contiguous_axis(::Type{<:AbstractRange}) = One()
contiguous_axis(::Type{<:Tuple}) = One()
function contiguous_axis(::Type{T}) where {T<:VecAdjTrans}
c = contiguous_axis(parent_type(T))
if c === nothing
return nothing
elseif c === One()
return StaticInt{2}()
else
return -One()
end
end
function contiguous_axis(::Type{T}) where {T<:MatAdjTrans}
c = contiguous_axis(parent_type(T))
if c === nothing
return nothing
elseif isone(-c)
return c
else
return StaticInt(3) - c
end
end
function contiguous_axis(::Type{T}) where {T<:PermutedDimsArray}
c = contiguous_axis(parent_type(T))
if c === nothing
return nothing
elseif isone(-c)
return c
else
return from_parent_dims(T, c)
end
end Not shown is code for By using
That said, these wrapper types are annoying to deal with, so my array packages tend to canonicalize strided arrays to a |
@fredrikekre, I mirrored the documentation for |
I don't see how you can use How can you do anything with the return value from julia> parent(A)
3×3 Matrix{Float64}:
0.867555 0.833865 0.671841
0.78186 0.954977 0.16845
0.901881 0.855753 0.398882 Ok, thanks, so what does that tell me about |
Is this actually helpful? The That's not to say there isn't a pain point for wrapped arrays — there definitely is — but I really don't see I'd favor an approach like Chris suggests: canonicalizing wrapped arrays to a standard format (like strided or sparse or the like). |
In practice the previously mentioned device(::Type{T}) where {T<:Array} = CPUPointer()
function device(::Type{T}) where {T<:AbstractArray}
if parenttype(T) <: T
return UnkownDevice()
else
return device(parenttype(T))
end
end So we can still pull meaningful information out of an array's parent type without knowing the type of the most superficial layer.
Is the alternative proposal here to have something like struct WrappedArray{T,N,P,M} <: AbstractArray{T,N}
parent::P
metadata::M
end ...so that we don't need If we could replace all the current array implementations with something like Alternatively, if the issue is generalizability then perhaps |
The point is that we don't really define what You simply don't know how to interpret And no, I'm not suggesting that |
I've always thought that For example, composing a canonical StrideArray(x::DenseArray) = StrideArray(x, StrideIndex(x))
function StrideArray(x::AbstractArray)
p = parent(x)
x === p || error("Cannot convert $x to StrideArray")
# transform StrideArray by index transformations specific to x
transform_strides(StrideIndex(p), ArrayIndex(x))
end This probably isn't the most optimal way to do this, but it still demonstrates that having a standard way of referring to the parent memory is still useful if we don't know the type of |
If this julia> pointer(rand(3)') # works through a wrapper
Ptr{Float64} @0x00007f24e8dee4d0
julia> pointer(cu(rand(3))) # different type
CuPtr{Float32}(0x0000000cf6000400)
julia> pointer(1:3) # maybe this should be `nothing`
ERROR: conversion to pointer not defined for UnitRange{Int64} That works on an instance, not the type. Would it be too weird to just add methods something like these? Base.strides(::Type{<:Matrix}) = (1, missing)
Base.strides(::Type{<:AbstractMatrix}) = (missing, missing)
Base.strides(::Type{<:Adjoint{<:Any, P}}) where P<:AbstractMatrix = reverse(strides(P))
Base.pointer(::Type{T}) where T = Base._return_type(pointer, Tuple{T}) In some cases, you could probably use inference to get the value, if the wrapper's author defined the method for an instance. And if they did not define |
This is less a tool meant to be used generically, and more of a tool for peeling through wrappers. function contiguous_axis(::Type{T}) where {T<:MatAdjTrans}
c = contiguous_axis(parent_type(T))
if c === nothing
return nothing
elseif isone(-c)
return c
else
return StaticInt(3) - c
end
end Your example tells us that axis 2 of This means every new wrapper type has to implement methods indicating how they transform the properties of interest. For example, the The primary use case is to facilitate introspection, for those who want to support it. Anyone implementing |
There is nothing in the docs for the interface of struct FArray{T, N} <: AbstractArray{T, N}
f::Function
size::NTuple{N, Int}
end
Base.getindex(F::FArray{T, N}, I::Vararg{Int, N}) where {N, T} = F.f(I...)::T
Base.size(f::FArray) = f.size
f = FArray{Float64, 2}((x,y) -> sin(x)*cos(y), (2,3)) is a fine |
In the Julia package ecosytem, EDIT:
Yes, unfortunately there is no way around this. The benefit is that it makes wrappers more composable. It's a means around exponential explosion in number of possible type combinations from nested wrappers. |
Chris's example is a good case where the syntax itself provides more succinct code (although not strictly necessary to get the parent of |
But then what's gained by
If the fallback was |
But it's not really doing that in any meaningful way. In your example:
this could just as easily be: function contiguous_axis(::Type{MatAdjTrans{<:Any, P}}) where {P}
c = contiguous_axis(P)
# ... This is what I mean by needing to know about the wrapper in order to use the function. You already know the answer. It's not gaining you anything.
This is where I disagree. I think it's a trap. |
I don't understand how this is any different than what I said. In this case it is nice syntax when you don't want to write out all the other parameters of a type to get to the parent type. Not necessary but sometimes nice. I'm not sure how it's a trap. I gave an example where the parent buffer can safely be accessed if we also grab the index transformation. If you're going to use |
Thinking a bit more on the general strategies of |
Yeah, and depending on what we need to do with the parent data you can have another method inquiring about that. I admit that there's a limit to how useful blindly recursing through the layers of an array is. That doesn't mean we can't have other methods to guide us while traversing nested arrays. |
Yes, exactly! That's the problem. It's that you can't just implement That's the trap. It feels like a solution, but there's this other part that you need to know about. |
Examples from ArrayInterface's tests: @test @inferred(contiguous_axis(@view(PermutedDimsArray(A,(3,1,2))[2:3,2,:])')) === ArrayInterface.StaticInt(-1)
@test @inferred(contiguous_axis(@view(PermutedDimsArray(A,(3,1,2))[:,1:2,1])')) === ArrayInterface.StaticInt(1) E.g., To re-emphasize what I said earlier: The point is to avoid having to know about what the combinations are doing, letting you treat them individually. It makes the number of method definitions needed linear, rather than exponential.
You shouldn't implement |
Here's a trait example where using the parent type requires indirect information about the wrapper type: abstract type TraitStyle end
struct FooTrait <: TraitStyle end
struct BarTrait <: TraitStyle end
struct DefaultTrait <: TraitStyle end
abstract type TransformationStyle end
struct UnknownTransformation <: TransformationStyle end
struct FooToBar <: TransformationStyle end
function TraitStyle(::Type{T}) where {T}
if parenttype(T) <: T
DefaultTrait()
else
transform(TraitStyle(parenttype(T)), TransformationStyle(T))
end
end
TransformationStyle(::Type{T}) = UnknownTransformation()
transform(::FooTrait, ::FooToBar) = BarTrait()
transform(::FooTrait, ::TransformationStyle) = DefaultTrait() Sure, you can't blindly access the parent array making any assumption you want, but I don't think that was ever asserted. Blindly using any method is a bad idea. |
Those tests, as I understand it, would work just as well without the generic definition of: function contiguous_axis(::Type{T}) where {T}
if parent_type(T) <: T
return nothing
else
return contiguous_axis(parent_type(T))
end
end They rely upon ArrayInterface implementing
It looks like that |
Just for the record, I admit that there are some places we could tight things up in |
I still don't see how |
You're right, I agree. All it's buying is maybe making it slightly easier to type/DRY w/ respect to where the type parameters are. |
Let's say we take But ultimately I'm not suggesting that these traits be implemented or methods changed in this PR. |
Yeah, I get that. But:
It's really just My beef with But we do have |
Maybe I just don't understand what you mean by "generic". I assumed "generic" meant that
It's not always appropriate to use but I've already provided examples where it has a real benefit if used correctly. It's not exactly a complicated method, so willing misuse by others is a pretty high bar to overcome.
Do you mean it won't return the correct type for arrays that haven't yet defined Edit:
|
Yes, that's precisely my working definition as well — and my argument throughout here is that you cannot do this with the Now, yes, you can define secondary methods that say "alright, now here's how you can interpret that parent," but my argument is that it's a roundabout sort of solution that would be better as a single direct question you ask of the wrapper that is completely and fully defined in a generic fashion.
Yes, that's precisely what I mean. It means that In terms of what we're allowed to do within the bounds of semantic versioning, adding a new function is always legal, but that doesn't mean it's always a good idea. Changing the output of an existing function (e.g., changing the return type of |
Traits depending on other traits isn't exactly an unheard of idea. For example, this is a lot like trait contracts in BinaryTraits.jl. I guess you could argue that entire approach is bad and people should stop pursuing it, but there are also plenty of registered packages that define I'm just trying to contribute to the Julia community. I know I don't exactly have a whole lot of weight behind my name here, so if you don't want this method out of principle that's fine. I'm not going to be a thorn in anyone's side here.
I understand their different, but my point is that changing a return type is technically a breaking change. That seems a lot more disruptive than what I've proposed. |
Yes, I'm really sorry to be so negative about your first pull request here — and I must say I'm really thrilled to see the things that are happening in ArrayInterface.jl. It's addressing a huge pain point that I wish I would've had more time to help with. Your name does carry weight, but it doesn't really matter if you were Jeff himself or someone completely new to the ecosystem; my comments would be the same. My negativity here is all about |
Sorry for the slow response, but I think most of what needs to be said here has been.
We've been able to do most of what we need to do with the current array interface so far. Therefore, I think the utility of most new methods/traits will be low impact for most users. In my efforts to defend the addition of If concerns are specifically due to how it is used in ArrayInterface, then I'd like to make it clear that I'm aware of it's poor usage in several places and have been making steps towards fixing it. This PR is a big effort to resolve this issue (and others) by pairing I'm not aware of all the other ways people have used |
This is a fairly simple method that has proven to be a critical component for a lot of methods in ArrayInterface.jl. It seems an appropriate companion to the current methods that compose the
AbstractArray
interface in base.