28 min read

> "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)

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 fgl collections 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 TIntStack without generics is simpler and clearer than specialize TStack<Integer>.
  • When the type parameter would be meaningless. A TDatabaseConnection does 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 a TFPGMap<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 TEntity descendant 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