Skip to content

Commit

Permalink
Crazy type introspection for default similar_type
Browse files Browse the repository at this point in the history
Make similar_type() work by default in nearly all circumstances, by
introspecting the type tree using fsa_abstract(), supertype() and a
bunch of other ugly stuff involving `TypeVar`s.  Ugh... just ugh, but I
hope it'll make it easier for users.

* Remove the specialized similar_type() implementations for concrete
  types as they're no longer needed
* Fix up README to reflect the new implementation
* Add some extra tests
  • Loading branch information
Chris Foster committed Jun 1, 2016
1 parent 39a9595 commit 34bc334
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 41 deletions.
37 changes: 17 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,35 +103,32 @@ similar_type{FSA<:FixedArray, T, NDim}(::Type{FSA}, ::Type{T}, sz::NTuple{NDim,I

This is quite similar to `Base.similar` but the first argument is a type rather
than a value. Given a custom FixedArray type, eltype and size, this function
should return a similar output type which will be used to store the results of a
elementwise operations, general `map()` invocation, etc.
should return a similar output type which will be used to store the results of
elementwise operations, general `map()` invocations, etc.

By default, `similar_type` returns the input type `FSA` if both `eltype(FSA) == T`
and `size(FSA) == sz`. If not, the canonical concrete FixedArray type (a `Vec`
or `Mat`) are returned. If your custom FixedArray subtype is parameterized on
size or eltype this may not be the right thing.
By default, `similar_type` introspects `FSA` to determine whether it can be
reparameterized by both `eltype(FSA) == T` and `size(FSA) == sz`. If not, the
canonical concrete FixedArray type (a `Vec` or `Mat`) are returned by calling
the fallback `similar_type(FixedArray, T, sz)`. Sometimes this may not make
sense for your custom FixedArray subtype.

For example, suppose you define the type `RGB{T}` as above. This inherently has
a fixed size but variable eltype. Perhaps you want mixed operations with
`RGB{Int}` and `RGB{Float64}` to return an `RGB{Float64}`. In this case you
should write something like:
For example, suppose you define the type `RGB{T}` as above, and you'd prefer
relational operators to return a `Vec{3,Bool}` as a mask rather than an
`RGB{Bool}`. In this case you could write something like:

```julia
function FixedSizeArrrays.similar_type{FSA<:RGB,T}(::Type{FSA}, ::Type{T}, n::Tuple{Int})
n[1] == 3 ? RGB{T} : similar_type(FixedArray, T, n)
function FixedSizeArrays.similar_type{FSA<:RGB,T}(::Type{FSA}, ::Type{T}, n::Tuple{Int})
n == (3,) && T != Bool ? RGB{T} : similar_type(FixedArray, T, n)
end
```

We then have `RGB(1,2,3) + RGB(1.0,1.0,1.0) === RGB(2.0,3.0,4.0)`, but also more
exotic things, such as `RGB(1,2,3) + RGB(1.0im,1.0im,1.0im) === RGB(1.0 +
1.0im,2.0 + 1.0im,3.0 + 1.0im)`. More usefully, this all works with types such
as `Dual{T}` from DualNumbers which allows derivative information to be
naturally propagated in FixedArray elements.
We then have `RGB(1,2,3) .< RGB(2,2,2) === Vec{3,Bool}(true,false,false)`.

Note that `similar_type` as written above isn't type stable. For the internal
Note that `similar_type` isn't type stable in julia-0.4. For the internal
use in `FixedSizeArrays` (type deduction inside `@generated` functions) this
isn't a problem, but you may want to annotate it with `Base.@pure` if you're
using julia-0.5 and you want to use `similar_type` in a normal function.
isn't a problem, but you may want to annotate your custom overlads with
`Base.@pure` if you're using julia-0.5 and you want to use `similar_type` in a
normal function.


#### Roadmap
Expand Down
2 changes: 0 additions & 2 deletions src/FixedSizeArrays.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ include("constructors.jl")
immutable Mat{Row, Column, T} <: FixedMatrix{Row, Column, T}
_::NTuple{Column, NTuple{Row, T}}
end
@pure similar_type{FSA<:Mat,T}(::Type{FSA}, ::Type{T}, sz::NTuple{2, Int}) = Mat{sz[1], sz[2], T}

# most common FSA types
immutable Vec{N, T} <: FixedVector{N, T}
Expand All @@ -39,7 +38,6 @@ end
immutable Point{N, T} <: FixedVector{N, T}
_::NTuple{N, T}
end
@pure similar_type{FSA<:Point,T}(::Type{FSA}, ::Type{T}, sz::Tuple{Int}) = Point{sz[1],T}

include("mapreduce.jl")
include("destructure.jl")
Expand Down
82 changes: 70 additions & 12 deletions src/core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,80 @@ done(A::FixedArray, state::Integer) = length(A) < state
:($(T.name.primary))
end

@pure function similar_type{FSA <: FixedArray, T}(::Type{FSA}, ::Type{T}, n::Tuple)
# Exact match - return the same type again
if eltype(FSA) == T && n == size(FSA)
return FSA
end
# Fallback - return a standard FixedArray container by default
if length(n) == 1
return Vec{n[1],T}
elseif length(n) == 2
return Mat{n[1],n[2],T}
"""
similar_type(::Type{FSA}, [::Type{T}=eltype(FSA)], [sz=size(FSA)])
Given an array type `FSA`, element type `T` and size `sz`, return a `FixedArray`
subtype which is as similar as possible. `similar_type` is used in the same
spirit as `Base.similar` to store the results of `map()` operations, etc.
(`similar` cannot work here, because the types are generally immutable.)
By default, `similar_type` introspects `FSA` to determine whether `T` and `sz`
can be used; if not a canonical FixedArray container is returned instead.
"""
@pure function similar_type{T}(::Type{FixedArray}, ::Type{T}, sz::Tuple)
if length(sz) == 1
return Vec{sz[1],T}
elseif length(sz) == 2
return Mat{sz[1],sz[2],T}
else
throw(ArgumentError("similar_type not implemented for size = $n"))
throw(ArgumentError("No built in FixedArray type is implemented for eltype $T and size $sz"))
end
end

@pure function similar_type{FSA <: FixedArray, T}(::Type{FSA}, ::Type{T}, sz::Tuple)
fsa_size = fsa_abstract(FSA).parameters[3].parameters
if eltype(FSA) == T && fsa_size == sz
return FSA # Common case optimization
end

# The default implementation for similar_type is follows. It involves a
# fair bit of crazy type introspection: We check whether the type `FSA` has
# the necessary type parameters to replace with `T` and `sz`, and if so
# figure out how to do the replacement. It's complicated because users may
# arbitrarily rearrange type parameters in their subtypes, and possibly
# even add new type parameters which aren't related to the abstract
# FixedArray but should be preserved.

# Propagate the available type parameters of FSA down to the abstract base
# FixedArray as `TypeVar`s.
pritype = FSA.name.primary
fsatype = fsa_abstract(pritype)
T_parameter = fsatype.parameters[1]
ndim_parameter = fsatype.parameters[2]
sz_parameter = fsatype.parameters[3]
sz_parameters = fsatype.parameters[3].parameters

# Figure out whether FSA can accommodate the new eltype `T` and size `sz`.
# If not, delegate to the fallback by default.
if !((eltype(FSA) == T || isa(T_parameter, TypeVar)) &&
(ndims(FSA) == length(sz) || isa(ndim_parameter, TypeVar)) &&
(fsa_size == sz || all(i -> (sz[i] == fsa_size[i] || isa(sz_parameters[i],TypeVar)), 1:length(sz))))
return similar_type(FixedArray, T, sz)
end

# Iterate type parameters, replacing as necessary with T and sz
params = collect(FSA.parameters)
priparams = pritype.parameters
for i=1:length(params)
if priparams[i] === T_parameter
params[i] = T
elseif priparams[i] === ndim_parameter
params[i] = length(sz)
elseif priparams[i] === sz_parameter
params[i] = Tuple{sz...}
else
for j = 1:length(sz_parameters)
if priparams[i] === sz_parameters[j]
params[i] = sz[j]
end
end
end
end
pritype{params...}
end

# Versions with defaulted eltype and size
# similar_type versions with defaulted eltype and size
@pure similar_type{FSA <: FixedArray, T}(::Type{FSA}, ::Type{T}) = similar_type(FSA, T, size(FSA))
@pure similar_type{FSA <: FixedArray}(::Type{FSA}, sz::Tuple) = similar_type(FSA, eltype(FSA), sz)

Expand Down
17 changes: 10 additions & 7 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,17 @@ immutable RGB{T} <: FixedVectorNoTuple{3, T}
new{T}(a[1], a[2], a[3])
end
end
function similar_type{FSA<:RGB,T}(::Type{FSA}, ::Type{T}, n::Tuple{Int})
n[1] == 3 ? RGB{T} : similar_type(FixedArray, T, n)
end

# subtyping:
immutable TestType{N,T} <: FixedVector{N,T}
_::NTuple{N,T}
end

# Test similar_type usage for custom FSA with non-parameterized size and eltype
# Custom FSA with non-parameterized size and eltype
immutable Coord2D <: FixedVectorNoTuple{2,Float64}
x::Float64
y::Float64
end
function similar_type{T}(::Type{Coord2D}, ::Type{T}, n::Tuple{Int})
n[1] == 2 && T == Float64 ? Coord2D : similar_type(FixedArray, T, n)
end


typealias Vec1d Vec{1, Float64}
Expand Down Expand Up @@ -130,16 +124,25 @@ context("core") do

@fact ndims_or(FixedArray, nothing) --> nothing
end

context("similar_type") do
@fact similar_type(Vec{3,Int}, Float32) --> Vec{3, Float32}
@fact similar_type(Vec{3}, Float32) --> Vec{3, Float32}
@fact similar_type(Vec, Float32, (3,)) --> Vec{3, Float32}
@fact similar_type(Vec, Float32, (1,2)) --> Mat{1,2, Float32}

@fact similar_type(RGB, Float32) --> RGB{Float32}
@fact similar_type(RGB{Float32}, Int) --> RGB{Int}
@fact similar_type(RGB{Float32}, Int, (3,)) --> RGB{Int}
@fact similar_type(RGB{Float32}, Int, (2,2)) --> Mat{2,2,Int}

@fact similar_type(Mat{3,3,Int}, Float32) --> Mat{3,3,Float32}
@fact similar_type(Mat, Float32, (3,3)) --> Mat{3,3,Float32}
@fact similar_type(Mat{2,2,Int}, (3,3)) --> Mat{3,3,Int}

@fact similar_type(Coord2D, Float64, (2,)) --> Coord2D
@fact similar_type(Coord2D, Int, (2,)) --> Vec{2,Int}
@fact similar_type(Coord2D, Float64, (3,)) --> Vec{3,Float64}
end

context("construct_similar") do
Expand Down

0 comments on commit 34bc334

Please sign in to comment.