Skip to content

Commit fbd2b43

Browse files
authored
Add @pack! and @pack_fields! (#4)
* Add `@pack!` and `@pack_fields!` * Simplify file structure * A few initial tests * Fix macros and add more tests
1 parent 6be3aec commit fbd2b43

File tree

4 files changed

+299
-70
lines changed

4 files changed

+299
-70
lines changed

Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "SimpleUnPack"
22
uuid = "ce78b400-467f-4804-87d8-8f486da07d0a"
33
authors = ["David Widmann"]
4-
version = "1.0.1"
4+
version = "1.1.0"
55

66
[compat]
77
julia = "1"

README.md

+40-9
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@
44
[![Coverage](https://codecov.io/gh/devmotion/SimpleUnPack.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/devmotion/SimpleUnPack.jl)
55
[![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle)
66

7-
This package provides the `@unpack` and `@unpack_fields` macros for destructuring properties and fields, respectively.
8-
The behaviour of `@unpack` is equivalent to the destructuring that was introduced in [Julia#39285](https://github.com/JuliaLang/julia/pull/39285) and is available in Julia >= 1.7.0-DEV.364.
7+
This package provides four macros, namely
8+
9+
- `@unpack` for destructuring properties,
10+
- `@pack!` for setting properties,
11+
- `@unpack_fields` for destructuring fields,
12+
- `@pack_fields!` for setting fields.
13+
14+
`@unpack`/`@pack!` are based on `getproperty`/`setproperty` whereas `@unpack_fields`/`@pack_fields!` are based on `getfield`/`setfield!`.
15+
16+
In Julia >= 1.7.0-DEV.364, `@unpack` is expanded to the destructuring syntax that was introduced in [Julia#39285](https://github.com/JuliaLang/julia/pull/39285).
917

1018
## Examples
1119

@@ -40,7 +48,7 @@ An example with a custom struct in a function:
4048
```julia
4149
julia> using SimpleUnPack
4250

43-
julia> struct MyStruct{X,Y}
51+
julia> mutable struct MyStruct{X,Y}
4452
x::X
4553
y::Y
4654
end
@@ -55,6 +63,14 @@ julia> function Base.getproperty(m::MyStruct, p::Symbol)
5563
end
5664
end
5765

66+
julia> function Base.setproperty!(m::MyStruct, p::Symbol, v)
67+
if p === :y
68+
setfield!(m, p, 2 * v)
69+
else
70+
setfield!(m, p, v)
71+
end
72+
end
73+
5874
julia> function g(m::MyStruct)
5975
@unpack x, y = m
6076
return (; x, y)
@@ -63,22 +79,37 @@ julia> function g(m::MyStruct)
6379
julia> g(MyStruct(1.0, -5))
6480
(x = 1.0, y = 42)
6581

82+
julia> function g!(m::MyStruct, x, y)
83+
@pack! m = x, y
84+
return m
85+
end;
86+
87+
julia> g!(MyStruct(2.1, 5), 1.2, 2)
88+
MyStruct{Float64, Int64}(1.2, 4)
89+
6690
julia> function h(m::MyStruct)
6791
@unpack_fields x, y = m
6892
return (; x, y)
6993
end
7094

7195
julia> h(MyStruct(1.0, -5))
7296
(x = 1.0, y = -5)
97+
98+
julia> function h!(m::MyStruct, x, y)
99+
@pack_fields! m = x, y
100+
return m
101+
end;
102+
103+
julia> h!(MyStruct(2.1, 5), 1.2, 2)
104+
MyStruct{Float64, Int64}(1.2, 2)
73105
```
74106

75107
## Comparison with UnPack.jl
76108

77-
The syntax of `@unpack` is based on [`UnPack.@unpack`](https://github.com/mauro3/UnPack.jl).
78-
However, `UnPack.@unpack` is more flexible and based on `UnPack.unpack` instad of `getproperty`.
79-
While `UnPack.unpack` falls back to `getproperty`, it also supports `AbstractDict`s with keys of type `Symbol` and `AbstractString`, and can be specialized for other types.
80-
Since `UnPack.unpack` dispatches on `Val(property)` instances, this increased flexibility comes at the cost of increased compilation times.
109+
The syntax of `@unpack` and `@pack!` is based on `UnPack.@unpack` and `UnPack.@pack!` in [UnPack.jl](https://github.com/mauro3/UnPack.jl).
81110

82-
In contrast to SimpleUnPack, UnPack provides an `UnPack.@pack!` macro for setting properties.
111+
`UnPack.@unpack`/`UnPack.@pack!` are more flexible since they are based on `UnPack.unpack`/`UnPack.pack!` instad of `getproperty`/`setproperty!`.
112+
While `UnPack.unpack`/`UnPack.pack!` fall back to `getproperty`/`setproperty!`, they also support `AbstractDict`s with keys of type `Symbol` and `AbstractString` and can be specialized for other types.
113+
Since `UnPack.unpack` and `UnPack.pack!` dispatch on `Val(property)` instances, this increased flexibility comes at the cost of increased number of specializations and increased compilation times.
83114

84-
However, currently UnPack does not support destructuring based on `getfield` only ([UnPack#23](https://github.com/mauro3/UnPack.jl/issues/23)).
115+
In contrast to SimpleUnPack, currently UnPack does not support destructuring/updating based on `getfield`/`setfield!` only ([UnPack#23](https://github.com/mauro3/UnPack.jl/issues/23)).

src/SimpleUnPack.jl

+110-30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module SimpleUnPack
22

3-
export @unpack, @unpack_fields
3+
export @unpack, @unpack_fields, @pack!, @pack_fields!
44

55
"""
66
@unpack a, b, ... = x
@@ -9,93 +9,173 @@ Destructure properties `a`, `b`, ... of `x` into variables of the same name.
99
1010
The behaviour of the macro is equivalent to `(; a, b, ...) = x` which was introduced in [Julia#39285](https://github.com/JuliaLang/julia/pull/39285) and is available in Julia >= 1.7.0-DEV.364.
1111
12-
See also [`@unpack_fields`](@ref)
12+
See also [`@pack!`](@ref), [`@unpack_fields`](@ref), [`@pack_fields!`](@ref)
1313
"""
1414
macro unpack(args)
15-
# Extract names of properties and RHS
16-
names, rhs = split_names_rhs(:unpack, args)
15+
# Extract names of properties and object
16+
names, object = split_names_object(:unpack, args, true)
1717

1818
# Construct destructuring expression
1919
expr = if VERSION >= v"1.7.0-DEV.364"
2020
# Fall back to destructuring in Base when available:
2121
# https://github.com/JuliaLang/julia/pull/39285
22-
Expr(:(=), Expr(:tuple, Expr(:parameters, (esc(p) for p in names)...)), esc(rhs))
22+
Expr(:(=), Expr(:tuple, Expr(:parameters, (esc(p) for p in names)...)), esc(object))
2323
else
24-
destructuring_expr(:getproperty, names, rhs)
24+
destructuring_expr(:getproperty, names, object)
2525
end
2626

2727
return expr
2828
end
2929

30+
"""
31+
@pack! x = a, b, ...
32+
33+
Set properties `a`, `b`, ... of `x` to the given values.
34+
35+
See also [`@unpack`](@ref), [`@unpack_fields`](@ref), [`@pack_fields!`](@ref)
36+
"""
37+
macro pack!(args)
38+
# Extract names of properties and the object that will be updated
39+
names, object = split_names_object(:pack!, args, false)
40+
41+
# Construct updating expression
42+
expr = updating_expr(:setproperty!, object, names)
43+
44+
return expr
45+
end
46+
3047
"""
3148
@unpack_fields a, b, ... = x
3249
3350
Destructure fields `a`, `b`, ... of `x` into variables of the same name.
3451
35-
See also [`@unpack`](@ref)
52+
See also [`@pack_fields!`](@ref), [`@unpack`](@ref), [`@pack!`](@ref)
3653
"""
3754
macro unpack_fields(args)
38-
# Extract names of fields and RHS
39-
names, rhs = split_names_rhs(:unpack_fields, args)
55+
# Extract names of fields and object
56+
names, object = split_names_object(:unpack_fields, args, true)
4057

4158
# Construct destructuring expression
42-
expr = destructuring_expr(:getfield, names, rhs)
59+
expr = destructuring_expr(:getfield, names, object)
60+
61+
return expr
62+
end
63+
64+
"""
65+
@pack_fields! x = a, b, ...
66+
67+
Set fields `a`, `b`, ... of `x` to the given values.
68+
69+
See also [`@unpack_fields`](@ref), [`@unpack`](@ref), [`@pack!`](@ref)
70+
"""
71+
macro pack_fields!(args)
72+
# Extract names of properties and the object that will be updated
73+
names, object = split_names_object(:pack_fields!, args, false)
74+
75+
# Construct updating expression
76+
expr = updating_expr(:setfield!, object, names)
4377

4478
return expr
4579
end
4680

4781
"""
48-
split_names_rhs(macrosym::Symbol, expr)
82+
split_names_object(macrosym::Symbol, expr, object_on_rhs::Bool)
4983
50-
Split an expression `expr` of the form `a, b, ... = x` into a tuple consisting of a vector of symbols `a`, `b`, ..., and the right-hand side `x`.
84+
Split an expression `expr` of the form `a, b, ... = x` (if `object_on_rhs = true`) or `x = a, b, ...` (if `object_on_rhs = false`) into a tuple consisting of a vector of symbols `a`, `b`, ..., and the expression or symbol for `x`.
5185
5286
The symbol `macro_name` specifies the macro from which this function is called.
5387
54-
This function is used internally with `macrosym = :unpack` and `macrosym = :unpack_fields`.
88+
This function is used internally with `macrosym = :unpack`, `macrosym = :unpack_fields`, `macrosym = :pack!`, and `macrosym = :pack_fields!`.
5589
"""
56-
function split_names_rhs(macrosym::Symbol, expr)
90+
function split_names_object(macrosym::Symbol, expr, object_on_rhs::Bool)
91+
# Split expression in LHS and RHS
5792
if !Meta.isexpr(expr, :(=), 2)
5893
throw(
5994
ArgumentError(
60-
"`@$macrosym` can only be applied to expressions of the form `a, b, ... = x`",
95+
"`@$macrosym` can only be applied to expressions of the form " *
96+
(object_on_rhs ? "`a, b, ... = x`" : "`x = a, b, ...`"),
6197
),
6298
)
6399
end
64100
lhs, rhs = expr.args
65-
names = if lhs isa Symbol
66-
[lhs]
67-
elseif Meta.isexpr(lhs, :tuple) &&
68-
!isempty(lhs.args) &&
69-
all(x -> x isa Symbol, lhs.args)
70-
lhs.args
101+
102+
# Clean expression with keys a bit:
103+
# Remove line numbers and unwrap it from `:block` expression
104+
names_expr = object_on_rhs ? lhs : rhs
105+
Base.remove_linenums!(names_expr)
106+
if Meta.isexpr(names_expr, :block, 1)
107+
names_expr = names_expr.args[1]
108+
end
109+
110+
# Ensure that names are given as symbol or tuple of symbols,
111+
# and convert them to a vector of symbols
112+
names = if names_expr isa Symbol
113+
[names_expr]
114+
elseif Meta.isexpr(names_expr, :tuple) &&
115+
!isempty(names_expr.args) &&
116+
all(x -> x isa Symbol, names_expr.args)
117+
names_expr.args
71118
else
72119
throw(
73120
ArgumentError(
74-
"`@$macrosym` can only be applied to expressions of the form `a, b, ... = x`",
121+
"`@$macrosym` can only be applied to expressions of the form " *
122+
(object_on_rhs ? "`a, b, ... = x`" : "`x = a, b, ...`"),
75123
),
76124
)
77125
end
78-
return names, rhs
126+
127+
# Extract the object
128+
object = object_on_rhs ? rhs : lhs
129+
130+
return names, object
79131
end
80132

81133
"""
82-
destructuring_expr(fsym::Symbol, names, rhs)
134+
destructuring_expr(fsym::Symbol, names, object)
83135
84-
Return an expression that destructures `rhs` based on a function of name `fsym` and keys `names` into variables of the same `names`.
136+
Return an expression that destructures `object` based on a function of name `fsym` and keys `names` into variables of the same `names`.
85137
86138
This function is used internally with `fsym = :getproperty` and `fsym = :getfield`.
87139
"""
88-
function destructuring_expr(fsym::Symbol, names, rhs)
89-
@gensym object
140+
function destructuring_expr(fsym::Symbol, names, object)
141+
@gensym instance
90142
block = Expr(:block)
91143
for p in names
92-
push!(block.args, Expr(:(=), esc(p), Expr(:call, fsym, esc(object), QuoteNode(p))))
144+
push!(
145+
block.args, Expr(:(=), esc(p), Expr(:call, fsym, esc(instance), QuoteNode(p)))
146+
)
93147
end
94148
return Base.remove_linenums!(
95149
quote
96-
local $(esc(object)) = $(esc(rhs)) # In case the RHS is an expression
150+
local $(esc(instance)) = $(esc(object)) # In case the object is an expression
97151
$block
98-
$(esc(object)) # Return evaluation of the RHS
152+
$(esc(instance)) # Return evaluation of the object
153+
end,
154+
)
155+
end
156+
157+
"""
158+
updating_expr(fsym::Symbol, object, names)
159+
160+
Return an expression that updates keys `names` of `object` with variables of the same `names` based on a function of name `fsym`.
161+
162+
This function is used internally with `fsym = :setproperty!` and `fsym = :setfield!`.
163+
"""
164+
function updating_expr(fsym::Symbol, object, names)
165+
@gensym instance
166+
if length(names) == 1
167+
p = first(names)
168+
expr = Expr(:call, fsym, esc(instance), QuoteNode(p), esc(p))
169+
else
170+
expr = Expr(:tuple)
171+
for p in names
172+
push!(expr.args, Expr(:call, fsym, esc(instance), QuoteNode(p), esc(p)))
173+
end
174+
end
175+
return Base.remove_linenums!(
176+
quote
177+
local $(esc(instance)) = $(esc(object)) # In case the object is an expression
178+
$expr
99179
end,
100180
)
101181
end

0 commit comments

Comments
 (0)