> "The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise."
Learning Objectives
- Define generic classes with type parameters
- Define generic procedures and functions
- Apply type constraints to generic parameters
- Use built-in generic collections (TFPGList, TFPGMap, etc.)
- Compare generics with untyped approaches (why generics are safer)
In This Chapter
- 20.1 The Problem Generics Solve
- 20.2 Generic Classes
- 20.3 Generic Methods
- 20.4 Type Constraints
- 20.5 Built-in Generic Collections in Free Pascal
- 20.6 Generic Algorithms
- 20.7 Generics vs. Untyped/Variant Approaches
- 20.8 Generics in Free Pascal vs. Delphi
- 20.9 Project Checkpoint: PennyWise Goes Generic
- 20.10 Summary
- Key Terms Introduced in This Chapter
Chapter 20: Generics: Type-Safe Reusable Code
"The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise." — Edsger W. Dijkstra
In Chapter 9, we built arrays of integers. In Chapter 11, we built arrays of records. In Chapter 15, we built linked lists that used untyped pointers to hold any kind of data. Each time, we wrote code that was structurally identical — pushing, popping, searching, sorting — but the types differed. We wrote one stack for integers, another for strings, and if we wanted a stack for TExpense objects, we would need to write yet another.
This is tedious, error-prone, and fundamentally wasteful. The algorithm for a stack does not depend on what the stack holds. Push is push. Pop is pop. Whether the elements are integers, strings, or expense records, the logic is the same. What changes is only the type.
Generics solve this problem. A generic class or procedure is a template parameterized by one or more types. You write the code once, using a placeholder type (like T), and then specialize it for any concrete type you need: TStack<Integer>, TStack<String>, TStack<TExpense>. The compiler generates type-specific code for each specialization, ensuring full type safety at compile time with zero runtime overhead.
This chapter teaches you how to define and use generics in Free Pascal. By the end, PennyWise will replace its hand-maintained expense arrays with a generic TList<TExpense>, and you will have generic sorting procedures that work with any comparable type.
Generics represent one of the most significant evolution points in any programming language. When Java added generics in version 5, it transformed how Java code was written overnight. When C# added generics in version 2.0, the entire .NET collection library was redesigned around them. When Go added generics in version 1.18, it resolved one of the language's most controversial design decisions. The addition of generics to Free Pascal and Delphi followed the same pattern: they fundamentally changed how Object Pascal developers structure their code, moving from unsafe Pointer casts and code duplication to type-safe, reusable abstractions.
Understanding generics is not optional for modern Pascal programming. Every serious Free Pascal and Delphi codebase uses them extensively. The fgl unit and Delphi's Generics.Collections unit are among the most imported units in production code. If you read someone else's Pascal code — and you will, because programming is as much about reading code as writing it — you will encounter generics on the first day. This chapter ensures you are ready.
The concept itself is not hard once you grasp the key insight: generics are templates parameterized by type. You write the code once with a placeholder, and the compiler fills in the concrete type when you use it. Everything else — syntax, constraints, specialization — follows from this core idea. Once you have written your first generic class, the second one is easy. By the third, it feels natural. By the end of this chapter, you will wonder how you ever lived without them.
20.1 The Problem Generics Solve
Let us make the problem concrete. Suppose we need a simple stack — a last-in, first-out data structure — for three different types.
Without Generics: Three Stacks
type
{ Stack of integers }
TIntStack = class
private
FItems: array of Integer;
FCount: Integer;
public
procedure Push(AValue: Integer);
function Pop: Integer;
function Peek: Integer;
property Count: Integer read FCount;
end;
{ Stack of strings — identical logic, different type }
TStringStack = class
private
FItems: array of String;
FCount: Integer;
public
procedure Push(const AValue: String);
function Pop: String;
function Peek: String;
property Count: Integer read FCount;
end;
{ Stack of TExpense — identical logic again }
TExpenseStack = class
private
FItems: array of TExpense;
FCount: Integer;
public
procedure Push(AValue: TExpense);
function Pop: TExpense;
function Peek: TExpense;
property Count: Integer read FCount;
end;
Every method implementation is identical except for the type of the elements. If we find a bug in Push, we must fix it in three places. If we want a stack of TCategory, we need a fourth copy. This violates a fundamental principle of software engineering: Don't Repeat Yourself (DRY).
The Untyped Approach: Dangerous Freedom
One might try using untyped pointers to create a single "universal" stack:
type
TUntypedStack = class
private
FItems: array of Pointer;
FCount: Integer;
public
procedure Push(AValue: Pointer);
function Pop: Pointer;
end;
This works mechanically, but it sacrifices type safety. When you pop a value, you get a raw Pointer that could point to anything — an integer, a string, an expense, or a completely invalid memory location. The compiler cannot help you: if you push a PInteger and pop it as a PString, the compiler says nothing. The program crashes at runtime, or worse, silently corrupts data.
The Generic Approach: One Stack, All Types, Full Safety
type
generic TStack<T> = class
private
FItems: array of T;
FCount: Integer;
public
procedure Push(AValue: T);
function Pop: T;
function Peek: T;
property Count: Integer read FCount;
end;
One definition. One set of methods to maintain. Full type safety for every specialization:
type
TIntStack = specialize TStack<Integer>;
TStringStack = specialize TStack<String>;
TExpenseStack = specialize TStack<TExpense>;
The compiler generates specialized code for each type. TIntStack.Push accepts only integers. TStringStack.Pop returns only strings. Type confusion is impossible — the compiler catches it at compile time. And if we fix a bug in TStack<T>, the fix automatically applies to all specializations.
💡 Key Insight: Generics give you the reusability of untyped approaches with the safety of typed approaches. You write code once and use it with any type, but the compiler still checks everything at compile time. It is the best of both worlds.
20.2 Generic Classes
Let us build a complete generic stack to see all the mechanics.
Declaring a Generic Class
In Free Pascal with {$mode objfpc}, the syntax uses the generic keyword:
type
generic TStack<T> = class
private
FItems: array of T;
FCount: Integer;
FCapacity: Integer;
procedure Grow;
public
constructor Create;
procedure Push(AValue: T);
function Pop: T;
function Peek: T;
function IsEmpty: Boolean;
procedure Clear;
property Count: Integer read FCount;
end;
The T in angle brackets is the type parameter. Within the class, T behaves like a real type — you can declare variables of type T, use T in array declarations, pass T as parameters, and return T from functions.
Implementing Generic Methods
constructor TStack.Create;
begin
inherited Create;
FCount := 0;
FCapacity := 4;
SetLength(FItems, FCapacity);
end;
procedure TStack.Grow;
begin
FCapacity := FCapacity * 2;
SetLength(FItems, FCapacity);
end;
procedure TStack.Push(AValue: T);
begin
if FCount >= FCapacity then
Grow;
FItems[FCount] := AValue;
Inc(FCount);
end;
function TStack.Pop: T;
begin
if FCount = 0 then
raise Exception.Create('Stack underflow: cannot pop from an empty stack');
Dec(FCount);
Result := FItems[FCount];
end;
function TStack.Peek: T;
begin
if FCount = 0 then
raise Exception.Create('Stack underflow: cannot peek an empty stack');
Result := FItems[FCount - 1];
end;
function TStack.IsEmpty: Boolean;
begin
Result := FCount = 0;
end;
procedure TStack.Clear;
begin
FCount := 0;
end;
Notice that in the implementation section, we write TStack without the type parameter — Free Pascal infers it from the declaration.
Specializing a Generic Class
To use the generic stack, you specialize it for a concrete type:
type
TIntStack = specialize TStack<Integer>;
TStringStack = specialize TStack<String>;
TRealStack = specialize TStack<Double>;
Each specialization is a distinct type. TIntStack and TStringStack are unrelated types — you cannot assign one to the other.
Multiple Type Parameters
Generic types can have more than one type parameter. A common example is a key-value pair:
type
generic TPair<TKey, TValue> = record
Key: TKey;
Value: TValue;
end;
generic TDictionary<TKey, TValue> = class
private
FPairs: array of specialize TPair<TKey, TValue>;
FCount: Integer;
public
procedure Add(AKey: TKey; AValue: TValue);
function Find(AKey: TKey): TValue;
property Count: Integer read FCount;
end;
Each type parameter is independent — TKey and TValue can be any types:
type
TNameAgeMap = specialize TDictionary<String, Integer>;
TIDExpenseMap = specialize TDictionary<Integer, TExpense>;
TCoordinate = specialize TPair<Double, Double>;
Multiple type parameters are common in collection types (TDictionary<K,V>, TPair<A,B>), transformation functions (TMapFunc<TIn, TOut>), and result types (TResult<TValue, TError>).
Using a Specialized Class
var
Numbers: TIntStack;
Names: TStringStack;
begin
Numbers := TIntStack.Create;
try
Numbers.Push(10);
Numbers.Push(20);
Numbers.Push(30);
WriteLn('Top: ', Numbers.Peek); { 30 }
WriteLn('Pop: ', Numbers.Pop); { 30 }
WriteLn('Pop: ', Numbers.Pop); { 20 }
WriteLn('Count: ', Numbers.Count); { 1 }
finally
Numbers.Free;
end;
Names := TStringStack.Create;
try
Names.Push('Pascal');
Names.Push('is');
Names.Push('great');
while not Names.IsEmpty do
Write(Names.Pop, ' '); { great is Pascal }
WriteLn;
finally
Names.Free;
end;
end;
Type safety is enforced at compile time:
Numbers.Push('hello'); { COMPILE ERROR: String is not Integer }
Names.Push(42); { COMPILE ERROR: Integer is not String }
This is the power of generics: the compiler catches type errors that untyped approaches would miss entirely.
Nested Generic Types
Generic classes can contain nested types that use the type parameter:
type
generic TLinkedList<T> = class
public
type
TNode = record
Data: T;
Next: ^TNode;
end;
PNode = ^TNode;
private
FHead: PNode;
FCount: Integer;
public
procedure Add(AValue: T);
function Contains(AValue: T): Boolean;
property Count: Integer read FCount;
end;
The TNode record uses the enclosing type parameter T to type its Data field. When you specialize TLinkedList<Integer>, TNode.Data becomes Integer. When you specialize TLinkedList<String>, TNode.Data becomes String. The entire node structure is type-safe.
Generic Class with Methods Operating on T
Here is a more complete example — a generic dynamic array wrapper with search and aggregation capabilities:
type
generic TDynArray<T> = class
private
FItems: array of T;
FCount: Integer;
procedure EnsureCapacity;
public
constructor Create;
procedure Add(AItem: T);
procedure Clear;
function Get(Index: Integer): T;
procedure SetItem(Index: Integer; AItem: T);
function ToArray: specialize TArray<T>;
property Count: Integer read FCount;
property Items[Index: Integer]: T read Get write SetItem; default;
end;
The default property directive on Items lets you use array-style access syntax:
type
TIntArray = specialize TDynArray<Integer>;
var
Arr: TIntArray;
begin
Arr := TIntArray.Create;
try
Arr.Add(10);
Arr.Add(20);
WriteLn(Arr[0]); { 10 — uses the default property }
Arr[1] := 25; { Sets index 1 to 25 }
finally
Arr.Free;
end;
end;
This makes your generic collection feel like a native array but with dynamic sizing, bounds checking, and type safety.
20.3 Generic Methods
In addition to generic classes, Free Pascal supports generic procedures and functions — standalone routines parameterized by a type.
Generic Procedure Syntax
generic procedure Swap<T>(var A, B: T);
var
Temp: T;
begin
Temp := A;
A := B;
B := Temp;
end;
Calling a Generic Procedure
You specialize the procedure at the call site:
var
X, Y: Integer;
S1, S2: String;
begin
X := 10; Y := 20;
specialize Swap<Integer>(X, Y);
WriteLn(X, ' ', Y); { 20 10 }
S1 := 'Hello'; S2 := 'World';
specialize Swap<String>(S1, S2);
WriteLn(S1, ' ', S2); { World Hello }
end;
Generic Functions
generic function Max<T>(A, B: T): T;
begin
if A > B then
Result := A
else
Result := B;
end;
Wait — there is a problem here. The > operator works for integers, reals, and strings, but does it work for every possible type T? What if T is a record or an object? The compiler cannot know whether > is defined for an arbitrary T.
This is where type constraints come in — the subject of our next section.
⚠️ Free Pascal vs. Delphi Syntax: In Delphi mode (
{$mode delphi}`), the syntax is slightly different: you omit the `generic` and `specialize` keywords and use angle brackets directly. In `{$mode objfpc}(which we use throughout this textbook), both keywords are required. We discuss the dialect differences in Section 20.8.
Generic Records
Generics are not limited to classes. You can also define generic records:
type
generic TPair<TKey, TValue> = record
Key: TKey;
Value: TValue;
end;
TNameAge = specialize TPair<String, Integer>;
TCoordinate = specialize TPair<Double, Double>;
Generic records are useful for simple data containers where the overhead of a class (heap allocation, constructor, destructor) is unnecessary. They live on the stack and are copied by value — perfect for small, immutable data aggregates.
Here is a more practical example — a result type that can represent either a successful value or an error:
type
generic TResult<T> = record
Success: Boolean;
Value: T;
ErrorMessage: String;
end;
function SafeDivide(A, B: Integer): specialize TResult<Integer>;
begin
if B = 0 then
begin
Result.Success := False;
Result.ErrorMessage := 'Division by zero';
end
else
begin
Result.Success := True;
Result.Value := A div B;
end;
end;
{ Usage }
var
R: specialize TResult<Integer>;
begin
R := SafeDivide(10, 3);
if R.Success then
WriteLn('Result: ', R.Value)
else
WriteLn('Error: ', R.ErrorMessage);
end;
This pattern — sometimes called the "Result type" or "Either type" — is common in functional programming languages like Rust and Haskell. It provides an alternative to exceptions for expected failure cases, making the possibility of failure explicit in the function's return type.
Generic Methods Inside Generic Classes
A generic class can contain methods that introduce additional type parameters beyond the class's own:
type
generic TDataStore<T> = class
private
FItems: array of T;
FCount: Integer;
public
procedure Add(AItem: T);
{ This method introduces a new type parameter TResult }
generic function Transform<TResult>(
Func: function(const AItem: T): TResult): specialize TArray<TResult>;
end;
This allows a TDataStore<Integer> to produce an array of strings by transforming each integer, or a TDataStore<TExpense> to produce an array of currency values by extracting amounts. The class type parameter (T) and the method type parameter (TResult) are independent.
Generics Compared to C++ Templates and Java Generics
Understanding how Pascal generics compare to other languages clarifies both the power and limitations of each approach.
C++ Templates: C++ templates are the most powerful form of generics. They are expanded at compile time through textual substitution — the compiler essentially copy-pastes the template code for each type and then compiles each copy independently. This means C++ templates can use any operation on T as long as the concrete type supports it. There is no need for constraints; the compiler checks each specialization individually. The downside is that error messages for template failures are notoriously incomprehensible — they can span hundreds of lines of compiler output referring to internal template expansion details.
Java Generics (Type Erasure): Java generics use type erasure — the generic type information exists only at compile time. At runtime, a List<Integer> and a List<String> are both just List. This means Java generics cannot create instances of T (new T() is illegal), cannot use primitive types as type parameters (List<int> is illegal — you must use List<Integer>), and have certain reflection limitations. The advantage is backward compatibility with pre-generics Java code.
Pascal Generics (Reification): Pascal generics are reified — each specialization generates distinct compiled code, similar to C++ templates. specialize TStack<Integer> and specialize TStack<String> produce separate implementations. This means Pascal generics have full runtime type identity and no boxing overhead for value types. Unlike C++ templates, Pascal requires explicit constraints to use operations on T — you cannot call T.ToString unless T is constrained to a class. This produces clearer error messages at the cost of slightly less flexibility.
| Feature | C++ Templates | Java Generics | Pascal Generics |
|---|---|---|---|
| Implementation | Code generation | Type erasure | Code generation |
| Runtime type info | Yes | No (erased) | Yes |
| Value type parameters | Yes | No (must box) | Yes |
| Constraints required | No | Yes | Yes |
| Error messages | Very poor | Good | Good |
| Multiple specializations | Yes (distinct) | No (single erased) | Yes (distinct) |
If you learn Java or C++ generics later, this comparison will help you immediately understand the differences and adapt your approach.
Generic Interfaces
You can combine generics with interfaces (from Chapter 18) to create type-safe abstract contracts:
type
generic IComparable<T> = interface
function CompareTo(const AOther: T): Integer;
end;
generic IRepository<T> = interface
procedure Add(AItem: T);
function GetByIndex(AIndex: Integer): T;
function Count: Integer;
end;
A class implementing specialize IRepository<TExpense> must provide Add(AItem: TExpense), GetByIndex(AIndex: Integer): TExpense, and Count: Integer. The type safety flows through the interface into every implementing class.
This is the same pattern used in Java (List<E>), C# (IList<T>), and other modern languages. Generic interfaces are the backbone of type-safe collection frameworks.
20.4 Type Constraints
Not every operation makes sense for every type. You can compare integers with >, but you cannot compare arbitrary objects with > (unless you have defined operator overloading, which we cover in Chapter 21). Type constraints let you tell the compiler what capabilities T must have.
Class Constraints
The most common constraint restricts T to be a class type:
type
generic TObjectList<T: class> = class
private
FItems: array of T;
FCount: Integer;
public
procedure Add(AItem: T);
function Get(Index: Integer): T;
procedure FreeAll;
property Count: Integer read FCount;
end;
The constraint T: class means T must be a class type (a descendant of TObject). This guarantees that T has methods like Free, ClassName, and InheritsFrom. You can now safely call FItems[i].Free in the FreeAll method.
Constructor Constraints
You can require that T has a parameterless constructor:
type
generic TFactory<T: class, constructor> = class
public
function CreateInstance: T;
end;
function TFactory.CreateInstance: T;
begin
Result := T.Create; { Safe because T has a constructor }
end;
The constructor constraint guarantees that T.Create is valid. Without it, the compiler would reject T.Create because it cannot know whether an arbitrary type has a Create method.
Interface Constraints
You can constrain T to implement a specific interface:
type
generic TSortedList<T: IComparable> = class
public
procedure Add(AItem: T);
{ Items are kept in sorted order using IComparable.CompareTo }
end;
This ensures that every item in the list can be compared to other items, which is necessary for maintaining sorted order.
Record Constraints
Free Pascal also supports constraining T to be a record type:
type
generic TRecordList<T: record> = class
{ T must be a value type (record), not a class }
end;
Multiple Constraints
You can combine constraints:
type
generic TManagedList<T: class, IExportable> = class
{ T must be a class that implements IExportable }
end;
Practical Example: Generic Comparison
Here is a generic binary search that requires items to be comparable:
type
TCompareFunc = function(const A, B: T): Integer;
generic function BinarySearch<T>(const Arr: array of T;
const Target: T; CompareFunc: specialize TCompareFunc<T>): Integer;
var
Low, High, Mid, Cmp: Integer;
begin
Low := 0;
High := Length(Arr) - 1;
Result := -1;
while Low <= High do
begin
Mid := (Low + High) div 2;
Cmp := CompareFunc(Arr[Mid], Target);
if Cmp = 0 then
Exit(Mid)
else if Cmp < 0 then
Low := Mid + 1
else
High := Mid - 1;
end;
end;
This function works with any array of any type, provided you supply a comparison function. The type safety is maintained: you cannot accidentally search an array of integers using a string comparison function.
Combining Constraints
You can require multiple constraints simultaneously:
type
generic TManagedRepository<T: TEntity, IExportable> = class
{ T must be a TEntity descendant AND implement IExportable }
procedure Add(AItem: T);
procedure ExportAll(AStream: TStream);
end;
The constraint T: TEntity, IExportable means T must satisfy both conditions: it must descend from TEntity (giving it an ID property) and implement IExportable (giving it ExportToStream and ExportAsString). This is the intersection of Chapter 18 (interfaces) and Chapter 20 (generics) — and it is a powerful combination.
Constraints in Practice: What Can You Do with T?
The constraints you apply determine what operations you can perform on T:
| Constraint | What you can do with T |
|---|---|
| None | Assign, compare with =, pass as parameter |
T: class |
All above + call methods of TObject (Free, ClassName, InheritsFrom), use nil |
T: constructor |
All of class + call T.Create |
T: TSpecificClass |
All of class + access methods/properties of TSpecificClass and ancestors |
T: ISpecificInterface |
Call methods defined in ISpecificInterface |
T: record |
Assign, compare, treat as a value type |
Choose the minimum constraint that lets you do what you need. More constraints mean fewer types can be used to specialize, so a tighter constraint reduces reusability. If all you need is assignment and equality, leave T unconstrained.
Constraints in Practice: A Detailed Example
Let us build a generic TOrderedList<T> that maintains its elements in sorted order. This requires the ability to compare elements, which means we need a constraint:
type
generic TOrderedList<T> = class
private
FItems: array of T;
FCount: Integer;
FCompare: specialize TCompareFunc<T>;
function FindInsertionPoint(const AItem: T): Integer;
public
constructor Create(ACompare: specialize TCompareFunc<T>);
procedure Add(const AItem: T);
function Contains(const AItem: T): Boolean;
function Get(Index: Integer): T;
property Count: Integer read FCount;
end;
function TOrderedList.FindInsertionPoint(const AItem: T): Integer;
var
Lo, Hi, Mid, Cmp: Integer;
begin
Lo := 0;
Hi := FCount - 1;
while Lo <= Hi do
begin
Mid := (Lo + Hi) div 2;
Cmp := FCompare(FItems[Mid], AItem);
if Cmp < 0 then
Lo := Mid + 1
else
Hi := Mid - 1;
end;
Result := Lo;
end;
procedure TOrderedList.Add(const AItem: T);
var
InsertAt, i: Integer;
begin
InsertAt := FindInsertionPoint(AItem);
if FCount >= Length(FItems) then
SetLength(FItems, FCount * 2 + 4);
{ Shift elements right to make room }
for i := FCount - 1 downto InsertAt do
FItems[i + 1] := FItems[i];
FItems[InsertAt] := AItem;
Inc(FCount);
end;
This ordered list uses binary search to find the correct insertion point, then shifts elements to make room. It works for any type that has a comparison function — integers, strings, records, objects. The type parameter T is unconstrained because comparison is provided as a function parameter rather than assumed from T's type.
Usage:
type
TIntOrderedList = specialize TOrderedList<Integer>;
function CompareInt(const A, B: Integer): Integer;
begin
Result := A - B;
end;
var
List: TIntOrderedList;
i: Integer;
begin
List := TIntOrderedList.Create(@CompareInt);
try
List.Add(42);
List.Add(17);
List.Add(99);
List.Add(5);
List.Add(73);
{ Elements are always in sorted order }
for i := 0 to List.Count - 1 do
Write(List.Get(i), ' '); { Output: 5 17 42 73 99 }
WriteLn;
finally
List.Free;
end;
end;
This is a real-world use of generics: a reusable data structure that works with any type and maintains a useful invariant (sorted order) automatically. The caller adds elements in any order and gets them back sorted. This is the kind of abstraction that makes code both safer and simpler.
20.5 Built-in Generic Collections in Free Pascal
Free Pascal provides several generic collection classes in the fgl (Free Pascal Generic Library) unit. These are production-ready, well-tested, and should be your first choice before writing custom collections.
TFPGList — Generic Dynamic List
uses
fgl;
type
TIntList = specialize TFPGList<Integer>;
var
Numbers: TIntList;
begin
Numbers := TIntList.Create;
try
Numbers.Add(42);
Numbers.Add(17);
Numbers.Add(99);
Numbers.Add(5);
WriteLn('Count: ', Numbers.Count); { 4 }
WriteLn('First: ', Numbers[0]); { 42 }
WriteLn('Last: ', Numbers[Numbers.Count - 1]); { 5 }
Numbers.Sort;
WriteLn('After sort: ');
var i: Integer;
for i := 0 to Numbers.Count - 1 do
Write(Numbers[i], ' '); { 5 17 42 99 }
WriteLn;
WriteLn('Index of 17: ', Numbers.IndexOf(17)); { 1 }
Numbers.Delete(0); { Remove first element }
WriteLn('After delete: Count = ', Numbers.Count); { 3 }
finally
Numbers.Free;
end;
end;
TFPGObjectList — Generic List That Owns Objects
When your list holds objects and should free them when removed, use TFPGObjectList:
uses
fgl;
type
TExpense = class
Amount: Currency;
Category: String;
constructor Create(AAmount: Currency; const ACategory: String);
end;
TExpenseList = specialize TFPGObjectList<TExpense>;
var
Expenses: TExpenseList;
begin
Expenses := TExpenseList.Create;
Expenses.FreeObjects := True; { When items are removed, Free them }
try
Expenses.Add(TExpense.Create(42.50, 'Groceries'));
Expenses.Add(TExpense.Create(15.00, 'Transport'));
Expenses.Add(TExpense.Create(89.99, 'Software'));
WriteLn('Expenses: ', Expenses.Count);
var i: Integer;
for i := 0 to Expenses.Count - 1 do
WriteLn(Format(' %s: $%.2f', [Expenses[i].Category, Expenses[i].Amount]));
Expenses.Delete(1); { Removes and frees the Transport expense }
WriteLn('After delete: ', Expenses.Count);
finally
Expenses.Free; { Frees remaining expenses because FreeObjects = True }
end;
end;
TFPGMap — Generic Key-Value Map
uses
fgl;
type
TBudgetMap = specialize TFPGMap<String, Currency>;
var
Budgets: TBudgetMap;
begin
Budgets := TBudgetMap.Create;
try
Budgets.Add('Groceries', 200.00);
Budgets.Add('Transport', 100.00);
Budgets.Add('Software', 50.00);
WriteLn('Grocery budget: $', Budgets['Groceries']:0:2);
{ Update a value }
Budgets['Groceries'] := 250.00;
WriteLn('Updated grocery budget: $', Budgets['Groceries']:0:2);
{ Check existence }
WriteLn('Has "Dining"? ', Budgets.IndexOf('Dining') >= 0);
WriteLn('Has "Software"? ', Budgets.IndexOf('Software') >= 0);
finally
Budgets.Free;
end;
end;
Iterating Over Generic Collections
All fgl collections support indexed access, but the idiomatic way to iterate depends on the collection:
{ Index-based iteration — works for all collections }
for i := 0 to Expenses.Count - 1 do
WriteLn(Expenses[i].ToString);
{ For-in iteration — if the collection supports GetEnumerator }
for Exp in Expenses do
WriteLn(Exp.ToString);
TFPGList<T> and TFPGObjectList<T> support for..in iteration out of the box. This is the cleanest syntax and should be your default choice when you do not need the index.
For TFPGMap, you iterate over indices and access keys and values separately:
for i := 0 to Budgets.Count - 1 do
WriteLn(Format('%s: $%.2f', [Budgets.Keys[i], Budgets.Data[i]]));
The Keys and Data array properties give you direct access to the sorted key-value pairs.
Performance Characteristics
Understanding the performance of generic collections helps you choose the right one:
| Operation | TFPGList |
TFPGObjectList |
TFPGMap |
|---|---|---|---|
| Add to end | O(1) amortized | O(1) amortized | O(n) — maintains sort |
| Access by index | O(1) | O(1) | O(1) |
| Find by value | O(n) | O(n) | O(log n) — binary search |
| Delete by index | O(n) — shifts elements | O(n) — shifts elements | O(n) — shifts elements |
| Sort | O(n log n) | O(n log n) | Automatic — always sorted |
For most PennyWise operations — adding expenses, iterating through all expenses, occasional sorting — TFPGObjectList<T> is the right choice. For key-value lookups (category budgets, settings), TFPGMap<K,V> is ideal because its sorted structure enables fast binary search.
Summary of Built-in Collections
| Class | Description | Owns elements? | Key features |
|---|---|---|---|
TFPGList<T> |
Dynamic array list | No | Sort, IndexOf, Add, Delete, Insert |
TFPGObjectList<T> |
Object list | Optional (FreeObjects) |
Same as TFPGList + automatic freeing |
TFPGMap<K,V> |
Sorted key-value map | No | Key-based access, sorted keys |
TFPGMapObject<K,V> |
Object value map | Optional | Same as TFPGMap + automatic freeing of values |
TFPGList vs. TList: Why Generics Win
To appreciate the value of generic collections, compare TFPGList<Integer> with the non-generic TList:
{ Non-generic TList: unsafe and verbose }
uses Classes;
var
List: TList;
P: PInteger;
Value: Integer;
begin
List := TList.Create;
New(P); P^ := 42; List.Add(P);
New(P); P^ := 17; List.Add(P);
Value := PInteger(List[0])^; { Must cast — no type safety }
{ List.Add(@SomeString) would compile but crash at runtime }
for var i := 0 to List.Count - 1 do
Dispose(PInteger(List[i])); { Must manually free each item }
List.Free;
end;
{ Generic TFPGList<Integer>: safe and clean }
uses fgl;
var
List: specialize TFPGList<Integer>;
begin
List := specialize TFPGList<Integer>.Create;
List.Add(42);
List.Add(17);
WriteLn(List[0]); { No cast needed — type-safe }
{ List.Add('hello') would not compile — caught at compile time }
List.Free; { No manual pointer management }
end;
The generic version is shorter, safer, and faster to write. Every cast in the non-generic version is a potential bug. Every Dispose is a potential memory leak if forgotten. The generic version eliminates both categories of error entirely. This is why the fgl unit should be your default choice for collections in modern Free Pascal.
💡 Practical Advice: Use the
fglcollections for most needs. Write custom generic classes only when the built-in collections do not fit your use case (e.g., you need a priority queue, a ring buffer, or a specialized tree).
20.6 Generic Algorithms
Generics are not just for data structures — they are equally powerful for algorithms. A generic sorting procedure, for instance, can sort any array provided you supply a comparison function.
Generic Bubble Sort
type
generic TCompareFunc<T> = function(const A, B: T): Integer;
generic procedure BubbleSort<T>(var Arr: array of T;
Compare: specialize TCompareFunc<T>);
var
i, j: Integer;
Temp: T;
begin
for i := High(Arr) downto 1 do
for j := 0 to i - 1 do
if Compare(Arr[j], Arr[j + 1]) > 0 then
begin
Temp := Arr[j];
Arr[j] := Arr[j + 1];
Arr[j + 1] := Temp;
end;
end;
Comparison Functions
function CompareIntegers(const A, B: Integer): Integer;
begin
Result := A - B;
end;
function CompareStrings(const A, B: String): Integer;
begin
Result := CompareStr(A, B);
end;
function CompareExpensesByAmount(const A, B: TExpense): Integer;
begin
if A.Amount < B.Amount then Result := -1
else if A.Amount > B.Amount then Result := 1
else Result := 0;
end;
function CompareExpensesByDate(const A, B: TExpense): Integer;
begin
if A.Date < B.Date then Result := -1
else if A.Date > B.Date then Result := 1
else Result := 0;
end;
Using Generic Sort
var
Numbers: array[0..4] of Integer = (42, 17, 99, 5, 73);
Names: array[0..3] of String = ('Delta', 'Alpha', 'Charlie', 'Bravo');
begin
specialize BubbleSort<Integer>(Numbers, @CompareIntegers);
{ Numbers: 5, 17, 42, 73, 99 }
specialize BubbleSort<String>(Names, @CompareStrings);
{ Names: Alpha, Bravo, Charlie, Delta }
end;
Generic Filter
type
generic TPredicateFunc<T> = function(const AItem: T): Boolean;
generic function Filter<T>(const Arr: array of T;
Predicate: specialize TPredicateFunc<T>): specialize TArray<T>;
var
i, Count: Integer;
begin
Count := 0;
SetLength(Result, Length(Arr)); { Over-allocate }
for i := 0 to High(Arr) do
if Predicate(Arr[i]) then
begin
Result[Count] := Arr[i];
Inc(Count);
end;
SetLength(Result, Count); { Trim to actual size }
end;
Generic Map (Transform)
generic function Map<TIn, TOut>(const Arr: array of TIn;
Transform: function(const AItem: TIn): TOut): specialize TArray<TOut>;
var
i: Integer;
begin
SetLength(Result, Length(Arr));
for i := 0 to High(Arr) do
Result[i] := Transform(Arr[i]);
end;
These generic algorithms — sort, filter, map — form the foundation of a functional programming style that is increasingly popular across all languages. Learning them in Pascal means you will recognize them instantly in Python (sorted(), filter(), map()), JavaScript (.sort(), .filter(), .map()), and every other modern language.
Generic Reduce (Fold)
The most powerful generic algorithm is Reduce (also called Fold) — it combines all elements of a collection into a single value using an accumulator function:
type
generic TReduceFunc<T, TAcc> = function(const Acc: TAcc; const Item: T): TAcc;
generic function Reduce<T, TAcc>(const Arr: array of T;
Func: specialize TReduceFunc<T, TAcc>; Initial: TAcc): TAcc;
var
i: Integer;
begin
Result := Initial;
for i := 0 to High(Arr) do
Result := Func(Result, Arr[i]);
end;
With Reduce, you can express sum, product, minimum, maximum, count, and virtually any aggregation as a single function call:
{ Sum }
Total := specialize Reduce<Integer, Integer>(Numbers, @Add, 0);
{ Product }
Product := specialize Reduce<Integer, Integer>(Numbers, @Multiply, 1);
{ Count items matching a condition }
function CountIf(const Acc: Integer; const Item: Integer): Integer;
begin
if Item > 50 then Result := Acc + 1
else Result := Acc;
end;
BigCount := specialize Reduce<Integer, Integer>(Numbers, @CountIf, 0);
The power of Reduce is its generality: any operation that processes a collection and produces a single result can be expressed as a reduce. The initial value serves as the identity element — 0 for addition, 1 for multiplication, '' for string concatenation, and so on.
Generic Search: Linear and Binary
A generic linear search finds the first occurrence of a target value in any array:
generic function LinearSearch<T>(const Arr: array of T;
const Target: T): Integer;
var
i: Integer;
begin
Result := -1;
for i := 0 to High(Arr) do
if Arr[i] = Target then
Exit(i);
end;
For sorted arrays, binary search is dramatically faster — O(log n) versus O(n):
type
generic TCompareFunc<T> = function(const A, B: T): Integer;
generic function BinarySearch<T>(const Arr: array of T;
const Target: T;
Compare: specialize TCompareFunc<T>): Integer;
var
Lo, Hi, Mid, Cmp: Integer;
begin
Lo := 0;
Hi := High(Arr);
Result := -1;
while Lo <= Hi do
begin
Mid := (Lo + Hi) div 2;
Cmp := Compare(Arr[Mid], Target);
if Cmp = 0 then
Exit(Mid)
else if Cmp < 0 then
Lo := Mid + 1
else
Hi := Mid - 1;
end;
end;
The comparison function parameter makes these generic searches work with any type. For integers, pass a function that subtracts. For strings, pass CompareStr. For expenses, pass a function that compares amounts or dates. The algorithm is fixed; the comparison is pluggable.
Why Generic Algorithms Matter
You might wonder: why write a generic BubbleSort<T> when the built-in TFPGList<T>.Sort already exists? The answer is that generic algorithms teach you to think abstractly about operations and data. When you write a generic filter, you are separating what to filter (the predicate) from how to filter (the iteration). When you write a generic sort, you are separating what constitutes order (the comparison) from how to achieve order (the algorithm).
This separation is the essence of good software design. It produces code that is reusable, testable, and composable. A generic filter works with expenses today and with users tomorrow, because the filtering logic is decoupled from the data type. You can test the filter with simple integers and be confident it works with complex records.
In Chapter 23, we will build on these generic algorithms to implement efficient sorting algorithms (quicksort, mergesort) generically.
20.7 Generics vs. Untyped/Variant Approaches
Before generics were added to Object Pascal, programmers used several workarounds for writing reusable data structures. Let us compare these approaches to understand why generics are the superior choice.
Approach 1: TList with Pointer Casting
The classic TList from the Classes unit stores untyped Pointer values:
uses Classes;
var
List: TList;
P: PInteger;
begin
List := TList.Create;
try
New(P); P^ := 42; List.Add(P);
New(P); P^ := 17; List.Add(P);
{ Reading requires casting — no type safety }
WriteLn(PInteger(List[0])^); { 42 }
{ Nothing prevents this dangerous code: }
WriteLn(PString(List[0])^); { CRASH or garbage — no compiler warning! }
finally
{ Must manually free each pointer }
for var i := 0 to List.Count - 1 do
Dispose(PInteger(List[i]));
List.Free;
end;
end;
Problems: No type safety. Requires manual pointer management. Every read requires a cast. Bugs are invisible at compile time.
Approach 2: Variant Arrays
The Variant type can hold any value:
var
Items: array of Variant;
begin
SetLength(Items, 3);
Items[0] := 42;
Items[1] := 'hello'; { Mixed types — no error! }
Items[2] := 3.14;
{ Type confusion happens silently }
WriteLn(Items[0] + Items[1]); { Runtime error or garbage }
end;
Problems: No type safety. Performance overhead (variants are dynamically typed). Mixed types in the same collection — defeats the purpose of strong typing.
Approach 3: Code Duplication
Write a separate class for each type, as shown in Section 20.1.
Problems: Maintenance burden. Bug fixes must be applied everywhere. Violates DRY.
Approach 4: Generics
type
TIntList = specialize TFPGList<Integer>;
var
List: TIntList;
begin
List := TIntList.Create;
try
List.Add(42);
List.Add(17);
{ List.Add('hello'); — COMPILE ERROR! }
WriteLn(List[0]); { 42 — no cast needed }
finally
List.Free;
end;
end;
Advantages: Full type safety at compile time. No pointer manipulation. No casting. No code duplication. Native performance (the compiler generates specialized code). Clean, readable syntax.
Comparison Table
| Criterion | Pointer/TList | Variant | Duplication | Generics |
|---|---|---|---|---|
| Type safety | None | None | Full | Full |
| Compile-time checks | No | No | Yes | Yes |
| Code reuse | Yes | Yes | No | Yes |
| Performance | Good | Poor (boxing) | Good | Good |
| Memory management | Manual | Automatic | Depends | Depends |
| Readability | Poor (casts) | Moderate | Good | Good |
Generics win on every criterion except one: they require learning the syntax. That is the purpose of this chapter. The syntax investment is modest; the safety and reusability dividends are enormous and compound over the lifetime of every program you write.
A Historical Perspective
The evolution of Pascal's approaches to reusable data structures mirrors the broader evolution of programming language design:
1970s-1980s (Original Pascal): No reuse mechanism at all. You wrote a stack of integers, then copied and modified it for a stack of strings. This was the state of most languages at the time.
1990s (Borland Pascal/Delphi): TList with Pointer casts. Better than code duplication, but type-unsafe. Required manual memory management and casting at every access.
2000s (Delphi 2009, Free Pascal 2.2+): Generics. Full type safety, code reuse, compile-time checking. No runtime overhead. The modern solution.
Each step represents a deeper understanding of the tension between reusability and safety. Generics resolve this tension completely: you get both. It is worth appreciating that this solution took decades to develop and is now available to you in Free Pascal.
📊 Theme 3 (Living Language): Generics were added to Free Pascal in version 2.2 (2007), inspired by the generics in Delphi 2009 and the template system in C++. They represent one of the most significant additions to Object Pascal since the introduction of classes. A language that adds generics is a language that is evolving to meet modern needs — not a museum piece.
Common Pitfalls with Generics
Before moving to dialect differences, let us address some common mistakes that trip up programmers who are new to generics.
Pitfall 1: Assuming operations exist on T. Without a constraint, the only things you can do with T are assignment and comparison for equality (using =). You cannot call methods, access fields, or use arithmetic operators unless you add appropriate constraints or pass comparison functions.
{ WRONG: assumes T has a ToString method }
generic function ItemToString<T>(Item: T): String;
begin
Result := Item.ToString; { COMPILE ERROR: T has no ToString }
end;
{ RIGHT: constrain T to a class (which has ToString) }
generic function ItemToString<T: class>(Item: T): String;
begin
Result := Item.ToString; { OK: TObject has ToString }
end;
Pitfall 2: Forgetting specialize in {$mode objfpc}. Every time you use a generic type concretely, you need the specialize keyword. Forgetting it produces confusing compiler errors:
{ WRONG }
var List: TFPGList<Integer>;
{ RIGHT }
var List: specialize TFPGList<Integer>;
Pitfall 3: Creating generic types inside procedures. In Free Pascal, generic types must be declared at the unit or program level — you cannot define them inside a procedure body:
{ WRONG: generic inside procedure }
procedure Demo;
type
generic TMyList<T> = class { COMPILE ERROR }
end;
begin
end;
{ RIGHT: generic at unit level, specialize anywhere }
type
generic TMyList<T> = class
end;
procedure Demo;
type
TIntList = specialize TMyList<Integer>; { OK }
begin
end;
Pitfall 4: Object ownership confusion with TFPGObjectList. When FreeObjects is True, removing an item from the list frees it. If you are still holding a reference to that object elsewhere, you have a dangling pointer:
var
List: TExpenseList;
Exp: TExpense;
begin
List := TExpenseList.Create;
List.FreeObjects := True;
Exp := TExpense.Create(...);
List.Add(Exp);
List.Delete(0); { Exp is now FREED! }
WriteLn(Exp.Amount); { CRASH: use after free }
end;
The rule: once an object is in a list with FreeObjects := True, the list owns the object. Do not use the object reference after removing it from the list.
When Not to Use Generics
Generics are not always the answer. Here are situations where simpler approaches work better:
- When you only need one type. If your stack only ever holds integers, a
TIntStackwithout generics is simpler and clearer thanspecialize TStack<Integer>. - When the type parameter would be meaningless. A
TDatabaseConnectiondoes not benefit from being generic — it connects to a database, period. - When you need runtime type flexibility. Generics are resolved at compile time. If you need a list that holds mixed types determined at runtime, you need interfaces or a common base class, not generics.
The guideline: use generics when you find yourself writing the same data structure or algorithm for multiple types. If you are writing it for one type, skip the generics.
Generics and Memory Management
An important practical concern is how generics interact with memory management. For value types (integers, records, enumerations), there is no memory management concern — values are copied and destroyed automatically.
For class types, you must decide who owns the objects in a generic collection. There are three common patterns:
Pattern 1: Collection owns the objects. Use TFPGObjectList<T> with FreeObjects := True. When an object is removed from the list or the list is destroyed, the object is freed. This is the simplest pattern and the one you should default to.
Expenses := TExpenseList.Create;
Expenses.FreeObjects := True; { List owns the objects }
Expenses.Add(TExpense.Create(42.50, 'Groceries', 'Shop', Date));
{ When Expenses.Free is called, all TExpense objects are freed too }
Pattern 2: Collection does not own the objects. Use TFPGObjectList<T> with FreeObjects := False, or use TFPGList<T> with pointer types. The caller is responsible for freeing objects. This is necessary when objects are shared between multiple collections.
AllExpenses := TExpenseList.Create;
AllExpenses.FreeObjects := True; { Master list owns objects }
RecentExpenses := TExpenseList.Create;
RecentExpenses.FreeObjects := False; { View — does NOT own objects }
{ RecentExpenses holds references to objects owned by AllExpenses }
Pattern 3: Interface references. Use TFPGList<IExportable> or similar interface-typed lists. Reference counting handles memory management automatically. This is the cleanest approach when your objects are already accessed through interfaces.
type
TExportableList = specialize TFPGList<IExportable>;
var
Items: TExportableList;
begin
Items := TExportableList.Create;
Items.Add(TExpense.Create(42.50, 'Groceries', 'Shop', Date) as IExportable);
{ Reference counting handles cleanup }
Items.Free; { Interface references are released; objects freed when count = 0 }
end;
Choosing the right ownership pattern is one of the most important decisions in Object Pascal programming. Getting it wrong leads to either memory leaks (nobody frees the objects) or use-after-free crashes (one collection frees an object that another collection still references). When in doubt, use Pattern 1 — a single owner with FreeObjects := True.
20.8 Generics in Free Pascal vs. Delphi
Free Pascal supports two syntactic styles for generics, depending on the compiler mode. Since this textbook uses {$mode objfpc}, we have been using the Free Pascal native syntax. Here is a comparison for reference.
Free Pascal Syntax ({$mode objfpc})
type
generic TStack<T> = class
procedure Push(AValue: T);
function Pop: T;
end;
TIntStack = specialize TStack<Integer>;
Key differences:
- The generic keyword is required before the type name.
- The specialize keyword is required when creating concrete types.
- Method implementations use the class name without the type parameter: TStack.Push.
Delphi Syntax ({$mode delphi})
type
TStack<T> = class
procedure Push(AValue: T);
function Pop: T;
end;
TIntStack = TStack<Integer>;
Key differences:
- No generic or specialize keywords.
- Angle brackets alone denote genericity.
- Closer to C#/Java generic syntax.
Which Should You Use?
For new code, either is fine — it is a matter of preference and project conventions. {$mode objfpc}` is the traditional Free Pascal style and makes the generic nature of types explicit with keywords. `{$mode delphi} is more concise and familiar to developers coming from Delphi, C#, or Java.
If you are working on a team or contributing to an existing project, use whatever mode the project already uses. If you are starting a new project, either mode works. This textbook uses {$mode objfpc} consistently.
Compatibility Notes
Most generic features work identically in both modes. The main differences are: - Syntax (keywords vs. bare angle brackets) - Some edge cases in type parameter inference - Method resolution in deeply nested generic types
For the vast majority of code, you can freely translate between the two syntaxes by adding or removing the generic and specialize keywords.
20.9 Project Checkpoint: PennyWise Goes Generic
Time to modernize PennyWise's data management. We will replace the fixed-size array of expenses with a generic TFPGObjectList<TExpense> and add a generic sorting capability.
Step 1: Replace the Array with a Generic List
Previously, PennyWise used:
var
Expenses: array[0..MAX_EXPENSES - 1] of TExpense;
ExpenseCount: Integer;
This has a fixed maximum size, requires manual count tracking, and makes insertion and deletion awkward. The generic alternative:
uses
fgl;
type
TExpense = class
private
FAmount: Currency;
FCategory: String;
FDate: TDateTime;
FDescription: String;
public
constructor Create(AAmount: Currency; const ACategory, ADesc: String;
ADate: TDateTime);
property Amount: Currency read FAmount write FAmount;
property Category: String read FCategory write FCategory;
property Date: TDateTime read FDate write FDate;
property Description: String read FDescription write FDescription;
end;
TExpenseList = specialize TFPGObjectList<TExpense>;
Usage:
var
Expenses: TExpenseList;
begin
Expenses := TExpenseList.Create;
Expenses.FreeObjects := True;
try
Expenses.Add(TExpense.Create(45.99, 'Groceries', 'Weekly shop', Date));
Expenses.Add(TExpense.Create(12.50, 'Transport', 'Bus pass', Date));
WriteLn('Expense count: ', Expenses.Count); { 2 }
WriteLn('First: $', Expenses[0].Amount:0:2); { $45.99 }
{ No more manual count tracking! }
{ No more fixed-size limit! }
{ No more manual memory management for expenses! }
finally
Expenses.Free; { Automatically frees all TExpense objects }
end;
end;
Step 2: Generic Sorting with Custom Comparers
We want to sort expenses by different criteria — date, amount, category — without writing separate sort routines for each:
type
TExpenseCompareFunc = function(const A, B: TExpense): Integer;
function CompareByAmount(const A, B: TExpense): Integer;
begin
if A.Amount < B.Amount then Result := -1
else if A.Amount > B.Amount then Result := 1
else Result := 0;
end;
function CompareByDate(const A, B: TExpense): Integer;
begin
if A.Date < B.Date then Result := -1
else if A.Date > B.Date then Result := 1
else Result := 0;
end;
function CompareByCategory(const A, B: TExpense): Integer;
begin
Result := CompareStr(A.Category, B.Category);
end;
function CompareByAmountDesc(const A, B: TExpense): Integer;
begin
Result := -CompareByAmount(A, B); { Negate for descending }
end;
Step 3: Generic Category Totals with TFPGMap
Instead of looping through all expenses to total up each category, we use a generic map:
type
TCategoryTotals = specialize TFPGMap<String, Currency>;
function CalculateCategoryTotals(Expenses: TExpenseList): TCategoryTotals;
var
i, Idx: Integer;
CurrentTotal: Currency;
begin
Result := TCategoryTotals.Create;
for i := 0 to Expenses.Count - 1 do
begin
Idx := Result.IndexOf(Expenses[i].Category);
if Idx >= 0 then
begin
CurrentTotal := Result.Data[Idx];
Result.Data[Idx] := CurrentTotal + Expenses[i].Amount;
end
else
Result.Add(Expenses[i].Category, Expenses[i].Amount);
end;
end;
Step 4: Putting It Together
procedure DisplayExpenses(Expenses: TExpenseList);
var
i: Integer;
begin
WriteLn(Format(' %-12s %-15s %10s %s',
['Date', 'Category', 'Amount', 'Description']));
WriteLn(' ', StringOfChar('-', 55));
for i := 0 to Expenses.Count - 1 do
WriteLn(Format(' %-12s %-15s $%9.2f %s', [
FormatDateTime('yyyy-mm-dd', Expenses[i].Date),
Expenses[i].Category,
Expenses[i].Amount,
Expenses[i].Description
]));
end;
procedure DisplayCategoryTotals(Expenses: TExpenseList);
var
Totals: TCategoryTotals;
i: Integer;
begin
Totals := CalculateCategoryTotals(Expenses);
try
WriteLn(' Category Totals:');
for i := 0 to Totals.Count - 1 do
WriteLn(Format(' %-15s $%.2f', [Totals.Keys[i], Totals.Data[i]]));
finally
Totals.Free;
end;
end;
What Changed
| Before (Chapters 9-15) | After (Chapter 20) |
|---|---|
| Fixed-size arrays | Dynamic TFPGObjectList<TExpense> — grows as needed |
| Manual count tracking | Expenses.Count managed automatically |
| Manual memory management for each expense | FreeObjects := True frees all on list destruction |
| No sorting | Generic comparer functions for any sort order |
| Manual category totals | TFPGMap<String, Currency> for clean aggregation |
✅ Checkpoint Status: PennyWise now uses generic collections throughout. The expense list is a
TFPGObjectList<TExpense>with automatic memory management. Category totals use aTFPGMap<String, Currency>. Sorting accepts any comparison function. The code is shorter, safer, and more flexible than the array-based version.
What Changed in Rosa's and Tomas's Experience
Rosa immediately notices that her expense list no longer has a maximum size. In the old array-based PennyWise, she once hit the 100-expense limit mid-month and had to delete old entries to add new ones. With TFPGObjectList<TExpense>, the list grows dynamically. She never thinks about capacity again.
Tomas appreciates the sorting. He sorts his expenses by amount to find the biggest spending categories, then by date to see his spending timeline, then by category to group related expenses. Each sort is a single line of code with a different comparison function. He experiments with writing his own comparison functions and discovers that sorting by "category, then by date within category" is just a matter of writing a two-level comparison:
function CompareByCategoryThenDate(const A, B: TExpense): Integer;
begin
Result := CompareStr(A.Category, B.Category);
if Result = 0 then { Same category — break tie by date }
begin
if A.Date < B.Date then Result := -1
else if A.Date > B.Date then Result := 1;
end;
end;
This kind of composable comparison — building complex sorting from simple building blocks — is one of the gifts that generics give to everyday programming.
Generics and the Future of PennyWise
Looking ahead to later chapters, generics will continue to play a central role in PennyWise's architecture:
- Chapter 22 (Recursion): Generic tree structures for category hierarchies.
generic TTreeNode<T>will hold any type in a recursive tree. - Chapter 23 (Sorting): All sorting algorithms will be generic. Quicksort, mergesort, and heapsort will work with any type given a comparison function.
- Chapter 24 (Trees and Graphs): Binary search trees, AVL trees, and graph traversals will all use generic types. A
generic TBinarySearchTree<T>is exactly the kind of reusable data structure that generics were invented for. - Chapter 31 (Database): The repository pattern from Case Study 2 will become the foundation for database-backed persistence.
TRepository<TExpense>backed by SQLite. - Chapter 34 (Serialization): Generic serialization that can convert any
TEntitydescendant to JSON or XML.
The investment in learning generics pays dividends across the entire rest of the textbook and across your entire programming career. Every modern language uses generics (or their equivalent): C++ templates, Java generics, C# generics, Rust generics, Go generics (added in Go 1.18), Swift generics, TypeScript generics. The concept is universal; the Pascal syntax is one dialect of a shared idea.
The TFPGMap<String, Currency> for category totals replaces what used to be a loop with a nested if statement searching through an array. The map's IndexOf and Add methods make the intent clear: "Is this category in the map? If yes, add to its total. If no, create a new entry." The code reads like the algorithm, not like the bookkeeping.
20.10 Summary
Generics are one of the most powerful features in modern Object Pascal. They let you write code once and use it with any type, maintaining full type safety at compile time with zero runtime overhead.
Generic classes use type parameters (T) as placeholders for concrete types. You define the class with generic TClassName<T> and create concrete specializations with specialize TClassName<ConcreteType>.
Generic procedures and functions work the same way — parameterized by type and specialized at the call site.
Type constraints restrict what T can be: T: class (must be a class), T: constructor (must have a parameterless constructor), T: IInterface (must implement an interface), or T: record (must be a value type). Constraints let you use operations on T that require specific capabilities.
Built-in collections in the fgl unit — TFPGList<T>, TFPGObjectList<T>, TFPGMap<K,V> — provide production-ready generic data structures that should be your first choice for most applications.
Generic algorithms — sort, filter, map, search — work with any type when paired with appropriate comparison or transformation functions. They are the foundation of a functional programming style that transfers directly to every modern language.
Compared to untyped approaches, generics win on every criterion: type safety, compile-time checking, code reuse, readability, and maintainability. The only cost is learning the syntax — and you have now done that.
PennyWise has graduated from fixed-size arrays to dynamic generic collections, gaining flexibility, safety, and cleaner code in the process.
In Chapter 21, we push Object Pascal to its limits with operator overloading, class helpers, anonymous functions, and RTTI. These advanced features reveal that Pascal's ceiling is very high indeed — and that the language Niklaus Wirth designed to teach clear thinking has evolved into a language that can express ideas as sophisticated as any in the programming world.
Key Terms Introduced in This Chapter
| Term | Definition |
|---|---|
| Generic | A class, procedure, or function parameterized by one or more types |
| Type parameter | A placeholder type (like T) in a generic declaration, to be replaced with a concrete type at specialization |
| Specialization | Creating a concrete type from a generic by providing specific type arguments |
generic keyword |
Free Pascal keyword marking a type as generic (in {$mode objfpc}) |
specialize keyword |
Free Pascal keyword creating a concrete specialization of a generic type |
| Type constraint | A restriction on what types can be used for a type parameter (class, constructor, record, or an interface) |
| TFPGList |
Free Pascal's generic dynamic list for value types |
| TFPGObjectList |
Free Pascal's generic dynamic list for objects, with optional ownership |
| TFPGMap |
Free Pascal's generic sorted key-value map |
| DRY | "Don't Repeat Yourself" — the principle that every piece of knowledge should have a single, unambiguous representation |
| Reification | The process by which the compiler generates concrete code from a generic template |