Skip to content

Commit c102065

Browse files
authored
introduce function collect_as for construction from an iterator (#48)
* introduce function `collect_as` for construction from an iterator Makes constructing `FixedSizeArray`s more convenient! Inspired by JuliaLang/julia#36288 This currently ignores `Base.IteratorElType`, xref https://discourse.julialang.org/t/i-dont-get-base-iteratoreltype/113604 The allocations in some code paths are probably excessive/could be optimized. But I guess this is good for a start. Fixes #20 * also test an iterator with `BigInt`-valued `size` and `length` * remove the premature optimization for `AbstractArray` * improve tests * add to the Readme * whitespace/formatting fix * delete two useless `nothing` lines One of these wasn't being recorded by code coverage (another Julia coverage bug, I guess). * simplify a bit
1 parent b56cfd5 commit c102065

File tree

3 files changed

+265
-0
lines changed

3 files changed

+265
-0
lines changed

README.md

+40
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,46 @@ Main differences between `FixedSizeArray` and `MArray` are:
1313
* `FixedSizeArray` is based on the `Memory` type introduced in Julia v1.11, `MArray` is backed by tuples;
1414
* the size of the array is part of the type parameters of `MArray`, this isn't the case for `FixedSizeArray`, where the size is only a constant field of the data structure.
1515

16+
FixedSizeArrays supports the usual array interfaces, so things like broadcasting, matrix
17+
multiplication, other linear algebra operations, `similar`, `copyto!` or `map` should just work.
18+
19+
Use the constructors to convert from other array types. Use `collect_as` to convert from
20+
arbitrary iterators.
21+
22+
```julia-repl
23+
julia> arr = [10 20; 30 14]
24+
2×2 Matrix{Int64}:
25+
10 20
26+
30 14
27+
28+
julia> iter = (i for i ∈ 7:9 if i≠8);
29+
30+
julia> using FixedSizeArrays
31+
32+
julia> FixedSizeArray(arr) # construct from an `AbstractArray` value
33+
2×2 FixedSizeMatrix{Int64}:
34+
10 20
35+
30 14
36+
37+
julia> FixedSizeArray{Float64}(arr) # construct from an `AbstractArray` value while converting element type
38+
2×2 FixedSizeMatrix{Float64}:
39+
10.0 20.0
40+
30.0 14.0
41+
42+
julia> const ca = FixedSizeArrays.collect_as
43+
collect_as (generic function with 1 method)
44+
45+
julia> ca(FixedSizeArray, iter) # construct from an arbitrary iterator
46+
2-element FixedSizeVector{Int64}:
47+
7
48+
9
49+
50+
julia> ca(FixedSizeArray{Float64}, iter) # construct from an arbitrary iterator while converting element type
51+
2-element FixedSizeVector{Float64}:
52+
7.0
53+
9.0
54+
```
55+
1656
Note: `FixedSizeArray`s are not guaranteed to be stack-allocated, in fact they will more likely *not* be stack-allocated.
1757
However, in some *extremely* simple cases the compiler may be able to completely elide their allocations:
1858
```julia

src/FixedSizeArrays.jl

+148
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ module FixedSizeArrays
22

33
export FixedSizeArray, FixedSizeVector, FixedSizeMatrix
44

5+
public collect_as
6+
57
"""
68
Internal()
79
@@ -94,6 +96,41 @@ end
9496

9597
# helper functions
9698

99+
dimension_count_of(::Base.SizeUnknown) = 1
100+
dimension_count_of(::Base.HasLength) = 1
101+
dimension_count_of(::Base.HasShape{N}) where {N} = convert(Int, N)::Int
102+
103+
struct LengthIsUnknown end
104+
struct LengthIsKnown end
105+
length_status(::Base.SizeUnknown) = LengthIsUnknown()
106+
length_status(::Base.HasLength) = LengthIsKnown()
107+
length_status(::Base.HasShape) = LengthIsKnown()
108+
109+
function check_count_value(n::Int)
110+
if n < 0
111+
throw(ArgumentError("count can't be negative"))
112+
end
113+
end
114+
function check_count_value(n)
115+
throw(ArgumentError("count must be an `Int`"))
116+
end
117+
118+
struct SpecFSA{T,N} end
119+
function fsa_spec_from_type(::Type{FixedSizeArray})
120+
SpecFSA{nothing,nothing}()
121+
end
122+
function fsa_spec_from_type(::Type{FixedSizeArray{<:Any,M}}) where {M}
123+
check_count_value(M)
124+
SpecFSA{nothing,M}()
125+
end
126+
function fsa_spec_from_type(::Type{FixedSizeArray{E}}) where {E}
127+
SpecFSA{E::Type,nothing}()
128+
end
129+
function fsa_spec_from_type(::Type{FixedSizeArray{E,M}}) where {E,M}
130+
check_count_value(M)
131+
SpecFSA{E::Type,M}()
132+
end
133+
97134
parent_type(::Type{<:FixedSizeArray{T}}) where {T} = Memory{T}
98135

99136
underlying_storage(m) = m
@@ -168,4 +205,115 @@ function Base.reshape(a::FixedSizeArray{T}, size::NTuple{N,Int}) where {T,N}
168205
FixedSizeArray{T,N}(Internal(), a.mem, size)
169206
end
170207

208+
# `collect_as`
209+
210+
function collect_as_fsa0(iterator, ::Val{nothing})
211+
x = only(iterator)
212+
ret = FixedSizeArray{typeof(x),0}(undef)
213+
ret[] = x
214+
ret
215+
end
216+
217+
function collect_as_fsa0(iterator, ::Val{E}) where {E}
218+
E::Type
219+
x = only(iterator)
220+
ret = FixedSizeArray{E,0}(undef)
221+
ret[] = x
222+
ret
223+
end
224+
225+
function fill_fsa_from_iterator!(a, iterator)
226+
actual_count = 0
227+
for e iterator
228+
actual_count += 1
229+
a[actual_count] = e
230+
end
231+
if actual_count != length(a)
232+
throw(ArgumentError("`size`-`length` inconsistency"))
233+
end
234+
end
235+
236+
function collect_as_fsam_with_shape(
237+
iterator, ::SpecFSA{nothing,M}, shape::Tuple{Vararg{Int}},
238+
) where {M}
239+
E = eltype(iterator)::Type
240+
ret = FixedSizeArray{E,M}(undef, shape)
241+
fill_fsa_from_iterator!(ret, iterator)
242+
map(identity, ret)::FixedSizeArray{<:Any,M}
243+
end
244+
245+
function collect_as_fsam_with_shape(
246+
iterator, ::SpecFSA{E,M}, shape::Tuple{Vararg{Int}},
247+
) where {E,M}
248+
E::Type
249+
ret = FixedSizeArray{E,M}(undef, shape)
250+
fill_fsa_from_iterator!(ret, iterator)
251+
ret::FixedSizeArray{E,M}
252+
end
253+
254+
function collect_as_fsam(iterator, spec::SpecFSA{<:Any,M}) where {M}
255+
check_count_value(M)
256+
shape = if isone(M)
257+
(length(iterator),)
258+
else
259+
size(iterator)
260+
end::NTuple{M,Any}
261+
shap = map(Int, shape)::NTuple{M,Int}
262+
collect_as_fsam_with_shape(iterator, spec, shap)::FixedSizeArray{<:Any,M}
263+
end
264+
265+
function collect_as_fsa1_from_unknown_length(iterator, ::Val{nothing})
266+
v = collect(iterator)::AbstractVector
267+
T = FixedSizeVector
268+
map(identity, T(v))::T
269+
end
270+
271+
function collect_as_fsa1_from_unknown_length(iterator, ::Val{E}) where {E}
272+
E::Type
273+
v = collect(E, iterator)::AbstractVector{E}
274+
T = FixedSizeVector{E}
275+
T(v)::T
276+
end
277+
278+
function collect_as_fsa_impl(iterator, ::SpecFSA{E,0}, ::LengthIsKnown) where {E}
279+
collect_as_fsa0(iterator, Val(E))::FixedSizeArray{<:Any,0}
280+
end
281+
282+
function collect_as_fsa_impl(iterator, spec::SpecFSA, ::LengthIsKnown)
283+
collect_as_fsam(iterator, spec)::FixedSizeArray
284+
end
285+
286+
function collect_as_fsa_impl(iterator, ::SpecFSA{E,1}, ::LengthIsUnknown) where {E}
287+
collect_as_fsa1_from_unknown_length(iterator, Val(E))::FixedSizeVector
288+
end
289+
290+
function collect_as_fsa_checked(iterator, ::SpecFSA{E,nothing}, ::Val{M}, length_status) where {E,M}
291+
check_count_value(M)
292+
collect_as_fsa_impl(iterator, SpecFSA{E,M}(), length_status)::FixedSizeArray{<:Any,M}
293+
end
294+
295+
function collect_as_fsa_checked(iterator, ::SpecFSA{E,M}, ::Val{M}, length_status) where {E,M}
296+
check_count_value(M)
297+
collect_as_fsa_impl(iterator, SpecFSA{E,M}(), length_status)::FixedSizeArray{<:Any,M}
298+
end
299+
300+
"""
301+
collect_as(t::Type{<:FixedSizeArray}, iterator)
302+
303+
Tries to construct a value of type `t` from the iterator `iterator`. The type `t`
304+
must either be concrete, or a `UnionAll` without constraints.
305+
"""
306+
function collect_as(::Type{T}, iterator) where {T<:FixedSizeArray}
307+
spec = fsa_spec_from_type(T)::SpecFSA
308+
size_class = Base.IteratorSize(iterator)
309+
if size_class == Base.IsInfinite()
310+
throw(ArgumentError("iterator is infinite, can't fit infinitely many elements into a `FixedSizeArray`"))
311+
end
312+
dim_count_int = dimension_count_of(size_class)
313+
check_count_value(dim_count_int)
314+
dim_count = Val(dim_count_int)::Val
315+
len_stat = length_status(size_class)
316+
collect_as_fsa_checked(iterator, spec, dim_count, len_stat)::T
317+
end
318+
171319
end # module FixedSizeArrays

test/runtests.jl

+77
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ using OffsetArrays: OffsetArray
44
import Aqua
55

66
const checked_dims = FixedSizeArrays.checked_dims
7+
const collect_as = FixedSizeArrays.collect_as
78

89
# helpers for testing for allocation or suboptimal inference
910

@@ -353,4 +354,80 @@ end
353354
end
354355
end
355356
end
357+
358+
@testset "`collect_as`" begin
359+
for T (FixedSizeArray, FixedSizeVector, FixedSizeArray{Int}, FixedSizeVector{Int})
360+
for iterator (Iterators.repeated(7), Iterators.cycle(7))
361+
@test_throws ArgumentError collect_as(T, iterator)
362+
end
363+
end
364+
for T (FixedSizeArray{<:Any,-1}, FixedSizeArray{Int,-1}, FixedSizeArray{Int,3.1})
365+
iterator = (7:8, (7, 8))
366+
@test_throws ArgumentError collect_as(T, iterator)
367+
end
368+
for T (FixedSizeArray{3}, FixedSizeVector{3})
369+
iterator = (7:8, (7, 8))
370+
@test_throws TypeError collect_as(T, iterator)
371+
end
372+
struct Iter{E,N,I<:Integer}
373+
size::NTuple{N,I}
374+
length::I
375+
val::E
376+
end
377+
function Base.iterate(i::Iter)
378+
l = i.length
379+
iterate(i, max(zero(l), l))
380+
end
381+
function Base.iterate(i::Iter, state::Int)
382+
if iszero(state)
383+
nothing
384+
else
385+
(i.val, state - 1)
386+
end
387+
end
388+
Base.IteratorSize(::Type{<:Iter{<:Any,N}}) where {N} = Base.HasShape{N}()
389+
Base.length(i::Iter) = i.length
390+
Base.size(i::Iter) = i.size
391+
Base.eltype(::Type{<:Iter{E}}) where {E} = E
392+
@testset "buggy iterator with mismatched `size` and `length" begin
393+
for iterator (Iter((), 0, 7), Iter((3, 2), 5, 7))
394+
E = eltype(iterator)
395+
dim_count = length(size(iterator))
396+
for T (FixedSizeArray, FixedSizeArray{E}, FixedSizeArray{<:Any,dim_count}, FixedSizeArray{E,dim_count})
397+
@test_throws ArgumentError collect_as(T, iterator)
398+
end
399+
end
400+
end
401+
iterators = (
402+
(), (7,), (7, 8), 7, (7 => 8), Ref(7), fill(7),
403+
(i for i 1:3), ((i + 100*j) for i 1:3, j 1:2), Iterators.repeated(7, 2),
404+
(i for i 7:9 if i==8), 7:8, 8:7, map(BigInt, 7:8), Int[], [7], [7 8],
405+
Iter((), 1, 7), Iter((3,), 3, 7), Iter((3, 2), 6, 7),
406+
)
407+
abstract_array_params(::AbstractArray{T,N}) where {T,N} = (T, N)
408+
@testset "iterator: $iterator" for iterator iterators
409+
a = collect(iterator)
410+
(E, dim_count) = abstract_array_params(a)
411+
af = collect(Float64, iterator)
412+
@test abstract_array_params(af) == (Float64, dim_count) # meta
413+
@test_throws MethodError collect_as(FixedSizeArray{E,dim_count+1}, iterator)
414+
for T (FixedSizeArray, FixedSizeArray{<:Any,dim_count})
415+
fsa = collect_as(T, iterator)
416+
@test a == fsa
417+
@test first(abstract_array_params(fsa)) <: E
418+
end
419+
for T (FixedSizeArray{E}, FixedSizeArray{E,dim_count})
420+
test_inferred(collect_as, FixedSizeArray{E,dim_count}, (T, iterator))
421+
fsa = collect_as(T, iterator)
422+
@test a == fsa
423+
@test first(abstract_array_params(fsa)) <: E
424+
end
425+
for T (FixedSizeArray{Float64}, FixedSizeArray{Float64,dim_count})
426+
test_inferred(collect_as, FixedSizeArray{Float64,dim_count}, (T, iterator))
427+
fsa = collect_as(T, iterator)
428+
@test af == fsa
429+
@test first(abstract_array_params(fsa)) <: Float64
430+
end
431+
end
432+
end
356433
end

0 commit comments

Comments
 (0)