3
votes

In Python, one can use the * operator in the unpacking of an iterable.

In [1]: head, *tail = [1, 2, 3, 4, 5]

In [2]: head
Out[2]: 1

In [3]: tail
Out[3]: [2, 3, 4, 5]

I would like to produce the same behavior in Julia. I figured that the equivalent ... operator would work, but it seems to just produce an error in this context.

julia> head, tail... = [1, 2, 3, 4, 5]
ERROR: syntax: invalid assignment location "tail..."

I was able to produce the results I want using the following, but this is an ugly solution.

julia> head, tail = A[1], A[2:end]
(1,[2,3,4,5])

Can I unpack the array such that tail would contain the rest of the items after head using the splat (...) operator? If not, what is the cleanest alternative?


Edit: This feature has been proposed in #2626. It looks like it will be part of the 1.0 release.

2
I don't think head, tail = A[1], A[2:end] is ugly, it explicitly tells what head and tail are. if A has no potential usage, using head, tail = shift!(A), A is a little bit more efficient. - Gnimuc
@Gnimuc It becomes increasingly ugly as more items need to be unpacked. a,b,c,*d = [1,2,3,4,5] is much cleaner than using lots of indexing or shift!ing, IMHO. :) - Harrison Grodin
This sounds like a job for a macro. - Chris Rackauckas
yeah, indeed, in that case, you could use (a,b,c), d = splice!(A,1:3), A as a workaround. - Gnimuc
This feature is not definitely going to be in 1.0 – the milestone label means that it will be considered for inclusion. As that issue indicates, there are still some design considerations to be worked out and, of course, someone needs to actually implement it. - StefanKarpinski

2 Answers

2
votes

That does indeed sound like a job for a macro:

function unpack(lhs, rhs) 
    len = length(lhs.args)
    if len == 1
        # just remove the splatting
        l, is_splat = remove_splat(lhs.args[1])
        return :($l = $(esc(rhs)))
    else
        new_lhs = :()
        new_rhs = quote 
            tmp = $(esc(rhs))
            $(Expr(:tuple)) 
        end
        splatted = false
        for (i, e) in enumerate(lhs.args)
            l, is_splat = remove_splat(e)
            if is_splat
                splatted && error("Only one splatting operation allowed on lhs")
                splatted = true
                r = :(tmp[$i:end-$(len-i)])
            elseif splatted
                r = :(tmp[end-$(len-i)])
            else
                r = :(tmp[$i])
            end
            push!(new_lhs.args, l)
            push!(new_rhs.args[4].args, r)
        end
        return :($new_lhs =  $new_rhs)
    end
end

remove_splat(e::Symbol) = esc(e),  false

function remove_splat(e::Expr)
    if e.head == :(...)
        return esc(e.args[1]), true
    else
        return esc(e), false
    end
end

macro unpack(expr)
    if Meta.isexpr(expr, :(=))
        if Meta.isexpr(expr.args[1], :tuple)
            return unpack(expr.args[1], expr.args[2])
        else
            return unpack(:(($(expr.args[1]),)), expr.args[2])
        end
    else
        error("Cannot parse expression")
    end
end

It is not very well tested, but basic things work:

julia> @unpack head, tail... = [1,2,3,4]
(1,[2,3,4])

julia> @unpack head, middle..., tail = [1,2,3,4,5]
(1,[2,3,4],5)

A few Julia gotchas:

x,y = [1,2,3] #=> x = 1, y = 2

a = rand(3)
a[1:3], y = [1,2,3] #=> a = [1.0,1.0,1.0], y = 2

The macro follows this behavior

@unpack a[1:3], y... = [1,2,3]
#=> a=[1.0,1.0,1.0], y=[2,3]
0
votes

As of Julia 1.6

It is now possible to use ... on the left-hand side of destructured assignments for taking any number of items from the front of an iterable collection, while also collecting the rest.

Example of assigning the first two items while slurping the rest:

julia> a, b, c... = [4, 8, 15, 16, 23, 42]
# 6-element Vector{Int64}:
#   4
#   8
#  15
#  16
#  23
#  42

julia> a
# 4

julia> b
# 8

julia> c
# 4-element Vector{Int64}:
#  15
#  16
#  23
#  42

This syntax is implemented using Base.rest, which can be overloaded to customize its behavior.

Example of overloading Base.rest(s::Union{String, SubString{String}}, i::Int) to slurp a Vector{Char} instead of the default SubString:

julia> a, b... = "hello"
julia> b
# "ello"

julia> Base.rest(s::Union{String, SubString{String}}, i=1) = collect(SubString(s, i))
julia> a, b... = "hello"
julia> b
# 4-element Vector{Char}:
#  'e': ASCII/Unicode U+0065 (category Ll: Letter, lowercase)
#  'l': ASCII/Unicode U+006C (category Ll: Letter, lowercase)
#  'l': ASCII/Unicode U+006C (category Ll: Letter, lowercase)
#  'o': ASCII/Unicode U+006F (category Ll: Letter, lowercase)