Skip to content
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
4 changes: 4 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ uuid = "3821ddf9-e5b5-40d5-8e25-6813ab96b5e2"
version = "0.1.0"
authors = ["Mosè Giordano <[email protected]>"]

[deps]
Collects = "08986516-18db-4a8b-8eaa-f5ef1828d8f1"

[weakdeps]
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[extensions]
RandomExt = "Random"

[compat]
Collects = "1"
Random = "1"
julia = "1.10"
2 changes: 2 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[deps]
Changelog = "5217a498-cd5d-4ec6-b8c2-9b85a09b6e3e"
Collects = "08986516-18db-4a8b-8eaa-f5ef1828d8f1"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
FixedSizeArrays = "3821ddf9-e5b5-40d5-8e25-6813ab96b5e2"

Expand All @@ -8,4 +9,5 @@ FixedSizeArrays = {path = ".."}

[compat]
Changelog = "1.1"
Collects = "1"
Documenter = "1"
2 changes: 1 addition & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
FixedSizeArrays supports the [standard array interfaces](https://docs.julialang.org/en/v1/manual/interfaces/#man-interface-array), so things like broadcasting, matrix multiplication, other linear algebra operations, `similar`, `copyto!` or `map` should just work.

Use the constructors to convert from other array types.
Use [`collect_as`](@ref) to convert from arbitrary iterators.
Use `collect_as` from the [Collects.jl](https://github.com/JuliaCollections/Collects.jl) package to convert from arbitrary iterators.

## Comparison with other array types

Expand Down
3 changes: 0 additions & 3 deletions docs/src/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ FixedSizeMatrix
FixedSizeArrayDefault
FixedSizeVectorDefault
FixedSizeMatrixDefault
FixedSizeArrays.collect_as
Base.parent
BoundsErrorLight
```
Expand All @@ -29,6 +28,4 @@ This section gathers documentation of internal functionalities of `FixedSizeArra
```@docs
FixedSizeArrays.with_stripped_type_parameters
FixedSizeArrays.with_stripped_type_parameters_unchecked
FixedSizeArrays.collect_as_fsv
FixedSizeArrays.push!!
```
14 changes: 7 additions & 7 deletions docs/src/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,32 +64,32 @@ To make it easier to refer to the concrete type `FixedSizeArray{T,N,Mem}` with t
## The `collect_as` utility

The [array literals syntax](https://docs.julialang.org/en/v1/manual/arrays/#man-array-literals) `[A, B, C, ...]` to construct arrays is limited to `Base`'s `Array` and cannot be extended to custom array types.
`FixedSizeArrays.jl` provides a convenient function [`collect_as`](@ref) to overcome this limitation and construct `FixedSizeArray`s out of any iterable:
The package [Collects.jl](https://github.com/JuliaCollections/Collects.jl) provides a convenient function, `collect_as`, to overcome this limitation and construct `FixedSizeArray`s out of any iterable:

```jldoctest
julia> iter = (i for i ∈ 7:9 if i≠8);

julia> using FixedSizeArrays

julia> const ca = FixedSizeArrays.collect_as;
julia> using FixedSizeArrays, Collects

julia> ca(FixedSizeArray, iter) # construct from an arbitrary iterator
julia> collect_as(FixedSizeArray, iter) # construct from an arbitrary iterator
2-element FixedSizeArray{Int64, 1, Memory{Int64}}:
7
9

julia> ca(FixedSizeArray{Float64}, iter) # construct from an arbitrary iterator while converting element type
julia> collect_as(FixedSizeArray{Float64}, iter) # construct from an arbitrary iterator while converting element type
2-element FixedSizeArray{Float64, 1, Memory{Float64}}:
7.0
9.0

julia> ca(FixedSizeVectorDefault, (3.14, -4.2, 2.68)) # construct from a tuple
julia> collect_as(FixedSizeVectorDefault, (3.14, -4.2, 2.68)) # construct from a tuple
3-element FixedSizeArray{Float64, 1, Memory{Float64}}:
3.14
-4.2
2.68
```

See the Collects.jl Readme for more information.

## `BoundsErrorLight` exception

To facilitate the [escape analysis](https://en.wikipedia.org/wiki/Escape_analysis) of `FixedSizeArray`s, accessing an out-of-bound index of these arrays raises a [`BoundsErrorLight`](@ref) exception when possible.
Expand Down
15 changes: 13 additions & 2 deletions src/FixedSizeArray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -325,12 +325,23 @@ axes_are_one_based(axes) = all(isone ∘ first, axes)

# converting constructors for copying other array types

function (::Type{FSA})(src::AbstractArray) where {FSA <: FixedSizeArray}
function converting_constructor(::Type{FSA}, src::AbstractArray) where {FSA <: FixedSizeArray}
axs = axes(src)
if !axes_are_one_based(axs)
throw(DimensionMismatch("source array has a non-one-based indexing axis"))
end
collect_as(FSA, src)
collect_as_haseltype(FSA, src)
end

function (::Type{FSA})(src::AbstractArray) where {N, FSA <: FixedSizeArray{<:Any, N}}
if N::Int != ndims(src)
throw(DimensionMismatch("dimensionality mismatch"))
end
converting_constructor(FSA, src)
end

function (::Type{FSA})(src::AbstractArray) where {FSA <: FixedSizeArray}
converting_constructor(FSA, src)
end

# `copy`: avoid the `similar` and `copyto!` in the generic fallback
Expand Down
2 changes: 2 additions & 0 deletions src/FixedSizeArrays.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module FixedSizeArrays

using Collects: Collect, collect_as

include("BoundsErrorLight.jl")

const default_underlying_storage_type = (@isdefined Memory) ? Memory : Vector
Expand Down
180 changes: 33 additions & 147 deletions src/collect_as.jl
Original file line number Diff line number Diff line change
@@ -1,168 +1,54 @@
export collect_as

function throw_bottom_type()
function (::Collect)(::Type{<:(FixedSizeArray{E, N, Union{}} where {E, N})}, ::Any)
throw(ArgumentError("`Union{}` not expected"))
end

function eltype_is_known(::Type{Storage}) where {S, Storage <: AbstractVector{S}}
@isdefined S
end
function eltype_is_known(::Type{Storage}) where {Storage <: AbstractVector}
false
end

function collect_as_storage_type_helper(::Type{Storage}, ::Type) where {S, Storage <: AbstractVector{S}}
Storage
end
function collect_as_storage_type_helper(::Type{Storage}, ::Type{E}) where {Storage <: AbstractVector, E}
Storage{E}
end

function make_abstract_vector_from_collection_with_length(::Type{V}, elems) where {E, V <: AbstractVector{E}}
ret = V(undef, length(elems))
copyto!(ret, elems)
end

function fsv_type_from_underlying_storage_type(::Type{V}) where {E, V <: DenseVector{E}}
FixedSizeVector{E, V}
function infer_ndims_impl(::Base.HasShape{N}) where {N}
N::Int
end

function make_fsv_from_collection_with_length(::Type{V}, elems) where {V <: DenseVector}
stor = collect_as_storage_type_helper(V, eltype(elems))
ret_type = fsv_type_from_underlying_storage_type(stor)
make_abstract_vector_from_collection_with_length(ret_type, elems)
end

function make_vector_from_tuple(::Type{V}, elems::Tuple) where {V <: DenseVector}
stor = collect_as_storage_type_helper(V, eltype(elems))
ret_type = Vector{eltype(stor)}
make_abstract_vector_from_collection_with_length(ret_type, elems)
end

function push(v::Vector, e)
E = typejoin(typeof(e), eltype(v))
ret = Vector{E}(undef, length(v) + 1)
ret = copyto!(ret, v)
ret[end] = e
ret
end

"""
push!!(t::Type{<:AbstractVector}, v::Vector, e)::Vector
Return a `Vector`, `r`, respecting these properties:
* `all(r[begin:(end - 1)] .=== v)`
* if `t` specifies an element type, `E`, `r[end] == E(e)`, otherwise `r[end] === e`
"""
function push!! end
function push!!(::Type{<:AbstractVector{E}}, v::Vector{E}, e::E) where {E}
push!(v, e)
end
function push!!(::Type{<:AbstractVector{E}}, v::Vector{E}, e) where {E}
push!(v, e)
end
function push!!(::Type{<:AbstractVector}, v::Vector{E}, e::E) where {E}
push!(v, e)
end
function push!!(::Type{<:AbstractVector}, v::Vector, e)
push(v, e)
function infer_ndims_impl(::Base.IteratorSize)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an easy test to exercise this function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's already exercised (checked by replacing 1 with error("exercised!")). This is just this issue again:

1

Check warning on line 10 in src/collect_as.jl

View check run for this annotation

Codecov / codecov/patch

src/collect_as.jl#L9-L10

Added lines #L9 - L10 were not covered by tests
end

function empty_fsv(::Type{V}, ::Any) where {E, V <: DenseVector{E}}
fsv_type_from_underlying_storage_type(V)(undef, 0)
function infer_ndims(iterator)
infer_ndims_impl(Base.IteratorSize(iterator))
end

function empty_fsv(::Type{V}, iterator) where {V <: DenseVector}
let E = eltype(iterator)
if isconcretetype(E)
fsv_type_from_underlying_storage_type(V{E})(undef, 0)
else
fsv_type_from_underlying_storage_type(V{Union{}})(undef, 0)
end
function inferred_shape_impl(output_ndims::Int, iterator)
if (!isone(output_ndims)) && (output_ndims != infer_ndims(iterator))
throw(DimensionMismatch("mismatched dimensionalities"))
end
end

"""
collect_as_fsv(V::Type{<:DenseVector}, iterator)::FixedSizeVector
Collect the elements of `iterator` into a `FixedSizeVector`, `r`. The argument `V`
specifies the underlying storage type parameter of `r`. When possible (specified as
a parameter in `V`), `eltype(r)` is also taken from `V`, otherwise it is determined
by the types of the elements of the iterator.
When `V` does not provide an element type and `isempty(iterator)`, the element type
of the return value is:
* `eltype(iterator)`, when it's a concrete type
* `Union{}`, otherwise
"""
function collect_as_fsv(::Type{V}, iterator) where {V <: DenseVector}
es1 = iterate(iterator) # unroll a bit to avoid unnecessary allocations and help inference
if es1 isa Tuple
let (e1, s1) = es1, state = s1, ret = make_vector_from_tuple(V, (e1,))
while true
es = iterate(iterator, state)
if es isa Tuple
let (e, s) = es
state = s
ret = push!!(V, ret, e)
end
else
break
end
end
ret_type_storage = collect_as_storage_type_helper(V, eltype(ret))
ret_type = fsv_type_from_underlying_storage_type(ret_type_storage)
ret_type(ret)
end
if iszero(output_ndims)
()
elseif isone(output_ndims)
(:,)
else
empty_fsv(V, iterator)
size(iterator)
end
end

function checked_dimension_count_of(::Type{<:AbstractArray{<:Any, N}}, input_size_class) where {N}
n = check_count_value(N)
m = dimension_count_of(input_size_class)::Int
if n != m
throw(DimensionMismatch())
end
n
end
function checked_dimension_count_of(::Type, input_size_class)
check_count_value(dimension_count_of(input_size_class))
function inferred_shape(::Type{<:(AbstractArray{T, N} where {T})}, iterator) where {N}
inferred_shape_impl(N::Int, iterator)
end

function collect_as(::Type{<:(FixedSizeArray{E, N, Union{}} where {E, N})}, ::Any)
throw_bottom_type()
function inferred_shape(::Type{<:AbstractArray}, iterator)
inferred_shape_impl(infer_ndims(iterator), iterator)
end
function collect_as(::Type{Union{}}, ::Any)
throw_bottom_type()
end

"""
collect_as(t::Type{<:FixedSizeArray}, iterator)

Tries to construct a value of type `t` from the iterator `iterator`. The type `t`
must either be concrete, or a `UnionAll` without constraints.
"""
function collect_as(::Type{FSA}, iterator) where {FSA<:FixedSizeArray}
size_class = Base.IteratorSize(iterator)
if size_class == Base.IsInfinite()
throw(ArgumentError("iterator is infinite, can't fit infinitely many elements into a `FixedSizeArray`"))
end
function (collect::Collect)(::Type{FSA}, iterator) where {FSA<:FixedSizeArray}
T = check_constructor_is_allowed(FSA)
mem = parent_type_with_default(T)
output_dimension_count = checked_dimension_count_of(T, size_class)
fsv = if (
(size_class isa Union{Base.HasLength, Base.HasShape}) &&
(eltype_is_known(mem) || isconcretetype(eltype(iterator)))
)
make_fsv_from_collection_with_length(mem, iterator) # fast path
else
collect_as_fsv(mem, iterator)
end
if isone(output_dimension_count)
fsv # `size(iterator)` may throw in this branch
else
reshape(fsv, size(iterator))
end
shape = inferred_shape(T, iterator)
backing = collect(mem, iterator)
fsv = new_fixed_size_array(backing, (length(backing),))
reshape(fsv, shape)
end

function collect_as_haseltype(::Type{FSA}, iterator) where {T, FSA<:FixedSizeArray{T}}
collect_as(check_constructor_is_allowed(FSA), iterator)
end

function collect_as_haseltype(::Type{FSA}, iterator) where {FSA<:FixedSizeArray}
T = check_constructor_is_allowed(FSA){eltype(iterator)}
collect_as(T, iterator)
end
2 changes: 2 additions & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
[compat]
Aqua = "0.8"
Collects = "1"
OffsetArrays = "1.14"
Random = "1.10"
Test = "1.10"

[deps]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
Collects = "08986516-18db-4a8b-8eaa-f5ef1828d8f1"
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Loading