Scope of Included Files

In my understanding, the scope of an included file includes the scope of the file it was include'd in. For example:

file1.jl:

abstract type AbstractFoo end
include("file2.jl")

file2.jl:

struct Foo <: AbstractFoo end

This is valid if I include file2.jl inside file1.jl as above, but throws an error if not, because AbstractFoo is not found. So whether or not file2.jl is valid depends on whether it’s include'd in another file, which I think is confusing. Is there a “better” way to confidently/safely navigate/utilize such behavior? How do IDE’s like VSCode understand this?

include purpose is to help organize code that makes a single module in many different files. It is a code organization tool on the package developer side, but it should be invisible/irrelevant to the package user. As you should be only includeing once (but importing and using many times) you can look for the single place that include that file to know its context, i.e., you should always have in mind the only context in which that file is included.

2 Likes

Thanks, I understand I should only include a file once. I suppose it’s confusing to me (and any IDE) whether the code is valid or not - modifying a different file affects the validity of the current file. I realize this isn’t unique behavior per say, but it’s very confusing when determining the current scope when looking at source code one didn’t write. In my example above, I would be confused as to whether the code is valid - do I have to search every other file in a package to see where AbstractFoo was defined and file2.jl is include'd?

in general, as a good-enough operation rule of thumb:

  1. include() is the same as copy-pasting those lines in place
  2. never include the same file twice
4 Likes

Why do you say IDE’s get confused by include? The analysis rule for include is very simple: substitute exactly the contents of path whenever you see include(path). This is the sort of thing computers are very good at, even if humans can struggle if there’s many layers of nesting.

For example, you should really interpret your two examples as identical to:

File 1:

abstract type AbstractFoo end
struct Foo <: AbstractFoo end

File 2:

struct Foo <: AbstractFoo end

This is literally just the application of “substitute the file text for the call to include” and it should show why one file is “more complete” in some sense than the other file and why getting different errors is the only sensible behavior.

Getting back to your question, what part of this is creating problems in an IDE? And what part is confusing you as a human?

1 Like

I suppose my confusion is that having come from Python, I’m used to being able to explicitly invoke (python filename.py) any file (for say testing purposes). Not all Julia files are intended to be run as standalone scripts, unlike Python. A file may only be valid in the context of the package its defined in.

As for the IDE confusion, how does the IDE know AbstractFoo in file2.jl is a valid identifier? Without any import statements at the top, how can it possibly know? I could have a typo, say AbstractGoo, and I wouldn’t know just from file2.jl. And what if I quickly wanted to jump to the file where AbstractFoo is defined? For example, I can’t hover my cursor over AbstractFoo to get further information.

See here for an example. Where is the function value! defined? ZerothOrderOptimizer? etc… How would I go about finding those definitions?

I realize some of these are just habits from Python, but in this case I don’t see a clear “workaround”.

Ok, this helps a lot. There’s definitely a bunch going on here, so let’s take it piece by piece.

  1. It is true that Julia does not emphasize the __name__ == "__main__" approach that’s so common in Python, but large Python codebase also don’t always get written with the assumption each file can be executed in isolation. So setting aside the cultural tradition of “code duality” in Python, it’s useful to keep in mind that Julia’s cultural tradition of using include does mean that code is often written with the assumption that both (a) the sources of identifiers may be quasi-implicit (via include) and (b) the enclosing context may be implicit rather than strictly determined by the filesystem directory structure. But neither of these are big challenges for IDE’s, which can trivially scan and index thousands of files to do binding analysis.

  2. Regarding “how does the IDE know AbstractFoo in file2.jl is a valid identifier”, the truth is that it’s not a valid identifier except in the broader context. But usually you’d edit a Julia codebase starting from a directory where this context is present and the IDE could easily index it: for example, starting in the main directory for the GitHub repo for Optim.jl. The current Julia VSCode toolchain doesn’t seem to do as much indexing as it could, but there’s no deep reason that’s substantially harder than handling Python code – it’s just an ecosystem gap that exists today.

  3. It seems like you’re used to languages with very mature IDE tooling, but it’s actually worth learning more foundational techniques than “hover my cursor over” X since you’ll likely hit up against limitations of IDE’s many more times in your life with many different languages and/or situations. Instead of assuming the IDE does all binding analysis for you, it’s quite easy to find the definitions of bindings from purely syntactic considerations – just look for abstract type AbstractFoo. It’s always possible that such syntactic approaches will fail, but that’s an issue with Python too and really with any dynamic language in which bindings can get created at runtime in ways that require running something close to the full program to understand.

  4. For your specific example, it’s a great test of a purely syntactic search. The first thing I did was to search for function value!, which produced two results that didn’t seem correct. So I then did a regex-based search for import .* value!, which immediately shows exactly one result: import NLSolversBase: value, value!, ...

In summary, you are hitting up on maturity gaps in Julia’s VSCode integration, but they’re also gaps that the approaches you’d need in most environments will easily solve.

3 Likes

completely unrelated but what do you think “per say” means ?

“per se” is Latin for “in and of itself”, so perhaps you are confusing it with that phrase.

1 Like

Didn’t know that, quite embarrassing. Thank you!

1 Like

As other said, the code inside an included file may not be valid if it is run in a standalone fashion. As I have a C/C++ background this was completely natural to me, #include "file.c" works basically the exact same way. Your perspective is interesting, coming from Python it surely must be something strange to have a piece of code that is incomplete by design. If you gonna study other people’s codes, I suggest you start from the main file and enter the included files from there, or do search where the file is included before studying it.

1 Like

Gotcha, thank you all for the tips. It is indeed a strange feeling, but one I’m sure I’ll get used to.

oh don’t be embarrassed, it is a very common mistake

2 Likes