Intermediate

Functions in Delphi

Learn how to define functions and procedures in Delphi (Object Pascal) - parameters, default values, var/const/out modes, scope, recursion, and function references with Docker-ready examples

Functions are how you break a program into reusable, named pieces of logic. Delphi inherits Pascal’s clear distinction between two kinds of routines: a function returns a value, while a procedure performs an action but returns nothing. This separation, dating back to Pascal’s original 1970 design, makes intent obvious at a glance — if a routine gives something back, it’s a function; if it just does something, it’s a procedure.

Delphi is a multi-paradigm, statically and strongly typed language, and its routines reflect that. Every parameter has a declared type, every function declares its return type, and the compiler enforces both. Delphi also adds conveniences that standard Pascal lacks: default parameter values, several parameter-passing modes (var, const, out), and procedural types that let you treat functions as values you can pass around.

In this tutorial you’ll learn how to declare functions and procedures, pass parameters in different modes, give parameters default values, understand local versus global scope (including Delphi’s nested routines), write recursive functions, and use procedural types to build higher-order routines. All examples run with Free Pascal in Delphi compatibility mode.

Functions vs Procedures

The most fundamental distinction in Delphi is between a function (returns a value) and a procedure (does not). Inside a function, the special implicit variable Result holds the value to be returned.

Create a file named functions_basic.dpr:

program FunctionsBasic;

{$APPTYPE CONSOLE}

// A function returns a value via the implicit Result variable.
function Add(A, B: Integer): Integer;
begin
  Result := A + B;
end;

// A procedure performs an action but returns nothing.
procedure Greet(const Name: string);
begin
  WriteLn('Hello, ', Name, '!');
end;

begin
  WriteLn('3 + 4 = ', Add(3, 4));
  Greet('Delphi');
end.

A few things to notice:

  • function Add(A, B: Integer): Integer; — parameters A and B share the type Integer, and the part after the colon declares the return type.
  • Result — Delphi’s implicit return variable. You assign to it instead of using a return statement. (The older Pascal style of assigning to the function’s own name, Add := A + B, also works.)
  • procedure Greet — no return type, because procedures don’t return values.

Parameters: Modes and Default Values

Delphi gives you precise control over how parameters are passed:

  • By value (the default): the routine gets a copy; changes don’t affect the caller.
  • const: read-only; the compiler forbids modification and can pass large values efficiently.
  • var: passed by reference; changes are visible to the caller.
  • out: like var, but signals the parameter is purely for output; its incoming value is ignored.

You can also give trailing parameters default values.

Create a file named parameters.dpr:

program Parameters;

{$APPTYPE CONSOLE}

// const parameter is read-only; Greeting has a default value.
procedure Greet(const Name: string; const Greeting: string = 'Hello');
begin
  WriteLn(Greeting, ', ', Name, '!');
end;

// var parameter is passed by reference - the change is visible to the caller.
procedure Double(var Value: Integer);
begin
  Value := Value * 2;
end;

// out parameters are used to return multiple values.
procedure SplitName(const FullName: string; out First, Last: string);
var
  SpacePos: Integer;
begin
  SpacePos := Pos(' ', FullName);
  First := Copy(FullName, 1, SpacePos - 1);
  Last := Copy(FullName, SpacePos + 1, Length(FullName));
end;

var
  N: Integer;
  FirstName, LastName: string;
begin
  Greet('World');              // uses the default greeting
  Greet('Delphi', 'Welcome');  // overrides the default

  N := 21;
  Double(N);                   // var parameter modifies N in place
  WriteLn('Doubled: ', N);

  SplitName('Anders Hejlsberg', FirstName, LastName);
  WriteLn('First: ', FirstName);
  WriteLn('Last: ', LastName);
end.

The Double procedure changes its caller’s variable because Value is a var parameter. The SplitName procedure uses two out parameters to hand back two strings at once — a common Delphi idiom when a function can only return one value but you need several results.

Recursion

A function that calls itself is recursive. Delphi handles recursion naturally, and the classic examples are factorial and Fibonacci.

Create a file named recursion.dpr:

program Recursion;

{$APPTYPE CONSOLE}

uses
  SysUtils;  // for IntToStr and Trim

// Factorial: N! = N * (N-1) * ... * 1
function Factorial(N: Integer): Int64;
begin
  if N <= 1 then
    Result := 1
  else
    Result := N * Factorial(N - 1);
end;

// Fibonacci: each number is the sum of the previous two.
function Fib(N: Integer): Integer;
begin
  if N < 2 then
    Result := N
  else
    Result := Fib(N - 1) + Fib(N - 2);
end;

var
  I: Integer;
  Sequence: string;
begin
  for I := 1 to 6 do
    WriteLn(I, '! = ', Factorial(I));

  Sequence := '';
  for I := 0 to 9 do
    Sequence := Sequence + IntToStr(Fib(I)) + ' ';
  WriteLn('Fibonacci: ', Trim(Sequence));
end.

Factorial returns an Int64 because factorials grow quickly and would overflow a 32-bit Integer. Each call to Factorial(N) reduces the problem to Factorial(N - 1) until it reaches the base case N <= 1.

Scope and Nested Routines

A local variable, declared inside a routine, exists only while that routine runs. A global variable, declared in the main program’s var section, is visible everywhere. Delphi (like all Pascal dialects) also supports nested routines — a procedure defined inside another procedure, which can see the enclosing routine’s local variables.

