A brief expansion of jozefg's answer ...
Primops are precisely those operations that are supplied by the runtime because they can't be defined within the language (or shouldn't be, for reasons of efficiency, say). The true purpose of GHC.Prim
is not to define anything, but merely to export some operations so that Haddock can document their existence.
The construction let x = x in x
is used at this point in GHC's codebase because the value undefined
has not yet been, um, "defined". (That waits until the Prelude.) But notice that the circular let
construction, just like undefined
, is both syntactically correct and can have any type. That is, it's an infinite loop with the semantics of ⊥, just as undefined
is.
... and an aside
Also note that in general the Haskell expression let x = z in y
means "change the variable x
to the expression z
wherever x
occurs in the expression y
". If you're familiar with the lambda calculus, you should recognize this as the reduction rule for the application of the lambda abstraction \x -> y
to the term z
. So is the Haskell expression let x = x in x
nothing more than some syntax on top of the pure lambda calculus? Let's take a look.
First, we need to account for the recursiveness of Haskell's let expressions. The lambda calculus does not admit recursive definitions, but given a primitive fixed-point operator fix
,1 we can encode recursiveness explicitly. For example, the Haskell expression let x = x in x
has the same meaning as (fix \r x -> r x) z
.2 (I've renamed the x
on the right side of the application to z
to emphasize that it has no implicit relation to the x
inside the lambda).
Applying the usual definition of a fixed-point operator, fix f = f (fix f)
, our translation of let x = x in x
reduces (or rather doesn't) like this:
(fix \r x -> r x) z ==>
(\s y -> s y) (fix \r x -> r x) z ==>
(\y -> (fix \r x -> r x) y) z ==>
(fix \r x -> r x) z ==> ...
So at this point in the development of the language, we've introduced the semantics of ⊥ from the foundation of the (typed) lambda calculus with a built-in fixed-point operator. Lovely!
We need a primitive fixed-point operation (that is, one that is built into the language) because it's impossible to define a fixed-point combinator in the simply typed lambda calculus and its close cousins. (The definition of fix
in Haskell's Prelude doesn't contradict this—it's defined recursively, but we need a fixed-point operator to implement recursion.)
If you haven't seen this before, you should read up on fixed-point recursion in the lambda calculus. A text on the lambda calculus is best (there are some free ones online), but some Googling should get you going. The basic idea is that we can convert a recursive definition into a non-recursive one by abstracting over the recursive call, then use a fixed-point combinator to pass our function (lambda abstraction) to itself. The base-case of a well-defined recursive definition corresponds to a fixed point of our function, so the function executes, calling itself over and over again until it hits a fixed point, at which point the function returns its result. Pretty damn neat, huh?