<p>As per my current understanding, there are many different ways that imports, and other related operations, are handled in different programming languages.</p>
<p>For example, <code>import</code> in Java merely tells the compiler the full path of a specific package. The package itself can be used without the <code>import</code> statement but the full package name is needed. However, this could 'clutter the namespace' when many different packages and if two packages have the same name one needs the full path identifier.</p>
<p>In Python, on the other hand, imports allow the module to be used in the code. The functions of that module cannot be used otherwise. The <a href="https://docs.python.org/3/reference/import.html" rel="nofollow noreferrer">Python reference</a> says that the <code>import</code> statement &quot;binds the results of that search to a name in the local scope&quot; and the imported code runs when imported.</p>
<p>Then, of course, there are others ways such as <code>use</code> in Rust.</p>
<hr />
<p>My main questions are as follows:</p>
<ol>
<li>What are all the ways that imports can be designed in terms of semantics?</li>
<li>What are the pros and cons of each style?</li>
</ol>

<language-comparison><semantics><imports>
What are the different types of import semantics?
CC BY-SA 4.0
2023-05-18T13:11:26.150Z
I've edited the question to make it more design specific
FireTheLost
CC BY-SA 4.0
2023-05-19T09:22:00.557Z
the deeper (but quite broad) question: what different approaches are there towards modularity and module systems?
starball
<p>As a start:</p>
<h3>Direct file text includes</h3>
<p>E.g. <code>#include &quot;foo.h&quot;</code></p>
<ul>
<li>simple to implement, clear parse ordering</li>
</ul>
<ul>
<li>Poor for dependency tracking and caching.</li>
<li>Worse performance due to more text to parse.</li>
<li>No understanding of name spacing.</li>
</ul>
<h3>Files-as-additional source dependencies</h3>
<p>E.g. Go</p>
<ul>
<li>May be extended to remote retrieval.</li>
<li>Easy to understand</li>
<li>Only compiles what is needed.</li>
</ul>
<ul>
<li>Cyclic dependency must be handled.</li>
<li>No understanding of name spacing.</li>
</ul>
<h3>Visibility module imports with ad hoc path access</h3>
<p>E.g Java imports.</p>
<ul>
<li>Easy to use</li>
<li>Name spacing</li>
</ul>
<ul>
<li>All files must be parsed.</li>
<li>Implicitly includes all.</li>
<li>The dependencies cannot solely be resolved by looking at imports.</li>
</ul>
<h3>Module directory imports</h3>
<p>Loading files from name using a known file scheme. E.g. &quot;import foo.bar&quot; loads file <code>project/src/foo/bar.abc</code></p>
<ul>
<li>Easy to use</li>
<li>Name spacing</li>
<li>May be used to only compile what is needed</li>
</ul>
<ul>
<li>Strict path scheme may interact poorly with tools and generic IDEs</li>
<li>Harder to split and merge code between files.</li>
</ul>
<h3>File agnostic module imports</h3>
<p>Parsing all files, assembling modules prior to import resolution. (Similar to Java but without ad hoc path access)</p>
<ul>
<li>Easy to use</li>
<li>Name spacing</li>
</ul>
<ul>
<li>All files must be parsed.</li>
</ul>
<p>How these are then implemented depends a lot on the overall language semantics and must be determined from case to case.</p>

