Inline imports redivivus

Zach Tollen zach at mystic.yeah
Wed Mar 16 05:31:45 UTC 2022


On Sunday, 13 March 2022 at 08:23:52 UTC, Dom DiSc wrote:
> Locality can be overdone. I think this would suffice:
>
> ```d
> if (Values.length != 0)
> {
>     import std.functional : binaryFun;
>     foreach (uint i, ref v; values)
>         if (binaryFun!pred(value, v)) return i + 1;
>     return 0;
> }
> ```
>
> So any new feature is here totally superfluous IMHO.

There are three considerations here:

- Functionality - What you can do
- Performance - How fast you can do it
- Readability - How easy it is to see what you're doing

Your argument about it being superfluous addresses Functionality 
and Performance, but not Readability. And the feature is not 
entirely superfluous with regards to Functionality and 
Performance either. But I'll address those later.

Regarding performance in this particular case, I agree with you. 
When you import a symbol as locally as is done in the example 
above, the compiler find its definition quickly enough. The only 
slow contender in this case is `imported` from object.d:
```d
    if (imported!"std.functional".binaryFun!pred(value, v)) return 
i + 1;
```
Regarding functionality, the functionality between having a 
separate import statement and the inline one is the similar in 
ordinary code. Both the statement and the inline bring in the 
symbol and then use it once.

It should be pointed out that if you wanted to use the symbol 
again in the same scope, the separate import statement is 
superior. This is a deficiency of the proposed single-use imports 
that I will address below.

But we have to consider readability.

Ideally, code is written in a way which allows a programmer to 
see clearly what is important, without having to be distracted by 
what is not important. Unimportant code which takes too prominent 
a place constitutes an undesirable distraction — in the recent 
thread about Exceptions vs. other Error Handling, the chief fear 
with Error Codes is that they make unimportant code too prominent.

Because symbols must be imported to be used, programmers have 
gotten used to seeing what is imported blasted like a trumpet at 
the top of their scope or module.
```d
{
    // Look at me! I must be important!
    import std.functional : binaryFun;

    ...
}
```
But the actual importance of any given imported symbol depends 
greatly on the specific symbol and the context. Certainly, the 
readability of some code is not diminished by having notable 
symbols be prominently announced before they are used. But just 
as often, the imported functionality is so ordinary that it is 
nothing but a distraction having to see it promoted so blatantly.

Therefore, being able to inline imports, and make them as 
innocuous as possible, will enable the programmer to indicate how 
significant each symbol actually is, instead of forcing them all 
into the limelight.

Local imports, even though they can force unimportant symbols 
into the limelight, have been widely adopted in D for perhaps two 
reasons — performance and readability. Performance, because of 
shortened lookup times. And readability, because you can more 
easily find the source of a specified symbol the closer its 
definition is to its usage.

