Perl and Me, Part 2: The Power of OOP
This is part 2 of an ongoing series where I explore my relationship with Perl. You may wish to begin at the beginning.
This week continues the topic of why a former C++ programmer might retain some nostalgia after moving to Perl: objects.
Last week I talked about my discovery of object-oriented programming and how it led me to become a C++ programmer. As I described, it really was an epiphany: we would say back then that I suffered a paradigm shift, although that’s become somewhat cliché these days.1 From this you might gather that I’m an object zealot, but I’m not.
Here’s my thinking on OOP.
Throughout your life as a programmer, you’re going to run into many classes of problems. Some of those problems will need to be solved using objects, and if you try to solve them procedurally, you’re going to create a huge mess. Some of those problems will need to be solved procedurally, and if you try to solve them using objects, you’re going to create just as huge a mess. But life is not black and white,2 and, while it might be convenient to divide the universe of something-or-others (in this case, programming problems) into two, neatly opposed camps, the world doesn’t work that way. So you’re also going to run into many, many classes of problems which can be solved perfectly well using either technique. In fact, I would contend that the vast majority of programming problems fall into the middle of this spectrum.
That’s my theory, anyway, which is borne out by my personal experience. It’s a bit simplistic, yes—it ignores other paradigms such as functional programming or event-driven programming—but it’s workable for certain discussions, such as this one.
Because what’s interesting to me is, what do we do in the middle? That is, if you have a procedural problem, you obviously should write a procedural solution. Of course, there are people who want to use objects for everything: CPAN has many modules where you must constantly instantiate objects merely to call a single method on it and never use it again.3 But, in general, most people will use a procedural solution for a procedural problem. Likewise, most people will use objects when objects are required. Sure, there are still some hold-outs who feel that objects are “overrated” or “needlessly complex” (I worked with one such person not that long ago), but, for the most part, people will use objects when they need objects.
But what about when neither approach is superior? which, as I contend, is most of the time? What do we do then? Should we just flip a coin?
I say, “no.” Because the assertion that either approach will work perfectly well is in fact a deceptive one. It’s incomplete. The proper statement should be that either approach will work perfectly well ... right now. Because, you see, code ages.
Joel Spolsky, of course, famously contested this assertion. Of course, like everything Spolsky writes, that article was both brilliant and moronic.4 Sure, he’s right that code doesn’t decay, or rust, or rot, while sitting there on its hard drive. But code most certainly does age, and I’ll bet that every single one of you reading this knows it. Unlike Spolsky, I’m not going to tell you that you’re wrong and you’ve been too stupid to realize it all these years. I’m going to tell you that you’re right, and you probably were just never able to articulate exactly why.
Code “ages” because technology advances. Because the codebase that it’s sitting in grows and changes around it. Because the hardware it’s sitting on gets newer and smarter, and the older code doesn’t speak its language any more. Because there are just plain better ways to do things these days, and the older code can’t keep up. Is it true that sometimes programmers just want to rewrite everything because it’s easier than trying to understand what all that messy old code is doing in there? Sure. But it’s also true that the messy old code is harder to work on, less efficient, and more likely to need maintenance than new code, and that is costing your business time, and therefore money. Is it true that sometimes programmers want to rewrite everything because the old code just offends their sense of aesthetics? Sure. But it’s also true that those crappy old techniques contain bugs that no one ever noticed, and now no one knows how to fix any more. Sorry, Mr. Spolsky: code most definitely does age ... or at least there’s an equivalent process which causes it to be less useful as time passes, and we may as well call that “aging,” because it’s a commonly understood term.
And, here’s the thing: object-oriented code just ages better. You go back to some procedural code you wrote several years ago, and it’s going to be ugly in there. A real horror-show. Your OOP code, on the other hand ... well, it’s still not going to be pretty, but you can almost guarantee that there will be fewer problems overall, and what problems there are will be easier to fix. Unless you’ve tried to graft an object-oriented solution onto a procedural problem—which is a terrible idea in the first place, and certainly isn’t going to improve with the passage of time. But, for that class of problems where neither procedural nor object-oriented is better today, you will inevitably find that object-oriented is better tomorrow.
So, while you really should consider whether you have a real choice or not before beginning, once you do determine that you have a choice, you’re nearly always better off choosing OOP. Good; that’s one choice down. But it’s hardly the only choice you’ll need to make when starting a project. In fact, much more of our programming lives are ruled by another choice: do it fast, or do it right.
Now, just like procedural vs object-oriented, this is not really a binary choice. But there are definitely two opposing forces that act on us. Time pressure is something we often associate with work: we imagine that only pointy-haired bosses demand that things get done faster. But, in fact, we put time pressure on ourselves even in our personal projects: we know that we can’t move on to the next thing until we finish this one, and who wants to spend a lot of time on one problem? Variety is the spice of life and all that. In fact, when we are inexperienced programmers, time pressure is the biggest pressure there is. It takes a few years under our belts—a few instances where we have to go back and actually maintain the code that we wrote quickly as immature coders—before we start to put pressure on ourselves to do things properly, not to take shortcuts, to spend some time making sure that the code will be easier to change once that becomes necessary. And time to learn that it will become necessary ... it’s always necessary, unless you need to rewrite the code entirely, and tell me that isn’t a sign that you should’ve paid more attention to doing it right the first time.
So, for most of us, we start out spending more effort in doing it fast, until we have to maintain our own code for a while. And then we spend more effort doing it right, until we realize we’re not ever finishing anything. And, eventually, we find the proper balance. But still there will be day-to-day fluctuations on the amounts of these two pressures, in different situations at different times. And now I’ll finally come back to Perl, which is after all where we started.
I mentioned that, for a time, I was bilingual in both C++ and Perl. This meant that, every time I sat down to write a progam for myself (which, as I mentioned, I do a lot), I would have to decide whether I wanted to write it in C++ or Perl. There were many factors that would influence that decision, but one of the biggest was whether I wanted to use objects or not.
See, objects were easy to create in C++. Whereas, in Perl, it was possible to create objects, certainly, but it was a bit of a pain in the ass. Lots of boring, boilerplate code involved in hand-rolled objects. This meant that the “do it fast” force pressured me towards Perl (where I could quickly Get Shit Done), but the “do it right” force pressured me towards C++. This is a terrible place to be.
Of course, it actually got a bit worse once I left C++ behind. At that point I was in the position of “do it fast” pressuring me not to use objects. Oh, sure: if I was facing one of those problems where objects were obviously the right answer, then I’d suffer the pain of hand-rolling the objects in Perl. But when I was in the position of either procedural or object-oriented solutions being equally good (today), I would rarely bother with the hassle of using objects, even though I knew I’d regret it (tomorrow). The language itself was working against me, in a sense.
Which is why Moose becomes vital to my Perl experience. Which is what we’ll look at next week.
1 Which is a damn shame, really, because it’s a perfect description for certain times in one’s life, and it’s not the phrase’s fault that we overused it.
2 See also my discussion of balance and paradox on my Other Blog.
3 Not meaning to pick on any particular author, but I’ve always felt Text::CSV was a particularly egregious example of this, at least for its common usage.
4 For a longer rant on Spolsky—with Steve Yegge thrown in, just for fun—I will again refer you to my Other Blog.
You've mentioned several times that writing objects/classes in C++ is easier than in Perl. Would you be able to provide examples? Generally I find it to be the opposite, to the extent that I have Perl write my C++ code for me (via TT2).
Even without using any of the helper modules (whether Moo* or Class::*) I'm not really seeing how class definitions are any easier or shorter in C++ than they would be in Perl.
This is the sort of pattern I'm thinking of - maybe your code would look different and if so I'd be interested to know more:
vs. a similar definition in C++:
(both examples untested but hopefully they convey intention - motivation faded somewhat after fighting with movable type to get this submitted!)IMHO, it's not always about the length of the respective code samples. C++ does have its quirks of course--the big ones that I recall is the morass that is copy ctors and assignment operators, and the question of virtual dtors--but, in general, class/object code that you write in C++ Just Works. In Perl (sans Moose or other helpers), you have to handle a lot of things yourself, and there are lots of easy ways to shoot yourself in the foot. To take the most egregious example:
Since an object in Perl is just a (blessed) hash, your "attributes" are just keys in that hash. What happens if you mistype an attribute name? A run-time error ... if you're lucky. A long and frustrating debug session followed by a lot of cursing if you're not.
And that's just the most obvious one. You can get yourself into trouble by manually fiddling with
@ISA
, it's easy to write yourbless
es in such a way as to make inheritance harder, it's hard to remember the proper way to call the superclass in an overridden method, etc etc etc.But I'm not necessarily here to convince people they should not try to reinvent classes every time they sit down to write Perl code. There are lots of very good articles out there that will do that better than I. Let's assume for purposes of this discussion that I'm just one of those weird people who likes to write "class" when I mean "class" and go from there. (Although I will touch briefly on this next week.)
It would be nice if there were at least some arguments to back this with :)
Well, it's been my personal experience, and I'm guessing it's been the personal experience of most people who have spent any time trying it both ways. Of course, there are going to be people who reject OOP, as I mentioned, and I'm not going to convince them of this ... but then I wasn't going to convice those people no matter what I said. :-)
In the end, everyone's relationship with their language is a personal journey, and this one is mine. Other people will have had other journeys, and that's fine too.
Interesting. It sounded like it was the boilerplate that you were calling out in particular, apologies if I misunderstood.
Anyway, seems my experiences are pretty much the opposite - it's the Perl OO code which Just Works. Sure, a typo in a constructor call might not be obvious at first, if there are no checks on the keys:
but there's a lot more code required (and thus scope for errors) to get named parameters in C++ code - even with Boost or C++11 features like user-defined literals. Likewise with direct hash access: if you bypass accessors, it's easy to miss a typo. Even easier in C++ if you go poking around in internals, and I'd prefer to be looking for spelling errors than trying to decipher vtable trickery like (*(int**)obj)...
Then there's casting, slicing, lack of built-in introspection (this is one area where Perl has a *massive* advantage even without any of the Moo*/MOP extras), diamond inheritance... and that's not even starting on the fun task of deciding who owns what, whether a reference or shared_ptr/unique_ptr is more correct, or RAII helper classes.
C++ is one of my favourite languages but the object system provides epic opportunities for lost hours and frustration. Even on a good day Perl just doesn't feel like a challenge in comparison ;)
No, I probably just overemphasized that aspect of it. My C++ is so far behind me at this point that I forget all the boring boilerplate I had to write there too. :-)
Although, to be fair, I don't think the two code examples you gave in your original comment are strictly apples-to-apples. That Perl class is a fairly naive implementation. It doesn't validate any parameters, it assumes ctor args come in as a hash but doesn't check that, it wouldn't handle calling
new
as an instance method instead of a class method, etc. Whereas your C++ implementation is fairly sophisticated: it goes beyond the language-generated default ctor, it's smart enough to realize you need a virtual dtor, it uses methods for the rw attribute instead of just making it public, etc.Likewise, when you compare the troubles of objects in Perl (such as mistyping the attribute names) to the troubles of objects in C++:
again, that's not necessarily a fair comparison. Other than casting (which is a huge PITA in C++, certainly, but that's nothing to do with objects per se: you run into that annoyance with just
int
s andstring
s), those are not things that you're typically going to run into when doing simple object things. Whereas mistyping something happens all the time.At the end of the day, though, we agree completely:
Which is, ultimately, why I don't program in C++ any more. ;->