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