These are notes from 2018 when I was first introduced to Generic programming in Haskell.
Generic programming in Haskell refers to a style of programming that lets us abstract at the datatype level. For newcomers to the language, Generics in Haskell is not to be conflated with Java Generics or other languages that, confusingly enough, use overlapping terminology. In Haskell-land, generic programming instead allows us to abstract over the language’s fundamental building blocks: datatypes. The library powering generic programming, GHC.Generics, provides this capability by allowing developers to abstractly represent algorithms without tying their implementation to particular data types. Since Haskell follows a very regular structure, it provides us with primitives that can be used in conjunction with its expressive type system to auto-generate instances for any datatype. The benefit of this approach is that it reduces code duplication. By writing functions that can be used not only across different types, but across multiple types of types, a single algorithmic description can be used for different result programs. In this way, we gain the advantage of writing less boilerplate.
The idea of generalizing closely relates to the notion of polymorphism. Polymorphism comes in three variations: parametric, ad hoc, and polytypic polymorphism.
1. Parametric polymorphism
Parametric polymorphism is when a function or datatype can handle different input types, rather than binding a function to only accept a specific type of input. This means that it can be instantiated over any type. In Haskell, this means the function takes a type variable (ex.,
a) rather than a specific type (like
Int). An example of a parametrically polymorphic type is list (
[a]), where a can be instantiated to any number of types, denoting elements of
String, etc. An example of a parametrically polymorphic function is
length; it provides a single implementation that works with a variety of types, returning the number of elements of a finite, foldable structure.
2. Ad-hoc polymorphism
Ad hoc polymorphism denotes functions that may have heterogeneous implementations. This is when we override functions to be used in different contexts. Unlike parametric polymorphism which uses the same function definition, ad-hoc polymorphism requires we change the underlying implementation to make it work for different arguments. In Haskell, this is when we speak in terms of typeclasses. Each typeclass has a set of methods that can be adapted for various purposes depending on the context.
3. Polytypic polymorphism
This is the generic variety of polymorphism. A polytypic function is more general than a polymorphic one because rather than at the type level, it is abstracted a further level up. A datatype-generic function is a function with instances for some type constructors needed to create a generic representation type, such as
Rep T. While both GHC.Generics and polymorphism provide abstractions, they are not to be conflated. The main difference is that polymorphism allows us to abstract at the level of type variables, whereas generic programming is about being abstract at the level of data types.
Contrasting similar concepts
You may be familiar with concepts either within Haskell or other languages similar to generic programming. The following section compares and constrasts similarities and differences between vaguely reminiscent of generic programming in Haskell.
Metaprogramming vs. Generic programming
Generic programming looks very similar to metaprogramming. The key difference is that generic programming is a compile-time operation, whereas metaprogramming occurs at run-time.
Java’s reflection API is comparable to Haskell’s
java.lang.reflect package allows users to modify the behavior of data structures at run-time. By contrast, Haskell’s generic programming occurs at compile-time.
Template Haskell vs. GHC.Generics
Template Haskell provides compile-time metaprogramming. Template Haskell allows us to write several syntactically different programs at once by creating one single meta program. There are significant differences between Template Haskell and
GHC.Generics, the most notable of which is that
GHC.Generics is used to produce the shape of the data. In contrast, Template Haskell is used to generate Haskell code. Another difference is that Template Haskell is compile-time metaprogramming, while
GHC.Generics is run-time.
|Pros||exploded view provides a clearer way to inspect behavior in GHCI||very flexible; requires less code; can automatically synthesize data types and generate functions and instances given required parameters|
|Cons||more code||more opaque; difficult to inspect in GHCi|
Fashion analogies for understanding polymorphism
Unfortunately, my brain is not wired to digest straightforward definitions of complex topics. Instead, it must process them into bizarre, contrived, and often inaccurate analogies for a concept to stick. I was thinking about generic programming while picking out an outfit, so I’ll wrap up with the following analogy relating outfit decisions and polymorphism:
- Parametrically polymorphic functions have a go-to outfit: they just wear athleisure dress-yoga pants everywhere with different shirts and blouses (you know, the ones that cleverly disguise the athleisure as something more formal and put-together), using a single abstract implementation for different types of activities.
- Ad hoc polymorphism, by contrast, is when your function changes outfits depending on the types of activities it does. It needs a whole new outfit implementation for different purposes. But the good news is, it can be used for different activities!
- Polytypicity extends this analogy to mean the function allows its outfit to put itself together based on instructions provided to a stylist (AI or human). You provide a definition of your personal preferences, the weather, the activities you intend to do in said outfit, and the trends at the time, and the stylist uses derives an outfit based on induction.