Unpacking a named tuple using field names and slurping?

I’ve got a named tuple with a bunch of fields and I’d like to unpack certain fields based on the key and slurp the remaining.


r = (x=1, y=2, z=3)
(y, rest...) = r

The trouble with the above is that it will unpack based on the order of fields in the named tuple rather than the keys so, the above would return

y = 1
rest = (y = 2, z = 3)

However, I’m essentially looking for something that would give me the same behaviour as the javascript code

const r  = {x: 1, y: 2, z: 3}
const {y, ...rest} = r

Where the result is

y = 2
rest = {x: 1, z: 3}

What would be the succinct way to write this in Julia?

The best I can come up with is

r = (x=1, y=2, z=3)
idx = [:y]
y = r[idx]
rest = r[InvertedIndex(idx)]

But it’s not ideal as I’ve still got some duplication with the idx and variable name.

Try this:

using Accessors

(;y) = r
rest = @delete r.y  # can use InvertedIndex here as you did, but @delete is type-stable

Don’t think it’s possible to get rid of the remaining duplication.

3 Likes

Using a quick anonymous function:

y, rest = ((;y,kwargs...)->(y,(;kwargs...)))(;r...)

But the important bit is to check compilation optimizes everything away (which I haven’t for this solution, but aplavin’s does), as this operation is mainly compile-time assignment.

1 Like

You could use a macro.

julia> macro alajs(e)
           pos=findfirst(el -> startswith(string(el),string(e.args[1].args[1])), e.args[2].args)
           e.args[2].args[pos], e.args[2].args[1] = e.args[2].args[1], e.args[2].args[pos]
           return esc(e);
       end
@alajs (macro with 1 method)

julia> @alajs (y, rest...) = (x=1, y=2, z=3)
(y = 2, x = 1, z = 3)

julia> y
2

julia> rest
(x = 1, z = 3)

If someone explains how to expand within the macro the symbol r in the namedtuple that defines it, it would also be valid for expressions like:

@alajs (y,rest...)=r

You can’t. What the macro needs to do is to generate code which at runtime can manipulate the contents of r.

But yes, it’s definitely possible to implement a macro which allows you to do

@alajs (y,rest...)=r

Combined with an earlier suggestion, but with not a shred of sanity or error checking:

macro alajs(ex)
    unpacked = ex.args[1].args[1:(end - 1)]
    rest = ex.args[1].args[end].args[1]
    rhs = ex.args[2]
    return esc(quote
        $(unpacked...), $rest = ((;$(unpacked...), $(rest)...) -> ($(unpacked...), (;($rest)...)))(;$(rhs)...)
    end)
end

Testing it:

julia> @alajs (y, z, rest...) = r
(2, 3, (x = 1,))

julia> y
2

julia> z
3

julia> rest
(x = 1,)
2 Likes
macro alajs(e)
    nms = [propertynames(eval(e.args[2]))...]
    vls=[values(eval(e.args[2]))...]
    pos=findfirst(el -> string(el)== string(e.args[1].args[1]), nms)
    nms[1],nms[pos]=nms[pos],nms[1]
    vls[1],vls[pos]=vls[pos],vls[1]
    return esc(:($(e.args[1])=$((;zip(nms,vls)...))))
end

is that okay?
I wrote the last expression tentatively, without fully understanding the syntax.

No, eval will happen in global scope when the macro is expanded, so you can’t really use that macro inside a function.

1 Like

I struggled a lot to make it work, but in the end I think it’s decent.

julia> macro alajs(ex)
           nm = ex.args[1].args[1:(end - 1)]
           rest = ex.args[1].args[end].args[1]
           r = ex.args[2]
       esc(quote
           function swap($(r))
               ($NamedTuple{$(nm...,)}($r)...,Base.structdiff($r,($NamedTuple{$(nm...,)}($r))))
           end
               $(nm...),$(rest)= swap($(r))
           end)
       end
@alajs (macro with 1 method)

julia> @alajs (y, rest...) = r
(2, (x = 1, z = 3))

julia> @alajs (z, rest...) = r
(3, (x = 1, y = 2))

julia> @alajs (z, x, rest...) = r
(3, 1, (y = 2,))

julia> @alajs (z, x,y, rest...) = r
(3, 1, 2, NamedTuple())

Compared to the other, it would have the advantage of having specific error indications for NamedTuple

julia> macro alajs(ex)
           unpacked = ex.args[1].args[1:(end - 1)]
           rest = ex.args[1].args[end].args[1]
           rhs = ex.args[2]
           return esc(quote
               $(unpacked...), $rest = ((;$(unpacked...), $(rest)...) -> ($(unpacked...), (;$rest...)))(;$(rhs)...)
           end)
       end
