I complained in my previous post that discussions of character building often err in one of two ways, either focusing on manageable changes whose benefits are vague and indirect, or on changes whose benefits would be specific and direct if they could be achieved, but failing to explain exactly how they can be achieved. Right after I finished the post I started in on a project which has been on my stack for awhile, and as I proceeded it slowly dawned on me that it an excellent illustration of how to walk a third path. Unfortunately, the project is a programming project, and to get to my point I will need to lay some groundwork in an area my readers may not be familiar with. I’ll try to be gentle and do my best to not burden you with unneeded details. I think the effort it takes to understand the example will yield rewards, but I also completely sympathize with any reader who chooses to wait until I have a more relatable example with which to make the point.
When writing a program of any scale, the traditional approach has been to decide what tasks the program needs to perform, then start building pieces that accomplish the various tasks, and once the pieces seem to be capable of performing their tasks to assemble them into a whole which a user can use to get their work done. This means, for one thing, that a lot of writing must happen before one has even the smallest bit of practical capability, i.e. a program that does anything at all.
Thirty years ago, though, I saw someone approach the problem in a totally different way. I was working in the research labs at Texas Instruments, and as part of a joint project with MIT I was “donated” to Prof. Steve Ward, then the head of their Laboratory for Computer Science. Debbie and I moved to the Boston area, and I spent the next two years being directed by Steve on various programming projects.
Though he didn’t program much anymore, Steve had a reputation around the lab for being an excellent intuitive programmer. One lab member told me (approvingly) that he was known for being able to “debug a blank screen.” I had no idea what that meant until one day when we were both in his office discussing a project, deciding that we needed a small utility program that shouldn’t take more than a day or two to write. He suddenly turned to his computer, opened a text editor window, wrote a program that contained exactly one function with no content, i.e. it did exactly nothing, ran the program, saw that it did nothing, and said “Well, that didn’t work right!” He then proceeded to “debug” the program into existence, making small changes that inched the behavior of the program toward the behavior he wanted, checking each step of the way to see that his incremental change did exactly what he intended, no more and no less. After an hour or so he had a program which not only did what was needed, and only what was needed, it “did it right”. I was amazed, even more so over the coming days when I built on his initial work and came to appreciate how robust and elegant his code was. And I absorbed part of his ethic in my own work. But only informally, and whenever the chips were down, i.e. deadlines loomed, I always reverted to my old ways. I knew I would be much better off in the long run if I could fully embrace Steve’s approach, and was pained each time I suffered consequences I knew his approach would have spared me. But none of that was enough to convert me.
Meanwhile, Steve’s approach (not original with him) has become more known, more understood, and more formalized, even earning a name: Test-Driven Development (TDD). Programmers have long known that they benefit greatly by subjecting their code to automated testing. A body of tests is developed which in their way specify how the program is supposed to behave, and as the program grows and evolves the tests can be run again and again to insure that no matter what changes the programmers have made, the program still behaves the way the tests say it should. The major weakness in automated testing, as you can imagine, is that only the running of the tests is automatic—the tests themselves still need to be written, by programmers who find writing tests boring and tedious and of little immediate benefit. So the comprehensiveness of test suites, when they exist at all, tends to be scattershot.
Those who preach the gospel of Test-Driven Development, though, are promoting a very different approach to creating a program, more or less turning the old model on its head—write the tests first, then create a program which can pass them. You can see right away that, if nothing else, this approach will not only get the test written but will produce a battery of them that comprehensively tests the program in question. And if you squint a bit, you can see how practicing such a discipline requires a very different rhythm and outlook from the traditional approach.
Being quite aware of the benefits, I still never wrote tests in tandem with writing code, only long after the fact, and only when the organization required it. If the project was solely my own, I might resolve to give it a try, but any effort I made quickly petered out in the face of just wanting to get the work done. And for the years 2001-2012 the question was more or less moot, since what little computer work I did didn’t involve coding. But then I saw that my new job, managing a worldwide network of bluegrass music teachers, would be much easier if only I had a certain sort of software program, one which—as far as I could discover—no one had yet taken the trouble to write. I had idly kept up with developments in programming, but now I took the opportunity to re-immerse myself in that world. I dusted off my second-favorite programming language, learned my way around a website-building framework which used it, and started sketching out the program I needed.
I also stumbled across an amazingly good book, Harry Perceval’s Test-Driven Development with Python. I can’t imagine a better introduction to the discipline, warm and engaging, clearly explaining the how and why, containing an extended tutorial centered around the creation of a useful and realistic website for creating and managing to-do lists. I read the book, caught TDD fever, and knew that I would use its techniques to create my new program. And then time pressures built up, and my good intentions were jettisoned in order to get the job done, using the old approach which for better or worse I had thoroughly internalized.
Normally that would be the end of the story, a tale of one more Good Idea abandoned in the breach. I managed to get the first version of my program sufficiently functional to be put into everyday use—and it worked pretty well, and indeed made our back office operation way more efficient and error-free. In fact it was so useful that I wanted to add some additional capabilities I hadn’t initially thought of, and so I did that. And as I did, the program often broke in unexpected (sometimes embarrassing) ways, because I had forgotten details of how the internals actually worked. And of course nearly every one of those flaws would have been detected right away if only I had written tests for my program—and not necessarily the comprehensive testing that TDD requires, but any tests at all, feeble as they might have felt at the time. By the way, feeble is an important part of the puzzle, at least as it exists in the mind of the practitioner—I was sold on testing, had a clear understanding of what a comprehensive test suite would do and the blessings it would bestow—but I also knew how much work it would be to create a comprehensive test suite, and how feeble my initial efforts would be when measured against that monumental standard. In other words, my understanding of the ultimate goal, and even my zeal to reach it, were working against me—anything I would actually be able to do as I set out on this path would be so small and ineffective in comparison, that I was more comfortable daydreaming about the destination than setting out on the journey.
So when a week-long lull came along, I sat down and dutifully worked through Perceval’s tutorial, getting hands-on experiences with TDD (even if it was only second-hand), chasing down his references, immersing myself as best I could in the project. The idea was that I would get a better feel for the kind of tests that TDD leads one to create, then go back to my program and do my best to create such tests after the fact. Imperfect, of course, since the tests should really come first, with the program evolved to pass them. But since any tests would be better than none, it would be a way to get at least a limited and imperfect test suite in place.
And still I wasn’t able to make progress on the real problem. Even though I had absorbed the TDD principles, and even practiced them through the hands-on tutorial, it was all very little help when I thought about how to go about creating even a single test for my already written program. Since Perceval’s tutorial was aimed at the initial creation of a program, the “But … how” gap was just too wide for me to bridge in trying to apply it to an already created program.
One possible solution, which I thought about seeking (though I don’t know if it actually exists) is to find a similar treatment of the problem I actually faced, creating a test suite for an already existing program. But although there are likely such treatments, I doubt that any of them are excellent—otherwise I would have heard of them already (the software community is quick to promote an excellent canonical example). And more important, I didn’t really want to learn how to retrofit tests, I wanted to become a practicing TDD disciple. Creating the tests for my existing program was only intended to be a step on the path, and if it turned out not to be practical I didn’t want to get sidetracked.
Fortunately I have some time in my schedule to take on a discretionary project, so I decided I would repeat the process that Perceval had taken me through, but this time with a program of my own choosing, similar enough that I could adapt the steps of his tutorial but different enough that I would not be able to unthinkingly fall back on the details he had worked out in advance. I searched around for a good idea, a program that wasn’t too ambitious but would still be useful to me in day-to-day work. Not an easy task! But fortunately as I was exploring some of the capabilities my to-do list manager provides (Todoist, highly recommended), I ran across a program some other fellow had written to make Todoist do certain very useful tricks, and as I looked at his source code I found myself thinking, “I would do all this differently … and better, at least for my purposes”. So my personalized tutorial had now become the task of building such a program.
And still I found myself blocked. But this time in a good way. It wasn’t immediately obvious to me how to take the first couple of baby steps Perceval had provided and adapt them—my program was just different enough to make adapting them less than trivial. But why should they be trivial? I resigned myself to the fact that there was going to be some work involved here. But at least the work was clearly defined and not open-ended. It took me a day or so of fairly hard thinking to lay the groundwork and take that first step (which is so trivial in its scope that a programmer might laugh at spending any time at all worrying over it). And it finally became clear how to take the step as TDD required. And once I actually done it—installed the needed software, created the file folders, edited the first file, and so on—I had a feeling that I was actually on my way, plus a clearer notion of how to take the second step.
To invoke an idea my boss (the music teacher) is fond of quoting, I am currently traveling the “ugly part of the learning curve”. There are many ways of viewing the learning curve (and you’ll see a lot of bogus examples out there), but this is the one I’m referring to.
The X-axis represents the amount of time invested in learning the task, and the Y-axis one’s proficiency at the task. The earliest portion, say between 0.0 and 2.0 in the diagram, is the ugliest part—lots of effort expended, very little payback in terms of proficiency gained. But there’s no other path to the latter part of the curve. And, good news—between 2.0 and 4.0 the increase in proficiency is finally detectable, and between 4.0 and 8.0 it actually becomes dramatic, major leaps in proficiency costing relatively small amounts of effort. There are good solid reasons behind this, some of them intuitive (e.g. we build on what we’ve done, so the more we’ve done the easier the subsequent building).
So the initial lesson is: if you can find a way to get started on the learning curve and stick with your efforts during the early ugly section, you will be amply rewarded as time goes on. And you have probably heard this before, in some form or other—just stick with it, it gets easier, the rewards will be worth it. This often comes off as fairly trite cheerleading, for good reason. When someone is stuck in a practice and not making obvious tangible progress, it is possible that they are still in the early ugly section of the curve. But it ain’t necessarily so— they could also be headed down the wrong path, one which will never repay effort. Or they could be engaged in something where proficiency is not involved (I suspect prayer is one of these things).
Given these uncertainties, I wouldn’t conclude that the idea of the learning curve is too vague or trite to be of use in pursuing a practice. What I would suggest is that there are a certain subset of practices which for certain people will exhibit a learning curve, and for those we should be willing to put up with initial difficulties for the sake of future payback. How to know whether a certain practice will exhibit a learning curve for oneself? Advice from an expert coach or mentor can be very helpful, but in the absence of that I’d recommend low-risk experimentation. Identify some small, easy steps that seem to head in the right direction, take them, and (keeping expectations low) see what happens. The results might not be dramatic—but is there any positive result at all? If so, this should warrant a bit more experimentation, a few more similar steps to build reassurance, or one step that is slightly more ambitious. After that, do you find yourself somewhat more comfortable with the practice, or more adept at detecting the effects, or having a clearer understanding of how you could have been slightly more effective in your efforts, or with a clearer view of how to proceed into new areas? These are all hints that a learning curve exists and is available to a person with your makeup.
Applying this to my tale of software testing: all my understanding and zeal for the Test-Driven Development approach were not enough to make me a TDD practitioner, or set me on a path to becoming one. I had to find a path which was viable for me, given my background and abilities, one where each step was small enough to be manageable yet in-the-right-direction enough to produce a tangible result, one that would keep me at the work of becoming a practitioner—in fact, allowing me to understand that I actually was a practitioner at that point, though a very inexperienced and unskilled one. But experience and skills are things that can be gained over time, through effort.
And now (finally!) applying this to my complaint of teachings on the Christian disciplines falling short in the “But … how?” department. “Read your Bible, pray every day” are achievable goals, but practiced for their own sake I don’t know they will result in much more than a greater knowledge of the Bible and … well, I’m not sure that excellence in prayer is actually a suitable goal, any more than achieving excellence in driving or breathing. Similarly with faithful church attendance, or giving of our time/talents/resources, or even obeying God’s commandments—these things can have beneficial effects as part of greater efforts in certain directions, but on their own they can be performed with little or no effect on the practitioner. They can increase one’s understanding and stoke one’s zeal, but—as with most of my encounters with TDD philosophizing—they will not by themselves make you a practitioner, and are not even likely to set you on the path to becoming one.
Similarly with lofty goals that are effectively destinations easy to envision, admire, and yearn for but impossible to chart a path towards. We all agree that we should love God and others selflessly, live godly lives, esteem others more highly than ourselves, love our enemies, bless those who persecute us, and on and on. But if we select one of those as a goal—say, selflessness, or godliness—our imaginations tend to fail us when we try to envision any practice beyond “be less selfish” or “think about God more”—we are unable to map out a practice that will slowly, steadily, and inevitably make us into people who are truly selfless, or godly. And so we content ourselves with establishing the standard, and discussing it endlessly among ourselves, and exhorting one another towards it (without giving explicit directions), and feeling bad when we contemplate how far we fall short of it. But rarely do we seek out and set off on a path we think will lead us there—and even more rarely do we stick with it through the difficult early stages.
This is why I’m intrigued by the idea of focusing on kindness as a goal. Most everyone understands what kindness is—they can usually identify a given action as kind, unkind, or neutral, and are generally willing to do so (as opposed to right or wrong or good or sinful). Most everyone is attracted by kindness and repelled by unkindness. Most everyone sees that kindness is operative at all levels, in small actions and grand ones, easy actions and difficult ones. And most everyone has encountered kindly people, and found them attractive. Kindliness is not the whole of Christian character, but it is certainly a vital part of it, an outworking of many other Christian virtues, a mature Christian should certainly exhibit it, and a Christian who doesn’t exhibit it is flawed in some fundamental way.
One thing I think most everyone no longer believes, though, is that it is possible to become a kindly person, to grow in kindliness through deliberate practice. I disagree with that, and I think in fact that a clear path can be charted where the early steps are not only small and manageable but have definite, tangible (though small) rewards to pay, enough to motivate the pilgrim to follow a path which will slowly, steadily, and inevitably turn them into a kindly person. I also think that anyone who walks the path of kindliness, no matter where they begin, will slowly, steadily, and inevitably be drawn closer to God—even an unbeliever. And I think it is possible to chart such a clear and manageable path that we could honestly to say to any modern: Would you like to become a kindly person? Come with me! (I think such paths were available to premoderns, but times really have changed and the path must be charted anew for these post-Christendom times.)
If you’ve stuck with me this far and find any of this intriguing, I’d like to hear your reactions. It doesn’t have to be kindness, but: do you think we can improve any specific aspect of our Christian character through deliberate long-term practice? and, if so, what keeps us from doing it (assuming that you agree with me that it hardly happens, and is certainly not part of normal church practice)?