Skip to content

Commit

Permalink
Add @pack! and @pack_fields! (#4)
Browse files Browse the repository at this point in the history
* Add `@pack!` and `@pack_fields!`

* Simplify file structure

* A few initial tests

* Fix macros and add more tests
  • Loading branch information
devmotion authored Mar 10, 2023
1 parent 6be3aec commit fbd2b43
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 70 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "SimpleUnPack"
uuid = "ce78b400-467f-4804-87d8-8f486da07d0a"
authors = ["David Widmann"]
version = "1.0.1"
version = "1.1.0"

[compat]
julia = "1"
Expand Down
49 changes: 40 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@
[![Coverage](https://codecov.io/gh/devmotion/SimpleUnPack.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/devmotion/SimpleUnPack.jl)
[![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle)

This package provides the `@unpack` and `@unpack_fields` macros for destructuring properties and fields, respectively.
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.
This package provides four macros, namely

- `@unpack` for destructuring properties,
- `@pack!` for setting properties,
- `@unpack_fields` for destructuring fields,
- `@pack_fields!` for setting fields.

`@unpack`/`@pack!` are based on `getproperty`/`setproperty` whereas `@unpack_fields`/`@pack_fields!` are based on `getfield`/`setfield!`.

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).

## Examples

Expand Down Expand Up @@ -40,7 +48,7 @@ An example with a custom struct in a function:
```julia
julia> using SimpleUnPack

julia> struct MyStruct{X,Y}
julia> mutable struct MyStruct{X,Y}
x::X
y::Y
end
Expand All @@ -55,6 +63,14 @@ julia> function Base.getproperty(m::MyStruct, p::Symbol)
end
end

julia> function Base.setproperty!(m::MyStruct, p::Symbol, v)
if p === :y
setfield!(m, p, 2 * v)
else
setfield!(m, p, v)
end
end

julia> function g(m::MyStruct)
@unpack x, y = m
return (; x, y)
Expand All @@ -63,22 +79,37 @@ julia> function g(m::MyStruct)
julia> g(MyStruct(1.0, -5))
(x = 1.0, y = 42)

julia> function g!(m::MyStruct, x, y)
@pack! m = x, y
return m
end;

julia> g!(MyStruct(2.1, 5), 1.2, 2)
MyStruct{Float64, Int64}(1.2, 4)

julia> function h(m::MyStruct)
@unpack_fields x, y = m
return (; x, y)
end

julia> h(MyStruct(1.0, -5))
(x = 1.0, y = -5)

julia> function h!(m::MyStruct, x, y)
@pack_fields! m = x, y
return m
end;

julia> h!(MyStruct(2.1, 5), 1.2, 2)
MyStruct{Float64, Int64}(1.2, 2)
```

## Comparison with UnPack.jl

The syntax of `@unpack` is based on [`UnPack.@unpack`](https://github.com/mauro3/UnPack.jl).
However, `UnPack.@unpack` is more flexible and based on `UnPack.unpack` instad of `getproperty`.
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.
Since `UnPack.unpack` dispatches on `Val(property)` instances, this increased flexibility comes at the cost of increased compilation times.
The syntax of `@unpack` and `@pack!` is based on `UnPack.@unpack` and `UnPack.@pack!` in [UnPack.jl](https://github.com/mauro3/UnPack.jl).

In contrast to SimpleUnPack, UnPack provides an `UnPack.@pack!` macro for setting properties.
`UnPack.@unpack`/`UnPack.@pack!` are more flexible since they are based on `UnPack.unpack`/`UnPack.pack!` instad of `getproperty`/`setproperty!`.
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.
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.

However, currently UnPack does not support destructuring based on `getfield` only ([UnPack#23](https://github.com/mauro3/UnPack.jl/issues/23)).
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)).
140 changes: 110 additions & 30 deletions src/SimpleUnPack.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module SimpleUnPack

export @unpack, @unpack_fields
export @unpack, @unpack_fields, @pack!, @pack_fields!

"""
@unpack a, b, ... = x
Expand All @@ -9,93 +9,173 @@ Destructure properties `a`, `b`, ... of `x` into variables of the same name.
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.
See also [`@unpack_fields`](@ref)
See also [`@pack!`](@ref), [`@unpack_fields`](@ref), [`@pack_fields!`](@ref)
"""
macro unpack(args)
# Extract names of properties and RHS
names, rhs = split_names_rhs(:unpack, args)
# Extract names of properties and object
names, object = split_names_object(:unpack, args, true)

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

return expr
end

"""
@pack! x = a, b, ...
Set properties `a`, `b`, ... of `x` to the given values.
See also [`@unpack`](@ref), [`@unpack_fields`](@ref), [`@pack_fields!`](@ref)
"""
macro pack!(args)
# Extract names of properties and the object that will be updated
names, object = split_names_object(:pack!, args, false)

# Construct updating expression
expr = updating_expr(:setproperty!, object, names)

return expr
end

"""
@unpack_fields a, b, ... = x
Destructure fields `a`, `b`, ... of `x` into variables of the same name.
See also [`@unpack`](@ref)
See also [`@pack_fields!`](@ref), [`@unpack`](@ref), [`@pack!`](@ref)
"""
macro unpack_fields(args)
# Extract names of fields and RHS
names, rhs = split_names_rhs(:unpack_fields, args)
# Extract names of fields and object
names, object = split_names_object(:unpack_fields, args, true)

# Construct destructuring expression
expr = destructuring_expr(:getfield, names, rhs)
expr = destructuring_expr(:getfield, names, object)

return expr
end

"""
@pack_fields! x = a, b, ...
Set fields `a`, `b`, ... of `x` to the given values.
See also [`@unpack_fields`](@ref), [`@unpack`](@ref), [`@pack!`](@ref)
"""
macro pack_fields!(args)
# Extract names of properties and the object that will be updated
names, object = split_names_object(:pack_fields!, args, false)

# Construct updating expression
expr = updating_expr(:setfield!, object, names)

return expr
end

"""
split_names_rhs(macrosym::Symbol, expr)
split_names_object(macrosym::Symbol, expr, object_on_rhs::Bool)
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`.
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`.
The symbol `macro_name` specifies the macro from which this function is called.
This function is used internally with `macrosym = :unpack` and `macrosym = :unpack_fields`.
This function is used internally with `macrosym = :unpack`, `macrosym = :unpack_fields`, `macrosym = :pack!`, and `macrosym = :pack_fields!`.
"""
function split_names_rhs(macrosym::Symbol, expr)
function split_names_object(macrosym::Symbol, expr, object_on_rhs::Bool)
# Split expression in LHS and RHS
if !Meta.isexpr(expr, :(=), 2)
throw(
ArgumentError(
"`@$macrosym` can only be applied to expressions of the form `a, b, ... = x`",
"`@$macrosym` can only be applied to expressions of the form " *
(object_on_rhs ? "`a, b, ... = x`" : "`x = a, b, ...`"),
),
)
end
lhs, rhs = expr.args
names = if lhs isa Symbol
[lhs]
elseif Meta.isexpr(lhs, :tuple) &&
!isempty(lhs.args) &&
all(x -> x isa Symbol, lhs.args)
lhs.args

# Clean expression with keys a bit:
# Remove line numbers and unwrap it from `:block` expression
names_expr = object_on_rhs ? lhs : rhs
Base.remove_linenums!(names_expr)
if Meta.isexpr(names_expr, :block, 1)
names_expr = names_expr.args[1]
end

# Ensure that names are given as symbol or tuple of symbols,
# and convert them to a vector of symbols
names = if names_expr isa Symbol
[names_expr]
elseif Meta.isexpr(names_expr, :tuple) &&
!isempty(names_expr.args) &&
all(x -> x isa Symbol, names_expr.args)
names_expr.args
else
throw(
ArgumentError(
"`@$macrosym` can only be applied to expressions of the form `a, b, ... = x`",
"`@$macrosym` can only be applied to expressions of the form " *
(object_on_rhs ? "`a, b, ... = x`" : "`x = a, b, ...`"),
),
)
end
return names, rhs

# Extract the object
object = object_on_rhs ? rhs : lhs

return names, object
end

"""
destructuring_expr(fsym::Symbol, names, rhs)
destructuring_expr(fsym::Symbol, names, object)
Return an expression that destructures `rhs` based on a function of name `fsym` and keys `names` into variables of the same `names`.
Return an expression that destructures `object` based on a function of name `fsym` and keys `names` into variables of the same `names`.
This function is used internally with `fsym = :getproperty` and `fsym = :getfield`.
"""
function destructuring_expr(fsym::Symbol, names, rhs)
@gensym object
function destructuring_expr(fsym::Symbol, names, object)
@gensym instance
block = Expr(:block)
for p in names
push!(block.args, Expr(:(=), esc(p), Expr(:call, fsym, esc(object), QuoteNode(p))))
push!(
block.args, Expr(:(=), esc(p), Expr(:call, fsym, esc(instance), QuoteNode(p)))
)
end
return Base.remove_linenums!(
quote
local $(esc(object)) = $(esc(rhs)) # In case the RHS is an expression
local $(esc(instance)) = $(esc(object)) # In case the object is an expression
$block
$(esc(object)) # Return evaluation of the RHS
$(esc(instance)) # Return evaluation of the object
end,
)
end

"""
updating_expr(fsym::Symbol, object, names)
Return an expression that updates keys `names` of `object` with variables of the same `names` based on a function of name `fsym`.
This function is used internally with `fsym = :setproperty!` and `fsym = :setfield!`.
"""
function updating_expr(fsym::Symbol, object, names)
@gensym instance
if length(names) == 1
p = first(names)
expr = Expr(:call, fsym, esc(instance), QuoteNode(p), esc(p))
else
expr = Expr(:tuple)
for p in names
push!(expr.args, Expr(:call, fsym, esc(instance), QuoteNode(p), esc(p)))
end
end
return Base.remove_linenums!(
quote
local $(esc(instance)) = $(esc(object)) # In case the object is an expression
$expr
end,
)
end
Expand Down
Loading

2 comments on commit fbd2b43

@devmotion
Copy link
Owner Author

Choose a reason for hiding this comment

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

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

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

Registration pull request created: JuliaRegistries/General/79330

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v1.1.0 -m "<description of version>" fbd2b43380f1953f50bb1419fae359e36e99b542
git push origin v1.1.0

Please sign in to comment.