A procedure is simply a collection of statements that you can call and run whenever you need them. Think of it as a self-contained block of code that does one specific job.
In RPG on IBM i you have three kinds of procedures: regular subprocedures, linear-main procedures, and cycle-main procedures. When the compiler turns your RPG source into a module it looks at the procedures you have written and decides what kind of module to create. That decision is controlled by the NOMAIN or MAIN keyword on the Control specification. You end up with one of three module types: Cycle, Nomain, or Linear-main.
The word subprocedure is used as a general term for both regular subprocedures and linear-main procedures.
You can split an RPG source program into two main sections that hold your procedures:
Main source section
This is everything from the very first line of your source down to the first Procedure specification. In a cycle module this section can contain calculation specifications (either standard or free-form) that form the cycle-main procedure. Even if you write no calculation specs at all, a cycle-main procedure is still implied. You do not use Procedure-Begin and Procedure-End specs to mark it. A cycle module can be written without any subprocedures at all, so it may have no separate Procedure section.
Procedure section
This section holds zero or one linear-main procedure plus one or more regular subprocedures. Each procedure starts with a Procedure-Begin specification and finishes with a Procedure-End specification. The linear-main procedure is the special one you create by putting the MAIN keyword on the Control specification.
Subprocedure Definition
A subprocedure is any procedure you define after the main source section. The biggest difference between a subprocedure and a cycle-main procedure is that a subprocedure never uses the RPG cycle. It runs once from start to finish and returns control when it is done.
You can (and usually should) write a prototype for the subprocedure in the definition specifications of the main source section. The prototype tells the compiler exactly how to call the procedure and makes sure the caller passes the right parameters. If you do not write a prototype the compiler creates one automatically from the procedure interface.
Here is a quick tip that will save you a lot of headaches later: it is optional to put a prototype inside the module that actually defines the procedure. But as soon as that procedure is exported and will be called from other RPG modules, the prototype becomes essential. Put the prototype in a copy file and include that copy file in both the defining module and every module that calls the subprocedure. You will thank yourself later.
The examples below show the same subprocedure written two ways. First in free-form and then in fixed-form. The procedure takes three numeric values passed by value and does a simple calculation on them. It also shows how you define the procedure interface and how you return a value.
Free-form version:
DCL-PR Function INT(10);
TERM1 INT(5) VALUE;
TERM2 INT(5) VALUE;
TERM3 INT(5) VALUE;
END-PR;
DCL-PROC Function;
DCL-PI *N INT(10);
TERM1 INT(5) VALUE;
TERM2 INT(5) VALUE;
TERM3 INT(5) VALUE;
END-PI;
DCL-S Result INT(10);
Result = (Term1 / Term2) * Term3;
return Result;
END-PROC;
Let's walk through the numbered parts so you can see exactly what is happening:
- Prototype. This tells the compiler the procedure name, the return value (if any), and the parameters (if any). Because this procedure is not exported from the module the prototype is optional.
- Begin-Procedure specification. This marks the start of the subprocedure.
- Procedure-Interface definition. This repeats the return value and parameters. It must match the prototype exactly. If the subprocedure returns nothing and takes no parameters you can skip this line. If you did not code a prototype the compiler would use this interface to build one automatically.
- Local definitions. Any variables you need only inside this procedure go here.
- Calculation specifications. These are the actual working code (standard or free-form). You can use both local and global fields. Any subroutines you write inside the subprocedure stay local to it. If the procedure returns a value you must include a RETURN operation.
- End-Procedure specification. This marks the end of the subprocedure.
Except for the procedure-interface definition (which can appear anywhere in the definition specs) you must code the subprocedure in exactly this order.
Because no cycle code is generated for subprocedures you cannot use:
- Prerun-time or compile-time arrays and tables
- *DTAARA definitions
- Total calculations
The calculations run once and the procedure returns at the end of the calculation specs.
You can export a subprocedure so other modules in the program can call it. Just add the EXPORT keyword to the Procedure-Begin specification. If you leave it off the subprocedure can only be called from inside its own module.
Procedure Interface Definition
If your prototyped procedure has parameters or returns a value, you must code a procedure interface definition. For the full details on procedure interfaces see the section on Procedure Interface later in the manual.
Return Values
When a subprocedure returns a value it works just like a user-defined function. To make it return something you do two things:
- Define the return value on both the prototype and the procedure-interface definitions.
- Code a RETURN operation that supplies the actual value.
You set the length and data type of the return value on the procedure-interface line (the DCL-PI statement or the PI definition spec). You can also use these useful keywords:
- DATFMT(fmt) for date format
- DIM(N) to return an array
- LIKE(name)
- LIKEDS(name)
- LIKEREC(name{,type})
- PROCPTR for a procedure pointer
- TIMFMT(fmt) for time format
The operand of the RETURN operation follows the same rules as the right-hand side of an EVAL. The returned value itself behaves like the left-hand side of an EVAL. Make sure you always execute a RETURN if the subprocedure is defined to return a value. Otherwise the caller will get an exception.
Scope of Definitions
Anything you define inside a subprocedure is local to that subprocedure. If you create a local item with the same name as a global item the local one wins inside the subprocedure.
A couple of important exceptions to remember:
- Subroutine names and tag names are always local to the procedure where they are coded, even inside the cycle-main procedure.
- Fields used on input and output specifications are always global. When a subprocedure does a READ or WRITE the global name is used even if a local field has the same name.
Global KLIST and PLIST definitions can be tricky. If a field in the list has the same name as a local field the global field is used. This can cause unexpected results when you set up the list before using it.
Subprocedures and Subroutines
A subprocedure is like a supercharged version of a subroutine
Here is why most modern RPG programmers prefer subprocedures:
- You can pass parameters by value (so the caller does not have to worry about the called code changing its data).
- The compiler checks parameter types and counts at compile time, which catches errors early.
- You can use a subprocedure directly in an expression just like a built-in function.
- Names defined inside the subprocedure are invisible outside it, so you get much better encapsulation and less risk of accidentally changing shared data.
- You can export the subprocedure and call it from other modules.
- Subprocedures support recursion.
If you do not need any of those benefits a simple EXSR to a subroutine is usually a little faster. But for almost everything else a subprocedure is the cleaner and safer choice.
