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

  1. Functional patterns work in Pascal. Map, filter, and reduce are not tied to "functional languages" — they are universal programming patterns.
  2. 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.
  3. Composition produces readable pipelines. Chain filter-map-reduce operations to build complex queries from simple, testable steps.
  4. 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.
  5. Pascal's verbosity is its clarity. The specialize keywords 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.