"Under the hood" difference between import and using

So, based on the docs I know that eg. import doesn’t bring in any exported identifiers to your namespace when importing a module, and if you want to add a method to a function without using its fully qualified name, you have to do eg. import Base: show before defining function show(io, x::MyType) …, but I’d like to know what the difference between using and import really is under the hood.

Base docs say this for using:

using Foo will load the module or package Foo and make its exported names available for direct use.

and this for import:

import Foo will load the module or package Foo . Names from the imported Foo module can be accessed with dot syntax (e.g. Foo.foo to access the name foo ). See the manual section about modules for details.

For modules the difference between using and import is pretty clear, but using Foo: corge and import Foo: corge seem to do the same thing except that you can’t add methods to a function that was brought into the namespace with using, so this left me wondering what the difference of those two forms is from the compiler’s / runtime’s standpoints

You might want to add “under the hood” or “from the compiler’s / runtime’s standpoints” to the title. Otherwise, the people most qualified to answer this might skip over it thinking it’s asking about the basic difference between using and import that you already have an idea about.

How deep do you want to go here? At a high level, Julia keeps track of the two things separately. There’s a list of bindings (things that have been explicitly assigned to or import’ed) and a list of modules that have been brought in by using (which are used to looked up things as needed).

You can see one half of this difference at the user-level with names(Main, imported=true). import’ed and explicitly assigned names will appear in that list, but all the things available with using aren’t. That usings list is tracked separately and internally and isn’t exposed directly in Julia-land. Tab completion, for example, traverses both lists, combining the bindings names returns with all the exports from the usings.

2 Likes

How deep do you want to go here?

How much free time do you have? :grin:

Interesting that using’d names are stashed away deeper in the runtime, but imported ones are easily available. Sort of makes it feel like if I want to simply eg. bring a function into my package’s namespace so that users of the package can call MyPakidge.foo() instead of SomeDependency.foo(), import SomeDependency: foo might be preferable, although I assume that really wouldn’t make much of a difference except for eg. tooling that uses Base.names to see what names are available in a module.

This doesn’t appear to be the case:

julia> using Base: OneTo

julia> OneT<tab> # no completion

whereas the completion works if one uses import Base: OneTo

3 Likes

Aha-ha, I didn’t even think to check that. Also, interestingly neither of these completes:

module Corge
  bleb(x) = 2x
end

module ModWithUsing
  using ..Corge: bleb
end

module ModWithImport
  import ..Corge: bleb
end

Both ModWithUsing.<tab> and ModWithImport.<tab> just give eval include, so I guess if I want to just “surface” names from other packages, using a const might make them more discoverable for REPL users?

module ModWithConst
    import ..Corge
    const bleb::typeof(Corge.bleb) = Corge.bleb
end

Also, side question, how necessary is the type annotation ::typeof(Corge.bleb) for that const? I’ve seen some packages do that in similar cases but I have no idea if it’s just a leftover from older Julia versions or what. The manual does impress on you that typing even const globals is important, but it honestly feels funny to have to explicitly type consts

Yeah, I’m honestly not sure where the using Foo: X bindings go. Tab completion only traverses the two lists I mentioned (the bindings and the usings), which is why those aren’t showing up there.

If you want to go deeper, it’s the module itself that keeps track of all this… and modules are one of the few portions of Julia implemented in C:

The bindings field is what names uses. The usings field are those modules that are brought in wholesale by using (you can see this list with @ccall jl_module_usings(Main::Any)::Any).

1 Like

You you can also explicitly export them for them to show up in those tab-completed lists. The tab-completion for modules uses names(Mod, imported=false).

Ah derr, of course export would work for names brought in from other modules too :person_facepalming:

Edit: I guess the const version above could be useful if you want to rename your “export”?

Yeah, you can also use Reexport.jl.

The special case of using Mod: X and its lack of tab-completion is issue #29275.