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.
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.
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.
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).
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).