I just want to note that data types and pattern matching (to a first approximation) are just useful, but redundant language features that can be implemented exclusively using lambda calculus. If you understand how to implement them in lambda calculus, you can understand why they do not need Eq
to implement pattern matching.
The implementation of data types in lambda calculus is known as the “church coding” of them (after Alonzo Church , who demonstrated how to do this). The functions encoded by the Church are also known as the Continuation Style.
It is called a “continuation transfer style,” because instead of providing a value, you provide a function that will handle the value. So, for example, instead of giving the user Int
, I could instead give him a value of the following type:
type IndirectInt = forall x . (Int -> x) -> x
The above type is "isomorphic" Int
. Isomorphic is just a fancy way of saying that we can convert any IndirectInt
to Int
:
fw :: IndirectInt -> Int fw indirect = indirect id
... and we can convert any Int
to IndirectInt
:
bw :: Int -> IndirectInt bw int = \f -> f int
... such that:
fw . bw = id -- Exercise: Prove this bw . fw = id -- Exercise: Prove this
Using the continuation transfer style, we can convert any data type to the term lambda calculus. Let's start with a simple type like:
data Either ab = Left a | Right b
In the style of passing continuation, it will become:
type IndirectEither ab = forall x . (Either ab -> x) -> x
But Alonzo Church was smart and noticed that for any type with multiple constructors, we can simply provide a separate function for each constructor. Thus, in the case of the above type, instead of providing a function of type (Either ab → x)
, we can instead provide two separate functions, one for a
and one for b
, and that would be just as good:
type IndirectEither ab = forall x . (a -> x) -> (b -> x) -> x -- Exercise: Prove that this definition is isomorphic to the previous one
What about a type of type Bool
where the constructors have no arguments? Well, Bool
is isomorphic to Either()()
(Exercise: prove it), so we can just encode it as:
type IndirectBool = forall x . (() -> x) -> (() -> x) -> x
And () → x
just isomorphic to x
(Exercise: Prove it), so we can rewrite it as follows:
type IndirectBool = forall x . x -> x -> x
There are only two functions that can have the above type:
true :: a -> a -> a true a _ = a false :: a -> a -> a false _ a = a
Due to isomorphism, we can guarantee that all church encodings will have as many implementations as there were possible values of the original data type. Therefore, it is no coincidence that IndirectBool
has exactly two functions, exactly the same as exactly two constructors in Bool
.
But how do we compare patterns on our IndirectBool
? For example, with a regular Bool
we could simply write:
expression1 :: a expression2 :: a case someBool of True -> expression1 False -> expression2
Well, with our IndirectBool
it already comes with tools to deconstruct itself. We would just write:
myIndirectBool expression1 expression2
Note that if myIndirectBool
is true
, it will select the first expression, and if it is false
it will select the second expression, as if we somehow matched its value pattern.
Let's try to do the same with IndirectEither
. Using regular Either
, we would write:
f :: a -> c g :: b -> c case someEither of Left a -> fa Right b -> gb
With IndirectEither
we simply write:
someIndirectEither fg
In short, when we write types in the style of passing a continuation, the continuations are similar to the case statements of the case construct, so all we do is pass each individual case statement as arguments to the function.
For this reason, you do not need to make any sense in Eq
to match the type pattern. In the lambda calculus, the type decides what it is "equal to" by simply determining which argument it chooses from the ones given to it.
Therefore, if I am true
, I will prove that I am "equal" to true
, always choosing my first argument. If I am false
, I prove that I am "equal" to false
, always choosing the second argument. In short, the constructor "equality" comes down to "positional equality", which is always defined in the lambda calculus, and if we can distinguish one parameter as the "first" and the other as the "second", then all we need to have skill is " compare "constructors.