|
I programmed in Perl (5) in a large enterprise environment for five years. On a huge, monolithic codebase, with people who were innovating with the language in interesting ways. The business environment was restrictive (healthcare), but the amount of people leveraging Perl's flexibility to build powerful, flexible tools to enable faster development on the codebase at scale was startlingly high. A large proportion of developers engaged at every level, from XS to MOP (we used a modified version/fork of Moose) to distributed computing (we used a home-rolled queue worker solution, think Celery for Perl, but without the AnyEvent cancer), and novel web frameworks on the frontend, some of which were put back out on CPAN. Basically anyone who wanted to be productive there demonstrated an impressively innovative, cross-cutting skillset that combined deep knowledge of UNIX technologies, the particulars of the application, and an incredibly expert language of the particulars of the language itself. And honestly? It sucked. A lot. I won't say this about any other platform--not modern security-vuln-in-the-package-manager-every-two-weeks JS, not "do you mean actually cross-platform or relying-on-compiler-UB cross-platform?" C, not the worst old-layered-on-new-layered-on-old-again PHP, but: Perl as a platform on which to build something non-tiny, or something that requires more than a small handful of developers, is unutterably awful. And I say that as someone that got pretty good at it, I think. At the micro level, TIMTOWDI confuses newcomers, makes code review inconsistent, and means that as soon as someone feels fluent and productive on the codebase, then they have to engage with someone else's code, and they get stuck all over again. This means that mentorship is a complete bastard, and developer progression is incredibly hard to gauge, teams form fiefdoms (even in the face of huge linting tools; things that make an ultra locked-down JS/TS project look paltry by comparison--even for off-to-the-side greenfield projects) and can't transfer developers, you name it. Perl at any sort of scale is worse than the quoted "write-only" slogan: it's "write-once, re-learn from scratch on read". For a junior dev, rewriting would be a blessing. At the medium (between micro and macro) level, the metaprogramming abilities of Perl just . . . fuck everyone up, no matter what they want to do. Want to get your work done in as straightforward and repetitive a style as possible? Welp, no matter how simple the task, and how straightforward-looking the utilities for it might be (on CPAN or in house), they won't interoperate for shit. Want to reduce boilerplate and ease the pain of common tasks by encapsulating (or, god forbid, applying metaprogramming) to speed up some process? No problem, first-class laws-of-the-universe-altering facilities are available to everyone--to get your change functional, you'll just have to interoperate with . . . well, everyone (some of whom wrote third party modules, and aren't people you can ask nicely for help). Object systems will fight with message queue clients for control over how calling nonexistent methods on arbitrary objects that neither one created should work (you thought Ruby's method_missing was a foot gun? Ha!). A tool for printing console logs will override the alarm(2) hooks used by your main HTTP client, meaning that if someone leaves a debug print in the wrong place, HTTP connections to a down endpoint will start blocking forever and kill you. Can this happen in other languages? Sure! Python, Ruby, and PHP (to some extent) all allow the same flexibility and low-level access. But only Perl makes this the default convention* to follow. I've heard people say "Perl programmers are just C programmers who couldn't hack it, but still want to write C". There's a grain of truth to that. Problem is, those aren't the kind of people I want to share a codebase with. At the large (5MMSLOC+ codebase) level, Perl's an operational nightmare. Thanks to all the ways that libraries can customize the language, it has the metastasized version of most other interpreted scripting languages' problems when it comes to compilation phase and memory, namely: "what happens when I have to compile a huge dependency graph on startup? Can I cache those things in some sort of intermediately usable format or do I have to wait many minutes to start a test script? Can I fork? When I fork, what gets shared? Just filehandles opened by the application? Or random shit inside libraries too (and are compiled dependencies encouraged to make their IO resources' lifecycles manageable by the outer runtime)? Will my box crash due to GC-caused refcount/allocation cycles if all my forks exit at once?" These aren't unique to Perl, but I think they're worse in it than any other language. Oh, and that's without getting into the insane degree of mutability Perl permits. It's the freedom of C without the discipline ("set environment vars any time you want! Hell, they're a first class language data structure! Oh, and change what the STD* streams point to, that's fine to. And dynamic scoping and Scope::Upper means you can't tell when something will change because some code totally unrelated to yours decided it should!"). When trying to handle requests or do anything "nested" (terminate SSL, alter things that would go on e.g. "Context" in now-unpopular Go idiom), Perl's conventional answer is just "dynamically overwrite globals!" In general, this means that it doesn't matter in what context you tested your code in, it'll do something different at runtime because a) if it does anything interesting it depends on global-ish state, and b) other random code can rewrite SIBLING global state whenever it wants (think Python, but if the convention were for any coder that got stumped about how to pass data around to just modify globals/locals/vars willy-nilly). Again, a risk in most environments, only actively encouraged in Perl. I promise that wall of text isn't just specific-employer PTSD. I've been through CPAN code, negotiated with package maintainers, gone to conferences, tried to get a sense of how people are contextualizing these problems. And the impression I came away with is that the vast majority of the entire Perl ecosystem--from the practices understood to be desirable by programmers to the behavior of existing/hardened/public code--is overwhelmingly harmfully inaccurate, poorly-thought-through, and defended by the worst strain of cleverness-above-practicality (or "don't touch it, it works when you hold it just right and don't breathe" for incredibly simple requests) when challenged. Perhaps at some point in the past this ecosystem reflected the cutting edge, but I think that time is long past. If you're making a small commandline utility or personal one-off in Perl, go nuts. I don't think you're a bad person. Just make sure it doesn't get any bigger than one developer's worth of code. EDIT (probably the first of many, because essay): typos and grammatical fixes. Promise I won't change the substance. * Why the hell are either of those libraries calling alarm(2)? Answer: the logger didn't realize that write(2) wasn't interruptible, and the HTTP lib author didn't realize that connect(2) took a timeout. By the way, both were in incredibly popular modules on CPAN. |
I once wrote a sub at this gig that returned a list (because hashes are lists) containing a string built by sprintf, which contained a sub dereference wrapping a sub that returned a string built by sprintf. Though it was necessary at the time I'm still just really, genuinely sorry about that. I guess the takeaway is that perl reverts even the most civilized devs to utter savagery.
>> Perl at any sort of scale is worse than the quoted "write-only" slogan: it's "write-once, re-learn from scratch on read". For a junior dev, rewriting would be a blessing.
Rewriting is really risky too; global state is problematic in any language that offers it but the almost complete lack of guarantees provided by the language is exhausting and makes it difficult to reason about even the most trivial change. Imagine being dropped into a 5k line function that hasn't been touched in 10 years. There are no tests, few comments, and the author quit 6 years ago. What types can this function return? Is it always called with all of its expected arguments? Where is it called from? None of these questions can be answered trivially. You'd think grep would handle the last, but you'd be wrong because people can and do build identifiers piecemeal as strings and eval.
The casual use of evals and symbol table manipulation in probably any large perl codebase only became more terrifying as I became more comfortable with the language. I cried a little bit when I figured out how the import system is cobbled together, and not exactly for its elegance or simplicity.
On the bright side, building healthcare systems with perl made me a very disciplined and defensive coder. It also got me used to saying things about my code like "reasonably confident" instead of "it works". Software engineering is so much more exciting with assumptions and guesses, who doesn't like to roll the dice every now and then? Now if I could just remember what all the runtime flags do...