CC BY-SA 4.0
2023-05-17T10:43:27.167Z
2023-05-17T10:43:27.167Z
Nuoji
CC BY-SA 4.0
2023-05-18T09:10:30.180Z
@MaxHeiber the case where multiple modules are defined in a single file is covered by "File agnostic module imports". That said, the above isn't an exhaustive list.
Nuoji
CC BY-SA 4.0
2023-05-19T10:57:17.343Z
Thanks @Nuoji, deleted my comment. But note that for Rust, not all files are parsed. The module graph is walked root-to-leaves, so (even for .rs files), if they aren't referenced they aren't pared iuc. But understoood that the list isn't exhaustive.
Max Heiber
CC BY-SA 4.0
2023-05-20T12:34:08.527Z
I hope people add more examples even though this answer was accepted. There are a lot of different schemes, with each suited better for some languages and worse for others, so investigating many languages is useful when designing imports.
Nuoji
CC BY-SA 4.0
2023-06-08T00:17:59.180Z
It's worth noting that the approach to module imports should, in general, be tied to how you would interface this with make/build systems. They interact in... interesting ways.
Pseudonym
CC BY-SA 4.0
2023-08-24T06:37:43.480Z
Since your "Module directory imports" example seems to mimic Python, it may be worth noting that part of the point is that there *isn’t* necessarily just one corresponding path or even a path at all. In the case of Python, packages can be *transparently* loaded from inside archives and so/dll files by default, and custom loaders can fetch code in practically arbitrary ways.
MisterMiyagi
<p>Broadly, there are two kinds of importing:</p>
<ul>
<li>Import <em>declarations</em>, which make other modules or their exported members available by name in the current scope;</li>
<li>Import <em>statements</em>, which execute the code of another module, including its top-level declarations and any top-level statements it might have, and then bind that module or its exported members to some name or names in the current scope.</li>
</ul>
<p>The distinction is that import statements may have side-effects, whereas declarations do not. Referring to your examples, Java and Rust have import declarations whereas Python has import statements.</p>
<p>There are other ways of handling modules, particularly &quot;includes&quot; (which dump the source code from the included file directly or somewhat directly into the current one), but I won't address those in this answer as it's already quite long. These are normally not called &quot;imports&quot;.</p>
<hr />
<h2>Import declarations</h2>
<p>Import declarations tend to be used in compiled languages, especially languages where statements cannot occur at the top-level of a compilation unit. These are generally the languages where you declare a &quot;main&quot; function as the program's entry point.</p>
<p>They have no side-effects because the modules they import don't have statements at the top-level, so there is nothing to execute just from &quot;importing&quot; those modules. The order of import declarations typically doesn't matter, and they are only used at compile-time; they have no effect at runtime, after the various modules have already been compiled and linked together into a single binary.</p>
<p>Import declarations may be implemented in a compiler by doing two passes over each module ─ one to find the names and types/signatures of all exported members of each module, and then another to resolve the names used in the imports of each module.</p>
<hr />
<h2>Import statements</h2>
<p>Import statements tend to be used in interpreted, dynamic languages, especially languages where declarations of functions, classes, etc. are also statements. These are generally the languages where a program's entry point is just the top of the source file that the interpreter is executing.</p>
<p>They can have side-effects because the modules they import can have statements at the top-level, so the order of import statements can matter. Import statements in dynamic languages generally load the imported modules from the filesystem (or otherwise) at runtime, unless the module has already been loaded and cached by an earlier import of the same module. This is why they only appear in interpreted languages ─ in a compiled and linked binary, the source files (and hence, the separate modules) simply wouldn't exist at runtime.</p>
<p>Import statements will typically be implemented by an interpreter loading, parsing and evaluating the imported source file at runtime, basically invoking the interpreter itself recursively. Once the recursive invocation of the interpreter completes, the exported members may be bound as properties of an object representing the module, and then that object is bound to the name associated with the module, e.g. as in a simple import statement like <code>import foo</code>. Alternatively, an import statement like <code>from foo import bar, baz</code> may destructure this object in order to bind the exported members to simple names.</p>
<p>The loaded modules (and their exported members) are typically also cached, so that if they are imported multiple times, their side-effects only trigger once, and later imports don't have to parse and evaluate the whole source file each time. Additionally, an &quot;empty&quot; or &quot;busy&quot; placeholder may be added to the cache for a module while it is currently being loaded, in order to detect circular imports, which could otherwise cause unbounded recursion. If a circular import is detected, this may either be handled by throwing a runtime error, or just using the &quot;empty&quot; object in the cache instead of the unavailable result.</p>

