Functions in PL/I
Learn how PL/I packages reusable logic with subroutine procedures, function procedures, parameters passed by reference, nested scope, and recursion - all with Docker-ready Iron Spring PL/I examples
In PL/I, the building block for reusable logic is the procedure - the same PROCEDURE construct you already met in Hello World, where the program itself is just a procedure marked OPTIONS(MAIN). PL/I doesn’t have a separate “function” keyword. Instead, a single mechanism plays two roles depending on how you declare and invoke it.
A subroutine procedure performs an action and returns no value. You invoke it with the CALL statement. A function procedure computes and returns a value by declaring RETURNS(type) and executing a RETURN(value) statement; you invoke it by writing its name inside an expression, exactly like a built-in such as SQRT. As an imperative, procedural, structured language, PL/I lets you nest these procedures inside one another, giving each its own block scope while still seeing the variables of the enclosing block.
This tutorial covers both forms, how parameters are passed (PL/I’s default is by reference), how nested scope works, and how to write a recursive function. Because PL/I is strongly and statically typed, every parameter and return value carries an explicit data attribute, and we’ll declare them throughout. We finish with a function that returns a character string, showing that procedures aren’t limited to numbers.
Subroutines vs. Functions
The clearest way to understand PL/I procedures is to see both kinds side by side. GREET is a subroutine: it has no RETURNS clause, produces output as a side effect, and is invoked with CALL. RECTANGLE_AREA is a function: it declares RETURNS(FIXED BINARY(31)), hands a value back with RETURN, and is used inside an assignment expression.
Create a file named functions.pli:
FUNCS: PROCEDURE OPTIONS(MAIN);
DECLARE RESULT FIXED BINARY(31);
/* A subroutine is invoked with the CALL statement */
CALL GREET;
/* A function is invoked inside an expression */
RESULT = RECTANGLE_AREA(5, 3);
PUT SKIP EDIT('Area of 5 x 3 rectangle: ', RESULT) (A, F(2));
/* Subroutine: performs an action, returns no value */
GREET: PROCEDURE;
PUT SKIP LIST('Welcome to PL/I procedures!');
END GREET;
/* Function: declares RETURNS and hands back a value */
RECTANGLE_AREA: PROCEDURE(WIDTH, HEIGHT) RETURNS(FIXED BINARY(31));
DECLARE (WIDTH, HEIGHT) FIXED BINARY(31);
RETURN(WIDTH * HEIGHT);
END RECTANGLE_AREA;
END FUNCS;
The two internal procedures sit at the end of the main procedure block. PL/I does not “fall into” them during normal sequential execution - a nested PROCEDURE statement is bypassed and only entered through a CALL or a function reference. Note the use of PUT EDIT with the F(2) format item: edit-directed output places the number right-justified in a field exactly 2 characters wide, which makes the output predictable rather than relying on default spacing.
Parameters and Pass-by-Reference
PL/I passes arguments by reference by default: the parameter inside the procedure refers to the same storage as the argument in the caller, provided their data attributes match. This means a subroutine can modify the caller’s variable directly - which is one common way PL/I procedures hand results back without using RETURNS.
Create a file named byref.pli:
BYREF: PROCEDURE OPTIONS(MAIN);
DECLARE COUNTER FIXED BINARY(31);
COUNTER = 10;
PUT SKIP EDIT('Before: COUNTER = ', COUNTER) (A, F(2));
CALL DOUBLE_IT(COUNTER); /* COUNTER is passed by reference */
PUT SKIP EDIT('After: COUNTER = ', COUNTER) (A, F(2));
/* The parameter N aliases the caller's COUNTER, so this
assignment is visible after the call returns */
DOUBLE_IT: PROCEDURE(N);
DECLARE N FIXED BINARY(31);
N = N * 2;
END DOUBLE_IT;
END BYREF;
Because COUNTER and the parameter N share the attribute FIXED BINARY(31), PL/I binds them to the same storage. The assignment N = N * 2 inside DOUBLE_IT therefore changes COUNTER in the caller. If the attributes did not match, PL/I would silently create a temporary “dummy” copy, and the change would be lost - a classic source of bugs, so keep argument and parameter types aligned.
Nested Procedures and Scope
PL/I has true block scope. A procedure nested inside another can see the enclosing procedure’s variables, but variables it declares with DECLARE are local to itself. The following program shows both: SHOW_SHARED reads BASE from the outer block, while SHOW_LOCAL declares its own BASE that shadows the outer one.
Create a file named scope.pli:
NESTING: PROCEDURE OPTIONS(MAIN);
DECLARE BASE FIXED BINARY(31);
BASE = 100;
CALL SHOW_LOCAL;
CALL SHOW_SHARED;
/* Declares its own BASE - the outer BASE is shadowed here */
SHOW_LOCAL: PROCEDURE;
DECLARE BASE FIXED BINARY(31);
BASE = 7;
PUT SKIP EDIT('Inside SHOW_LOCAL, BASE = ', BASE) (A, F(1));
END SHOW_LOCAL;
/* No local BASE - refers to the outer block's BASE */
SHOW_SHARED: PROCEDURE;
PUT SKIP EDIT('Inside SHOW_SHARED, BASE = ', BASE) (A, F(3));
END SHOW_SHARED;
END NESTING;
SHOW_LOCAL runs first and prints 7, the value of its own private BASE; the outer BASE is untouched. SHOW_SHARED then prints 100, the outer BASE, because it never declared one of its own. This lexical scoping - inner blocks seeing outer names unless they redeclare them - was one of PL/I’s structured-programming advances over FORTRAN and influenced later languages like Ada and C.
Recursion
A PL/I procedure that calls itself must carry the RECURSIVE attribute on its PROCEDURE statement; without it, the compiler assumes a single activation and the call would corrupt state. The classic example is factorial, where each invocation multiplies its argument by the factorial of the next-smaller number until it reaches the base case.
Create a file named factorial.pli:
FACT: PROCEDURE OPTIONS(MAIN);
DECLARE N FIXED BINARY(31);
DO N = 1 TO 6;
PUT SKIP EDIT(N, '! = ', FACTORIAL(N)) (F(1), A, F(3));
END;
/* RECURSIVE lets the function call itself safely */
FACTORIAL: PROCEDURE(N) RETURNS(FIXED BINARY(31)) RECURSIVE;
DECLARE N FIXED BINARY(31);
IF N <= 1 THEN
RETURN(1);
ELSE
RETURN(N * FACTORIAL(N - 1));
END FACTORIAL;
END FACT;
The parameter N inside FACTORIAL is local to each activation, so every recursive level keeps its own copy while the call beneath it runs. The base case N <= 1 returns 1 and stops the recursion; otherwise the function returns N * FACTORIAL(N - 1). The loop in the main procedure calls the function six times, building the familiar factorial sequence.
Returning Strings
Functions in PL/I are not limited to numbers - a RETURNS clause can name any data type, including a varying-length character string. This function assembles a greeting using the concatenation operator || and returns it for the caller to print.
Create a file named greeting.pli:
STRINGS: PROCEDURE OPTIONS(MAIN);
DECLARE MESSAGE CHARACTER(40) VARYING;
MESSAGE = MAKE_GREETING('Ada');
PUT SKIP LIST(MESSAGE);
MESSAGE = MAKE_GREETING('Grace');
PUT SKIP LIST(MESSAGE);
/* A function that returns a varying character string */
MAKE_GREETING: PROCEDURE(WHO) RETURNS(CHARACTER(40) VARYING);
DECLARE WHO CHARACTER(20) VARYING;
RETURN('Hello, ' || WHO || '!');
END MAKE_GREETING;
END STRINGS;
The RETURNS(CHARACTER(40) VARYING) clause declares the result type just as FIXED BINARY did for the numeric functions. Inside the body, || concatenates the literal text with the parameter, and RETURN hands the finished string back. Because the result is a VARYING string, only the characters actually used are stored, and PUT LIST writes the value with no surrounding quotes.
Running with Docker
Compile and run each example with the Iron Spring PL/I compiler inside the project’s image. The plicc wrapper compiles and links in one step, producing an executable named after the source file. The --security-opt seccomp=unconfined flag is required for the 32-bit compiler on Docker Desktop.
| |
Expected Output
functions.pli:
Welcome to PL/I procedures!
Area of 5 x 3 rectangle: 15
byref.pli:
Before: COUNTER = 10
After: COUNTER = 20
scope.pli:
Inside SHOW_LOCAL, BASE = 7
Inside SHOW_SHARED, BASE = 100
factorial.pli:
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
greeting.pli:
Hello, Ada!
Hello, Grace!
Key Concepts
- Everything is a
PROCEDURE- PL/I has no separate function keyword. A procedure with noRETURNSis a subroutine invoked withCALL; a procedure withRETURNS(type)is a function used inside an expression. RETURNhands back a value - A function declares its result type withRETURNS(...)and produces the value with aRETURN(expression)statement; a subroutine simply runs to itsEND.- Arguments pass by reference - When argument and parameter attributes match, they share storage, so a subroutine can modify the caller’s variable. Mismatched types create a hidden dummy copy, silently dropping any changes.
- Parameters need explicit attributes - PL/I is statically and strongly typed, so each parameter is declared inside the procedure with its data type, e.g.
DECLARE (WIDTH, HEIGHT) FIXED BINARY(31). - Nested procedures have block scope - An inner procedure sees the enclosing block’s variables but can declare its own, which shadow the outer ones - true lexical scoping.
- Recursion is opt-in - A self-calling procedure must carry the
RECURSIVEattribute; each activation gets its own copy of the parameters. - Functions can return any type - Beyond numbers,
RETURNS(CHARACTER(n) VARYING)lets a function build and return strings, using||for concatenation. PUT EDITgives precise output - Edit-directed format items likeF(3)(fixed-width number) andA(character) control column layout, where list-directedPUT LISTleaves spacing to the compiler.
Running Today
All examples can be run using Docker:
docker pull codearchaeology/pli:latest
Comments
Loading comments...
Leave a Comment