Destructuring syntax in julia 1.7

I’m aware of that logic and I agree that it is quite clever. My worry is mainly that it is a bit too clever.

Playing along with these ideas, one could argue that (1; a=2) should be a mixed positional and named tuple. Instead, it’s a shorthand begin ... end block equivalent to simply a = 2. Similarly, one might expect the following to work:

julia> (first_entry; diag) = Diagonal([1,2])
ERROR: syntax: invalid assignment location "begin
    first_entry
    # REPL[10], line 1
    diag
end" around REPL[10]:1

those sound like nice features to me. If you write the PR, I’ll give it a thumbs up :grinning_face_with_smiling_eyes:

Ref https://github.com/ararslan/FrankenTuples.jl

8 Likes

The (expr1; expr2) syntax for a block long predates named tuples. You’re right that if we’d had foreseen that it would eventually make sense as a frakentuple syntax, then we might have reserved the syntax for that. Of course, I’m not sure if frankentuples will even be added to the language, let alone need a syntax, so for now the fact that this syntax means something else seems fine. Syntax collisions happen and when they do, you just have to pick one of the possible meanings (or error because it’s ambiguous, but that’s kind of annoying).

7 Likes

A nice but missing close feature AFAIK, will be to convert a tuple to a namedtuple given some names.

Suppose one needs to compute something upon the datum v = (1, 2, 3)
But let suppose again that we prefer to handle it thru syntax v.x * v.x - v.y * v.y rather than v[1] * v[1] - v[2] * v[2]

Today, one can do the following

v = NamedTuple((:x,:y,:z) .=> v) # a bit awkward

or

v = (; ((:x,:y,:z) .=> v)...) # a bit very awkward

but not

v = (; (:x,:y,:z) .=> v) # for example

to finally get

using Test
@test v.x * v.x - v.y * v.y == -3

Some syntax may be alleviated there…

just:

vx, vy, vz = v

??? and vx is easier to type than v.x… and we don’t create type instability Tuple => NamedTuple

This is an example. You will not be able to flatten everything everywhere with only oneliners.
Think to a function call with some args, tuples or not; a body with many if branches.
Oneliners will not scale

Type can be infer very simply.

r i g h t


