Since Julia’s most recent release (1.11) it seems the de-facto way to write a main
(entrypoint function) is
function (@main)(args)
println("hello world")
end
Why the parenthesis around @main
?
Not that it matters, I’m just curious.
Since Julia’s most recent release (1.11) it seems the de-facto way to write a main
(entrypoint function) is
function (@main)(args)
println("hello world")
end
Why the parenthesis around @main
?
Not that it matters, I’m just curious.
Because @main
is not supposed to be applied to args
, it just returns the main
symbol as if you had written function main
plus some behind the scenes bookkeeping
You could also call @main
as a function-like macro
function @main()(args)
println("hello world")
end
which makes Jules’ point clearer, but looks kinda weird.
In this context, what does the macro do?
Does it just expand to the word main
?
Here’s the definition (see it with julia> @less @main
)
macro main(args...)
if !isempty(args)
error("USAGE: `@main` is expected to be used as `(@main)` without macro arguments.")
end
if isdefined(__module__, :main)
if Base.binding_module(__module__, :main) !== __module__
error("USAGE: Symbol `main` is already a resolved import in module $(__module__). `@main` must be used in the defining module.")
end
end
Core.eval(__module__, quote
# Force the binding to resolve to this module
global main
global var"#__main_is_entrypoint__#"::Bool = true
end)
esc(:main)
end
That’s odd. Should I be able to do this?
function (@main)(args)
println("first main")
end
function (@main)(args)
println("second main")
end
$ julia main.jl
second main
Just like with every method. However, you’ll get a warning if you start julia with --warn-overwrite=yes
.
I can’t read macros very well yet.
What is this doing?
if isdefined(__module__, :main)
if Base.binding_module(__module__, :main) !== __module__
error("USAGE: Symbol `main` is already a resolved import in module $(__module__). `@main` must be used in the defining module.")
end
end
It checks if main
is already defined and bound in the current module. If it’s bound in another module, it throws an error. I.e. if you have done something like import OtherModule: main
.
Ok thank you.
Finally, what does this bit do?
esc(:main)
I looked at the documentation for esc
.
That’s what the macro returns, just the symbol :main
. Without esc
the macro system will “sanitize” the return value. E.g. making variables local etc. so they don’t interfere with the caller’s context. This sanitation can be avoided with esc
. I don’t think it’s strictly needed in this case. A symbol is a symbol.
Consider the following macro which just returns its input:
julia> module Foo
macro bar(s)
s
end
macro baz(s)
esc(s)
end
end
Main.Foo
julia> @macroexpand Foo.@bar(:foo)
:(:foo)
julia> @macroexpand Foo.@bar(a)
:(Main.Foo.a)
julia> @macroexpand Foo.@baz(a)
:a
Ah, ok, so sanitization means to do something which prevents a macro from interacting with variables which are in the same scope as where a macro is used?
For example, in your second example @macroexpand Foo.@bar(a)
, a
here is some local variable (it doesn’t actually exist, but the fact you used the symbol a as an argument implies that it could exist as a local variable?) but because of sanitization, when bar
returns s
, a
has become sanitized so that it is no longer the local Main.a
but Main.Foo.a
?
Not sure if I’m quite following the logic correctly.
I thought that’s what it isn’t?
julia> module Foo
macro escaped() esc(:main) end
macro unescaped() :main end
end
Main.Foo
julia> @macroexpand Foo.@escaped
:main
julia> @macroexpand Foo.@unescaped
:(Main.Foo.main)
Ah, right, the esc
is necessary. My example bar(:foo)
is more like:
julia> macro unescaped() :(:foo) end
@unescaped (macro with 1 method)
julia> @unescaped
:foo
Yes, the idea is that the macro may return a piece of code which assigns its own variables, they will be rewritten. Or they may use global variables in the module where the macro was defined. To use the caller’s variables/expressions, the sanitation must be switched off.
julia> module Foo
const S = 42
macro myadd(a, b)
quote
q = $(esc(a)) + $(esc(b))
if q < $(esc(a))
s = q + S
else
s = q - S
end
s
end
end
end
Main.Foo
julia> using .Foo: @myadd
julia> @macroexpand @myadd(x, y)
quote
var"#3#q" = x + y
if var"#3#q" < x
var"#4#s" = var"#3#q" + Main.Foo.S
else
var"#4#s" = var"#3#q" - Main.Foo.S
end
var"#4#s"
end
The effect of this is that, semantically, a macro has its own scope, inside the module in which it was defined. Interaction with the caller’s scope is explicit by switching the sanitation off with esc
.
This thread contains low-level answers to the question “why?”, but not a high-level answer. The reason this syntax was adopted as the standard entry-point for Julia programs is because it is backwards-compatible. There are already many scripts in the wild with a main
function. I haven’t really followed the development, but I believe the (@main)
entry point has semantics that are somewhat different from a regular old script with a main
function.
Thank you for parting the mists.