On autoloading
erock.io
erock.io
DRY is for logic, not linkage. Linkage is a necessary part of sharding your logic into intelligible and discreet modules, packages or classes. The linkages between those must be obvious for anyone who comes along and reads your code later.
In PHP, everything can be configured with composer, PHP's defacto package manager, including all your directories and their namespaces.
> I absolutely hate having to work around autoloaders in third party APIs.
If your "third party API" aren't using composer themselves, a defacto standard in PHP, then they are not professionally written.
PHP doesn't have modules or any form of modularity or packages, unlike Ruby. PHP only has direct file references from other files (include,require...) and SPL autoloading which is just a way to automate direct file reference (through namespaces or other techniques).
I'm not sure what PHP autoloading has to do with any of this article at first place.
> or a static in ../const/
> or a pseudo-global function in ../fwk/
For the love of God or Atheismo, stop making these kinds of ass backwards structures the rest of us have to deal with when a legacy project gets dropped on our desk..
There's an established pattern that works across all projects, psr-4, use it.
What I'm objecting to are "autoloaders" that allow "packages" to register in a way that makes it difficult to follow code. Things like Square's API which includes at least a dozen PHP files called things like "Autoloader.php", "autoload.php", "bootstrap.php", etc, each with their own unique twist on if (file_exists($file)) requre($file).
Autoloading sounds like a feature which reduces the friction while iterating on the program (when the mental model of the program is fresh), but adds a lot of friction if you need to maintain the program and are unfamiliar with the codebase.
It feels like the points cited in the article in favour of autoloading are "liberal" ("nicer DX to just have all the classes available everywhere"), and the counterpoints are all "conservative" ("code should be readable/maintainable").
If you can write highly maintainable code without paying the cost of slower speed, surely that's a winning strategy.
But I suspect an increase in maintainability means "more friction to change". (e.g. I'd expect adding documentation and unit tests is to take more time in the short term than just writing the code without documentation and unit tests).
Your program won't compile if you have unused imports, or even unused variables.
Which is low-friction for maintenance. You're not going to have to worry if deleting something is unsafe.
But this strong-arm implementation of a lint means that iterating on code meets high friction. It slows down developer iterations as you have to comment/uncomment in order to compile. -- "But if you use some tool it's not so bad" is advice I've been given, which strikes me as the same as "automatic loading isn't so bad if you're using an IDE".
My least favourite part - dicts of dicts of dicts parsed from JSON, so good luck knowing what conf["foo"]["bla"]["bob"] was without a lot of code reading, good luck figuring out what other properties conf["foo"]["bla"] had, and good luck figuring out if those properties always existed.
In the end I introduced Pydantic to give the JSON objects a known structure with validation, and that dramatically eased the maintenance friction in one foul swoop (and bringing in Pydantic exposed properties that everyone was setting religiously but never using) - instead of a dict of dicts, it was a now a dict of app name to BuildConfig.
Bringing in mypy and gradually introducing type annotations took more effort, but once it was done, the codebase was far easier to read and understand, large sections of dead code were easily identified, and runtime bugs became far less common.
In other words, I removed all the dynamic bits of Python.
That said, when I'm working with Python libs that do terrible things (like deciding if they're running in a multiprocessing context by checking for an ENV var when the module is imported(!)) I do appreciate how Python's magic lets me reach in and monkey patch terrible things away - although I feel a bit icky doing so.
And if a given library doesn't use type annotations, people will often create type stubs for it that mypy can consume.
https://github.com/python/typeshed/tree/master/stubs
TBH, Python generics are rather clunky, although I think they're probably still going to have cleaner type signatures than C++
It is irritating to work with a system that from time to time requires you many orders of magnitude more time to do a relatively simple task.
I prefer a little bit of upkeep (which I can usually amortize/automate) of a simpler system because it lets me be more predictable with my estimates. I can plan better because there is very little surprise lurking that could suddenly cost me an additional day of debugging.
And the cost of that upkeep might be very overestimated. For examples, I never felt that adding imports to a file is costing me anything. My IDE usually handles this for me. Or I might just copy something from somewhere else I had gotten something working in the past.
If you were to remove the autoloader and to explictly use `require` in your app, when you `require a` if `a` also `require b`, you implicitly get access to `b` at the same time.
So the problem isn't the autoloader, but Ruby's and PHP single global namespace.
If anything the autoloader makes it easier to locate the source, because it forces you to follow a strict naming convention. If you see `Foo::Bar`, you know it's defined in either `foo/bar.rb` or `foo.rb`, and it's easy to locate with your editor fuzzy finder.
Another proof is that Django doesn't have this problem even thought it has an autoloader (IIRC).
I'd be very much in favor of a Node or Python like import system for Ruby, but there's nothing wrong with the autoloader.
Interesting point and I agree! The thing I like about python and javascript's import system is you import the object that you end of using, creating a namespace as a variable, defined at the top of every source file. This lends itself to traceability which was my primary argument in the post.
I can see how I conflated the global namespace in ruby with automatically loading classes.
Cheers!
I'm not saying the OP is "wrong"—that is, they're more than welcome to use whatever language they like and write all the import statements they prefer. I'm just glad I can choose to do away with all those distractions.
Are those two different meanings for "autoloading"? Or do the two go together in Ruby? (As is probably obvious, I'm not a Ruby dev)
[0]: http://www.rubyinside.com/ruby-techniques-revealed-autoload-...
Meanwhile, in golang projects, within-package circular dependencies are happening blatantly everywhere, and somehow I don't see much complain about it.
And we can "import as" to avoid name conflict.
Using import together Mock library we can even swap out(Mock) particular imported item during unit test.
If one thing I would love to see is having that import system in Python ported to Ruby.
Granted that the way Ruby currently works also expose a few functionality for doing DSL easiser.
Previously I prefer import bla.* because it's shorter and neater, but as the code grows, more naming conflict and more difficult to find where's where, I prefer explicit import. I can configure my IDE to collapse the import lines anyway.
In source-based languages, if a library adds some class, you'll get into some trouble. In Java, this will also happen at source level, but results in not being able to compile.
PHP autoloading (with composer, the de facto standard) is basically just a one to one mapping of namespaces to file names, which have matching class names.
Now there can be more magical autoloading, if it is manually defined, but that's not very common compared to directly using `include` or `require`.
This one line kinda says it all.
If you reach the point where you grep your whole project for SomeClass and nothing comes up, everything else is just going to be a bandaid.