Create your own
Lesson illustration

Generic Functions with Type Variables

Hello! Welcome to the first lesson in our course on programming theory.

Given your background in mathematics and statistical science, and your interest in the theoretical foundations of programming, we'll start by building a solid base in the principles that underpin modern functional languages.

Today's lesson focuses on a cornerstone concept: parametric polymorphism. You'll learn how to write "generic" functions that can operate on values of any type, a feature crucial for writing reusable and abstract code. We'll explore how this is achieved using type variables, trace its origins to the ML language family, and touch upon its theoretical foundation in the lambda calculus.

This lesson corresponds to the first learning outcome in the "Polymorphism and Generic Programming" module: Apply parametric polymorphism to write generic functions, explaining its implementation via type variables as pioneered by the ML language family.

1. The Problem: The Need for Generic Code

Imagine you want to write a simple identity function, id(x), which just returns its argument x. In a dynamically typed language like Python, this is trivial:

def id(x):
    return x

id(10)      # returns 10
id("hello") # returns "hello"

However, in a statically typed language, the compiler needs to know the type of x at compile time. Without a mechanism for writing generic code, you would be forced to write a separate version of the function for each type you want to use it with:

// Hypothetical C-like language
int id_int(int x) { return x; }
char* id_string(char* x) { return x; }
// ... and so on for every type.

This is clearly repetitive and unscalable. It violates the "Don't Repeat Yourself" (DRY) principle and misses a fundamental abstraction: the logic of the identity function is the same regardless of the type. This is the problem that polymorphism solves.

2. The "What": Defining Polymorphism

The word "polymorphism" comes from Greek, meaning "many forms." In programming, it refers to the ability of a single piece of code (like a function or a data structure) to work with multiple types.

There are several kinds of polymorphism. For this lesson, we're focused on parametric polymorphism. To get a clear definition and see how it differs from other forms, please read the following resource.

Parametric polymorphism

This reading from the Wikipedia article on Parametric Polymorphism provides a concise definition and contrasts it with ad-hoc polymorphism. It also gives the historical context we need.

Please read the first two paragraphs of the introduction and the short 'History' section. Focus on the distinction between uniform behavior (parametric) and distinct definitions (ad-hoc), and note the mention of ML as the pioneer.

As you read, the key idea to grasp is that a parametrically polymorphic function behaves uniformly for all types. The id function doesn't inspect its argument or change its behavior based on whether it receives an integer or a string; it simply returns it. This is different from ad-hoc polymorphism (like operator overloading), where + does integer addition for numbers but string concatenation for strings—different behaviors for different types. We will explore ad-hoc polymorphism in our next lesson.

3. The "How": Type Variables in Action

Parametric polymorphism is achieved through the use of type variables. A type variable is a placeholder in a type signature that can be replaced by any concrete type.

The ML language family, which includes Standard ML and OCaml, pioneered this feature. In Haskell (a close relative of ML), the identity function's type is written as:

id :: a -> a

Here, a is a type variable. The signature a -> a can be read as: "a function that takes a value of some type a and returns a value of that same type a." When you call id 5, the compiler infers that a is Int. When you call id "hello", it infers a is String. The function's code remains the same.

This concept has been adopted by many modern languages. In Python, you can achieve parametric polymorphism explicitly using the typing module.

from typing import TypeVar

# Declare a type variable 'T'
T = TypeVar('T')

# Use 'T' in the function signature
def identity(x: T) -> T:
    return x

# Usage:
num = identity(10)        # Inferred type of num is int
text = identity("world")  # Inferred type of text is str

Here, T serves the same role as a in the Haskell example. It's a placeholder that allows the identity function to be type-checked generically, ensuring the input and output types match, whatever they may be for a specific call.

4. The Theoretical Underpinnings: System F

The formal basis for parametric polymorphism is a system called the polymorphic lambda calculus, or System F. Your background in mathematics and interest in formal systems makes this a particularly relevant area to explore.

System F extends the simply typed lambda calculus by allowing abstraction over types. Just as λx. t abstracts a term t over a value x, System F introduces a new construct, Λα. t (read "big lambda alpha dot t"), which abstracts a term t over a type variable α.

To use such a function, you first apply it to a concrete type. For example, our generic identity function in System F is:

