Skip to content

Latest commit

 

History

History
698 lines (552 loc) · 42.1 KB

DIP1005.md

File metadata and controls

698 lines (552 loc) · 42.1 KB

Dependency-Carrying Declarations

Field Value
DIP: 1005
Author: Andrei Alexandrescu (andrei@erdani.com)
Implementation: n/a
Status: Draft

Abstract

A Dependency-Carrying Declaration is a D declaration that does not require any import declaration to be present outside of it. Such declarations encapsulate their own dependencies, which makes dependency relationships more fine-grained than traditional module- and package-level dependencies.

Currently D allows definitions to carry their own dependencies by means of the recently-added scoped import declarations feature. However, this is not possible with symbols that are present in the symbol declaration itself, for example as function parameter types or template constraints. The limitation reduces the applicability and power of scoped imports. This DIP proposes a language addition called "inline import", which allows any function and aggregate D declarations to be transformed into a Dependency-Carrying Declaration.

Rationale

Consider the following D code:

import std.datetime;
import std.stdio;
void log(string message)
{
    writeln(Clock.currTime, ' ', message);
}

Traditionally (though not required by the language), imports are placed at the top of the module and then implicitly used by the declarations in the module. This has two consequences. First, the setup establishes a dependency of the current module on two other modules or packages (and by transitivity, on the transitive closure of the modules/packages those depend on). Second, it defines a relationship at distance between the log function and the imports at the top. As a immediate practical consequence, log cannot be moved across the codebase without ensuring the appropriate import declarations are present in the target module.

Let us compare and contrast the setup above with the following:

void log(string message)
{
    import std.datetime;
    import std.stdio;
    writeln(Clock.currTime, ' ', message);
}

This layout still preserves the dependency of the current module on the two std entities because the compiler would need them in order to compile log. However, the relationship at distance disappears---log encapsulates its dependencies, which migrate together with it. We call such a declaration that does not depend on imports outside of it, a Dependency-Carrying Declaration.

Consider now the case when log is a generic function:

void log(T)(T message)
{
    import std.datetime;
    import std.stdio;
    writeln(Clock.currTime, ' ', message);
}

In this case, the current module depends on std.datetime and std.stdio only if it uses log directly from within a non-template function (including a unittest). Otherwise, the log generic function is only parsed to an AST (no symbol lookup) and not processed further. Should another module import this module and use log, the dependency is realized because log needs to be compiled. This makes the module that actually uses log---and only it---dependent on std.datetime and std.stdio, in addition of course to the module that defines log.

The same reasoning applies to template struct, class, or interface definitions:

struct FileBuffer(Range)
{
    import std.stdio;
    private File output;
    ...
}

Such an entity only realizes the dependencies when actually instantiated, therefore moving the carried dependencies to the point of instantiation.

