Intermediate

Functions in COBOL

Learn how COBOL organizes reusable logic with PERFORM paragraphs, CALL subprograms, parameters via the LINKAGE SECTION, scope, and recursion - all with Docker-ready GnuCOBOL examples

Most languages have a single construct called a “function.” COBOL, true to its procedural, business-oriented design, offers two distinct ways to package reusable logic, and neither is called a function in the traditional sense.

Inside a single program, you organize code into paragraphs (and sections) and execute them with the PERFORM verb. To share logic across programs - the COBOL equivalent of a library call - you write a separate subprogram and invoke it with CALL, passing data through the LINKAGE SECTION. COBOL 2002 added intrinsic functions and recursion to the mix as well.

This tutorial walks through both mechanisms: PERFORM for in-program structure and CALL for true subprograms with parameters and return values. Because COBOL has no block-level scope, we’ll also see how data sharing actually works, and finish with a recursive subprogram that computes a factorial. Throughout, we’ll use modern free-format syntax with GnuCOBOL.

Paragraphs as Functions

The most common unit of reuse in COBOL is the paragraph: a named block of statements in the PROCEDURE DIVISION. You invoke it with PERFORM, which is COBOL’s “call this routine and come back” verb. PERFORM also has built-in looping forms - PERFORM n TIMES and PERFORM ... VARYING - that fold iteration into the call itself.

Create a file named functions.cob:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
IDENTIFICATION DIVISION.
PROGRAM-ID. FUNCTIONS.

DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-I    PIC 9(2) VALUE 0.

PROCEDURE DIVISION.
MAIN-PARAGRAPH.
    DISPLAY "Calling paragraphs like functions:".
    PERFORM SHOW-BANNER.
    PERFORM GREET 3 TIMES.
    PERFORM COUNT-LINE VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 5.
    PERFORM SHOW-BANNER.
    STOP RUN.

SHOW-BANNER.
    DISPLAY "==============================".

GREET.
    DISPLAY "Paragraphs make code reusable.".

COUNT-LINE.
    DISPLAY "Line number " WS-I.

PERFORM SHOW-BANNER runs that paragraph once and returns. PERFORM GREET 3 TIMES repeats the paragraph three times. PERFORM COUNT-LINE VARYING ... runs the paragraph in a loop, incrementing WS-I until the condition is met. Note that STOP RUN sits at the end of MAIN-PARAGRAPH so the program halts before “falling through” into the paragraphs below - those are only reached when explicitly performed.

Sharing Data and Scope

COBOL has no local variables. Every item declared in WORKING-STORAGE SECTION is global to the entire program, and every paragraph can read and write all of it. A paragraph “receives arguments” simply by reading shared fields and “returns a value” by writing to one. This is very different from languages with parameter lists and local scope - it makes COBOL paragraphs simple, but it also means you must be deliberate about which fields each paragraph touches.

Create a file named average.cob:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
IDENTIFICATION DIVISION.
PROGRAM-ID. AVERAGE.

DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-NUM-1     PIC 9(3) VALUE 10.
01 WS-NUM-2     PIC 9(3) VALUE 20.
01 WS-NUM-3     PIC 9(3) VALUE 60.
01 WS-TOTAL     PIC 9(4) VALUE 0.
01 WS-AVERAGE   PIC 9(3) VALUE 0.

PROCEDURE DIVISION.
MAIN-PARAGRAPH.
    PERFORM SUM-VALUES.
    PERFORM COMPUTE-AVERAGE.
    DISPLAY "Total:   " WS-TOTAL.
    DISPLAY "Average: " WS-AVERAGE.
    STOP RUN.

SUM-VALUES.
    COMPUTE WS-TOTAL = WS-NUM-1 + WS-NUM-2 + WS-NUM-3.

COMPUTE-AVERAGE.
    DIVIDE WS-TOTAL BY 3 GIVING WS-AVERAGE.

SUM-VALUES writes its result into the shared WS-TOTAL; COMPUTE-AVERAGE then reads that same field. There are no parameters - the data flows through global WORKING-STORAGE. Notice the output: a PIC 9(4) field always displays its full width with leading zeros, so 90 prints as 0090. To suppress those zeros you would use an edited picture like PIC ZZZ9 (where Z blanks leading zeros).

Subprograms with CALL

For logic you want to reuse across multiple programs, COBOL uses a separate subprogram: its own compilation unit with its own IDENTIFICATION DIVISION. The caller uses CALL "NAME" USING ... to pass arguments, and the subprogram declares matching fields in its LINKAGE SECTION. Arguments are passed by reference by default, so the subprogram can modify the caller’s data - which is exactly how a subprogram “returns” a result. GOBACK returns control to the caller.

Create a file named square.cob:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
IDENTIFICATION DIVISION.
PROGRAM-ID. SQUARE.

DATA DIVISION.
LINKAGE SECTION.
01 LK-INPUT     PIC 9(2).
01 LK-RESULT    PIC 9(3).

PROCEDURE DIVISION USING LK-INPUT LK-RESULT.
    COMPUTE LK-RESULT = LK-INPUT * LK-INPUT.
    GOBACK.

Create a file named mainsq.cob:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
IDENTIFICATION DIVISION.
PROGRAM-ID. MAINSQ.

DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-VALUE     PIC 9(2) VALUE 12.
01 WS-SQUARE    PIC 9(3) VALUE 0.

