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
- Operator overloading makes mathematical code natural.
(A + B) * 0.5 - Cis immediately readable to anyone who knows vector math. - Records are the right choice for small value types. Stack allocation, value semantics, and structural comparison are what vectors need.
- Overload sensibly. Every overloaded operator has an obvious mathematical meaning. We did not overload
modordivbecause those do not make sense for vectors. - Handle edge cases. Division by zero, normalization of zero vectors, and floating-point comparison all require careful handling.