Import struct without changing its "scope"

I have problems on importing struct from other files. Suppose I have the file structure:

- Lattices.jl
- Hamiltonians.jl
- main.jl

Lattices.jl defines a struct called Lattice:

module Lattices
export Lattice

struct Lattice
    lx::Int64
    ly::Int64
end
end

Hamiltonians.jl defines Hamiltonian on a Lattice:

module Hamiltonians
export Hamiltonian

include("Lattices.jl")
using .Lattices

struct Hamiltonian
    lat::Lattice
    coeffs::Vector{Float64}
end
end

main.jl tries to create a Hamiltonian:

include("Lattices.jl")
include("Hamiltonians.jl")
using .Lattices
using .Hamiltonians

a = Lattice(16, 16)
ham = Hamiltonian(a, [1.0, 0.0])
display(ham)

However, I get the error

ERROR: LoadError: MethodError: Cannot `convert` an object of type Lattice to an object of type Main.Hamiltonians.Lattices.Lattice

So the problem is that when I import Lattice into the Hamiltonians module, its “scope” is also changed (sorry if I am not using the correct terminology; I come from Python so I have never encountered such behavior before). It is no longer regarded as the same Lattice from Lattices.jl. How do I tell Julia that different Lattice’s are the same thing?

You should not include("Lattices.jl") inside “Hamiltonians”, because include is like copying-and-pasting the code there. That means that you are defining a completely independent Lattices module inside Hamiltonians.

What you want there is to use:

module Hamiltonians
export Hamiltonian

using ..Lattices # two dots
...

to load the module you defined before.

(the takeaway is: never include the same file twice).

2 Likes

Thanks! Actually I never understand the dots in front of the module names. Could you please further explain? In addition, if Lattices.jl is in a folder lattices/Lattices.jl, the two-dot import will not work; how to deal with this case?

The dots indicate relative module paths. When you import a package, you don’t have dots because packages are stored distantly; the lack of dots informs the import statement to look elsewhere. In this case, you are includeing a module directly into your module tree (Main containing modules that can contain other modules). To share names among the modules, you do dotted imports, the dots helping the import statement walk along the tree. So that’s why you don’t want to include twice, you’re evaluating a copy, a separate branch so to speak.

tbh, the dots still gets me with off-by-one errors, but hopefully this contrived example helps:

julia> module Foo
         module Bar0 end
         module Bar1
           module Bar2 end
           using ...Foo # Bar1 -> Foo -> Main.Foo
           using ..Foo  # Bar1 -> Foo.Foo (watch out, modules contain own name)
           using ..Bar0 # Bar1 -> Foo.Bar0
           using .Bar2  # Bar1.Bar2
         end 
       end;

One dot is a starting point, it just means you look in the global scope that the import statement is in. With each additional dot, you go up a global scope to look. With enough dots you reach Main, and more dots won’t go anywhere.

The dots are necessary. You can’t qualify the name of the searched scope like relativeimport Foo.Bar0 because two separate modules can share the name Foo in different branches of the module tree. If it helps you look up a file faster, you could just comment the name like the above and use the dots to tell synonyms apart sometimes; I personally don’t nest modules deeply so the walk isn’t far. The Bar0 part on the other hand must be named to select one out of possibly many modules in the searched scope.

The include command will change, but not the using ..Lattices (in that case).

That said, the most common approach is to just have one “big module”, lets say, called “MyQuantumPackage”, and just include the source of the sub-functionalities in that module, without splitting them into different modules each (which will make your like harder). Something like:

module MyQuantumPackage
      include("lattices/Lattices.jl")
      include("Hamiltonian.jl")
end

where Lattices.jl is just:

export Lattice

struct Lattice
    lx::Int64
    ly::Int64
end

(without the module), and the same for Hamiltonian, such that you don’t need to import names from other modules all the time within your package.

1 Like

In other words, split into modules if you need separate namespaces that share select names via imports. If you don’t need such encapsulation, it’s easier to work in 1 namespace, and include can evaluate multiple source files into it, which helps prevent a big namespace from making too big a file.

@Benny in your example, what is the difference between Main.Foo and Foo?

Main and Foo are names of 2 modules. Modules contain their own names by default and the names of nested modules, so Foo.Foo and Main.Foo access the same module. I was warning you of that because it might be confusing why using ..Foo and using ...Foo both work; you’re just searching the name Foo in different modules.

I recommend avoiding Foo.Foo so the same number of dots reach modules on the same level; Foo isn’t on the level of Bar0 or any other module it may contain. It’ll also cover the pathological cases where a module’s own contained name is reassigned to another module:

julia> module Foo
         export x
         x = "outer"
         module Foo # don't repeat names in practice
           export x
           x = :inner
         end
         module Bar
           using ..Foo # Bar -> Foo.Foo
         end
       end
WARNING: replacing module Foo.
Main.Foo

julia> Foo, Foo.Foo
(Main.Foo, Main.Foo.Foo)

julia> Foo.Bar.x
:inner