// Type-level Programming: A Most Trivial Example
#import "/template-en.typ": doc-template
#doc-template(
title: "Type-level Programming: A Most Trivial Example",
date: "May 17, 2025",
body: [
This article will use Haskell.
First, enable some extensions:
```
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DataKinds#-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -Werror=incomplete-patterns #-}
import Data.Type.Equality
```
= Zero and One
Everything in a computer is "zero" and "one":
```
data Bit = Zero | One
```
Create a type for each of the two values in the `Bit` type, "zero" and "one," which are singleton types:
```
-- Prefix S is an abbreviation for Singleton
data SBit :: Bit -> * where
SZero :: SBit 'Zero
SOne :: SBit 'One
```
= NAND Gate
All boolean circuits can be completed with NAND gates, but we must first define the NAND gate:
```
type family Nand (a :: Bit) (b :: Bit) :: Bit where
Nand 'One 'One = 'Zero
Nand _ _ = 'One
```
`type family` gives the "definition," which can also be called a "specification" (Spec). Whether it is proof or checking, the program must comply with the standard here.
Then we can implement it:
```
nandGate :: SBit a -> SBit b -> SBit (Nand a b)
nandGate SOne SOne = SZero
nandGate SZero _ = SOne
nandGate _ SZero = SOne
```
= NOT Gate
Zero becomes one, and one becomes zero; this is the NOT gate. Therefore, give the definition:
```
type family Not (a :: Bit) :: Bit where
Not 'One = 'Zero
Not 'Zero = 'One
```
A NOT gate can be implemented using a NAND gate:
```
notGateImpl x = nandGate x x
```
Submit the implementation to the compiler:
```
notGate :: SBit a -> SBit (Not a)
notGate = notGateImpl
```
Rejected by the compiler:
```
Couldn't match type: Nand a a
with: Not a
Expected: SBit a -> SBit (Not a)
Actual: SBit a -> SBit (Nand a a)
```
`notGateImpl` returns `SBit (Nand a a)`, while what `notGate` expects is `SBit (Not a)`. We need to prove that `SBit (Not a)` and `SBit (Nand a a)` are the same thing. In this way, conversion can be achieved.
There are two ideas here. The first is to use pure force to crush it, proving by brute-force enumeration that this thing is absolutely true:
```
notGateProof :: SBit a -> SBit (Nand a a) :~: SBit (Not a)
notGateProof SOne = Refl
notGateProof SZero = Refl
```
Then use this proof to achieve safe type conversion:
```
notGate :: SBit a -> SBit (Not a)
notGate x = castWith (notGateProof x) (notGateImpl x)
```
The NOT gate is complete.
The second idea is to settle for the second best and only add a type constraint:
```
notGate :: (SBit (Nand a a) ~ SBit (Not a)) => SBit a -> SBit (Not a)
notGate x = nandGate x x
```
This also passes compilation, and the code is relatively simple.
However, this is more like a "static check." For example, if we deliberately write a wrong implementation:
```
notGate :: (SBit 'Zero ~ SBit (Not a)) => SBit a -> SBit (Not a)
notGate x = SZero
```
Obviously, `SBit 'Zero` and `SBit (Not a)` are not equivalent when `a` is `'Zero`, but it still passes compilation.
If a `notGate` call where `x` is `SOne` appears in the program:
```
zero = notGate SOne
```
Because the condition happens to be met, it still passes compilation.
Only when a `notGate` call where `x` is `SZero` appears in the program:
```
one = notGate SZero
```
will the problem be found in type checking. At this time, the compiler will report an error:
```
• Couldn't match type ‘'Zero’ with ‘'One’
arising from a use of ‘notGate’
• In the expression: notGate SZero
In an equation for ‘one’: one = notGate SZero
```
Since the second method is simpler, it will not be repeated later, and only the first one will be introduced. From a practical point of view, proofs for the first method are often difficult to give, and most of the time "static checks" like the second method are enough.
= AND Gate
What is an AND gate? One and one makes one, and the rest are zero:
```
type family And (a :: Bit) (b :: Bit) :: Bit where
And 'One 'One = 'One
And _ _ = 'Zero
```
A NAND gate plus a NOT gate is an AND gate:
```
andGateImpl a b = notGate $ nandGate a b
```
Use enumeration to brute-force proof:
```
andGateProof :: SBit a -> SBit b -> SBit (Not (Nand a b)) :~: SBit (And a b)
andGateProof SZero SOne = Refl
andGateProof SZero SZero = Refl
andGateProof SOne SOne = Refl
andGateProof SOne SZero = Refl
```
Submit to the compiler:
```
andGate :: SBit a -> SBit b -> SBit (And a b)
andGate a b = castWith (andGateProof a b) (andGateImpl a b)
```
The AND gate is complete.
= OR Gate
As usual, first give the definition: zero and zero make zero, and the rest are one:
```
type family Or (a :: Bit) (b :: Bit) :: Bit where
Or 'Zero 'Zero = 'Zero
Or _ _ = 'One
```
Try a fancy implementation method. Use De Morgan's laws to combine AND gates and NOT gates into OR gates:
```
orGateImpl a b = notGate (andGate (notGate a) (notGate b))
```
Continue with brute-force enumeration to prove I'm right:
```
orGateProof :: SBit a -> SBit b
-> SBit(Not (And (Not a) (Not b))) :~: SBit (Or a b)
orGateProof SZero SZero = Refl
orGateProof SZero SOne = Refl
orGateProof SOne SZero = Refl
orGateProof SOne SOne = Refl
```
The compiler accepts it:
```
orGate :: SBit a -> SBit b -> SBit (Or a b)
orGate a b = castWith (orGateProof a b) (orGateImpl a b)
```
= Conclusion
This is a very trivial example, but it uses quite a few language features, which can be considered a summary of my recent learning. However, if I want to implement non-trivial examples, I often need to use some very complex and ugly Template Haskell libraries, which I don't really want to touch. So I'm planning to look into Lean 4 later.
])
Email: i (at) mistivia (dot) com