Case Study 2: Functional Programming in Pascal
The Scenario
Functional programming emphasizes three core operations on collections: map (transform each element), filter (select elements matching a criterion), and reduce (combine all elements into a single result). These operations, combined with anonymous functions, produce code that is concise, expressive, and composable.
Pascal is not traditionally considered a functional language, but with anonymous functions (closures), generics, and method references, it can express functional patterns naturally. This case study builds a small functional toolkit and demonstrates its use with PennyWise data.
The Functional Toolkit
Type Definitions
unit FuncTools;
{$mode objfpc}{$H+}
{$modeswitch anonymousfunctions}
interface
uses
SysUtils;
type
{ Function types for functional operations }
generic TMapFunc<TIn, TOut> = reference to function(const Item: TIn): TOut;
generic TFilterFunc<T> = reference to function(const Item: T): Boolean;
generic TReduceFunc<T, TAcc> = reference to function(const Acc: TAcc; const Item: T): TAcc;
generic TForEachProc<T> = reference to procedure(const Item: T);
{ Map: Transform each element }
generic function Map<TIn, TOut>(
const Arr: array of TIn;
Func: specialize TMapFunc<TIn, TOut>
): specialize TArray<TOut>;
{ Filter: Select elements matching a predicate }
generic function Filter<T>(
const Arr: array of T;
Pred: specialize TFilterFunc<T>
): specialize TArray<T>;
{ Reduce: Combine all elements into a single value }
generic function Reduce<T, TAcc>(
const Arr: array of T;
Func: specialize TReduceFunc<T, TAcc>;
Initial: TAcc
): TAcc;
{ ForEach: Execute a procedure on each element }
generic procedure ForEach<T>(
const Arr: array of T;
Proc: specialize TForEachProc<T>
);
implementation
generic function Map<TIn, TOut>(
const Arr: array of TIn;
Func: specialize TMapFunc<TIn, TOut>
): specialize TArray<TOut>;
var
i: Integer;
begin
SetLength(Result, Length(Arr));
for i := 0 to High(Arr) do
Result[i] := Func(Arr[i]);
end;
generic function Filter<T>(
const Arr: array of T;
Pred: specialize TFilterFunc<T>
): specialize TArray<T>;
var
i, Count: Integer;
begin
SetLength(Result, Length(Arr));
Count := 0;
for i := 0 to High(Arr) do
if Pred(Arr[i]) then
begin
Result[Count] := Arr[i];
Inc(Count);
end;
SetLength(Result, Count);
end;
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;
generic procedure ForEach<T>(
const Arr: array of T;
Proc: specialize TForEachProc<T>
);
var
i: Integer;
begin
for i := 0 to High(Arr) do
Proc(Arr[i]);
end;
end.
Using the Toolkit: Basic Examples
Map: Transform Elements
{ Double every number }
var
Numbers: array[0..4] of Integer = (1, 2, 3, 4, 5);
Doubled: specialize TArray<Integer>;
begin
Doubled := specialize Map<Integer, Integer>(Numbers,
function(const X: Integer): Integer
begin
Result := X * 2;
end);
{ Doubled = [2, 4, 6, 8, 10] }
end;
{ Convert numbers to strings }
var
Labels: specialize TArray<String>;
begin
Labels := specialize Map<Integer, String>(Numbers,
function(const X: Integer): String
begin
Result := 'Item #' + IntToStr(X);
end);
{ Labels = ['Item #1', 'Item #2', ...] }
end;
Filter: Select Elements
{ Keep only even numbers }
var
Evens: specialize TArray<Integer>;
begin
Evens := specialize Filter<Integer>(Numbers,
function(const X: Integer): Boolean
begin
Result := X mod 2 = 0;
end);
{ Evens = [2, 4] }
end;
Reduce: Aggregate
{ Sum all numbers }
var
Sum: Integer;
begin
Sum := specialize Reduce<Integer, Integer>(Numbers,
function(const Acc: Integer; const X: Integer): Integer
begin
Result := Acc + X;
end, 0);
{ Sum = 15 }
end;
{ Find maximum }
var
Max: Integer;
begin
Max := specialize Reduce<Integer, Integer>(Numbers,
function(const Acc: Integer; const X: Integer): Integer
begin
if X > Acc then Result := X else Result := Acc;
end, Numbers[0]);
{ Max = 5 }
end;
PennyWise Application
Expense Data
type
TExpenseRec = record
Date: TDateTime;
Category: String;
Amount: Currency;
Description: String;
end;
var
Expenses: array[0..7] of TExpenseRec;
{ Populated with sample data... }
Functional Queries
{ Total spending }
TotalSpending := specialize Reduce<TExpenseRec, Currency>(Expenses,
function(const Acc: Currency; const E: TExpenseRec): Currency
begin
Result := Acc + E.Amount;
end, 0);
WriteLn('Total: $', TotalSpending:0:2);
{ Only grocery expenses }
GroceryExpenses := specialize Filter<TExpenseRec>(Expenses,
function(const E: TExpenseRec): Boolean
begin
Result := E.Category = 'Groceries';
end);
{ Grocery total }
GroceryTotal := specialize Reduce<TExpenseRec, Currency>(GroceryExpenses,
function(const Acc: Currency; const E: TExpenseRec): Currency
begin
Result := Acc + E.Amount;
end, 0);
WriteLn('Groceries: $', GroceryTotal:0:2);
{ Descriptions of expensive items }
ExpensiveDescriptions := specialize Map<TExpenseRec, String>(
specialize Filter<TExpenseRec>(Expenses,
function(const E: TExpenseRec): Boolean
begin
Result := E.Amount > 50;
end),
function(const E: TExpenseRec): String
begin
Result := Format('%s ($%.2f)', [E.Description, E.Amount]);
end);
{ Print them }
WriteLn('Expensive items:');
specialize ForEach<String>(ExpensiveDescriptions,
procedure(const S: String)
begin
WriteLn(' - ', S);
end);
Composing Operations (Pipeline Style)
The real power of functional programming emerges when you compose operations. Here is a "pipeline" that filters, transforms, and aggregates in a readable sequence:
{ Average amount of dining expenses }
var
DiningExpenses: specialize TArray<TExpenseRec>;
DiningAmounts: specialize TArray<Currency>;
DiningTotal: Currency;
DiningAverage: Currency;
begin
{ Step 1: Filter to dining only }
DiningExpenses := specialize Filter<TExpenseRec>(Expenses,
function(const E: TExpenseRec): Boolean
begin
Result := E.Category = 'Dining';
end);
{ Step 2: Extract amounts }
DiningAmounts := specialize Map<TExpenseRec, Currency>(DiningExpenses,
function(const E: TExpenseRec): Currency
begin
Result := E.Amount;
end);
{ Step 3: Sum }
DiningTotal := specialize Reduce<Currency, Currency>(DiningAmounts,
function(const Acc: Currency; const A: Currency): Currency
begin
Result := Acc + A;
end, 0);
{ Step 4: Average }
if Length(DiningAmounts) > 0 then
DiningAverage := DiningTotal / Length(DiningAmounts)
else
DiningAverage := 0;
WriteLn(Format('Dining: %d expenses, avg $%.2f',
[Length(DiningAmounts), DiningAverage]));
end;
Each step is a pure transformation with no side effects. The pipeline reads from top to bottom: filter, extract, sum, average.
Comparison with Imperative Style
Imperative (traditional Pascal)
GroceryTotal := 0;
GroceryCount := 0;
for i := 0 to High(Expenses) do
begin
if Expenses[i].Category = 'Groceries' then
begin
GroceryTotal := GroceryTotal + Expenses[i].Amount;
Inc(GroceryCount);
end;
end;
if GroceryCount > 0 then
GroceryAvg := GroceryTotal / GroceryCount;
Functional
GroceryExps := Filter(Expenses, IsGrocery);
GroceryAmts := Map(GroceryExps, GetAmount);
GroceryTotal := Reduce(GroceryAmts, Sum, 0);
GroceryAvg := GroceryTotal / Length(GroceryAmts);
The functional version separates each concern into a distinct step. The imperative version mixes filtering, accumulation, and counting in a single loop. For simple cases, the imperative version is fine. For complex queries with multiple filters and transformations, the functional pipeline scales better in readability.
Language Comparison
| Feature | Pascal | Python | JavaScript | Java |
|---|---|---|---|---|
| Map | specialize Map<T,U>(arr, func) |
list(map(func, lst)) |
arr.map(func) |
stream.map(func) |
| Filter | specialize Filter<T>(arr, pred) |
list(filter(pred, lst)) |
arr.filter(pred) |
stream.filter(pred) |
| Reduce | specialize Reduce<T,A>(arr, func, init) |
functools.reduce(func, lst, init) |
arr.reduce(func, init) |
stream.reduce(init, func) |
| Lambda syntax | function(X: Integer): Integer begin ... end |
lambda x: x * 2 |
x => x * 2 |
x -> x * 2 |
Pascal's syntax is more verbose (explicit types, begin..end blocks), but the concepts are identical. Once you understand map/filter/reduce in Pascal, you understand them in every language.
Key Takeaways
- Functional patterns work in Pascal. Map, filter, and reduce are not tied to "functional languages" — they are universal programming patterns.
- Anonymous functions enable inline logic. Without anonymous functions, you would need to define dozens of named predicates and transformers. With them, the logic lives at the call site.
- Composition produces readable pipelines. Chain filter-map-reduce operations to build complex queries from simple, testable steps.
- Know both styles. Imperative loops and functional pipelines are both valid. Use the style that makes the intent clearest for each situation. Simple aggregations? A loop is fine. Multi-step transformations with filtering? Functional pipelines often win.
- Pascal's verbosity is its clarity. The
specializekeywords and explicit types are more characters than Python or JavaScript, but they make the types and operations unambiguous — which is what Pascal has always prioritized.