CC BY-SA 4.0
2023-05-17T10:54:27.120Z
2023-05-17T10:54:27.120Z
kaya3
CC BY-SA 4.0
2024-02-01T09:49:08.337Z
very useful distinction! considering how annoying side-effect can be (e.g. when trying to treeshake JS imports), would you say that "import statements" are just a historic accident of how scripting languages evolved, or are there benefits to doing things that way?
mb21
CC BY-SA 4.0
2024-02-01T14:57:24.203Z
@mb21 Neither. The fact that modules can have side-effects is due to a fundamental feature of scripting languages: statements are allowed at the top-level. So it's not an accident, it's a necessary consequence.
kaya3
CC BY-SA 4.0
2024-02-02T11:18:15.890Z
right... but modules could be special in not allowing top-level-statements, while normal script files (without any exports, only imports) could have top-level statements.
mb21
CC BY-SA 4.0
2024-02-05T17:49:55.263Z
@mb21 I suppose if there is a distinction between module source files and script source files then you could do that; I don't know of any languages which work that way ─ e.g. Javascript has `.js` and `.mjs` extensions but I think modules are allowed to be `.js`, and they can have top-level statements anyway ─ but you could have a language like that, sure. I'm not sure if I would call it a scripting language any more, though, and people might use it like a non-scripting language and just have a "runner" script which does nothing but import and run a `main()` function.
kaya3
# What are the different types of import semantics?
Posted by: username_0 on 2023-05-17
As per my current understanding, there are many different ways that imports, and other related operations, are handled in different programming languages.

For example, `import` in Java merely tells the compiler the full path of a specific package. The package itself can be used without the `import` statement but the full package name is needed. However, this could 'clutter the namespace' when many different packages and if two packages have the same name one needs the full path identifier.

