33
44Extract all parts of `obj` that are selected by `optic`.
55Returns a flat `Tuple` of values, or an `AbstractVector` if the selected parts contain arrays.
6- This function is experimental and we might change the precise output container in future.
6+
7+ This function is experimental and we might change the precise output container in the future.
8+
9+ See also [`setall`](@ref).
710
811
912```jldoctest
@@ -20,6 +23,34 @@ julia> getall(obj, @optic _ |> Elements() |> last)
2023"""
2124function getall end
2225
26+ """
27+ setall(obj, optic, values)
28+
29+ Replace a part of `obj` that is selected by `optic` with `values`.
30+ The `values` collection should have the same number of elements as selected by `optic`.
31+
32+ This function is experimental and might change in the future.
33+
34+ See also [`getall`](@ref), [`set`](@ref). The former is dual to `setall`:
35+
36+ ```jldoctest
37+ julia> using Accessors
38+
39+ julia> obj = (a=1, b=(2, 3));
40+
41+ julia> optic = @optic _ |> Elements() |> last;
42+
43+ julia> getall(obj, optic)
44+ (1, 3)
45+
46+ julia> setall(obj, optic, (4, 5))
47+ (a = 4, b = (2, 5))
48+ ```
49+ """
50+ function setall end
51+
52+ # implementations for individual noncomposite optics
53+
2354getall (obj:: Union{Tuple, AbstractVector} , :: Elements ) = obj
2455getall (obj:: Union{NamedTuple} , :: Elements ) = values (obj)
2556getall (obj:: AbstractArray , :: Elements ) = vec (obj)
@@ -29,17 +60,63 @@ getall(obj, ::Properties) = getproperties(obj) |> values
2960getall (obj, o:: If ) = o. modify_condition (obj) ? (obj,) : ()
3061getall (obj, f) = (f (obj),)
3162
63+ function setall (obj, :: Properties , vs)
64+ names = propertynames (obj)
65+ setproperties (obj, NamedTuple {names} (NTuple {length(names)} (vs)))
66+ end
67+ setall (obj:: NamedTuple{NS} , :: Elements , vs) where {NS} = NamedTuple {NS} (NTuple {length(NS)} (vs))
68+ setall (obj:: NTuple{N, Any} , :: Elements , vs) where {N} = (@assert length (vs) == N; NTuple {N} (vs))
69+ setall (obj:: AbstractArray , :: Elements , vs:: AbstractArray ) = (@assert length (obj) == length (vs); reshape (vs, size (obj)))
70+ setall (obj:: AbstractArray , :: Elements , vs) = setall (obj, Elements (), collect (vs))
71+ setall (obj, o:: If , vs) = error (" Not supported" )
72+ setall (obj, o, vs) = set (obj, o, only (vs))
73+
74+
75+ # implementations for composite optics
76+
77+ # A straightforward recursive approach doesn't actually infer,
78+ # see https://github.com/JuliaObjects/Accessors.jl/pull/64 and https://github.com/JuliaObjects/Accessors.jl/pull/68.
79+ # Instead, we need to generate separate functions for each recursion level.
3280
33- # A recursive implementation of getall doesn't actually infer,
34- # see https://github.com/JuliaObjects/Accessors.jl/pull/64.
35- # Instead, we need to generate unrolled code explicitly.
3681function getall (obj, optic:: ComposedFunction )
3782 N = length (decompose (optic))
38- _GetAll {N} ()(obj, optic)
83+ _getall (obj, optic, Val (N))
84+ end
85+
86+ function setall (obj, optic:: ComposedFunction , vs)
87+ N = length (decompose (optic))
88+ vss = to_nested_shape (vs, Val (getall_lengths (obj, optic, Val (N))), Val (N))
89+ _setall (obj, optic, vss, Val (N))
90+ end
91+
92+
93+ # _getall: the actual workhorse for getall
94+ _getall (_, _, :: Val{N} ) where {N} = error (" Too many chained optics: $N is not supported for now. See also https://github.com/JuliaObjects/Accessors.jl/pull/64." )
95+ _getall (obj, optic, :: Val{1} ) = getall (obj, optic)
96+ for i in 2 : 10
97+ @eval function _getall (obj, optic, :: Val{$i} )
98+ _reduce_concat (
99+ map (getall (obj, optic. inner)) do obj
100+ _getall (obj, optic. outer, Val ($ (i- 1 )))
101+ end
102+ )
103+ end
104+ end
105+
106+ # _setall: the actual workhorse for setall
107+ # takes values as a nested tuple with proper leaf lengths, prepared in setall above
108+ _setall (_, _, _, :: Val{N} ) where {N} = error (" Too many chained optics: $N is not supported for now. See also https://github.com/JuliaObjects/Accessors.jl/pull/68." )
109+ _setall (obj, optic, vs, :: Val{1} ) = setall (obj, optic, vs)
110+ for i in 2 : 10
111+ @eval function _setall (obj, optic, vs, :: Val{$i} )
112+ setall (obj, optic. inner, map (getall (obj, optic. inner), vs) do obj, vss
113+ _setall (obj, optic. outer, vss, Val ($ (i - 1 )))
114+ end )
115+ end
39116end
40117
41- struct _GetAll{N} end
42- ( :: _GetAll{N} )(_) where {N} = error ( " Too many chained optics: $N is not supported for now. See also https://github.com/JuliaObjects/Accessors.jl/pull/64. " )
118+
119+ # helper functions
43120
44121_concat (a:: Tuple , b:: Tuple ) = (a... , b... )
45122_concat (a:: Tuple , b:: AbstractVector ) = vcat (collect (a), b)
@@ -51,26 +128,48 @@ _reduce_concat(xs::AbstractVector) = reduce(append!, xs; init=eltype(eltype(xs))
51128_reduce_concat (xs:: Tuple{AbstractVector, Vararg{AbstractVector}} ) = reduce (vcat, xs)
52129_reduce_concat (xs:: AbstractVector{<:AbstractVector} ) = reduce (vcat, xs)
53130
54- function _generate_getall (N:: Int )
55- syms = [Symbol (:f_ , i) for i in 1 : N]
56-
57- expr = :( getall (obj, $ (syms[end ])) )
58- for s in syms[1 : end - 1 ] |> reverse
59- expr = :(
60- _reduce_concat (
61- map (getall (obj, $ (s))) do obj
62- $ expr
63- end
64- )
65- )
66- end
131+ _staticlength (:: NTuple{N, <:Any} ) where {N} = Val (N)
132+ _staticlength (x:: AbstractVector ) = length (x)
67133
68- :(function (:: _GetAll{$N} )(obj, optic)
69- ($ (syms... ),) = deopcompose (optic)
70- $ expr
71- end )
134+ getall_lengths (obj, optic, :: Val{1} ) = _staticlength (getall (obj, optic))
135+ for i in 2 : 10
136+ @eval function getall_lengths (obj, optic, :: Val{$i} )
137+ # convert to Tuple: vectors cannot be put into Val
138+ map (getall (obj, optic. inner) |> Tuple) do o
139+ getall_lengths (o, optic. outer, Val ($ (i - 1 )))
140+ end
141+ end
72142end
73143
144+ _val (N:: Int ) = N
145+ _val (:: Val{N} ) where {N} = N
146+
147+ nestedsum (ls:: Int ) = ls
148+ nestedsum (ls:: Val ) = ls
149+ nestedsum (ls:: Tuple ) = sum (_val ∘ nestedsum, ls)
150+
151+ # to_nested_shape() definition uses both @eval and @generated
152+ #
153+ # @eval is needed because the code for different recursion depths should be different for inference,
154+ # not the same method with different parameters.
155+ #
156+ # @generated is used to unpack target lengths from the second argument at compile time to make to_nested_shape() as cheap as possible.
157+ #
158+ # Note: to_nested_shape() only operates on plain Julia types and won't be affected by user lens definition, unlike setall for example.
159+ # That's why it's safe to make it @generated.
160+ to_nested_shape (vs, :: Val{LS} , :: Val{1} ) where {LS} = (@assert length (vs) == _val (LS); vs)
74161for i in 2 : 10
75- eval (_generate_getall (i))
162+ @eval @generated function to_nested_shape (vs, ls:: Val{LS} , :: Val{$i} ) where {LS}
163+ vi = 1
164+ subs = map (LS) do lss
165+ n = nestedsum (lss)
166+ elems = map (vi: vi+ _val (n)- 1 ) do j
167+ :( vs[$ j] )
168+ end
169+ res = :( to_nested_shape (($ (elems... ),), $ (Val (lss)), $ (Val ($ (i - 1 )))) )
170+ vi += _val (n)
171+ res
172+ end
173+ :( ($ (subs... ),) )
174+ end
76175end
0 commit comments