What programming language has been available for more than 20 years, is used by hundreds of thousands of mainframe programmers, has clear, crisp syntax and supports the following features?
The answer is Programming Language/I (PL/I). This article discusses some powerful features of the PL/I programming language -- those marked with ** in the preceding list. The article also covers the new PL/I features found in IBM's PL/I compiler for OS/2. It concludes with some comments on the possible future of PL/I in today's I/S shop.
[The article also applies to Enterprise PL/I, VisualAge PL/I, and AIX, as the OS/2 PL/I compiler has been ported to those systems - Eds]
Refer to Table 1 for a list of programming languages and language features. Note that PL/I stacks up nicely against other, even newer, programming languages.
Start Table 1:
____________________________________________________________________ Language Feature PL/I COBOL Pascal Ada C REXX Data Typing Rep Rep Strong Strong Strong Weak Dynamic Memory Allocation Yes No Yes Yes Yes Yes Pointer Usage/ Yes Yes Yes Yes Yes No Pointer Arithmetic Yes No Yes Yes Yes No User-Written Functions Yes No Yes Yes Yes Yes Approx # Built-in Functions 100 None 20 50 100 150 Multi-tasking Yes No Yes Yes Yes No Generic Procedures Yes No No Yes No No Parameter Passing Ref/ Ref/ Ref/ Ref/ Ref/ Ref/ Mechanism Val(1) Val Val Val Val(2) Val Parameters of Adjustable Extents Yes No No Yes No Yes Condition Trapping Yes Yes No Yes(3) No Some DBMS Connectivity IMS/DB2 IMS/DB2 DB2 DB2 DB2 DB2(4) Local Variables Yes No Yes Yes Yes Yes Array Support(5) Much Some Much Much Some Some PROCEDURE data type Yes No Yes Yes No No Object Extensions No Yes Yes Yes Yes No _______________________________________________________________________________ (1) Pass by value done by forcing PL/I to evaluate arguments as expressions [PL/I programmers on the mainframe, AIX & OS/2 can specify the attributes BYADDR and BYVALUE for parameters, in addition to using an expression to indicate call-by-value. -- Eds] (2) Pass by reference done by passing a pointer to variables (3) User-written exception handlers (4) Available via third-party software vendor (5) Some: Define arrays and pass arrays as arguments Much: Some, plus performing arithmetic on arrays and referencing array cross sections. _______________________________________________________________________________End Table 1: Language Comparison Chart (MVS Implementations)
PL/I gives programmers great flexibility in storage use. PL/I programmers can have their programs create and destroy data structures as the program executes. This feature comes in handy when programmers need a data structure of undetermined size, such as a stack or linked list.
Refer to Example 1 for a possible (but not unique) PL/I stack implementation. The stack itself has the CONTROLLED storage class, which means the program will create and destroy stack elements during execution. The ALLOCATE statement creates a new generation of the data structure -- a character string of varying length in this example. The FREE statement releases the memory held by the string created by the last ALLOCATE command.
Start Example 1:
_______________________________________________________________________________ STACK:PROC; DCL STACK CHAR(32767) VARYING CONTROLLED; POP: ENTRY RETURNS( CHAR(32767) VARYING ); DCL STACK_ELEMENT CHAR(32767) VARYING; STACK ELEMENT = ""; IF ^STACK_IS_EMPTY() THEN DO; STACK_ELEMENT = STACK; FREE STACK; END; RETURN( STACK_ELEMENT ); PUSH : ENTRY( STACK_ELEMENT ); DCL STACK_ELEMENT CHAR(32767) VARYING; ALLOCATE STACK; STACK = STACK_ELEMENT; STACK_IS_EMPTY:ENTRY RETURNS( BIT(01) ); RETURN( NUMBER_OF_ELEMENTS() = ZERO );; NUMBER_OF_ELEMENTS: ENTRY RETURNS( FIXED BIN(15) ); DCL ALLOCATION BUILTIN; DCL ZERO FIXED BIN(31) INIT(0B); RETURN( ALLOCATION( STACK ) ); END STACK; _______________________________________________________________________________End Example 1: PL/I Stack Implementation
The calling program invokes the stack routines with these statements:
STACK_ELEMENT = POP() ; ELEMENT_SUCCESSFULLY_ADDED = PUSH( STACK ELEMENT );Example 1 shows an interesting feature of PL/I: the ability to declare multiple entry points within one load module. The main routine STACK contains routines POP, PUSH, IS_STACK_EMPTY and NUMBER_OF_STACK_ELEMENTS in one load module. This feature helps package related routines; everything related to stack processing is in one place.
PL/I contains extensive support for arrays. PL/I programmers can create arrays up to 49(!) dimensions. The only limitation is that any dimension cannot exceed 32K elements. [this limit was true for the older PL/I-F compiler, but is increased to 2Gbytes in both the mainframe, AIX, Windows PL/I, and OS/2 PL/I products. -- Eds].
PL/I supports array arithmetic. For example, if the arrays A, B and C have identical dimensions and contain numbers, the following expressions perform element-by-element operations:
C = A + B: C = A - B; C = A * B; C = A / B;A handy PL/I array feature is starting the array index with any integer. For example, assume the following array holds average temperatures by year and day for the 1990s:
TEMPERATURE( 1990:1999, 365 )The average temperature for January 1, 1993 is:
TEMPERATURE( 1993, 1 )Negative integers and zero are also valid PL/I array subscripts.
PL/I programmers can pass entire arrays as arguments to procedures. They can use or pass array Cross Sections as arguments. A Cross Section is a piece of an array composed from one or more adjacent dimensions.
Assume the following array holds claim counts by state, by county within each state and by zip code within each county (assume no state has more than 255 counties and no county has more than 120 zips):
CLAIM_COUNTS( 52, 255, 120 )Assume the routine COMPUTE_STD_DEV takes a one-dimensional array as an argument. These expressions pass a Cross Section of CLAIM_COUNTS to COMPUTE_STD_DEV:
STD_DEV = COMPUTE_STD_DEV(CLAIM_COUNTS(24,10 *)); STD_DEV = COMPUTE_STD_DEV(CLAIM_COUNTS(*, 1, 1));This expression adds up the counts for state 2, county 5:
SUM( CLAIM_COUNTS( 2, 5, * ) )This expression adds up the counts for state 25:
SUM( CLAIM_COUNT5( 25, *, * ), )
The default parameter passing mechanism in PL/I is reference. Passing parmameters by reference means the runtime environment passes the starting address and a description of the variable, not the variable itself. In effect, the declared variables in the calling and called routines share the same storage.
PL/I programmers can easily pass arguments by value. Passing arguments by value means the runtime environment generates a copy of the argument and makes the copy known to the called program. The declared variables in the calling and called procedures do not share the same storage.
PL/I programmers enclose the arguments in parentheses to pass by value. For example:
CALL X( A, B, C ); A, B, C passed by reference CALL X( (A), (B), (C) ); A, B, C passed by value CALL X( A, (B), C ); A and C passed by reference, B passed by value.[PL/I programmers on the mainframe, AIX & OS/2 can specify the attributes BYADDR and BYVALUE for parameters, in addition to using an expression to indicate call-by-value. -- Eds]
PL/I routines accept parameters of varying extents. A parameter of varying extent has no true length or size until the calling program passes the argument. For example, a PL/I programmer can write a routine to reverse the order of the characters in a character string of any allowable length. This, of course, helps programmers write reusable routines.
Arrays can also be of varying extents. PL/I programmers can write a routine to sort an array of 10, 100 or 30,000 elements. The only requirement is that the variables in the calling and called routines have the same number of dimensions.
PL/I programmers use built-in functions to determine the parameter's true length or size. The LENGTH built-in function returns a string's length, and the HBOUND function returns the number of elements in a dimension of an array. [HBOUND gives the upper bound of an array. HBOUND() - LBOUUND () + 1 gives the extent -- Eds]
One of PL/I's most interesting (and probably underused) features is its ability to spawn concurrently executing tasks. These tasks can execute independently (asynchronously) or execute in a coordinated fashion (synchronously).
PL/I programmers can use asynchronous tasking to perform complex calculations on large arrays. One task can process the even-numbered array elements and another, separately executing task, can process the odd-numbered array elements. PL/I programmers can use synchronous tasking to simultaneously read and write files. One task can read the file; another task can write a different file.
PL/I programmers use event or task variables to spawn and coordinate tasks, with two built-in functions -- STATUS and COMPLETION. PL/I programmers can prioritize tasks with the PRIORITY option and check the priority of any executing task with the PRIORITY built-in function. PL/I performs task rendezvous with the WAIT statement.
See Example 2 for a PL/I code block that spawns two asynchronous tasks. The overall idea is to set the task status to normal and completion value to incomplete. The program generates the tasks, then waits for their completion. The program ends by resetting the status and completion values using the built-in functions.
Start Example 2:
_______________________________________________________________________________ TASKER:PROC OPTIONS(MAIN REENTRANT); DCL TASKS( 2 ) EVENT; DCL TASK_READY(2) EVENT; DCL TASK_COMPLETE(2) EVENT; DCL THE_TASKS(2) ENTRY INIT(TASK1,TASK2}; DCL NUMBER_OF_TASKS FIXED BIN(15) INIT(2); DCL TASK_INDEX FIXED BIN(15); DCL (STATUS,COMPLETION) BUILTIN; DO TASK INDEX = 1 TO NUMBER_OF_TASKS; STATUS( TASK_READY(TASK_INDEX ) ) = 0; CALL THE_TASKS( TASK_INDEX ) ) EVENT( TASKS( TASK_INDEX ) ); COMPLETION( TASK_READY (TASK_INDEX ) ) = '1'B; END; WAIT( TASK_COMPLETE(1), TASK_COMPLETE(2) ); DO TASK_INDEX = 1 TO NUMBER_OF_TASKS; COMPLETION( TASK_COMPLETE (TASK_INDEX ) ) = '0'B; STATUS( TASK_READY( TASK_INDEX ) ) = 4; COMPLETION( TASK_READY( TASK_INDEX ) ) = '1'B; WAIT( TASKS( TASK_INDEX ) ); END; END; _______________________________________________________________________________End Example 2: PL/I Spawns Two Asynchronous Tasks
The situation for synchronous tasking is more complicated than the asynchronous one. In synchronous tasking, the programmer must assume responsibility for correct task coordination. The two big problems that arise with synchronously tasking programs are the use of shared memory and task locking.
Shared memory refers to two or more tasks referencing and changing the same variables. The programmer must ensure that each task receives the data from shared memory that the task expects. Remember that just because you call Task A before Task B, you have no guarantee that Task A begins execution first.
Task locking refers to a task entering a WAIT state because of activity in another task. The simplest case is that Task A must wait for some event to complete in Task B, and Task B must wait for some event to complete in Task A.
PLITEST, the IBM source-level debugger for PL/I, supports multitasking. As you step through your program, PLITEST jumps from task to task. You can step through your program and examine the contents of shared memory. If PLITEST hangs on a WAIT statement, your tasks are locked.
[Mainframe PL/I also provides the interactive debugger CODE/370. --Eds]
You must allocate the PL/I multitasking libraries to DDName SYSLIB when linking multitasking PL/I programs.
None of these language features is possible without using PL/I built-in functions. Every example shown uses these functions. They allow the PL/I program to determine facts about data and change it.
PL/I built-in functions deal with arithmetic, mathematical computations, I/O, tasking and character strings. If you want to do something to, or learn something about, a variable, look first to PL/I's extensive built-in function library. You could save yourself plenty of code.
PL/I also supports user-written functions. A user-written function is a PL/I routine (internal or external) that returns a single value. Syntactically, you use a user-written (or built-in function) in expressions as you would a variable or constant. Semantically, the runtime replaces the function's returned value with the function reference.
PL/I programmers can code routines to trap and handle conditions, regardless of where these conditions occur in the program. The only requirement is that the condition trapping code must appear before the condition it traps can occur. When the trapped condition occurs (the PL/I term for this is "raised"), the program transfers control to the trapping code, executes and returns.
For example, if a program requirement is to replace invalid data items with a zero, the condition trapping code can be:
ON CONVERSION BEGIN ; DCL ONSOURCE BUILTIN ; ONSOURCE() = '0'; END ;[text and example changed to correct an error -- Eds.]
ONSOURCE is a built-in function that returns the invalid data. Here, the program uses ONSOURCE as a pseudovariable, which replaces the invalid data with a zero and blanks. Note that the program in Example 2 uses the built-in functions STATUS and COMPLETION as pseudovariables.
PL/I allows programmers to detect any errors that may arise in the program. For example, the RECORD condition means the program found a mismatch between the file's logical record length and the data structure to hold the file's record. The RECORD condition is actually two separate conditions: the LRECL could be shorter or longer than the data structure. If programmers need to know which condition arose, they can use the ONCODE built-in function to return a four-digit code. The trapped condition is the ERROR condition. For example:
ON ERROR BEGIN : DCL ONCODE BUILTIN ; SELECT ( ONCODE() ) WHEN ( 20 ) PUT SKIP LIST( 'LRECL Longer Than Data Structure'); WHEN ( 21 ) PUT SKIP LIST( 'LRECL Shorter Than Data Structure); OTHERWISE PUT SKIP LlST('Unknown Error - ', ONCODE( ) ); END; END ;
PL/I Package/2 is IBM's PL/I implementation under OS/2. [called PL/I for OS/2 from 1994. -- Eds] IBM added features to the OS/2 PL/I compiler not found in the MVS product. These features include the following.
DCL MAX_NUM_GENS FIXED DEC(5) VALUE( 24543 ) ;The compiler catches any statement that tries to change the constant's value.
DCL STDDEV ENTRY( (*,*) FLOAT(15) BYVALUE) EXTERNAL RETURNS( FLOAT(15) ) ;This declaration identifies an external function that accepts a two-dimensional array of any allowable size as a parameter, passes the array by value and returns a floating-point number.
DEFINE ORDINAL DAY_OF_WEEK ( SUN, MON, TUES, WED, THURS, FRI, SAT ) ; DCL TODAY ENTRY EXTERNAL RETURNS( DAY_OF_WEEK ) ; DCL TOMORROW ENTRY EXTERNAL RETURNS( DAY_OF_WEEK ) ;The preceding shows an enumerated data type and two routines that return a value of this type.
DAYS DAYSTODATE DAYSTOSECS SECSTODATE SECSTODAYS WEEKDAY DATETIME (giving the date in a variety of formats) DATE TIME SECS [Current IBM Enterprise PL/I, VA PL/I for Windows, and PL/I for OS/2 functions substituted-- Eds.]
Packages allow PL/I programmers to encapsulate data structures and operations on these structures. PL/I programmers can declare these structures and operations (in the form of PL/I PROCs) in packages and export only the operations. The programmer importing the operations has no knowledge of the details of the data structure. This programmer must deal with the data structure through the exported interface.
For example, Example 1 shows a PL/I stack implementation. Here, the PL/I program using a stack must declare the stack as a variable-length character string. If the program in the example changes the stack representation, all programs using the stack must also change. Using packages, the program using the stack routines need not declare a stack element. The program using the stack knows only about the stack manipulation routines. If the stack representation changes in the package, the programmer does not change any programs that use the stack.
[These features have now been available on the mainframe, Windows, and AIX for some years. -- Eds]
The future holds some surprises for PL/I programmers. Many believe PL/I is a dying language. Those with long memories recall that industry pundits have also predicted the death of COBOL for 20 years! If you browse SHARE or GUIDE literature, you will see strong interest in PL/I. With PL/I now available under OS/2, you may see more PL/I programming in the future.
[IBM PL/I for OS/2 was released in 1994. IBM PL/I for AIX was released in September 1995, 3 months prior to the publication of this article. Liant's Open PL/I is available under Windows and many Unix platforms. Kednos's PL/I compiler is available on Digital Unix. --Eds]
PL/I programmers may find a home in COBOL/370. COBOL/370 (which some say suffers from PL/I envy) has language features found in PL/I. If your COBOL/370 application needs a user-written function library or some dynamic data structures, look to PL/I programmers; they may have experience coding these features.
Lou Marco has 13 years' experience in data processing training and I/S and is the author of "ISPF/REXX For Experienced Programmers" (ISBN I-87895650-7, CBM Books, Ft. Washington, PA). He is a computer scientist and mathematician at Shapiro Consulting Group, Inc., 1310 Highway 620 South, Austin, TX 78734, (512)263-7540.