Recent from talks
Contribute something
Nothing was collected or created yet.
Generic programming
View on Wikipedia
Generic programming is a style of computer programming in which algorithms are written in terms of data types to-be-specified-later that are then instantiated when needed for specific types provided as parameters. This approach, pioneered in the programming language ML in 1973,[1][2] permits writing common functions or data types that differ only in the set of types on which they operate when used, thus reducing duplicate code.
Generic programming was introduced to the mainstream with Ada in 1977. With templates in C++, generic programming became part of the repertoire of professional library design. The techniques were further improved and parameterized types were introduced in the influential 1994 book Design Patterns.[3]
New techniques were introduced by Andrei Alexandrescu in his 2001 book Modern C++ Design: Generic Programming and Design Patterns Applied. Subsequently, D implemented the same ideas.
Such software entities are known as generics in Ada, C#, Delphi, Eiffel, F#, Java, Nim, Python, Go, Rust, Swift, TypeScript, and Visual Basic (.NET). They are known as parametric polymorphism in ML, Scala, Julia, and Haskell. (Haskell terminology also uses the term generic for a related but somewhat different concept.)
The term generic programming was originally coined by David Musser and Alexander Stepanov[4] in a more specific sense than the above, to describe a programming paradigm in which fundamental requirements on data types are abstracted from across concrete examples of algorithms and data structures and formalized as concepts, with generic functions implemented in terms of these concepts, typically using language genericity mechanisms as described above.
Stepanov–Musser and other generic programming paradigms
[edit]Generic programming is defined in Musser & Stepanov (1989) as follows,
Generic programming centers around the idea of abstracting from concrete, efficient algorithms to obtain generic algorithms that can be combined with different data representations to produce a wide variety of useful software.
— Musser, David R.; Stepanov, Alexander A., Generic Programming[5]
The "generic programming" paradigm is an approach to software decomposition whereby fundamental requirements on types are abstracted from across concrete examples of algorithms and data structures and formalized as concepts, analogously to the abstraction of algebraic theories in abstract algebra.[6] Early examples of this programming approach were implemented in Scheme and Ada,[7] although the best known example is the Standard Template Library (STL),[8][9] which developed a theory of iterators that is used to decouple sequence data structures and the algorithms operating on them.
For example, given N sequence data structures, e.g. singly linked list, vector etc., and M algorithms to operate on them, e.g. find, sort etc., a direct approach would implement each algorithm specifically for each data structure, giving N × M combinations to implement. However, in the generic programming approach, each data structure returns a model of an iterator concept (a simple value type that can be dereferenced to retrieve the current value, or changed to point to another value in the sequence) and each algorithm is instead written generically with arguments of such iterators, e.g. a pair of iterators pointing to the beginning and end of the subsequence or range to process. Thus, only N + M data structure-algorithm combinations need be implemented. Several iterator concepts are specified in the STL, each a refinement of more restrictive concepts e.g. forward iterators only provide movement to the next value in a sequence (e.g. suitable for a singly linked list or a stream of input data), whereas a random-access iterator also provides direct constant-time access to any element of the sequence (e.g. suitable for a vector). An important point is that a data structure will return a model of the most general concept that can be implemented efficiently—computational complexity requirements are explicitly part of the concept definition. This limits the data structures a given algorithm can be applied to and such complexity requirements are a major determinant of data structure choice. Generic programming similarly has been applied in other domains, e.g. graph algorithms.[10]
Although this approach often uses language features of compile-time genericity and templates, it is independent of particular language-technical details. Generic programming pioneer Alexander Stepanov wrote,
Generic programming is about abstracting and classifying algorithms and data structures. It gets its inspiration from Knuth and not from type theory. Its goal is the incremental construction of systematic catalogs of useful, efficient and abstract algorithms and data structures. Such an undertaking is still a dream.
I believe that iterator theories are as central to Computer Science as theories of rings or Banach spaces are central to Mathematics.
— Alexander Stepanov, An Interview with A. Stepanov[13]
Bjarne Stroustrup noted,
Following Stepanov, we can define generic programming without mentioning language features: Lift algorithms and data structures from concrete examples to their most general and abstract form.
— Bjarne Stroustrup, Evolving a language in and for the real world: C++ 1991-2006[12]
Other programming paradigms that have been described as generic programming include Datatype generic programming as described in "Generic Programming – an Introduction".[14] The Scrap your boilerplate approach is a lightweight generic programming approach for Haskell.[15]
In this article we distinguish the high-level programming paradigms of generic programming, above, from the lower-level programming language genericity mechanisms used to implement them (see Programming language support for genericity). For further discussion and comparison of generic programming paradigms, see.[16]
Programming language support for genericity
[edit]Genericity facilities have existed in high-level languages since at least the 1970s in languages such as ML, CLU and Ada, and were subsequently adopted by many object-based and object-oriented languages, including BETA, C++, D, Eiffel, Java, and DEC's now defunct Trellis-Owl.
Genericity is implemented and supported differently in various programming languages; the term "generic" has also been used differently in various programming contexts. For example, in Forth the compiler can execute code while compiling and one can create new compiler keywords and new implementations for those words on the fly. It has few words that expose the compiler behaviour and therefore naturally offers genericity capacities that, however, are not referred to as such in most Forth texts. Similarly, dynamically typed languages, especially interpreted ones, usually offer genericity by default as both passing values to functions and value assignment are type-indifferent and such behavior is often used for abstraction or code terseness, however this is not typically labeled genericity as it's a direct consequence of the dynamic typing system employed by the language.[citation needed] The term has been used in functional programming, specifically in Haskell-like languages, which use a structural type system where types are always parametric and the actual code on those types is generic. These uses still serve a similar purpose of code-saving and rendering an abstraction.
Arrays and structs can be viewed as predefined generic types. Every usage of an array or struct type instantiates a new concrete type, or reuses a previous instantiated type. Array element types and struct element types are parameterized types, which are used to instantiate the corresponding generic type. All this is usually built-in in the compiler and the syntax differs from other generic constructs. Some extensible programming languages try to unify built-in and user defined generic types.
A broad survey of genericity mechanisms in programming languages follows. For a specific survey comparing suitability of mechanisms for generic programming, see.[17]
In object-oriented languages
[edit]When creating container classes in statically typed languages, it is inconvenient to write specific implementations for each datatype contained, especially if the code for each datatype is virtually identical. For example, in C++, this duplication of code can be circumvented by defining a class template:
class Animal {
// ...
};
class Car {
// ...
};
template <typename T>
class MyList {
// Class contents.
};
MyList<Animal> animalList;
MyList<Car> carList;
Above, T is a placeholder for whatever type is specified when the list is created. These "containers-of-type-T", commonly called templates, allow a class to be reused with different datatypes as long as certain contracts such as subtypes and signature are kept. This genericity mechanism should not be confused with inclusion polymorphism, which is the algorithmic usage of exchangeable sub-classes: for instance, a list of objects of type Moving_Object containing objects of type Animal and Car. Templates can also be used for type-independent functions as in the Swap example below:
import std;
using std::string;
// A similar, but safer and potentially faster function
// is defined in the standard library header <utility>
template <typename T>
void swap(T& a, T& b) noexcept {
T temp = b;
b = a;
a = temp;
}
int main(int argc, char* argv[]) {
string world = "World!";
string hello = "Hello,";
swap(world, hello);
std::println("{} {}", world, hello); // Output is "Hello, World!".
}
The C++ template construct used above is widely cited[citation needed] as the genericity construct that popularized the notion among programmers and language designers and supports many generic programming idioms. The D language also offers fully generic-capable templates based on the C++ precedent but with a simplified syntax. The Java language has provided genericity facilities syntactically based on C++'s since the introduction of Java Platform, Standard Edition (J2SE) 5.0.
C# 2.0, Oxygene 1.5 (formerly Chrome) and Visual Basic (.NET) 2005 have constructs that exploit the support for generics present in Microsoft .NET Framework since version 2.0.
Generics in Ada
[edit]Ada has had generics since it was first designed in 1977–1980. The standard library uses generics to provide many services. Ada 2005 adds a comprehensive generic container library to the standard library, which was inspired by C++'s Standard Template Library.[18][19]
A generic unit is a package or a subprogram that takes one or more generic formal parameters.[20]
A generic formal parameter is a value, a variable, a constant, a type, a subprogram, or even an instance of another, designated, generic unit. For generic formal types, the syntax distinguishes between discrete, floating-point, fixed-point, access (pointer) types, etc. Some formal parameters can have default values.[21]
To instantiate a generic unit, the programmer passes actual parameters for each formal. The generic instance then behaves just like any other unit. It is possible to instantiate generic units at run-time, for example inside a loop.[21]
Example
[edit]The specification of a generic package:
generic
Max_Size : Natural; -- a generic formal value
type Element_Type is private; -- a generic formal type; accepts any nonlimited type
package Stacks is
type Size_Type is range 0 .. Max_Size;
type Stack is limited private;
procedure Create (S : out Stack;
Initial_Size : in Size_Type := Max_Size);
procedure Push (Into : in out Stack; Element : in Element_Type);
procedure Pop (From : in out Stack; Element : out Element_Type);
Overflow : exception;
Underflow : exception;
private
subtype Index_Type is Size_Type range 1 .. Max_Size;
type Vector is array (Index_Type range <>) of Element_Type;
type Stack (Allocated_Size : Size_Type := 0) is record
Top : Index_Type;
Storage : Vector (1 .. Allocated_Size);
end record;
end Stacks;
Instantiating the generic package:
type Bookmark_Type is new Natural;
-- records a location in the text document we are editing
package Bookmark_Stacks is new Stacks (Max_Size => 20,
Element_Type => Bookmark_Type);
-- Allows the user to jump between recorded locations in a document
Using an instance of a generic package:
type Document_Type is record
Contents : Ada.Strings.Unbounded.Unbounded_String;
Bookmarks : Bookmark_Stacks.Stack;
end record;
procedure Edit (Document_Name : in String) is
Document : Document_Type;
begin
-- Initialise the stack of bookmarks:
Bookmark_Stacks.Create (S => Document.Bookmarks, Initial_Size => 10);
-- Now, open the file Document_Name and read it in...
end Edit;
Advantages and limits
[edit]The language syntax allows precise specification of constraints on generic formal parameters. For example, it is possible to specify that a generic formal type will only accept a modular type as the actual. It is also possible to express constraints between generic formal parameters; for example:
generic
type Index_Type is (<>); -- must be a discrete type
type Element_Type is private; -- can be any nonlimited type
type Array_Type is array (Index_Type range <>) of Element_Type;
In this example, Array_Type is constrained by both Index_Type and Element_Type. When instantiating the unit, the programmer must pass an actual array type that satisfies these constraints.
The disadvantage of this fine-grained control is a complicated syntax, but, because all generic formal parameters are completely defined in the specification, the compiler can instantiate generics without looking at the body of the generic.
Unlike C++, Ada does not allow specialised generic instances, and requires that all generics be instantiated explicitly. These rules have several consequences:
- the compiler can implement shared generics: the object code for a generic unit can be shared between all instances (unless the programmer requests inlining of subprograms, of course). As further consequences:
- there is no possibility of code bloat (code bloat is common in C++ and requires special care, as explained below).
- it is possible to instantiate generics at run-time, and at compile time, since no new object code is required for a new instance.
- actual objects corresponding to a generic formal object are always considered to be non-static inside the generic; see Generic formal objects in the Wikibook for details and consequences.
- all instances of a generic being exactly the same, it is easier to review and understand programs written by others; there are no "special cases" to take into account.
- all instantiations being explicit, there are no hidden instantiations that might make it difficult to understand the program.
- Ada does not permit arbitrary computation at compile time, because operations on generic arguments are performed at runtime.
Templates in C++
[edit]C++ uses templates to enable generic programming techniques. The C++ Standard Library includes the Standard Template Library or STL that provides a framework of templates for common data structures and algorithms. Templates in C++ may also be used for template metaprogramming, which is a way of pre-evaluating some of the code at compile-time rather than run-time. Using template specialization, C++ Templates are Turing complete.
Technical overview
[edit]There are many kinds of templates, the most common being function templates and class templates. A function template is a pattern for creating ordinary functions based upon the parameterizing types supplied when instantiated. For example, the C++ Standard Template Library contains the function template std::max(x, y) that creates functions that return either x or y, whichever is larger. max() could be defined like this:
template <typename T>
[[nodiscard]]
constexpr T max(T x, T y) noexcept {
return x < y ? y : x;
}
Specializations of this function template, instantiations with specific types, can be called just like an ordinary function:
std::println("{}", max(3, 7)); // Outputs 7.
The compiler examines the arguments used to call max and determines that this is a call to max(int, int). It then instantiates a version of the function where the parameterizing type T is int, making the equivalent of the following function:
[[nodiscard]]
constexpr int max(int x, int y) noexcept {
return x < y ? y : x;
}
This works whether the arguments x and y are integers, strings, or any other type for which the expression x < y is sensible, or more specifically, for any type for which operator< is defined. Common inheritance is not needed for the set of types that can be used, and so it is very similar to duck typing. A program defining a custom data type can use operator overloading to define the meaning of < for that type, thus allowing its use with the std::max() function template. While this may seem a minor benefit in this isolated example, in the context of a comprehensive library like the STL it allows the programmer to get extensive functionality for a new data type, just by defining a few operators for it. Merely defining < allows a type to be used with the standard std::sort(), std::stable_sort(), and std::binary_search() algorithms or to be put inside data structures such as std::sets (equivalent to TreeSet, heaps, and associative arrays.
C++ templates are completely type safe at compile time. As a demonstration, the standard type std::complex does not define the < operator, because there is no strict order on complex numbers. Therefore, std::max(x, y) will fail with a compile error, if x and y are complex values. Likewise, other templates that rely on < cannot be applied to complex data unless a comparison (in the form of a functor or function) is provided. For instance, std::complex cannot be used as key for a std::map (equivalent to TreeMap) unless a comparison is provided. Unfortunately, compilers historically generate somewhat esoteric, long, and unhelpful error messages for this sort of error. Ensuring that a certain object adheres to a method protocol can alleviate this issue. Languages which use std::compare instead of < can also use std::complex values as keys.
Another kind of template, a class template, extends the same concept to classes. A class template specialization is a class. Class templates are often used to make generic containers. For example, the STL has a doubly linked list container, std::list (equivalent to LinkedList). To make a linked list of integers, one writes std::list<int>. A list of strings is denoted std::list<std::string>. A std::list has a set of standard functions associated with it, that work for any compatible parameterizing types.
C++20 introduces constraining template types using concepts. Constraining the max() could look something like this:
import std;
using std::totally_ordered;
// in typename declaration:
template <totally_ordered T>
[[nodiscard]]
constexpr T max(T x, T y) noexcept {
return x < y ? y : x;
}
// in requires clause:
template <typename T>
requires totally_ordered<T>
[[nodiscard]]
constexpr T max(T x, T y) noexcept {
return x < y ? y : x;
}
Template specialization
[edit]A powerful feature of C++'s templates is template specialization. This allows alternative implementations to be provided based on certain characteristics of the parameterized type that is being instantiated. Template specialization has two purposes: to allow certain forms of optimization, and to reduce code bloat.
For example, consider a sort() template function. One of the primary activities that such a function does is to swap or exchange the values in two of the container's positions. If the values are large (in terms of the number of bytes it takes to store each of them), then it is often quicker to first build a separate list of pointers to the objects, sort those pointers, and then build the final sorted sequence. If the values are quite small however it is usually fastest to just swap the values in-place as needed. Furthermore, if the parameterized type is already of some pointer-type, then there is no need to build a separate pointer array. Template specialization allows the template creator to write different implementations and to specify the characteristics that the parameterized type(s) must have for each implementation to be used.
Unlike function templates, class templates can be partially specialized. That means that an alternate version of the class template code can be provided when some of the template parameters are known, while leaving other template parameters generic. This can be used, for example, to create a default implementation (the primary specialization) that assumes that copying a parameterizing type is expensive and then create partial specializations for types that are cheap to copy, thus increasing overall efficiency. Clients of such a class template just use specializations of it without needing to know whether the compiler used the primary specialization or some partial specialization in each case. Class templates can also be fully specialized, which means that an alternate implementation can be provided when all of the parameterizing types are known.
Advantages and disadvantages
[edit]Some uses of templates, such as the max() function, were formerly filled by function-like preprocessor macros (a legacy of the C language). For example, here is a possible implementation of such macro:
#define max(a,b) ((a) < (b) ? (b) : (a))
Macros are expanded (copy pasted) by the preprocessor, before program compiling; templates are actual real functions. Macros are always expanded inline; templates can also be inline functions when the compiler deems it appropriate.
However, templates are generally considered an improvement over macros for these purposes. Templates are type-safe. Templates avoid some of the common errors found in code that makes heavy use of function-like macros, such as evaluating parameters with side effects twice. Perhaps most importantly, templates were designed to be applicable to much larger problems than macros.
There are four primary drawbacks to the use of templates: supported features, compiler support, poor error messages (usually with pre C++20 substitution failure is not an error (SFINAE)), and code bloat:
- Templates in C++ lack many features, which makes implementing them and using them in a straightforward way often impossible. Instead programmers have to rely on complex tricks which leads to bloated, hard to understand and hard to maintain code. Current developments in the C++ standards exacerbate this problem by making heavy use of these tricks and building a lot of new features for templates on them or with them intended.
- Many compilers historically had poor support for templates, thus the use of templates could make code somewhat less portable. Support may also be poor when a C++ compiler is being used with a linker that is not C++-aware, or when attempting to use templates across shared library boundaries.
- Compilers can produce confusing, long, and sometimes unhelpful error messages when errors are detected in code that uses SFINAE.[22] This can make templates difficult to develop with.
- Finally, the use of templates requires the compiler to generate a separate instance of the templated class or function for every type parameters used with it. (This is necessary because types in C++ are not all the same size, and the sizes of data fields are important to how classes work.) So the indiscriminate use of templates can lead to code bloat, resulting in excessively large executables. However, judicious use of template specialization and derivation can dramatically reduce such code bloat in some cases:
So, can derivation be used to reduce the problem of code replicated because templates are used? This would involve deriving a template from an ordinary class. This technique proved successful in curbing code bloat in real use. People who do not use a technique like this have found that replicated code can cost megabytes of code space even in moderate size programs.
— Bjarne Stroustrup, The Design and Evolution of C++, 1994[23]
- Templated classes or functions may require an explicit specialization of the template class which would require rewriting of an entire class for a specific template parameters used by it.
The extra instantiations generated by templates can also cause some debuggers to have difficulty working gracefully with templates. For example, setting a debug breakpoint within a template from a source file may either miss setting the breakpoint in the actual instantiation desired or may set a breakpoint in every place the template is instantiated.
Also, the implementation source code for the template must be completely available (e.g. included in a header) to the translation unit (source file) using it. Templates, including much of the Standard Library, if not included in header files, cannot be compiled. (This is in contrast to non-templated code, which may be compiled to binary, providing only a declarations header file for code using it.) This may be a disadvantage by exposing the implementing code, which removes some abstractions, and could restrict its use in closed-source projects.[citation needed]
Templates in D
[edit]The D language supports templates based in design on C++. Most C++ template idioms work in D without alteration, but D adds some functionality:
- Template parameters in D are not restricted to just types and primitive values (as it was in C++ before C++20), but also allow arbitrary compile-time values (such as strings and struct literals), and aliases to arbitrary identifiers, including other templates or template instantiations.
- Template constraints and the
static ifstatement provide an alternative to respectively C++'s C++ concepts andif constexpr. - The
is(...)expression allows speculative instantiation to verify an object's traits at compile time. - The
autokeyword and thetypeofexpression allow type inference for variable declarations and function return values, which in turn allows "Voldemort types" (types that do not have a global name).[24]
Templates in D use a different syntax than in C++: whereas in C++ template parameters are wrapped in angular brackets (Template<param1, param2>),
D uses an exclamation sign and parentheses: Template!(param1, param2).
This avoids the C++ parsing difficulties due to ambiguity with comparison operators.
If there is only one parameter, the parentheses can be omitted.
Conventionally, D combines the above features to provide compile-time polymorphism using trait-based generic programming.
For example, an input range is defined as any type that satisfies the checks performed by isInputRange, which is defined as follows:
template isInputRange(R)
{
enum bool isInputRange = is(typeof(
(inout int = 0)
{
R r = R.init; // can define a range object
if (r.empty) {} // can test for empty
r.popFront(); // can invoke popFront()
auto h = r.front; // can get the front of the range
}));
}
A function that accepts only input ranges can then use the above template in a template constraint:
auto fun(Range)(Range range)
if (isInputRange!Range)
{
// ...
}
Code generation
[edit]In addition to template metaprogramming, D also provides several features to enable compile-time code generation:
- The
importexpression allows reading a file from disk and using its contents as a string expression. - Compile-time reflection allows enumerating and inspecting declarations and their members during compiling.
- User-defined attributes allow users to attach arbitrary identifiers to declarations, which can then be enumerated using compile-time reflection.
- Compile-time function execution (CTFE) allows a subset of D (restricted to safe operations) to be interpreted during compiling.
- String mixins allow evaluating and compiling the contents of a string expression as D code that becomes part of the program.
Combining the above allows generating code based on existing declarations. For example, D serialization frameworks can enumerate a type's members and generate specialized functions for each serialized type to perform serialization and deserialization. User-defined attributes could further indicate serialization rules.
The import expression and compile-time function execution also allow efficiently implementing domain-specific languages.
For example, given a function that takes a string containing an HTML template and returns equivalent D source code, it is possible to use it in the following way:
// Import the contents of example.htt as a string manifest constant.
enum htmlTemplate = import("example.htt");
// Transpile the HTML template to D code.
enum htmlDCode = htmlTemplateToD(htmlTemplate);
// Paste the contents of htmlDCode as D code.
mixin(htmlDCode);
Genericity in Eiffel
[edit]Generic classes have been a part of Eiffel since the original method and language design. The foundation publications of Eiffel,[25][26] use the term genericity to describe creating and using generic classes.
Basic, unconstrained genericity
[edit]Generic classes are declared with their class name and a list of one or more formal generic parameters. In the following code, class LIST has one formal generic parameter G
class
LIST [G]
...
feature -- Access
item: G
-- The item currently pointed to by cursor
...
feature -- Element change
put (new_item: G)
-- Add `new_item' at the end of the list
...
The formal generic parameters are placeholders for arbitrary class names that will be supplied when a declaration of the generic class is made, as shown in the two generic derivations below, where ACCOUNT and DEPOSIT are other class names. ACCOUNT and DEPOSIT are considered actual generic parameters as they provide real class names to substitute for G in actual use.
list_of_accounts: LIST [ACCOUNT]
-- Account list
list_of_deposits: LIST [DEPOSIT]
-- Deposit list
Within the Eiffel type system, although class LIST [G] is considered a class, it is not considered a type. However, a generic derivation of LIST [G] such as LIST [ACCOUNT] is considered a type.
Constrained genericity
[edit]For the list class shown above, an actual generic parameter substituting for G can be any other available class. To constrain the set of classes from which valid actual generic parameters can be chosen, a generic constraint can be specified. In the declaration of class SORTED_LIST below, the generic constraint dictates that any valid actual generic parameter will be a class that inherits from class COMPARABLE. The generic constraint ensures that elements of a SORTED_LIST can in fact be sorted.
class
SORTED_LIST [G -> COMPARABLE]
Generics in Java
[edit]Support for the generics, or "containers-of-type-T" was added to the Java programming language in 2004 as part of J2SE 5.0. In Java, generics are only checked at compile time for type correctness. The generic type information is then removed via a process called type erasure, to maintain compatibility with old JVM implementations, making it unavailable at runtime.[27] For example, a List<String> is converted to the raw type List. The compiler inserts type casts to convert the elements to the String type when they are retrieved from the list, reducing performance compared to other implementations such as C++ templates.
Genericity in .NET [C#, VB.NET]
[edit]Generics were added as part of .NET Framework 2.0 in November 2005, based on a research prototype from Microsoft Research started in 1999.[28] Although similar to generics in Java, .NET generics do not apply type erasure,[29]: 208–209 but implement generics as a first class mechanism in the runtime using reification. This design choice provides additional functionality, such as allowing reflection with preservation of generic types, and alleviating some of the limits of erasure (such as being unable to create generic arrays).[30][31] This also means that there is no performance hit from runtime casts and normally expensive boxing conversions. When primitive and value types are used as generic arguments, they get specialized implementations, allowing for efficient generic collections and methods. As in C++ and Java, nested generic types such as Dictionary<string, List<int>> are valid types, however are advised against for member signatures in code analysis design rules.[32]
.NET allows six varieties of generic type constraints using the where keyword including restricting generic types to be value types, to be classes, to have constructors, and to implement interfaces.[33] Below is an example with an interface constraint:
using System;
class Sample
{
static void Main()
{
int[] a = { 0, 1, 2, 3 };
MakeAtLeast<int>(a, 2); // Change a to { 2, 2, 2, 3 }
foreach (int i in a)
{
Console.WriteLine(i); // Print results.
}
Console.ReadKey(true);
}
static void MakeAtLeast<T>(T[] list, T lowest) where T : IComparable<T>
{
for (int i = 0; i < list.Length; i++)
{
if (list[i].CompareTo(lowest) < 0)
{
list[i] = lowest;
}
}
}
}
The MakeAtLeast() method allows operation on arrays, with elements of generic type T. The method's type constraint indicates that the method is applicable to any type T that implements the generic IComparable<T> interface. This ensures a compile time error, if the method is called if the type does not support comparison. The interface provides the generic method CompareTo(T).
The above method could also be written without generic types, simply using the non-generic Array type. However, since arrays are contravariant, the casting would not be type safe, and the compiler would be unable to find certain possible errors that would otherwise be caught when using generic types. In addition, the method would need to access the array items as objects instead, and would require casting to compare two elements. (For value types like types such as int this requires a boxing conversion, although this can be worked around using the Comparer<T> class, as is done in the standard collection classes.)
A notable behavior of static members in a generic .NET class is static member instantiation per run-time type (see example below).
using System;
// A generic class
class GenericTest<T>
{
// A static variable - will be created for each type on reflection
static CountedInstances OnePerType = new CountedInstances();
// A data member
private T _t;
// Default constructor
public GenericTest(T t)
{
_t = t;
}
}
// a class
class CountedInstances
{
// Static variable - this will be incremented once per instance
public static int Counter;
// Default constructor
public CountedInstances()
{
// Increase counter by one during object instantiation
CountedInstances.Counter++;
}
}
public class GenericExample
{
static void Main(string[] args)
{
// Main code entry point
// At the end of execution, CountedInstances.Counter = 2
GenericTest<int> g1 = new GenericTest<int>(1);
GenericTest<int> g11 = new GenericTest<int>(11);
GenericTest<int> g111 = new GenericTest<int>(111);
GenericTest<double> g2 = new GenericTest<double>(1.0);
}
}
Genericity in Pascal
[edit]For Pascal, generics were first implemented in 2006, in the implementation Free Pascal.
In Delphi
[edit]The Object Pascal dialect Delphi acquired generics in the 2007 Delphi 11 release by CodeGear, initially only with the .NET compiler (since discontinued) before being added to the native code in the 2009 Delphi 12 release. The semantics and abilities of Delphi generics are largely modelled on those of generics in .NET 2.0, though the implementation is by necessity quite different. Here is a more or less direct translation of the first C# example shown above:
program Sample;
{$APPTYPE CONSOLE}
uses
Generics.Defaults; //for IComparer<>
type
TUtils = class
class procedure MakeAtLeast<T>(Arr: TArray<T>; const Lowest: T;
Comparer: IComparer<T>); overload;
class procedure MakeAtLeast<T>(Arr: TArray<T>; const Lowest: T); overload;
end;
class procedure TUtils.MakeAtLeast<T>(Arr: TArray<T>; const Lowest: T;
Comparer: IComparer<T>);
var
I: Integer;
begin
if Comparer = nil then Comparer := TComparer<T>.Default;
for I := Low(Arr) to High(Arr) do
if Comparer.Compare(Arr[I], Lowest) < 0 then
Arr[I] := Lowest;
end;
class procedure TUtils.MakeAtLeast<T>(Arr: TArray<T>; const Lowest: T);
begin
MakeAtLeast<T>(Arr, Lowest, nil);
end;
var
Ints: TArray<Integer>;
Value: Integer;
begin
Ints := TArray<Integer>.Create(0, 1, 2, 3);
TUtils.MakeAtLeast<Integer>(Ints, 2);
for Value in Ints do
WriteLn(Value);
ReadLn;
end.
As with C#, methods and whole types can have one or more type parameters. In the example, TArray is a generic type (defined by the language) and MakeAtLeast a generic method. The available constraints are very similar to the available constraints in C#: any value type, any class, a specific class or interface, and a class with a parameterless constructor. Multiple constraints act as an additive union.
In Free Pascal
[edit]Free Pascal implemented generics in 2006 in version 2.2.0, before Delphi and with different syntax and semantics. However, since FPC version 2.6.0, the Delphi-style syntax is available when using the language mode {$mode Delphi}. Thus, Free Pascal code supports generics in either style.
Delphi and Free Pascal example:
// Delphi style
unit A;
{$ifdef fpc}
{$mode delphi}
{$endif}
interface
type
TGenericClass<T> = class
function Foo(const AValue: T): T;
end;
implementation
function TGenericClass<T>.Foo(const AValue: T): T;
begin
Result := AValue + AValue;
end;
end.
// Free Pascal's ObjFPC style
unit B;
{$ifdef fpc}
{$mode objfpc}
{$endif}
interface
type
generic TGenericClass<T> = class
function Foo(const AValue: T): T;
end;
implementation
function TGenericClass.Foo(const AValue: T): T;
begin
Result := AValue + AValue;
end;
end.
// example usage, Delphi style
program TestGenDelphi;
{$ifdef fpc}
{$mode delphi}
{$endif}
uses
A,B;
var
GC1: A.TGenericClass<Integer>;
GC2: B.TGenericClass<String>;
begin
GC1 := A.TGenericClass<Integer>.Create;
GC2 := B.TGenericClass<String>.Create;
WriteLn(GC1.Foo(100)); // 200
WriteLn(GC2.Foo('hello')); // hellohello
GC1.Free;
GC2.Free;
end.
// example usage, ObjFPC style
program TestGenDelphi;
{$ifdef fpc}
{$mode objfpc}
{$endif}
uses
A,B;
// required in ObjFPC
type
TAGenericClassInt = specialize A.TGenericClass<Integer>;
TBGenericClassString = specialize B.TGenericClass<String>;
var
GC1: TAGenericClassInt;
GC2: TBGenericClassString;
begin
GC1 := TAGenericClassInt.Create;
GC2 := TBGenericClassString.Create;
WriteLn(GC1.Foo(100)); // 200
WriteLn(GC2.Foo('hello')); // hellohello
GC1.Free;
GC2.Free;
end.
Functional languages
[edit]Genericity in Haskell
[edit]The type class mechanism of Haskell supports generic programming. Six of the predefined type classes in Haskell (including Eq, the types that can be compared for equality, and Show, the types whose values can be rendered as strings) have the special property of supporting derived instances. This means that a programmer defining a new type can state that this type is to be an instance of one of these special type classes, without providing implementations of the class methods as is usually necessary when declaring class instances. All the necessary methods will be "derived" – that is, constructed automatically – based on the structure of the type. For example, the following declaration of a type of binary trees states that it is to be an instance of the classes Eq and Show:
data BinTree a = Leaf a | Node (BinTree a) a (BinTree a)
deriving (Eq, Show)
This results in an equality function (==) and a string representation function (show) being automatically defined for any type of the form BinTree T provided that T itself supports those operations.
The support for derived instances of Eq and Show makes their methods == and show generic in a qualitatively different way from parametrically polymorphic functions: these "functions" (more accurately, type-indexed families of functions) can be applied to values of various types, and although they behave differently for every argument type, little work is needed to add support for a new type. Ralf Hinze (2004) has shown that a similar effect can be achieved for user-defined type classes by certain programming techniques. Other researchers have proposed approaches to this and other kinds of genericity in the context of Haskell and extensions to Haskell (discussed below).
PolyP
[edit]PolyP was the first generic programming language extension to Haskell. In PolyP, generic functions are called polytypic. The language introduces a special construct in which such polytypic functions can be defined via structural induction over the structure of the pattern functor of a regular datatype. Regular datatypes in PolyP are a subset of Haskell datatypes. A regular datatype t must be of kind * → *, and if a is the formal type argument in the definition, then all recursive calls to t must have the form t a. These restrictions rule out higher-kinded datatypes and nested datatypes, where the recursive calls are of a different form. The flatten function in PolyP is here provided as an example:
flatten :: Regular d => d a -> [a]
flatten = cata fl
polytypic fl :: f a [a] -> [a]
case f of
g+h -> either fl fl
g*h -> \(x,y) -> fl x ++ fl y
() -> \x -> []
Par -> \x -> [x]
Rec -> \x -> x
d@g -> concat . flatten . pmap fl
Con t -> \x -> []
cata :: Regular d => (FunctorOf d a b -> b) -> d a -> b
Generic Haskell
[edit]Generic Haskell is another extension to Haskell, developed at Utrecht University in the Netherlands. The extensions it provides are:
- Type-indexed values are defined as a value indexed over the various Haskell type constructors (unit, primitive types, sums, products, and user-defined type constructors). In addition, we can also specify the behaviour of a type-indexed values for a specific constructor using constructor cases, and reuse one generic definition in another using default cases.
The resulting type-indexed value can be specialized to any type.
- Kind-indexed types are types indexed over kinds, defined by giving a case for both * and k → k'. Instances are obtained by applying the kind-indexed type to a kind.
- Generic definitions can be used by applying them to a type or kind. This is called generic application. The result is a type or value, depending on which sort of generic definition is applied.
- Generic abstraction enables generic definitions be defined by abstracting a type parameter (of a given kind).
- Type-indexed types are types that are indexed over the type constructors. These can be used to give types to more involved generic values. The resulting type-indexed types can be specialized to any type.
As an example, the equality function in Generic Haskell:[34]
type Eq {[ * ]} t1 t2 = t1 -> t2 -> Bool
type Eq {[ k -> l ]} t1 t2 = forall u1 u2. Eq {[ k ]} u1 u2 -> Eq {[ l ]} (t1 u1) (t2 u2)
eq {| t :: k |} :: Eq {[ k ]} t t
eq {| Unit |} _ _ = True
eq {| :+: |} eqA eqB (Inl a1) (Inl a2) = eqA a1 a2
eq {| :+: |} eqA eqB (Inr b1) (Inr b2) = eqB b1 b2
eq {| :+: |} eqA eqB _ _ = False
eq {| :*: |} eqA eqB (a1 :*: b1) (a2 :*: b2) = eqA a1 a2 && eqB b1 b2
eq {| Int |} = (==)
eq {| Char |} = (==)
eq {| Bool |} = (==)
Clean
[edit]Clean offers generic programming based PolyP and the Generic Haskell as supported by the GHC ≥ 6.0. It parametrizes by kind as those but offers overloading.
Other languages
[edit]Languages in the ML family support generic programming through parametric polymorphism and generic modules called functors. Both Standard ML and OCaml provide functors, which are similar to class templates and to Ada's generic packages. Scheme syntactic abstractions also have a connection to genericity – these are in fact a superset of C++ templates.
A Verilog module may take one or more parameters, to which their actual values are assigned upon the instantiation of the module. One example is a generic register array where the array width is given via a parameter. Such an array, combined with a generic wire vector, can make a generic buffer or memory module with an arbitrary bit width out of a single module implementation.[35]
VHDL, being derived from Ada, also has generic abilities.[36]
C has a feature called "type-generic expressions" using the _Generic keyword:[37] This feature gives C function overloading capabilities, but is not related to generic programming. Generic programming can be achieved by using the preprocessor to define the generic code, with macro expansion taking the role of instantiation. Sometimes, the non-standard extension "statement expressions" is used to simulate function-like behaviour for generic code. Combining this two features enables implementing a generic max function, similar to std::max in c++:
#define max(a,b) \
({ typeof (a) _a = (a); \
typeof (b) _b = (b); \
_a > _b ? _a : _b; })
See also
[edit]References
[edit]- ^ Lee, Kent D. (15 December 2008). Programming Languages: An Active Learning Approach. Springer Science & Business Media. pp. 9–10. ISBN 978-0-387-79422-8.
- ^ Milner, R.; Morris, L.; Newey, M. (1975). "A Logic for Computable Functions with Reflexive and Polymorphic Types". Proceedings of the Conference on Proving and Improving Programs.
- ^ Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John (1994). Design Patterns. Addison-Wesley. ISBN 0-201-63361-2.
- ^ Musser & Stepanov 1989.
- ^ Musser, David R.; Stepanov, Alexander A. Generic Programming (PDF).
- ^ Alexander Stepanov; Paul McJones (19 June 2009). Elements of Programming. Addison-Wesley Professional. ISBN 978-0-321-63537-2.
- ^ Musser, David R.; Stepanov, Alexander A. (1987). "A library of generic algorithms in Ada". Proceedings of the 1987 annual ACM SIGAda international conference on Ada - SIGAda '87. pp. 216–225. CiteSeerX 10.1.1.588.7431. doi:10.1145/317500.317529. ISBN 0897912438. S2CID 795406.
- ^ Alexander Stepanov and Meng Lee: The Standard Template Library. HP Laboratories Technical Report 95-11(R.1), 14 November 1995
- ^ Matthew H. Austern: Generic programming and the STL: using and extending the C++ Standard Template Library. Addison-Wesley Longman Publishing Co., Inc. Boston, MA, USA 1998
- ^ Jeremy G. Siek, Lie-Quan Lee, Andrew Lumsdaine: The Boost Graph Library: User Guide and Reference Manual. Addison-Wesley 2001
- ^ Stepanov, Alexander. Short History of STL (PDF).
- ^ a b Stroustrup, Bjarne. Evolving a language in and for the real world: C++ 1991-2006 (PDF). doi:10.1145/1238844.1238848. S2CID 7518369.
- ^ Lo Russo, Graziano. "An Interview with A. Stepanov".
- ^ Backhouse, Roland; Jansson, Patrik; Jeuring, Johan; Meertens, Lambert (1999). Generic Programming – an Introduction (PDF).
- ^ Lämmel, Ralf; Peyton Jones, Simon (January 2003). "Scrap Your Boilerplate: A Practical Design Pattern for Generic Programming" (PDF). Microsoft. Retrieved 16 October 2016.
- ^ Dos Reis, Gabriel; Ja ̈rvi, Jaakko (2005). "What is Generic Programming? (preprint LCSD'05)" (PDF). Archived from the original (PDF) on 25 December 2005.
- ^ R. Garcia; J. Ja ̈rvi; A. Lumsdaine; J. Siek; J. Willcock (2005). "An extended comparative study of language support for generic programming (preprint)". CiteSeerX 10.1.1.110.122.
- ^ "Ada 2005 Overview - Ada Resource Association". www.adaic.org. Retrieved 6 October 2025.
- ^ "Overview: Exceptions, numerics, generics etc". www.adaic.org. Retrieved 6 October 2025.
- ^ "Generic Units". www.adaic.org. Retrieved 25 April 2024.
- ^ a b ""learn.adacore.com"". learn.adacore.com. Retrieved 6 October 2025.
- ^ Stroustrup, Dos Reis (2003): Concepts - Design choices for template argument checking
- ^ Stroustrup, Bjarne (1994). "15.5 Avoiding Code Replication". The Design and Evolution of C++. Reading, Massachusetts: Addison-Wesley. pp. 346–348. Bibcode:1994dec..book.....S. ISBN 978-81-317-1608-3.
- ^ Bright, Walter. "Voldemort Types in D". Dr. Dobbs. Retrieved 3 June 2015.
- ^ Object-Oriented Software Construction, Prentice Hall, 1988, and Object-Oriented Software Construction, second edition, Prentice Hall, 1997.
- ^ Eiffel: The Language, Prentice Hall, 1991.
- ^ Bloch 2018, p. 126, §Item 28: Prefer lists to arrays.
- ^ .NET/C# Generics History: Some Photos From Feb 1999
- ^ Albahari 2022.
- ^ C#: Yesterday, Today, and Tomorrow: An Interview with Anders Hejlsberg
- ^ Generics in C#, Java, and C++
- ^ Code Analysis CA1006: Do not nest generic types in member signatures
- ^ Constraints on Type Parameters (C# Programming Guide)
- ^ The Generic Haskell User's Guide
- ^ Verilog by Example, Section The Rest for Reference. Blaine C. Readler, Full Arc Press, 2011. ISBN 978-0-9834973-0-1
- ^ https://www.ics.uci.edu/~jmoorkan/vhdlref/generics.html VHDL Reference
- ^ WG14 N1516 Committee Draft — October 4, 2010
Sources
[edit]- Albahari, Joseph (2022). C# 10 in a Nutshell (First ed.). O'Reilly. ISBN 978-1-098-12195-2.
- Bloch, Joshua (2018). "Effective Java: Programming Language Guide" (third ed.). Addison-Wesley. ISBN 978-0134685991.
- Musser, D. R.; Stepanov, A. A. (1989). "Generic programming". In P. Gianni (ed.). Symbolic and Algebraic Computation: International symposium ISSAC 1988. Lecture Notes in Computer Science. Vol. 358. pp. 13–25. doi:10.1007/3-540-51084-2_2. ISBN 978-3-540-51084-0.
- Stroustrup, Bjarne (2007). Evolving a language in and for the real world: C++ 1991-2006 (PDF). ACM HOPL 2007.
- Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. Bibcode:1995dper.book.....G. ISBN 0-201-63361-2.
Further reading
[edit]- Gabriel Dos Reis and Jaakko Järvi, What is Generic Programming?, LCSD 2005 Archived 28 August 2019 at the Wayback Machine.
- Gibbons, Jeremy (2007). Backhouse, R.; Gibbons, J.; Hinze, R.; Jeuring, J. (eds.). Datatype-generic programming. Spring School on Datatype-Generic Programming 2006. Lecture Notes in Computer Science. Vol. 4719. Heidelberg: Springer. pp. 1–71. CiteSeerX 10.1.1.159.1228.
- Meyer, Bertrand (1986). "Genericity versus inheritance". Conference proceedings on Object-oriented programming systems, languages and applications - OOPSLA '86. pp. 391–405. doi:10.1145/28697.28738. ISBN 0897912047. S2CID 285030.
External links
[edit]- generic-programming.org
- Alexander A. Stepanov, Collected Papers of Alexander A. Stepanov (creator of the STL)
- C++, D
- Walter Bright, Templates Revisited.
- David Vandevoorde, Nicolai M Josuttis, C++ Templates: The Complete Guide, 2003 Addison-Wesley. ISBN 0-201-73484-2
- C#, .NET
- Jason Clark, "Introducing Generics in the Microsoft CLR," September 2003, MSDN Magazine, Microsoft.
- Jason Clark, "More on Generics in the Microsoft CLR," October 2003, MSDN Magazine, Microsoft.
- M. Aamir Maniar, Generics.Net. An open source generics library for C#.
- Delphi, Object Pascal
- Nick Hodges, "Delphi 2009 Reviewers Guide," October 2008, Embarcadero Developer Network, Embarcadero.
- Craig Stuntz, "Delphi 2009 Generics and Type Constraints," October 2008
- Dr. Bob, "Delphi 2009 Generics"
- Free Pascal: Free Pascal Reference guide Chapter 8: Generics, Michaël Van Canneyt, 2007
- Delphi for Win32: Generics with Delphi 2009 Win32, Sébastien DOERAENE, 2008
- Delphi for .NET: Delphi Generics Archived 14 January 2023 at the Wayback Machine, Felix COLIBRI, 2008
- Eiffel
- Haskell
- Johan Jeuring, Sean Leather, José Pedro Magalhães, and Alexey Rodriguez Yakushev. Libraries for Generic Programming in Haskell. Utrecht University.
- Dæv Clarke, Johan Jeuring and Andres Löh, The Generic Haskell user's guide
- Ralf Hinze, "Generics for the Masses," In Proceedings of the ACM SIGPLAN International Conference on Functional Programming (ICFP), 2004.
- Simon Peyton Jones, editor, The Haskell 98 Language Report, Revised 2002.
- Ralf Lämmel and Simon Peyton Jones, "Scrap Your Boilerplate: A Practical Design Pattern for Generic Programming," In Proceedings of the ACM SIGPLAN International Workshop on Types in Language Design and Implementation (TLDI'03), 2003. (Also see the website devoted to this research)
- Andres Löh, Exploring Generic Haskell, PhD thesis, 2004 Utrecht University. ISBN 90-393-3765-9
- Generic Haskell: a language for generic programming
- Java
- Gilad Bracha, Generics in the Java Programming Language, 2004.
- Maurice Naftalin and Philip Wadler, Java Generics and Collections, 2006, O'Reilly Media, Inc. ISBN 0-596-52775-6
- Peter Sestoft, Java Precisely, Second Edition, 2005 MIT Press. ISBN 0-262-69325-9
- Generic Programming in Java, 2004 Sun Microsystems, Inc.
- Angelika Langer, Java Generics FAQs
Generic programming
View on GrokipediaCore Concepts
Definition and Principles
Generic programming is a methodology in computer science that enables the development of reusable software components by parameterizing algorithms over types and other entities, allowing them to operate effectively on a wide variety of data types without requiring code duplication or modification.[2] This approach decomposes programs into independent components—such as data types, data structures, and algorithms—that can be developed separately and composed arbitrarily, provided they adhere to well-defined interfaces.[2] At its core, generic programming relies on several key principles: abstraction over types through parameterization, which minimizes assumptions about specific types to maximize applicability; the separation of algorithms from underlying data structures to promote flexibility in composition; and a strong emphasis on reusability and efficiency, ensuring that the resulting code performs comparably to hand-written, type-specific implementations.[2] These principles facilitate the creation of libraries where algorithms are expressed in terms of abstract requirements, such as the ability to compare elements, rather than concrete type details.[2] Unlike subtype polymorphism, which achieves code reuse through inheritance hierarchies and runtime dispatching, generic programming realizes polymorphism via compile-time parameterization and the use of concepts—sets of axioms defining required behaviors for types—enabling static resolution and avoiding overhead from dynamic typing.[2] For instance, a generic function to find the maximum of two elements can be written in pseudocode as follows, assuming the type supports a comparison operation:function max(a: T, b: T) where T has less-than operator:
if a < b then
return b
else
return a
function max(a: T, b: T) where T has less-than operator:
if a < b then
return b
else
return a
Parametric Polymorphism
Parametric polymorphism is a form of polymorphism that allows functions and data structures to be defined generically using type parameters, enabling the same code to operate uniformly across different types without reliance on their specific properties or implementations. In this approach, the code is written independently of concrete types and is instantiated by substituting the parameters with actual types either at compile-time or runtime, ensuring type-safe reuse. This mechanism forms a core theoretical foundation for generic programming by promoting abstraction over type details while maintaining behavioral uniformity.[7][8] The mechanism of parametric polymorphism relies on type variables, such as in pseudocode, which serve as placeholders for arbitrary concrete types. These variables allow algorithms to be expressed in a way that applies the same logic regardless of the underlying type, as long as the operations invoked are valid for that type. For instance, a generic identity function can be represented as taking an input of type and returning an output of type , where is not specified until instantiation. This uniformity ensures that the polymorphic code behaves consistently across instantiations, preserving relational properties between inputs and outputs.[9] Theoretically, parametric polymorphism is rooted in type theory, particularly through its connection to the lambda calculus extended with universal quantification. In the polymorphic lambda calculus, known as System F, types incorporate universal quantifiers like , indicating that a term holds for all possible types within a type expression . This formalization captures the essence of parametricity, where the polymorphic function treats all type instantiations equivalently, leading to free theorems about its behavior derived solely from its type signature. Such connections enable rigorous proofs of correctness and properties like naturality in generic functions.[9][7] To distinguish parametric polymorphism from other forms, the following table provides a concise comparison:| Polymorphism Type | Description | Key Characteristics | Example Mechanism |
|---|---|---|---|
| Parametric | Code operates uniformly on any type via parameters. | Universal over all types; same implementation for all. | Type variables and universal quantification ().[7] |
| Ad-hoc | Different behaviors for specific, unrelated types. | Type-specific overloads; finite set of implementations. | Function overloading or explicit type matching.[7] |
| Subtype (Inclusion) | Code works on types within an inheritance hierarchy. | Bounded to subtypes/supertypes; runtime or compile-time dispatch. | Bounded quantification () and subtyping relations.[7] |
Historical Development
Early Influences
The foundations of generic programming trace back to mathematical concepts in abstract algebra and category theory, which provided the intellectual framework for abstracting operations over types and structures. Abstract algebra's emphasis on algebraic structures, such as groups and rings, influenced the idea of defining algorithms in terms of generic operations that apply uniformly across different data types, emphasizing reusability through abstraction rather than specificity. Similarly, category theory, developed in the 1940s, introduced functors as mappings between categories that preserve structure, serving as a precursor to type constructors in programming—entities that generate new types parametrically without altering underlying behaviors.[12] These mathematical ideas laid the groundwork for viewing programs as compositions of abstract entities, influencing later efforts to parameterize code over types in formal systems like lambda calculus.[7] In early computing, the 1960s languages ALGOL 60 and Simula introduced rudimentary forms of polymorphism that hinted at generic mechanisms. ALGOL 60 supported parametric procedures through its call-by-name evaluation and flexible procedure parameters, where procedures could be passed without rigid type specifications, enabling a form of ad-hoc polymorphism that allowed code to operate on varied argument types at runtime.[13] This approach permitted generic-like behavior in algorithms, such as sorting routines that could handle different numeric types, though it relied on textual substitution rather than true type parameterization. Simula, building on ALGOL 60, extended this with class concepts and formal parameters in classes, allowing early attempts at parameterizing object behaviors—such as defining simulation classes that could adapt to different entity types—foreshadowing object-oriented genericity.[14] These features represented practical steps toward code reusability across types, albeit limited by the era's computational constraints. Key theoretical advancements in the 1970s solidified these ideas through type theory and abstraction techniques. John Reynolds' 1974 work, "Towards a Theory of Type Structure," explored parametric polymorphism by introducing polymorphic types in the lambda calculus, where functions could operate uniformly over all types without type-specific knowledge, establishing a formal basis for generic functions that behave identically regardless of instantiation. Concurrently, Lisp in the 1970s leveraged macros for type abstraction; macros enabled programmatic code generation that simulated generic structures, such as defining list-processing functions adaptable to different element types via symbolic manipulation, though this was more metaprogramming than static genericity.[15] These contributions, including Reynolds' later 1983 elaboration on parametricity, emphasized uniformity and abstraction as core to avoiding type-dependent implementations.[16] However, these early approaches faced significant limitations, particularly in dynamic languages like Lisp, where the absence of compile-time type enforcement meant generic abstractions could not guarantee type safety, often resulting in runtime errors from mismatched operations.[7] In contrast to static systems, ALGOL 60's parametric procedures suffered from inefficiencies due to call-by-name expansion, which simulated genericity through repeated textual substitution but lacked the efficiency and safety of modern compile-time checks. Simula's parameterization attempts were similarly constrained, as virtual procedures provided dynamic dispatch but not full static parameterization, limiting scalability for complex generic hierarchies. These shortcomings highlighted the need for stronger type systems to enforce generic constraints at compile time, paving the way for later developments.[13]Key Milestones
In the late 1980s, Alexander Stepanov and David Musser's collaboration on the 1987 Ada-based library for list processing at GE R&D marked an early milestone in generic programming, emphasizing the separation of algorithms from specific implementations to enhance reusability. In 1988, Stepanov moved to HP Laboratories, where he continued developing reusable libraries, focusing on generic algorithms that could operate across various data structures. That year, Musser and Stepanov published "Generic Programming" at the ISSAC conference, coining the term and formalizing the paradigm through concepts for abstracting algorithms over types.[17][18][19] The 1990s saw a major breakthrough with the creation of the Standard Template Library (STL) for C++, initially prototyped in 1994 by Stepanov, Musser, and Meng Lee at HP Labs.[19] This library exemplified generic programming through parametric polymorphism, enabling efficient, type-safe containers and algorithms, and was standardized as part of C++98 in 1998 by the ISO/IEC JTC1/SC22/WG21 committee.[20] During the 2000s, generic programming expanded into mainstream object-oriented languages to address demands for type-safe collections in enterprise applications. Java introduced generics in version 5.0 (J2SE 5.0) in 2004, allowing parameterized types while maintaining backward compatibility through type erasure.[21] Similarly, C# 2.0, released in 2005 with .NET Framework 2.0, incorporated full runtime generics for improved performance and safety in collections.[22] Recent developments through the early 2020s, including C++23 published in 2023, have further refined genericity with enhanced safety and expressiveness, such as improved module support and pattern matching that aid generic algorithm design.[23] Rust, stable since version 1.0 in 2015, utilizes traits to implement safe generics, preventing common errors like null dereferences at compile time.[24] Python added type hints in version 3.5 via PEP 484 in 2015, supporting static analysis for generic-like code without runtime overhead.[25] C++20, published by ISO in December 2020, introduced concepts in 2018 proposals to constrain templates more expressively, building on the Stepanov–Musser paradigm.[26] Ongoing research as of 2025 explores dependent types for even more precise genericity, as seen in efforts to integrate them into languages like Haskell for verified programming.| Year | Milestone | Language/Technology | Key Contributors |
|---|---|---|---|
| 1987 | Ada generic library for list processing | Ada | Alexander Stepanov, David Musser |
| 1988 | Coined "generic programming" in ISSAC paper | N/A | David Musser, Alexander Stepanov |
| 1994 | STL prototype development | C++ | Alexander Stepanov, David Musser, Meng Lee |
| 1998 | STL standardization | C++ | ISO/IEC JTC1/SC22/WG21 |
| 2004 | Generics introduction | Java | JSR-14 team (Sun Microsystems) |
| 2005 | Generics introduction | C# | Anders Hejlsberg (Microsoft) |
| 2015 | Stable release with trait-based generics | Rust | Rust core team (Mozilla) |
| 2015 | Type hints for static analysis | Python | Guido van Rossum et al. (PEP 484) |
| 2020 | Concepts standardization | C++ | Bjarne Stroustrup, Andrew Sutton et al. |
| 2023 | C++23 enhancements (modules, patterns for generics) | C++ | ISO/IEC JTC1/SC22/WG21 |
Paradigms and Approaches
Stepanov–Musser Paradigm
The Stepanov–Musser paradigm, developed by Alexander Stepanov and David Musser in the early 1990s, forms a foundational approach to generic programming by treating algorithms as mathematical functions that operate independently of specific data representations. This paradigm emphasizes the reusability of algorithms through abstraction, allowing them to apply to a wide variety of types and structures without modification, provided those types satisfy minimal operational requirements. Central to this is the decomposition of software into interchangeable components—algorithms, containers, and iterators—that can be composed arbitrarily while maintaining efficiency.[27][2] Key components include iterators for abstracting sequence traversal and access, containers for holding data with uniform interfaces, and requirements on types that specify only the necessary operations, such as equality or ordering. Iterators enable separation of concerns by decoupling algorithms from the underlying storage details, supporting traversal models like forward, bidirectional, or random access. Type requirements, often expressed informally in early works as axioms (e.g., symmetry of equality: if then ), ensure consistency and allow algorithms to work with user-defined types mimicking built-in behaviors. The paradigm prioritizes compile-time genericity and efficiency, leveraging techniques like template instantiation to avoid runtime overhead, thus achieving performance comparable to hand-coded solutions.[27][2] A representative example is the generic sort algorithm, which operates on a range defined by iterators rather than a specific container:template <class RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last) {
// Sorts the range [first, last) using operator<, with O(N log N) average complexity
// Implementation uses partitioning (e.g., quicksort hybrid) via iterator dereferencing
if (first != last) {
RandomAccessIterator pivot = partition(first, last); // Pivot selection and swap via iterators
sort(first, pivot);
sort(pivot + 1, last);
}
}
template <class RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last) {
// Sorts the range [first, last) using operator<, with O(N log N) average complexity
// Implementation uses partitioning (e.g., quicksort hybrid) via iterator dereferencing
if (first != last) {
RandomAccessIterator pivot = partition(first, last); // Pivot selection and swap via iterators
sort(first, pivot);
sort(pivot + 1, last);
}
}
sort to work on arrays, vectors, or lists by providing appropriate iterators, focusing on algorithmic logic independent of representation.[28][27]
The paradigm's influence is evident in modern standard libraries, serving as the basis for the C++ Standard Template Library (STL), which standardized these ideas in 1998. It evolved to incorporate more explicit type requirements through concepts in C++20, formalizing earlier informal constraints to improve error messages and compilation speed while preserving the iterator-container model. Unlike functional higher-order approaches that rely on lambdas or currying for abstraction, this paradigm adopts an imperative style centered on sequences and mutable state via iterators.[28][29][2]
Concept-Based Programming
Concept-based programming extends generic programming by introducing concepts, which are named sets of syntactic and semantic requirements that types must satisfy to be substitutable in generic algorithms.[30] These concepts, such asEqualityComparable (requiring equality and inequality operations) or Iterable (requiring traversal capabilities), enable precise specification of type behaviors without relying solely on duck typing or unrestricted parameterization.[30]
The approach was proposed in the 1990s as part of efforts to refine the Standard Template Library (STL), with roots in Alex Stepanov's work on abstracting common type requirements.[30] It was formalized in the C++20 standard, providing language support for explicit concept definitions and checks.[30] While similar to Haskell's type classes in constraining polymorphism, concept-based programming is adapted for imperative contexts, emphasizing compile-time verification over runtime dispatch.[31]
Key benefits include enhanced type safety by preventing invalid template instantiations at compile time, improved error messages that reference specific unmet requirements rather than deep instantiation failures, and opportunities for better optimization through clearer interface contracts that guide inlining and specialization.[30] For instance, constraining template parameters to a concept like RandomAccessIterator avoids substituting incompatible types, such as forward-only iterators, thereby promoting safer and more efficient genericity.[30]
An example is a generic sorting algorithm constrained by the RandomAccessIterator concept, which requires bidirectional traversal, indexing, and value comparability:
concept RandomAccessIterator {
requires Iterator<I> &&
SignedIntegral<difference_type_t<I>> &&
requires(I i, difference_type_t<I> n) {
{ i + n } -> I;
{ i - n } -> I;
{ i[n] } -> reference_t<I>;
};
}
template<RandomAccessIterator I>
requires Sortable<I>
void sort(I first, I last) {
// Implementation using random access operations
// e.g., std::swap(i[j], i[k]);
}
concept RandomAccessIterator {
requires Iterator<I> &&
SignedIntegral<difference_type_t<I>> &&
requires(I i, difference_type_t<I> n) {
{ i + n } -> I;
{ i - n } -> I;
{ i[n] } -> reference_t<I>;
};
}
template<RandomAccessIterator I>
requires Sortable<I>
void sort(I first, I last) {
// Implementation using random access operations
// e.g., std::swap(i[j], i[k]);
}
Genericity in Object-Oriented Languages
Generics in Ada
Ada's support for generics originated in the Ada 83 standard, released in 1983, where they were introduced as a core language feature to enable the creation of parameterized packages and subprograms for reusable, type-safe modules.[33] This mechanism allows developers to abstract algorithms and data structures over types and values, promoting modularity in large-scale software development while maintaining compile-time type safety.[33] Generics in Ada align with parametric polymorphism by instantiating generic units with specific actual parameters, ensuring that the resulting code behaves as if written directly for those types.[34] The syntax for declaring generics begins with thegeneric keyword, followed by formal parameter declarations such as types (e.g., type T is private;), objects (e.g., X : Integer;), or subprograms (e.g., function Less_Than (Left, Right : T) return Boolean;).[33] Instantiation occurs at compile-time via a new clause, substituting actual parameters for formals, with the Ada compiler performing strong type checking to enforce compatibility and prevent errors like mismatched operations.[34] Formal types can be specified as private for abstraction, discrete for enumeration-like behavior, or floating-point for numeric operations, enabling flexible reuse without exposing implementation details.[33]
A distinctive feature of Ada's generics is their support for formal derived types, which inherit operations from a parent type (e.g., type T is new [Integer](/page/Integer);), and formal private types, which allow limited visibility to maintain encapsulation while permitting instantiation with concrete types.[35] These capabilities make Ada generics particularly suitable for developing reusable components in safety-critical domains like defense and aerospace, where reliability and verifiability are paramount, as evidenced by their widespread adoption in military embedded systems for abstracting common functionalities such as data structures and algorithms.[36]
For instance, a generic stack package can be defined to manage elements of any private type, as shown below:
generic
type Element_Type is private;
package Generic_Stack is
type Stack_Type is private;
procedure Push (S : in out Stack_Type; E : Element_Type);
procedure Pop (S : in out Stack_Type; E : out Element_Type);
private
-- Implementation details hidden
end Generic_Stack;
generic
type Element_Type is private;
package Generic_Stack is
type Stack_Type is private;
procedure Push (S : in out Stack_Type; E : Element_Type);
procedure Pop (S : in out Stack_Type; E : out Element_Type);
private
-- Implementation details hidden
end Generic_Stack;
package Int_Stack is new Generic_Stack ([Integer](/page/Integer));) or floats (package Float_Stack is new Generic_Stack (Float);), yielding type-safe stacks with compile-time verification of operations.[33]
Ada's generics evolved significantly in the Ada 2012 standard, incorporating contract-based programming with preconditions and postconditions that can be applied to generic subprograms and packages to specify behavioral invariants, enhancing reliability through formal verification in complex systems.[34] These enhancements also include support for indefinite formal types (e.g., unbounded arrays like strings) and improved container libraries, allowing generics to handle variable-sized data more efficiently without runtime overhead.[33]
Templates in C++
Templates in C++ provide the primary mechanism for generic programming, enabling the creation of reusable code that operates on multiple data types without sacrificing type safety or performance. Introduced in the original C++98 standard, templates allow for the definition of function templates, class templates, and, since C++14, variable templates, which serve as blueprints instantiated at compile time with specific types or values. This parameterization supports advanced metaprogramming techniques, where templates can compute values or select behaviors during compilation, forming the foundation for libraries like the Standard Template Library (STL).[37][38] Key features of C++ templates include two-phase name lookup, which separates the parsing of template definitions from their instantiation to ensure dependent names are resolved correctly in context, and SFINAE (Substitution Failure Is Not an Error), a rule that discards invalid template specializations from overload resolution sets rather than halting compilation, enabling conditional compilation based on type traits. Subsequent standards have enhanced template capabilities: C++11 introducedauto for type deduction and constexpr for compile-time evaluation; C++14 added variable templates and relaxed constexpr rules; C++17 enabled class template argument deduction; and C++20 brought concepts for explicit constraints on template parameters, along with consteval and constinit for stricter compile-time guarantees. These evolutions address longstanding complexities in template usage while preserving zero-overhead abstraction.[39][40][37]
In the STL, templates underpin generic containers such as std::vector<T>, a dynamic array class parameterized by element type T and an optional allocator, which manages contiguous storage with efficient random access. Algorithms like std::sort exemplify generic function templates, operating on ranges defined by iterators—abstractions that decouple algorithms from specific container implementations—allowing the same sorting logic to work across vectors, arrays, or lists while meeting random access iterator requirements. Iterators themselves, often implemented as template classes, enable this uniformity by providing a consistent interface for traversal and access, aligning with the Stepanov–Musser paradigm of iterator-based generic programming.[41][42][28]
A representative example is a simple generic stack class template, which can be specialized for optimization:
template <typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(const T& value) { elements.push_back(value); }
T pop() {
T top = elements.back();
elements.pop_back();
return top;
}
bool empty() const { return elements.empty(); }
};
// Specialization for char* to use string management
template <>
class Stack<char*> {
private:
std::vector<std::string> elements; // Avoids raw pointer issues
public:
void push(const char* value) { elements.emplace_back(value); }
std::string pop() {
std::string top = std::move(elements.back());
elements.pop_back();
return top;
}
bool empty() const { return elements.empty(); }
};
template <typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(const T& value) { elements.push_back(value); }
T pop() {
T top = elements.back();
elements.pop_back();
return top;
}
bool empty() const { return elements.empty(); }
};
// Specialization for char* to use string management
template <>
class Stack<char*> {
private:
std::vector<std::string> elements; // Avoids raw pointer issues
public:
void push(const char* value) { elements.emplace_back(value); }
std::string pop() {
std::string top = std::move(elements.back());
elements.pop_back();
return top;
}
bool empty() const { return elements.empty(); }
};
char*, where the template is replaced entirely to handle memory semantics differently, improving safety without altering the interface.[43]
One challenge with C++ templates is code bloat, where each unique instantiation generates separate object code, potentially increasing executable size despite optimizations like inlining and dead code elimination. Concepts in C++20 mitigate this partially by constraining templates upfront—discarding invalid instantiations early and providing clearer error messages—thus reducing unnecessary compilations and improving diagnostics over SFINAE-based techniques.
Templates in D
D's template system, introduced with the language's first public release in December 2001 and formalized in version 1.0 in January 2007, enables parametric polymorphism through compile-time code generation for functions, structs, classes, and aliases.[44][45] Unlike C++'s angle-bracket syntax, D uses a cleaner parameter list in parentheses, such asT foo(T)(T arg) { return arg * 2; } for a generic doubling function, allowing implicit instantiation without explicit type specification.[45] This design reduces verbosity while supporting eponymous templates for shorthand declarations and instantiations, like struct Pair(T, U) { T first; U second; } instantiated as Pair!(int, string) p;.[46]
Key features include template mixins, which facilitate code generation by inserting templated declarations into the current scope, enabling reusable boilerplate such as parameterized loops or interfaces.[47] For type introspection, is() expressions serve as type traits, allowing compile-time checks like static if (is(T : ElementType)) to verify if a type matches a pattern, often used in constraints or conditional compilation.[48] Compile-time function evaluation (CTFE) further enhances genericity by executing functions at compile time within templates, supporting recursive computations like factorial without runtime overhead.[49]
D's templates uniquely integrate support for contracts—preconditions (in), postconditions (out), and invariants—and pure functions, which restrict mutable global access and ensure referential transparency even in generic contexts.[50] For instance, a pure generic function like pure T sum(T)(T[] arr) in { assert(arr.length > 0); } out(result; result >= 0) { ... } enforces safety across instantiations. To avoid C++'s substitution failure is not an error (SFINAE) pitfalls, D employs explicit constraints via if clauses on template parameters, providing clear error messages and precise overload resolution; an example is T process(T)(T val) if (isIntegral!T) for integer-only operations.[51][52]
A representative example is a generic range-based algorithm using foreach with template parameters, demonstrating seamless iteration over containers:
import std.stdio;
import std.range;
void printRange(R)(R range) if (isInputRange!R) {
foreach (elem; range) {
writeln(elem);
}
}
void main() {
int[] ints = [1, 2, 3];
printRange(ints); // Outputs: 1\n2\n3\n
}
import std.stdio;
import std.range;
void printRange(R)(R range) if (isInputRange!R) {
foreach (elem; range) {
writeln(elem);
}
}
void main() {
int[] ints = [1, 2, 3];
printRange(ints); // Outputs: 1\n2\n3\n
}
std.range traits for constraint validation, ensuring type safety at compile time.[45]
As of November 2025, D version 2.114.0 was released in October, with the first beta for 2.115.0 in November, enhancing user-defined attributes (UDAs) in templates for improved metaprogramming by allowing recursive attachment to nested instances and better integration with traits for annotation querying.[53][54][55]
Genericity in Eiffel
Eiffel's genericity mechanism, introduced with the language's first implementation in 1986, enables the creation of reusable classes parameterized by types through formal generic parameters, such asG in a class declaration like LIST[G].[56][57] This approach allows developers to define data structures and algorithms that operate on arbitrary types without sacrificing static type safety, distinguishing Eiffel from early object-oriented languages that relied on universal supertypes like ANY for polymorphism.[58]
Key features of Eiffel's genericity include unconstrained parameters, which can represent any type, and constrained genericity, where a parameter is restricted to descendants of a specific class, as in SORTED_LIST[G -> COMPARABLE], ensuring that operations like comparison are available for the actual type.[56][59] Genericity integrates seamlessly with inheritance, allowing generic classes to inherit from other generic or non-generic classes— for instance, STACK[G] can inherit from LIST[G]—which supports polymorphic reuse while maintaining type conformance through substitution rules.[56] This combination enables flexible hierarchies, such as a LIST[[POLYGON](/page/Polygon)] that can hold RECTANGLE instances due to inheritance relationships.[56]
Genericity in Eiffel is deeply intertwined with design by contract, where preconditions, postconditions, and class invariants can reference generic parameters to enforce behavioral specifications.[56] Constraints on parameters allow contracts to assume the availability of certain features from the constraining type; for example, in a class with G -> COMPARABLE, a postcondition might assert item < other_item using the comparison routine guaranteed by the constraint.[56] Invariants further ensure properties like non-null elements across the class's lifecycle, adapting via generic substitution when actual parameters are provided.[59]
A representative example is the LIST[G] class, which models a sequence of elements of type G:
class LIST [G]
create
make
feature -- Access
item: G
-- Current item
feature -- Modification
put (x: G)
-- Add x to end
require
x_not_void: x /= Void
do
-- Implementation details
ensure
one_more: count = old count + 1
last_is_x: last = x
end
invariant
non_void_elements: item /= Void
valid_count: 0 <= count
end
class LIST [G]
create
make
feature -- Access
item: G
-- Current item
feature -- Modification
put (x: G)
-- Add x to end
require
x_not_void: x /= Void
do
-- Implementation details
ensure
one_more: count = old count + 1
last_is_x: last = x
end
invariant
non_void_elements: item /= Void
valid_count: 0 <= count
end
Generics in Java
Java generics were introduced in Java 5 in 2004 through JSR 14, which aimed to add generic types and methods to the language to improve type safety, particularly for the collections framework.[61] This addition allowed developers to parameterize classes, interfaces, and methods with types, enabling compile-time checks for type correctness without runtime overhead.[21] Prior to generics, collections likeList could hold any object type, leading to frequent casting and potential runtime errors; generics addressed this by specifying element types, such as List<String>.[62]
The core mechanism of Java generics relies on type parameters, denoted by angle brackets (e.g., List<T> where T is a type variable), which are placeholders for actual types substituted at usage.[63] At compile time, the Java compiler performs type erasure, replacing type parameters with their bounds (or Object if unbounded) and removing generic-specific information to generate bytecode compatible with pre-Java 5 JVMs.[64] This erasure ensures backward compatibility but means generic types are not reified at runtime, limiting reflection and instanceof checks on parameterized types.[65]
Key features include bounded type parameters, which restrict type variables to subtypes of a specified class or interface (e.g., T extends Comparable<T> for ensuring comparability). Additionally, wildcards provide flexibility for covariance and contravariance: upper-bounded wildcards (? extends T) allow reading from a type hierarchy (covariant use), while lower-bounded wildcards (? super T) enable writing into supertypes (contravariant use).[66][67] These constructs, part of the PECS principle (Producer Extends, Consumer Super), facilitate reusable APIs like collection utilities.
Despite these advances, Java generics have notable limitations. They do not support primitive types directly (e.g., no List<int>; wrappers like Integer must be used instead), due to the JVM's type system and erasure model.[68] Type erasure also erases runtime type information, which can lead to heap pollution—a situation where a parameterized type variable references an incompatible object, potentially causing ClassCastException at runtime, especially in varargs or legacy code interoperation.[69]
A representative example is a generic Pair class for holding two values of different types:
public class Pair<T, U> {
private T first;
private U second;
public Pair(T first, U second) {
this.first = first;
this.second = second;
}
public T getFirst() { return first; }
public U getSecond() { return second; }
}
public class Pair<T, U> {
private T first;
private U second;
public Pair(T first, U second) {
this.first = first;
this.second = second;
}
public T getFirst() { return first; }
public U getSecond() { return second; }
}
Pair<String, Integer> p = new Pair<>("Hello", 42);. To demonstrate bounded wildcards, consider a method that prints pairs where the first element is comparable:
public static void printComparableFirst(Pair<? extends Comparable<? super T>, U> pair) {
[System](/page/System).out.println("First: " + pair.getFirst());
}
public static void printComparableFirst(Pair<? extends Comparable<? super T>, U> pair) {
[System](/page/System).out.println("First: " + pair.getFirst());
}
? extends Comparable<? super T> ensures the first type is readable and comparable to some supertype T.
As of November 2025, Project Valhalla remains in development, with early access builds available and JEP 401 (Value Classes and Objects) in candidate status, targeting integration in upcoming Java releases to support primitive types in generics without boxing.[70]
Generics in .NET
Generics in .NET were introduced with version 2.0 of the .NET Framework in 2005, coinciding with C# 2.0 and Visual Basic .NET 2005 (VB.NET 2005), to enable type-safe, reusable code without performance overhead from boxing or casting.[71] This addition addressed limitations in earlier collections like ArrayList by providing parameterized types directly in the Common Language Runtime (CLR), allowing developers to create flexible data structures and algorithms that work across multiple types while preserving type information at runtime.[71] A key feature of .NET generics is full reification, where type parameters are retained in the compiled Common Intermediate Language (CIL) metadata, making them accessible via reflection and enabling runtime optimizations such as specialized code generation for different type arguments.[72] This contrasts with erasure-based systems and supports generic classes, interfaces, methods, and delegates, facilitating scenarios like type-safe collections (e.g., Listpublic interface IRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
}
public class Repository<T> : IRepository<T> where T : class
{
private readonly DbContext _context;
private readonly DbSet<T> _dbSet;
public Repository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public async Task<T> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(T entity)
{
_dbSet.Update(entity);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_dbSet.Remove(entity);
await _context.SaveChangesAsync();
}
}
}
public interface IRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
}
public class Repository<T> : IRepository<T> where T : class
{
private readonly DbContext _context;
private readonly DbSet<T> _dbSet;
public Repository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public async Task<T> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(T entity)
{
_dbSet.Update(entity);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_dbSet.Remove(entity);
await _context.SaveChangesAsync();
}
}
}
Generics in Pascal
Generics in Object Pascal, the extension of Pascal used in environments like Delphi and Free Pascal, were introduced to enable type parameterization, allowing algorithms and data structures to be written independently of specific types while maintaining compile-time type safety. Unlike earlier versions of Pascal, such as Turbo Pascal 7 from the early 1990s, which lacked native support for generics and relied on manual workarounds like conditional compilation or macros for reusable code, true generics emerged later as part of Object Pascal's evolution toward object-oriented and modular programming. Free Pascal implemented generics first, supporting them in ObjFPC mode from version 2.2.0 released in 2006, predating their addition to Delphi.[79] Delphi, developed by Embarcadero (formerly Borland), introduced generics in version 2009, integrating them as first-class language features to enhance the Visual Component Library (VCL) and runtime library (RTL) for desktop applications.[80] This development built on Pascal's unit system, which promotes modular code organization by encapsulating declarations and implementations, allowing generics to facilitate reusable components without sacrificing the language's structured heritage. Key features of generics in Object Pascal include the ability to define parameterized types for classes, records, interfaces, procedures, and functions using angle-bracket syntax for type parameters, such asT or multiple parameters like TKey, TValue. Instantiation occurs at compile time via the specialize keyword in Free Pascal or direct usage in Delphi, generating type-specific code that avoids runtime type casting and improves performance. For example, generic procedures can operate on arbitrary types, while constraints—introduced in Free Pascal around version 3.0 and refined in subsequent releases like 3.2.0 (2020)—allow restrictions such as requiring a type parameter to be a class, record, or implement a specific interface, using syntax like where T is class. These features are seamlessly integrated with Pascal's unit system, where generic units can be included across projects for modular reuse, enabling developers to create foundational libraries for GUI components in Delphi or cross-platform applications in Free Pascal and Lazarus IDE. Compared to earlier Pascal dialects, this approach is simpler and more focused on procedural and object-oriented hybrids for desktop and embedded systems, though it offers less metaprogramming flexibility than contemporaries.[81][79]
A representative example is a generic dynamic array wrapper, which leverages Object Pascal's built-in dynamic arrays within a parameterized class for type-safe collections:
type
TGenericArray<T> = class
private
FData: array of T;
function GetItem(Index: Integer): T;
procedure SetItem(Index: Integer; const Value: T);
public
constructor Create(ACapacity: Integer = 0);
procedure Add(const Item: T);
property Items[Index: Integer]: T read GetItem write SetItem; default;
end;
constructor TGenericArray<T>.Create(ACapacity: [Integer](/page/Integer) = 0);
begin
SetLength(FData, ACapacity);
end;
procedure TGenericArray<T>.Add(const Item: T);
var
NewLength: [Integer](/page/Integer);
begin
NewLength := Length(FData) + 1;
SetLength(FData, NewLength);
FData[NewLength - 1] := Item;
end;
function TGenericArray<T>.GetItem(Index: [Integer](/page/Integer)): T;
begin
if (Index >= 0) and (Index < Length(FData)) then
Result := FData[Index]
else
raise Exception.Create('Index out of bounds');
end;
procedure TGenericArray<T>.SetItem(Index: [Integer](/page/Integer); const Value: T);
begin
if (Index >= 0) and (Index < Length(FData)) then
FData[Index] := Value
else
raise Exception.Create('Index out of bounds');
end;
type
TGenericArray<T> = class
private
FData: array of T;
function GetItem(Index: Integer): T;
procedure SetItem(Index: Integer; const Value: T);
public
constructor Create(ACapacity: Integer = 0);
procedure Add(const Item: T);
property Items[Index: Integer]: T read GetItem write SetItem; default;
end;
constructor TGenericArray<T>.Create(ACapacity: [Integer](/page/Integer) = 0);
begin
SetLength(FData, ACapacity);
end;
procedure TGenericArray<T>.Add(const Item: T);
var
NewLength: [Integer](/page/Integer);
begin
NewLength := Length(FData) + 1;
SetLength(FData, NewLength);
FData[NewLength - 1] := Item;
end;
function TGenericArray<T>.GetItem(Index: [Integer](/page/Integer)): T;
begin
if (Index >= 0) and (Index < Length(FData)) then
Result := FData[Index]
else
raise Exception.Create('Index out of bounds');
end;
procedure TGenericArray<T>.SetItem(Index: [Integer](/page/Integer); const Value: T);
begin
if (Index >= 0) and (Index < Length(FData)) then
FData[Index] := Value
else
raise Exception.Create('Index out of bounds');
end;
var IntArray: TGenericArray<Integer>; IntArray := TGenericArray<Integer>.Create(10); IntArray.Add(42);, demonstrating reuse across types like TGenericArray<String> without code duplication. This pattern underpins standard RTL classes like TList<T> in Delphi's System.Generics.Collections unit.[80]
In modern implementations, Free Pascal 3.2.2 (2021) and later, along with Lazarus, have expanded generics with improved constraint expressiveness and compatibility, including better support for generic methods and specializations in variable declarations, making them more viable for contemporary applications while preserving backward compatibility with Delphi syntax. These enhancements solidify generics' role in Object Pascal as a tool for efficient, type-safe modular programming, particularly in GUI and cross-platform development.[82][79]
Genericity in Functional Languages
Genericity in Haskell
Haskell's approach to genericity centers on type classes, a mechanism for ad-hoc polymorphism introduced in Haskell 1.0 in 1990.[83] Type classes define interfaces for overloaded operations, allowing functions to be generic over types that implement shared behaviors, such as equality or numeric computation, while complementing Haskell's parametric polymorphism.[84] This system, first proposed by Philip Wadler and Stephen Blott in 1988, provides a structured way to resolve overloading at compile time, enabling reusable code without runtime dispatch.[85] The core mechanism involves class declarations that specify a type parameter and required operations, followed by instance declarations for concrete types. For example, theEq class is declared as:
[class](/page/Haskell-class_attack_transport) Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
x /= y = not (x == y)
x == y = not (x /= y)
[class](/page/Haskell-class_attack_transport) Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
x /= y = not (x == y)
x == y = not (x /= y)
[Integer](/page/Integer), provides implementations:
instance Eq [Integer](/page/Integer) where
x == y = x `integerEq` y
x /= y = not (x == y)
instance Eq [Integer](/page/Integer) where
x == y = x `integerEq` y
x /= y = not (x == y)
class C a b.[87] To ensure coherence and avoid overlapping instances, functional dependencies—developed by Mark Jones in 2000—constrain parameters by declaring deterministic mappings, e.g., class C a b | a -> b meaning a determines b.[88] Additionally, support for higher-kinded types enables classes over type constructors, as in the Functor class:
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Functor f where
fmap :: (a -> b) -> f a -> f b
f has kind * -> *, allowing generic mapping over structures like lists or trees without specifying the contained type.[86]
A representative example of genericity via type classes is a generic fold over foldable structures using the Monoid class, which provides an associative binary operation <> and identity mempty. The Foldable class enables:
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
foldMap length ["hello", "world"] yields 10 when using Sum as the monoid. For direct folding of monoid values, fold :: (Foldable t, [Monoid](/page/Monoid) m) => t m -> m or mconcat :: [Monoid](/page/Monoid) a => [a] -> a reduces lists generically.[89]
In the functional paradigm, type classes enhance composability by integrating seamlessly with pure, higher-order functions, allowing generic interfaces like [Functor](/page/Functor) and [Monoid](/page/Monoid) to chain without explicit type annotations. Haskell's strong type inference further reduces boilerplate, automatically deriving constraints and instances where possible, promoting concise, maintainable generic code.[90]
Genericity in Clean
Clean introduced support for parametric polymorphism through type variables and universal quantification in its early versions during the 1990s.[91] These features allow functions to operate uniformly across different types, such as the standardmap function defined as map :: (a -> b) [a] -> [b], which applies a transformation to each element of a list regardless of the specific types a and b.[91] Clean's type system combines this with rank-2 polymorphism via explicit universal quantifiers (e.g., A.a: [a] -> Int), supporting higher-order polymorphic functions.[91] Generic programming extensions, enabling datatype-generic functions, were added in the early 2000s.[92]
A distinctive aspect of genericity in Clean is its integration with dynamic types, which provide runtime polymorphism by allowing values of arbitrary types to be stored and manipulated dynamically.[93] This is achieved through the Dynamic type constructor, enabling flexible handling of heterogeneous data structures at runtime while maintaining compile-time type safety for static portions.[91] Additionally, generic replication and sharing are managed through uniqueness inference, where the type system tracks whether data is uniquely owned, optimizing memory usage by avoiding unnecessary copies during function applications.[91]
The key innovation lies in uniqueness typing, which permits destructive updates on uniquely referenced data within a purely functional paradigm, and this mechanism is fully parameterized over generic types.[91] Uniqueness is denoted by an asterisk (e.g., *{Int} for a unique integer), ensuring single-threaded access that allows in-place modifications without violating purity, as the data cannot be shared or observed elsewhere during the update.[91] This propagates through generic functions; for instance, a unique list can be updated destructively if the function consumes it without returning references to intermediate states.[91]
For example, consider a generic function for processing unique lists, such as an in-place reversal:
reverse :: ![a] -> ![a]
reverse list = reverseAcc list []
where
reverseAcc :: ![a] ![a] -> ![a]
reverseAcc [head:tail] acc = reverseAcc tail [head:acc]
reverseAcc [] acc = acc
reverse :: ![a] -> ![a]
reverse list = reverseAcc list []
where
reverseAcc :: ![a] ![a] -> ![a]
reverseAcc [head:tail] acc = reverseAcc tail [head:acc]
reverseAcc [] acc = acc
!) on the input list enables destructive reversal without copying, leveraging the type system's inference to ensure safety across any element type a.[91] Strictness annotations (!) can further optimize evaluation in generic contexts, combining with uniqueness for efficient I/O and array manipulations.[91]
Clean's genericity remains primarily utilized within Dutch academic and research communities, particularly at Radboud University Nijmegen where it originated, and it has not achieved the mainstream adoption of languages like Haskell.[93]
Genericity in ML
In Standard ML, genericity is achieved through functors, which are parameterized modules introduced as part of the language's module system in the 1980s to support large-scale functional programming.[94] Functors are parametric over structures (modules) and signatures (module interfaces), allowing the creation of reusable components that can be instantiated with different type parameters while preserving type safety.[94] This approach enables the definition of abstract data types that operate uniformly across compatible types, contrasting with ad-hoc polymorphism by enforcing structural constraints at the module level.[95] The mechanism of functors involves declaring a functor with a parameter structure that must match a specified signature, which defines the required types and operations. Upon instantiation, the functor is applied to an actual structure conforming to that signature, producing a new specialized structure where the parameter is substituted, often generating fresh type names to ensure abstraction (known as generative semantics).[94] For example, signature constraints ensure that the argument provides necessary elements, such as a comparison function, without exposing implementation details. This instantiation process supports modular composition, where the resulting structure can be further used in higher-level modules.[96] Key features of ML functors include support for higher-order functors in extended implementations, allowing functors to take other functors as parameters for more flexible parameterization.[97] They are commonly used to implement abstract data types, such as ordered sets parameterized by an element type with ordering. A representative example is a functor for a generic set structure requiring anORD signature for the key type:
signature ORD =
sig
type key
val compare : key * key -> order
end
signature SET =
sig
type elem
type set
val empty : set
val insert : elem * set -> set
val member : elem * set -> bool
(* other operations *)
end
functor MakeSet (OrdKey : ORD) : SET =
struct
type elem = OrdKey.key
type set = (* implementation, e.g., balanced tree *)
(* definitions using OrdKey.compare *)
val empty = (* ... *)
(* etc. *)
end
signature ORD =
sig
type key
val compare : key * key -> order
end
signature SET =
sig
type elem
type set
val empty : set
val insert : elem * set -> set
val member : elem * set -> bool
(* other operations *)
end
functor MakeSet (OrdKey : ORD) : SET =
struct
type elem = OrdKey.key
type set = (* implementation, e.g., balanced tree *)
(* definitions using OrdKey.compare *)
val empty = (* ... *)
(* etc. *)
end
MakeSet can be instantiated as IntSet = MakeSet (struct type key = int val compare = Int.compare end), yielding a type-safe integer set module.[98] This pattern abstracts common data structures while ensuring operations like insertion and membership rely on the provided ordering.[96]
In variants like OCaml, introduced in 1996 as a successor to Caml Light, the core functor system from Standard ML is retained and extended with features enhancing genericity, such as polymorphic variants (added in 2000) and generalized algebraic data types (GADTs, stabilized in 2011).[99][100] Polymorphic variants allow open sum types with subtyping, enabling more flexible variant handling without exhaustive matching, while GADTs permit constructors to refine type parameters based on values, supporting advanced type-level programming for generic algorithms.[101] These extensions build on functors to provide finer-grained genericity in practical applications, such as typed expression languages or heterogeneous collections.[101]
Genericity in Other Languages
Traits in Rust
Traits in Rust provide a mechanism for defining shared behavior across types, enabling generic programming while integrating deeply with the language's ownership and borrowing system. Introduced in Rust 1.0 on May 15, 2015, traits function similarly to interfaces in other languages by specifying methods and associated items that types can implement, but they include unique features like automatic derivation via macros and strict coherence rules to prevent implementation conflicts.[102] The coherence rules, including the orphan rule, ensure that trait implementations are defined either for types in the same crate or for local traits on foreign types, maintaining modularity and avoiding ambiguous resolutions.[103] Key features of Rust traits include trait bounds, which constrain generic parameters to types implementing specific traits, such asT: Clone + Debug in a function signature to require cloning and debugging capabilities.[104] Traits also support associated types, which allow a trait to define a placeholder type that implementors must specify, as in the Iterator trait's type Item; for the yielded element type.[105] Associated constants provide compile-time values, like const ID: u32 = 42;, and generic associated types (GATs), stabilized in Rust 1.65 on November 3, 2022, extend this by allowing associated types to be generic over lifetimes, types, or constants, such as type Assoc<'a, T>;.[106]
Rust's borrow checker enforces memory safety in generic code using traits by tracking lifetimes as parameters, ensuring that references in trait methods respect borrowing rules and prevent data races at compile time.[107] For instance, traits can declare lifetime parameters like trait Ref<'a> { type Out: 'a; }, where the associated type must outlive the specified lifetime, allowing the compiler to verify safe borrowing across generic implementations without runtime overhead.[108]
A representative example is the Vec<T> type, which implements the Iterator trait to enable iteration over its elements:
impl<T> Iterator for std::vec::IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
// Implementation yields T values
}
}
impl<T> Iterator for std::vec::IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
// Implementation yields T values
}
}
Vec<T> to be iterated generically for any T, with the borrow checker ensuring safe access to elements based on ownership.[109]
As of 2025, Rust Edition 2024 includes previews of a next-generation trait solver, which enhances expressiveness by improving resolution of complex trait bounds and associated types through better handling of normalization and caching, building on the foundational solver from earlier editions.[110][111]
Type Classes in Scala
Scala's type classes provide a mechanism for ad-hoc polymorphism, allowing developers to define behaviors for types without modifying their definitions or relying on inheritance hierarchies. Introduced in Scala 2.8 in 2010 through implicit parameters and classes, this feature draws inspiration from Haskell's type classes but adapts them to Scala's object-oriented and functional hybrid model. The foundational work, detailed in a 2010 paper by Oliveira, Moors, and Odersky, leverages implicits for automatic resolution of type class instances, enabling retroactive extension of closed types such as those from the standard library.[112] Key features include implicit resolution, which automatically selects appropriate instances based on type context, facilitating composable and modular code. This supports advanced constructs like higher-kinded types (HKTs), abstracting over type constructors (e.g., Functor[F[_]] for mapping operations across containers like List or Option). Libraries such as Cats extend this capability by providing a rich hierarchy of type classes for functional programming abstractions, including monads and applicatives, which promote reusable, type-safe designs.[113][114] Type classes integrate seamlessly with Scala's case classes via implicit instances, enabling the creation of type-safe domain-specific languages (DSLs) for tasks like serialization or validation. The Shapeless library further enhances this by offering generic programming tools that operate over nested structures, such as converting case classes to heterogeneous lists (HLists) for type-level operations while preserving safety.[115] A representative example is a generic JSON encoder, where a type classEncoder[A] defines how to serialize any type A to a JSON string:
trait Encoder[A] {
def encode(a: A): String
}
object Encoder {
def apply[A](implicit enc: Encoder[A]): Encoder[A] = enc
implicit val stringEncoder: Encoder[String] =
(s: String) => s'"$s"'
implicit val intEncoder: Encoder[Int] =
(i: Int) => i.toString
implicit def listEncoder[A](implicit enc: Encoder[A]): Encoder[List[A]] =
(l: List[A]) => "[" + l.map(enc.encode).mkString(", ") + "]"
}
case class User(name: String, age: Int)
implicit val userEncoder: Encoder[User] = (u: User) =>
s"""{"name": ${Encoder.stringEncoder.encode(u.name)}, "age": ${Encoder.intEncoder.encode(u.age)}}"""
trait Encoder[A] {
def encode(a: A): String
}
object Encoder {
def apply[A](implicit enc: Encoder[A]): Encoder[A] = enc
implicit val stringEncoder: Encoder[String] =
(s: String) => s'"$s"'
implicit val intEncoder: Encoder[Int] =
(i: Int) => i.toString
implicit def listEncoder[A](implicit enc: Encoder[A]): Encoder[List[A]] =
(l: List[A]) => "[" + l.map(enc.encode).mkString(", ") + "]"
}
case class User(name: String, age: Int)
implicit val userEncoder: Encoder[User] = (u: User) =>
s"""{"name": ${Encoder.stringEncoder.encode(u.name)}, "age": ${Encoder.intEncoder.encode(u.age)}}"""
Encoder[User].encode(User("Alice", 30)), yielding {"name": "Alice", "age": 30} without boilerplate for each type.[116]
In Scala 3, released in 2021, type classes benefit from Dotty compiler improvements, including the replacement of implicits with explicit given instances for clearer resolution and reduced ambiguity, alongside enhanced type inference and context functions for more intuitive higher-kinded type usage.[117][113]
Type Hints in Python
Python introduced type hints in version 3.5 through PEP 484, which proposed a syntax for adding optional type annotations to variables, function arguments, return types, and class attributes, enabling better static analysis and IDE support without altering runtime behavior. These hints leverage thetyping module to support generic programming in a dynamically typed language, allowing developers to define reusable components that work with multiple types while facilitating tools for early error detection. For instance, collections like List[T] where T is a type variable, enable parametric polymorphism adapted to Python's dynamic nature.
Key features for genericity include TypeVar for creating type variables, the Generic base class for defining generic classes, and Protocol for structural subtyping, which allows duck typing to be expressed statically without inheritance. With TypeVar, developers can bound types (e.g., T = TypeVar('T', int, str)) to restrict generics to specific kinds, while Generic[T] enables classes like a stack to handle any type T uniformly. Protocols support custom genericity by defining interfaces based on structure rather than nominal typing, such as a SupportsClose protocol for objects with a close() method, promoting flexible, interface-based polymorphism.
A practical example is a generic Stack class:
from [typing](/page/Typing) import TypeVar, Generic, [List](/page/List)
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: [List](/page/List)[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
from [typing](/page/Typing) import TypeVar, Generic, [List](/page/List)
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: [List](/page/List)[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
Stack[int] only accepts integers, aiding static checkers in verifying type safety.
Type hints remain optional at runtime, with no enforcement by the Python interpreter, relying instead on external tools like mypy for static type checking, which can catch type-related errors before execution but may not cover all dynamic behaviors. This approach avoids compile-time rigidity, suiting Python's scripting paradigm, though it can lead to inconsistencies if hints and runtime code diverge. As of Python 3.12, PEP 695 introduced syntactic sugar for generics, such as declaring type parameters directly in class or function definitions (e.g., class MyGeneric[T]: ... or def func[T](): ...), along with improved support for variance annotations like covariant and contravariant types, enhancing expressiveness without the typing module's verbosity. By November 2025, these features in Python 3.13 and later have seen broader adoption in libraries, with ongoing refinements in tools like mypy to handle the new syntax more efficiently.
Benefits and Applications
Advantages Over Traditional Polymorphism
Generic programming, through parametric polymorphism, promotes reusability by enabling a single implementation of algorithms and data structures to operate across diverse types without requiring manual duplication or type-specific overloads, unlike traditional subtype polymorphism that often necessitates inheritance hierarchies or explicit overrides. This approach allows developers to write versatile code once and instantiate it for multiple types, significantly reducing redundancy and enhancing library portability.[118] In terms of performance, generic programming leverages compile-time resolution, eliminating the runtime dispatch overhead inherent in subtype polymorphism's virtual function calls, which involve vtable lookups and indirection. This static binding facilitates aggressive compiler optimizations like inlining, resulting in faster execution and smaller code sizes compared to dynamic alternatives. For instance, redesigns using generic techniques in container iterators have demonstrated speedups of 1.2x to 2.1x in practical applications by avoiding virtual dispatch and unnecessary dependencies.[119] The following table summarizes key performance differences:| Aspect | Generic (Static) Polymorphism | Subtype (Dynamic) Polymorphism |
|---|---|---|
| Binding Time | Compile-time | Runtime |
| Dispatch Overhead | None (direct calls, inlining) | Runtime indirection (potential cache misses, hundreds of cycles) |
| Optimization Potential | High (monomorphization) | Limited (indirect calls) |
Real-World Use Cases
Generic programming has found widespread adoption in foundational libraries for data structures and algorithms. In C++, the Standard Template Library (STL) exemplifies this through its generic containers likestd::vector<T> and algorithms such as std::sort, which operate on any iterable type satisfying iterator requirements, enabling reusable code for tasks ranging from scientific simulations to embedded systems.[121] Similarly, the Java Collections Framework leverages generics introduced in Java 5 to provide type-safe implementations of interfaces like List<E> and Map<K,V>, facilitating efficient data management in enterprise applications such as banking software and e-commerce platforms where collections handle diverse object types without runtime type errors.[122]
In systems programming, Rust's standard library extensively employs traits and generics to ensure memory safety and performance in low-level components. For instance, collections like Vec<T> and HashMap<K,V> use generic types bounded by traits such as Clone and Hash, supporting the development of safe operating system kernels and drivers, as seen in projects like the Redox OS where generics abstract over hardware-specific data layouts.[123][124] The D programming language further demonstrates generics in performance-critical domains, with template metaprogramming enabling flexible entity-component systems in game engines like Dagon, a 3D rendering framework, and HipremeEngine, which supports cross-platform game development by parameterizing rendering pipelines over arbitrary types.[125]
Data science workflows benefit from generic programming through Python's evolving type system. Libraries like NumPy utilize generic array operations via type hints in recent versions, allowing functions such as numpy.array to handle scalar, vector, or matrix data with static type checking for better IDE support and error detection in analytical pipelines; Pandas supports type hinting through third-party tools like pandas-stubs and pandera, enabling annotations for specific column types and promoting reusable code for data manipulation in machine learning preprocessing tasks.[126]
In web and enterprise development, generics enhance abstraction in object-relational mapping (ORM) tools. Microsoft's Entity Framework Core in .NET uses generic repositories like DbSet<TEntity> where TEntity implements IEntity, enabling type-safe queries across diverse database schemas in large-scale applications such as CRM systems and supply chain management software.[127] In Scala, Akka's actor model incorporates type classes and generics for building type-safe distributed APIs; for example, typed actors with generic message handlers like Behavior[Command[T]] ensure compile-time verification of API contracts in microservices frameworks, as utilized in event-driven web services for real-time data processing.[128]
As of 2025, generic programming is increasingly applied in AI and machine learning libraries to handle polymorphic tensor operations. In Rust, crates like ndarray employ generics with trait bounds (e.g., ArrayBase<S, D> where D: Dimension) for efficient, type-safe multi-dimensional arrays used in neural network implementations, supporting hardware acceleration without boilerplate. Haskell's ecosystem advances this through type-indexed tensors in libraries like hmatrix, where type classes parameterize linear algebra routines over numeric types, facilitating verifiable ML models in functional pipelines.[129]
Challenges and Limitations
Implementation Trade-offs
Generic programming implementations involve several key design trade-offs that balance runtime behavior, compatibility, performance, and usability. One fundamental choice is between reification and type erasure for handling generic types at runtime. Reification, as implemented in the .NET Common Language Runtime (CLR), preserves full type information for generic instantiations, allowing distinct runtime representations for types likeList<string> and List<object>. This enables powerful reflection capabilities and avoids boxing for value types, supporting efficient polymorphic code that matches hand-specialized performance with only 10-20% overhead in allocations due to lazy instantiation. However, reification duplicates virtual method tables (vtables) for each unique instantiation, increasing memory usage compared to non-generic code.[130]
In contrast, type erasure, used in Java, removes generic type parameters during compilation, replacing them with their bounds or Object, which ensures backward compatibility with pre-generics bytecode and incurs no runtime overhead from type metadata. This approach maintains a single bytecode representation per generic class, preserving binary compatibility but limiting runtime introspection—generic types cannot be distinguished at runtime without additional mechanisms like annotations. Erasure also necessitates boxing primitives in generic collections, introducing allocation and unboxing costs that can degrade performance in numerical or high-throughput scenarios.[131][65]
Another trade-off concerns static versus dynamic resolution of generics. Static genericity, exemplified by C++ templates, performs monomorphization at compile time, generating specialized code for each type instantiation, which eliminates runtime polymorphism overhead and enables optimizations like inlining for zero-cost abstractions. This yields high performance but can lead to longer compile times and larger binary sizes due to code duplication. Dynamic genericity, as in Python's type hints combined with runtime duck typing, defers type resolution to execution, offering greater flexibility for heterogeneous data and easier prototyping without rigid compile-time checks. However, this incurs runtime type dispatching costs, contributing to slower execution compared to static approaches, though it enhances adaptability in exploratory or scripting contexts.[132]
Language designers also weigh expressiveness against simplicity in constraint systems. Rust's traits provide bounded polymorphism with associated types and methods, improving usability by enforcing requirements at compile time and enabling safe, zero-cost abstractions over diverse types. This expressiveness supports advanced patterns like iterator chains but adds complexity to the type system, requiring careful coherence rules to avoid ambiguity and increasing the learning curve for developers. Simpler systems, like early Java generics without bounds, prioritize ease of adoption but limit error detection until runtime.[102]
Performance implications further highlight these choices, particularly instantiation overhead in templates versus virtual calls in inheritance-based polymorphism. C++ template instantiation generates type-specific code, avoiding the indirection of virtual function lookups but potentially bloating executables; for instance, extensive use can significantly increase binary size in template-heavy libraries. Inheritance with virtual methods incurs predictable runtime dispatch overhead but shares code across types, reducing size at the cost of indirect calls that hinder optimizations like devirtualization.[133]
| Approach | Pros | Cons |
|---|---|---|
| C++ Monomorphization | Zero runtime overhead; full optimization per type; no boxing. | Compile-time explosion; binary bloat from code duplication. |
| Java Type Erasure/Boxing | Backward compatibility; single bytecode per class; no metadata overhead. | Limits reflection; boxing primitives causes allocation costs.[131] |
| .NET Reification | Exact runtime types; enables reflection; no boxing for values. | Memory increase from duplicated vtables; potential compatibility issues with legacy code.[130] |
Common Pitfalls
One common pitfall in generic programming arises from verbose and cryptic error diagnostics, particularly in languages like C++ where templates are instantiated only upon use, leading to delayed and nested error messages that span multiple files and obscure the root cause. For instance, a simple type mismatch in a template parameter can produce pages of compiler output referencing internal standard library implementations, making diagnosis time-consuming for developers.[38] The introduction of C++20 concepts addresses this by allowing explicit constraints on template parameters, enabling earlier validation and more readable error messages that pinpoint violations directly. Another frequent issue is infinite recursion in template metaprogramming, where recursive template instantiations lack a proper base case, resulting in excessive compile-time computation that exceeds the compiler's recursion limit—typically recommended to be at least 1024 levels by the C++ standard—or triggers undefined behavior if truly infinite. This often occurs in attempts to compute values at compile time, such as factorial implementations without specialization for zero, causing compilation failures with messages like "recursive template instantiation exceeded maximum depth." In languages with type-erased generics like Java, type instability emerges from variance handling via wildcards, where bounded types (e.g.,List<? extends Number>) permit subtyping flexibility but obscure the exact type at runtime, necessitating unsafe casts to access elements and risking ClassCastException if assumptions about the wildcard fail. This stems from Java's type erasure mechanism, which removes generic information post-compilation to maintain backward compatibility, forcing developers to insert explicit casts that bypass compile-time checks and introduce fragility.[134]
Over-generalization occurs when developers design excessively broad generic interfaces to maximize reusability, inadvertently exposing implementation details and violating encapsulation, as seen in C++ where returning specific iterator types (e.g., std::vector<T>::iterator) ties client code to internal choices like container selection, leading to recompilation cascades or broken functionality upon changes. Such designs prioritize parametric polymorphism over domain-specific constraints, resulting in cluttered APIs that complicate maintenance and obscure intended usage patterns.[135]
Tooling gaps further exacerbate these issues, particularly in older or less mature generic systems where integrated development environments (IDEs) struggle to provide accurate introspection, autocompletion, or refactoring for uninstantiated generic code, as type checking defers to instantiation and debuggers display expanded, verbose forms that do not align with source-level abstractions. In C++, for example, debugging heavily templated code often requires manual expansion or specialized tools, since standard IDE debuggers may fail to resolve dependent names or show meaningful stack traces without full instantiation.[136]