Programs-to-to Programs Calls
Pogrammers new to the AS/400, especially those from the System/36 world, are sometimes confounded by all of the features of AS/400 programming. In addition to Control Language and new data base concepts, the capability of one program calling another is a programming construct that must be understood. Using the manuals, it is quite easy to understand the mechanics, but some of the concepts can only be learned through experience. This month, we will review program-to-program calls and some opinions that I have formed about this feature of AS/400 programming. The discussion is in terms of RPG, but the concepts are universal for any AS/400 language.
The simplest way to think of one program calling another is to think of RPG subroutines. Usually, a subroutine is written to perform a specific function within a program. Carefully coded subroutines can sometimes be used in multiple programs so that you don't have to keep rewriting the same function. Although some programmers insist otherwise, it has been fairly well accepted in the last two decades that programming with subroutines can make a program easier to understand, modify and extend.
The idea of program-to-program calls builds on this concept in a rather exciting way. Instead of your program executing a subroutine, the program calls another program. Just as a subroutine can modify variables, so can the called program.
Value for Effort
The value of coding a program-to-program call rather than a subroutine is that the called program has a life of its own, apart from the caller. A subroutine is part of the program that contains it, and is subject to the limitations of that program. For example, in a very large RPG program, you might exceed the number of files or table and arrays allowed. In order to keep the functionality that you want, you need some way to split out parts of the program. The program-to-program call technique is ideal for this situation.
Another advantage of calling programs rather than subroutines is that the functionality provided by the called program can change without necessitating a recompile. More to the point, called programs can be thought of as the mythical "black boxes" that we hear about. The idea of a black box is that we provide certain specific inputs and can expect certain specific outputs. Exactly how the outputs are produced is of no concern. In fact, we may never learn exactly how a black box does its job. Examples of this on the AS/400 are as near as your terminal. Type in any command. Here we have a well-defined interface, with specific inputs, and based on those inputs, we can expect certain results. How those results are produced doesn't really matter.
You might think that it makes no difference whether a function is included within a program or called. I have generally found it much easier to program and subsequently modify programs if the "grand events" are written as separate programs. For example, in an order processing application, it would be sensible to code the customer, inventory and order searches as separate programs. The actual order entry program itself can then call those other programs as needed.
What is the advantage of that approach? Primarily, it causes you to think of each program in terms of the problem that it solves. The customer search function should simply provide a search, probably returning the customer number to the caller. It really makes no sense to include the inventory search in the same program as the customer search, so that should be made into another program. The value of this approach is that the single purpose programs will be easier to maintain than the composite program. Also, when you need to change either search, you don't affect the other. This is especially advantageous in RPG, due to the lack of "local variables" in the language. My experience has been that adding new functions to RPG programs usually results in bugs being introduced to previously working functions. I am much happier when I can add a new function through a call to another program, since I feel more confident that the side effects are minimized.
One question that you will surely have is, "How far do I carry this?" To what level should you go, splitting out functions into callable programs? For example, should you create a separate program to edit or calculate dates, another to perform file I/O, and another to control everything by issuing multiple calls?
This is perhaps the most difficult part of program-to-program calls. The decision as to how granular your programs will be affects the design of the programs, the ease of coding them, and the ability to test and correct them. Also, your future interaction with the code in terms of modification, correction, and extension is influenced by your choices. Finally, there may be an impact on system performance.
Working backward through that list of worries, we can dispense of the performance concern rather quickly. For the most part, I don't worry too much about it. I would much rather trade ease of programming and modification for a performance hit, which may or may not be there. I mention this because in the early days of the System/38 we were subjected to much contradictory advice from IBM about what was the favored technique. At one presentation, we were told to split everything into modules as small as would be practical. A few months later, word was that keeping things together in bulky programs was more "efficient." The upshot was that we decided nobody really knew what worked best, so we decided to do what was best for us.
What has seemed to work best for me over the years is splitting to the level of a main screen. For example, for a customer file maintenance program, I would prefer putting the search into a separate program from the name and address entry screen. My reasoning is that these are two distinct functions, and I can foresee instances where I want to use the search, but not the detail display. Also, when I must change the detail display, the effect is contained (for certain) to that display, and should not have any effect on the operation of the search. This is just one example; another would be a purchasing or order entry application, in which you would split the header entry from the detail entry.
On the S/36, those programs would have been written as one monster program. You have probably worked on that program: remember the one that used all ninety-nine indicators - the one which everybody was afraid to touch? Well, I'm here to tell you that you do not have to do such things ever again, and the sooner you get away from it, the better off you will be.
Now, obviously if you go splitting out things that once were together, you will find that you must repeat quite a bit of code in each program. For example, an order header might require date editing, and the detail entry might also use date editing. So you will need to include the date edit subroutine in each program. Should you worry about this?
I don't think so. If you examine the routines that are going to become common to multiple modules, you can direct some attention to standardizing and optimizing those modules. You can then package those modules into /COPY modules (or the equivalent in whatever language you use), and then not worry too much about those functions in the future.
Isn't that bad, thoughDhaving the same code in lots of places? After all, if you have five main programs in an application, and four of the five need date edits, then you have four times as much code for that function, compared to putting all five main functions into one program. Well, so what? Some of the most common routines you will need, such as date editing, are really quite trivial in the scheme of things. Squeezing a few bytes with these will probably never yield any detectable performance benefit.
Don't get the wrong idea; I am not advocating profligacy with storage. It is just that some things are best done in the simplest possible way, which can be summarized as "code it and forget it." So where should you be looking if you are splitting out functions but trying to have some awareness of how that will affect the system?
There are a couple of factors at work here. Those are the manner in which you leave a called program, and the manner in which you reference files among programs.
Easy Come, Easy Go
If you have had the chance to observe a "native" AS/400 application, one that has programs calling programs, you have probably noticed that the first time you call a program, the delay is quite noticeable. Subsequent calls to that program usually go much more quickly.
The reason for this phenomenon is that there is a significant amount of work the system must do on the first call to a program. In addition to locating and loading the program, the system must also determine the files being used and create open data paths (ODPs) for those files, initialize work areas, verify authorizations, and on and on.
There are basically two controlled ways that you can return from a called program: either by terminating the called program, or by simply returning without terminating. In RPG terms, you can either return with LR set on, or you can return and not be at LR. If you terminate and return, the system must go through the exercise again on a subsequent call. You will notice the delay again, even if you use the program over and over.
So returning without terminating seems to be the obvious way to go. However, you must be aware that on subsequent calls, things in the program are exactly as they were when you left them. That is, no initializations are done unless you specifically code them.
The difference between these two techniques is similar to the difference between staying at the best hotel in the city or staying at the budget motel. In the fine hotel, you expect to be greeted and fussed over by the doorman and elevator operator and other staff; at day's end when you return to go to your room, the process is orderly and controlled, and you have the assurance that your room has been made up. When you return to the budget motel, you pretty much let yourself in without interference, and may find your room in the same condition that you left it. So the tradeoff is performance (getting to your room without delay) for expectations (expecting your room to be perfect as opposed to the way you left it).
Generally, it is not a problem to simply return without termination. You can always include an initialization routine to "clear up" things when you come back into a program. Most of the time, you won't have any problems with this, but it is something that you should look at when you are getting difficult bugs.
In a program-to-program application, one of the biggest performance boosters comes about by sharing open data paths. For example, our suite of five programs might use a total of twenty files, fifteen of which are used in multiple programs. Sharing a file is a technique by which you tell the system that you want it to work through the overhead of opening the file only once in your application. After the first open, the next program that uses that file can simply attach to the prior open.
Sharing files helps most noticeably when you have large applications with a large number of possible called programs. For example, an application might include thirty interactive programs, of which maybe a dozen are always called, and the remainder are called as needed. If the crucial dozen use the files as shared opens, the others will load more quickly when they are first called. If you return without termination, and then recall the program, you get the same effect as a shared open, since one of the consequences of returning without termination is that open files remain open. So sharing opens is most noticeable on the first call to a program, and does not really speed things on subsequent calls.
Does it Hurt to MRT?
Hot-shot S/36 programmers are probably yawning by now, having figured that a lot of this sounds like MRT programming. It's true, in a lot of ways; it is like that. The difference is that the system now takes care of the details for you so you can write your program without regard for the number of simultaneous users.
It turns out that the system knows how to split programs into the "common" and the "per user" parts. If your order entry program is being used by a dozen data entry people, you do not necessarily have an even dozen programs loaded. During compilation, the system determines the parts of your programs which must be unique for each user (their variables and work areas), and determines which parts of the code can be shared by all users. So without any extra effort on your part, your users are actually sharing parts of the same program. I have always found this to be more of a pleasing theoretical concept than something that I could readily observe, but I believe. So the long answer is, no, you do not and should not do anything special to your programs, such as try to replicate MRT logic. Write as if you were writing SRTs, and let the system worry about the rest.
To and Fro
Another idea that you will want to investigate is just how, exactly, you effect this program-to-program communication. Most S/36 programmers would use the LDA to pass data from one program to another. When the amount of data to be passed was too great, there was always the simple expedient of writing to a file, then letting the next program read that in.
Those techniques are still available with the AS/400, but the preferred method is to define a parameter list. A parameter list is simply a list of fields to which you want the calling program to have access. If you call a program and pass it a parameter, then change that parameter within the called program, the change is "known" to the caller when you return to it. This is in contrast to some PC languages, in which a copy of the variable is passed, but changes affect only the copy. The rule is easy to live with if you are aware of it.
The main rule about parameter lists is so ridiculously simple that I am astonished when other programmers (and I) don't get it right. That is, the number and types of parameters must be in agreement between caller and called. For example, if you have three parameters, "A," a character field length 10, "B," a character field length 1, and "C," a numeric 5.0 field, then you must reference those fields in that order with those definitions in the caller and the called. The names of the fields do not have to be the same, just the attribute (character or numeric) and the length.
I suppose the reason this causes problems is because a change in one parameter list might not occur in another. I get around this by simply creating an external data structure that contains all of the parameters, then referring to the data structure name in the parameter list. If I make a change to the list, I simply recompile all of the programs using the list; I do not have to go into each program and manually change the list. Think about it.
So how do you get started with programs calling programs? If you have just brought over a mass of programs from the S/36 to the AS/400, you might be desiring to get into "native" mode. I guess the most likely places to start looking are in the monster programs. See if you have searches coded into those programs; you may be able to split out a search and then call that rather than include it with other things. Or you might see if you have some unique calculations that are repeated in a number of programs. By splitting that routine out, you can make changes in only one place in the future, and breath easier knowing that you have covered all of its usages.
One of the nice features of the S/36 environment is that you can use the CALL RPG opcode so you can start taking steps without having to go the full route all at once. If all of your programming has been with RPG II, it might be hard at first to know what to do. But take some time to study and implement a program-to-program call application, because once you do, the benefits will be obvious. And you will never want to be without it again.