Version 17 of Combinator Engine

Updated 2002-12-04 18:14:26

KBK - After burning myself on hot curry, I started wasting entirely too much time on an engine that builds up recursive function theory from a minimalist set of primitives.

In this page, we'll be developing an implementation of the factorial function in a language with:

  • no if statement.
  • no while loops. In fact, no control structures at all, except as shown below.
  • no named variables.
  • no named user functions (Well, we'll use named functions for shorthand, but we could expand their definitions recursively.

The only control structures are [S] and [K], defined as

    S a b c = a c { b c }
    K a b   = a

And we have a few additional functions:

   addition: + a b
   subtraction: - a b
   multiplication: * a b
   division: / a b
   test for zero: zero? a
         This last function returns K if a is zero, and K { S K K } otherwise.

And that's all. We'll allow for simple expansion of named functions, so that we can write

    define I S K K

but that's just shorthand, we could simply spell things out everywhere.


(4 December 2002) KBK improved the optimizer and added a little bit more discussion.

# In order to keep my sanity, I found myself needing to trace what the engine did, so my source file begins with

 set tracemax 100000
 set tracen 0
 proc trace args {
     global tracen tracemax
     if { [incr tracen] > $tracemax } exit
     puts [format %*s%s [info level] {} $args]
 }
 proc trace args {}

# The fundamental problem with hot curry is that eager evaluation gets in the way of searching for fixed points. Hence we do our own lazy evaluator. The evaluator can be extremely simple, since every function that it recognizes has a single argument!

 proc lazyEval args {
     # if we've got a list as the first arg, flatten it.
     set f [lindex $args 0]
     while { 1 } {
         trace evaluating $args
         if { [llength $f] > 1 || [string match {{*}} $f] } {
             trace expanding $f
             set args [eval [list lreplace $args 0 0] $f]
             set f [lindex $args 0]
         } elseif { [llength $args] > 1 } {
             set f [lindex $args 0]
             set g [lindex $args 1]
             trace applying $f to $g
             set result [$f $g]
             trace replacing $f $g -> $result
             set args [eval [list lreplace $args 0 1] $result]
             set f [lindex $args 0]
         } else {
             break
         }
     }
     # return the single arg
     trace result $args
     return $args
 }

# It's convenient to have anonymous lambdas. Alas, there isn't any graceful way to implement lambda in Tcl. Here's a hack that leaks memory.

 variable lambdaIndex 0
 proc lambda { x y } {
    variable lambdaIndex
    variable lambdaCache
    set key [list $x $y]
    if { ! [info exists lambdaCache($key)] } {
        set pname lambda[incr lambdaIndex]
        set lambdaCache($key) $pname
        proc $pname $x $y
    }
    return $lambdaCache($key)
 }

# We curry the K and S combinators by hand. Note that some special handling is needed because the arguments are unevaluated - we are doing lazy evaluation, after all!

# K x y = x

 proc K x { lambda y "lazyEval $x" }

# S f g x = f x (g x)

 proc S f { lambda g "[list S2 $f] \$g" }
 proc S2 { f g } { lambda x "[list S3 $f $g] \$x" }
 proc S3 { f g x } {
     trace evaluating curried S $f $g $x
     set toEval [list lazyEval $f $x [list $g $x]]
     set result [eval $toEval]
     trace returning $result
     return $result
 }

# Now, if we've done everything right, we should see that S K K is the identity operation.

 puts [lazyEval S K K this-should-pass-unchanged]
 #                    -> prints this-should-pass-through-unchanged

# Let's define lazy, curried, versions of the four arithmetic operators.

 proc + x { lambda y "[list ++ $x] \$y" }
 proc ++ { x y } {
     trace evaluating + $x $y
     set realX [uplevel 1 [list lazyEval $x]]
     set realY [uplevel 1 [list lazyEval $y]]
     set result [expr { $realX + $realY }]
     trace returning $result
     return $result
 }
 proc - x { lambda y "[list -- $x] \$y" }
 proc -- { x y } {
     trace evaluating - $x $y
     set realX [uplevel 1 [list lazyEval $x]]
     set realY [uplevel 1 [list lazyEval $y]]
     set result [expr { $realX - $realY }]
     trace returning $result
     return $result
 }
 proc * x { lambda y "[list ** $x] \$y" }
 proc ** { x y } {
     trace evaluating * $x $y
     set realX [uplevel 1 [list lazyEval $x]]
     set realY [uplevel 1 [list lazyEval $y]]
     set result [expr { $realX * $realY }]
     trace returning $result
     return $result
 }
 proc / x { lambda y "[list // $x] \$y" }
 proc // { x y } {
     trace evaluating / $x $y
     set realX [uplevel 1 [list lazyEval $x]]
     set realY [uplevel 1 [list lazyEval $y]]
     set result [expr { $realX / $realY }]
     trace returning $result
     return $result
 }
 puts [lazyEval + 6 2]
 puts [lazyEval - 6 2]
 puts [lazyEval * 6 2]
 puts [lazyEval / 6 2]
 #                    print 8, 4, 12, and 3 respectively

