Making Generic HashSet More User-Friendly in Free Pascal
Documentation home · Current hash-set guide
Historical design record. This document captures an earlier constructor design discussion and may not match the current public API in every detail.
Audience: contributors researching the evolution of the hash-set API.
The Challenge#
We wanted to make a generic TThreadSafeHashSet<T> that would be easy to use with common types (Integer, String, Boolean, Real) while maintaining type safety and proper generic support.
Evolution of the Solution#
1. First Attempt: Generic Constructor with RTTI#
We tried using RTTI (Run Time Type Information) to automatically select the right comparers:
constructor TThreadSafeHashSet.Create;
var
TypeData: PTypeInfo;
begin
TypeData := TypeInfo(T); // Get the type info for T - WORKS
case TypeData^.Kind of // Check the type kind - WORKS
tkInteger: // If it's an integer - WORKS
Create(@IntegerEquals, @IntegerHash); // Use the integer comparer and hash function - DID NOT WORK!
tkString:
Create(@StringEquals, @StringHash);
// ...
end;
end;Problem: Type compatibility issues with generic function types.
2. Second Attempt: Static Class Functions#
We tried using static class functions to create specialized instances:
type
generic TThreadSafeHashSet<T> = class
class static function CreateInteger: specialize TThreadSafeHashSet<Integer>;
class static function CreateString: specialize TThreadSafeHashSet<string>;
// ...
end;Problem: Static methods were a workaround and didn't follow proper OOP patterns.
3. Third Attempt: Type-Specific Constructors#
We tried adding constructors directly to the generic class:
type
generic TThreadSafeHashSet<T> = class
constructor CreateInteger;
constructor CreateString;
// ...
end;Problem: Can't mix generic and specific types in the same class definition.
4. Final Solution: Specialized Derived Classes#
The working solution uses derived classes for each specific type:
type
// Base generic class
generic TThreadSafeHashSet<T> = class
constructor Create(AEqualityComparer: specialize TEqualityComparer<T>;
AHashFunction: specialize THashFunction<T>;
AInitialCapacity: Integer = INITIAL_BUCKET_COUNT);
// ...
end;
// Specialized classes with simple constructors
TThreadSafeHashSetInteger = class(specialize TThreadSafeHashSet<Integer>)
constructor Create(AInitialCapacity: Integer = INITIAL_BUCKET_COUNT); overload;
end;
TThreadSafeHashSetString = class(specialize TThreadSafeHashSet<string>)
constructor Create(AInitialCapacity: Integer = INITIAL_BUCKET_COUNT); overload;
constructor Create(AHashFunction: specialize THashFunction<string>;
AInitialCapacity: Integer = INITIAL_BUCKET_COUNT); overload;
end;Why This Works#
- Type Safety: Each specialized class is a concrete type with its own constructor
- Inheritance: Proper use of OOP inheritance patterns
- Encapsulation: Hides the complexity of comparer and hash function setup
- User-Friendly: Simple
Createconstructor for common types - Maintainable: Clear separation between generic and specialized code
Usage Examples#
// Simple usage with integers
var
Numbers: TThreadSafeHashSetInteger;
begin
Numbers := TThreadSafeHashSetInteger.Create;
try
Numbers.Add(42);
// ...
finally
Numbers.Free;
end;
end;
// Custom type usage still possible with generic version
type
TMyRecord = record
// ...
end;
var
CustomSet: specialize TThreadSafeHashSet<TMyRecord>;
begin
CustomSet := specialize TThreadSafeHashSet<TMyRecord>.Create(@MyRecordEquals, @MyRecordHash);
try
// ...
finally
CustomSet.Free;
end;
end;Key Learnings#
- Free Pascal's generic system requires careful type matching
- Inheritance can solve problems that generics alone cannot
- Creating specialized classes is better than type aliases for adding functionality
- RTTI-based solutions can be problematic with generics
- Following OOP principles leads to cleaner solutions