Why the parenthesis around `(@main)`?

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.

3 Likes

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

5 Likes

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.

4 Likes

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
1 Like

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.

3 Likes

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.

1 Like

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

1 Like

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
1 Like

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.

11 Likes

Thank you for parting the mists.

1 Like