(and it doesn’t go away even if you use g = NamedTuple...:

│   %5 = Base.getproperty(g, :x)::Any
│   %6 = Base.getproperty(g, :y)::Any
│   %7 = (%5 + %6)::Any

you’re still typing :x ,:y, I don’t see how that’s easier than vx, vx = ; what would be additionally useful is keyword destruction with renaming, but that one will have counterintuitive syntax at first glance so probably won’t be in Base in a while

This is a syntaxic form proposal. Do not erase too much things !

# lowering wo excessive erasure
_2nt(v::Tuple{Int,Int,Int}) =
    NamedTuple( (:x,:y,:z) .=> v) :: @NamedTuple{x::Int,y::Int,z::Int}
@code_warntype _2nt((1,2,3))

Body::NamedTuple{(:x, :y, :z), Tuple{Int64, Int64, Int64}} # good
1 ─ %1 = (:x, :y, :z)::Core.Const((:x, :y, :z))
│   %2 = Base.broadcasted(Main.:(=>), %1, v)::Core.PartialStruct(Base.Broadcast.Broadcasted{Base.Broadcast.Style{Tuple}, Nothing, Type{Pair}, Tuple{Tuple{Symbol, Symbol, Symbol}, Tuple{Int64, Int64, Int64}}}, Any[Core.Const(Pair), Core.PartialStruct(Tuple{Tuple{Symbol, Symbol, Symbol}, Tuple{Int64, Int64, Int64}}, Any[Core.Const((:x, :y, :z)), Tuple{Int64, Int64, Int64}]), Core.Const(nothing)])
│   %3 = Base.materialize(%2)::Core.PartialStruct(Tuple{Pair{Symbol, Int64}, Pair{Symbol, Int64}, Pair{Symbol, Int64}}, Any[Core.PartialStruct(Pair{Symbol, Int64}, Any[Core.Const(:x), Int64]), Core.PartialStruct(Pair{Symbol, Int64}, Any[Core.Const(:y), Int64]), Core.PartialStruct(Pair{Symbol, Int64}, Any[Core.Const(:z), Int64])])
│   %4 = Main.NamedTuple(%3)::NamedTuple
│   %5 = (:x, :y, :z)::Core.Const((:x, :y, :z))
│   %6 = Core.apply_type(Base.Tuple, Main.Int, Main.Int, Main.Int)::Core.Const(Tuple{Int64, Int64, Int64})
│   %7 = Core.apply_type(Base.NamedTuple, %5, %6)::Core.Const(NamedTuple{(:x, :y, :z), Tuple{Int64, Int64, Int64}})
│   %8 = Core.typeassert(%4, %7)::NamedTuple{(:x, :y, :z), Tuple{Int64, Int64, Int64}}
└──      return %8

some calc

# inference is out of scope from desugaring from there
nt = _2nt((1,2,3))
nt.x + nt.y


No comment about other point, my first remark was clear enough.

Isn’t it just NamedTuple{(:x,:y,:z)}(v) ?

4 Likes

@aplavin close …

> ant = (a=2, n=6, t=11)
(a = 2, n = 6, t = 11)

> tan = NamedTuple{(:t, :a, :n)}( values(ant) )
(t = 2, a = 6, n = 11)

What exactly do you want to achieve? My comment was about converting a tuple to a namedtuple by adding names.
Looks like, you need to reorder namedtuple fields for some reason? Then ant[(:t, :a, :n)] is the easiest way.

2 Likes

I misread your intent. Thank you for clarifying

Wanted to destructure a dictionary, found this convenient:

mydict = Dict(:x=>1, :y=>2, :z=>3)
(; x, y) = NamedTuple(mydict)

Unfortunately it works only for dictionaries whose keys are symbols, Dict{Symbol, T}. It doesn’t work for a dictionary whose keys are strings Dict{String, T} (as is frequently the case for config files, etc.).

This can be addressed by overloading NamedTuple to convert string keys to symbols:

NamedTuple(d::Dict{String,T} where T) = NamedTuple{Tuple(Symbol(k) for k ∈ keys(d))}(v for v ∈ values(d))
2 Likes

Note that this is type piracy, and is generally frowned upon. You don’t own any of the functions or types here, so should generally be avoided.

Also, the conversion to NamedTuple is type unstable, so using that will probably be slower than necessary, even than regular Dict indexing.

2 Likes

Note that you can use the destructuring syntax with any datatype that supports getproperty. So, for example, if you use PropDicts.jl, you can do:

julia> using PropDicts

julia> mydict = PropDict(:x=>1, :y=>2, :z=>3)
PropDict with 3 entries:
  :y => 2
  :z => 3
  :x => 1

julia> (; x, y) = mydict;

julia> x, y
(1, 2)

(Unlike conversion to NamedTuple, this is type-stable, at least with a typed PropDict, and should be equivalent to x = mydict[:x], y = mydict[:y].)

3 Likes

This is a good point.

If I understand correctly, the concerns here are:

  • returning a tuple of variable length
  • returning a tuple whose type parameterization is an unknown sequence of Symbol names

Is this correct? I hope to understand it properly.

Thanks for the input! I’ve been playing with making my own toy PropDict-like datatype and came to this realization. Naturally I like mine better, but that’s a different story.

Along this vein, something I found weird: tab-autocomplete of property names seems to depend on Base.propertynames(d::D) existing. However, for all of PropDicts, PropertyDicts, and for my own datatype too, tab-autocomplete doesn’t work beyond the first nesting level.

For example:

using PropDicts
a = PropDict()
a[:a] = PropDict()
a.a[:a] = PropDict()
a.a.a[:a] = PropDict()

julia> a.<tab> # autocompletes to a.a

julia> a.a.<tab> # no autocomplete

By contrast, structs do autocomplete to deeper nesting depths:

struct MyStruct a end
a = MyStruct(MyStruct(MyStruct(nothing)))

julia> a.<tab> # autocompletes to a.a

julia> a.a.<tab> # autocompletes to a.a.a

julia> a.a.a.<tab> # autocompletes to a.a.a.a

Do you have any clue what’s going on here? Am I missing something?

Not just that, but accessing the elements of that NamedTuple is also going to require a lookup due to only being able to infer Any. That’s ultimately what’s going to make this slower than just having the dict accesses there, since such a type instability is pretty infectious.

You may use UnPack.jl for this.

julia> using UnPack

julia> D = Dict("a"=>1, "b"=>2)
Dict{String, Int64} with 2 entries:
  "b" => 2
  "a" => 1

julia> @unpack a, b = D;

julia> a
1

julia> b
2
2 Likes

Would it really infer Any? Even for something like NamedTuple{NS, NTuple{N, Int}} where {NS, N}?

1 Like