Jonathan M Davis
| 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
|