35 min read

> "Divide each difficulty into as many parts as is feasible and necessary to resolve it."

Chapter 7: Procedures and Functions — Structuring Your Code

"Divide each difficulty into as many parts as is feasible and necessary to resolve it." — René Descartes, Discourse on Method (1637)


:mirror: Spaced Review — Before You Begin

Before diving into new material, take sixty seconds to test your recall of earlier concepts:

  1. From Chapter 5: When should you use a CASE statement instead of nested IF statements? What types can a CASE selector be?
  2. From Chapter 3: What types can a FOR loop control variable be? Can a FOR loop variable be of type Real?

Try to answer from memory before checking. If you struggled, revisit the relevant sections — these concepts will surface again in this chapter's exercises.


7.1 Why Procedures and Functions?

Rosa Martinelli stares at the PennyWise program she has been building across the last four chapters. It works. The menu displays, the loop repeats, the IF statements validate input, and the totals appear at the end. But the program is now 180 lines long, and all 180 of those lines live inside a single begin...end block. When she wants to change how expenses are displayed, she has to scroll through input validation code, menu handling code, and calculation code to find the right spot. When a bug appears in the total calculation, she has to mentally trace through the entire program to isolate it.

Tomás Vieira has the same problem. His version of PennyWise is slightly shorter — 140 lines — but he has already copied and pasted the "display all expenses" code in three different places. When he fixed a formatting bug in one copy, he forgot to fix the other two. His program shows different formatting depending on which menu option triggered the display.

Both Rosa and Tomás have hit the wall that every programmer hits: the monolithic program problem. A program written as a single, continuous sequence of statements becomes increasingly difficult to understand, modify, and debug as it grows. The solution to this problem is one of the most important ideas in all of computer science: procedural decomposition — breaking a program into named, self-contained pieces that each handle one specific task.

Pascal calls these pieces procedures and functions, and they are the subject of this chapter.

Three Reasons to Decompose

:bulb: Key Insight — The Three Pillars of Decomposition

