A Necessary Compromise
Bringing modern abstractions into Delphi is usually an exercise in compromise.
There are two concerns to balance.
- Idiomatic: Preserving the semantic clarity of an abstraction without unnecessary ceremony
- Ergonomics: Making the type behave naturally within Object Pascal's own execution model
For example, this is overly complex:
Result := FirstStructuralToken.Match<Boolean>(
function (const t: TToken): Boolean
begin
Result := t.TermKind = aKind;
end,
function: Boolean
begin
Result := False;
end);
Whereas this is a better Delphi compromise:
Result := FirstStructuralToken.TryGetValue(t) and (t.TermKind = aKind);
Adapting Option Types to Delphi's Record Model
When implementing modern types such as TOption<T> in Delphi, the second compromise appears quickly: theoretical purity must give way to Delphi's actual operational model.
Option types are usually simple value abstractions. In languages such as Rust or Swift, they are modeled as algebraic data types with two clear constructors:
- Some(value)
- None
They are immutable and straightforward to reason about.
In Delphi, however, records do not map onto that model quite so directly.
The Record Result Slot
When a Delphi function returns a record, the compiler automatically allocates memory for the Result variable before the function body executes.
That means the result slot already exists before any factory method runs.
This has two implications:
- Record construction cannot always be treated as a pure factory operation
- Returning record values may introduce unnecessary copying if the implementation is not careful
In a language with algebraic types, a constructor would simply build a new value. In Delphi, the Result slot already exists, so construction often becomes initialization of an existing record.
This subtle difference shapes how TOption<T> must be designed.
A Simple Option Type
Conceptually, TOption<T> has two states:
- Some(value)
- None
In Delphi, however, a third internal state is useful: Unknown
It represents a record value that exists but has not yet been initialized as either Some or None.
The internal state machine therefore becomes:
Unknown → Some
Unknown → None
Once initialized, the state cannot change.
Preserving Immutability
To preserve immutability, the internal setters enforce a single-assignment rule:
procedure TOption<T>.SetSome(const aValue: T);
begin
Ensure.IsTrue(fState = osUnknown, 'Option already initialized');
fState := osSome;
fValue := aValue;
end;
SetNone follows the same pattern.
These setters are only valid while the record remains in the Unknown state.
Once the state transitions to Some or None, further mutation is disallowed.
This preserves the immutability of the option type while still allowing efficient in-place initialization.
Factory Methods
Factory methods then become thin wrappers around these setters:
class function TOption<T>.Some(const aValue: T): TOption<T>;
begin
Result.fState := osUnknown;
Result.SetSome(aValue);
end;
class function TOption<T>.None: TOption<T>;
begin
Result.fState := osUnknown;
Result.SetNone;
end;
Because the Result slot already exists, these methods simply initialize it. This avoids unnecessary intermediate construction and keeps the implementation aligned with Delphi's Result semantics.
Making It Delphi-Friendly
A useful improvement comes from giving the Unknown state a clear semantic meaning.
Rather than exposing it, the implementation treats Unknown as equivalent to None when observed.
This means that a default-initialized TOption<T> behaves exactly like None.
In practice, this gives very natural Delphi ergonomics:
function FirstStructuralToken: TOption<TToken>;
begin
// iterate tokens to find structural token
if found then
Result.SetSome(token);
end;
If SetSome is never called, the option simply behaves as None.
No explicit None assignment is required.
The Payoff
The final design has several advantages:
- Efficient record construction with no unnecessary copies
- Immutability through single-assignment setters
- Natural integration with Delphi's Result model
- Safe default values
- Clear Option semantics for callers
Internally, the type has three states. Externally, it behaves like the familiar two-state option abstraction. That is the contract.
A Pattern for Delphi Abstractions
This pattern illustrates an important principle when designing reusable abstractions in Delphi.
Rather than forcing the language to emulate another model exactly, it is often better to adapt the abstraction to Delphi's execution model while preserving its semantic guarantees.
In this case, the additional Unknown state is not a conceptual feature of the option type itself. It exists purely to reconcile option semantics with Delphi's record construction behavior.
The result is an abstraction that remains correct, efficient, and natural to use in Delphi code.