Destructuring & setindex!

Tuple destructuring works even if the variables on the LHS are x[i...] expressions, eg

g(y) = y, 2 * y

function foo!(x1, x2, y)
    for i in eachindex(y)
        x1[i], x2[i] = g(y[i])
    end
    x1, x2
end

x1 = zeros(3)
x2 = zeros(3)
foo!(x1, x2, 1:3)

as opposed to writing out

function foo!(x1, x2, y)
    for i in eachindex(y)
        z = g(y[i])
        x1[i] = z[1]
        x2[i] = z[2]
    end
    x1, x2
end

but I am wondering where this is documented. Or does this follow logically from something else?

3 Likes

I do not know where exactly this is documented, but I often set multiple variables on the LHS with a tuple or even a vector. It’s super neat.

It would be great to have this clarified/documented. Maybe submit a PR for the docs?

I would prefer to leave this to someone more familiar with lowering, as it appears to be recursive. Eg

julia> Meta.@lower a[i][j], b[k] = (2, 3)
:($(Expr(:thunk, CodeInfo(
    @ none within `top-level scope'
1 ─ %1 = 2
│   %2 = 3
│   %3 = Base.getindex(a, i)
│        Base.setindex!(%3, %1, j)
│        Base.setindex!(b, %2, k)
│   %6 = Core.tuple(%1, %2)
└──      return %6
))))

and the exact form is important for macros.

This is one of those features that “just works”, and users don’t even need to think about them, so the documentation is more for understanding the AST and confirming that this is something one can rely on.

This is one of those features that “just works”.

Right, but it feels risky to use an undocumented feature (maybe it’s a bug…).

My experience with filing a PR for the docs is that the core team takes it as a starting point. If you prefer, I could write up something simple, for instance, by borrowing your example (a[i][j], b[k]) = (2, 3). Maybe that would trigger someone to add details.

3 Likes

I discovered this just a few weeks ago myself when I had an occasion to update two refs at the same time like this. I wasn’t sure if it would do what I wanted so I checked the lowering and was happy to discover that it did.

It’s even recursive into nested tuples.

julia> Meta.@lower a.x, b[], (c.y, d[]) = (1,2, (3,4))
:($(Expr(:thunk, CodeInfo(
...
│         (Base.setproperty!)(a, :x, %12)
│         (Base.setindex!)(b, %3)
...
│         (Base.setproperty!)(c, :y, %24)
...
│         (Base.setindex!)(d, %28)

Even more obliquely, it’s even allowed to define limited functions this way:

julia> f(), g(), x+y = 1:3
1:3

julia> f()
1

julia> g()
2

julia> 1+1
3

Of course, we cannot do anything more interesting than constant-valued functions since the RHS is an immediately-evaluated value and not a function body.

Looking at the lisp, it looks like tuple destructuring is implemented exactly as a loop that generates the same code as a sequence of assignments for each thing on the LHS while iterating over the RHS. And I think it’s something you can count on as it’d be an error otherwise. Let’s document it!

4 Likes

I think it would be best to start with an issue (I could not find one) and ask for guidance there. I can imagine two places for this in the docs:

  1. in multiple return values, explaining this feature to the user, with an example,
  2. in the dev docs, documenting the AST and the rules (which appear to be “anything that even remotely makes sense goes”, as @mbauman pointed out).

An issue first would help clarify what of this is intended, and what isn’t. Also, the two points above would not need to be addressed in the same PR — I think I would leave 2. for core devs.

Recently I’ve been writing a massive pile of tests for lowering which can partially serve the need of documenting the intent of lowering without having to refer to the code itself. Not everything is covered yet, but nested destructuring is:

2 Likes

I filed an issue, 32547 and got a clear reply that

A[1][1], A[2][2], b[3] = (1, 2, 3)

and

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

are both intended.

I guess we can go ahead with filing a doc PR. @Tamas_Papp, would you care to take the lead?

1 Like

Yes, will make a PR.

2 Likes