[HN Gopher] Are modules without imports "considered harmful"?
       ___________________________________________________________________
        
       Are modules without imports "considered harmful"?
        
       Author : lerno
       Score  : 49 points
       Date   : 2022-02-08 17:30 UTC (5 hours ago)
        
 (HTM) web link (c3.handmade.network)
 (TXT) w3m dump (c3.handmade.network)
        
       | barchar wrote:
       | reminds me of the venerable "makeheaders" tool, which works very
       | well, however the tooling issues are very real.
        
       | madsbuch wrote:
       | I got a bit confused here. It appears the author talks about
       | explicit imports vs. implicit imports.
       | 
       | Ofcause it would be alright to not import anything in a module
       | meaning that it would have no external dependencies.
        
       | svachalek wrote:
       | I think there are plenty of real-world cases that can be studied
       | here: Ruby comes to mind, and the early days of JavaScript. Even
       | though most of it isn't stateful, this has a lot of the same
       | issues as global variables. Personal answer here is a solid
       | "yes". I don't think any of the ideas in the article help much.
        
       | chenglou wrote:
       | I help maintain ReScript (https://rescript-lang.org) and we've
       | been rolling without an import statement for years now (basically
       | OCaml's module system). The default is to just write
       | `MyModule.doThis` at callsites. Sometime you do wildcard open
       | (`open MyModule`) within the right scope. Sometime you do it at
       | the top level for convenience (e.g. stdlib), but folks try to be
       | judicious. And yes, you can alias a module path, e.g. `module
       | Student = School.Class.Student`. Worth noting: the reason why
       | fully qualified `MyModule.doThis` can be the default, is that we
       | _usually_ dictate filenames to be unique within a project, so
       | there's no verbose path issue nor refactoring issues (and yes
       | this works fine... Facebook does it too).
       | 
       | Static analysis-wise (which is what most of the blog post's
       | about), things are basically the same. The tradeoffs are mostly
       | just in terms of readability. I used to be ambivalent about this
       | myself, but looking at the growing body of GitHub TypeScript code
       | with mountains of imports automated by VSCode, imo we've landed
       | on a decent enough design space.
        
         | mutatio wrote:
         | You can do this in Rust, the imports in Rust being synonymous
         | with your aliasing of a module path.
         | 
         | I guess it depends on the depth of your paths, but I'm unsure
         | what it solves, it just moves the verbosity to call sites.
        
       | The_rationalist wrote:
        
       | codeptualize wrote:
       | This sounds like the OCaml module system.
        
       | jraph wrote:
       | It seems to me one of the worst problems explicit imports help
       | solve is forward compatibility of code with respect to the
       | modules and the standard library. And this problem seems worth
       | solving.
       | 
       | If you require user code to explicitly import functions from
       | modules, you can safely add a function to a module or to the
       | standard library without risking to break someone's code.
       | 
       | Importing the module is not sufficient: really, the functions (or
       | any other imported objects) themselves need to be named. (or, the
       | function needs to be qualified with the name of the module;
       | actually, Python seems to do a reasonable job at this)
       | 
       | As a text editor user, it also helps me discover where such or
       | such function is defined. The alternatives are:
       | 
       | - using an IDE that will consume a lot of CPU cycles and memory
       | to resolve the functions
       | 
       | - grepping, with the risk of finding another function with the
       | same name so I would also need to check if the type signature
       | matches.
       | 
       | I'll gladly spend the time to write my import statement to solve
       | all these issues. It does not bother me. It's not where I spend
       | my time when I program.
       | 
       | It's okay if IDEs hide those import statements or produce them
       | automatically (and it does not mean it's the wrong approach). The
       | point is: code should not break (compilation or runtime) if a
       | module or the standard library evolves and add stuff that could
       | have otherwise clashed. The work being done automatically and the
       | result being hidden by default does not mean it can be done the
       | same way in the future and therefore should be avoided; that's
       | because the environment can change, so it has to be done at the
       | time the code is written, manually or automatically.
        
         | phoe-krk wrote:
         | Common Lisp hacker here, with a real-world case of this:
         | 
         |  _> It seems to me one of the worst problems explicit imports
         | help solve is forward compatibility of code with respect to the
         | modules and the standard library. And this problem seems worth
         | solving._
         | 
         | This is exactly the problem in a lot of legacy Common Lisp code
         | in which packages :USE other packages. :USE means that all
         | symbols from package A become accessible in package B.
         | 
         | It also means that a widely :USEd package cannot add a new
         | symbol without possibly breaking a lot of code that depends on
         | it. In the optimistic case, nothing will happen; in the
         | realistic case, you will get package conflicts if the symbol
         | that is newly exported from A is also exported by another
         | package C that is :USEd; in the worst case, the :USEing package
         | successfully grabs that symbol overwrites its meaning with
         | something else, which means that the functions or variables or
         | classes or whatever else named by that symbol suddenly become
         | something else and everyone else that :USEs package A suddenly
         | needs to deal with an overwritten definition, and chaos ensues.
         | 
         | One CL solution is to never :USE other people's packages and
         | explicitly import single symbols; another solution is to use
         | local nicknames which means that it's possible to "import
         | org.random.foo as f" and refer to symbols via F:BAR.
        
         | travisd wrote:
         | Interestingly, I suspect Facebook employs the largest auto-
         | loaded codebase in the world and I suspect this is made easier
         | by the fact that everything is controlled under one roof. There
         | is some namespacing (a la PHP namespaces) for standard library
         | stuff, but most naming just takes the form
         | <TeamName><NormalClassName> to avoid any conflicts.
        
       | mjw1007 wrote:
       | It's worth thinking about.
       | 
       | Nowadays many people are letting their IDE do 90% of import
       | statement maintenance, and then folding the lines out of the way
       | so they don't have to bother reading them.
       | 
       | That suggests to me that having explicit imports at the top of
       | every file isn't the best representation of the information they
       | contain.
       | 
       | As far as naming is concerned, maybe it would be better to share
       | a description of the project's naming rules in some larger scope,
       | and let individual files override that.
       | 
       | In languages where imports are doing work for dependency
       | resolution as well as naming, maybe it would be more readable to
       | have this information in one place than scattered around many
       | files.
        
         | 8note wrote:
         | Import problems tend to get caught at code review time
         | 
         | > are you sure you want to use
         | com.do.not.use.this.is.a.copy.collection ?
        
         | kerblang wrote:
         | The folding would be mostly unnecessary if we wildcarded (e.g.
         | `import foo.bar.*`) instead of explicitly spelling every single
         | thing out, but many dev teams forbid that... and then want the
         | IDE to compensate for it.
         | 
         | My point actually being that if folks won't accept wildcards to
         | begin with, I can't imagine importlessness gaining any
         | traction, although perhaps we'll bifurcate as happened with the
         | extremes of checked-exceptions-everywhere vs. never-exceptions.
        
         | fiddlerwoaroof wrote:
         | I find passing dependencies via constructor arguments make
         | codebases a lot easier to navigate: it encourages a pattern
         | where you have a single file that wires up the entire system
         | and you can look at that file to get a sense of which modules
         | are talking to which other modules.
         | 
         | Gilad Bracha had some interesting posts that talk about this
         | pattern: https://gbracha.blogspot.com/2009/06/ban-on-
         | imports.html?m=1
        
           | wonnage wrote:
           | It's super painful to figure out where this magically
           | injected dependency came from. Now you have to look at every
           | place that the object was constructed. And god forbid those
           | constructors also use injected dependencies.
           | 
           | I hate Guice.
        
             | fiddlerwoaroof wrote:
             | The solution here is not to use something like Guice and to
             | only call the constructor once.
        
           | EdwardDiego wrote:
           | Constructor args are the simplest form of dependency
           | injection :)
           | 
           | And when I'm using a DI framework, the only injection point
           | I'm happy with.
           | 
           | But, having worked on a large codebase that used constructors
           | with no DI framework, it makes adding a new dependency used
           | in multiple bottom nodes of the graph very painful.
        
           | colanderman wrote:
           | I find that pattern makes navigation harder, _particularly_
           | in untyped languages: because the particular implementation
           | is now no longer statically resolvable, now I have to walk up
           | a call tree to find which implementation (or even, which
           | type, in an untyped language) corresponds to each of those
           | constructor arguments, rather than just looking at the top of
           | the file.
        
             | fiddlerwoaroof wrote:
             | I think this indicates bad modularity in the codebase
             | and/or badly specified interface contracts. I've found that
             | this makes finding bugs easier because I can start in the
             | wiring file and then drill down to the only code that
             | handles stuff relevant to the bug: by giving one place that
             | lays out the high-level structure of a codebase, that place
             | becomes like the "table of contents" for the rest of the
             | code.
             | 
             | This particular benefit is almost completely destroyed by
             | DI frameworks that use annotations and other magic to
             | automatically wire together a codebase. So, I just don't
             | use those.
        
         | armchairhacker wrote:
         | I honestly think having the IDE resolve imports and then fold
         | them is a good solution.
         | 
         | The users don't need to know the specific imports 90% of the
         | time, but it really helps the compiler. So it's a sort of
         | metadata for the code, something we don't really have. Usually,
         | the same code is seen by both developer and compiler.
         | 
         | (It may also help incoming developers, but personally i think
         | it's negligible. Now they just have module+identifier instead
         | of identifier. The real help comes from "view symbol
         | documentation" "jump to source" which is there regardless).
        
       | tehjoker wrote:
       | This seems crazy to me that anyone would want to use wildcards in
       | anything but a one off script. The lack of discoverability and
       | potential for crazy-making clashes is omnipresent.
        
       | withinboredom wrote:
       | PHP via composer has autoloading that "just works." I've never
       | had any issues figuring out which thing is being used.
        
         | ehnto wrote:
         | Namespaces and PSR are the reason I suspect. For anyone not
         | aware, the PHP community has a set of standards that outline
         | some behaviours around modularity that aren't stricly language
         | structures, just best practices. The defactor package manager,
         | composer, also follows these standards, and life is sweet.
         | 
         | Namespaces is pretty much what this article ends up landing on
         | also, if I understand. There is no material difference between
         | `\App\MyClass::myFunc()` or putting at the top of your file,
         | `use App\MyClass` then latter, `myFunc()`
         | 
         | Autoloading: https://www.php-fig.org/psr/psr-4/
        
           | notpachet wrote:
           | Etsy has (or had when I was still working there) a cool
           | directory-namespace based custom autoloader for PHP (named,
           | not inexplicably, EtsyLoader). When the PHP runtime detected
           | that the script was trying to use an identifier not in scope,
           | the loader would try to find a matching file by splitting the
           | identifier on underscores into different folders, with the
           | last segment being the target file. So
           | Listings_Migration_Checker would get resolved to
           | listings/migration/checker.php.
           | 
           | Way more deterministic and predictable than Rails
           | autoloading, which always seems to devolve into a crazy mess.
           | And you knew exactly where a constant was located on disk
           | just by eyeballing the name.
        
             | metadat wrote:
             | This sounds really cool!
             | 
             | Unfortunate and a bit bummed out that nothing relevant
             | currently turns up in google:
             | 
             | https://www.google.com/search?q=EtsyLoader+php+github
        
       | fpoling wrote:
       | Just using modules names as prefixes everywhere instead of
       | importing individual symbols works when modules have short names.
       | Which in turn requires that the modules should be relatively fat
       | so relatively few of those should exist allowing for short and
       | descriptive names.
       | 
       | But in a language like JS where the module cannot span multiple
       | files one quickly runs out of good short names. Then importing
       | individual symbols or at least aliasing the module becomes a
       | necessity.
        
       | bradrn wrote:
       | I know of at least one language which actually uses this approach
       | in its module system; namely, Io [http://iolanguage.com/]. In Io,
       | if you refer to an undefined name, all Io files in the current
       | directory are searched for that name. The file containing that
       | name is then automatically imported. I've never used Io so I
       | don't know how it works out in practice, but I suspect that the
       | small community helps, in that backwards compatibility becomes
       | less important.
        
       ___________________________________________________________________
       (page generated 2022-02-08 23:01 UTC)