id = Λα. λx:α. x

Its type is ∀α. α -> α, which reads "for all types α, a function from α to α."

To get an identity function for integers, you apply id to the type Int: id Int. This produces the specialized term λx:Int. x, which has the type Int -> Int.

The following reading provides a more detailed, yet accessible, introduction to these concepts.

Lecture 8: Polymorphism and System F

This excerpt from lecture notes by James Bornholt on 'Polymorphism and System F' explains the motivation for and the core mechanics of System F.

Please read the following sections: The introduction (up to 'Varieties of polymorphism'). 'Varieties of polymorphism' (to understand the context). 'Polymorphic types' (focus on the ∀α. T syntax). 'Type abstractions and applications' (focus on the Λα. t and t T syntax and the id and double examples). You can skim the formal rules and focus on grasping the concepts of type abstraction and type application.

The key takeaway is that System F provides the theoretical machinery to talk about and work with generic types in a sound and formal way. It's the foundation upon which the type systems of languages like Haskell and ML are built.

5. Implementation in Practice: Monomorphization

How does a compiler actually implement a generic function? One common technique, especially in ahead-of-time compiled languages, is monomorphization.

The compiler takes the generic function (polymorphic code) and generates a specialized, concrete version (monomorphic code) for each type it is used with. For our identity function, if you use it with Int and String, the compiler would generate two separate functions in the final machine code, effectively identity_int and identity_string. This process ensures static typing and high performance, as there's no runtime overhead to figure out types.

The following video clip explains this process clearly in the context of the Odin programming language.

Generic Odin procedures: Parametric polymorphism

This clip from a video by Karl Zylinski on generic procedures in Odin explains the concept of the compiler generating specialized versions of a generic function.

Watch the segment from 02:45 to 03:25. The speaker explains what happens when a generic clamp function is called with a specific type (integer). Focus on the idea that the compiler 'will generate different versions of clamp based on the parameter'.

This strategy contrasts with the implementation in languages like Java (using type erasure) or dynamically typed languages, where a single function body handles all types at runtime, often with performance trade-offs.

Exercise

Let's apply these concepts.

  1. In Python, write a generic function swap that takes a tuple of two elements and returns a new tuple with the elements swapped. Use TypeVar to make it fully generic, allowing the two elements to have different types.
  2. After writing the Python code, write down the polymorphic type signature for your swap function using the System F notation (, type variables, and ->).

Take about 10 minutes for this. Once you're done, you can check your solution below.

Solution
  1. Python implementation:

    from typing import TypeVar, Tuple
    
    # Declare two type variables for potentially different types
    T1 = TypeVar('T1')
    T2 = TypeVar('T2')
    
    def swap(pair: Tuple[T1, T2]) -> Tuple[T2, T1]:
        """Swaps the elements of a two-element tuple."""
        return (pair[1], pair[0])
    
    # Example usage:
    swapped_pair = swap((10, "hello")) # -> ("hello", 10)
    print(swapped_pair)
    # The inferred type of swapped_pair is Tuple[str, int]
    
  2. System F-style type signature:

    The function takes a pair of type (α, β) and returns a pair of type (β, α). Since this works for any two types α and β, the polymorphic type is:

    Here, × denotes a product type, which corresponds to a tuple.

Conclusion

In this lesson, we've explored the fundamentals of parametric polymorphism. Here are the key takeaways:

  • Purpose: Parametric polymorphism allows us to write generic, reusable code that works uniformly across multiple types, avoiding code duplication.
  • Mechanism: It is implemented using type variables, which act as placeholders for concrete types in function signatures and data structures.
  • Origin: The concept was pioneered in the ML language family and has since been adopted by many modern statically typed languages.
  • Theory: Its formal foundation is System F (the polymorphic lambda calculus), which extends lambda calculus with type abstraction (Λα. t) and type application (t T).
  • Implementation: A common compiler strategy is monomorphization, where specialized versions of generic code are generated for each concrete type used.

Next Lesson Preview:

In our next lesson, we will explore ad-hoc polymorphism. We'll see how it allows a single name (like the + operator) to refer to different implementations for different types, and compare its trade-offs with the uniform behavior of parametric polymorphism.

Can't find a good explanation? Sign up and we'll make it for you

Sign up