Create a file named scope.dpr:

program Scope;

{$APPTYPE CONSOLE}

var
  // Global variable: visible to every routine in the program.
  CallCount: Integer;

procedure DoWork;
var
  // Local variable: exists only during this call.
  LocalValue: Integer;
begin
  Inc(CallCount);              // modifies the global
  LocalValue := CallCount * 10;
  WriteLn('Call #', CallCount, ' -> local value ', LocalValue);
end;

// Outer contains a nested routine, Inner.
procedure Outer;
var
  Shared: Integer;

  // Inner is nested inside Outer and can see Outer's local variables.
  procedure Inner;
  begin
    Shared := Shared + 1;
  end;

begin
  Shared := 100;
  Inner;
  Inner;
  WriteLn('Shared after two Inner calls: ', Shared);
end;

begin
  CallCount := 0;
  DoWork;
  DoWork;
  DoWork;
  Outer;
end.

DoWork reads and modifies the global CallCount, so its value persists between calls. LocalValue, by contrast, starts fresh each call. Inside Outer, the nested Inner procedure shares access to Outer’s Shared variable — something not possible in languages without nested functions.

Higher-Order Routines with Procedural Types

Delphi lets you declare a procedural type — a type whose values are functions. You can store a function in a variable or pass one as a parameter, which is the foundation of higher-order routines.

Create a file named higher_order.dpr:

program HigherOrder;

{$APPTYPE CONSOLE}

uses
  SysUtils;  // for IntToStr and Trim

type
  // A procedural type: any function taking an Integer and returning an Integer.
  TIntFunc = function(X: Integer): Integer;

function Square(X: Integer): Integer;
begin
  Result := X * X;
end;

function Cube(X: Integer): Integer;
begin
  Result := X * X * X;
end;

// MapRange takes a function as a parameter and applies it across a range.
function MapRange(Func: TIntFunc; Lo, Hi: Integer): string;
var
  I: Integer;
begin
  Result := '';
  for I := Lo to Hi do
    Result := Result + IntToStr(Func(I)) + ' ';
  Result := Trim(Result);
end;

begin
  WriteLn('Squares: ', MapRange(Square, 1, 5));
  WriteLn('Cubes:   ', MapRange(Cube, 1, 5));
end.

TIntFunc describes the shape of a function: it takes one Integer and returns an Integer. Both Square and Cube match that shape, so they can be passed to MapRange, which applies whichever function it receives to every value in the range.

Running with Docker

These examples run with Free Pascal in Delphi compatibility mode (-Mdelphi). The compiler names each executable after its source file (without the extension).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Pull the Free Pascal image
docker pull freepascal/fpc:3.2.2-slim

# Functions vs procedures
docker run --rm -v $(pwd):/app -w /app freepascal/fpc:3.2.2-slim \
    sh -c "fpc -Mdelphi functions_basic.dpr && ./functions_basic"

# Parameter modes and default values
docker run --rm -v $(pwd):/app -w /app freepascal/fpc:3.2.2-slim \
    sh -c "fpc -Mdelphi parameters.dpr && ./parameters"

# Recursion
docker run --rm -v $(pwd):/app -w /app freepascal/fpc:3.2.2-slim \
    sh -c "fpc -Mdelphi recursion.dpr && ./recursion"

# Scope and nested routines
docker run --rm -v $(pwd):/app -w /app freepascal/fpc:3.2.2-slim \
    sh -c "fpc -Mdelphi scope.dpr && ./scope"

# Higher-order routines
docker run --rm -v $(pwd):/app -w /app freepascal/fpc:3.2.2-slim \
    sh -c "fpc -Mdelphi higher_order.dpr && ./higher_order"

Expected Output

Running functions_basic:

3 + 4 = 7
Hello, Delphi!

Running parameters:

Hello, World!
Welcome, Delphi!
Doubled: 42
First: Anders
Last: Hejlsberg

Running recursion:

1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
Fibonacci: 0 1 1 2 3 5 8 13 21 34

Running scope:

Call #1 -> local value 10
Call #2 -> local value 20
Call #3 -> local value 30
Shared after two Inner calls: 102

Running higher_order:

Squares: 1 4 9 16 25
Cubes:   1 8 27 64 125

Key Concepts

  • Functions vs procedures — a function returns a value; a procedure does not. This distinction is explicit in Delphi and communicates intent clearly.
  • The Result variable — inside a function, assign the return value to Result. Assigning to the function name also works but Result is the modern, preferred style.
  • Parameter modes matter — by value (a copy), const (read-only and efficient), var (by reference, changes visible to caller), and out (for returning data). Choosing the right mode documents intent and improves performance.
  • Default parameter values let you make trailing parameters optional, reducing the need for overloaded routines.
  • Scope is lexical — local variables live only during a call, globals persist, and Delphi’s nested routines can access their enclosing routine’s locals.
  • Recursion is natural — define a base case to stop the recursion and a recursive case that moves toward it. Watch your return types: factorials need Int64, not Integer.
  • Procedural types enable higher-order routines — declare a type that describes a function’s signature, then pass matching functions as values to build reusable, generic logic.

Running Today

All examples can be run using Docker:

docker pull freepascal/fpc:3.2.2-slim
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining