Haskell’s runtime system (RTS) is a collection of mechanisms and optimizations that support the execution of Haskell programs, which are typically lazy and pure. The RTS provides features such as garbage collection, lightweight concurrency, profiling, and parallelism. In this answer, we will focus on thunks, lazy evaluation, and the spine, which are key concepts in Haskell’s RTS.
Lazy Evaluation and Thunks
Haskell uses lazy evaluation to avoid computing the values of expressions until they are needed. Lazy evaluation enables sharing of intermediate results and allows working with infinite data structures (e.g., infinite lists). However, it also necessitates the tracking of unevaluated expressions in the RTS. These unevaluated expressions are called "thunks."
A thunk is a closure that captures the environment required to evaluate an expression. When an expression is demanded for the first time, the RTS evaluates the thunk, replaces it with its value, and then uses the value for future computations. Thunks serve as a memoization technique.
For example, consider the following lazy list:
ones = 1 : ones
The list is an infinite sequence of 1’s:
ones = 1 : (1 : (1 : ...
In memory, ‘ones‘ would be represented by a thunk. When the first element is demanded, the thunk for ‘ones‘ evaluates to ‘1 : ones‘. The list would look like this:
┌─────┐
│ 1 │
└─────┘
:
v
┌─────┐
│ THUNK(ones) │
└─────┘
The Spine
The spine in Haskell refers to the structure of a (possibly lazy) data structure, such as a list or a tree, where the "spine" is the skeleton of the data structure, and the "values" are the individual elements within the data structure. We can think of the spine as the "shape" or "structure" of the data structure.
Consider the following list:
xs = [1,2,3]
The spine of ‘xs‘ consists of the ‘:‘ constructors and the empty list ‘[]‘.
┌───┐ ┌───┐ ┌───┐
(:)───(:)───(:)───[]
│ │ │
v v v
1 2 3
When dealing with lazy data structures, the spine and the values may be partially or fully represented by thunks. As elements are demanded, those thunks are evaluated, gradually building the spine and the values.
For example:
lazyList = [1..]
The spine and values of ‘lazyList‘ are both represented by thunks:
┌─────┐
│ THUNK │ => 1 : THUNK
└─────┘
When the first element is demanded, the first thunk evaluates to ‘1 : THUNK‘, revealing part of the spine and the next value:
┌───┐ ┌─────┐
(:)───TAIL
│
v
1
I hope this explanation clarifies how Haskell’s runtime system supports lazy evaluation by using thunks to represent unevaluated expressions and the concept of the spine to understand the structure of data types.