How to construct a type using Meta programming?

I would like to contruct a type using meta programming. More specific,I need a tuple type like

Tuple{Vector{String}, Vector{String}}

.
Constructing a value is quite easily possible,
e.g.

ex = Expr(:tuple)
push!(ex.args, 1)
push!(ex.args, 2)

with

eval(ex)

resulting in the tuple value

(1,2)

But how to construct a tuple type?
I used dump to get a syntax tree of the tuple type mentioned above.

dump(:(Tuple{Vector{String}, Vector{String}}))

It results in the following syntax tree:

Expr
  head: Symbol curly
  args: Array{Any}((4,))
    1: Symbol Tuple
    2: Expr
      head: Symbol curly
      args: Array{Any}((2,))
        1: Symbol Vector
        2: Symbol String
    3: Expr
      head: Symbol curly
      args: Array{Any}((2,))
        1: Symbol Vector
        2: Symbol String
    4: Expr
      head: Symbol curly
      args: Array{Any}((2,))
        1: Symbol Vector
        2: Symbol String

However, using the symbol curly, using

ex = Expr(:curly)

did not work to get a well-formed expression so far.
I would be very happy if you had a suggesion.

Expr(:curly, :Tuple, :Int, :Char) works just fine to construct :(Tuple{Int, Char}), for example.

PS. Obligatory warning that metaprogramming is the wrong tool 99% of the time … but is very useful in remaining 1%.

First, I’ll echo the above that there might be a better way to do this without metaprogramming. Metaprogramming is difficult to write and read and it can be brittle.

To answer your specific query based on the dump you gave (less args[4], which should not actually be present):

julia> Expr(:curly, :Tuple, Expr(:curly, :Vector, :String), Expr(:curly, :Vector, :String))
:(Tuple{Vector{String}, Vector{String}})

or, for an incremental build:

julia> ex = Expr(:curly, :Tuple); for _ in 1:2; push!(ex.args, Expr(:curly, :Vector, :String)); end; ex
:(Tuple{Vector{String}, Vector{String}})

julia> ex = :(Tuple{}); for _ in 1:2; push!(ex.args, :(Vector{String})); end; ex # quotes are usually nicer
:(Tuple{Vector{String}, Vector{String}})

Deconstructing the above a bit, notice that :curly needs an args[1] to tell it what is curly to make a “well-formed expression”:

julia> ex = Expr(:curly); push!(ex.args, :Tuple); ex
:(Tuple{})

In earlier steps, typing source code into quoted expressions and interpolation could be easier than constructing and mutating Expr directly:

julia> x = :(Vector{String})
:(Vector{String})

julia> :(Tuple{$x, $x})
:(Tuple{Vector{String}, Vector{String}})

julia> :(Tuple{$( (x, x)... )})
:(Tuple{Vector{String}, Vector{String}})

It’s good to know the Expr structure and how to mutate it regardless; beyond a point, it gets simpler and more efficient than only writing quoted expressions.

I never liked how this is worded. IIRC the basis is that macros and generated functions made up <1% of method definitions in base Julia at some point, but that has some caveats:

  • eval loops are also metaprogramming.
  • A lot of Expr and raw string processing is done in plain functions called by macros.
  • Macro or generated function calls do transform or generate code, so it’s comes across as a bit strange to say metaprogramming is a rare tool then suggest macro calls for routine timing and reflection. Maybe “implementing metaprogramming” would be closer, but that still sounds odd to me.
  • Some languages don’t specify expression objects like Julia does (though they may expose internals in implementation-specific libraries), changing the concept of metaprogramming. This advice for beginners may backfire, say make a Python user avoid higher-order functions because that’s what most decorators do. I think most people would study enough to avoid that pitfall, but some won’t, might even throw off an AI prompt.

Thank you! That helps a lot.