Stupid Haskell Trick: Factorials in the Type System

Jul 21, 2010 23:59



Ahh, summer! A time for backyard barbeques, ice cold beer, baseball games, and playing with experimental Haskell type system extensions. Or in my case, just the latter. In this article I'll show you how to compute factorials using Haskell's type system, i.e. at compile time.

Why would we want to do this? you may well ask. The real reason is "because it's neat". This is a really beautiful construction; it took my breath away when I figured it out. But another reason is that the techniques I'll illustrate are useful in many other contexts. For instance, you can use something like this to define a data type of a list which carries its length around with it (so that e.g. taking the head of an empty list is a compile-time error). This is very powerful; the more errors you can check at compile-time, the better.

Nothing in this article is even remotely original. The direct inspiration for it came from a blog post by Brent Yorgey, which I've shamelessly stolen from and extended (thanks, Brent!). The indirect inspiration was from some comments on my previous blog post on Scala, which suggested that I look at a paper on embedding object-oriented programming in Haskell. That paper, called Haskell's Overlooked Object System, by Oleg Kiselyov and Ralf Lammel, is an amazing piece of work. In order to understand it fully, though, you first have to read another paper called Strongly Typed Heterogeneous Collections, by Oleg, Ralf and Keean Schupke. So that's what I did, and in the process learned quite a bit, which I'll use to good effect below. Both of these papers are strongly recommended if you're ready for them and if you enjoy having your mind blown.

To understand this article you need to know Haskell up to and including polymorphic types and (single parameter) type classes. I'll explain everything else as we go. If you want to run the examples you'll need the GHC Haskell compiler (Hugs may also work, but I haven't tried it). I used GHC version 6.12.1 to test this code.
Step 1: Factorial using integers

Everybody knows that you can trivially define the factorial function on integers as follows:

factorial :: Integer -> Integer factorial 0 = 1 factorial n = n * factorial (n - 1)
Let's test this using ghci:

ghci> factorial 0 1 ghci> factorial 10 3628800 ghci> factorial 100 93326215443944152681699238856266700490715968264381621468592963895 21759999322991560894146397615651828625369792082722375825118521091 6864000000000000000000000000
OK, cool. I could easily generalize this to work on any argument whose type is an instance of the Num type class, but that won't be necessary in what follows.
Step 2: Factorial using natural numbers

One problem with the above definition is that it will go into an infinite loop if the argument to factorial is less than 0. You can always add an error clause:

factorial :: Integer -> Integer factorial n | n < 0 = error "invalid argument" factorial 0 = 1 factorial n = n * factorial (n - 1)
but an alternative approach is to come up with a data definition for natural numbers i.e. integers that cannot be less than zero. One way to do this is to define naturals in terms of a zero value and a successor function. We can do this as follows:

data Nat = Zero | Succ Nat deriving (Show)
This states that a Nat value is either Zero or the successor (Succ) of another Nat. The deriving (Show) part is just to allow it to be printable at the ghci prompt. Let's test it:

ghci> Zero Zero ghci> Succ Zero Succ Zero ghci> Succ (Succ Zero) Succ (Succ Zero)
Let's add some functions to convert back and forth between Integers and Nats:

natToInteger :: Nat -> Integer natToInteger Zero = 0 natToInteger (Succ n) = 1 + natToInteger n integerToNat :: Integer -> Nat integerToNat n | n < 0 = error "invalid argument" integerToNat 0 = Zero integerToNat n = Succ $ integerToNat (n - 1)
Now we can write:

ghci> natToInteger (Succ (Succ (Succ Zero))) 3 ghci> integerToNat 7 Succ (Succ (Succ (Succ (Succ (Succ (Succ Zero))))))
This encoding of natural numbers is called a "unary" encoding. As you can imagine, it's not terribly efficient, but it does work.

To define a factorial function in terms of Nats, we need to have Nat equivalents of the integer functions used in the factorial definition. We can't just re-use e.g. the * operator:

ghci> Succ Zero * Succ Zero :1:0: No instance for (Num Nat) arising from a use of `*' at :1:0-20 Possible fix: add an instance declaration for (Num Nat) In the expression: Succ Zero * Succ Zero In the definition of `it': it = Succ Zero * Succ Zero
As the error message states, we could fix this by making Nats an instance of the Num type class, but there is a simpler way: just define the necessary operations directly on Nats.

Let's start with addition of Nats:

plus :: Nat -> Nat -> Nat plus Zero n = n plus (Succ m) n = Succ (plus m n)
The first clause states that adding Zero to any Nat n gives back n. Let's see:

ghci> plus Zero (Succ (Succ Zero)) Succ (Succ Zero)
The second clause states that adding the successor of the Nat m to another Nat n is equivalent to the successor of the sum of m and n. In other words: (m + 1) + n = 1 + (m + n). Let's check:

ghci> plus (Succ (Succ Zero)) (Succ (Succ Zero)) Succ (Succ (Succ (Succ Zero)))
As George Orwell states:

Freedom is the freedom to say that two plus two make four. If that is granted, all else follows.

Nice to see that Haskell doesn't violate Orwell's prime directive. Also, since every recursive call (in the second clause) reduces the first argument by one, this function will always terminate.

From here, it's a simple matter to define multiplication on Nats:

mult :: Nat -> Nat -> Nat mult Zero n = Zero mult (Succ m) n = plus n (mult m n)
The first clause is obvious, and the second just says that (m + 1) * n = n + (m * n).

And with this, it's easy to define factorial on Nats:

fact :: Nat -> Nat fact Zero = Succ Zero fact (Succ n) = mult (Succ n) (fact n)
You should be able to figure out why this works. Let's check it:

ghci> fact Zero Succ Zero ghci> fact (Succ (Succ (Succ Zero))) Succ (Succ (Succ (Succ (Succ (Succ Zero)))))
We can use our natToInteger and integerToNat functions to test larger numbers conveniently:

ghci> natToInteger $ fact $ integerToNat 8 40320
This just says that the factorial of 8 is 40320, which is the case.

This unary way of doing factorials is what we are going to compute using Haskell's type system. But first, I need to show you how to rewrite this in a new and (probably) unfamiliar way.
Step 3: Rewriting into relational style

We normally write a function definition with the function name and arguments on the left-hand side and the result on the right-hand side. But we don't have to do it that way (at least not in theory; in Haskell we do have to write it that way). We could write it in "relational style" where the arguments and return values are treated the same syntactically. (The logic programming language Prolog uses a relational style, so if you hate this, you know what to avoid.) Let's go back to our original factorial function:

factorial :: Integer -> Integer factorial 0 = 1 factorial n = n * factorial (n - 1)
In relational style, this would look something like this:

-- factorial in relational style -- NOTE: This is not legal Haskell syntax! factorialR :: Integer, Integer -- factorial relates two integers factorialR 0 1 -- factorial of 0 is 1 factorialR n (n * r) -- factorial of n is n * r for some r where factorialR (n - 1) r -- r is the factorial of (n - 1)
You would use this to compute a factorial as follows:

factorialR 10 n
and then n would get bound to the correct result, which is 3628800.

This is critical; if you don't understand this, the rest of the article will be incomprehensible. When using relational style you can't write inline function calls; each "relational call" has to have its result bound to a specific name (for instance, r in the last line of factorialR), and that name can be used elsewhere (for instance, in the second-last line of factorialR). Getting the hang of writing functions in this style takes some practice.

We can also write out our functions on Nats in relational style. As I said above, this is not Haskell syntax, though I hope it's clear what I'm trying to do.

Addition:

-- functional style: plus :: Nat -> Nat -> Nat plus Zero n = n plus (Succ m) n = Succ (plus m n) -- relational style, not Haskell syntax: plusR :: Nat, Nat, Nat -- plusR relates three natural numbers plusR Zero n n -- 0 plus any n is n plusR (Succ m) n (Succ r) -- (m + 1) + n is (r + 1) where (plusR m n r) -- where r is m + n
Multiplication:

-- functional style: mult :: Nat -> Nat -> Nat mult Zero n = Zero mult (Succ m) n = plus n (mult m n) -- relational style, not Haskell syntax: multR :: Nat, Nat, Nat -- multR relates three natural numbers multR Zero n Zero -- 0 times any n is 0 multR (Succ m) n s -- (m + 1) * n is s where plusR n r s -- where s is n + r where multR m n r -- where r is m * n
Factorial:

-- functional style: fact :: Nat -> Nat fact Zero = Succ Zero fact (Succ n) = mult (Succ n) (fact n) -- relational style, not Haskell syntax: factR :: Nat, Nat -- factR relates two natural numbers factR Zero (Succ Zero) -- factorial of 0 is 1 factR (Succ n) s -- factorial of (n + 1) is s where multR (Succ n) r s -- where s is (n + 1) * r where factR n r -- where r is factorial of n
Well, that was a bit of a pain, but the good news is that if you've understood everything so far, the rest will be comparatively easy.
Multi-parameter type classes

When encountering Haskell's type classes for the first time, you probably saw what are called "single-parameter type classes". For instance, let's look at the Eq type class, which is used for data types which can support equality comparisons:

class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool
Note the a in Eq a. This refers to the type for which equality is to be defined. For instance, you might have:

instance Eq Integer where -- Integer-specific definitions of == and /=
So here Integer is the single parameter of the type class Eq. If you think about it, what Eq does is specify that certain types are equality-comparable, and any single-parameter type class separates the set of all types into two groups: those which are instances of that type class and those which aren't. You can also think of single-parameter type classes as defining a predicate on types: those types which have instances of that type class are the ones satisfying the predicate.

It seems logical that if there are single-parameter type classes, there might be situations that call for multi-parameter type classes. For instance, a hypothetical type class Foo with two parameters might look like this:

class Foo a b where -- some functions involving types a and b
To instantiate this, you would need to give two classes:

instance Foo Integer Integer where -- some definition of Foo for Integers as types a and b instance Foo Double Double where -- some definition of Foo for Doubles instance Foo Integer Double where ...
etc. You can think of Foo (and other multi-parameter type classes) as defining not predicates on types, but relations between types. This will become important below.
Step 4: Natural numbers at the type level

Let's say we want to define natural numbers at the type level. At the value level, we defined an algebraic data type:

data Nat = Zero | Succ Nat
What would the type equivalent of this be? We would need one type for each value. So we have to have a type corresponding to Zero and, for every type-level natural number N, then something like Succ N would have to be a type-level natural too. Let's do this.

First, defining a zero value at the type level is not hard. For instance, you could write something like this:

data Z = ZeroVal
Z will be our type-level zero. It turns out that we won't care about the value ZeroVal of the type Z at all, so the ZeroVal constructor is redundant (it'll never be used). That being the case, we can just write:

data Z
For this to work, we have to enable the ghc extension EmptyDataDecls, which stands for "empty data declarations". The data Z above is an empty data declaration; it defines a type Z which has no constructors. You might think that this makes the type completely useless because it has no corresponding values, but (a) a type doesn't have to have values in order to be useful, and (b) there is one special value called undefined which is a value of every possible type. (In type theory, undefined is called "bottom"). You can't use undefined for anything at the value level, because evaluating it sends Haskell into an infinite loop, but you can use it for type hacking as we'll see.

You can actually see the Z type in ghci:

ghci> :t undefined :: Z undefined :: Z :: Z
The compiler is telling you "undefined, which you are telling me is of type Z, is computed to be of type Z". Nice job, compiler! This may seem totally useless, but it will end up being the way we can check our type-level computations later on, and the results will not be trivial.

By the way, inside a Haskell source code file, here's how to specify the extension:

{-# LANGUAGE EmptyDataDecls #-}
This is normally placed at the top of the file. You can have multiple extensions specified one after another e.g.

{-# LANGUAGE EmptyDataDecls, MultiParamTypeClasses, UndecidableInstances, FlexibleInstances #-}
This is what I used to get the code in this article to work. The need for MultiParamTypeClasses should be obvious; the other two are more subtle and I'd rather you just take it on faith for now.

We still haven't finished defining natural numbers at the type level. So far, all we have is a type-level version of zero:

data Z
What about the successor type? It's pretty easy too:

data S n
This is another empty data declaration, though here S is a polymorphic type: you need to give it a type n for S n to be a type. Another way to think about this is that S is like a function on types: it takes a type as an argument and returns another type. I have to fight to keep myself from going into a long digression on "kinds" here, but I will.

This means that the following are all going to be types:

Z -- type version of Zero (0) S Z -- type version of (Succ Zero) (1) S (S Z) -- type version of (Succ (Succ Zero)) (2) S (S (S Z)) -- type version of (Succ (Succ (Succ Zero))) (3) -- etc.
Cool! So with very little work, we have defined natural numbers at the type level. We can ask ghci to convert the undefined value to our types:

ghci> :t undefined :: S (S (S Z)) undefined :: S (S (S Z)) :: S (S (S Z))
Once again, we haven't accomplished anything except verifying that ghci does indeed consider these type-level naturals to be valid types.
Step 5: Addition at the type level

Okeedokee. Let's see if we can do some math on our type-level natural numbers. It's natural to start with addition. Recall the definition of addition for our Nat type (natural numbers at the value level):

-- functional style: plus :: Nat -> Nat -> Nat plus Zero n = n plus (Succ m) n = Succ (plus m n)
And let's not forget the relational version of this:

-- relational style, not Haskell syntax: plusR :: Nat, Nat, Nat -- plusR relates three natural numbers plusR Zero n n -- 0 plus any n is n plusR (Succ m) n (Succ r) -- (m + 1) + n is (r + 1) where (plusR m n r) -- where r is m + n
To do a computation on types, we are going to do something a bit weird. For every type-level operation we want to have, we will define a (multi-parameter) type class. This type class will contain one parameter for each type argument and one for the type result (in other words, it will be written in a relational manner; I told you this would turn out to be useful!). Here we will have:

class Plus m n r where -- no methods!
As I said above, a multi-parameter type class defines a relation on types. A function on types can be viewed as a relation on types (using the relational style). But this is a very weird type class, since it has no methods! What use could this have? All it does is assert that types m, n, and r (whatever they get instantiated to be) have some kind of relation that we're calling Plus. Conceptually, the type r is the type-level sum of types m and n. This is an empty class declaration, so you can leave out the where:

class Plus m n r -- no methods!
As we'll see in the next section, this is not quite adequate, but let's run with it for a while.

You can think of empty class definitions of this sort as defining the "type signature" of our type-level addition function. To this, we have to add a single instance declaration for every clause in the definition of plus (or, more accurately, plusR). Let's translate the relational style plusR to the type level:

instance Plus Z n n -- no method definitions!
This line says that adding the Zero type to any type-level natural number n just gives back n. Notice that this defines an entire family of instances, one for each type n. This is the type level analog of:

-- functional style: plus Zero n = n -- relational style, not Haskell syntax: plusR Zero n n
The next line is this:

instance (Plus m n r) => Plus (S m) n (S r) -- no method definitions!
The context symbol => says that if the types m, n, and r together are an instance of the type class Plus, then the types S m, n, and S r together also are. So if Haskell is required to check that Plus (S m) n (S r) is a valid instance for some m and n, all it has to do is check that Plus m n r is a valid instance.

Compare this to:

-- functional style: plus (Succ m) n = Succ (plus m n) -- relational style, not Haskell syntax: plusR (Succ m) n (Succ r) -- (m + 1) + n is (r + 1) where (plusR m n r) -- where r is m + n
Notice that the instance declaration is an almost exact copy of the addition function in relational style, modulo the syntax changes. Notice also that the instance definitions have no method definitions, because there are no methods in the Plus type class.

You may feel like what we are doing is completely abusing the type class mechanism to do something it was never intended to do. Perhaps that's true, but a happier way to think about it is that we are showing that the type class mechanism has more uses than were previously suspected.

We are almost at a point where we can do addition at the type level, but there is one crucial detail I have to cover first.
Functional dependencies

If the type-level addition function is truly a relation on types that preserves the addition property, it should be possible to define things like:

instance Plus Z Z Z -- 0 + 0 = 0 instance Plus Z (S Z) (S Z) -- 0 + 1 = 1 instance Plus (S Z) Z (S Z) -- 1 + 0 = 1 instance Plus (S Z) (S Z) (S (S Z)) -- 1 + 1 = 2 -- etc.
All of these are derivable from the two instance equations for Plus given above. For instance:

instance Plus Z n n -- let n = Z, so: instance Plus Z Z Z -- let n = (S Z), so: instance Plus Z (S Z) (S Z) instance (Plus m n r) => Plus (S m) n (S r) -- let m = Z, n = Z: instance (Plus Z Z r) => Plus (S Z) Z (S r) -- from (instance Plus Z Z Z), we know that r can equal Z, so: instance (Plus Z Z Z) => Plus (S Z) Z (S Z) -- let m = Z, n = (S Z): instance (Plus Z (S Z) r) => Plus (S Z) (S Z) (S r) -- from (instance Plus Z (S Z) (S Z)), we know that r can equal (S Z), so: instance (Plus Z (S Z) (S Z)) => Plus (S Z) (S Z) (S (S Z))
This is cool, but unfortunately, it's not sufficiently constrained. So far, all we've shown is that all valid additions of natural numbers will have a corresponding instance of the Plus type class. We haven't shown that invalid instances of the Plus type class are not allowed. So far, there is nothing preventing us from defining e.g.

instance Z Z (S (S (S Z))) -- 0 + 0 = 3 ???
For addition at the type level to mean anything, we must be able to forbid instances of this kind. This leads us to the notion of a functional dependency. We would like to say, in effect, that the third type parameter to the Plus type class is uniquely determined by the first two type parameters. This makes sense, since we are trying to define Plus as a type-level function, and the crucial characteristic of a function is that the arguments (here, the first two type parameters) uniquely determine the result (here, the last type parameter). Haskell's type classes allow you to specify functional dependencies. In this case, we change the class definition of Plus to:

class Plus m n r | m n -> r
The functional dependency stuff is the | m n -> r part. What this does is tell the compiler "for any two specific types m and n, only allow one type r in instances of this class". This will solve the bogus instance problem. Since the compiler already knows that there is an instance for m = Z, n = Z (that being instance Plus Z Z Z, derived from the rule instance Plus Z n n), it will not allow an instance like instance Z Z (S (S (S Z))). In effect, it will say "Whoa there! You already have an instance for m = Z, n = Z; you can't have another!" And that's what functional dependencies do, and that's all that they do. You can imagine that once you define the instance rules above, all possible instances that conform to those rules are instantaneously created, and so you can't add any more instances that violate the functional dependency.

With this updated class definition, we can now do addition at the type level:

-- 1 + 1 = 2, type level: ghci> :t undefined :: (Plus (S Z) (S Z) a) => a undefined :: (Plus (S Z) (S Z) a) => a :: S (S Z) -- 2 + 2 = 4, type level: ghci> :t undefined :: (Plus (S (S Z)) (S (S Z)) a) => a undefined :: (Plus (S (S Z)) (S (S Z)) a) => a :: S (S (S (S Z)))
The result in each case is the type of a. Take that, George Orwell! ;-)

It's worth spending a bit of time going through what's happening here. Since we don't care about values, and our type-level natural numbers don't have specific values anyway, we are using the undefined value as our only value (because it can have any type whatsoever). When we type something like:

ghci> :t undefined :: (Plus (S Z) (S Z) a) => a
we are saying to ghci: "What is the type of undefined, given that I'm telling you that its type is a, where the three types (S Z), (S Z), and a together are a valid instance of the type class Plus?" Or at least, that's what ghci thinks we're asking it. What we are really asking is: "Given a type a such that the three types (S Z), (S Z), and a together are a valid instance of the type class Plus, what does the type a have to be?" This is a valid question, since the type a must be uniquely determined by the other two types due to the functional dependency; without the functional dependency, there would be no way to compute a unique value for a. Haskell will then compute the type a by applying the applicable rules until a unique value for a is derived; in this case, it will be the type S (S Z), which is the type-level analog of the number 2.

Parenthetically, this reminds me a lot of a Sidney Harris cartoon where two people are looking on as a scientist has written on a large blackboard:

1 + 1 -----
and is staring intently at the board. One onlooker says to the other: "He's working on very, very, very basic research!" This is my all-time favorite cartoon, and I've never been able to track it down in any of Harris' cartoon collections; if anyone knows where I can find it please send me an email. I guess I never realized that it wasn't a joke.

From here on, it's smooth sailing all the way.
Step 6: Multiplication at the type level

Recall how we defined multiplication for (value-level) natural numbers:

-- functional style: mult :: Nat -> Nat -> Nat mult Zero n = Zero mult (Succ m) n = plus n (mult m n) -- relational style, not Haskell syntax: multR :: Nat, Nat, Nat -- multR relates three natural numbers multR Zero n Zero -- 0 times any n is 0 multR (Succ m) n s -- (m + 1) * n is s where plusR n r s -- where s is n + r where multR m n r -- where r is m * n
Multiplication on type-level naturals is essentially the same as the relational-style version:

class Mult m n r | m n -> r -- No methods! instance Mult Z n Z instance (Mult m n r, Plus n r s) => Mult (S m) n s
Let's test it:

ghci> :t undefined :: (Mult (S (S Z)) (S (S (S Z))) a) => a undefined :: (Mult (S (S Z)) (S (S (S Z))) a) => a :: S (S (S (S (S (S Z)))))
We've just computed 2 * 3 = 6 at the type level.
Step 7: Factorial at the type level

Now for the piece de resistance. If you've understood everything so far, this should be easy. Recall the versions for (value-level) natural numbers:

-- functional style: fact :: Nat -> Nat fact Zero = Succ Zero fact (Succ n) = mult (Succ n) (fact n) -- relational style, not Haskell syntax: factR :: Nat, Nat -- factR relates two natural numbers factR Zero (Succ Zero) -- factorial of 0 is 1 factR (Succ n) s -- factorial of (n + 1) is s where multR (Succ n) r s -- where s is (n + 1) * r where factR n r -- where r is factorial of n
Now look at the type-level factorial implementation:

class Fact n r | n -> r -- No methods! instance Fact Z (S Z) instance (Fact n r, Mult (S n) r s) => Fact (S n) s
Note that the Fact class has only two parameters (since factorial is a function of one argument) and so the functional dependency specifies that the result type r depends on the argument type n.

Let's test it:

ghci> :t undefined :: (Fact (S (S (S Z))) a) => a undefined :: a :: (Fact (S (S (S Z))) a) => a :: S (S (S (S (S (S Z)))))
We've just shown that factorial(3) = 6 at the type level. But wait, why not try this:

ghci> :t undefined :: (Fact (S (S (S (S (S (S Z)))))) a) => a undefined :: (Fact (S (S (S (S (S (S Z)))))) a) => a :: S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S (S Z) )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) ))))))))))))))))))))))))))))))))))))
This clearly shows that the factorial of 6 is 720. I hope you're satisfied.
Wrapping up

This is a pretty cool construction, but one thing that really struck me while working on it is that it seemed very similar to what I understand is possible with C++ template metaprogramming. I'm no C++ guru, but I'm sure I've seen examples of factorials computed at compile-time in C++.

If you like this, I again urge you to read the papers by Ralf Lammel, Oleg Kiselyov, and Keann Schupke referred to above, and check out Brent Yorgey's excellent blog.
Previous post Next post
Up