FrankenTuple Fun



Turns out that (a,; b) parses, and has for awhile (according to this time machine):

An experiment with FrankenTuples:

julia> using FrankenTuples

julia> macro ft(ex)  esc(ft!(ex))  end
@ft (macro with 1 method)

julia> function ft!(ex)
           # FrankenTuple Construction
           if ex isa Expr && ex.head == :tuple && length(ex.args) > 1 &&
           ex.args[1] isa Expr && ex.args[1].head == :parameters
               t = Expr(:tuple, ex.args[2:end]...)
               nt = Expr(:tuple, ex.args[1])
               ex.head = :call
               ex.args = [:FrankenTuple, t, nt]
           ex isa Expr && map(ft!, ex.args)
ft! (generic function with 1 method)

Can be called like:

julia> @ft x=(1,; a=2)
FrankenTuple((1,), (a = 2,))

Better yet:

julia> pushfirst!(Base.active_repl_backend.ast_transforms, ft!);

julia> x=(1,; a=2)
FrankenTuple((1,), (a = 2,))

Wrap any other expression in @ft and its behavior is unchanged. For example:

julia> @ft (1; a=2)

julia> @ft (1, 2, 3)
(1, 2, 3)

julia> @ft (a=1, b=2)
(a = 1, b = 2)

Of course, @ft is kinda redundant after adding ft! to our REPL AST transforms.

More FrankenTuple fun:

julia> (1, 2; a=3, b=4)
FrankenTuple((1, 2), (a = 3, b = 4))

julia> (1, 2; )
FrankenTuple((1, 2), NamedTuple())

julia> (()...; a=3, b=4)
FrankenTuple((), (a = 3, b = 4))

julia> (()...; )

julia> ((1,2,3)...; (a=4, b=5, c=6)...)
FrankenTuple((1, 2, 3), (a = 4, b = 5, c = 6))

Keep in mind, this is an experiment! I’m just playing around to see what’s possible.

Having to splat an empty tuple to make the FrankenTuple’s tuple an empty tuple is weird :sweat_smile: While () is an empty Tuple and (;) is an empty NamedTuple, an edit is needed for (,;) to parse so it can be an empty FrankenTuple.

The FrankenTuple’s splatting behavior is wrong. Iterating over (x...,) should splat only the numbered elements from its tuple (right now it splats both numbered and named elements), and for named elements (;x...) a custom Base.merge(nt::NamedTuple, ft::FrankenTuple) = merge(nt, getfield(ft, :nt)) should be implemented that splats out its named elements. Then you could write f(x...; x...) and each part would splat out as expected :wink::ok_hand:

Like this:

julia> Base.iterate(x::FrankenTuple) = iterate(getfield(x, :t))

julia> Base.iterate(x::FrankenTuple, n) = iterate(getfield(x, :t), n)

julia> Base.merge(nt::NamedTuple, ft::FrankenTuple) = merge(nt, getfield(ft, :nt))

julia> f(args...; kwargs...) = (args, kwargs)
f (generic function with 1 method)

julia> f((1,)...; (a=2,)...)
((1,), Base.Pairs(:a => 2))

julia> f(x...; x...)
((1,), Base.Pairs(:a => 2))