www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Understanding the Use of Nested Import and Selective Import in D

reply Orfeo <dlang orfeo.fastmail.com> writes:
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?
Jan 16
next sibling parent "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
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).
Jan 16
prev sibling next sibling parent reply user1234 <user1234 12.de> writes:
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](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?
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).
Jan 16
parent user1234 <user1234 12.de> writes:
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.
Jan 16
prev sibling next sibling parent Orfeo <dlang orfeo.fastmail.com> writes:
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](http://forum.dlang.org/post/tzqzmqhankrkbrfsrmbo forum.dlang.org)
Jan 16
prev sibling parent reply Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
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
Jan 16
next sibling parent reply bomat <Tempest_spam gmx.de> writes:
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?
Jan 16
parent Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
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
Jan 16
prev sibling parent reply Orfeo <dlang orfeo.fastmail.com> writes:
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.
Jan 17
parent reply Renato <renato athaydes.com> writes:
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.
Jan 17
parent Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Wednesday, January 17, 2024 7:03:18 AM MST Renato via Digitalmars-d-learn 
wrote:
 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)...
A tool could do it assuming that it's opinionated enough about local imports, and you're willing to let code break if imports that arguably should be local aren't. Otherwise, yeah, you can't really do it in the general case because of conditional compilation. Specifically, the problem is that it's possible to have an import statement which is always compiled into the code but where the symbols from it are only used some of the time. So, for the tool to know which imports are used or not, it would have to somehow be able to hit every conditional branch (from version statements, static if statements, templates, etc.), which isn't really something that can be done. For instance, if you have version statements in your code (e.g. Posix vs Windows), you can have symbols which are used only within a portion of the versioned blocks, but the import is at the top-level and always compiled in. So, if those version statements aren't being compiled in when the tool is run, then it would conclude that the import was unnecessary and remove it, which would then break the code when it's compiled in a situation where those version statements do get compiled in. And of course, the situation is further complicated by the fact that the module being imported could have a different set of symbols depending on conditional compilation. As such, the tool can't really determine for sure that an import isn't used. It _might_ be able to detect whether any code branches depend on conditional compilation and remove unused imports if there aren't any, but with how often conditional compilation is used in typical D code, that's not necessarily very useful, and if you have a situation where one module provides the symbols on one platform, but another module provides them on another platform (which could definitely happen with OS stuff), and both modules are being imported, then even if the module you're checking for unused imports doesn't have conditional compilation, you could still end up removing imports that you shouldn't have due to conditional compilation in the modules being imported. That being said, if the tool is opinionated about local imports, it could be done. Specifically, what it could do is take the stance that all imports that are used in conditionally compiled code must be local (and thus only imported when that code is compiled in), in which case, if it finds an import which isn't actually used, then it can just remove it. That would then break any code that hadn't used local imports to import symbols that were only used within conditionally compiled code, so whether it would really be a desirable tool to have in general would be debatable, but taking that approach should make it possible to have such a tool. Another approach would be to have a linting tool which warned you about possibly unused imports but didn't actually remove any. Since it would be less automatic, it would be more annoying to use, but it would avoid removing imports that were actually used in conditionally compiled code, and if you wanted to get rid of the warning you could make those imports local. Still, you can't really remove all unused imports, because the ones that are conditionally compiled in couldn't be examined properly unless that code was being compiled in, which would potentially require running the tool on a variety of platforms and with a variety of conditions that you wouldn't always run into. So, as is often the case, D's metaprogramming capabiltiies complicate the situation considerably with regards to tooling. - Jonathan M Davis
Jan 17