Breaking apart the Haskell type class
Type classes are perhaps the most distinctive feature of Haskell. I’ve found them pretty confusing, but in reality they are an incredibly elegant solution to a certain problem found in functional languages.
Why?
In Haskell, we can write monomorphic functions which accept concrete types.
As an example, the function add
shown below adds together two values of type Int
.
We can also create functions which are polymorphic.
The signatures of such functions contain type variables.
As an example, the function head
shown below has a type variable a
, which means that it accepts lists of any type a
.
However, there are situations where we want to create functions that accept a specific set of different types.
For example, a function should work with both Int
and Float
values:
Using a type variable like a
in the function’s signature would make it too permissive, allowing unintended types like String
.
To address this issue, Haskell introduces a concept called “type classes”.
In this case the types Int
and Float
are instances of the Num
type class.
By specifying the Num a => ...
constraint in the function’s signature, we declare that the function should be polymorphic for instances of the Num
type class.
How it works
You can visualize a type class as a set of operations defined for a given type.
The Num
type class, for example, includes operations such as addition, multiplication, and negation:
In this class declaration, each operation is like a record field for the type. It’s as if there’s a data type hidden behind the scenes, and each operation is a selector function for that data type:
Effectively, Num a
serves as a table of operations for the Num
class for type a
.
This elegant mechanism allows Haskell to implement polymorphism with type classes while maintaining strict type safety.
Kinds
A type in Haskell is a classification that defines what kind of data a value can represent.
For example, Int
is a type that represents integer values, and Float
is a type for floating-point numbers.
Kinds are a higher-level classification system for types in Haskell. Every type in Haskell has a kind, and this kind represents the “type of types”.
The notation for kinds in Haskell uses an asterisk *
to represent the most basic kind, which corresponds to concrete types.
You can check the kind of any type by using the :kind
command in GHCi.
The kind of Int
is *
, indicating that Int
is a concrete type.
Similarly, the kind of Float
is *
, signifying that it is also a concrete type.
However, Haskell’s type system can go beyond basic types and deal with more complex constructs.
For instance, type classes introduce a kind called Constraint
, denoted as =>
.
The kind Constraint is used to represent constraints on types and is commonly encountered when defining type classes and their instances.
The kind of Num
is * -> Constraint
, showing that Num
is a type class that takes a type (like Int
) as an argument.
When Num
is applied to Int
, it becomes a constraint on the Int
type, indicating that Int
is an instance of the Num
type class.
Another example of a kind in Haskell is the kind of unary type constructors * -> *
.
These unary type constructors are similar to generic types in other languages because they accept a type parameter and produce a new type.
The signature * -> *
signifies that the type constructor transforms one concrete type into another concrete type.
An example of a trivial type constructor Item
is shown below.
In this definition, Item
is a unary type constructor because it takes a type parameter a
and produces a new type Item a
, where a
can be any concrete type.
A commonly used example of a unary type constructor in Haskell is the Maybe
type.
Maybe
is used for representing optional values and handling potentially missing or undefined data in a type-safe manner.
Its definition is as follows:
Haskell type constructors can be parameterized with more than one type variable, as seen in the case of binary type constructors.
An example of a binary type constructor is the Either
data type:
In this definition, Either
is a binary type constructor because it requires two type parameters, a
and b
, to create a new type, Either a b
.
The Either
type is often used for scenarios where a value can have one of two possible types, such as representing success or failure or providing an alternative to error handling.
Conventionally, the Left
value indicates error conditions, while the Right
value signifies valid or correct values.
Higher Kinded Types
In Haskell, understanding the concept of “Higher Kinded Types” is crucial for grasping how certain more abstract type classes work. HKTs involve type classes that are parameterized by type constructors, rather than just simple types.
The Functor
type class is an example of a HKT class.
It defines two fundamental operations, fmap
and <$
, which operate on a type constructor f
.
The Functor type class is defined as follows:
The Functor
type class is not constrained to specific types but is rather parameterized by type constructors.
Its kind signature is (* -> *) -> Constraint
, indicating that it works with type constructors.
This allows it to be applied to types like Maybe
, List
, or other container types that accept a type argument.
Similarly to Functor
, the Monad
type class is another example of a HKT type class.
It defines functions for monadic operations, such as >>=
, >>
, and return
.
The definition of the Monad
type class is as follows:
Like the Functor
type class, the Monad
type class is not tied to specific types but is parameterized by type constructors.
Its kind signature is also (* -> *) -> Constraint
.
It is worth noting that type classes with a kind of (* -> *) -> Constraint
can be applied to kinds like * -> * -> *
.
An example of this is the Functor
instance of Either
:
In the instance declaration above, we specify that, for any type a
, Either a
is an instance of the Functor
type class.
This allows you to use the fmap
function with Either a
, providing a way to map over the second type argument of the Either
type while keeping the first type argument fixed.
Other languages
Scala
scala
Rust
rust
Bibliography
- P. Hudak, J. Hughes, S. Peyton Jones, and P. Wadler, “A History of Haskell: Being Lazy with Class,” in Proceedings of the Third ACM SIGPLAN Conference on History of Programming Languages, in HOPL III. San Diego, California: Association for Computing Machinery, 2007, pp. 12–11. doi: 10.1145/1238844.1238856.
- S. Peyton Jones, “Escape from the ivory tower: the Haskell journey.” [Online]. Available: https://youtu.be/re96UgMk6GQ
- R. Skinner, Effective Haskell: Solving Real-World Problems with Strongly Typed Functional Programming. in Pragmatic Bookshelf. O'Reilly Media, 2023.