Jump to page: 1 2
Thread overview
Understanding the Use of Nested Import and Selective Import in D
Jan 16
Orfeo
Jan 16
user1234
Jan 16
user1234
Jan 16
Orfeo
Jan 16
bomat
Jan 17
Orfeo
Jan 17
Renato
January 16

I found myself a bit perplexed when it comes to the usage of "nested imports" and selective imports. It seems that prominent D programmers have varied opinions on the matter. I would love to hear your insights and experiences on this topic.

Here's a quick summary of what I've come across from three influential D programmers:

  • Adam Ruppe: In his blog post titled D's selective imports have effects you may not want have effects you may not want, Adam advises against the use of selective imports. He highlights potential unwanted side effects and suggests caution when employing them.

  • Atila Neves: At DConf 2023, Atila Neves recommended the use of nested imports. He argues that nested imports can make refactoring easier and help in assessing the dependencies a function has.

  • Rober Schadek: Also at DConf 2023, Rober Schadek discouraged the use of nested imports, taking a stance different from Atila Neves.

Now, the big question is: What's your preferred approach?

January 17
Yes, I try to place my imports at the most general location that makes sense.

Sometimes that is at the module level, other times its in a single function.

Or anywhere in between (such as a struct).
January 16

On Tuesday, 16 January 2024 at 13:19:59 UTC, Orfeo wrote:

>

I found myself a bit perplexed when it comes to the usage of "nested imports" and selective imports. It seems that prominent D programmers have varied opinions on the matter. I would love to hear your insights and experiences on this topic.

Here's a quick summary of what I've come across from three influential D programmers:

  • Adam Ruppe: In his blog post titled D's selective imports have effects you may not want have effects you may not want, Adam advises against the use of selective imports. He highlights potential unwanted side effects and suggests caution when employing them.

  • Atila Neves: At DConf 2023, Atila Neves recommended the use of nested imports. He argues that nested imports can make refactoring easier and help in assessing the dependencies a function has.

  • Rober Schadek: Also at DConf 2023, Rober Schadek discouraged the use of nested imports, taking a stance different from Atila Neves.

Now, the big question is: What's your preferred approach?

Another point is that the use of selective imports tends to be "a refactoring". You start with a global or local import. Once everything is fine you'll think that's it's a good idea to make the import selective, i.e "because I only use that in there".