# We want to be able to define new combinators. The define procedure is a little bit of syntactic sugar around [interp alias]:

 proc define {key args} {
     trace [list interp alias {} $key {} lazyEval] $args
     eval [list interp alias {} $key {} lazyEval] $args
 }

# OK, now we have enough in hand to define some of the basic combinators.

# The identity function, I x == x

 define I S K K
 puts [lazyEval I this-should-pass-through-unchanged]
 #                    prints this-should-pass-through-unchanged

# Exercise 1: Try to prove this yourself, it isn't hard. If you get stuck, see Combinator engine: answers to exercises

# The composition operator B f g x = f (g x)

 define B S {K S} K
 puts [lazyEval B {+ 1} {* 2} 3]
 #                    prints 7

# The alternative curry C f x y = f y x

 define C S {B B S} {K K}
 puts [lazyEval C - 5 8]
 #                    prints 3

# Apply a function to a constant T x f = f x

 define T C I
 puts [lazyEval T 3 {- 10}]
 #                    prints 7

# Duplicate an argument to a function W f x = f x x

 define W C S I
 puts [lazyEval W * 6]
 #                    prints 36

# Exercise 2: Prove all of these. If you get stuck, the answers are over in Combinator engine: answers to exercises.

# While it isn't strictly necessary, it's helpful to have a compiler to abstract free variables from expressions. Essentially, this is translating the lambda calculus to the algebra of S and K

 #    * Lambda x (f g) => S (Lambda x f) (Lambda x g)
 #    * Lambda x a ==> K a
 #    * Lambda x x ==> I

 proc Lambda { x f } {
     trace abstracting $x from $f
     if { [llength $f] > 1 } {
         set retval [list S \
                        [Lambda $x [lrange $f 0 end-1]] \
                        [Lambda $x [lindex $f end]]]
     } elseif { [string equal $f $x] } {
         set retval I
     } else {
         set retval [list K $f]
     }
     trace returning $retval
     return [Opt $retval]
 }

# We also apply a few optimizations:

 #    * Redundant brackets are removed
 #    * S (K a) I ==> a
 #    * S (K a) (K b) ==> K (a b)
 #    * S (K a) => B a
 #    * S a (K b) => C a b

# Exercise 3: Prove that the last four optimizations are correct. Combinator engine: answers to exercises

 proc Opt x {
    trace optimizing $x
    # remove redundant brackets
    if { [string match {{*}} $x] && [llength $x] == 1 } {
        trace removing redundant brackets
        set x [Opt [lindex $x 0]]
    }
    set first [lindex $x 0]
    if { [llength $first] > 1 || [string match {{*}} $first] } {
        trace removing redundant brackets from $first
        set y [Opt $first]
        eval [list lappend y] [lrange $x 1 end]
        set x [Opt $y]
    }
    if { [llength $x] > 1 } {
        trace optimizing subordinate items
        set r [list]
        foreach item $x {
            lappend r [Opt $item]
            set x $r
        }
    }
    if { [llength $x] >= 3 && [string equal S [lindex $x 0]] } {
        set first [lindex $x 1]
        set second [lindex $x 2]
        if { [llength $first] == 2 && [string equal K [lindex $first 0]] } {
            set a [lindex $first 1]
            if { [string equal I [lindex $second 0]] } {
                set r [lreplace $x 0 2 $a]
                trace replacing [lrange $x 0 2] with $a
                set x [Opt $r]
            } elseif { [llength $second] == 2
                       && [string equal K [lindex $second 0]] } {
                set b [lindex $second 1]
                set r [lreplace $x 0 2 K [list $a $b]]
                trace replacing [lrange $x 0 2] with [list K [list $a $b]]
                set x [Opt $r]
            } else {
                set r [lreplace $x 0 2 B $a $second]
                trace replacing [lrange $x 0 2] with [list B $a $second]
                set x [Opt $r]
            }
        } elseif { [llength $second]== 2
                   && [string equal K [lindex $second 0]] } {
            set q [lindex $second 1]
            set r [lreplace $x 0 2 C $first $q]
            trace replacing [lrange $x 0 2] with [list C $first $q]
            set x [Opt $r]
        }
    } elseif { [llength $x] == 2 && [string equal S [lindex $x 0]] } {
        set first [lindex $x 1]
        if { [llength $first] == 2 && [string equal K [lindex $first 0]] } {
            set p [lindex $first 1]
            trace replacing $x with [list B $p]
            set x [Opt [list B $p]]
        }
    }
    trace returning $x
    return $x
 }

