Skip to main content

Pattern Matching — TMatch<T>

Unit: ModernSyntax.Match

TMatch<T> is a fluent, composable pattern-matching record for Delphi. It replaces nested if-else chains and limited case statements with expressive, type-safe branches that can produce side effects (TProc) or return values (TFunc<TValue>).

Entry point

uses ModernSyntax.Match;

TMatch<Integer>.Value(LScore)
// ... add cases ...
.Execute;

TMatch<T>.Value(AValue) starts a match session on AValue. The static method is the canonical constructor (the Create constructor is also accessible but Value is preferred for readability).

Case types

CaseEq — equality

TMatch<string>.Value(LStatus)
.CaseEq('active', procedure begin WriteLn('Active'); end)
.CaseEq('inactive', procedure begin WriteLn('Inactive'); end)
.Default(procedure begin WriteLn('Unknown status'); end)
.Execute;

CaseGt / CaseLt — comparison

TMatch<Integer>.Value(LAge)
.CaseLt(18, procedure begin WriteLn('Minor'); end)
.CaseGt(64, procedure begin WriteLn('Senior'); end)
.Default(procedure begin WriteLn('Adult'); end)
.Execute;

CaseRange — inclusive range

TMatch<Integer>.Value(LScore)
.CaseRange(90, 100, procedure begin WriteLn('A'); end)
.CaseRange(75, 89, procedure begin WriteLn('B'); end)
.CaseRange(60, 74, procedure begin WriteLn('C'); end)
.Default(procedure begin WriteLn('F'); end)
.Execute;

CaseIn — set/array membership

// Array membership
TMatch<string>.Value(LDay)
.CaseIn(['Saturday', 'Sunday'], procedure begin WriteLn('Weekend'); end)
.Default(procedure begin WriteLn('Weekday'); end)
.Execute;

// Char set
TMatch<Char>.Value(LChar)
.CaseIn(['a', 'e', 'i', 'o', 'u'], procedure begin WriteLn('Vowel'); end)
.Default(procedure begin WriteLn('Consonant'); end)
.Execute;

CaseIn also accepts TSysCharSet and TIntegerSet overloads.

CaseIs — type dispatch

Matches based on the runtime type kind of T. Useful for TObject hierarchies or variant values:

TMatch<TValue>.Value(LValue)
.CaseIs<Integer>(procedure(const V: Integer) begin WriteLn('Integer: ', V); end)
.CaseIs<string>(procedure(const V: string) begin WriteLn('String: ', V); end)
.Default(procedure begin WriteLn('Other type'); end)
.Execute;

TDateTime is special-cased to tkFloat internally.

CaseIf — conditional guard

Guards run before the case evaluation. A guard that evaluates to False skips the entire match block:

TMatch<Integer>.Value(LValue)
.CaseIf(LEnabled) // guard: skip all if not enabled
.CaseGt(0, procedure begin WriteLn('Positive'); end)
.Default(procedure begin WriteLn('Non-positive'); end)
.Execute;

CaseRegex — regular expression

TMatch<string>.Value(LEmail)
.CaseRegex(LEmail, '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
.Default(procedure begin WriteLn('Invalid or valid email'); end)
.Execute;

CaseRegex(AInput, APattern) takes exactly two string arguments — the text to test and the regex pattern. It does not accept a callback; it increments an internal counter when the pattern matches (a positive count allows Execute to proceed). Pair it with a Default or other cases to handle both matched and unmatched outcomes.

Note: CaseRegex is implemented via TModernRegEx.IsMatch from ModernSyntax.RegExpression. The regex match increments/decrements an internal counter; a negative count means no regex matched.

Returning a value — Execute<R>

When cases use TFunc<TValue> instead of TProc, call Execute<R> to get a typed result:

var LResult: TResultPair<TValue, string>;
begin
LResult := TMatch<Integer>.Value(LInput)
.CaseEq(1, function(const V: Integer): TValue begin Result := TValue.From('One'); end)
.CaseEq(2, function(const V: Integer): TValue begin Result := TValue.From('Two'); end)
.Default(function(const V: Integer): TValue begin Result := TValue.From('Other'); end)
.Execute<TValue>;

if LResult.IsSuccess then
WriteLn(LResult.ValueSuccess.AsString);
end;

Execute (without type parameter) returns TResultPair<Boolean, String>True on a successful proc match.

Combining matches

Combine merges two independent TMatch<T> instances so both are evaluated:

var
LMatch1, LMatch2: TMatch<Integer>;
begin
LMatch1 := TMatch<Integer>.Value(LValue)
.CaseGt(10, procedure begin WriteLn('>10'); end);

LMatch2 := TMatch<Integer>.Value(LValue)
.CaseLt(100, procedure begin WriteLn('<100'); end);

LMatch1.Combine(LMatch2).Execute;
end;

Exception handling in a match

TryExcept registers a fallback proc that runs if any case raises an exception:

TMatch<Integer>.Value(LValue)
.CaseEq(0, procedure begin raise Exception.Create('Oops'); end)
.TryExcept(procedure begin WriteLn('Exception caught inside match'); end)
.Execute;

Session state machine

TMatch<T> enforces a legal call sequence through an internal TMatchSession state:

StateMeaning
sMatchInitial state after Value()
sGuardAfter a CaseIf that evaluated to True
sCaseAfter any CaseEq, CaseGt, etc.
sDefaultAfter Default()
sTryExceptAfter TryExcept()

Execute / Execute<R> is only valid from sCase, sDefault, or sTryExcept.