But local imports have also come at a cost to readability, in the 
sense that they are a visual distraction. We can say, at least in 
the case of module level imports, that you can just use their 
symbols without much 
[fanfare](https://en.wikipedia.org/wiki/Fanfare).

------------

We have to address functionality. It was argued above that inline 
imports as a feature are "totally superfluous." Even apart from 
readability, that is not true. Particularly, with regards to 
template constraints, they are necessary in order to be able to 
use local imports at all.

This was written about with a level of rigor suited to a serious 
academic paper in Andrei Alexandrescu's 
[DIP1005](https://github.com/dlang/DIPs/blob/master/DIPs/other/DIP1005.md). That proposal suggests the following syntax to allow inline imports at the template level:
```d
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))
{
     ...
}
```
Now let's see how it would look with my proposed long-form 
syntax. Since the improved shorter form of the syntax 
(`:modname:symbol`) now uses two colons, it makes sense to 
retroactively make the original longer form reflect that. Let it 
be an example of the [Mandela 
Effect](https://www.entitymag.com/mandela-effect-examples/). Let 
no one remember that I ever suggested doing anything else. :-)
```d
auto uninitializedArray(T, I...)(I sizes) nothrow @system
if (import:std.traits:isDynamicArray!T &&
     import:std.meta:allSatisfy!(import:std.traits:isIntegral, I) 
&&
     
import:std.traits:hasIndirections!(import:std.range:ElementEncodingType!T))
{
     ...
}
```
This form is just as good if not better than `with(import x.y : 
z)`, and it can be used anywhere.

--

An additional point of functionality regarding inline imports is 
that you can import a symbol in a [function 
contract](https://dlang.org/spec/function.html#contracts) without 
having to use the verbose form. So the clunky:
```d
int func(A a, B b)
in
{
     import std.algorithm.comparison : cmp;
     assert(cmp(a,b) < 0);
}
do
{
     // function body
}
```
...becomes:
```d
int func(A a, B b)
in (:std.algorithm.comparison: cmp(a, b) < 0)
{
     // function body
}
```

------------------

The feature looks pretty good. The syntax looks good. But the one 
remaining concern is reusability. If inline imports are designed 
to be used exactly once without any further effect, many common 
use cases will find them too burdensome.
```d
unittest {
     assert(:std.traits: sometest(myFunc, [1,2,3]));

     // you mean I have to import it every single time?
     assert(sometest(myFunc, [4,5,6])); // error: undefined symbol 
`sometest`
     assert(sometest(myFunc, [7,8,9])); // error: undefined symbol 
`sometest`
     // etc.
}
```
What we really want is to be able to import the symbol inline, 
and then have it available as a regular symbol for the rest of 
the scope. The implementation of this feature could be as simple 
as a statement-level syntactic rewrite. So the above would be 
rewritten to:
```d
unittest {
     import std.traits : sometest; // <-- inline import rewritten 
to
     assert(/* :std.traits: */ sometest(myFunc, [1,2,3]));

     // now we don't have to keep importing it
     assert(sometest(myFunc, [4,5,6]));
     assert(sometest(myFunc, [7,8,9]));
     // etc.
}
```
What are the caveats of such a rewrite? Well, we would have to 
deal with the extremely rare case of a valid symbol changing its 
meaning due to a subsequent import in the same statement. Since 
defining the same variable twice within a function rightfully 
produces an error, this would only happen with symbols defined 
outside the function:
```d
// a.d
module a;
int x = 4;
----------
// main.d
int x = 3;

void main() {
    {
       assert(x == 3);
       // import a : x; // <-- this is where the rewrite occurs
       assert(x + :a: x + x == 4 + 4 + 4);
       assert(x == 4);
    }
    assert(x == 3);
}
```
The above code already compiles and runs as long as we rewrite 
the inline import to included the commented out line. Such a 
simple rewrite may be so easy to implement as a feature — and the 
alternative, more complex interpretation (`x + :a: x + x == 3 + 4 
+ 4`) so ill-conceived with regard to any coding style that could 
possibly be considered allowable — that the language can simply 
be specified to say that the above, statement-level syntactic 
rewrite is how inline imports work.

--

Finally, we have to consider the "scope" of symbols imported by 
template constraints and other parts of the function signature. 
Symbols which are imported in the constraints, contracts, or just 
the regular signature (assuming you can import basic types there) 
should be made available for all the other parts of the function 
to access. Let's give a name to this greater space into which all 
these symbols are imported. Let's call it the "metascope" of the 
function.

This simply means that inline imports are designed so that you 
don't have to import any symbol twice for the same function. So 
for example with this signature from phobos, the first import of 
`isInputRange` should put it into the function's metascope so 
there's no need to import it again:
```d
auto cmp(R1, R2)(R1 r1, R2 r2)
if (:std.range: isInputRange!R1 && /* :std.range:(<-- no need) */ 
isInputRange!R2)
{ ... }
```


More information about the Digitalmars-d mailing list