We have come a long way in Part III. Chapter 16 introduced classes and objects. Chapter 17 gave us inheritance and polymorphism. Chapter 18 added interfaces and SOLID design principles. Chapter 19 made our code robust with exception handling...
Learning Objectives
- Overload operators for custom types (arithmetic, comparison, assignment)
- Write class helpers to extend existing classes without inheritance
- Use anonymous functions (closures) for callbacks and functional patterns
- Apply RTTI (Run-Time Type Information) basics
- Write fluent interfaces and method chaining
In This Chapter
- 21.1 Operator Overloading
- 21.2 Class Helpers
- 21.3 Record Helpers
- 21.3 Record Helpers
- 21.4 Anonymous Functions and Closures
- 21.5 Method References and Callbacks
- 21.6 RTTI Basics
- 21.7 Fluent Interfaces and Method Chaining
- 21.8 When to Use These Features: Taste and Restraint
- 21.9 Project Checkpoint: PennyWise Gets Polished
- 21.10 Summary
- Key Terms Introduced in This Chapter
Chapter 21: Advanced Object Pascal: Operator Overloading, Class Helpers, and Anonymous Functions
"Simplicity is prerequisite for reliability." — Edsger W. Dijkstra
We have come a long way in Part III. Chapter 16 introduced classes and objects. Chapter 17 gave us inheritance and polymorphism. Chapter 18 added interfaces and SOLID design principles. Chapter 19 made our code robust with exception handling. Chapter 20 brought generics for type-safe, reusable data structures.
This chapter surveys the features that take Object Pascal from "capable" to "expressive." Operator overloading lets you write A + B for custom types. Class helpers let you add methods to existing classes without touching their source code. Anonymous functions bring closures and functional programming idioms. RTTI lets you inspect types at runtime. Fluent interfaces let you chain method calls into readable sentences.
These are power tools. Like any power tool, they can produce elegant work or dangerous messes depending on how they are used. The chapter's secondary theme — "simplicity is strength" — is not accidental. Every feature we introduce here comes with the warning: use it when it makes code clearer, not when it makes code clever. The goal is readability, not showing off.
By the end, PennyWise will have a TCurrency record with operator overloading (so Rosa can write Total := Subtotal + Tax), string helpers that make date formatting natural, and the architectural taste to know when these features help and when they hurt.
This chapter differs from the others in Part III in an important way: the previous chapters taught features you should use routinely. Classes, interfaces, exceptions, generics — these are the bread and butter of professional Object Pascal. This chapter teaches features you should use selectively. Operator overloading is wonderful for mathematical types but inappropriate for business objects. Class helpers are invaluable for extending types you do not own but can become confusing if overused. Anonymous functions are perfect for short callbacks but should not replace named functions for complex logic. RTTI is essential for frameworks but has no place in application business logic.
The theme that runs through this chapter is judgment. Not every feature should be used at every opportunity. The best code is not the cleverest code — it is the clearest code. Features exist to serve clarity, and when they do not serve clarity, they should be set aside. This is the hardest lesson in programming: knowing when not to use a powerful tool. It takes practice, code review, and the occasional painful refactoring to develop this instinct. But it is the instinct that separates competent programmers from excellent ones.
21.1 Operator Overloading
Operator overloading lets you define what standard operators (+, -, *, /, =, <, >, etc.) mean for your custom types. When the compiler encounters A + B, it looks for an operator + function that matches the types of A and B. If it finds one, it calls your function instead of the built-in addition.
Why Overload Operators?
Consider a type representing money:
type
TCurrency = record
Amount: Currency;
CurrencyCode: String;
end;
Without operator overloading, adding two monetary values looks like this:
Total.Amount := Subtotal.Amount + Tax.Amount;
With operator overloading:
Total := Subtotal + Tax;
The second version is cleaner, more natural, and less error-prone (you cannot accidentally forget to access the Amount field). Operator overloading does not add new functionality — it makes existing functionality more readable.
Operator Overloading Syntax for Records
In Free Pascal, operators are overloaded using the operator keyword inside an advanced record:
type
TCurrency = record
private
FAmount: Currency;
FCode: String;
public
constructor Create(AAmount: Currency; const ACode: String);
class operator + (const A, B: TCurrency): TCurrency;
class operator - (const A, B: TCurrency): TCurrency;
class operator * (const A: TCurrency; Factor: Double): TCurrency;
class operator = (const A, B: TCurrency): Boolean;
class operator < (const A, B: TCurrency): Boolean;
class operator > (const A, B: TCurrency): Boolean;
function ToString: String;
property Amount: Currency read FAmount;
property Code: String read FCode;
end;
Implementing Operators
constructor TCurrency.Create(AAmount: Currency; const ACode: String);
begin
FAmount := AAmount;
FCode := ACode;
end;
class operator TCurrency.+(const A, B: TCurrency): TCurrency;
begin
if A.FCode <> B.FCode then
raise Exception.CreateFmt(
'Cannot add %s and %s: different currencies', [A.FCode, B.FCode]);
Result := TCurrency.Create(A.FAmount + B.FAmount, A.FCode);
end;
class operator TCurrency.-(const A, B: TCurrency): TCurrency;
begin
if A.FCode <> B.FCode then
raise Exception.CreateFmt(
'Cannot subtract %s from %s: different currencies', [B.FCode, A.FCode]);
Result := TCurrency.Create(A.FAmount - B.FAmount, A.FCode);
end;
class operator TCurrency.*(const A: TCurrency; Factor: Double): TCurrency;
begin
Result := TCurrency.Create(A.FAmount * Factor, A.FCode);
end;
class operator TCurrency.=(const A, B: TCurrency): Boolean;
begin
Result := (A.FCode = B.FCode) and (A.FAmount = B.FAmount);
end;
class operator TCurrency.<(const A, B: TCurrency): Boolean;
begin
if A.FCode <> B.FCode then
raise Exception.Create('Cannot compare different currencies');
Result := A.FAmount < B.FAmount;
end;
class operator TCurrency.>(const A, B: TCurrency): Boolean;
begin
if A.FCode <> B.FCode then
raise Exception.Create('Cannot compare different currencies');
Result := A.FAmount > B.FAmount;
end;
function TCurrency.ToString: String;
begin
Result := Format('%s %.2f', [FCode, FAmount]);
end;
Using Overloaded Operators
var
Subtotal, Tax, Total, Budget: TCurrency;
begin
Subtotal := TCurrency.Create(100.00, 'USD');
Tax := TCurrency.Create(8.50, 'USD');
Total := Subtotal + Tax; { Calls operator + }
WriteLn('Total: ', Total.ToString); { USD 108.50 }
Budget := TCurrency.Create(200.00, 'USD');
if Total < Budget then
WriteLn('Within budget!')
else
WriteLn('Over budget!');
end;
The code reads like natural arithmetic. There are no .Amount field accesses scattered through the logic. The type enforces currency safety — trying to add USD and EUR raises an exception.
Overloadable Operators
Free Pascal supports overloading these operators for records and classes:
| Category | Operators |
|---|---|
| Arithmetic | +, -, *, /, **, mod, div |
| Comparison | =, <>, <, >, <=, >= |
| Logical | and, or, xor, not |
| Bitwise | shl, shr |
| Unary | + (unary), - (negation), not |
| Assignment | := (implicit conversion) |
| Indexing | [] (via Enumerator pattern) |
⚠️ Design Warning: Just because you can overload an operator does not mean you should. Operator overloading should make code more readable, not less. Overload
+for aTVector2D(vector addition is intuitive) but do not overload+for aTUser(what does "adding two users" even mean?). If the operator's meaning is not immediately obvious, use a named method instead.
Implicit Conversion with := Operator
You can overload the assignment operator to create implicit conversions between types:
type
TMoney = record
FAmount: Currency;
class operator := (AValue: Currency): TMoney;
class operator := (AValue: Integer): TMoney;
end;
class operator TMoney.:=(AValue: Currency): TMoney;
begin
Result.FAmount := AValue;
end;
class operator TMoney.:=(AValue: Integer): TMoney;
begin
Result.FAmount := AValue;
end;
With these in place, you can write:
var
Price: TMoney;
begin
Price := 42.50; { Implicitly converts Currency to TMoney }
Price := 100; { Implicitly converts Integer to TMoney }
end;
This is convenient but should be used with caution. Implicit conversions can make code harder to understand because the conversion is invisible. Explicit constructor calls like TMoney.Create(42.50) or helper functions like Money(42.50) are often clearer.
⚠️ Implicit Conversion Warning: Be especially careful with implicit conversions in expressions. If
TMoneyimplicitly converts fromInteger, thenPrice := 100 + 50might produce surprising results depending on whether the+is performed on integers (givingTMoney(150)) or onTMoneyvalues (which would require both sides to be converted). When in doubt, be explicit.
Operator Overloading for Comparison
When you overload comparison operators, you should overload them in consistent sets. If you define =, you should also define <>. If you define <, you should also define >, <=, and >=. Inconsistent comparison operators confuse users and can cause bugs in sorting algorithms:
type
TVersion = record
Major, Minor, Patch: Integer;
class operator = (const A, B: TVersion): Boolean;
class operator < (const A, B: TVersion): Boolean;
class operator > (const A, B: TVersion): Boolean;
class operator <= (const A, B: TVersion): Boolean;
class operator >= (const A, B: TVersion): Boolean;
end;
class operator TVersion.=(const A, B: TVersion): Boolean;
begin
Result := (A.Major = B.Major) and (A.Minor = B.Minor) and (A.Patch = B.Patch);
end;
class operator TVersion.<(const A, B: TVersion): Boolean;
begin
if A.Major <> B.Major then Result := A.Major < B.Major
else if A.Minor <> B.Minor then Result := A.Minor < B.Minor
else Result := A.Patch < B.Patch;
end;
class operator TVersion.>(const A, B: TVersion): Boolean;
begin
Result := B < A; { Reuse the < operator }
end;
class operator TVersion.<=(const A, B: TVersion): Boolean;
begin
Result := not (A > B);
end;
class operator TVersion.>=(const A, B: TVersion): Boolean;
begin
Result := not (A < B);
end;
Notice how > is defined in terms of <, and <= and >= are defined in terms of > and <. This ensures consistency — if A < B is True, then B > A is guaranteed to also be True. This pattern of defining comparison operators in terms of each other is a professional best practice.
Overloading String Conversion
One of the most useful operators to overload is the implicit conversion to and from String. This lets your custom types participate naturally in WriteLn and string concatenation:
type
TTemperature = record
private
FDegrees: Double;
FScale: Char; { 'C' or 'F' }
public
constructor Create(ADegrees: Double; AScale: Char);
class operator := (const ATemp: TTemperature): String; { To string }
class operator + (const A, B: TTemperature): TTemperature;
class operator < (const A, B: TTemperature): Boolean;
function ToCelsius: Double;
function ToFahrenheit: Double;
end;
class operator TTemperature.:=(const ATemp: TTemperature): String;
begin
Result := Format('%.1f%s%s', [ATemp.FDegrees, #176, ATemp.FScale]);
end;
With this conversion operator defined, you can write:
var
Temp: TTemperature;
Description: String;
begin
Temp := TTemperature.Create(100.0, 'C');
Description := 'Boiling point: ' + Temp; { Implicit conversion to String }
WriteLn(Description); { Output: Boiling point: 100.0°C }
end;
The implicit string conversion makes TTemperature feel like a first-class type that integrates seamlessly with Pascal's string operations. This is particularly valuable for types that are frequently displayed or logged.
⚠️ Design Warning: Implicit conversions are powerful but can obscure what is actually happening in your code. If a conversion is expensive (involving memory allocation, formatting, or calculation), make it explicit with a
ToStringmethod rather than implicit through an operator. Implicit conversions should be cheap and unsurprising.
The <> Inequality Operator
When you overload the equality operator =, you should also overload the inequality operator <>. Free Pascal does not automatically derive <> from =:
class operator TCurrency.<>(const A, B: TCurrency): Boolean;
begin
Result := not (A = B); { Defined in terms of = for consistency }
end;
This ensures that A <> B is always the exact opposite of A = B. Defining inequality separately (rather than deriving it from equality) could introduce inconsistencies if the two implementations drift apart during maintenance.
Standalone Operator Overloading
In addition to record operators, Free Pascal allows standalone operator functions:
operator + (const A: TCurrency; B: Currency): TCurrency;
begin
Result := TCurrency.Create(A.Amount + B, A.Code);
end;
This lets you write Total := Subtotal + 5.00 where the right operand is a plain Currency value rather than a TCurrency record.
Operator Overloading Best Practices Summary
To consolidate our discussion of operator overloading, here are the key best practices:
-
Only overload operators with obvious meanings. If you have to explain what
+means for your type, use a named method instead. Vector addition, money addition, string concatenation — these are obvious. User concatenation, file merging — these are not. -
Maintain mathematical invariants. If
A + B = C, thenC - Bshould equalA. IfA < B, thenB > A. IfA = BandB = C, thenA = C. These properties (commutativity, transitivity, etc.) are what users expect from operators. -
Handle edge cases explicitly. Division by zero should raise an exception, not return infinity or
NaN. Comparison of incompatible types (USD vs. EUR) should raise an exception, not return a meaningless result. -
Use epsilon comparison for floating-point equality. Never compare
DoubleorExtendedvalues with exact=. Use a small tolerance:Abs(A - B) < Epsilon. -
Overload in complete sets. If you define
<, also define>,<=,>=, and=. Partial operator sets confuse users and break algorithms that expect all comparisons to be available. -
Document the semantics. Add comments explaining what each operator does, especially if the behavior is not pure mathematics (e.g.,
+for strings means concatenation, which is non-commutative).
21.2 Class Helpers
A class helper extends an existing class with new methods, without modifying the class's source code and without using inheritance. This is useful when you want to add functionality to a class you did not write — such as TStringList, TDateTime, or any class from the RTL/FCL.
Syntax
type
TStringHelper = class helper for String
function ToCurrency: Currency;
function ToDateSafe(Default: TDateTime): TDateTime;
function IsNumeric: Boolean;
function Truncate(MaxLen: Integer): String;
end;
Implementation
function TStringHelper.ToCurrency: Currency;
begin
Result := StrToCurr(Self);
end;
function TStringHelper.ToDateSafe(Default: TDateTime): TDateTime;
begin
try
Result := StrToDate(Self);
except
on EConvertError do
Result := Default;
end;
end;
function TStringHelper.IsNumeric: Boolean;
var
Dummy: Double;
begin
Result := TryStrToFloat(Self, Dummy);
end;
function TStringHelper.Truncate(MaxLen: Integer): String;
begin
if Length(Self) <= MaxLen then
Result := Self
else
Result := Copy(Self, 1, MaxLen - 3) + '...';
end;
Using the Helper
var
Input: String;
Amount: Currency;
begin
Input := '42.50';
if Input.IsNumeric then
begin
Amount := Input.ToCurrency;
WriteLn('Amount: $', Amount:0:2);
end;
WriteLn('A very long description text'.Truncate(20));
{ Output: A very long descr... }
end;
The helper methods appear as if they were always part of String. You call them with dot notation on any string variable. This is enormously convenient for utility operations that you use frequently.
How Class Helpers Work
When the compiler encounters Input.IsNumeric, it checks:
1. Does String have a method called IsNumeric? No.
2. Is there a class helper for String in the current scope that has IsNumeric? Yes — TStringHelper.
3. Call TStringHelper.IsNumeric with Self bound to Input.
The compiler resolves helper methods at compile time. There is no runtime overhead — the helper call compiles to a direct function call with the object passed as the first parameter.
Limitations of Class Helpers
- Only one helper per class per scope. If two units each define a helper for
String, only the one in the most recently listed unit is active. You cannot have multiple helpers for the same class simultaneously. - No new fields. Class helpers cannot add data fields to the target class. They can only add methods and properties that read existing state.
-
No virtual methods. Helper methods are resolved statically, not dynamically. They do not participate in polymorphism.
-
Cannot override existing methods. If
Stringalready has aTrimmethod, you cannot redefineTrimin a helper. You can only add new methods. - Scope-dependent visibility. A helper is only active if the unit that declares it is in the
usesclause of the current unit. This can cause confusion when helpers "disappear" because a unit is not listed.
Despite these limitations, class helpers are invaluable for extending types you do not control, and they are used extensively in the Delphi and Lazarus ecosystems.
Real-World Class Helper Use Cases
Beyond the string and date helpers we have seen, here are common real-world uses of class helpers:
Extending TStringList with domain-specific operations:
type
TStringListHelper = class helper for TStringList
procedure LoadFromCSVLine(const Line: String);
function ToCSVLine: String;
function ContainsIgnoreCase(const S: String): Boolean;
end;
function TStringListHelper.ContainsIgnoreCase(const S: String): Boolean;
var
i: Integer;
begin
Result := False;
for i := 0 to Count - 1 do
if SameText(Strings[i], S) then
Exit(True);
end;
Extending Integer with display formatting:
type
TIntegerHelper = type helper for Integer
function ToMoneyStr: String;
function IsEven: Boolean;
function Clamp(AMin, AMax: Integer): Integer;
end;
function TIntegerHelper.Clamp(AMin, AMax: Integer): Integer;
begin
if Self < AMin then Result := AMin
else if Self > AMax then Result := AMax
else Result := Self;
end;
Now you can write UserAge.Clamp(0, 120) instead of Max(0, Min(120, UserAge)) — clearer intent, fewer nested function calls. The helper method reads left-to-right, following the natural direction of English prose, while the nested function calls read inside-out, requiring the reader to start in the middle and work outward.
Organizing Helpers
In a larger project, organize helpers by creating dedicated units:
unit PennyWise.Helpers;
{$mode objfpc}{$H+}
interface
type
TStringHelper = type helper for String
function IsBlank: Boolean;
function ToCurrencySafe(Default: Currency): Currency;
function Capitalize: String;
function IsNumeric: Boolean;
function WordCount: Integer;
end;
TDateTimeHelper = type helper for TDateTime
function ToISO: String;
function ToFriendly: String;
function DaysUntil(Other: TDateTime): Integer;
end;
TCurrencyHelper = type helper for Currency
function ToDisplay: String;
function PercentOf(Total: Currency): Double;
end;
implementation
{ ... implementations ... }
end.
Then any unit that adds PennyWise.Helpers to its uses clause gains all these helper methods on String, TDateTime, and Currency. This is a clean way to provide domain-specific extensions that feel like natural parts of the language.
A common practice in professional Pascal codebases is to have a single ProjectName.Helpers unit that every other unit imports. This gives the entire project a consistent set of utility methods on built-in types.
💡 Coming From C#: Class helpers in Pascal are conceptually similar to C# extension methods. Both let you "add" methods to existing types without inheritance. The main difference is syntax: C# uses
staticmethods withthisparameters; Pascal uses a dedicatedclass helperconstruct.
21.3 Record Helpers
Type Helpers
Free Pascal also supports type helpers — helpers for simple types like Integer, Boolean, Byte, and other ordinal types. These use the type helper syntax:
type
TBooleanHelper = type helper for Boolean
function ToYesNo: String;
function ToOnOff: String;
end;
function TBooleanHelper.ToYesNo: String;
begin
if Self then Result := 'Yes' else Result := 'No';
end;
function TBooleanHelper.ToOnOff: String;
begin
if Self then Result := 'On' else Result := 'Off';
end;
Now you can write:
var
Active: Boolean;
begin
Active := True;
WriteLn('Active: ', Active.ToYesNo); { Active: Yes }
WriteLn('Switch: ', Active.ToOnOff); { Switch: On }
end;
Type helpers and record helpers blur the line between "primitive type" and "object with methods." This is by design — modern Pascal treats even basic types as first-class citizens that can have behaviors attached to them. It is the same philosophy behind extension methods in C#, extension functions in Kotlin, and protocol extensions in Swift.
21.3 Record Helpers
Record helpers work exactly like class helpers but extend record types:
type
TDateTimeHelper = record helper for TDateTime
function ToISO8601: String;
function DaysUntil(Other: TDateTime): Integer;
function IsWeekend: Boolean;
function StartOfMonth: TDateTime;
end;
function TDateTimeHelper.ToISO8601: String;
begin
Result := FormatDateTime('yyyy-mm-dd"T"hh:nn:ss', Self);
end;
function TDateTimeHelper.DaysUntil(Other: TDateTime): Integer;
begin
Result := Trunc(Other - Self);
end;
function TDateTimeHelper.IsWeekend: Boolean;
var
DOW: Integer;
begin
DOW := DayOfWeek(Self);
Result := (DOW = 1) or (DOW = 7); { Sunday = 1, Saturday = 7 }
end;
function TDateTimeHelper.StartOfMonth: TDateTime;
var
Y, M, D: Word;
begin
DecodeDate(Self, Y, M, D);
Result := EncodeDate(Y, M, 1);
end;
Usage is natural and expressive:
var
Today, Deadline: TDateTime;
begin
Today := Now;
Deadline := EncodeDate(2024, 12, 31);
WriteLn('Today: ', Today.ToISO8601);
WriteLn('Days until deadline: ', Today.DaysUntil(Deadline));
WriteLn('Is today a weekend? ', Today.IsWeekend);
WriteLn('Start of month: ', Today.StartOfMonth.ToISO8601);
end;
Record helpers are especially useful for TDateTime, Currency, Integer, Double, and other built-in types where you want domain-specific convenience methods.
Combining with PennyWise
For PennyWise, a Currency helper makes financial formatting natural:
type
TCurrencyHelper = record helper for Currency
function ToDisplay: String;
function IsPositive: Boolean;
function PercentOf(Total: Currency): Double;
end;
function TCurrencyHelper.ToDisplay: String;
begin
Result := Format('$%.2f', [Self]);
end;
function TCurrencyHelper.IsPositive: Boolean;
begin
Result := Self > 0;
end;
function TCurrencyHelper.PercentOf(Total: Currency): Double;
begin
if Total = 0 then
Result := 0
else
Result := (Self / Total) * 100;
end;
Now Rosa can write:
WriteLn('Grocery spending: ', GroceryTotal.ToDisplay);
WriteLn('That is ', GroceryTotal.PercentOf(MonthlyTotal):0:1, '% of total');
21.4 Anonymous Functions and Closures
An anonymous function (also called a lambda or closure) is a function without a name. It is defined inline, right where it is needed, rather than declared separately as a named procedure or function. Anonymous functions are one of the most significant additions to modern Object Pascal, enabling functional programming patterns that were previously awkward to express.
Syntax
In Free Pascal, anonymous functions use the function or procedure keyword with an inline body. You must enable {$modeswitch anonymousfunctions}` or use `{$mode delphi}:
{$mode objfpc}{$H+}
{$modeswitch anonymousfunctions}
type
TIntFunc = function(X: Integer): Integer;
var
Double: TIntFunc;
begin
Double := function(X: Integer): Integer
begin
Result := X * 2;
end;
WriteLn(Double(5)); { 10 }
WriteLn(Double(21)); { 42 }
end;
Anonymous Function Syntax in Detail
The syntax for anonymous functions in Free Pascal deserves careful study because it differs slightly from anonymous functions in other languages.
An anonymous function is a complete function literal. It has a parameter list, a return type (for functions), a begin..end body, and an implicit Result variable (for functions). The difference from a named function is simply that it has no name and is defined at the point of use:
{ Named function }
function IsPositive(X: Integer): Boolean;
begin
Result := X > 0;
end;
{ Anonymous equivalent }
var
Pred: function(X: Integer): Boolean;
begin
Pred := function(X: Integer): Boolean
begin
Result := X > 0;
end;
end;
Both produce identical machine code. The difference is lexical scope: the anonymous version can be defined inside another function and can capture that function's local variables. The named version lives at unit scope and cannot.
Anonymous procedures (which return no value) use the same syntax but with procedure instead of function:
var
Action: reference to procedure(const Msg: String);
begin
Action := procedure(const Msg: String)
begin
WriteLn('[LOG] ', Msg);
end;
Action('Hello'); { Output: [LOG] Hello }
end;
Closures: Capturing Variables
The real power of anonymous functions comes from closures — they can capture variables from their enclosing scope:
function MakeAdder(Base: Integer): TIntFunc;
begin
Result := function(X: Integer): Integer
begin
Result := Base + X; { 'Base' is captured from MakeAdder's scope }
end;
end;
var
Add10, Add42: TIntFunc;
begin
Add10 := MakeAdder(10);
Add42 := MakeAdder(42);
WriteLn(Add10(5)); { 15 }
WriteLn(Add42(5)); { 47 }
end;
MakeAdder returns a function that "remembers" the value of Base even after MakeAdder has returned. The anonymous function closes over the variable Base — hence the name "closure." Each call to MakeAdder creates a new closure with its own captured Base value.
Why Closures Are Powerful
Closures solve a real problem: how do you pass configuration to a callback without global variables or extra parameters? Consider sorting expenses by a threshold that the user specifies:
function MakeThresholdFilter(MinAmount: Currency): TFilterFunc;
begin
Result := function(const E: TExpense): Boolean
begin
Result := E.Amount >= MinAmount; { MinAmount captured from enclosing scope }
end;
end;
{ Usage }
var
Filter50, Filter100: TFilterFunc;
begin
Filter50 := MakeThresholdFilter(50.00);
Filter100 := MakeThresholdFilter(100.00);
{ Both filters can be used independently }
ExpensiveItems := FilterExpenses(AllExpenses, Filter50);
VeryExpensiveItems := FilterExpenses(AllExpenses, Filter100);
end;
Without closures, you would need to either use global variables (fragile), pass extra parameters through the system (verbose), or create an object to hold the threshold (heavy). Closures capture the MinAmount value at creation time, carrying it silently within the function itself. Each closure is self-contained — it knows its threshold without any external state.
This pattern is so common in functional programming that it has a name: currying or partial application. MakeThresholdFilter takes a threshold and returns a specialized filter function. The returned function "remembers" its threshold forever.
Practical Use: Callbacks
Anonymous functions are ideal for callbacks — functions passed to other code that call them at the right time:
type
TPredicate = function(const S: String): Boolean;
procedure FilterStrings(const Arr: array of String; Pred: TPredicate);
var
S: String;
begin
for S in Arr do
if Pred(S) then
WriteLn(' Match: ', S);
end;
var
Words: array[0..5] of String = ('apple', 'banana', 'cherry',
'date', 'elderberry', 'fig');
begin
WriteLn('Long words (>5 chars):');
FilterStrings(Words, function(const S: String): Boolean
begin
Result := Length(S) > 5;
end);
WriteLn('Words starting with "b" or "c":');
FilterStrings(Words, function(const S: String): Boolean
begin
Result := (S[1] = 'b') or (S[1] = 'c');
end);
end;
Without anonymous functions, you would need to define two separate named functions and pass them by address (@LongWords, @StartsWithBC). With anonymous functions, the logic is right at the call site, where it is most readable.
Anonymous Procedures
Anonymous procedures (not functions — procedures that return no value) are equally useful:
type
TActionProc = reference to procedure(const Msg: String);
procedure ExecuteWithLogging(Action: TActionProc; const Label_: String);
begin
WriteLn('[START] ', Label_);
Action(Label_);
WriteLn('[END] ', Label_);
end;
begin
ExecuteWithLogging(
procedure(const Msg: String)
begin
WriteLn(' Performing: ', Msg);
{ Do actual work here }
end,
'Import expenses'
);
end;
Anonymous procedures are common for event handlers, deferred actions, and "execute-around" patterns (where you want to wrap an action with setup and teardown logic).
Limitations of Anonymous Functions in Free Pascal
A few limitations to be aware of:
- **Require
{$modeswitch anonymousfunctions}`** in `{$mode objfpc}. In{$mode delphi}, they are enabled by default. - Cannot capture
varparameters from the enclosing scope. Local variables andconstparameters can be captured, butvarparameters cannot — their lifetime is tied to the calling function, which may have already returned. - Performance: Each anonymous function creates a small object on the heap to hold the captured variables. In tight loops (millions of iterations), this overhead matters. For normal callback usage, it is negligible.
- Debugging: Anonymous functions show up in stack traces as compiler-generated names (like
$anon$1), which can make debugging harder. Named functions have readable names in stack traces.
Despite these limitations, anonymous functions are one of the most useful additions to modern Object Pascal. They bring Pascal in line with the functional features of C#, Java 8+, and Swift while maintaining Pascal's strong typing and explicit style.
Method References vs. Procedure Variables
Free Pascal has three different ways to reference functions, and understanding the differences is important for writing correct callback code.
1. Plain function type — a pointer to a standalone function:
type
TCompareFunc = function(const A, B: Integer): Integer;
function CompareAsc(const A, B: Integer): Integer;
begin Result := A - B; end;
var
Comp: TCompareFunc;
begin
Comp := @CompareAsc; { @ operator required }
WriteLn(Comp(3, 5)); { -2 }
end;
Plain function types can hold only standalone functions (not methods, not closures). They are the simplest and most efficient — just a raw code pointer with no overhead.
2. Method pointer — a pointer to an object method:
type
TNotifyEvent = procedure(Sender: TObject) of object;
procedure TMyForm.HandleClick(Sender: TObject);
begin
WriteLn('Button clicked!');
end;
var
Handler: TNotifyEvent;
begin
Handler := @MyForm.HandleClick; { Stores both code pointer and object reference }
end;
The of object suffix means the type carries two pointers: one to the method code and one to the object instance (Self). This is how event handlers work in Lazarus/Delphi — the button stores a method pointer that knows both what to call and whom to call it on.
3. reference to function — the most flexible, can hold anything:
type
TFilterFunc = reference to function(Value: Integer): Boolean;
A reference to variable can hold a standalone function, a method, or an anonymous function (closure). It is the only type that can hold closures, because closures need a hidden object to store captured variables.
| Type | Holds standalone functions? | Holds methods? | Holds closures? | Overhead |
|---|---|---|---|---|
function(...) |
Yes | No | No | None (raw pointer) |
procedure(...) of object |
No | Yes | No | Small (two pointers) |
reference to function(...) |
Yes | Yes | Yes | Moderate (heap object) |
Rule of thumb: Use plain function types when you only need standalone functions (comparison functions for sorting). Use of object when you need method callbacks (GUI event handlers). Use reference to when you need closures or maximum flexibility.
Free Pascal's reference to Syntax in Detail
Free Pascal supports reference to function types, which can hold named functions, anonymous functions, or methods:
type
TFilterFunc = reference to function(Value: Integer): Boolean;
procedure PrintFiltered(const Arr: array of Integer; Filter: TFilterFunc);
var
V: Integer;
begin
for V in Arr do
if Filter(V) then
Write(V, ' ');
WriteLn;
end;
The reference to syntax is more flexible than plain function types because it can hold closures (anonymous functions with captured variables), whereas plain function types cannot. When you need a callback that might be either a named function or a closure, always use reference to. When you know the callback will always be a plain function (no captured variables), a plain function type is slightly more efficient because it avoids the heap allocation that reference to requires.
💡 When to Use Anonymous Functions: Use them for short, one-off logic that is only meaningful in one place — callbacks, predicates, transformations. If the logic is complex or reused, give it a name. The guideline: if your anonymous function is more than 5-10 lines, extract it into a named function.
21.5 Method References and Callbacks
Building on anonymous functions, let us look at how method references enable flexible callback architectures.
Event Handlers as Method References
In GUI programming (which we will explore in Part V), events are central: a button click, a form resize, a timer tick. Events are implemented as method references:
type
TNotifyEvent = procedure(Sender: TObject) of object;
The of object qualifier means this is a method — it carries both a code pointer and an object reference. When you assign an event handler:
Button.OnClick := @MyForm.HandleButtonClick;
you are storing a reference to HandleButtonClick bound to the specific MyForm instance. When the button is clicked, it calls MyForm.HandleButtonClick(Button).
Callback Pattern Without GUI
Even in console applications, callbacks are useful. Here is a progress callback pattern:
type
TProgressCallback = reference to procedure(Percent: Integer; const Status: String);
procedure ProcessLargeFile(const FileName: String; OnProgress: TProgressCallback);
var
F: TextFile;
Line: String;
LineNum, TotalLines: Integer;
begin
{ Count lines first }
TotalLines := 0;
AssignFile(F, FileName);
Reset(F);
try
while not EOF(F) do begin ReadLn(F, Line); Inc(TotalLines); end;
finally
CloseFile(F);
end;
{ Process with progress }
LineNum := 0;
AssignFile(F, FileName);
Reset(F);
try
while not EOF(F) do
begin
ReadLn(F, Line);
Inc(LineNum);
{ Process the line... }
if (LineNum mod 100 = 0) or (LineNum = TotalLines) then
OnProgress((LineNum * 100) div TotalLines,
Format('Processing line %d of %d', [LineNum, TotalLines]));
end;
finally
CloseFile(F);
end;
end;
Usage with an anonymous callback:
ProcessLargeFile('big_data.csv',
procedure(Percent: Integer; const Status: String)
begin
Write(#13, Status, ' [', Percent, '%]');
end);
The processing function knows nothing about how progress is displayed. The caller provides the display logic as a callback. This is loose coupling at the function level — the same principle we applied at the class level with interfaces in Chapter 18.
21.6 RTTI Basics
Run-Time Type Information (RTTI) allows a program to inspect the types, methods, and properties of objects at runtime. This is also called reflection in other languages.
Basic RTTI: ClassName and InheritsFrom
Every TObject descendant has built-in RTTI:
var
Obj: TObject;
begin
Obj := TStringList.Create;
try
WriteLn('Class name: ', Obj.ClassName); { TStringList }
WriteLn('Is TStringList: ', Obj is TStringList); { True }
WriteLn('Is TObject: ', Obj is TObject); { True }
WriteLn('Is TComponent: ', Obj is TComponent); { False }
WriteLn('Inherits from TStrings: ',
Obj.InheritsFrom(TStrings)); { True }
finally
Obj.Free;
end;
end;
TypInfo Unit: Inspecting Published Properties
The TypInfo unit provides functions for inspecting published properties at runtime:
uses
TypInfo;
procedure InspectObject(Obj: TObject);
var
PropList: PPropList;
PropCount, i: Integer;
PropInfo: PPropInfo;
begin
PropCount := GetPropList(Obj, PropList);
try
WriteLn('Properties of ', Obj.ClassName, ':');
for i := 0 to PropCount - 1 do
begin
PropInfo := PropList^[i];
WriteLn(Format(' %s: %s = %s', [
PropInfo^.Name,
PropInfo^.PropType^.Name,
GetPropValue(Obj, PropInfo^.Name, True)
]));
end;
finally
FreeMem(PropList);
end;
end;
This function can inspect any object and list its published properties without knowing the object's class at compile time. This is the foundation of visual form designers (like Lazarus's Form Designer), serialization frameworks, and object-relational mappers.
Practical RTTI Example: Simple Object Serialization
Here is a practical example that uses RTTI to serialize any object's published properties to a key-value format:
uses
TypInfo;
function ObjectToString(Obj: TObject): String;
var
PropList: PPropList;
PropCount, i: Integer;
PropInfo: PPropInfo;
PropName, PropValue: String;
begin
Result := Obj.ClassName + '{';
PropCount := GetPropList(Obj, PropList);
try
for i := 0 to PropCount - 1 do
begin
PropInfo := PropList^[i];
PropName := PropInfo^.Name;
PropValue := GetPropValue(Obj, PropName, True);
if i > 0 then Result := Result + ', ';
Result := Result + PropName + '=' + PropValue;
end;
finally
FreeMem(PropList);
end;
Result := Result + '}';
end;
This function works with any object — it does not need to know the class at compile time. You could pass it a TExpense, a TBudget, a TUser, or any custom class, and it will enumerate and display all published properties. This is the kind of generic utility that RTTI enables.
When to Use RTTI
RTTI is powerful but should be used judiciously:
- Framework code: Serialization, persistence, visual designers — code that needs to work with arbitrary objects.
- Plugin systems: Loading and inspecting plugins whose types are not known at compile time.
- Debugging and logging: Printing object state for diagnostic purposes.
- Configuration systems: Mapping configuration files to object properties by name.
In normal application code, prefer compile-time type checking (strong typing, generics, interfaces) over runtime type inspection. RTTI bypasses the compiler's safety checks, which means errors that would normally be caught at compile time become runtime errors instead.
The hierarchy of preference for type flexibility: 1. Generics — Known at compile time, fully type-safe, zero overhead. 2. Interfaces — Known contract, runtime flexibility, type-safe through the interface. 3. Polymorphism — Virtual method dispatch, type-safe through the class hierarchy. 4. RTTI — Maximum flexibility, minimum safety, runtime overhead.
Choose the highest-numbered option only when the lower-numbered ones are insufficient. Most application code never needs RTTI.
⚠️ Performance Note: RTTI operations are slower than direct property access because they involve runtime lookups. Use RTTI for framework-level code, not in tight loops.
Enumerator Pattern for Custom Collections
While not technically an anonymous function feature, the enumerator pattern works beautifully with the functional style. You can make any custom collection support for..in by implementing GetEnumerator:
type
TExpenseListEnumerator = class
private
FList: TExpenseList;
FIndex: Integer;
public
constructor Create(AList: TExpenseList);
function MoveNext: Boolean;
function GetCurrent: TExpense;
property Current: TExpense read GetCurrent;
end;
constructor TExpenseListEnumerator.Create(AList: TExpenseList);
begin
FList := AList;
FIndex := -1;
end;
function TExpenseListEnumerator.MoveNext: Boolean;
begin
Inc(FIndex);
Result := FIndex < FList.Count;
end;
function TExpenseListEnumerator.GetCurrent: TExpense;
begin
Result := FList[FIndex];
end;
With this enumerator, you can write:
for Expense in MyExpenseList do
WriteLn(Expense.Description, ': $', Expense.Amount:0:2);
This is cleaner than manual index-based loops and works naturally with the functional patterns we are building. The for..in loop delegates iteration to the enumerator, so the collection controls how its items are traversed.
21.7 Fluent Interfaces and Method Chaining
A fluent interface is a design pattern where methods return Self (the object they are called on), enabling method calls to be chained into a single statement. This produces code that reads like a sentence.
Example: A Query Builder
type
TQueryBuilder = class
private
FTable: String;
FConditions: String;
FOrderBy: String;
FLimit: Integer;
public
constructor Create;
function From(const ATable: String): TQueryBuilder;
function Where(const ACondition: String): TQueryBuilder;
function OrderBy(const AField: String): TQueryBuilder;
function LimitTo(ACount: Integer): TQueryBuilder;
function Build: String;
end;
constructor TQueryBuilder.Create;
begin
inherited Create;
FLimit := -1;
end;
function TQueryBuilder.From(const ATable: String): TQueryBuilder;
begin
FTable := ATable;
Result := Self; { Return Self for chaining }
end;
function TQueryBuilder.Where(const ACondition: String): TQueryBuilder;
begin
if FConditions = '' then
FConditions := ACondition
else
FConditions := FConditions + ' AND ' + ACondition;
Result := Self;
end;
function TQueryBuilder.OrderBy(const AField: String): TQueryBuilder;
begin
FOrderBy := AField;
Result := Self;
end;
function TQueryBuilder.LimitTo(ACount: Integer): TQueryBuilder;
begin
FLimit := ACount;
Result := Self;
end;
function TQueryBuilder.Build: String;
begin
Result := 'SELECT * FROM ' + FTable;
if FConditions <> '' then
Result := Result + ' WHERE ' + FConditions;
if FOrderBy <> '' then
Result := Result + ' ORDER BY ' + FOrderBy;
if FLimit > 0 then
Result := Result + ' LIMIT ' + IntToStr(FLimit);
end;
Using the Fluent Interface
var
Query: TQueryBuilder;
SQL: String;
begin
Query := TQueryBuilder.Create;
try
SQL := Query
.From('expenses')
.Where('amount > 50')
.Where('category = ''Groceries''')
.OrderBy('date DESC')
.LimitTo(10)
.Build;
WriteLn(SQL);
finally
Query.Free;
end;
end;
Output: SELECT * FROM expenses WHERE amount > 50 AND category = 'Groceries' ORDER BY date DESC LIMIT 10
Compare this with the non-fluent equivalent:
Query.From('expenses');
Query.Where('amount > 50');
Query.Where('category = ''Groceries''');
Query.OrderBy('date DESC');
Query.LimitTo(10);
SQL := Query.Build;
The fluent version eliminates the repetitive Query. prefix and reads as a single expression rather than six separate statements.
Guidelines for Fluent Interfaces
- Return
Selffrom configuration methods. Methods that configure the object (setters, builders) returnSelf. Methods that produce a result (likeBuild) return the result. - Keep method names short and verb-like.
From,Where,OrderBy,LimitTo— each reads like a command. - Use fluent interfaces for builders and configuration. They work well when you are setting up an object before using it. They work less well for general-purpose classes where the calling order does not form a natural sentence.
- Consider the debugger. Fluent chains are harder to debug than separate statements because you cannot set a breakpoint on a single call within the chain. If you need to debug a fluent chain, temporarily break it into separate statements.
- Document the expected call order. If methods must be called in a specific order (e.g.,
FrombeforeWhere), document this clearly. Better yet, use the type system to enforce order — return different types from each method so thatWherecan only be called afterFrom.
A PennyWise Fluent Example: The Report Builder
Here is a more complete fluent builder for PennyWise reports that demonstrates several of these patterns together:
type
TReportBuilder = class
private
FTitle: String;
FDateFrom: TDateTime;
FDateTo: TDateTime;
FCategories: TStringList;
FMinAmount: Currency;
FSortField: String;
FSortDescending: Boolean;
public
constructor Create;
destructor Destroy; override;
function WithTitle(const ATitle: String): TReportBuilder;
function FromDate(ADate: TDateTime): TReportBuilder;
function ToDate(ADate: TDateTime): TReportBuilder;
function InCategory(const ACategory: String): TReportBuilder;
function MinimumAmount(AAmount: Currency): TReportBuilder;
function SortBy(const AField: String; Descending: Boolean = False): TReportBuilder;
function Generate: String;
end;
Usage:
var
Report: String;
Builder: TReportBuilder;
begin
Builder := TReportBuilder.Create;
try
Report := Builder
.WithTitle('Q1 Expense Summary')
.FromDate(EncodeDate(2024, 1, 1))
.ToDate(EncodeDate(2024, 3, 31))
.InCategory('Groceries')
.InCategory('Dining')
.MinimumAmount(10.00)
.SortBy('Amount', True)
.Generate;
WriteLn(Report);
finally
Builder.Free;
end;
end;
This reads almost like English: "Generate a report with title 'Q1 Expense Summary', from January 1 to March 31, in categories Groceries and Dining, with minimum amount $10, sorted by amount descending." The intent is unmistakable, and each method call is self-documenting.
21.8 When to Use These Features: Taste and Restraint
Every feature in this chapter adds expressiveness to Object Pascal. Every feature can also be misused. Here is a guide to when each feature helps and when it hurts.
Operator Overloading
Use when: The operator has an obvious, universally understood meaning for the type. Addition for vectors, money, matrices. Comparison for dates, versions, measurements. Multiplication of a vector by a scalar.
Avoid when: The operator's meaning is ambiguous. What does User1 + User2 mean? Merge their accounts? Concatenate their names? If you have to think about it, use a named method like MergeAccounts instead.
Rule of thumb: If someone reading A + B without context would correctly guess what it does, overload the operator. If they would be confused, use a named method.
Class/Record Helpers
Use when: You frequently need a utility operation on a type you did not write. String formatting helpers, date helpers, numeric conversion helpers. They keep the calling code clean and discover-friendly (IDE autocomplete shows helper methods).
Avoid when: The helper method is complex or has side effects. Helpers should feel like natural extensions of the type, not like full-featured classes hiding behind dot notation. Also avoid when you control the type's source code — just add the method to the type directly.
Rule of thumb: If the method feels like it "should have been there all along," it is a good helper. If it feels like a separate concern being attached, it belongs in its own class.
Anonymous Functions
Use when: The function is short (one to five lines), used exactly once, and most readable at the point of use. Predicates for filtering, comparison functions for sorting, callbacks for progress reporting.
Avoid when: The function is long, complex, or reused in multiple places. Named functions are easier to debug (stack traces show names), easier to test (you can call them independently), and easier to read (a good name is worth a hundred lines of inline code).
Rule of thumb: If you would struggle to name the function because it is so context-specific, use an anonymous function. If the name would be obvious and useful, give it a name.
RTTI
Use when: You are writing framework code that must work with unknown types: serialization, persistence, plugin loading, visual designers. These are legitimate uses that justify the runtime overhead and loss of compile-time safety.
Avoid when: You are writing application logic. If you find yourself using ClassName and InheritsFrom in business code, you probably need a better class hierarchy, an interface, or a generic. RTTI in application code usually signals a design problem.
Fluent Interfaces
Use when: You are building a configuration or construction API where calls naturally chain into a sentence. Builders, queries, configuration objects.
Avoid when: The calling order does not form a natural sequence, or when returning Self obscures whether a method succeeded or failed. Not every class benefits from method chaining.
📊 The Taste Test: For every feature in this chapter, ask: "Does this make the code easier to read for someone who has never seen it before?" If yes, use it. If it makes the code shorter but harder to understand, do not use it. Brevity is not the same as clarity. Pascal was designed for clarity.
The Spectrum of Expressiveness
Think of these features as a spectrum from "safe and simple" to "powerful and dangerous":
Simple Powerful
|------|--------|----------|-----------|------------|
Named Record Class Anonymous RTTI
methods helpers helpers functions reflection
On the left, named methods — the safest, most predictable, most debuggable. On the right, RTTI — the most flexible, least type-safe, hardest to debug. Each step rightward gives you more expressive power at the cost of some safety and clarity.
Professional programming is about choosing the right point on this spectrum for each situation. Most code should live on the left. Framework code occasionally needs the right. The mark of an experienced developer is not using the most advanced features — it is knowing when not to use them.
Consider the following progression for filtering expenses:
{ Level 1: Named function — simplest, most readable }
function IsGrocery(E: TExpense): Boolean;
begin
Result := E.Category = 'Groceries';
end;
Filtered := FilterExpenses(Expenses, @IsGrocery);
{ Level 2: Anonymous function — inline, contextual }
Filtered := FilterExpenses(Expenses,
function(E: TExpense): Boolean begin Result := E.Category = 'Groceries'; end);
{ Level 3: Closure capturing a variable — flexible }
Category := ReadInput;
Filtered := FilterExpenses(Expenses,
function(E: TExpense): Boolean begin Result := E.Category = Category; end);
Level 1 is appropriate when the filter is reused or when readability matters most. Level 2 is appropriate when the filter is used once and is short. Level 3 is appropriate when the filter depends on runtime data. Each level adds power but also complexity. Choose the simplest level that meets your needs.
Theme 6: Simplicity is Strength
This chapter might seem to contradict Theme 6 — "The simplicity of Pascal is a strength" — by introducing complex features. But the theme is not about avoiding complexity. It is about managing complexity. Operator overloading makes code simpler when the operators have obvious meanings. Class helpers make code simpler by eliminating boilerplate. Anonymous functions make code simpler when the alternative is a forest of named functions used exactly once.
The features in this chapter, used wisely, increase simplicity by letting you express intent more directly. The key word is "wisely." A TCurrency type with overloaded + is simpler than writing .Amount everywhere. A String helper .IsBlank is simpler than Trim(S) = ''. An anonymous filter is simpler than a separate named function that is used once and never referenced again.
Simplicity is not about using fewer features. It is about using the right features to make the code say what it means.
21.9 Project Checkpoint: PennyWise Gets Polished
Let us apply the features from this chapter to give PennyWise a polished, professional feel.
Step 1: TCurrency Record with Operator Overloading
type
TMoney = record
private
FAmount: Currency;
public
class operator + (const A, B: TMoney): TMoney;
class operator - (const A, B: TMoney): TMoney;
class operator * (const A: TMoney; Factor: Double): TMoney;
class operator = (const A, B: TMoney): Boolean;
class operator < (const A, B: TMoney): Boolean;
class operator > (const A, B: TMoney): Boolean;
class operator <= (const A, B: TMoney): Boolean;
class operator >= (const A, B: TMoney): Boolean;
function ToString: String;
function IsPositive: Boolean;
function IsZero: Boolean;
property Amount: Currency read FAmount write FAmount;
end;
function Money(AAmount: Currency): TMoney;
begin
Result.FAmount := AAmount;
end;
Now PennyWise calculations read naturally:
var
Groceries, Transport, Total, Budget, Remaining: TMoney;
begin
Groceries := Money(45.99);
Transport := Money(12.50);
Total := Groceries + Transport;
Budget := Money(200.00);
Remaining := Budget - Total;
WriteLn('Total: ', Total.ToString);
WriteLn('Remaining: ', Remaining.ToString);
if Remaining.IsPositive then
WriteLn('Under budget!')
else
WriteLn('Over budget!');
end;
Step 2: String Helpers for PennyWise
type
TPennyWiseStringHelper = type helper for String
function ToCurrencySafe(Default: Currency): Currency;
function ToDateSafe(Default: TDateTime): TDateTime;
function IsBlank: Boolean;
function Capitalize: String;
end;
function TPennyWiseStringHelper.ToCurrencySafe(Default: Currency): Currency;
begin
if not TryStrToCurr(Self, Result) then
Result := Default;
end;
function TPennyWiseStringHelper.ToDateSafe(Default: TDateTime): TDateTime;
begin
try
Result := StrToDate(Self);
except
Result := Default;
end;
end;
function TPennyWiseStringHelper.IsBlank: Boolean;
begin
Result := Trim(Self) = '';
end;
function TPennyWiseStringHelper.Capitalize: String;
begin
if Length(Self) = 0 then
Result := ''
else
Result := UpCase(Self[1]) + LowerCase(Copy(Self, 2, Length(Self) - 1));
end;
Now input handling is clean:
var
Input: String;
Amount: TMoney;
begin
ReadLn(Input);
if Input.IsBlank then
WriteLn('Please enter an amount')
else
Amount := Money(Input.ToCurrencySafe(0));
end;
Step 3: Fluent Expense Builder
type
TExpenseBuilder = class
private
FAmount: Currency;
FCategory: String;
FDate: TDateTime;
FDescription: String;
public
constructor Create;
function WithAmount(AAmount: Currency): TExpenseBuilder;
function InCategory(const ACategory: String): TExpenseBuilder;
function OnDate(ADate: TDateTime): TExpenseBuilder;
function Described(const ADesc: String): TExpenseBuilder;
function Build: TExpense;
end;
function TExpenseBuilder.WithAmount(AAmount: Currency): TExpenseBuilder;
begin
FAmount := AAmount;
Result := Self;
end;
{ ... other methods follow the same pattern ... }
Building expenses becomes expressive:
Expense := TExpenseBuilder.Create
.WithAmount(45.99)
.InCategory('Groceries')
.OnDate(Date)
.Described('Weekly shop')
.Build;
This reads like English: "Create an expense with amount 45.99 in category Groceries on today's date described as 'Weekly shop'." The intent is unmistakable.
✅ Checkpoint Status: PennyWise now has
TMoneywith operator overloading (natural arithmetic), string helpers for safe input parsing, and a fluentTExpenseBuilderfor clean expense creation. The code reads like it was written by someone who cares about the next person who will maintain it — because it was.
21.10 Summary
This chapter surveyed the advanced features that make Object Pascal an expressive, modern language while honoring its heritage of clarity and simplicity.
Operator overloading lets you define +, -, *, <, >, and other operators for custom types. Use it when the operator's meaning is universally obvious (vectors, money, matrices). Avoid it when the meaning is ambiguous.
Class helpers and record helpers extend existing types with new methods, without modifying source code or using inheritance. They are ideal for utility methods on types you do not control (String, TDateTime, Currency). They are limited to one active helper per type per scope.
Anonymous functions and closures enable inline function definitions that capture variables from their enclosing scope. Use them for short, one-off callbacks and predicates. Use named functions for anything complex or reused.
RTTI (Run-Time Type Information) allows runtime inspection of types, methods, and properties. Reserve it for framework code (serialization, plugin systems, form designers). In application code, prefer compile-time type safety.
Fluent interfaces use method chaining (returning Self) to produce readable, sentence-like code. They work well for builders and configuration APIs.
The overarching principle is taste and restraint. Every feature in this chapter can make code more readable — or less readable. The deciding factor is not whether you can use a feature, but whether it makes the code clearer for the next person who reads it. Pascal was designed for clarity. These advanced features, used wisely, amplify that clarity.
What Rosa and Tomas Take Away
Rosa's PennyWise now uses TMoney throughout. Her budget calculations — which used to involve careful .Amount field access on every line — now read like a spreadsheet: Remaining := Budget - Groceries - Transport - Dining. When she accidentally tries to add her USD expenses to a EUR receipt from a trip abroad, the operator raises an exception rather than silently producing garbage. Type safety through operator overloading is something she never knew she needed until she had it.
Tomas, always the experimentalist, goes on a helper-writing spree. He adds .Reverse and .IsPalindrome to strings. He adds .IsPrime to integers. He adds .IsLeapYear to date-time values. Most of these are useful; some are exercises in curiosity. But the practice teaches him something important: the language is not fixed. When a method should exist on a type but does not, you can add it. The language grows to fit your domain.
Both Rosa and Tomas use the fluent TExpenseBuilder to create expenses. Rosa finds it especially useful when she batch-enters her monthly business expenses: the builder's validation catches her typos immediately, and the fluent syntax means she can create an expense in one readable statement rather than five separate assignments.
The anonymous functions take the longest for both of them to internalize. Rosa finds them useful for one-off sorts ("sort by amount descending" as an inline closure rather than a named function she uses once and never again). Tomas eventually uses them in a progress callback when his import function processes large CSV files. "It is like passing a tiny program as an argument to another program," he says. That is exactly right.
Looking Back at Part III
With Chapter 21, we conclude Part III: Object Pascal. We entered Part III writing procedural code with records and arrays. We leave it with classes, interfaces, generics, exceptions, operator overloading, anonymous functions, and a solid understanding of SOLID design principles. PennyWise has been transformed from a flat procedural program into a well-architected object-oriented application.
The transformation was not just technical — it was conceptual. We learned to think in terms of contracts (interfaces), hierarchies (inheritance), capabilities (multiple interface implementation), safety (exceptions), reusability (generics), and expressiveness (operators, helpers, lambdas). These are not Pascal-specific skills. They are software engineering skills that translate to every object-oriented language you will ever encounter.
Part IV will take this foundation and build on it with algorithms and data structures — recursion, sorting, trees, graphs, and optimization. The disciplined thinking that Pascal has been teaching you will serve you well there.
Key Terms Introduced in This Chapter
| Term | Definition |
|---|---|
| Operator overloading | Defining the behavior of standard operators (+, -, <, etc.) for custom types |
| Advanced record | A record type that supports methods, properties, operators, and access modifiers |
| Class helper | A construct that adds new methods to an existing class without modifying its source or using inheritance |
| Record helper | A construct that adds new methods to an existing record type |
| Anonymous function | A function defined inline without a name, often used as a callback or predicate |
| Closure | An anonymous function that captures variables from its enclosing scope |
| Method reference | A variable that can hold a reference to a named function, method, or anonymous function |
reference to |
A type modifier that allows a function variable to hold closures |
| RTTI | Run-Time Type Information — the ability to inspect types, methods, and properties at runtime |
| TypInfo unit | Free Pascal unit providing functions for RTTI introspection |
| Fluent interface | A design pattern where methods return Self to enable method chaining |
| Method chaining | Calling multiple methods in sequence on the same object in a single expression |
| Builder pattern | A pattern where a builder object accumulates configuration via method calls and produces a result via a Build method |