The analysis above reveals that Dependency-Carrying Declarations have multiple benefits:

  • Specifies dependencies at declaration level, not at module level. This allows reasoning about the dependency cost of declarations in separation instead of aggregated at module level.
  • If all declarations use Dependency-Carrying style and there is no top-level import, human reviewers and maintainers can immediately tell where each symbol in a given declaration comes from. This is a highly nontrivial exercise without specialized editor support in projects that pull several other modules and packages wholesale. Even a project newcomer could gather an understanding of a declaration without needing to absorb an arbitrary amount of implied context from the declaration at the top of the module.
  • Dependency-Carrying Declarations are easier to move around, making for simpler and faster refactorings.
  • Dependency-Carrying Declarations allow scalable template libraries. Large libraries (such as D's standard library itself) are customarily distributed in packages and modules grouped by functional areas, such that client code can use the library without needing to import many dozens of small modules, each for one specific declaration. Conversely, client code often imports a package or module to use just a small fraction of it. Distributing a template library in the form of Dependency-Carrying Declarations creates a scalable, pay-as-you-go setup: The upfront cost of importing such a module is only that of parsing the module source, which can reasonably be considered negligible in the economy of any build. Then, dependencies are pulled on a need basis depending on the declarations used by client code.

Dependency-Carrying Declarations also have drawbacks:

  • If most declarations in a module need the same imports, then factoring them outside the declarations at top level is simpler and better than repeating them.
  • Traditional dependency-tracking tools such as make and other build systems assume file-level dependencies and need special tooling (such as rdmd) in order to work efficiently.
  • Dependencies at the top of a module are easier to inspect quickly than dependencies spread through the module.

On the whole, experience with using Dependency-Carrying Declarations in the D standard library suggests that the advantages outweigh disadvantages considerably. Of all import declarations in the D standard library, only about 10% are top-level---all others are local. Using local imports is considered good style in D code.

There are, however, declarations that cannot be reformulated in Dependency-Carrying Declaration form. Consider a simple example of a non-template function declaration:

import std.stdio;
void process(File input);

It is not possible to declare process without importing std.stdio outside of it. Another situation is that of template constraints:

import std.range;
struct Buffered(Range) if (isInputRange!Range) { ... }

There are combinations as well:

import std.range, std.stdio;
void fun(Range)(Range r, File f) if (isInputRange!Range) { ... }

In all of these cases the only way to state the declarations is to make the symbols they use visible in the scope outside it, which in turn requires the use of import statements separately from the declarations that use them.

This, combined with the ubiquitous use of static introspection and constrained templates, has led to an unpleasant situation in the D standard library whereby it is practically impossible to eliminate imports at the top level. To date, in spite of a large effort to place imports locally, the dependency structure of the D standard library has not clarified visibly because of this limitation.

Workaround: Increasing Granularity of Modules

The obvious workaround to the problem that dependencies must be module-level is to simply define many small modules---in the extreme, one per declaration. Each such small module would import the modules on which that declaration depends. For convenience, package.d modules may be provided to aggregate several modules.

This approach has the following tradeoffs:

  • Reduces unnecessary parsing: if used appropriately, only code that is used actually gets parsed.
  • Increases I/O: more small files cause more I/O activity. This may cause problems with large projects on shared network drives.
  • Library authors face a tension between organizing code in logical units pertaining to the problem domain, and organizing code according to low-level dependency details. They will also be forced to routinely navigate large file hierarchies with many files, which may not be the preferred project organization.
  • Client code must choose between using detailed import lists or convenient package.d imports.
    • If convenient grouped imports are used, the advantage of fine-grained dependency control is lost.
    • If detailed import lists are used, they are verbose and must be updated often. Because it is not an error to not use an imported symbol, over time the import lists will become a large set including the actually needed set, thus eroding the advantage of the approach in the first place. Special tooling and maintenance tasks would be needed to remove unneeded imports once in a while.

Such a project organization may be affordable for small and medium-sized projects and is not precluded by this proposal. An example of such an approach can be found in the mach.d library. It is organized as a small number of related declarations (such as canAdjoin, Adjoin, and AdjoinFlat) per module, along with documentation and unit tests. Currently mach.d has about 49 KLoC (as wc counts) distributed across 348 files. The average file length is 141 LoC and the median is 94 LoC. Each package offers collected package.d modules for convenience that import all small modules in the current package. Client code has the option of using more verbose single imports for precise dependencies, or these terse coarse-granular imports at the cost of less precision in dependency management.

Assuming module size is a project invariant, the number of files scales roughly with project size. This means mach.d would need 2000 files to scale up to the same size as the D standard library (about 6x larger) or about 7000 files to scale up to 1 MLoC. For comparison, Facebook's hhvm project includes about 1 MLoC of C++ code, distributed across 1235 headers and 1187 implementation files (median across all is 141 LoC/file, average 379 LoC/file without counting documentation or unittests). The prospect of tripling the number of files in the project would be tenuous, even if the payoff would be superior dependency management. (For another comparison point, the D Standard Library itself has about 282 KLoC distributed across 137 modules with median length 903 LoC and average length 2055 LoC; these include full documentation and unittests.)

We consider such a workaround nonscalable and undesirable for large-scale projects. It puts in tension the convenience of coarse-granular organization and the organizational advantage of of fine-grained dependencies. The workaround also adds additional project management chores (refreshing the lists of imports, enforcing disciplined use). This proposal eliminates the tension between the two, making them affordable simultaneously.

Workaround: Are Local Imports Good Enough?

A legitimate question to ask is whether consistent use of local imports wherever possible would be an appropriate approximation of the Dependency-Carrying Declarations goal with no change in the language at all. The reasoning is that most dependencies are actually needed by implementations, not declarations; once all local imports are moved where needed by the implementation, only a small residue of imports would remain at top level.

To verify that hypothesis, we ran a test against the D Standard Library. Fortunately this is made easier by the fact that a concerted effort has already been spent on making most imports local; for a non-exhaustive list, refer to PR4361, PR4365, PR4370, PR4373, PR4379, PR4392, PR4467. This work has taken the standard library from all imports being top-level to the current setup whereby 4605 import statements are nested and only 426 (under 8.5%) have remained at top level.

It would appear that the process has been successful: only a small fraction of all imports remained at top level; the vast majority have been pushed down into implementation. This ought to have radically improved the dependency structure of the standard library. However, to estimate the real cost of top-level imports, transitivity must be taken into account: any imported module triggers in turn its own imports, transitively. The entirety of imported modules is of importance for dependency management and build times.

To assess the real cost of the 8.5% top-level imports, we ran the following experiment. We compiled in separation each module in the D standard library, monitoring the number of imports. The compilation included the -unittest flag, which instantiates virtually all templates defined in the current module and therefore executes all local imports. The number obtained is the number of imports that must be transitively acquired assuming everything in the imported module is used. (It should be noted that the numbers obtained this way are slightly higher because some code uses version(unittest) to conditionally import modules used only during unittesting.)

We then repeated the compilation without -unittest, which estimates the cost (in transitive imports) of building the respective module.

Finally, we compiled a separate small file that simply imports the measured module without using any of it. This estimates the overhead (in transitive imports) of importing the respective module without using any of it.

Numbers in the first column reflect the total dependencies of the module. Numbers in the second column are smaller because they reflect dependency deferral---only non-template functions are compiled. If local imports are used successfully, the difference between the first and second column should be significant if the module in question defines many template functions and relatively few non-template functions. The third column is the direct measure of the efficacy of using local imports because it shows the true overhead in imports for an imported module that is not used at all. If local imports are enough, that number should be 0 or close to 0.

The table in Appendix A lists all of these results, sorted by the count of unittest imports in descending order. The table below shows the median and average of the imports count for the entire standard library.

Aggregate Imports (unittest) Imports (compile) Imports (top)
Median 45 18 13
Average 103.1 56.5 46.9

The numbers show that matters have improved but are far from optimum. A 12x reduction in top-level import declarations has led to a less than 2.2x improvement in the average number of files imported. Further improvements would require a major project reorganization. We conclude that local imports are not enough to ensure an efficient dependency structure, even after discounting the claimed advantages for maintainability, documentation, and code clarity.

Inline imports

We propose an addition to the D language that allows the use of the keyword import as part of any function and aggregate declaration. When that syntax is used, it instructs the compiler to execute the import before looking up any names in the declaration. To clarify by means of example, the previous declarations would be rewritten as:

with (import std.stdio) void process(File input) ;
with (import std.range) struct Buffered(Range) if (isInputRange!Range)
{
    ...
}

With this syntax, the import is executed only if the declared name (process) is actually looked up. Of course, simple caching will make several imports of the same module as expensive as the first. The following section motivates the use of the existing with statement as a declaration.

Refresher on the with Statement

The with statement is mainly used for manipulating multiple fields of an elaborate value. However, with is more general, accepting a type or a template instance (which is essentially a symbol table) as an argument. Consider:

enum EnumType { enumValue = 42 }
struct StructType { static structValue = 43; alias T = int; }
class ClassType { static classValue = 44; alias T = double; }
template TemplateType(X) { auto templateValue = 45; alias T = X; }
void main()
{
    with (EnumType) { void fun(int x = enumValue); }
    with (StructType) { void gun(T x = structValue); }
    with (ClassType) { void hun(T x = classValue); }
    with (TemplateType!int) { void iun(T x = templateValue); }
}

These declarations all work as expected and depend on names scoped within the type or template instance passed to with. This brings the with statement semantically close to the lookup rules needed for this DIP.

We propose that with (Type) and with (TemplateInstance) are allowed as declarations (not only statements). The language rules would be changed as follows:

  • Inside any function, all uses of with are statements and obey the current language rules.
  • Everywhere else, with (expression) is not allowed. with (Type) and with (TemplateInstance) are always declarations and do not introduce a new scope. Lookup of symbols inside the with declarations is similar to lookup inside the with statement: symbols in the scopes of Type or TemplateInstance have priority (hide) symbols outside the with declaration.

In addition, we propose the statement and declaration with (import ImportList). ImportList is any syntactical construct currently accepted by the import declaration. The with (import ImportList) declaration obeys the following rules:

  • Inside any function, with (Import ImportList) is a statement that introduces a scope. Inside the with, lookup considers the import local to the declaration (similar to the current handling of nested imports).
  • Everywhere else, with (Import ImportList) is always a declaration and does not introduce a new scope. Lookup of symbols is the same as for the statement case.

This extension removes an unforced limitation of the current with syntax (allows it to occur at top level) and introduces a natural extension from symbol tables present in a type or template instance, to symbol tables imported from a module. The drawback of this choice is the potentially confusing handling of scopes: the with statement introduces a scope, whereas the with declaration does not.

The with Declaration

The usual grammar of the import ImportList; declaration applies inside the with (import ImportList) declaration, with the following consequences:

  • The usual lookup rules apply, for example either with (import std.range) or the more precise with (import std.range.primitives) may be used to look up isInputRange.
  • Specific imports can be present as in with (import std.range : isInputRange) or with (import std.range.primitives : isInputRange).
  • Renamed imports may be present as in with (import std.range : isInput = isInputRange). This specification precludes the use of isInputRange and requires the use of isInput instead.

The static import feature is also available with this syntax: with (static import ImportList).

Inline imports apply to all declarations (template or not) and may guard multiple declarations:

with (import module_a : A, B)
{
    struct Widget(T = A) { ... }
    alias C = B;
}
Widget!int g_widget;

As mentioned, with declarations do not introduce a scope so Widget above is visible outside the with declaration, but A and B are not.

Inline imports apply to all declarations. This includes the with declaration itself, having the consequence that multiple with import declarations may be applied in a cascading manner:

with (import module_a : A)
with (import module_b : B)
A fun(B) { ... }

Lookup rules

When the name of a Dependency-Carrying Declaration is found via lookup, its corresponding inline imports are executed. Then the name is resolved.

The visibility of the imported symbol(s) lasts through the end of the with declaration. That includes function contracts and bodies and also module constructors and inner functions.

The inline imports have priority over existing imports visible to the declaration. This is so as to avoid other names present in the scope to have equal footing with names immediately present in the declaration. The lookup is equivalent to placing the inline imports in a scope unique to the declaration, where they take precedence in name resolution just like scoped imports per the current language rules. Example:

import module_b;
with (import module_a) void fun(X value) { ... }

The name X is looked up as if the code was structured as follows:

import module_b;
{
    import module_a;
    void fun(X value) { ... }
}

This equivalent code, however, is not legal at top level. In that case we can artificially introduce an imaginary template to analyze lookup on compilable code:

import module_b;
template __unused()
{
    import module_a;
    void fun(X value) { ... }
}

The symbol X is looked up per the current language rules in the working code above.

If two or more imports within the same with declaration define the same name, name resolution across these is the same as if the imports were top-level.

If a module defines a symbol at top level and then imports a module, lookup proceeds similarly with local imports. Consider:

int writeln;
with (import std.stdio)
void main() { writeln("hello, world"); }

This code is in error because writeln has type int. However, the following code is correct because it specifies the symbol name explicitly:

int writeln;
with (import std.stdio : writeln)
void main() { writeln("hello, world"); }

Examples

Below are a few examples taken from the standard library:

with (import std.meta, std.range, std.traits)
auto uninitializedArray(T, I...)(I sizes) nothrow @system
if (isDynamicArray!T && allSatisfy!(isIntegral, I) &&
    hasIndirections!(ElementEncodingType!T))
{
    ...
}

Alternatively, the declaration may specify the exact symbols needed by using multiple imports:

with (import std.meta : allSatisfy)
with (import std.range : ElementEncodingType)
with (import std.traits : hasIndirections, isDynamicArray, isIntegral)
auto uninitializedArray(T, I...)(I sizes) nothrow @system
if (isDynamicArray!T && allSatisfy!(isIntegral, I) &&
    hasIndirections!(ElementEncodingType!T))
{
    ...
}

Alternative: Lazy Imports

Assume all imports are lazy without any change in the language. (This has already been implemented in the SDC compiler.) The way the scheme works, all imports seen are not yet executed but instead saved in a list of package/module names. Following that, the actual imports are triggered by one of two situations.

First, consider the current module looks up a fully specified name:

import module_a, module_b;
void fun(T)(T value) if (module_a.condition!T)
{
    return module_b.process(value);
}
void fun(T)(T value) if (is(T == int)) { ... }

In this situation:

  • If fun is never looked up, neither module_a nor module_b needs to be loaded.
  • If fun(42) is used, even though the second overload is a match, then module_a must be loaded in order to ensure that module_a.condition!int is false so as to avoid ambiguity.
  • If fun is called with a non-int value, module_a is loaded to evaluate the template constraint. If the constraint is true, then module_b is also loaded so as to look up process.

Let us note that full specification of symbols used may be enabled with ease by using the static import feature. We will henceforth refer to this setup as "the static import setup".

Second, consider the situation (arguably more frequent in today's D code) when the current module does not fully specify names used. Instead, it imports the appropriate modules and relies on lookup to resolve symbols appropriately:

import module_a, module_b;
void fun(T)(T value) if (condition!T)
{
    return process(value);
}
void fun(T)(T value) if (is(T == int)) { ... }

In this situation:

  • If fun is never used, neither module must be loaded.
  • If fun is looked up, it will trigger an unspecified lookup for condition. This will trigger loading of both module_a and module_b (and generally all imports in the current module) so as to look up condition and ensure no ambiguity.

The same applies to the setup in which condition is imported selectively from module_a but module_b is entirely imported:

import module_a : condition;
import module_b;
void fun(T)(T value) if (condition!T)
{
    return process(value);
}
void fun(T)(T value) if (is(T == int)) { ... }

In this case, module module_b still needs to be opened if fun is looked up to ensure no ambiguity exists for condition.

Finally, there is the case when all imports specify the list of symbols imported:

import module_a : condition;
import module_b : process;
void fun(T)(T value) if (condition!T)
{
    return process(value);
}
void fun(T)(T value) if (is(T == int)) { ... }

In this case, fine-grained loading of modules is possible: each module is loaded only if a symbol inside it is used. We refer to this setup as "the selective import setup".

To generalize the observations above, fine-grained loading of modules is possible under either (or a combination of) the following circumstances: (a) the static import setup; (b) the selective import setup.

The advantages of such approaches are:

  • Fine-grained loading of imports is achieved with no changes in the language definition, only the implementation.
  • Project discipline may be enforced with relative ease, either manually or by means of simple tools. The rule is: "All private imports must be either static import or selective import".

The disadvantages are:

  • The fine-grained dependency structure is not attained by the selective import approach. A declaration using unspecified names does not clarify which imports it implicitly relies on. The relationship at distance remains between the import and the use thereof.
  • The static import setup does not share the issue above, at the cost of being cumbersome to use---all imported symbols must use full lookup everywhere. A reasonable engineering approach would be to define shorter names:
    static import std.range.primitives;
    alias isInputRange = std.range.primitives.isInputRange;
    alias isForwardRange = std.range.primitives.isInputRange;
    ...
Such scaffolding is of course undesirable in the first place. Also, at least by the current language rules, such `alias` definitions would need to load the module anyway so as to ensure the name does exist. In order for this idiom to work, it would require subtle changes to the language that specify how certain `alias` declarations are exempt from early checking and delayed to the first actual use.
  • In either setup, imports are collapsed into their union, usually at the top of the module. Such lists grow out of sync with the actual code because during maintenance the programmer working on one declaration is not motivated to simultaneously alter a module-level import list shared by all declarations in the module. Over time, the imports grow into a superset of the actual depedencies used by the code, and do not reflect which declarations cause which imports even when accurate.
  • The "carrying" aspect is lost: any migration of a declaration to another module must be followed by awkwardly doing surgery on the import list of the receiving module. Again, the migration may leave unused imports in the module the declaration is taken from. The only recourse to keeping the import list in sync is special tooling or time-consuming discipline (search the module for uses, attempt recompilation).

Although we consider introducing lazy imports an improvement over the current state of affairs, our assessment is that such a feature would fall short of truly allowing a project to rein in its dependency structure.

We have experimented with converting the standard library module std.array to one of the two idioms. Conversion to either the "static import" form or the "selective import" form may be by brute force by using dedicated tooling: first generate code that enumerates all symbols in the module, then eliminate them one by one and attempt to rebuild. Such an approach is time-consuming and would be only used at rare intervals.

The manual conversion of std.array to the "static import" form is shown here. It leads to the expected lengthening of the symbols used in declarations, which appears to eliminate one disadvantage by introducing another. Also the manual conversion process turned out to be prohibitively difficult; we would only recommend this conversion using automated tooling.

The manual conversion of std.array to the "selective import" form is shown here. Conversion was successful but because it collapses all imports at the top, it does not make it much easier to identify e.g. what dependencies would be pulled if a given artifact in std.array were used. Again the manual process was highly nontrivial.

Syntactic Alternatives

There are a number of alternative approaches that have been (and some still are) considered.

  • Specify import in a manner reminiscent of attributes:
    void process(File input) import (std.stdio);
    struct Buffered(import std.range)(Range) if (isInputRange!Range)
    {
        ...
    }
This form had significant differences from both the property syntax and the existing `import` syntax.
  • Add syntax to allow for an optional import declaration inside declarations:
    void process(import std.stdio)(File input);
    struct Buffered(import std.range)(Range) if (isInputRange!Range)
    {
        ...
    }

This has the advantage of being less verbose in case the same module is looked up several times. The disadvantages are a heavier and more ambiguous syntax (two sets of parens for nontemplates, three for templates) and an unclear relationship between the imported entities and the symbols used in the declaration.

  • Use import as a pseudo-package such that symbols are written like this:
        void process(import.std.stdio.File input);
        struct Buffered(Range) if (import.std.range.isInputRange!Range)
        {
            ...
        }

Such an option has an ambiguity problem shown by Timon Gehr: is import.std.range.isInputRange looking up symbol isInputRange in module/package std.range, or the symbol range.isInputRange (e.g. a struct member) in module/package std?

  • Stay as close to the existing import syntax as possible. This has the advantage of being instantly recognized, but the disadvantage of looking out of place within the declaration:
        void process(File input) import std.stdio;
        struct Buffered(Range) if (isInputRange!Range)
        import std.range;
        {
            ...
        }

One syntactical issue is that in this case the semicolon ending the import may or may not end the declaration; the scanner (and human reader) would need to look ahead to figure whether a definition continues (by means of an open brace, the in keyword, or the out keyword), or the declaration ends there.

  • Alternatively, the semicolon might be omitted in the approach above. This causes no syntactical ambiguity but makes the hanging import declaration even more out of place:
        void process(File input) import std.stdio;
        struct Buffered(Range) if (isInputRange!Range)
        import std.range
        {
            ...
        }
  • Use property syntax.
    @deps!({import std.stdio; pragma(lib, "curl"); }):
    // applies to 1 below
    @deps!({import std.range})
    void fun(T)(isInputRange!T){} // depends on both deps
    void fun2(File file){}  // depends on 1st deps ending with ':'
This adds no syntax to the language, only semantics. Another advantage is it supports other artifacts aside from `import` such as `pragma` illustrated above. The compiler would recognize `@deps` as a special attribute and would only allow certain constructs inside the lambda inside the attribute. The disadvantage of this approach is that of any non-specialized syntax---it is relatively unstructured and more difficult to follow.
  • Add no syntax at all; allow top-level braces. The most Spartan of all syntaxes simply allows top-level scopes:
{
    import std.stdio;
    void fun(File f) { ... }
}
void gun(); // no access to std.stdio here
We believe this syntax has similar advantages and disadvantages to C++ namespaces, which force indentation of everything within the scope. This has been long considered a nuisance of C++ namespaces, worked around in projects in one of two ways. One possibility is to define macros that enter and leave a namespace, see e.g. [1](https://www.ncbi.nlm.nih.gov/IEB/ToolBox/CPP_DOC/lxr/ident?i=BEGIN_NAMESPACE), [2](http://stackoverflow.com/questions/37907516/what-is-the-macro-qt-begin-namespace-mean-in-qt-5), [3](https://connect.microsoft.com/VisualStudio/feedback/details/1154097/c-intellisense-doesnt-work-correctly-with-namespace-macros), [4](http://stackoverflow.com/questions/2331557/declaring-namespace-as-macro-c). Another possibility is to not indent the code and have special editor indentation rules, see e.g. [1](http://stackoverflow.com/questions/13825188/suppress-c-namespace-indentation-in-emacs), [2](http://stackoverflow.com/questions/3727862/is-there-any-way-to-make-visual-studio-stop-indenting-namespaces), [3](http://stackoverflow.com/questions/2549019/how-to-avoid-namespace-content-indentation-in-vim). We expect similar issues and workarounds with such a feature in D.

Breaking changes / deprecation process

We do not anticipate any breaking changes brought by this language addition. The syntactical construct proposed is currently not accepted.

The inline imports specified with a declaration do not affect its type (e.g. the function type for a function declaration).

The changes to declaration syntax will impact third-party documentation generators, so they would need to be updated. There is an advantage herein---documentation generators (including ddoc itself) can show the user the dependencies that each declaration would incur.

Future Possibilities and Directions

Inline and scoped imports offer the option of better handling of static module constructors. Currently, modules that mutually import one another (either directly or through a longer chain) cannot simultaneously define shared static this() constructors. The reason is that, again, dependencies are computed at module level.

If instead modules have no top-level dependencies, then the compiler is able to compute the narrow set of dependencies needed for executing the static module constructor. The static constructor may be (a) a part of a with declaration, (b) use local imports within, and (c) call other functions within the module that have their own dependencies. For example:

// assume no top-level import
with (module_a) void fun(T)()
{
    import module_b;
    return gun();
}
with (module_c)
static shared this()
{
    import module_d;
    fun!int;
}

In this case, the module constructor depends (only) on module_a, module_b, module_c, and module_d. The full information is confined within the current module so it is inferrable during separate compilation.

Copyright & License

Copyright (c) 2016 by the D Language Foundation

Licensed under Creative Commons Zero 1.0

Reviews

Informal forum review

Appendix A: Imported Modules in the D Standard Library

The table below displays the total (transitive) imports needed for compiling separately each module in the D Standard Library. The first column is the module filename. The second column is the total number of imports needed to compile the module for unittesting (which instantiates virtually all templates and therefore pulls their local imports). The third column is the total number of imports needed to compile the module. The fourth column is the overhead in top-level imported modules needed to import the module without using it.

File Imports (unittest) Imports (compile) Imports (top)
std/net/curl.d 111 106 98
std/parallelism.d 88 73 59
std/algorithm/mutation.d 88 16 17
std/stdio.d 86 56 20
std/experimental/ndslice/package.d 86 20 21
std/datetime.d 86 82 57
std/experimental/logger/package.d 85 81 82
std/range/package.d 84 20 21
std/experimental/logger/multilogger.d 84 79 80
std/socket.d 83 62 56
std/experimental/logger/nulllogger.d 83 79 80
std/experimental/logger/core.d 83 78 79
std/experimental/logger/filelogger.d 82 78 79
std/typecons.d 81 14 15
std/process.d 81 76 52
std/algorithm/comparison.d 81 17 18
std/file.d 80 71 57
std/experimental/allocator/building_blocks/free_list.d 79 20 21
std/zip.d 78 65 61
std/path.d 78 60 57
std/mmfile.d 78 63 59
std/experimental/allocator/building_blocks/stats_collector.d 78 20 21
std/conv.d 77 29 6
std/base64.d 76 5 6
std/random.d 73 47 5
std/uuid.d 69 45 26
std/concurrency.d 69 64 63
std/traits.d 68 2 3
std/experimental/ndslice/slice.d 68 17 18
std/experimental/allocator/package.d 64 29 26
std/regex/internal/generator.d 62 0 1
std/algorithm/iteration.d 62 5 6
std/regex/package.d 61 32 33
std/regex/internal/tests.d 61 35 36
std/regex/internal/tests3.d 61 41 42
std/regex/internal/tests2.d 61 52 53
std/net/isemail.d 61 37 17
std/experimental/allocator/building_blocks/region.d 61 21 22
std/algorithm/sorting.d 60 22 23
std/regex/internal/shiftor.d 59 33 34
std/regex/internal/bitnfa.d 59 43 38
std/algorithm/searching.d 59 17 18
std/algorithm/setops.d 58 23 24
std/experimental/allocator/building_blocks/kernighan_ritchie.d 57 16 17
std/uni.d 55 50 7
std/regex/internal/parser.d 55 50 50
std/experimental/ndslice/selection.d 54 18 19
std/experimental/allocator/building_blocks/affix_allocator.d 54 0 1
std/xml.d 53 49 1
std/range/primitives.d 53 3 4
std/functional.d 53 4 4
std/bigint.d 53 38 31
std/regex/internal/thompson.d 52 31 32
std/regex/internal/backtracking.d 52 48 45
std/numeric.d 52 30 28
std/experimental/allocator/building_blocks/bitmapped_block.d 52 21 22
std/container/rbtree.d 52 13 14
std/regex/internal/ir.d 51 47 31
std/digest/digest.d 51 4 5
std/csv.d 51 13 8
std/array.d 51 6 7
std/internal/math/gammafunction.d 50 44 9
std/format.d 50 10 11
std/experimental/allocator/typed.d 50 25 26
std/container/util.d 50 0 1
std/bitmanip.d 50 36 27
std/container/array.d 49 6 7
std/variant.d 48 37 16
std/utf.d 48 23 19
std/string.d 48 35 27
std/outbuffer.d 48 28 5
std/meta.d 48 2 3
std/json.d 48 44 12
std/getopt.d 48 44 28
std/experimental/allocator/showcase.d 48 35 26
std/experimental/allocator/building_blocks/allocator_list.d 48 23 24
std/digest/sha.d 48 34 7
std/experimental/typecons.d 47 15 16
std/experimental/allocator/building_blocks/package.d 47 37 38
std/exception.d 47 6 5
std/math.d 46 6 7
std/container/binaryheap.d 46 5 6
std/experimental/allocator/building_blocks/scoped_allocator.d 45 20 21
std/encoding.d 45 39 40
std/complex.d 45 40 12
std/zlib.d 44 32 25
std/uri.d 44 38 22
std/digest/ripemd.d 44 32 6
std/digest/crc.d 44 32 6
std/experimental/allocator/building_blocks/fallback_allocator.d 43 20 21
std/experimental/allocator/building_blocks/bucketizer.d 43 0 1
std/digest/md.d 43 32 6
std/experimental/allocator/mallocator.d 42 21 21
std/experimental/allocator/building_blocks/quantizer.d 42 20 21
std/experimental/allocator/building_blocks/free_tree.d 42 20 21
std/experimental/ndslice/iteration.d 41 18 19
std/experimental/allocator/gc_allocator.d 41 23 22
std/container/dlist.d 41 5 6
std/experimental/ndslice/internal.d 40 17 18
std/container/slist.d 40 1 2
std/digest/hmac.d 38 5 6
std/experimental/allocator/building_blocks/segregator.d 36 20 21
std/concurrencybase.d 34 18 19
std/internal/math/biguintcore.d 33 11 10
std/internal/cstring.d 32 21 22
std/digest/murmurhash.d 32 5 6
std/internal/test/dummyrange.d 31 17 17
std/range/interfaces.d 30 4 5
std/c/linux/pthread.d 30 30 31
std/c/linux/linux.d 29 29 30
std/ascii.d 29 0 1
std/algorithm/package.d 27 26 27
std/internal/scopebuffer.d 24 6 7
std/signals.d 22 21 22
std/experimental/allocator/common.d 22 22 20
std/c/linux/socket.d 21 21 22
std/internal/digest/sha_SSSE3.d 20 20 1
std/mathspecial.d 19 9 10
std/container/package.d 19 19 20
std/internal/math/errorfunction.d 17 7 8
std/experimental/allocator/building_blocks/null_allocator.d 15 15 16
std/c/stdlib.d 14 14 15
std/experimental/allocator/mmap_allocator.d 12 12 1
std/c/linux/termios.d 12 12 13
std/stdint.d 11 11 12
std/c/wcharh.d 11 11 12
std/c/time.d 11 11 12
std/c/stdio.d 11 11 12
std/demangle.d 6 6 1
std/c/stdarg.d 4 4 5
std/windows/syserror.d 3 3 4
std/c/process.d 3 3 4
std/typetuple.d 2 2 3
std/c/math.d 2 2 3
std/windows/iunknown.d 1 1 2
std/internal/unicode_norm.d 1 1 2
std/internal/unicode_grapheme.d 1 1 2
std/internal/unicode_decomp.d 1 1 2
std/internal/unicode_comp.d 1 1 2
std/c/string.d 1 1 2
std/c/stddef.d 1 1 2
std/c/locale.d 1 1 2
std/c/linux/tipc.d 1 1 2
std/c/fenv.d 1 1 2
std/windows/registry.d 0 0 1
std/windows/charset.d 0 0 1
std/system.d 0 0 1
std/stdiobase.d 0 0 1
std/internal/windows/advapi32.d 0 0 1
std/internal/unicode_tables.d 0 0 1
std/internal/test/uda.d 0 0 1
std/internal/processinit.d 0 0 1
std/internal/math/biguintx86.d 0 0 1
std/internal/math/biguintnoasm.d 0 0 1
std/internal/encodinginit.d 0 0 1
std/c/windows/winsock.d 0 0 1
std/c/windows/windows.d 0 0 1
std/c/windows/stat.d 0 0 1
std/c/windows/com.d 0 0 1
std/c/osx/socket.d 0 0 1
std/compiler.d 0 0 1
std/c/linux/linuxextern.d 0 0 1
std/c/freebsd/socket.d 0 0 1
std/algorithm/internal.d 0 0 1