How to write an `include_once` function?

Closely related to this recent post:

Is it possible to write an include_once function, which calls include, but only once.

function include_once(path)
    if not run before
        include(path)
    end
end

I’m not sure how to approach the issue of writing in the if statement logic. Is there a way to detect if include has been called with a particular path before?

I suspect this is tricky to do. I don’t think there is any mechanism to query if a module has a name defined within a file, because finding names in files would require loading them (includeing them) first.

Possibly some boolean state could be injected into the include_once function? Or a global dict could be used to track which paths have been loaded from which modules?

Just some initial ideas…

Edit:

I tried to come up with something and came up with this, which seems to work. But I’m not sure I am really doing this in the best way. This feels like something a macro could be useful for?

function include_once(target_module, path)

    global g_include_once_dict

    if ! (@isdefined g_include_once_dict)
        g_include_once_dict = Dict()
    end

    key = (target_module, path)
    if !haskey(g_include_once_dict, key)
        println("include: $(target_module) $(path)")
        g_include_once_dict[key] = true
        include(path)
    end
end

Intended use is something like this

include_once(@__MODULE__, "test.jl")

You might find some ideas in FromFile

2 Likes

Just keep a Set of previously included paths?

let previous_includes = Set{String}()
    global include_once
    function include_once(path::AbstractString)
        if !in!(abspath(path), previous_includes)
          include(path)
        end
        return nothing
    end
end

(I used let here so that the Set is a local variable, rather than being global.)

However, since this should be module-specific, it should probably be a macro instead of a function so that it can expand to an include in the caller module (rather than in the module where include_once is defined) and use @__MODULE__ to determine the caller module for the Set{Tuple{Module,String}}. (FromFile.jl also uses macros.) For example:

const _included_once = Set{Tuple{Module,String}}()
macro include_once(path)
    quote
        path = $(esc(path))
        if !in!((@__MODULE__, abspath(path)), _included_once)
          include(path)
        end
        nothing
    end
end

which you can then call as @include_once(somepath).

1 Like

Considering the linked post about imported modules, I think this is the opposite of what OP wants. The typical “include once, import repeatedly” approach works if you have a good place to order your include calls for the imports to work consistently, like a PackageC.jl in a PackageC directory. include_once seems intended to be paired with import statements to mimick how packages are loaded once upon first import because there isn’t a good place to put include calls (I’ll leave whether such a place should be made or the soundness of the intents to the other post). In that case, you don’t want the modules with the import statements to contain the imported modules, it changes the dots you write in the import statements depending on which module happened to load a shared module first. Instead, the modules need to be evaluated into a consistent module, like how packages are organized into an environment. I’m not sure what that consistent module should be, I think Main always exists.

I think I was solving the problem as specified in this thread — @world-peace used include_once(@__MODULE__, path) above — why would it be once per @__MODULE__ (i.e. the caller’s module) if you don’t want to include into the caller’s namespace?

But then they called include in their include_once function, which is inconsistent (and I suspect that it was simply an error) — this will call the include for the module where include_once is defined, which is not necessarily the caller’s module.

That’s why I used a macro.

I agree that it is generally better to include files only once, into a single “parent” module, and then refer to that module’s namespace elsewhere. (You should not generally be including into Main, however — this top-level module doesn’t belong to your package, it belongs to interactive users or scripts!)

If you want to specify fixed parent module to do the including, you could have a function with a parentmodule argument and call parentmodule.include instead of include.

But if you are doing that, why not just include the files directly from the parent module in the first place, instead of including them obliquely from some submodule? (Which is, of course, what one normally does in Julia packages.)

Yeah, I stand corrected there. Maybe the module containing include_once then? A macro could make that import statement easier to write, too.

It sounds like there isn’t a designated parent module, hence the need for import statements to load a module’s code if it isn’t present in a cache (sys.modules in Python). That said, I’m also just guessing at what OP wants to accomplish, you also seem to find the intent unclear in the linked thread.