@alajs (macro with 1 method)

julia> @alajs (z, x,y,x, rest...) = r
ERROR: syntax: function argument name not unique: "x" around c:\Users\sprmn\.julia\environments

julia> @alajs (z, x,y,w, rest...) = r
ERROR: UndefKeywordError: keyword argument `w` not assigned

julia> macro alajs(ex)
           nm = ex.args[1].args[1:(end - 1)]
           rest = ex.args[1].args[end].args[1]
           r = ex.args[2]
       esc(quote
           function swap($(r))
               ($NamedTuple{$(nm...,)}($r)...,Base.structdiff($r,$NamedTuple{$(nm...,)}($r)))
           end
               $(nm...),$(rest)= swap($(r))
           end)
       end
@alajs (macro with 1 method)

julia> @alajs (z, x,y,w, rest...) = r
ERROR: type NamedTuple has no field w

julia> @alajs (z, x,y,x, rest...) = r
ERROR: duplicate field name in NamedTuple: "x" is not unique

1 Like
macro alajs(ex)
    nm = ex.args[1].args[1:(end - 1)]
    rest = ex.args[1].args[end].args[1]
    r = ex.args[2]
esc(quote
    function swap($(r))
        $(:dn)=setdiff(propertynames($r),$(nm))
        ($NamedTuple{$(nm...,)}($r)...,$NamedTuple{($(:dn)...,)}($r))
    end 
        $(nm...),$(rest)= swap($(r))
    end)
end
macro alajs(ex)
    nm = ex.args[1].args[1:(end - 1)]
    rest = ex.args[1].args[end].args[1]
    r = ex.args[2]
esc(quote
 $(nm...),$(rest)= $NamedTuple{$(nm...,)}($r)..., NamedTuple{filter(x -> x ∉ $nm, propertynames($r))}($r)
    end)
end

or

julia> y,z,rest=r.y,r.z, NamedTuple{(:x,)}(r)
(2, 3, (x = 1,))

The versions with swap have the following drawback:

julia> function f(r)
           @alajs (y, r...) = r
           @alajs (z, r...) = r
           return y, z
       end
WARNING: Method definition swap(Any) in module Main at REPL[1]:6 overwritten on the same line.
f (generic function with 1 method)

julia> f(r)
ERROR: type NamedTuple has no field z

The one without looks fine. At that point the tradeoff is between (somewhat) graceful errors and speed.

julia>  macro alajs(ex)
             nm = ex.args[1].args[1:(end - 1)]
             rest = ex.args[1].args[end].args[1]
             r = ex.args[2]
             fn = Symbol("swap" * "_" * join(nm,"_"))
         esc(quote
             function $(fn)($(r))
                 ($NamedTuple{$(nm...,)}($r)...,Base.structdiff($r,$NamedTuple{$(nm...,)}($r)))
             end
                 $(nm...),$(rest)= $(fn)($(r))
             end)
         end
@alajs (macro with 1 method)

julia> function f(r)
        @alajs (y, r...) = r
        @alajs (z, x, r...) = r

        return x, y, z
       end
f (generic function with 1 method)

julia> f(r)
(1, 2, 3)

all the solutions proposed here seem to have this problem.
what exactly happens?
how could it be overcome?

julia> function f(r)
        @alajs (y, r...) = r
        @alajs (z, x, r...) = r
#-----
        @alajs (y, x, r...) = r
#-----      
        return x, y, z
       end
f (generic function with 1 method)

julia> f(r)
ERROR: type NamedTuple has no field y

Print what you have left in r after each line there and it should be clear why you can’t extract the y field (or any other field) a second time.

julia> macro alajs(ex)
           nm = ex.args[1].args[1:(end - 1)]
           rest = ex.args[1].args[end].args[1]
           r = ex.args[2]
       esc(quote
        $(nm...),$(rest)= NamedTuple{$(nm...,)}($r)..., NamedTuple{filter(x -> x ∉ $nm, propertynames($r))}($r)
           end)
       end
@alajs (macro with 1 method)
# My concern was that the namedtupel r was not being consumed by the macro.
# Instead I had inadvertently put r in place of rst in the unpacking expression.
# this is the correct form of the function
julia> 
       function f(r)
        @alajs (y, rst...) = r
        @alajs (z, x, rst...) = r
        @alajs (y, x, rst...) = r
        return x, y, z
       end
f (generic function with 1 method)

julia> f(r)
(1, 2, 3)

Do you think this would be a useful addition to UnPack.jl ?