PROCEDURE DIVISION.
    DISPLAY "Calling the SQUARE subprogram...".
    CALL "SQUARE" USING WS-VALUE WS-SQUARE.
    DISPLAY WS-VALUE " squared is " WS-SQUARE.
    STOP RUN.

The USING clause on the CALL and the USING clause on the subprogram’s PROCEDURE DIVISION line up by position: WS-VALUE maps to LK-INPUT, and WS-SQUARE maps to LK-RESULT. The two fields in each pair must have compatible pictures. Because LK-RESULT is passed by reference, writing to it inside SQUARE updates WS-SQUARE back in the caller.

Recursion

COBOL programs are not recursive by default - calling a program that is already active is an error. To allow it, mark the subprogram RECURSIVE in its PROGRAM-ID. Each recursive invocation also needs its own copy of any temporary data, so we place that field in the LOCAL-STORAGE SECTION, which is allocated fresh on every call (unlike WORKING-STORAGE, which is shared across invocations).

Create a file named recurse.cob:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
IDENTIFICATION DIVISION.
PROGRAM-ID. RECURSE.

DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-N        PIC 9 VALUE 5.
01 WS-RESULT   PIC 9(3) VALUE 0.

PROCEDURE DIVISION.
    CALL "FACT" USING WS-N WS-RESULT.
    DISPLAY WS-N "! = " WS-RESULT.
    STOP RUN.

END PROGRAM RECURSE.

IDENTIFICATION DIVISION.
PROGRAM-ID. FACT IS RECURSIVE.

DATA DIVISION.
LOCAL-STORAGE SECTION.
01 LS-PREV     PIC 9.

LINKAGE SECTION.
01 LK-N        PIC 9.
01 LK-RESULT   PIC 9(3).

PROCEDURE DIVISION USING LK-N LK-RESULT.
    IF LK-N <= 1
        MOVE 1 TO LK-RESULT
    ELSE
        SUBTRACT 1 FROM LK-N GIVING LS-PREV
        CALL "FACT" USING LS-PREV LK-RESULT
        MULTIPLY LK-N BY LK-RESULT
    END-IF
    GOBACK.

END PROGRAM FACT.

Both programs live in one source file, separated by END PROGRAM. The main program RECURSE calls FACT, which calls itself with LK-N - 1 until it reaches the base case (LK-N <= 1). Because LS-PREV lives in LOCAL-STORAGE, each level of the recursion keeps its own value while the call below it runs - placing it in WORKING-STORAGE instead would corrupt the computation, since every invocation would share the same memory.

Running with Docker

Compile and run each example with GnuCOBOL inside the official container. The -x flag builds an executable and -free enables free-format source.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official image
docker pull esolang/cobol:latest

# Example 1: paragraphs with PERFORM
docker run --rm -v $(pwd):/app -w /app esolang/cobol sh -c 'cobc -x -free functions.cob && ./functions'

# Example 2: sharing data through WORKING-STORAGE
docker run --rm -v $(pwd):/app -w /app esolang/cobol sh -c 'cobc -x -free average.cob && ./average'

# Example 3: a subprogram called with CALL (compile both files together)
docker run --rm -v $(pwd):/app -w /app esolang/cobol sh -c 'cobc -x -free mainsq.cob square.cob && ./mainsq'

# Example 4: a recursive subprogram
docker run --rm -v $(pwd):/app -w /app esolang/cobol sh -c 'cobc -x -free recurse.cob && ./recurse'

For the subprogram example, both mainsq.cob and square.cob are passed to cobc in one command; the first file becomes the entry point and produces the mainsq executable.

Expected Output

functions.cob:

Calling paragraphs like functions:
==============================
Paragraphs make code reusable.
Paragraphs make code reusable.
Paragraphs make code reusable.
Line number 01
Line number 02
Line number 03
Line number 04
Line number 05
==============================

average.cob:

Total:   0090
Average: 030

mainsq.cob:

Calling the SQUARE subprogram...
12 squared is 144

recurse.cob:

5! = 120

Key Concepts

  • PERFORM runs paragraphs - A paragraph is COBOL’s in-program routine; PERFORM calls it and returns. PERFORM n TIMES and PERFORM ... VARYING add looping to the call.
  • No local scope - All WORKING-STORAGE data is global. Paragraphs “pass arguments” by reading shared fields and “return values” by writing them.
  • CALL invokes subprograms - A subprogram is a separate program unit. CALL "NAME" USING ... passes arguments to fields declared in the callee’s LINKAGE SECTION.
  • Arguments are passed by reference - The default BY REFERENCE means a subprogram can modify the caller’s data, which is how results are returned. Use BY CONTENT to pass a copy instead.
  • GOBACK vs STOP RUN - GOBACK returns from a subprogram to its caller; STOP RUN halts the entire run unit. Use GOBACK in subprograms.
  • Recursion is opt-in - Mark a program RECURSIVE to let it call itself, and put per-call temporaries in LOCAL-STORAGE SECTION so each invocation gets its own copy.
  • PIC controls display width - Numeric PIC 9 fields show leading zeros (0090); edited pictures with Z blank them for cleaner reports.
  • COPY for shared definitions - Beyond PERFORM and CALL, COBOL reuses data and code layouts across programs by pulling in copybooks with the COPY statement.

Running Today

All examples can be run using Docker:

docker pull esolang/cobol:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining