Compact syntax for combining `Tuple` and `NamedTuple`: `(1, 2; x=2, y=3) == ((1, 2), (x=2, y=3))`

This may have been brought up already, but it would be nice to have a compact syntax for making a data structure that is a mixture of positional and keyword arguments.

As an analogy to the function syntax f(1, 2; x=2, y=3), I would propose the syntax:

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

which could get lowered to:

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

or some other data structure storing the positional and named arguments.

I find that it is a common code pattern to carry around a data structure with a Tuple of positional arguments and a NamedTuple of named arguments, which will eventually get passed to a function as positional and keyword arguments.

In fact, we use that as a representation of quantum gates in our quantum circuit simulator: https://github.com/GTorlai/PastaQ.jl#simulating-quantum-circuits, I think the proposed syntax would make our interface a lot nicer (more readable and easier to type).

I would find interesting to have a structure for which f(positionals_and_keywords...) worked as f(positionals...; keywords...), but is there so much gain in having them together instead of separate? There could be a @splat macro that makes f(x) into f(x.positionals...; x.keywords...) no?

The difference in literal seems very small (just add two pairs or parenthesis), and not sure it merit even a macro, as it would save at most one character?

To me the main concern is the readability of the code, not the amount of typing. I often use verbose names for functions and variables with the goal of having the code readable long into the future with minimal reference to documentation, but I’m also of the opinion that simple things should have simple syntax.

It seems like fundamentally the question is, if we have that syntax for functions, why not for this as well (unless there is a better alternative proposal for the meaning of that syntax, which would be a good argument against it)? Obviously people like the function syntax and no would argue we should type f((1, 2), (x=2, y=3)) everywhere.

Of course we could make a macro or data structure for this but it is nicer to avoid that when possible (which is why I like using built-in types like Tuple and NamedTuple whenever I can).

Though I agree it would be nice to be able to splat whatever data structure you get into a function like f((1, 2; x=2, y=3)...). You could of course make that work by defining a version of your function f(args::Tuple, kwargs::NamedTuple) = f(args...; kwargs...), which is what we often do.

1 Like

There is https://github.com/ararslan/FrankenTuples.jl

5 Likes

Thanks! Exactly what I had in mind. Maybe an intermediate solution would be a macro that turns everything of the form (1, 2; x=2, y=3) into a FrankenTuple in a code block but of course it would be nicer if the syntax was built-in.

1 Like

That is a nice workaround I did not think about, but it makes necessary to add new methods to every function. This could be automated with macros but in the end you would need to do this in advance at the global scope for every function you want to have this syntax. Also, I think you meant:

f(args::Tuple{Tuple,NamedTuple}) = f(args[1]...; args[2]...)

That would depend on whether or not you splatted (1, 2; x=2, y=3), i.e. f((1, 2; x=2, y=3)) vs. f((1, 2; x=2, y=3)...).

1 Like

Yes, that is a major downside.

1 Like

Sincerely, thinking a little more about the subject, I think this is a no-go because of ambiguity.

julia> (1; x = 2)
2

For elements with a single positional argument and a single keyword argument the syntax already means do anything before the ; and then set the variable after the ;.

1 Like

Yeah, that’s an unfortunate syntax conflict. I regret making (ex; ex) a chained expression syntax. Not useful enough to be worth it.

1 Like

Not useful enough, but at the same time it would be kinda puzzling if it did not work, in my opinion.

I do not use (ex; ex) very much, but cond && (x = value) is common, so I am happy that at least that is not parsed as a NamedTuple automatically or something like that.

3 Likes

What if we required a trailing comma for that case?

julia> (1; x=2)
2

julia> (1,; x=2)
ERROR: syntax: unexpected semicolon in tuple around REPL[24]:1
Stacktrace:
 [1] top-level scope
   @ REPL[24]:1

Not much different from disambiguating (1) from (1,) for making either an expression or Tuple.

1 Like

I actually use that syntax quite a bit for quickly making one-line functions, for example:

hermitize(A) = (H = 0.5 * (A + A'); Hermitian(H))

Though that is usually in the REPL and I wouldn’t leave that in source code, where I would prefer making a proper multiline function.

4 Likes

It’s not a bad syntax and I use it too, just an unfortunate clash that seems maybe not worth it.

If this syntax does not clash with anything, maybe it would be an interesting addition. However, if splat and slurp are to work over it, then it would yet either break the symmetry of splatting, or be a breaking change. This is: f(x...) = typeof(x) should print what? If this should print FrankenTuple, then adding the new FrankenTuple would be a breaking change; if it should print Tuple then the slurp and splat operators are not symmetric anymore, because f(frankentuple...) would splat both positional and keyword arguments, while f(x...) = ... only captures one set of them (i.e., new syntax would be necessary to capture a frankentuple).