During JuliaCon discussion with @ahojukka5, I figured it would be cool to have NamedTuple unpacking in function arguments which honors the names of the named tuple. Currently we have:
julia> nt = (a=1, b=2)
julia> f((b,)) = b
f (generic function with 1 method)
julia> f(nt)
1
instead, I'd think this would be more intuitive:
julia> f(nt)
2
Similarly it could work for structs:
julia> struct A; a; b end
julia> aa = A(1,2)
A(1, 2)
julia> f(aa)
2
(Unfortunately, this needs a 2.0 label)
Edit: fixed unneeded/confusing default-value method-defs.
(b,)
already means positional unpacking, so the syntax would have to be (; b,)
or (b=b,
).
That would then look:
f((;a, )) = 2a
# or
f(x, y, (;a, b)) = x+y+a+b
Could be good. The only potential problem could be that it would be easy to miss the ;
and then silently do positional unpacking.
The (b=b,)
syntax seems a bit verbose.
As a bonus, this would be non-breaking.
Edit: fixed unneeded/confusing default-value method-defs.
The
(b=b,)
syntax seems a bit verbose.
No more verbose than the calling syntax and it would allow restructuring the named value to a variable with a different name. It's currently pretty common to see stuff like this:
something = ...
f(a, b, c; something=something)
If we had a shorthand for the case where the local name matches the keyword name then it would also make sense to use that same syntax for destructuring when the names match. Spitballing, it could be:
f(a, b, c, =something)
f(a, b, c, something=)
I think I prefer the former.
It has been proposed before (I believe by @davidanthoff ) to make (; x)
shorthand for (; x = x)
. In fact there is commented-out code for it in julia-syntax.scm. Also (; a.x)
can be shorthand for (; x = a.x)
. These are currently syntax errors.
I use myfuction(x; keyword::T) .. myfunction_doingless(y, keyword=keyword) .. end
with frequency.
when we are free to use some unicode as shorthand
x⥍ ≝ x = x
(with frequency .. about 80GHz today)
Also
(; a.x)
can be shorthand for(; x = a.x)
.
How would it be known what a
is when the function is called?
IMO, this feature is orthogonal to kwargs. It makes sense to name-unpack the fields of a positional argument (using the syntax of https://github.com/JuliaLang/julia/issues/28579#issuecomment-412261054):
f(x, y, (;a, b); kw1=1, kw2=y) = ...
f(1,2, (a=3, b=8, u=9))
and also name-unpack a kwarg:
g(x, y; kw1=1, (;a, b)=p, kw2=y) = ...
g(1,2, p=(a=7, b=9, u=8))
Thus a syntax working for both should be found. I think above seems reasonable.
Somewhat related: I am experimenting with a package called EponymTuples.jl, which allows replacing
f((a, b)::NamedTuple{(:a, :b), <: Tuple{Any, Int}}) = ...
(a = a, b = b, c = 3)
with
f(@eponymargs(a, b::Int)) = ...
@eponymtuple(a, b, c = 3)
I find it helpful for cases when I don't want to introduce and name a struct
for passing around a large number of parameters.
I happily noticed recently in the News.md for 1.5 that https://github.com/JuliaLang/julia/pull/34331 was merged, which I find super useful.
At this point the language seems perfectly set up to implement the Issue here, with the syntax being that a named tuple on the LHS means unpacking into those names. It gives a great symmetry between packing/unpacking named/unnamed arguments:
# pack named or unnamed arguments
foo(x,y)
foo(;x,y)
# unpack named or unnamed arguments
(x,y) = bar()
(;x,y) = bar()
If getproperty
is used to do the unpacking, then it works for structs and NamedTuples and is type stable, and you can always do (;x,y) = (;dict_like...)
for other stuff. Or a separate interface can be defined like @mauro3's @unpack
.
A nice testament to the consistency of all of this is that you would have that both of these are valid and a no-op as you'd expect:
# valid but no-op
(x,y) = (x,y)
(;x,y) = (;x,y)
Would be great if something like this could be considered.
Agreed. I'm particularly interested in the destructuring of structs and named tuples in function arguments. If we had that feature, then we would have ~70% of the pattern matching capabilities of ML languages. The only parts we would be missing are
Someone show me exactly what should happen given _what_?
Someone show me exactly what should happen given _what_?
Good question. The original post seems to focus on unpacking in function arguments, which is the part I'm most interested in. So, the syntax might look like the following:
# define
foo(q, (; x, z)) = q + x + z
# call
foo(1, (x=2, y=3, z=4)) # returns 7
I think struct unpacking in function arguments requires a separate syntax. Perhaps something like this:
struct A
x
y
z
end
# define
bar(q, A x z) = q + x + z
# call
a = A(2, 3, 4)
bar(1, a) # returns 7
@JeffreySarnoff, the idea is like this:
struct T
x::Int
y::Float64
end
t = T(10, 3.14)
nt = (a=1, b=2, c=3)
(;x, y) = t
(;a, b, c) = nt
so the (;x, y)
syntax on the LHS is a way to declare/make several variables that get their value by calling getproperty(x, :sym)
on the RHS.
Javascript has this (in recent versions).
thx
@CameronBieganek just want to highlight that
I think struct unpacking in function arguments requires a separate syntax.
doesn't need to be the case.
Since Julia already lowers argument unpacking from
foo((x,y),) = ...
to something like
foo(tmp) = ((x,y) = tmp; ...)
then it would be natural that foo((;x,y),) = ...
became foo(tmp) = ((;x,y) = tmp; ...)
, and then if (;x,y)
were allowed on the LHS with the meaning proposed above, then your struct unpacking into positional arguments would work exactly right.
then it would be natural that
foo((;x,y),) = ...
becamefoo(tmp) = ((;x,y) = tmp; ...)
, and then if(;x,y)
were allowed on the LHS with the meaning proposed above, then your struct unpacking into positional arguments would work exactly right.
@marius311 That's interesting, but I would want
foo(q, (; x, z))
and
foo(q, A x z)
to be two separate methods in the method table for foo
. In other words, the second method doesn't just match the property names x
and z
, it also matches the type A
.
However, I see now that the current tuple unpacking in positional arguments does not create a foo(::Tuple{Any, Any})
method, which is troubling. ☹️
julia> foo((x, y)) = x + y
foo (generic function with 1 method)
julia> methods(foo)
# 1 method for generic function "foo":
[1] foo(::Any) in Main at REPL[12]:1
You could imagine being able to do,
foo(q, (; x, z) :: A) = ...
which would be totally consistent with how you can currently do
julia> foo((x, y)::Tuple{Any,Any}) = x + y
foo (generic function with 1 method)
julia> methods(foo)
# 1 method for generic function "foo":
[1] foo(::Tuple{Any,Any}) in Main at REPL[1]:1
julia> foo((x, y)::Tuple{Any,Any}) = x + y foo (generic function with 1 method)
Phew, I'm glad that exists as a workaround, but it still seems wrong to me that foo((x, y))
creates a foo(::Any)
method.
In other words, foo(3)
should throw a method error if the only method I've defined is foo((x, y))
. Currently we get a bounds error instead:
julia> foo((x, y)) = x + y
foo (generic function with 1 method)
julia> foo(3)
ERROR: BoundsError: attempt to access Int64
at index [2]
Stacktrace:
[1] indexed_iterate(::Int64, ::Int64, ::Nothing) at ./tuple.jl:90
[2] foo(::Int64) at ./REPL[1]:1
[3] top-level scope at REPL[2]:1
I'm guessing the reason for this is because f((x,y))=...
currently also allows being called with anything that implements the iterator interface and can unpack into two arguments, e.g. f([1,2])
also works, so you don't want to exclude that by requiring the argument be a Tuple in the signature.
Would unpack(nt::NamedTuple)
that assigned (and could overwrite) symbols used as the names in nt to values given with Tuple(nt)
be a viable approach? (I do not know how to tell rhs from lhs unless I have them both). If not, clarify this for me.
Most helpful comment
It has been proposed before (I believe by @davidanthoff ) to make
(; x)
shorthand for(; x = x)
. In fact there is commented-out code for it in julia-syntax.scm. Also(; a.x)
can be shorthand for(; x = a.x)
. These are currently syntax errors.