Skip to content
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

similar_type implementation #118

Merged
merged 6 commits into from
Jun 1, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,91 @@ For some more advantages, you can take a look at [MeshIO](https://github.com/Jul
Because it's so easy to define different types like Point3, RGB, HSV or Normal3, one can create customized code for these types via multiple dispatch. This is great for visualizing data, as you can offer default visualizations based on the type.
Without FixedSizeArrays, this would end up in a lot of types which would all need to define the same functions over and over again.

#### FixedArray abstract types

The package provides several abstract types:

* `FixedArray{T,NDim,SIZE}` is the abstract base type for all fixed
arrays. `T` and `NDim` mirror the eltype and number of dimension type
parameters in `AbstractArray`. In addition there's a `SIZE` Tuple which
defines the extent of each fixed dimension as an integer.

There's some convenient type aliases:

* `FixedVector{N,T}` is a convenient type alias for a one dimensional fixed
vector of length `N` and eltype `T`.
* `FixedMatrix{N,M,T}` is a convenient type alias for a two dimensional fixed
matrix of size `(N,M)` and eltype `T`.

Finally there's an abstract type `FixedVectorNoTuple{N, T}` for use when you'd
like to name the fields of a `FixedVector` explicitly rather than accessing them
via an index.


#### FixedArray concrete types

The package currently provides three concrete FixedArray types

* `Vec{N,T}` is a length `N` vector of eltype `T`.
* `Mat{N,M,T}` is an `N×M` matrix of eltype `T`

These two types are intended to behave the same as `Base.Vector` and
`Base.Matrix`, but with fixed size. That is, the interface is a convenient
union of elementwise array-like functionality and vector space / linear algebra
operations. Hopefully we'll have more general higher dimensional fixed size
containers in the future (note that the total number of elements of a higher
dimensional container quickly grows beyond the size where having a fixed stack
allocated container really makes sense).

* `Point{N,T}` is a position type which is structurally identical to `Vec{N,T}`.

Semantically `Point{N,T}` should be used to represent position in an
`N`-dimensional Cartesian space. The distinction between this and `Vec` is
particularly relevant when overloading functions which deal with geometric data.
For instance, a geometric transformation applies differently depending on
whether you're transforming a *position* (`Point`) versus a *direction* (`Vec`).


#### User-supplied functions for FixedArray subtypes

Most array functionality comes for free when inheriting from one of the abstract
types `FixedArray`, `FixedVector`, `FixedMatrix`, or `FixedVectorNoTuple`.
However, the user may want to overload a few things. At the moment,
`similar_type` is the main function you may want to customize. The signature is

```julia
similar_type{FSA<:FixedArray, T, NDim}(::Type{FSA}, ::Type{T}, sz::NTuple{NDim,Int})
```

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
elementwise operations, general `map()` invocations, etc.

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, 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 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(2,2,2) === Vec{3,Bool}(true,false,false)`.

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 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
* improve coverage
Expand Down
22 changes: 13 additions & 9 deletions src/FixedSizeArrays.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,27 @@ import Base.LinAlg.chol!
# for 0.5 and 0.4 compat, use our own functor type
abstract Functor{N}

include("core.jl")
include("functors.jl")
include("constructors.jl")

if VERSION <= v"0.5.0"
supertype(x) = super(x)
end

if VERSION < v"0.5.0-dev+698"
macro pure(ex)
esc(ex)
end
else
import Base: @pure
end

include("core.jl")
include("functors.jl")
include("constructors.jl")

# put them here due to #JuliaLang/julia#12814
# needs to be before indexing and ops, but after constructors
immutable Mat{Row, Column, T} <: FixedMatrix{Row, Column, T}
_::NTuple{Column, NTuple{Row, T}}
end
function similar{FSA <: Mat, T}(::Type{FSA}, ::Type{T}, SZ::NTuple{2, Int})
Mat{SZ[1], SZ[2], T}
end
similar{FSA <: Mat}(::Type{FSA}, SZ::NTuple{2,Int}) = similar(FSA, eltype(FSA), SZ)
similar{N,M,S, T}(::Type{Mat{N,M,S}}, ::Type{T}) = Mat{N,M,T}

# most common FSA types
immutable Vec{N, T} <: FixedVector{N, T}
Expand Down Expand Up @@ -70,6 +73,7 @@ export MutableFixedVector
export MutableFixedMatrix
export Mat, Vec, Point
export @fsa
export similar_type
export construct_similar

export unit
Expand Down
2 changes: 1 addition & 1 deletion src/constructors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ and overwrites the default constructor.
end
SZ = size_or(FSA, (orlen, ntuple(x->1, ND-1)...))::NTuple{ND, Int}
T = eltype_or(FSA, ortyp)::DataType
FSAT = similar(FSA, T, SZ)
FSAT = similar_type(FSA, T, SZ)
if X <: Tuple
expr = fill_tuples_expr((inds...)->:($T(a[$(inds[1])])), SZ)
else
Expand Down
92 changes: 76 additions & 16 deletions src/core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -78,24 +78,84 @@ done(A::FixedArray, state::Integer) = length(A) < state
:($(T.name.primary))
end

similar{FSA <: FixedVector, T}(::Type{FSA}, ::Type{T}, n::Tuple) = similar(FSA, T, n...)
@generated function similar{FSA <: FixedVector, T}(::Type{FSA}, ::Type{T}, n::Int)
name = basetype(FSA)
:($name{n, T, $(FSA.parameters[3:end]...)})
end
@generated function similar{FSA <: FixedVector, T}(::Type{FSA}, ::Type{T})
name = basetype(FSA)
:($name{$(FSA.parameters[1]), T, $(FSA.parameters[3:end]...)})
end
@generated function similar{FSA <: FixedVectorNoTuple, T}(::Type{FSA}, ::Type{T})
name = basetype(FSA)
:($name{T, $(FSA.parameters[3:end]...)})
"""
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("No built in FixedArray type is implemented for eltype $T and size $sz"))
end
end
@generated function similar{FSA <: FixedVectorNoTuple, T}(::Type{FSA}, ::Type{T}, n::Int)
name = basetype(FSA)
:($name{T, $(FSA.parameters[3: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

# 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)


@generated function get_tuple{N, T}(f::FixedVectorNoTuple{N, T})
:(tuple($(ntuple(i->:(f[$i]), N)...)))
end
Expand Down Expand Up @@ -158,7 +218,7 @@ promoted element type of the nested tuple `elements`.
@generated function construct_similar{FSA <: FixedArray}(::Type{FSA}, elements::Tuple)
etype = promote_type_nested(elements)
shape = nested_Tuple_shape(elements)
outtype = similar(FSA, etype, shape)
outtype = similar_type(FSA, etype, shape)
converted_elements = convert_nested_tuple_expr(etype, :elements, elements)
constructor_expr(outtype, converted_elements)
end
Expand Down
6 changes: 0 additions & 6 deletions src/mapreduce.jl
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,6 @@ end
@generated function map{F<:FixedArray}(func, arg1::F)
unrolled_map_expr(:func, SimilarTo{arg1}, size(F), (arg1,), (:arg1,))
end
# Unary versions for type conversion. Need to override these explicitly to
# prevent conflicts with Base.
@inline map{T,N,S}(::Type{T}, arg1::FixedArray{T,N,S}) = arg1 # nop version
immutable ConstructTypeFun{T}; end
call{T}(::ConstructTypeFun{T}, x) = T(x)
@inline map{T,FSA<:FixedArray}(::Type{T}, arg1::FSA) = map(ConstructTypeFun{T}(), similar(FSA, T), arg1)


# Nullary special case version.
Expand Down
52 changes: 40 additions & 12 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ using FixedSizeArrays
using FactCheck, Base.Test
using Compat

import FixedSizeArrays: similar_type

immutable Normal{N, T} <: FixedVector{N, T}
_::NTuple{N, T}
end
Expand All @@ -18,9 +20,16 @@ immutable RGB{T} <: FixedVectorNoTuple{3, T}
new{T}(a[1], a[2], a[3])
end
end

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

# Custom FSA with non-parameterized size and eltype
immutable Coord2D <: FixedVectorNoTuple{2,Float64}
x::Float64
y::Float64
end


Expand Down Expand Up @@ -115,16 +124,25 @@ context("core") do

@fact ndims_or(FixedArray, nothing) --> nothing
end
context("similar") do
@fact similar(Vec{3}, Float32) --> Vec{3, Float32}
@fact similar(Vec, Float32, 3) --> Vec{3, Float32}

@fact similar(RGB, Float32) --> RGB{Float32}
@fact similar(RGB{Float32}, Int) --> RGB{Int}
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(Mat{3,3,Int}, Float32) --> Mat{3,3,Float32}
@fact similar(Mat, Float32, (3,3)) --> Mat{3,3,Float32}
@fact similar(Mat{2,2,Int}, (3,3)) --> Mat{3,3,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 Expand Up @@ -590,11 +608,15 @@ context("Ops") do
@fact isa(-v1, Vec3d) --> true
end

context("Negation") do
context("Addition") do
@fact @inferred(v1+v2) --> Vec3d(7.0,7.0,7.0)
@fact @inferred(RGB(1,2,3) + RGB(2,2,2)) --> exactly(RGB{Int}(3,4,5))
@fact @inferred(Coord2D(1,2) + Coord2D(3,4)) --> exactly(Coord2D(4,6))
end
context("Negation") do
context("Subtraction") do
@fact @inferred(v2-v1) --> Vec3d(5.0,3.0,1.0)
@fact @inferred(RGB(1,2,3) - RGB(2,2,2)) --> exactly(RGB{Int}(-1,0,1))
@fact @inferred(Coord2D(1,2) - Coord2D(3,4)) --> exactly(Coord2D(-2,-2))
end
context("Multiplication") do
@fact @inferred(v1.*v2) --> Vec3d(6.0,10.0,12.0)
Expand All @@ -603,6 +625,12 @@ context("Ops") do
@fact @inferred(v1 ./ v1) --> Vec3d(1.0,1.0,1.0)
end

context("Relational") do
@fact Vec(1,3) .< Vec(2,2) --> exactly(Vec{2,Bool}(true,false))
@fact RGB(1,2,3) .< RGB(2,2,2) --> exactly(RGB{Bool}(true,false,false))
@fact Coord2D(1,3) .< Coord2D(2,2) --> exactly(Vec{2,Bool}(true,false))
end

context("Scalar") do
@fact @inferred(1.0 + v1) --> Vec3d(2.0,3.0,4.0)
@fact @inferred(1.0 .+ v1) --> Vec3d(2.0,3.0,4.0)
Expand Down Expand Up @@ -1145,7 +1173,7 @@ end

facts("show for subtype") do

Base.show(io::IO, x::TestType) = print(io, "$(x.a)") # show for new type
Base.show(io::IO, x::TestType) = print(io, "$(x._)") # show for new type

x = TestType(1, 2)
@fact string(x) --> "(1,2)"
Expand Down