Procedures and functions serve three fundamental purposes:

  1. Abstraction. A procedure hides how something is done behind a name that describes what is done. When you call DisplaySummary, you do not need to remember which WriteLn calls produce the formatted output. The name tells you the intent; the details are tucked away inside.

  2. Reuse (DRY — Don't Repeat Yourself). If you need the same logic in three places, write it once as a procedure and call it three times. Fix a bug once, and all three callers benefit. Copy-paste is the enemy of maintainable code.

  3. Readability. A well-decomposed program reads like an outline. The main body says ShowMenu; GetChoice; ProcessChoice — and a human reader immediately understands the program's structure without wading through implementation details.

Niklaus Wirth understood this when he designed Pascal. He did not invent procedures — they existed in FORTRAN and ALGOL before him — but he made them a central, elegant, first-class feature of the language. In Pascal, procedures and functions are not afterthoughts bolted onto a scripting language. They are the primary tool for organizing thought.

A Fourth Reason: Testability

There is a fourth reason for decomposition that becomes increasingly important as programs grow: testability. When your calculation logic is tangled with your input/output code, the only way to test the calculation is to run the entire program and type in test data by hand. When the calculation lives in a separate function, you can write a small test program that calls the function with known inputs and checks the outputs. This is the foundation of automated testing — a topic that will grow in importance throughout this book.

Consider Tomás's situation: his total calculation is buried inside a 140-line begin...end block. To check whether the total is correct, he has to run the program, add several expenses by hand, and compare the displayed total with his mental arithmetic. If he extracted the calculation into a function CalculateTotal, he could write:

{ Quick test }
Expenses[1] := 10.00;
Expenses[2] := 20.00;
Expenses[3] := 30.00;
Total := CalculateTotal(Expenses, 3);
WriteLn('Total: ', Total:0:2, ' (expected: 60.00)');

Thirty seconds of typing versus two minutes of manual data entry, and the test can be run again instantly after any change. We will not build a formal testing framework in this chapter, but the mindset starts here.

:scroll: Historical Note

Wirth's 1971 paper "Program Development by Stepwise Refinement" formalized the idea that programs should be designed top-down: start with a high-level description of the solution, then refine each step into smaller sub-steps, each of which becomes a procedure. This paper influenced a generation of software engineers and remains relevant today. The approach we practice in Section 7.9 traces directly back to Wirth's insight.


7.2 Writing Your First Procedure

A procedure in Pascal is a named block of code that performs a specific task. You declare it before the main begin...end block, and you call it by name from wherever you need that task performed.

Here is the simplest possible procedure:

program FirstProcedure;

procedure SayHello;
begin
  WriteLn('Hello from inside a procedure!');
end;

begin
  WriteLn('About to call the procedure...');
  SayHello;    { <<< Calling the procedure }
  WriteLn('Back in the main program.');
  SayHello;    { <<< Calling it again — reuse! }
end.

Output:

About to call the procedure...
Hello from inside a procedure!
Back in the main program.
Hello from inside a procedure!

Let us examine the anatomy:

  1. The keyword procedure tells the compiler we are defining a procedure, not a variable or constant.
  2. The name SayHello follows Pascal's identifier rules: it starts with a letter, contains only letters, digits, and underscores, and is not a reserved word.
  3. The semicolon after the name is required. This is part of Pascal's declaration syntax — the semicolon terminates the procedure heading.
  4. The begin...end; block contains the procedure's body — the statements that execute when the procedure is called.
  5. Note the semicolon after end, not a period. Only the final end of the entire program gets a period. Every other end gets a semicolon.

:warning: Warning — The Semicolon Trap

Beginners frequently write end. instead of end; at the close of a procedure. The compiler will interpret the period as the end of the entire program, and everything after it will be ignored or produce baffling errors. Remember: only the program's final end gets a period. Every procedure's end gets a semicolon.

Where Procedures Must Be Declared

In standard Pascal and Free Pascal's default mode, procedures must be declared before the main begin...end block and before any code that calls them. This is Pascal's "declare before use" principle at work — the same principle you encountered with variables in Chapter 3.

program DeclarationOrder;

{ Procedures are declared here, BEFORE the main begin }

procedure StepOne;
begin
  WriteLn('Step one complete.');
end;

procedure StepTwo;
begin
  WriteLn('Step two complete.');
  StepOne;  { StepTwo can call StepOne because StepOne was declared first }
end;

begin
  StepOne;
  StepTwo;
end.

:door: Coming From Python

In Python, you define functions with def and can call them in any order as long as the definition has been executed before the call at runtime. Pascal is stricter: the declaration must appear textually before the call in the source file. This catches errors at compile time rather than runtime — a feature, not a bug.

:door: Coming From C/C++

Pascal's procedure declarations are analogous to defining functions in C, but without a separate header file. Pascal's forward declaration (Section 7.8) serves a similar purpose to C's function prototypes.

What Happens When You Call a Procedure

When the program encounters a procedure call like SayHello;, execution jumps to the first statement inside the procedure's begin...end block. When the procedure's end; is reached, execution returns to the statement immediately after the call. You can think of a procedure call as temporarily inserting the procedure's body into the calling code — though the compiler handles it much more efficiently than literal insertion (Chapter 8 explains the mechanism in detail).

Here is a more practical example — a procedure that prints a formatted separator of any character:

program SeparatorDemo;

procedure PrintSeparator;
begin
  WriteLn('----------------------------------------');
end;

procedure PrintHeader;
begin
  PrintSeparator;
  WriteLn('       EXPENSE REPORT');
  PrintSeparator;
end;

procedure PrintFooter;
begin
  PrintSeparator;
  WriteLn('       END OF REPORT');
  PrintSeparator;
end;

begin
  PrintHeader;
  WriteLn('  Groceries:     $47.50');
  WriteLn('  Transport:     $30.00');
  WriteLn('  Coffee:        $12.75');
  PrintFooter;
end.

Output:

----------------------------------------
       EXPENSE REPORT
----------------------------------------
  Groceries:     $47.50
  Transport:     $30.00
  Coffee:        $12.75
----------------------------------------
       END OF REPORT
----------------------------------------

Notice that PrintSeparator is called four times — twice from PrintHeader, twice from PrintFooter — but it is defined once. If Rosa decides to change the separator from dashes to equals signs, she changes one line and all four separators update. This is theme T6 (simplicity is strength) in action: a small, focused procedure that does one thing perfectly.

:test_tube: Experiment

Type in the SeparatorDemo program above. Then make these modifications and predict the output before compiling: 1. Change PrintSeparator to use = instead of -. 2. Add a new procedure PrintBlankLine that prints an empty line, and call it between PrintHeader and the first expense. 3. What happens if you try to call PrintFooter from inside PrintSeparator? Why?


7.3 Parameters: Passing Data to Procedures

A procedure that always does exactly the same thing is of limited use. Most procedures need data to work with — and the mechanism for providing that data is parameters.

Parameters are variables listed in the procedure heading that receive values when the procedure is called. The values provided by the caller are called arguments.

program ParameterDemo;

procedure Greet(Name: string; Age: Integer);
begin
  WriteLn('Hello, ', Name, '! You are ', Age, ' years old.');
end;

begin
  Greet('Rosa', 32);       { Name gets 'Rosa', Age gets 32 }
  Greet('Tomás', 20);      { Name gets 'Tomás', Age gets 20 }
end.

Output:

Hello, Rosa! You are 32 years old.
Hello, Tomás! You are 20 years old.

Value Parameters: The Default

When you declare parameters as shown above — just a name and a type — they are value parameters. This means the procedure receives a copy of the argument. Whatever the procedure does with that copy has no effect on the original variable in the calling code.

This is crucial to understand, so let us see it in action:

program ValueParameterDemo;

procedure DoubleIt(X: Integer);
begin
  X := X * 2;
  WriteLn('Inside DoubleIt: X = ', X);
end;

var
  Number: Integer;
begin
  Number := 5;
  WriteLn('Before call: Number = ', Number);
  DoubleIt(Number);
  WriteLn('After call: Number = ', Number);
end.

Output:

Before call: Number = 5
Inside DoubleIt: X = 10
After call: Number = 5

:bulb: Key Insight — Value Parameters Are Safe

Notice that Number is still 5 after the call. The procedure received a copy of the value 5, doubled the copy to 10, and when the procedure ended, the copy was discarded. The original Number was never touched. Value parameters protect the caller's data. This is the default for a reason — it prevents procedures from having unexpected side effects on your variables.

:triangular_ruler: Diagram — Value Parameter Memory Model

Before call:     Number: [  5  ]
                          |
                    copy ─┘
                          ▼
During call:     X:      [ 5 → 10 ]    (local copy, modified)
                 Number: [  5  ]        (untouched)

After call:      Number: [  5  ]        (still 5)
                 X:      (gone — destroyed when procedure ended)

Parameter Syntax Details

Multiple parameters of the same type can share a type declaration:

procedure DrawRectangle(X, Y, Width, Height: Integer);

Parameters of different types are separated by semicolons:

procedure LogExpense(Category: string; Amount: Real; IsRecurring: Boolean);

:warning: Warning — Commas vs. Semicolons in Parameter Lists

Parameters of the same type are separated by commas: X, Y: Integer. Parameters of different types are separated by semicolons: X: Integer; Name: string. Mixing this up produces compiler errors. Think of the semicolon as separating groups of parameters.


7.4 Variable Parameters: Pass by Reference

Sometimes a procedure needs to modify a variable in the calling code. For example, a procedure that reads validated input from the user needs to store the result somewhere the caller can access it. Value parameters cannot do this — they are copies, and changes to copies are discarded.

Pascal's solution is the variable parameter, declared with the keyword var:

program VarParameterDemo;

procedure DoubleIt(var X: Integer);    { <<< Note the 'var' keyword }
begin
  X := X * 2;
  WriteLn('Inside DoubleIt: X = ', X);
end;

var
  Number: Integer;
begin
  Number := 5;
  WriteLn('Before call: Number = ', Number);
  DoubleIt(Number);
  WriteLn('After call: Number = ', Number);
end.

Output:

Before call: Number = 5
Inside DoubleIt: X = 10
After call: Number = 10

Now Number is 10 after the call. The var keyword told the compiler: "Do not copy the value. Instead, give the procedure direct access to the original variable." Any change the procedure makes to X is actually a change to Number.

:triangular_ruler: Diagram — Variable Parameter Memory Model

Before call:     Number: [  5  ]
                          ▲
                    alias ┘
                          │
During call:     X ──────►Number: [ 5 → 10 ]    (same memory location)

After call:      Number: [ 10 ]
                 X:      (alias gone — but Number retains the change)

:bulb: Key Insight — var Means "I Will Modify This"

The var keyword is a contract. It tells the reader of your code: "This procedure will (or may) modify this argument." When you see var in a parameter list, you know the procedure has side effects. When you do not see var, you know the argument is safe. Pascal makes this distinction visible in the source code — unlike Python, where you cannot tell from a function signature whether it will modify a mutable argument.

When to Use var Parameters

Use var parameters when: - The procedure needs to return a value through a parameter (e.g., reading input from the user) - The procedure needs to modify a data structure passed to it - You are working with large data structures and want to avoid the cost of copying (though const parameters, covered next, are often a better choice for this case)

procedure ReadValidatedAmount(var Amount: Real);
var
  InputOK: Boolean;
begin
  InputOK := False;
  repeat
    Write('Enter amount (positive number): ');
    {$I-}
    ReadLn(Amount);
    {$I+}
    if (IOResult = 0) and (Amount > 0) then
      InputOK := True
    else
      WriteLn('Invalid input. Please try again.');
  until InputOK;
end;

Here, Amount must be a var parameter because the whole point of the procedure is to fill in the caller's variable with the validated input.

:warning: Warning — var Parameters Require Variables, Not Expressions

Because var parameters are aliases for the caller's variables, you must pass an actual variable — not a literal or expression. DoubleIt(5) will not compile when X is a var parameter. The compiler needs a memory location to alias, and the literal 5 does not have one.

Mixing Value and var Parameters

A single procedure can have both value and var parameters:

procedure Swap(var A, B: Integer);
var
  Temp: Integer;
begin
  Temp := A;
  A := B;
  B := Temp;
end;

procedure ClampToRange(var Value: Integer; Min, Max: Integer);
begin
  if Value < Min then
    Value := Min
  else if Value > Max then
    Value := Max;
end;

In ClampToRange, Value is a var parameter because the procedure may modify it. Min and Max are value parameters because they are read-only inputs — the procedure needs their values but will not change them.


7.5 Constant Parameters

Free Pascal (and Delphi) offer a third parameter-passing mode: constant parameters, declared with the keyword const:

procedure PrintLabel(const Label: string; const Width: Integer);
begin
  WriteLn(Label:Width);
  { Label := 'something else';  ← This would NOT compile! }
end;

A const parameter is a promise: "I will read this value but never modify it." The compiler enforces this promise — any attempt to assign to a const parameter produces a compile-time error.

Why Use const?

The const keyword serves two purposes:

  1. Documentation and safety. It tells anyone reading your code that this parameter is input-only. The compiler will catch accidental modifications, turning potential runtime bugs into compile-time errors.

  2. Efficiency. For large data types (strings, arrays, records), the compiler may pass a const parameter by reference internally — avoiding the cost of copying — while still preventing modification. You get the safety of a value parameter with the efficiency of a var parameter.

:white_check_mark: Best Practice — Default to const for Read-Only Parameters

If a procedure or function reads a parameter but does not modify it, declare it const. This is especially important for string parameters, which can be expensive to copy. Many experienced Pascal programmers follow this rule: const is the default; var is the exception; bare (value) parameters are for small, simple types where copying is trivial.

{ Good — const for read-only, var for output }
procedure FormatExpense(const Category: string; const Amount: Real;
                        var FormattedLine: string);
begin
  FormattedLine := Category + ': $' + FloatToStrF(Amount, ffFixed, 10, 2);
end;

Summary of Parameter Modes

Mode Keyword Copy or Alias? Can Modify? Use When...
Value (none) Copy Yes (copy only) Small types, modification needed locally
Variable var Alias Yes (original) Procedure must change the caller's data
Constant const Compiler's choice No Read-only; strings, records, large types

:test_tube: Experiment

Write a short program with a procedure that takes three parameters: one value, one var, and one const. Inside the procedure, try to assign a new value to the const parameter. What error does the compiler produce? Now try passing a literal (like 42) to the var parameter. What error do you get? These experiments build intuition faster than reading.

Choosing the Right Parameter Mode: A Decision Flowchart

When you are writing a new procedure and need to decide which parameter mode to use, ask yourself these questions in order:

  1. Does the procedure need to modify the caller's variable? - Yes → Use var. - No → Continue to question 2.

  2. Is the parameter a large type (string, array, record) where copying would be wasteful? - Yes → Use const. - No → Continue to question 3.

  3. Does the procedure need to modify a local copy of the value for internal computation? - Yes → Use a value parameter (no keyword) — the copy is your working copy. - No → Use const for clarity and safety, even for small types.

This flowchart leads to the same conclusion experienced Pascal programmers reach by instinct: const is the default, var is for outputs, and bare value parameters are for small types you need to modify locally.


7.6 Functions: Procedures That Return Values

A function is a procedure that computes and returns a single value. While a procedure does something, a function produces something. The distinction matters because a function call is an expression — it can appear anywhere a value of the appropriate type is expected.

Function Syntax

function FunctionName(Parameters): ReturnType;
begin
  { ... compute something ... }
  FunctionName := ComputedValue;    { Assign the return value }
end;

Or, using Free Pascal's Result variable (preferred in modern Pascal):

function FunctionName(Parameters): ReturnType;
begin
  { ... compute something ... }
  Result := ComputedValue;          { Assign the return value via Result }
end;

:bulb: Key Insight — Two Ways to Return a Value

Standard Pascal requires you to assign the return value to the function's own name: FunctionName := value. Free Pascal also supports the Result pseudo-variable, which is an alias for the return value. Both work. Result is generally preferred because it is clearer and avoids confusion with recursive calls (where using the function name can accidentally trigger recursion).

A Complete Example

program FunctionDemo;

function CircleArea(Radius: Real): Real;
begin
  Result := Pi * Radius * Radius;
end;

function Max(A, B: Integer): Integer;
begin
  if A > B then
    Result := A
  else
    Result := B;
end;

function IsPositive(Value: Real): Boolean;
begin
  Result := Value > 0;
end;

var
  R: Real;
begin
  R := 5.0;
  WriteLn('Area of circle with radius ', R:0:1, ' = ', CircleArea(R):0:4);
  WriteLn('Max of 17 and 42 is ', Max(17, 42));

  if IsPositive(-3.5) then
    WriteLn('Positive')
  else
    WriteLn('Not positive');
end.

Output:

Area of circle with radius 5.0 = 78.5398
Max of 17 and 42 is 42
Not positive

Notice how function calls are used as expressions: CircleArea(R) appears inside WriteLn, Max(17, 42) appears as an argument, and IsPositive(-3.5) appears as the condition of an IF statement. This is the power of functions — they compose naturally into larger expressions.

Functions Can Use All Parameter Modes

Functions can have value, var, and const parameters just like procedures. However, var parameters in functions are less common because the primary purpose of a function is to return a value, not to modify its arguments. If a function needs to modify something and return a value, consider whether two procedures (or a procedure with multiple var parameters) might be clearer.

:white_check_mark: Best Practice — Functions Should Be Side-Effect-Free

A well-designed function computes and returns a value without modifying anything else — no global variables, no var parameters, no I/O. Such functions are called pure functions, and they are dramatically easier to test, debug, and reason about. When you find yourself writing a function with side effects, pause and ask whether it should be a procedure instead.

Return Types

Functions can return any simple type: Integer, Real, Boolean, Char, string, or any user-defined type. In standard Pascal, functions could not return structured types (arrays, records), but Free Pascal lifts this restriction — you can return records, dynamic arrays, and even objects. We will explore this in later chapters.

The Exit Statement in Functions

Sometimes a function needs to return a value early, before reaching the end of its body. Pascal provides the Exit statement for this purpose. In Free Pascal, you can combine Exit with a value:

function Classify(Score: Integer): string;
begin
  if Score < 0 then
    Exit('Invalid');     { Return immediately with 'Invalid' }
  if Score >= 90 then
    Exit('Excellent');
  if Score >= 70 then
    Exit('Good');
  if Score >= 50 then
    Exit('Passing');
  Result := 'Failing';  { Only reached if none of the above matched }
end;

The Exit('value') syntax is a Free Pascal extension. In standard Pascal, you would assign to Result (or the function name) and then call Exit without an argument. Both approaches work in Free Pascal.

Procedures vs. Functions: When to Choose Which

The boundary between procedures and functions is sometimes blurry. Here is a guide:

Situation Use a... Why
Compute a single result from inputs Function The result is the purpose
Read input from the user Procedure (with var param) Side effect (I/O) is the purpose
Print formatted output Procedure Side effect (display) is the purpose
Modify a data structure Procedure (with var param) Side effect (mutation) is the purpose
Test a condition (Boolean result) Function returning Boolean The True/False result is the purpose
Both compute AND modify Consider splitting Single responsibility principle

When in doubt, ask: "Is the caller primarily interested in the value this produces, or the action it performs?" If the value, use a function. If the action, use a procedure.

:door: Coming From C/C++

Pascal's functions are like C functions with a return value. Pascal's procedures are like C void functions. The keyword distinction (procedure vs. function) makes the intent clear in the declaration, which is something C's uniform void approach does not.

:door: Coming From Java

Java uses void methods for procedures and typed methods for functions — the same concept, different syntax. Java's return keyword corresponds to Pascal's Result := assignment (or Exit(value) for early return). Unlike Java, Pascal does not allow you to omit the return value — the compiler will warn you if a function's Result is never assigned.


7.7 Local Variables and Nested Subprograms

Local Variables

Every procedure and function can declare its own variables in a var section between the heading and begin:

procedure PrintStars(Count: Integer);
var
  I: Integer;     { <<< Local variable — exists only inside PrintStars }
begin
  for I := 1 to Count do
    Write('*');
  WriteLn;
end;

The variable I exists only while PrintStars is executing. Once the procedure returns, I is destroyed. This is called local scope, and it is one of Pascal's most important features for writing maintainable code.

:bulb: Key Insight — Local Variables Prevent Contamination

If I were a global variable, every procedure that uses a loop counter called I would interfere with every other. By making I local, each procedure gets its own private copy. You can have ten procedures, each with a local variable called I, and they will never conflict. This is not a convenience — it is a necessity for writing programs larger than a few dozen lines.

Local variables can also include their own constants and even types:

function IsPrime(N: Integer): Boolean;
const
  SmallPrimes: array[1..4] of Integer = (2, 3, 5, 7);
var
  I: Integer;
  Divisor: Integer;
begin
  if N < 2 then
  begin
    Result := False;
    Exit;
  end;

  for I := 1 to 4 do
    if N = SmallPrimes[I] then
    begin
      Result := True;
      Exit;
    end;

  Divisor := 2;
  while Divisor * Divisor <= N do
  begin
    if N mod Divisor = 0 then
    begin
      Result := False;
      Exit;
    end;
    Inc(Divisor);
  end;
  Result := True;
end;

Nested Subprograms

Pascal allows you to declare procedures and functions inside other procedures and functions. The inner subprogram is called a nested subprogram, and it is visible only within the enclosing subprogram.

procedure GenerateReport(const Title: string; ItemCount: Integer);

  procedure PrintHeader;    { <<< Nested — only visible inside GenerateReport }
  begin
    WriteLn('=================================');
    WriteLn('  ', Title);   { Can access enclosing procedure's const parameter }
    WriteLn('=================================');
  end;

  procedure PrintFooter;
  begin
    WriteLn('---------------------------------');
    WriteLn('  Total items: ', ItemCount);
    WriteLn('---------------------------------');
  end;

begin  { GenerateReport body }
  PrintHeader;
  WriteLn('  ... report contents ...');
  PrintFooter;
end;

Nested procedures can access the parameters and local variables of their enclosing procedure. This is a powerful organizational tool: the helper procedures are hidden from the rest of the program, reducing clutter, and they naturally have access to the data they need.

:bar_chart: Technical Detail — Nested Scope Chain

When a nested procedure accesses a variable from its enclosing scope, the compiler generates code to follow the static link — a hidden pointer to the enclosing procedure's stack frame. This means nested procedures are slightly less efficient than top-level procedures due to the extra indirection. For most programs, this overhead is negligible. It matters only in performance-critical inner loops, and even then, profiling should guide your decisions (Chapter 26 covers profiling in depth).

:white_check_mark: Best Practice — Nesting Depth

Nested procedures are a useful organizational tool, but avoid nesting more than two levels deep. A procedure inside a procedure inside a procedure becomes difficult to read and signals that you should refactor into separate top-level procedures. One level of nesting is common and readable. Two levels are occasionally justified. Three levels are a code smell.

Global Variables vs. Local Variables: A Preview

At this point in the book, all of PennyWise's variables (the expense arrays, the count, etc.) are declared in the program's var section and passed to procedures through parameters. An alternative would be to declare them globally and let every procedure access them directly without parameters.

This temptation is strong — it seems simpler. No parameter lists, no worrying about var vs. const. But it is a trap:

  1. Any procedure can modify any global variable — and you cannot tell from the procedure's heading which variables it touches. The heading procedure DisplayExpenses; with no parameters gives you no information about what data it reads.

  2. Procedures with global dependencies cannot be reused in other programs. If CalculateTotal reads from a global array, you cannot use it in a different program with a different array name.

  3. Debugging becomes detective work. When a global variable has the wrong value, any procedure in the program might have changed it. With parameters, you know exactly which procedures have write access (the ones with var parameters).

We will treat global variables more thoroughly in Chapter 8. For now, the guideline is: pass data through parameters, not through global variables. The parameter list is a contract that documents exactly what data a procedure needs and whether it will modify that data.


7.8 Forward Declarations

Pascal's "declare before use" rule creates a problem when two procedures need to call each other. Consider:

{ This does NOT compile! }

procedure Ping(Count: Integer);
begin
  WriteLn('Ping ', Count);
  if Count > 0 then
    Pong(Count - 1);         { <<< Error: Pong is not yet declared! }
end;

procedure Pong(Count: Integer);
begin
  WriteLn('Pong ', Count);
  if Count > 0 then
    Ping(Count - 1);
end;

Ping calls Pong, but Pong is declared after Ping. Moving Pong before Ping just reverses the problem. The solution is a forward declaration: you declare the procedure's heading (name, parameters, return type) with the keyword forward, then provide the full body later.

program ForwardDemo;

procedure Pong(Count: Integer); forward;   { <<< Forward declaration }

procedure Ping(Count: Integer);
begin
  WriteLn('Ping ', Count);
  if Count > 0 then
    Pong(Count - 1);         { Now the compiler knows Pong exists }
end;

procedure Pong(Count: Integer);            { <<< Full definition (no 'forward') }
begin
  WriteLn('Pong ', Count);
  if Count > 0 then
    Ping(Count - 1);
end;

begin
  Ping(3);
end.

Output:

Ping 3
Pong 2
Ping 1
Pong 0

The forward declaration tells the compiler: "A procedure called Pong with this parameter list will be defined later in the file. Trust me." The compiler records the signature and allows calls to Pong before it sees the full body.

:warning: Warning — Forward Declaration Rules

  1. The forward declaration must include the complete parameter list and (for functions) the return type.
  2. When you write the full definition later, you must repeat the parameter list, and it must match the forward declaration exactly.
  3. In Free Pascal's default mode, the second (full) definition can omit the parameter list — the compiler uses the forward declaration's parameters. However, repeating them is recommended for clarity.

When Forward Declarations Are Needed

Forward declarations are relatively rare in everyday Pascal programming. You need them primarily in two situations:

  1. Mutual recursion — two procedures that call each other, as shown above. We will explore recursion in depth in Chapter 22.
  2. Organizational preference — sometimes you want to arrange your procedures in a specific order for readability, and forward declarations let you decouple declaration order from call order.

In Part III (Chapter 33), you will learn about units, which provide a much more powerful mechanism for organizing code across multiple files. Units have a dedicated interface section where procedure headings are declared, making forward declarations within a single file less necessary. But for now, forward declarations are the tool you have, and knowing them is part of being fluent in Pascal.

Forward Declaration Syntax Summary

For reference, here is the complete syntax:

{ Forward declaration — heading + 'forward' keyword }
procedure MyProcedure(X: Integer; var Y: Real); forward;
function  MyFunction(const S: string): Boolean; forward;

{ ... other code that can now call MyProcedure and MyFunction ... }

{ Full definitions — must match the forward declarations }
procedure MyProcedure(X: Integer; var Y: Real);
begin
  { body }
end;

function MyFunction(const S: string): Boolean;
begin
  { body }
end;

The forward declaration and the full definition must have identical parameter lists and return types. Any mismatch produces a compiler error.


7.9 Procedural Decomposition: The Art of Breaking Problems Down

We now have all the mechanical tools: procedures, functions, parameters of three kinds, local variables, nesting, and forward declarations. The remaining question is design: given a problem, how do you decide which procedures and functions to write?

The answer is procedural decomposition, also called top-down design or stepwise refinement. The approach is straightforward:

  1. Describe the overall solution in a few high-level steps (in English, not code).
  2. Turn each step into a procedure or function call in your main program.
  3. For each procedure, repeat the process: describe what it does in sub-steps, and turn those sub-steps into nested procedures or helper functions.
  4. Stop when each piece is simple enough to implement in a few lines of clear code.

Worked Example: Student Grade Report

Suppose we need a program that reads a student's name and five test scores, calculates the average, determines a letter grade, and prints a formatted report. Here is the decomposition:

Level 0 — The Program:

Read student information
Calculate results
Print report

Level 1 — Refine Each Step:

Read student information:
  - Read the student's name
  - Read five test scores

Calculate results:
  - Compute the average of the five scores
  - Determine the letter grade from the average

Print report:
  - Print a header with the student's name
  - Print each score
  - Print the average and letter grade

Level 2 — Write the Pascal:

program GradeReport;

const
  NumTests = 5;

type
  ScoreArray = array[1..NumTests] of Integer;

procedure ReadStudentName(var Name: string);
begin
  Write('Enter student name: ');
  ReadLn(Name);
end;

procedure ReadScores(var Scores: ScoreArray);
var
  I: Integer;
begin
  for I := 1 to NumTests do
  begin
    Write('Enter score ', I, ': ');
    ReadLn(Scores[I]);
  end;
end;

function CalculateAverage(const Scores: ScoreArray): Real;
var
  Sum, I: Integer;
begin
  Sum := 0;
  for I := 1 to NumTests do
    Sum := Sum + Scores[I];
  Result := Sum / NumTests;
end;

function DetermineGrade(Average: Real): Char;
begin
  case Round(Average) div 10 of
    10, 9: Result := 'A';
    8:     Result := 'B';
    7:     Result := 'C';
    6:     Result := 'D';
  else
    Result := 'F';
  end;
end;

procedure PrintReport(const Name: string; const Scores: ScoreArray;
                      Average: Real; Grade: Char);
var
  I: Integer;
begin
  WriteLn;
  WriteLn('=== Grade Report ===');
  WriteLn('Student: ', Name);
  WriteLn;
  for I := 1 to NumTests do
    WriteLn('  Test ', I, ': ', Scores[I]:4);
  WriteLn;
  WriteLn('  Average: ', Average:6:1);
  WriteLn('  Grade:   ', Grade);
  WriteLn('====================');
end;

var
  StudentName: string;
  Scores: ScoreArray;
  Average: Real;
  Grade: Char;
begin
  ReadStudentName(StudentName);
  ReadScores(Scores);
  Average := CalculateAverage(Scores);
  Grade := DetermineGrade(Average);
  PrintReport(StudentName, Scores, Average, Grade);
end.

:mirror: Reflection

Look at the main program body — the five lines between begin and end. Read them aloud. You understand the entire program at a glance: read the name, read the scores, calculate the average, determine the grade, print the report. This is the power of procedural decomposition. The main program reads like a table of contents; each procedure is a chapter that can be read (and tested) independently.

The Decomposition Process in Detail

Let us examine several design decisions in the grade report example to build your intuition for decomposition:

Why is ReadStudentName a separate procedure from ReadScores? They could be merged into ReadAllStudentData. But they serve different purposes: one reads a single string, the other reads five integers. If you later add validation (e.g., names must not be empty, scores must be 0–100), the validation logic for names is different from the validation logic for scores. Separating them makes each procedure simpler and easier to modify.

Why is DetermineGrade a function, not a procedure? Because its sole purpose is to compute a single character from a number. It has no side effects — no I/O, no variable modification. You can call it in an expression: WriteLn(DetermineGrade(88.5)). If it were a procedure with a var Grade: Char parameter, the calling code would be longer and less readable.

Why does CalculateAverage take the entire ScoreArray and not five separate parameters? With five separate parameters, the procedure heading would be function CalculateAverage(S1, S2, S3, S4, S5: Integer): Real — and if the number of tests changes, every call site must change. Using an array makes the function flexible. We have not formally covered arrays yet (that is Chapter 9), but this example previews why they matter.

Why is PrintReport one procedure instead of three (PrintHeader, PrintScores, PrintFooter)? It could go either way. For a simple report like this, one procedure is fine. If the report grew more complex — multiple sections, conditional formatting, page breaks — you would decompose it further. Decomposition is not about reaching a fixed number of procedures; it is about each piece being simple enough to understand at a glance.

Common Decomposition Mistakes

Beginners sometimes over-decompose (creating a procedure for every two lines of code) or under-decompose (putting 100 lines in a single procedure). Here are signs that your decomposition needs adjustment:

Signs of under-decomposition: - A procedure is longer than 30 lines - You cannot describe what a procedure does in one sentence - The same code pattern appears in multiple places - A procedure has more than 5–6 parameters

Signs of over-decomposition: - A procedure contains only one or two statements that will never be reused - You have to read 15 procedure definitions to understand a 20-line task - Procedure names are generic (DoStep1, DoStep2) because the pieces are too small to have meaningful names

How Big Should a Procedure Be?

There is no universal rule, but these guidelines serve well:

:white_check_mark: Best Practice — Procedure Size Guidelines

  • A procedure should do one thing. If you struggle to name it, it might be doing too much.
  • A procedure should fit on one screen (roughly 20–30 lines of code). If you need to scroll to see the whole thing, consider decomposing further.
  • The main program body should be short — ideally 5–15 lines of procedure and function calls. It is the outline of your solution, not the solution itself.
  • Name procedures with verbs (what they do): ReadInput, CalculateTotal, PrintReport, ValidateAmount.
  • Name functions with nouns or adjectives (what they produce): Average, Maximum, IsValid, FormattedDate.

7.10 Best Practices for Procedures and Functions

Let us consolidate the advice scattered through this chapter into a reference list. These practices are not arbitrary conventions — each one prevents a specific category of bugs or maintenance headaches.

:white_check_mark: Best Practices Summary

  1. Use const for read-only parameters. It documents intent and may improve performance. This is especially important for string, array, and record parameters.

  2. Use var only when necessary. Every var parameter is a potential side effect. If a procedure can accomplish its task without modifying the caller's variables, it should not use var.

  3. Prefer functions for computations. If a subprogram computes a single value, make it a function. If it performs an action (printing, reading input, modifying data), make it a procedure.

  4. Keep functions pure. A function should compute and return a value without side effects. Avoid WriteLn inside functions, avoid modifying global variables, avoid var parameters in functions.

  5. One task per subprogram. If you find yourself naming a procedure ReadAndValidateAndProcess, it is three procedures trying to be one.

  6. Name procedures with verbs, functions with nouns/adjectives. SortScores (procedure), AverageScore (function), IsValid (Boolean function).

  7. Limit nesting depth. One level of nested procedures is fine. Two is occasionally justified. More than two signals a need to refactor.

  8. Declare local variables. Every variable that a procedure needs for its internal work should be declared locally, not globally. Global variables should be rare — we will discuss this more in Chapter 8.

  9. Validate inputs early. If a procedure or function has preconditions (e.g., a count must be positive), check them at the top and handle the error case immediately. This prevents deeply nested logic and makes debugging easier.

  10. Write a comment at the top of each subprogram describing its purpose, parameters, and any preconditions. This takes thirty seconds and saves hours of future confusion.

A Procedure Documentation Template

Experienced Pascal programmers often use a consistent comment block above each subprogram. Here is a template:

{ ================================================================
  PROCEDURE: CalculateMonthlyBudget
  PURPOSE:   Computes remaining budget after subtracting expenses
             from the monthly income.
  PARAMETERS:
    Income    (const Real)  — Monthly income (must be >= 0)
    Expenses  (const array) — Array of expense amounts
    Count     (Integer)     — Number of expenses in the array
    Remaining (var Real)    — Receives the computed remaining budget
  PRECONDITIONS:
    Income >= 0, Count >= 0, Count <= MaxExpenses
  POSTCONDITIONS:
    Remaining = Income - sum of Expenses[1..Count]
  ================================================================ }

You do not need this level of detail for every helper procedure, but for any procedure that forms part of your program's public interface — the procedures called from the main body — this kind of documentation pays for itself many times over. When you return to the code in three months, you will be grateful.

The Connection to Theme T5: Algorithms + Data Structures = Programs

Procedures and functions are the mechanism through which Wirth's famous equation comes alive. An algorithm (the logic of how to solve a problem) is expressed as a series of procedure calls. A data structure (the organized information the program manipulates) is passed between procedures through parameters. The program is the composition of algorithms operating on data structures, orchestrated by the main body's sequence of calls.

This is why procedures are not merely a convenience feature — they are the structural foundation on which everything else in this book is built. Arrays (Chapter 9), records (Chapter 11), files (Chapter 13), objects (Chapter 16), and every data structure after them will be passed to and manipulated by procedures and functions. The habits you build in this chapter — clean parameter design, single responsibility, meaningful names — will compound across every remaining chapter.


7.11 Project Checkpoint: PennyWise Decomposed

:arrows_counterclockwise: PennyWise Update — Version 0.7: Procedural Decomposition

In Chapters 3 through 6, PennyWise grew from a simple variable declaration to a working menu-driven expense tracker. But it was a monolithic program — all logic in a single begin...end block. Now we decompose it into procedures and functions.

Here is how Rosa and Tomás redesign PennyWise. They identify five key tasks:

  1. ShowMenu — Display the menu options to the user
  2. AddExpense — Prompt for and store a new expense
  3. DisplayExpenses — Show all recorded expenses in a formatted table
  4. CalculateTotal — Compute the total of all expenses
  5. DisplaySummary — Show the total and expense count

The main program becomes a simple loop:

begin
  ExpenseCount := 0;
  repeat
    ShowMenu;
    ReadLn(Choice);
    case Choice of
      '1': AddExpense(Expenses, Categories, ExpenseCount);
      '2': DisplayExpenses(Expenses, Categories, ExpenseCount);
      '3': DisplaySummary(Expenses, ExpenseCount);
    end;
  until Choice = '4';
  WriteLn('Goodbye! You tracked ', ExpenseCount, ' expenses.');
end.

The full program (see code/project-checkpoint.pas) implements each of these procedures with appropriate parameter modes:

  • ShowMenu takes no parameters — it always displays the same menu
  • AddExpense takes var parameters for the expense and category arrays and the count, because it modifies all three
  • DisplayExpenses takes const parameters for the arrays and a value parameter for the count — it reads but does not modify
  • CalculateTotal is a function that takes const arrays and a count, and returns the total as a Real value
  • DisplaySummary calls CalculateTotal internally — an example of one subprogram using another

:bulb: Key Insight — Procedures Reveal Design Decisions

Notice how the parameter declarations document the design: AddExpense modifies the data (var), DisplayExpenses reads it (const), and CalculateTotal computes from it (const + function return). A reader can understand the program's data flow just by reading the procedure headings. This is theme T1 at work: Pascal teaches programming right by making design decisions visible in the code.

Tomás is particularly pleased: the copy-paste bug he had before — where expenses displayed differently depending on which menu option triggered the display — is now impossible. There is exactly one DisplayExpenses procedure, and every call goes through it.

Rosa notes something more subtle. She can now test each procedure independently. She can write a small test program that calls CalculateTotal with known data and checks the result. This is the beginning of a testing mindset — a topic we will revisit throughout the book.

Looking Under the Hood: PennyWise's Data Flow

Let us trace how data flows through PennyWise v0.7 to solidify our understanding of parameter modes:

Main Program
   │
   ├── ShowMenu              [no parameters — always the same menu]
   │
   ├── AddExpense            [var: Amounts, Categories, Count]
   │     │                    The procedure WRITES to these arrays
   │     ├── ReadValidAmount  [const: Prompt, var: Amount]
   │     │                    Reads from keyboard, stores via var
   │     └── ReadCategory     [var: Category]
   │                          Reads from keyboard, stores via var
   │
   ├── DisplayExpenses       [const: Amounts, Categories; value: Count]
   │     │                    Only READS the arrays — const prevents
   │     │                    accidental modification
   │     └── CalculateTotal   [const: Amounts; value: Count → returns Real]
   │                          Pure function called from inside display
   │
   └── DisplaySummary        [const: Amounts, Categories; value: Count]
         │                    Only READS the arrays
         └── CalculateTotal   [same function, called again — reuse!]

This diagram illustrates a crucial design principle: data flows in through var parameters (in AddExpense) and flows out through const parameters (in DisplayExpenses and DisplaySummary). The functions that compute values (CalculateTotal) sit at the bottom of the call tree, receiving only const data and returning results. This separation of reading from writing makes the program far easier to reason about.

The complete PennyWise 0.7 source code is in code/project-checkpoint.pas. Type it in, compile it, and run it. Then try modifying a single procedure — change the display format, add a new category, improve the input validation — and notice how the change is contained to that one procedure. That containment is the reward for the effort of decomposition.

What PennyWise Gains from Decomposition

Compare the monolithic version from Chapter 6 with this decomposed version:

Aspect Chapter 6 (Monolithic) Chapter 7 (Decomposed)
Lines in main body ~80+ 15
Can test total calculation alone? No Yes — call CalculateTotal
Bug in display format? Might exist in 3 copied locations Fix once in DisplayExpenses
Adding a new menu option? Insert code into the middle of a long block Add one CASE branch and one new procedure
Understanding the program structure? Read all 180 lines Read 15-line main body

Rosa observes: "The decomposed version has more total lines of code — maybe 220 lines instead of 180. But each individual piece is simpler, and finding anything takes seconds instead of minutes." This is a general truth: well-structured code is often slightly longer than unstructured code, but dramatically easier to understand, modify, and debug. The investment in structure pays compound interest.


7.12 Chapter Summary

This chapter introduced the most important organizational tool in Pascal: procedures and functions. Let us review what we covered.

Procedures are named blocks of code that perform specific tasks. They are declared before the main program body and called by name. Every procedure has a heading (name, parameters) and a body (begin...end;).

Parameters come in three flavors: - Value parameters (no keyword) receive copies of the arguments. Changes to the copy do not affect the original. This is the default and the safest mode. - Variable parameters (var) are aliases for the caller's variables. Changes affect the original. Use them when the procedure must modify the caller's data. - Constant parameters (const) are read-only. The compiler prevents modification and may optimize by passing large types by reference internally. Use them for any parameter that should not be changed.

Functions are like procedures but return a value. The return value is assigned to Result (or to the function's name in classic Pascal). Functions should be side-effect-free whenever possible.

Local variables are declared inside procedures and functions. They exist only during the subprogram's execution and cannot be seen by other code. They prevent naming conflicts and keep each subprogram's data private.

Nested subprograms are procedures or functions declared inside other procedures or functions. They can access the enclosing scope's variables and parameters. They provide encapsulation but should not be nested more than one or two levels deep.

Forward declarations solve the problem of mutual references between procedures. A forward declaration provides the heading (name, parameters, return type) with the forward keyword, promising that the full definition will follow later.

Procedural decomposition (top-down design) is the process of breaking a complex problem into small, named, self-contained pieces. The main program becomes an outline; each procedure is a chapter. This approach produces programs that are easier to read, test, debug, and modify.

:link: Cross-Reference — What Comes Next

Chapter 8 dives deeper into scope and the call stack. You will learn exactly what happens in memory when a procedure is called, how local variables are allocated and destroyed, why global variables should be used sparingly, and how parameters are actually passed at the machine level. Chapter 8 builds directly on the foundations laid here.


Before you move to the exercises, here are the mistakes that trip up nearly every beginner in this chapter. Read them now and you will save yourself debugging time later.

Error 1: Forgetting the semicolon after end in a procedure

procedure DoSomething;
begin
  WriteLn('Hello');
end.    { <<< WRONG — period ends the program! Use semicolon: end; }

Fix: Use end; (semicolon) for procedures and functions. Only the program's final end gets a period.

Error 2: Trying to pass a literal to a var parameter

procedure Increment(var X: Integer);
begin
  X := X + 1;
end;

begin
  Increment(5);   { <<< Compiler error: variable identifier expected }
end.

Fix: Pass a variable, not a literal: Increment(MyVar).

Error 3: Expecting value parameters to modify the original

procedure SetToZero(X: Integer);   { No 'var' — this is a value parameter }
begin
  X := 0;
end;

var
  N: Integer;
begin
  N := 42;
  SetToZero(N);
  WriteLn(N);    { Prints 42, not 0! }
end.

Fix: Add var if you intend to modify the caller's variable.

Error 4: Forgetting to assign a function's return value

function Double(X: Integer): Integer;
begin
  { Oops — we computed X * 2 but never assigned it to Result! }
  X * 2;    { This is not valid Pascal }
end;

Fix: Write Result := X * 2; (or Double := X * 2; in classic style).

Error 5: Using a function name on the left side of := inside the function, intending assignment but triggering recursion

function Factorial(N: Integer): Integer;
begin
  if N <= 1 then
    Factorial := 1
  else
    Factorial := N * Factorial(N - 1);   { This is recursion, not just assignment }
end;

This one actually works and is correct recursive code — but it surprises beginners who expected Factorial := to simply set the return value. Using Result := avoids this confusion entirely, which is one reason modern Pascal style prefers Result.

Error 6: Declaring a procedure after code that calls it

begin
  SayHello;    { <<< Error: SayHello not yet declared }
end.

procedure SayHello;
begin
  WriteLn('Hello');
end;

Fix: Move the procedure declaration before the main begin...end block, or use a forward declaration.


:running: Fast Track Summary

If you are an experienced programmer moving quickly through Part I, here is what matters most from this chapter:

  • Pascal has procedure (void) and function (returns a value) as distinct constructs.
  • Three parameter modes: value (copy), var (reference, mutable), const (reference, immutable).
  • const is the idiomatic choice for read-only parameters, especially strings and records.
  • Result variable is Free Pascal's preferred way to set function return values (vs. assigning to the function name).
  • Forward declarations exist for mutual recursion; forward keyword on the heading.
  • Procedures can nest. Inner procedures see enclosing scope.
  • Chapter 8 covers scope rules and the call stack in detail — that is where the deeper understanding lives.