# Our functions dispense with the [if] statement; instead, a Boolean value is a combinator with two arguments. true evaluates its first argument and discards the second; false discards the first and evaluates the second. (If we had no if).

 define true K
 define false K {S K K}
 proc zero? n {
     trace evaluating [info level 0]
     set realN [uplevel 1 lazyEval $n]
     if { $realN == 0 } {
         set result true
     } else {
         set result false
     }
     trace returning $result
     return $result
 }

if 0 { Proof: Consider:

    zero? 0 a b == K a b == a
    zero? 1 a b == K {S K K} a b
        == S K K b
        == K b {K b}
        == b

}

# We can test this out quickly:

 puts [lazyEval zero? 0 it-is-zero it-isn't-zero]
 #                    prints it-is-zero
 puts [lazyEval zero? 1 it-is-zero it-isn't-zero]
 #                    prints it-isn't-zero

# OK, now things can get interesting. Let's define Turing's fixed point combinator Y. The Y combinator searches for a fixed point of a function f, that is, a value of x such that x = f x.

 define A B {S I} {S I I}
 define Y A A
 set realY {B {S I} {S I I} {B {S I} {S I I}}}

# How does this work?

 # Y f = A A f
 #     = B (S I) (S I I) A F
 #     = (S I) ((S I I) A) f
 #     = (S I) (S I I A) f
 #     = (S I) (I A (I A)) f
 #     = S I (A A) f
 #     = I f ((A A) f)
 #     = f (A A f)
 #     = f (Y f)

# Who cares? Well, let's come up with a traditional recursive definition for factorial:

 # f(n) = (zero? n 1 (* n (f (- n 1)))).  

# In other words, "if n is zero, return 1, otherwise return n*f(n-1)." Make this function accept a second parameter, which is expected to be the function itself.

 set f1 [Opt [Lambda f [Lambda n { zero? n 1 { * n { f { - n 1 } } } }]]]
 puts [list f1 = $f1]
 #                    prints f1 = {B {S {C zero? 1}} {B {S *} {C B {C - 1}}}} 
 set fact1 [linsert $f1 0 define fact1]
 eval $fact1

# Now, let's see what evaluating Y fact1 2 does:

 # Y fact1 2 = fact1 (Y fact1) 2
 #           = (* 2 (fact1 (Y fact1) 1))
 #           = (* 2 (* 1 (fact1 (Y fact1) 0)))
 #           = (* 2 (* 1 1))

# Wow, recursion without explicit function names! Let's test it out:

 puts "factorial x is the same as [Opt [list $realY $f1 x]]"
 #                    prints a long expression so that you can see that
 #                    there are NO named functions in it anywhere, but
 #                    it's still recursive, and computes the factorial!
 define factorial $realY $f1
 puts [lazyEval factorial 5]
 #                    prints 120

# We can also do an iterative implementation of factorial:

 set f2 [Opt [Lambda f [Lambda n [Lambda t {
     zero? n t { f { - n 1 } { * t n } }
 }]]]]
 puts [list f2 = $f2]
 #                    prints a long expression...
 set f3 $realY; lappend f3 $f2
 puts "factorial x is also the same as [list C $f3 1 x]"
 #                    prints an even longer expression...
 define factorial2 C $f3 1
 puts [lazyEval factorial2 5]
 #                    once again, prints 120.  Who needs a while
 #                    loop, anyway?

# Even multiple recursion is a piece of cake, as we see with the Fibonacci numbers:

 set fib [Opt [Lambda f [Lambda x {
    zero? x 0 {zero? {- x 1} 1 {+ {f {- x 1}} {f {- x 2}}}}
 }]]]
 puts [list fib = $fib]
 set f3 [linsert $realY 0 define fib]
 lappend f3 $fib
 puts $f3
 eval $f3
 puts [lazyEval fib 7]