Skip to content

Commit 1c447d6

Browse files
tkfPietro Vertechi
authored and
Pietro Vertechi
committed
Add StructArrays.append!! (JuliaArrays#97)
* Expose grow_to_structarray! as a public API * Rename: collect_to_structarray! -> _collect_to_structarray! * Add collect_to_structarray!(dest, itr) * Rename: collect_to_structarray! -> append!! * Mention append!! in README
1 parent 7d399b1 commit 1c447d6

File tree

3 files changed

+97
-1
lines changed

3 files changed

+97
-1
lines changed

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,40 @@ end
211211
s = [MyType(rand(), a=1, b=2) for i in 1:10]
212212
StructArray(s)
213213
```
214+
215+
## Advanced: mutate-or-widen style accumulation
216+
217+
StructArrays provides a function `StructArrays.append!!(dest, src)` (unexported) for "mutate-or-widen" style accumulation. This function can be used via [`BangBang.append!!`](https://tkf.github.io/BangBang.jl/dev/#BangBang.append!!-Tuple{Any,Any}) and [`BangBang.push!!`](https://tkf.github.io/BangBang.jl/dev/#BangBang.push!!-Tuple{Any,Any,Any,Vararg{Any,N}%20where%20N}) as well.
218+
219+
`StructArrays.append!!` works like `append!(dest, src)` if `dest` can contain all element types in `src` iterator; i.e., it _mutates_ `dest` in-place:
220+
221+
```julia
222+
julia> dest = StructVector((a=[1], b=[2]))
223+
1-element StructArray(::Array{Int64,1}, ::Array{Int64,1}) with eltype NamedTuple{(:a, :b),Tuple{Int64,Int64}}:
224+
(a = 1, b = 2)
225+
226+
julia> StructArrays.append!!(dest, [(a = 3, b = 4)])
227+
2-element StructArray(::Array{Int64,1}, ::Array{Int64,1}) with eltype NamedTuple{(:a, :b),Tuple{Int64,Int64}}:
228+
(a = 1, b = 2)
229+
(a = 3, b = 4)
230+
231+
julia> ans === dest
232+
true
233+
```
234+
235+
Unlike `append!`, `append!!` can also _widen_ element type of `dest` array element types:
236+
237+
```julia
238+
julia> StructArrays.append!!(dest, [(a = missing, b = 6)])
239+
3-element StructArray(::Array{Union{Missing, Int64},1}, ::Array{Int64,1}) with eltype NamedTuple{(:a, :b),Tuple{Union{Missing, Int64},Int64}}:
240+
NamedTuple{(:a, :b),Tuple{Union{Missing, Int64},Int64}}((1, 2))
241+
NamedTuple{(:a, :b),Tuple{Union{Missing, Int64},Int64}}((3, 4))
242+
NamedTuple{(:a, :b),Tuple{Union{Missing, Int64},Int64}}((missing, 6))
243+
244+
julia> ans === dest
245+
false
246+
```
247+
248+
Since the original array `dest` cannot hold the input, a new array is created (`ans !== dest`).
249+
250+
Combined with [function barriers](https://docs.julialang.org/en/latest/manual/performance-tips/#kernel-functions-1), `append!!` is a useful building block for implementing `collect`-like functions.

src/collect.jl

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,41 @@ function widenarray(dest::AbstractArray, i, ::Type{T}) where T
128128
copyto!(new, 1, dest, 1, i-1)
129129
new
130130
end
131+
132+
"""
133+
`append!!(dest, itr) -> dest′`
134+
135+
Try to append `itr` into a vector `dest`. Widen element type of
136+
`dest` if it cannot hold the elements of `itr`. That is to say,
137+
138+
```julia
139+
vcat(dest, StructVector(itr)) == append!!(dest, itr)
140+
```
141+
142+
holds. Note that `dest′` may or may not be the same object as `dest`.
143+
The state of `dest` is unpredictable after `append!!`
144+
is called (e.g., it may contain just half of the elements from `itr`).
145+
"""
146+
append!!(dest::AbstractVector, itr) =
147+
_append!!(dest, itr, Base.IteratorSize(itr))
148+
149+
function _append!!(dest::AbstractVector, itr, ::Union{Base.HasShape, Base.HasLength})
150+
n = length(itr) # itr may be stateful so do this first
151+
fr = iterate(itr)
152+
fr === nothing && return dest
153+
el, st = fr
154+
i = lastindex(dest) + 1
155+
if iscompatible(el, dest)
156+
resize!(dest, length(dest) + n)
157+
@inbounds dest[i] = el
158+
return collect_to_structarray!(dest, itr, i + 1, st)
159+
else
160+
new = widenstructarray(dest, i, el)
161+
resize!(new, length(dest) + n)
162+
@inbounds new[i] = el
163+
return collect_to_structarray!(new, itr, i + 1, st)
164+
end
165+
end
166+
167+
_append!!(dest::AbstractVector, itr, ::Base.SizeUnknown) =
168+
grow_to_structarray!(dest, itr)

test/runtests.jl

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using StructArrays
2-
using StructArrays: staticschema, iscompatible, _promote_typejoin
2+
using StructArrays: staticschema, iscompatible, _promote_typejoin, append!!
33
using OffsetArrays: OffsetArray
44
import Tables, PooledArrays, WeakRefStrings
55
using Test
@@ -646,3 +646,24 @@ end
646646
str = String(take!(io))
647647
@test str == "StructArray(::Array{Int64,1}, ::Array{Int64,1})"
648648
end
649+
650+
@testset "append!!" begin
651+
dest_examples = [
652+
("mutate", StructVector(a = [1], b = [2])),
653+
("widen", StructVector(a = [1], b = [nothing])),
654+
]
655+
itr = [(a = 1, b = 2), (a = 1, b = 2), (a = 1, b = 12)]
656+
itr_examples = [
657+
("HasLength", () -> itr),
658+
("SizeUnknown", () -> (x for x in itr if isodd(x.a))),
659+
# Broken due to https://github.com/JuliaArrays/StructArrays.jl/issues/100:
660+
# ("empty", (x for x in itr if false)),
661+
# Broken due to https://github.com/JuliaArrays/StructArrays.jl/issues/99:
662+
# ("stateful", () -> Iterators.Stateful(itr)),
663+
]
664+
@testset "$destlabel $itrlabel" for (destlabel, dest) in dest_examples,
665+
(itrlabel, makeitr) in itr_examples
666+
667+
@test vcat(dest, StructVector(makeitr())) == append!!(copy(dest), makeitr())
668+
end
669+
end

0 commit comments

Comments
 (0)