In Python, on the other hand, imports allow the module to be used in the code. The functions of that module cannot be used otherwise. The [Python reference](https://docs.python.org/3/reference/import.html) says that the `import` statement "binds the results of that search to a name in the local scope" and the imported code runs when imported.

Then, of course, there are others ways such as `use` in Rust.

---

My main questions are as follows:

1. What are all the ways that imports can be designed in terms of semantics?
2. What are the pros and cons of each style?
**Tags:** language-comparison semantics imports
> **Comments:**
> **username_0**: I've edited the question to make it more design specific  
> **username_1**: the deeper (but quite broad) question: what different approaches are there towards modularity and module systems?  
---
## 2 answers
### Answer by username_2 on 2023-05-17. Score: 8 | Accepted answer
As a start:

### Direct file text includes

E.g. `#include "foo.h"`

* simple to implement, clear parse ordering

* Poor for dependency tracking and caching.
* Worse performance due to more text to parse.
* No understanding of name spacing.

### Files-as-additional source dependencies

E.g. Go

* May be extended to remote retrieval.
* Easy to understand
* Only compiles what is needed.

* Cyclic dependency must be handled.
* No understanding of name spacing.

### Visibility module imports with ad hoc path access

E.g Java imports.

* Easy to use
* Name spacing

* All files must be parsed.
* Implicitly includes all.
* The dependencies cannot solely be resolved by looking at imports.

### Module directory imports

Loading files from name using a known file scheme. E.g. "import foo.bar" loads file `project/src/foo/bar.abc`

* Easy to use
* Name spacing
* May be used to only compile what is needed

* Strict path scheme may interact poorly with tools and generic IDEs
* Harder to split and merge code between files.

### File agnostic module imports

Parsing all files, assembling modules prior to import resolution. (Similar to Java but without ad hoc path access)

* Easy to use
* Name spacing

* All files must be parsed.

How these are then implemented depends a lot on the overall language semantics and must be determined from case to case.
> **Comments:**
> **username_2** (2023-05-18): @MaxHeiber the case where multiple modules are defined in a single file is covered by "File agnostic module imports". That said, the above isn't an exhaustive list.  
> **username_3** (2023-05-19): Thanks @Nuoji, deleted my comment. But note that for Rust, not all files are parsed. The module graph is walked root-to-leaves, so (even for .rs files), if they aren't referenced they aren't pared iuc. But understoood that the list isn't exhaustive.  
> **username_2** (2023-05-20): I hope people add more examples even though this answer was accepted. There are a lot of different schemes, with each suited better for some languages and worse for others, so investigating many languages is useful when designing imports.  
> **username_4** (2023-06-08): It's worth noting that the approach to module imports should, in general, be tied to how you would interface this with make/build systems. They interact in... interesting ways.  
> **username_5** (2023-08-24): Since your "Module directory imports" example seems to mimic Python, it may be worth noting that part of the point is that there *isn’t* necessarily just one corresponding path or even a path at all. In the case of Python, packages can be *transparently* loaded from inside archives and so/dll files by default, and custom loaders can fetch code in practically arbitrary ways.  
---
### Answer by username_6 on 2023-05-17. Score: 9
Broadly, there are two kinds of importing:

* Import *declarations*, which make other modules or their exported members available by name in the current scope;
* Import *statements*, which execute the code of another module, including its top-level declarations and any top-level statements it might have, and then bind that module or its exported members to some name or names in the current scope.

The distinction is that import statements may have side-effects, whereas declarations do not. Referring to your examples, Java and Rust have import declarations whereas Python has import statements.

There are other ways of handling modules, particularly "includes" (which dump the source code from the included file directly or somewhat directly into the current one), but I won't address those in this answer as it's already quite long. These are normally not called "imports".

---

## Import declarations

Import declarations tend to be used in compiled languages, especially languages where statements cannot occur at the top-level of a compilation unit. These are generally the languages where you declare a "main" function as the program's entry point.

They have no side-effects because the modules they import don't have statements at the top-level, so there is nothing to execute just from "importing" those modules. The order of import declarations typically doesn't matter, and they are only used at compile-time; they have no effect at runtime, after the various modules have already been compiled and linked together into a single binary.

Import declarations may be implemented in a compiler by doing two passes over each module ─ one to find the names and types/signatures of all exported members of each module, and then another to resolve the names used in the imports of each module.

---

## Import statements

Import statements tend to be used in interpreted, dynamic languages, especially languages where declarations of functions, classes, etc. are also statements. These are generally the languages where a program's entry point is just the top of the source file that the interpreter is executing.

They can have side-effects because the modules they import can have statements at the top-level, so the order of import statements can matter. Import statements in dynamic languages generally load the imported modules from the filesystem (or otherwise) at runtime, unless the module has already been loaded and cached by an earlier import of the same module. This is why they only appear in interpreted languages ─ in a compiled and linked binary, the source files (and hence, the separate modules) simply wouldn't exist at runtime.

Import statements will typically be implemented by an interpreter loading, parsing and evaluating the imported source file at runtime, basically invoking the interpreter itself recursively. Once the recursive invocation of the interpreter completes, the exported members may be bound as properties of an object representing the module, and then that object is bound to the name associated with the module, e.g. as in a simple import statement like `import foo`. Alternatively, an import statement like `from foo import bar, baz` may destructure this object in order to bind the exported members to simple names.

The loaded modules (and their exported members) are typically also cached, so that if they are imported multiple times, their side-effects only trigger once, and later imports don't have to parse and evaluate the whole source file each time. Additionally, an "empty" or "busy" placeholder may be added to the cache for a module while it is currently being loaded, in order to detect circular imports, which could otherwise cause unbounded recursion. If a circular import is detected, this may either be handled by throwing a runtime error, or just using the "empty" object in the cache instead of the unavailable result.
> **Comments:**
> **username_7** (2024-02-01): very useful distinction! considering how annoying side-effect can be (e.g. when trying to treeshake JS imports), would you say that "import statements" are just a historic accident of how scripting languages evolved, or are there benefits to doing things that way?  
> **username_6** (2024-02-01): @mb21 Neither. The fact that modules can have side-effects is due to a fundamental feature of scripting languages: statements are allowed at the top-level. So it's not an accident, it's a necessary consequence.  
> **username_7** (2024-02-02): right... but modules could be special in not allowing top-level-statements, while normal script files (without any exports, only imports) could have top-level statements.  
> **username_6** (2024-02-05): @mb21 I suppose if there is a distinction between module source files and script source files then you could do that; I don't know of any languages which work that way ─ e.g. Javascript has `.js` and `.mjs` extensions but I think modules are allowed to be `.js`, and they can have top-level statements anyway ─ but you could have a language like that, sure. I'm not sure if I would call it a scripting language any more, though, and people might use it like a non-scripting language and just have a "runner" script which does nothing but import and run a `main()` function.  
---
