Case Study 1: Building a Mathematical Vector Type

The Scenario

Vectors are fundamental in mathematics, physics, and computer graphics. A 2D vector has two components (X and Y) and supports operations like addition, subtraction, scalar multiplication, dot product, magnitude, and normalization. Without operator overloading, vector math is verbose and error-prone:

{ Without operator overloading — tedious and error-prone }
Result.X := A.X + B.X;
Result.Y := A.Y + B.Y;

With operator overloading, the same operation reads like mathematics:

Result := A + B;

This case study builds a complete TVector2D type with operator overloading, demonstrating that Pascal can express mathematical ideas as clearly as any language.

The Implementation

unit MathVector;

{$mode objfpc}{$H+}

interface

uses
  SysUtils, Math;

type
  TVector2D = record
  private
    FX, FY: Double;
  public
    { Constructors }
    class function Create(AX, AY: Double): TVector2D; static;
    class function Zero: TVector2D; static;
    class function UnitX: TVector2D; static;
    class function UnitY: TVector2D; static;

    { Arithmetic operators }
    class operator + (const A, B: TVector2D): TVector2D;
    class operator - (const A, B: TVector2D): TVector2D;
    class operator * (const V: TVector2D; Scalar: Double): TVector2D;
    class operator * (Scalar: Double; const V: TVector2D): TVector2D;
    class operator / (const V: TVector2D; Scalar: Double): TVector2D;
    class operator - (const V: TVector2D): TVector2D;  { Negation }

    { Comparison operators }
    class operator = (const A, B: TVector2D): Boolean;

    { Vector operations }
    function Dot(const Other: TVector2D): Double;
    function Cross(const Other: TVector2D): Double;  { 2D cross = scalar }
    function Magnitude: Double;
    function MagnitudeSquared: Double;
    function Normalized: TVector2D;
    function DistanceTo(const Other: TVector2D): Double;
    function AngleTo(const Other: TVector2D): Double;  { Radians }
    function AngleToDeg(const Other: TVector2D): Double;  { Degrees }
    function Rotated(AngleRad: Double): TVector2D;
    function Lerp(const Target: TVector2D; T: Double): TVector2D;
    function Perpendicular: TVector2D;
    function Reflected(const Normal: TVector2D): TVector2D;

    { Utility }
    function ToString: String;
    function ToStringF(Decimals: Integer): String;
    function IsZero: Boolean;

    { Properties }
    property X: Double read FX write FX;
    property Y: Double read FY write FY;
  end;

{ Convenience constructor }
function Vec(AX, AY: Double): TVector2D;

implementation

function Vec(AX, AY: Double): TVector2D;
begin
  Result := TVector2D.Create(AX, AY);
end;

{ Static constructors }

class function TVector2D.Create(AX, AY: Double): TVector2D;
begin
  Result.FX := AX;
  Result.FY := AY;
end;

class function TVector2D.Zero: TVector2D;
begin
  Result := TVector2D.Create(0, 0);
end;

class function TVector2D.UnitX: TVector2D;
begin
  Result := TVector2D.Create(1, 0);
end;

class function TVector2D.UnitY: TVector2D;
begin
  Result := TVector2D.Create(0, 1);
end;

{ Arithmetic operators }

class operator TVector2D.+(const A, B: TVector2D): TVector2D;
begin
  Result := TVector2D.Create(A.FX + B.FX, A.FY + B.FY);
end;

class operator TVector2D.-(const A, B: TVector2D): TVector2D;
begin
  Result := TVector2D.Create(A.FX - B.FX, A.FY - B.FY);
end;

class operator TVector2D.*(const V: TVector2D; Scalar: Double): TVector2D;
begin
  Result := TVector2D.Create(V.FX * Scalar, V.FY * Scalar);
end;

class operator TVector2D.*(Scalar: Double; const V: TVector2D): TVector2D;
begin
  Result := TVector2D.Create(V.FX * Scalar, V.FY * Scalar);
end;

class operator TVector2D./(const V: TVector2D; Scalar: Double): TVector2D;
begin
  if Scalar = 0 then
    raise Exception.Create('Cannot divide vector by zero');
  Result := TVector2D.Create(V.FX / Scalar, V.FY / Scalar);
end;

class operator TVector2D.-(const V: TVector2D): TVector2D;
begin
  Result := TVector2D.Create(-V.FX, -V.FY);
end;

{ Comparison }

class operator TVector2D.=(const A, B: TVector2D): Boolean;
const
  Epsilon = 1E-10;
begin
  Result := (Abs(A.FX - B.FX) < Epsilon) and (Abs(A.FY - B.FY) < Epsilon);
end;

{ Vector operations }

function TVector2D.Dot(const Other: TVector2D): Double;
begin
  Result := FX * Other.FX + FY * Other.FY;
end;

function TVector2D.Cross(const Other: TVector2D): Double;
begin
  Result := FX * Other.FY - FY * Other.FX;
end;

function TVector2D.Magnitude: Double;
begin
  Result := Sqrt(FX * FX + FY * FY);
end;

function TVector2D.MagnitudeSquared: Double;
begin
  Result := FX * FX + FY * FY;
end;

function TVector2D.Normalized: TVector2D;
var
  Mag: Double;