Problem is, if someone else at some point work on your code:

  1. he needs to add more to the selection
  2. completion may not work anymore (will only show what's selected)

So it's a bit a thing of expert.

Given these arguments I think that global imports should not be selective, only local ones should.

Implementation detail. D frontend resolves identifiers using associative arrays (that's called symtabs in the compiler IIRC), hence the only complexity is the scope (plus the import decls found while going back to the module scope).

January 16

On Tuesday, 16 January 2024 at 13:37:59 UTC, user1234 wrote:

>

Implementation detail. D frontend resolves identifiers using associative arrays (that's called symtabs in the compiler IIRC), hence the only complexity is the scope (plus the import decls found while going back to the module scope).

oops forgot to say: so it's fast.

January 16

On Tuesday, 16 January 2024 at 13:19:59 UTC, Orfeo wrote:

>

I found myself a bit perplexed when it comes to the usage of "nested imports" and selective imports. It seems that prominent D programmers have varied opinions on the matter. I would love to hear your insights and experiences on this topic.

[...]

see also Workaround for DIP 1005

January 16
On Tuesday, January 16, 2024 6:19:59 AM MST Orfeo via Digitalmars-d-learn wrote:
> I found myself a bit perplexed when it comes to the usage of "nested imports" and selective imports. It seems that prominent D programmers have varied opinions on the matter. I would love to hear your insights and experiences on this topic.
>
> Here's a quick summary of what I've come across from three influential D programmers:
>
> - Adam Ruppe: In his blog post titled [D's selective imports have
> effects you may not
> want](http://dpldocs.info/this-week-in-arsd/Blog.Posted_2023_11_06.html)
> have effects you may not want, Adam advises against the use of selective
> imports. He highlights potential unwanted side effects and suggests caution
> when employing them.
>
> - Atila Neves: At DConf 2023, Atila Neves recommended the use of nested imports. He argues that nested imports can make refactoring easier and help in assessing the dependencies a function has.
>
> - Rober Schadek: Also at DConf 2023, Rober Schadek discouraged the use of nested imports, taking a stance different from Atila Neves.
>
> Now, the big question is: What's your preferred approach?

When local imports were introduced, they were pushed as best practice (in part, because Andrei is a big fan of them), and I think that for the most part, they still are, but there's definitely going to be some disagreement on it.

The benefits of local imports have to do with encapsulation. When you localize an import as much as possible, you make it clear which parts of the code are using that import. That makes it easier to see which imports are used by a section of code and where symbols are coming from. It also makes it much easier to refactor imports, because you can see what's using the import and see whether it's still needed, whereas if an import is put at the top of a module, it'll probably sit there forever. Tools for removing unused imports would help with that problem, but having the imports be local still helps you reason about the imports for a section of code (and to an extent removes the need for such a tool). And of course, if all of the imports that a function uses are within its body, then removing the function removes all of those imports without you having to even spend the time to figure out what they were.

Arguably an even bigger benefit of local imports comes from conditional compilation. When you have a template, static if block, or version statement in your code, the code inside of it may or may not be compiled into your program (depending on what your code is doing). So, by putting the imports used within that code inside of that code, you can avoid having them be imported at all if that code isn't actually compiled in (which is particularly critical in the case of version statements that could use platform-specific modules but helps with avoiding unnecessary compilation in general).

The downside of course is that you then have import statements throughout your code, and they're often going to be duplicated throughout a module (or even within a function if you localize them far enough), because separate parts of the code then need their own local imports. So, some folks think that it's just simpler to throw the imports at the top of the module (and particularly for small programs, it probably is).

Another issue is that local imports don't really work for stuff in function signatures. For member functions, you can localize imports to the class or struct, but for module-level imports, they have to go at the module level, so they're not local. There was talk of adding a language feature to fix that, but it never happened. However, we did at some point get a template to do it for you. object.d contains imported, which allows you to do stuff like

auto foo(imported!"std.datetime".SysTime st) {...}

I'm not sure how much it's really used though. I suspect that it's ugly enough that it's not used much, and I don't know if it's even very well known at this point (personally, I keep forgetting that it was added). However, it does have the benefit of making it so that removing the parameter or return type using that template would remove the import, just like removing a function removes any of its local imports in the process. So, it's arguably a good idea, but personally, I find it ugly enough that I don't use it.

Another thing to keep in mind (though it also affects module-level imports) is that public, package, and private affect imports - even when used with : or with {} - so if you use an access modifier in a way that affects an import (e.g. if you put public: at the top of your class and then have imports right under it), you can accidentally end up with imports that aren't private. If you're in the habit of just slapping all of your imports at the top of the module, then this is unlikely to be an issue, but if you put them throughout the module (and not just within functions), then it could bite you. So, you do need to be careful about where you put imports that aren't at the top of a module and which aren't within a function.

As for selective imports, their utility is more debatable. As Adam pointed out in his blog post, there are subtleties that can cause confusion with them, but for better or worse, they are usually touted as best practice. The issues that Adam described mostly have to do with selective imports of functions (though they can sometimes cause problems with other types of symbols), and you don't usually need to have selective imports of functions outside of other functions. If you're going to go to the trouble of using selective imports, then you're almost certainly also using local imports, and if you're calling a function, it's almost always inside of another function (though occasionally, you might do something like initialize an enum with a function call outside of a function), so when you selectively import a function, it's almost always with a local import, and that generally isn't a problem in my experience.

The big benefit from selective imports is that you can see where symbols come from, whereas if you just have naked imports, you have to be familiar with those modules to know what comes from where (which you often are, but it can definitely help when reading unfamiliar code). So, they can definitely help with understanding code - but on the other hand, you do then have to change them pretty much every time that you change which symbols you're using, which is a maintenance cost and can get pretty annoying (the same happens to an extent with local imports, but since you often use several symbols from a module, you don't have to change local imports as often as you have to change selective imports). So, the cost-benefit analysis for selective imports is far less clear than it is with local imports, and I think that you're much more likely to see disagreement over whether they should be used. Often with the code that I've worked on, selective imports get used, but they then get dropped if you end up with more than a handful of symbols from that module are being used. I'm also much less likely to use selective imports outside of functions.

IIRC, at one point, it was thought by some that selective imports would allow the compiler to reduce compilation times (by not actually compiling the symbols that aren't used), but with how D's compilation model works, selective imports don't work that way, and I don't think that they actually can work that way (particularly when compiling modules individually is a thing).

Personally, I would say that you probably should be using local imports as much as is reasonable, though where that line is obviously depends on you and your pain threshhold for code duplication. Initially, I was resistant to using them, but ultimately, I've found them to be beneficial enough that I use them quite heavily now (though I haven't gone so far as to use the imported template). I would also suggest that you use selective imports as much as reasonable, since I've found that they make reasoning about the code easier, because they make it clear where the symbols are coming from. You do obviously need to be aware that you could run into problems with them depending on how you use them, but I don't even recall the last time that I had a problem due to a selective import. Regardless, obviously, you are going to have to decide what works for you, and that could change over time. Both features can definitely help with code encapsulation and with understanding where the symbols being used are actually coming from, but the cost-benefit analysis is likely to differ based on how you think and function.

- Jonathan M Davis



January 16

Wow, that was... exhaustive. Thanks for that. :)
One more question that I have, though...

On Tuesday, 16 January 2024 at 19:05:43 UTC, Jonathan M Davis wrote:

>

The downside of course is that you then have import statements throughout your code, and they're often going to be duplicated throughout a module (or even within a function if you localize them far enough), because separate parts of the code then need their own local imports.

Apart from the aesthetic "clutter" of duplicate imports, will they also put additional strain on the compiler and/or affect the resulting binary? I mean, will the imports actually be compiled in several times?

January 16
On Tuesday, January 16, 2024 1:42:04 PM MST bomat via Digitalmars-d-learn wrote:
> Wow, that was... exhaustive. Thanks for that. :)
> One more question that I have, though...
>
> On Tuesday, 16 January 2024 at 19:05:43 UTC, Jonathan M Davis
>
> wrote:
> > The downside of course is that you then have import statements throughout your code, and they're often going to be duplicated throughout a module (or even within a function if you localize them far enough), because separate parts of the code then need their own local imports.
>
> Apart from the aesthetic "clutter" of duplicate imports, will they also put additional strain on the compiler and/or affect the resulting binary? I mean, will the imports actually be compiled in several times?

Imports are never compiled in. Importing a D module is nothing like #including a C/C++ header file. It does not result in any code being inserted into the current module, and if it results in anything being added to the binary, it's because a template from that module was instantiated with a new set of arguments, resulting in a new template instantiation that has to end up in the binary.

An import statement tells the compiler to allow the current code to use the symbols from the imported module. That requires compiling the imported module sufficiently for the compiler to then let those symbols be correctly used within the module that's doing the importing, but importing a module doesn't make it so that the compiler fully compiles the imported module. To do that, the module has to also be passed to the compiler to be compiled (be it as part of the same compilation process or compiled separately to be linked in later).

And once a module has been imported, the compiler has already built the symbol table for that module, so importing the module additional times during the same round of compilation will not result in it being processed again. The import statement will need to be parsed, which isn't free, but it's so cheap in comparison to everything else that you'd likely have a very hard time detecting the cost even in a very large codebase with a lot of local import statements. And if anything, using local imports could reduce compilation times, because if an import is local to a template, and your code doesn't end up instantiating that template (or doesn't compile in a particular branch of a static if or version block), then the compiler doesn't need to do anything with that import.

- Jonathan M Davis



January 17

On Tuesday, 16 January 2024 at 19:05:43 UTC, Jonathan M Davis wrote:

>

When local imports were introduced, they were pushed as best practice (in part, because Andrei is a big fan of them), and I think that for the most part, they still are, but there's definitely going to be some disagreement on it.

[...]

thanks a bunch for your in-depth analysis.
Your insights have been helpful in clarifying my understanding of how to approach import in my code.

January 17

On Wednesday, 17 January 2024 at 13:38:10 UTC, Orfeo wrote:

>

On Tuesday, 16 January 2024 at 19:05:43 UTC, Jonathan M Davis wrote:

>

When local imports were introduced, they were pushed as best practice (in part, because Andrei is a big fan of them), and I think that for the most part, they still are, but there's definitely going to be some disagreement on it.

[...]

thanks a bunch for your in-depth analysis.
Your insights have been helpful in clarifying my understanding of how to approach import in my code.

I've come to prefer function/struct-level imports where possible... with the exception being those imports which get used by a lot of functions within the same module... because they just become tiringly repetitive, there's a point where it simply becomes cleaner to have a single import at the top of the file instead of the same imports again and again within function scope (though there's never going to be a "threshold" everyone agrees on, I think everyone has one).

My main reasoning is that D tools, surprisingly, cannot do "Optimize Imports" (turns out that with all the metaprogramming going on , it's impossible to tell for sure which imports are being used - or so I read in another thread about this topic)... so you're likely to get lots of unused imports over time as you change your code, which I just find distatestful - and it become harder and harder to keep the imports "correct". Keeping imports more local makes that a little bit easier to avoid, besides the other reasons like making it easier to refactor.

Regarding selective imports: those are helpful to people reading your code, because they may not be as familiar with all the names exported by the modules you're using. The argument against that from the "D selective imports have effects you may not want" seem highly unconvincing to me because it's basically talking about ugly edge cases related to how UFCS works which maybe should even be fixed and not exist at all?! Avoiding selective imports because of that is the proverbial throwing the baby out with the bath water.

« First   ‹ Prev
1 2