# Typed globals: macro to define multiple, without manual type annotations

I vaguely remember seeing a macro somewhere to define typed globals without having to give the type explicitly, something like

``````@typed begin
a = 2
b = 2.8
⋮
end
``````

which would expand to

``````a::Int = 2
b::Float64 = 2.8
⋮
``````

Does anyone remember where that was?

Not sure if something like this is already in a package somewhere, but here’s one way to write such a macro:

``````function rewrite_assignment(@nospecialize(ex))
if Meta.isexpr(ex, :(=))
l, r = ex.args
l isa Symbol || error("Destructuring not supported")
tmp = gensym(l)
return esc(:(let \$tmp = \$r; global \$l::typeof(\$tmp) = \$tmp end))
elseif Meta.isexpr(ex, :block)
return Expr(:block, Base.mapany(rewrite_assignment, ex.args)...)
else
return ex
end
end

macro typed(ex)
return rewrite_assignment(ex)
end
``````
``````julia> @typed begin
a = 2
b = 2.8
end
2.8

julia> code_typed() do
a, b
end
1-element Vector{Any}:
CodeInfo(
1 ─ %1 = Main.a::Int64
│   %2 = Main.b::Float64
│   %3 = Core.tuple(%1, %2)::Tuple{Int64, Float64}
└──      return %3
) => Tuple{Int64, Float64}
``````
5 Likes

Amazing, thanks!

Could we write `\$l::typeof(\$r) = \$r` instead? (i.e. w/o `let` and `tmp`)

That’s fine if the right-hand side is a literal or symbol, but you might also have an expression with side effects, for example something like `@typed a = pop!(v)`. Or even one that returns different results each time like `@typed a = rand((1, 2.0))`. In those cases you want to avoid evaluating `r` twice.

5 Likes

Just adding this for reference: “typeconst” is a relevant keyword for this problem on the discourse: Search results for 'typeconst' - Julia Programming Language (thanks Stefan Karpinski)

Ah, found what I was originally thinking of: it’s the “@stable” macro by @giordano in the Seven Lines of Julia thread:

Simeon’s is a bit robuster indeed, with the \$tmp.

@simeonschaub I was also wondering, why the `Base.mapany` (and not just `map`)?

EDIT: I found why here (in the SnoopCompile docs) (maybe):

`mapany` avoids trying to narrow the type of `f(v[i])` and just assumes it will be `Any`, thereby avoiding invalidations of many `convert` methods.

So, it’s used to spare the compiler some type inference work and/or to avoid `convert` invalidations, I think

The final thing I wonder about is the @nospecialize:
all Exprs are the same type, so there is no “too many specializations” problem right?

Like, why not say `rewrite_assignment(ex::Expr)`.
And why the `Meta.isexpr(ex, …)` instead of just `ex.head == …`?

Ah, these are to handle `LineNumberNode`s, I assume.

Still, how is the @nospecialize useful?

1 Like

I think I’m missing why that’s more robust (than what?)

1 Like

I thought it said something like

``````:(
\$lhs::typeof(\$rhs) = \$rhs
)
``````

(With left- and right-hand sides `lhs, rhs = ex.args`).

But instead it uses a `local` temporary variable.
I.e. (rewriting a bit for ease of comparison):

``````:(
begin
local \$tmp = \$rhs
\$(lhs)::typeof(\$tmp) = \$tmp
end
)
``````

(with `tmp = gensym(lhs)`, for when applying the macro to more than one assignment at once)

…which I suppose is equivalent to Simeon’s `let` & `global` approach:

``````:(
let \$tmp = \$rhs
global \$(lhs)::typeof(\$tmp) = \$tmp
end
)
``````

As an addition, in my code where I define a macro like this, I added branch on whether `typeof(rhs) ∈ [Expr, Symbol]` or not.

If it is, I do as above (i.e. the `\$tmp` approach, to handle non-determinism and side effects).

But if not (i.e. the right-hand side is a literal (hopefully)), I `eval` the `typeof` already in the macro:

``````T = Symbol(@eval typeof(\$rhs))
:(
\$lhs::\$T = \$rhs
)
``````

The goal is to show something less scary to users when they e.g. `@macroexpand`, in the simple (and most common) cases.

For the record, I had written a blog post about a slightly more elaborate version of my macro:

1 Like