Order matters in the conflicts with imported names

I have been struggling to understand the nuances of what happens when there are conflicts with names imported from other modules.

I eventually found by myself a consistent explanation for import Foo: bar. If I’m not mistaken, in practice that’s the same as attempting to bind the content of Foo.bar to Main.bar (replace Main with the module into which you are importing): if Main.bar already exists, the import is ignored with a warning; and if the name is available, it’s taken. That’s all.

However, with using things become complicated, and the results depend a lot on the order in which things are done. It is as if using makes some kind of “lazy import” with the exported names. So, if I load two packages that export shared names, I get this well-known warning when I first try to use the offending name:

julia> module M1
           export foo
           foo() = 1
       end
Main.M1

julia> module M2
           export foo
           foo() = 2
       end
Main.M2

julia> using .M1, .M2

julia> foo()
WARNING: both M2 and M1 export "foo"; uses of it in module Main must be qualified

But if I load one of the modules and use the exported name before loading the other, the first takes the place in Main:

julia> module M1
           export foo
           foo() = 1
       end
Main.M1

julia> module M2
           export foo
           foo() = 2
       end
Main.M2

julia> using .M1

julia> foo()
1

julia> using .M2
WARNING: using M2.foo in module Main conflicts with an existing identifier.

julia> foo === M1.foo
true

That sort of “lazy import” produces other inconsistencies in certain corner cases. For instance:

If I define something with the exported names in Main after using the module, but before calling that name for the first time, it’s as if I had defined it before using (there is a conflict and the import does not happen), but the warning is missed:

julia> module M1
           foo() = 1
       end
Main.M1

julia> using .M1

julia> foo() = 2 # No warning
foo (generic function with 1 method)

julia> foo()
2

julia> M1.foo()
1

Besides, if the name is successfully imported, although functions are not available for method extension with just their “raw” names, I can extend them by prefixing not only the name of the original module, but also Main (or whatever module I’m in):

julia> module M1
           export foo
           foo() = 1
       end
Main.M1

julia> using .M1

julia> foo()
1

julia> foo(x) = 2 # I knew that this is not allowed
ERROR: error in method definition: function M1.foo must be explicitly imported to be extended
Stacktrace:
 [1] top-level scope at none:0
 [2] top-level scope at REPL[5]:1

julia> Main.foo(x) = 2 # instead of orthodox M1.foo

julia> M1.foo("hi") # This is very confusing, isn't it?
2

julia> Main.foo === M1.foo
true

I found these apparent inconsistencies after having read the documentation about modules, and honestly in my eyes they looked as a somewhat buggy behavior. Upon a second read (after having experimented with this), I realized that the documentation is accurate, and the observed results are consistent. For what is worth, these are the key paragraphs that I had failed to understand:

My question is whether other people think that this is clear enough --so my confusion was a personal failure to understand that information–, or it might be worth to emphasize or rephrase it to make it clearer. Maybe a section explaining conflicts caused by imports?

3 Likes

In my humble opinion, you raise awareness for different behaviours that might be confusing, but I don’t find them inconsistent. The only one thing that was surprising to me was the possibility of overloading the module function with the Main binding.

That is not inconsistent (because the functions are the same), but certainly very confusing. Hopefully nobody will ever do that :smiley:

Describing those behaviours in words is so hard that I don’t know if anyone would benefit from a rewrite of the docs except if the docs contained the examples themselves.

Yes, after re-reading I found that the documentation is accurate, just that I had a wrong understanding of what it tells. To be precise, my incorrect preconceived ideas were:

  1. That using .M1 would create bindings in the current module for the variables exported by M1 immediately, not later, when those variables are “read” for the first time.

  2. That if foo has been made visible in Main by using .M1, then the definition of the method Main.foo(x) = 2 would either:

    • be disallowed (or at least emit a warning);
    • or bypass the prohibition of creating variables with the same name as imported ones, making Main.foo a new function different from M1.foo.

I know that all that is wrong; the docs don’t mean that. But I was not sure if now I understand it because I read it more carefully, or because after some experimentation and unexpected results I can make the right connections with the written statements. If my initial failure to understand it right is accidental and personal, we can leave it as it is. If others might arrive at similar wrong assumptions as I did, perhaps some clarifying examples or explanations would be suitable.

1 Like