begin
  Mag := Magnitude;
  if Mag < 1E-10 then
    raise Exception.Create('Cannot normalize zero vector');
  Result := Self / Mag;
end;

function TVector2D.DistanceTo(const Other: TVector2D): Double;
begin
  Result := (Self - Other).Magnitude;
end;

function TVector2D.AngleTo(const Other: TVector2D): Double;
begin
  Result := ArcTan2(Cross(Other), Dot(Other));
end;

function TVector2D.AngleToDeg(const Other: TVector2D): Double;
begin
  Result := RadToDeg(AngleTo(Other));
end;

function TVector2D.Rotated(AngleRad: Double): TVector2D;
var
  CosA, SinA: Double;
begin
  CosA := Cos(AngleRad);
  SinA := Sin(AngleRad);
  Result := TVector2D.Create(FX * CosA - FY * SinA, FX * SinA + FY * CosA);
end;

function TVector2D.Lerp(const Target: TVector2D; T: Double): TVector2D;
begin
  Result := Self + (Target - Self) * T;
end;

function TVector2D.Perpendicular: TVector2D;
begin
  Result := TVector2D.Create(-FY, FX);
end;

function TVector2D.Reflected(const Normal: TVector2D): TVector2D;
var
  N: TVector2D;
begin
  N := Normal.Normalized;
  Result := Self - 2.0 * Self.Dot(N) * N;
end;

function TVector2D.IsZero: Boolean;
begin
  Result := (Abs(FX) < 1E-10) and (Abs(FY) < 1E-10);
end;

function TVector2D.ToString: String;
begin
  Result := Format('(%.2f, %.2f)', [FX, FY]);
end;

function TVector2D.ToStringF(Decimals: Integer): String;
begin
  Result := Format('(%.*f, %.*f)', [Decimals, FX, Decimals, FY]);
end;

end.

Demonstration

program VectorDemo;

{$mode objfpc}{$H+}

uses
  SysUtils, Math, MathVector;

var
  A, B, C, D: TVector2D;
begin
  WriteLn('=== Vector2D with Operator Overloading ===');
  WriteLn;

  A := Vec(3, 4);
  B := Vec(1, 2);

  WriteLn('A = ', A.ToString);
  WriteLn('B = ', B.ToString);
  WriteLn;

  { Arithmetic }
  WriteLn('A + B = ', (A + B).ToString);
  WriteLn('A - B = ', (A - B).ToString);
  WriteLn('A * 2 = ', (A * 2).ToString);
  WriteLn('3 * B = ', (3 * B).ToString);
  WriteLn('-A = ', (-A).ToString);
  WriteLn;

  { Vector operations }
  WriteLn('|A| = ', A.Magnitude:0:4);
  WriteLn('A dot B = ', A.Dot(B):0:4);
  WriteLn('A cross B = ', A.Cross(B):0:4);
  WriteLn('A normalized = ', A.Normalized.ToString);
  WriteLn('Distance A to B = ', A.DistanceTo(B):0:4);
  WriteLn;

  { Rotation }
  C := Vec(1, 0);
  D := C.Rotated(Pi / 4);  { 45 degrees }
  WriteLn('(1,0) rotated 45 deg = ', D.ToStringF(4));
  WriteLn;

  { Linear interpolation }
  WriteLn('Lerp A->B at t=0.0: ', A.Lerp(B, 0.0).ToString);
  WriteLn('Lerp A->B at t=0.5: ', A.Lerp(B, 0.5).ToString);
  WriteLn('Lerp A->B at t=1.0: ', A.Lerp(B, 1.0).ToString);
  WriteLn;

  { The beauty: mathematical notation in Pascal }
  WriteLn('=== Complex expression: (A + B) * 0.5 - C ===');
  C := Vec(1, 1);
  D := (A + B) * 0.5 - C;
  WriteLn('Result = ', D.ToString);

  WriteLn;
  WriteLn('=== This reads like mathematics. That is operator overloading done right. ===');
end.

Design Decisions

Why a Record, Not a Class?

Vectors are small value types — two doubles, 16 bytes. They should be allocated on the stack, passed by value, and compared structurally. Records model this perfectly. Classes would add heap allocation overhead, require Create/Free, and compare by reference instead of by value.

Epsilon Comparison

The = operator uses epsilon comparison (Abs(A.X - B.X) < 1E-10) because floating-point arithmetic is inherently imprecise. Comparing floating-point values with exact = is almost always a bug.

Exception on Zero Division

The / operator and Normalized method raise exceptions for division by zero and normalization of zero vectors, respectively. These are genuine error conditions that should not be silently ignored.

Commutative Scalar Multiplication

We define both V * Scalar and Scalar * V so that both A * 2 and 2 * A work naturally. In mathematics, scalar multiplication is commutative, so our overloaded operator should be too.

Key Takeaways

  1. Operator overloading makes mathematical code natural. (A + B) * 0.5 - C is immediately readable to anyone who knows vector math.
  2. Records are the right choice for small value types. Stack allocation, value semantics, and structural comparison are what vectors need.
  3. Overload sensibly. Every overloaded operator has an obvious mathematical meaning. We did not overload mod or div because those do not make sense for vectors.
  4. Handle edge cases. Division by zero, normalization of zero vectors, and floating-point comparison all require careful handling.