Save time and reduce errors with subprocedures
We don’t use subroutines in our code these days. We use subprocedures instead. Subprocedures have been around for years—since V3R2, in fact. Yet, when our Subprocedure Basics sessions are on a conference agenda they’re still among the best attended. Based on that, we must assume that many RPGers are still not regularly coding them. This month we'll discuss why we think you should be using subprocedures instead of subroutines. We’ll follow up next month reviewing how to write them—for those of you who haven't started yet or who’ve only cloned them from others’ examples and have never really learned the basics.
Both subroutines and subprocedures let us segregate and structure our logic. The performance of executing a subroutine versus calling a subprocedure is almost the same, although subroutines are marginally faster. So what makes subprocedures better? We have three primary reasons.
Reason 1: Local Data
Unlike subroutines, subprocedures can have data that’s not only defined inside the procedure, but is also private to that procedure (i.e., the locally defined data can’t be referenced outside the boundaries of the subprocedure). Local data is encapsulated along with the routine’s logic. Prior to IBM i 6.1 (aka V6R1), we could only define local data with D specs, so while fields, arrays and data structures could be local, files couldn’t be made local to a subprocedure. That’s not to say that file I/O can't be done in subprocedures prior to 6.1. That's because subprocedures can access the program's global data, including files, as well as their own local data. Not only that, but starting in V5R2, we’re able to use I/O operations with a Data Structure as the result field and this allowed the use of local data in I/O operations. Beginning with 6.1, subprocedures can include F specs as well as D specs, so we can now define local files too.
Many advantages to this local data concept exist. Perhaps the most significant is the data’s isolation to the specific routine makes maintenance easier, faster and less error prone. Say you're making a modification and you must change a field’s definition or clear out its contents. If that field were used in a subroutine, you’d have to check where else in the program that field was used before proceeding to avoid causing problems elsewhere. This takes time and slows down the maintenance process. Of course, in the interest of speed, we often assume this little work field (called TEMP5) is surely not used elsewhere, so it's not a problem if I make it bigger or smaller. If our assumptions are wrong, the program may fail in an area apparently unrelated to the change we just made or—perhaps even worse—just mess up the database!
On the other hand, if that little work field had been defined as local data inside a subprocedure, it couldn’t have been used anywhere else in the program. So we have far less code to review to accurately determine the impact of the change. Also, since the D and F specs related to the local data are physically close to the logic where they’re used, it's much easier to find them.
Furthermore, we probably wouldn't name that little work field TEMP5 anymore; we'd simply call it TEMP because it no longer matters if there are other fields in the program called TEMP—this one is local to this procedure. We're not suggesting it's a good idea to see how many fields of the same name you can define in one program, but it’s nice that you don’t need to search through the program to find a unique name for those little temporary work fields (or if you don't search, potentially reusing a field defined elsewhere by accident).
As a general rule, we restrict our subprocedure logic to accessing only local data, even though technically they can access global data as well. This practice helps ensure the aforementioned benefits. It also gives us the flexibility to decide later to take this routine to its own source member and compile it separately should we decide to share this code with other programs, which leads us to the second reason we use subprocedures.
Save time and reduce errors with subprocedures
Reason 2: Reusability
A large percentage of subroutines don't need to be shared. But for those occasions where the logic can and/or should be the same in multiple programs, sharing subroutines is problematic. First they must always be compiled with the program where they’re being used. Some shops have standardized subroutines that are /Copy'd into many programs. However, in our experience, it’s uncommon and even shops that do it don’t use as much of their common code as they should.
One of the biggest problems with the /Copy technique is that typically data definitions must be included along with the logic. While simple variables can be defined on the Result field, that coding style has (thankfully) fallen out of favor and is completely banned in most modern RPG shops. And, of course, it can’t be used with /Free format logic at all. Additionally, there’s no way to define arrays and data structures. To avoid this problem, some shops have gone to the trouble of pairing a D spec member with a C spec member and then /Copy both. Alternatively, they use conditional compiler directives to allow the use of a single-source member containing both the D and C specs. Both of these techniques are cumbersome. They also fail to solve other issues such as having a variable with the same name in the subroutine as one that’s already defined and used elsewhere in the program.
In practice, many shops, rather than /Copy a subroutine, clone the routine's logic and data definitions (changing variable names when needed) into other programs. Alternatively, the routine is written instead as a very small called program.
While the called-program solution is much better than code cloning, if the logic needs to be called frequently (e.g., once for each of several fields on every line of a long report), the program call’s performance could become an issue. More importantly, the call to the program may not make the intention of the call as obvious as it could be (see Reason 3).
Using block copy to clone the logic from program to program is absolutely the worst way to share logic. We could probably write an entire article covering the evils of cloning in this manner, but we'll just mention a couple of the biggest issues.
Maintenance of that shared logic is multiplied by the number of occasions it’s been cloned. If you’re lucky, this is the only problem. But most of us aren’t lucky and many of the instances of the cloned logic won’t be maintained consistently. This is because, even with good change-management tools, there’s no way to determine the location of all the cloned instances. Worse yet, since each copy is maintained separately and in its own context, over time logic that started off the same becomes very different—perhaps not even functionally identical any more and certainly not visibly identical. At a minimum, we tend to find that field names are changed to fit the program into which the code has been copied.
How do subprocedures avoid these problems? First, due to the local data we mentioned, mechanically a subprocedure can be more easily and safely /Copy'd into multiple programs. However, as good as that news is, it's not the recommended way to share the logic. The better way is to not only write the subprocedure in its own source member, but to also compile it separately and put it into a Service Program, typically along with several other sharable routines. Using ILE, that Service Program can be referenced by all programs that need the functionality. We retain the subroutine-like speed while keeping the advantages of an external program. Now, we have only one copy of the logic, so maintenance is simplified. When changes are required, we only need to recompile the small source member containing our shared routine. We don't need to recompile all the programs that use it because when the programs connect to the updated Service Program, they’ll automatically get the new version. Additionally, we can test any logic changes in isolation, secure in the knowledge that if our logical black box works in isolation, it’ll work when it’s called.
Save time and reduce errors with subprocedures
Reason 3: Function Calls are More Obvious Than EXSR
We've saved the best reason for last. We'd use subprocedures in place of subroutines for this reason alone because this often greatly enhances programmer productivity.
When subprocedures are called, we can pass parameters into them much like we do when calling programs. However, unlike programs, subprocedures can be called as functions—more like calling RPG's built-in functions (BIFs). This makes the logic obvious. A simple example can help illustrate this. Suppose you have a subroutine that does something very simple, such determining the day of the week of a given date. If you call this routine from several places in the program with different date fields each time, then you probably use some kind of generic field names in the subroutine to act as parameters. This means you must move data to and/or from those fields before and/or after you run the subroutine. So the logic may look something like this:
WorkDate = InputDate;
DayNumber = WorkDay;
When you and your fellow programmers look at this logic during maintenance, you'll almost certainly page down to look at the logic in DayOfWeek even if the task you're currently working on has nothing to do with the calculation. Why? Because otherwise you don't know what date value is being used as input to the routine nor where the result of running the routine (the day of week) will be stored. You may argue that the statements on either side of the EXSR provide that information, but chances are those three lines won't stay contiguous over years of maintenance. It’d be an educated guess at best, and perhaps more importantly, you’d have no idea what changes have been made to the program's fields without studying the subroutine code. So nearly all of the programmers who do maintenance in this part of the program will spend time looking for and understanding the logic of DayOfWeek even though it's not specifically relevant to the task at hand.
Now look at a subprocedure call to perform the same logic:
DayNumber = DayofWeek(InputDate);
How much more obvious is this? InputDate is clearly being used as an input parameter to DayOfWeek and the result of running the routine goes into DayNumber. It's not a matter of one line of code versus three lines. It's the fact that in one line of code, we get far more valuable information about what's going on—so much so that, in most cases, it won't be necessary for all of the programmers who look at this code to find or follow the logic of DayOfWeek. We don't have the full story about the routine, but it probably gives us enough information to move on without reading the logic. And that just saved us time.
Think of the possibilities of writing programs where the mainline logic consists of a series of these kinds of function calls. It’d read almost as pseudo-code and the overall logic flow would be much easier to follow. You’re more likely to only delve into the logic details of those functions that are potentially related to your specific maintenance task, eliminating the need to read and understand tons of logic you would’ve looked at if they were subroutines.
If you still doubt the benefits of subprocedures in this area, ask yourself this question: On average each day how many lines of code do I have to understand so that I can ignore them? If you answer that question honestly, you'll see that by making your code more understandable you can radically improve your productivity. Many programmers who perform mostly maintenance tasks will freely accept that approximately 90 percent of the code they read daily falls into this category. By reducing that percentage to 80 percent we’ve effectively doubled our productive time. Isn't that a worthwhile change?
The Power of Subprocedures
We’ve only scratched the surface of the tremendous benefits that using subprocedures offer. We could (and often do) go on for hours about why we use subprocedures. We hope we’ve convinced you, so next month, we'll review how to code these little gems.