During the
FizzBuzz exercise, we used recursive functions likeMOD
andRANGE
to demonstrate the use of
the Z combinator. This is convenient, because it lets us translate from an unconstrained
recursive Ruby implementation to a proc-only one without changing the structure of the
code, but technically, we can implement these functions without the Z combinator by taking
advantage of the behavior of Church numerals.
For example, our implementation ofMOD[m][n]
works by repeatedly subtractingn
fromm
as long asn
, always checking the condition to decide whether to
<= m
make the next recursive call. But we can get the same result by
blindly performing the action “subtractn
fromm
ifn <= m
” a fixed number of
times instead of using recursion to dynamically control the
repetition. We don’t know exactly how many times we need to repeat it,
but we do know thatm
times is
definitely enough (for the worst case wheren
is1
),
and it doesn’t hurt to do it more times than necessary:
def
decrease
(
m
,
n
)
if
n
<=
m
m
-
n
else
m
end
end
>>
decrease
(
17
,
5
)
=> 12
>>
decrease
(
decrease
(
17
,
5
),
5
)
=> 7
>>
decrease
(
decrease
(
decrease
(
17
,
5
),
5
),
5
)
=> 2
>>
decrease
(
decrease
(
decrease
(
decrease
(
17
,
5
),
5
),
5
),
5
)
=> 2
>>
decrease
(
decrease
(
decrease
(
decrease
(
decrease
(
17
,
5
),
5
),
5
),
5
),
5
)
=> 2
We can therefore rewriteMOD
to make use of a proc that takes a number and either subtractsn
from it (if it’s greater thann
) or returns it untouched. This
proc gets calledm
times onm
itself to give the final
answer:
MOD
=
->
m
{
->
n
{
m
[->
x
{
IF
[
IS_LESS_OR_EQUAL
[
n
][
x
]][
SUBTRACT
[
x
][
n
]
][
x
]
}
][
m
]
}
}
This version ofMOD
works
just as well as the recursive one:
>>
to_integer
(
MOD
[
THREE
][
TWO
]
)
=> 1
>>
to_integer
(
MOD
[
POWER
[
THREE
][
THREE
]
][
ADD
[
THREE
][
TWO
]
]
)
=> 2
Although this implementation is arguably simpler than the original, it is both harder
to read and less efficient in general, because it always performs a worst-case number of
repeated calls instead of stopping as soon as possible. It’s also not extensionally equal
to the original, because the old version ofMOD
would
loop forever if we asked it to divide byZERO
(the
conditionn <= m
would never become false), whereas
this implementation just returns its first argument:
>>
to_integer
(
MOD
[
THREE
][
ZERO
]
)
=> 3
RANGE
is slightly more challenging, but we can use
a trick similar to the one that makesDECREMENT
work:
design a function that, when calledn
times on some
initial argument, returns a list ofn
numbers from the
desired range. As withDECREMENT
, the secret is to use
a pair to store both the resulting list and the information needed by the next
iteration:
def
countdown
(
pair
)
[
pair
.
first
.
unshift
(
pair
.
last
),
pair
.
last
-
1
]
end
>>
countdown
(
[[]
,
10
]
)
=> [[10], 9]
>>
countdown
(
countdown
(
[[]
,
10
]
))
=> [[9, 10], 8]
>>
countdown
(
countdown
(
countdown
(
[[]
,
10
]
)))
=> [[8, 9, 10], 7]
>>
countdown
(
countdown
(
countdown
(
countdown
(
[[]
,
10
]
))))
=> [[7, 8, 9, 10], 6]
This is easy to rewrite with procs:
COUNTDOWN
=
->
p
{
PAIR
[
UNSHIFT
[
LEFT
[
p
]][
RIGHT
[
p
]]][
DECREMENT
[
RIGHT
[
p
]]]
}
Now we just need to implementRANGE
so that it callsCOUNTDOWN
the right number of times (the
range fromm
ton
always hasm - n
elements) and unpacks the result list from the final
+ 1
pair:
RANGE
=
->
m
{
->
n
{
LEFT
[
INCREMENT
[
SUBTRACT
[
n
][
m
]][
COUNTDOWN
][
PAIR
[
EMPTY
][
n
]]]
}
}
Again, this combinator-free version works just
fine:
>>
to_array
(
RANGE
[
FIVE
][
TEN
]
)
.
map
{
|
p
|
to_integer
(
p
)
}
=> [5, 6, 7, 8, 9, 10]
We’re able to implementMOD
andRANGE
by performing a
predetermined number of iterations—rather than executing an
arbitrary loop that runs until its condition becomes true—because
they’re
primitive recursive
functions. See
Partial Recursive Functions
for more about
this.
Our FizzBuzz experiment
has given us a sense of how it feels to write programs in
the untyped lambda calculus. The constraints forced us to implement a lot
of basic functionality from scratch rather than relying on features of the
language, but we did eventually manage to build all of the data structures
and algorithms we needed to solve the problem we were given.
Of course, we haven’t
really
been writing
lambda calculus programs, because we don’t have a lambda calculus
interpreter; we’ve just written Ruby programs in the style of the lambda
calculus to get a feel for how such a minimal language can work. But we
already have all the knowledge we need to build a lambda calculus
interpreter and use it to evaluate actual lambda calculus expressions, so
let’s give that a try.
The
untyped lambda calculus is a programming language with
only three kinds of expression: variables, function definitions, and
calls. Rather than introduce a new concrete syntax for lambda calculus
expressions, we’ll stick with the Ruby conventions—variables look likex
, functions look like-> x { x }
, and calls look likex[y]
—and try not to get the two languages
confused.
In this context, the word
calculus
means a system of rules
for manipulating strings of symbols.
[
49
]
The native syntax of the lambda calculus uses the Greek letter lambda (λ) in
place of Ruby’s->
symbol; for instance,ONE
is written asλp.λx.p
.
x
We can implementLCVariable
,LCFunction
, andLCCall
syntax classes in the usual way:
class
LCVariable
<
Struct
.
new
(
:name
)
def
to_s
name
.
to_s
end
def
inspect
to_s
end
end
class
LCFunction
<
Struct
.
new
(
:parameter
,
:body
)
def
to_s
"->
#{
parameter
}
{
#{
body
}
}"
end
def
inspect
to_s
end
end
class
LCCall
<
Struct
.
new
(
:left
,
:right
)
def
to_s
"
#{
left
}
[
#{
right
}
]"
end
def
inspect
to_s
end
end
These classes let us build abstract syntax trees of lambda
calculus expressions, just like we did with
Simple
in
Chapter 2
and regular expressions in
Chapter 3
:
>>
one
=
LCFunction
.
new
(
:p
,
LCFunction
.
new
(
:x
,
LCCall
.
new
(
LCVariable
.
new
(
:p
),
LCVariable
.
new
(
:x
))
)
)
=> -> p { -> x { p[x] } }
>>
increment
=
LCFunction
.
new
(
:n
,
LCFunction
.
new
(
:p
,
LCFunction
.
new
(
:x
,
LCCall
.
new
(
LCVariable
.
new
(
:p
),
LCCall
.
new
(
LCCall
.
new
(
LCVariable
.
new
(
:n
),
LCVariable
.
new
(
:p
)),
LCVariable
.
new
(
:x
)
)
)
)
)
)
=> -> n { -> p { -> x { p[n[p][x]] } } }
>>
add
=
LCFunction
.
new
(
:m
,
LCFunction
.
new
(
:n
,
LCCall
.
new
(
LCCall
.
new
(
LCVariable
.
new
(
:n
),
increment
),
LCVariable
.
new
(
:m
))
)
)
=> -> m { -> n { n[-> n { -> p { -> x { p[n[p][x]] } } }][m] } }
Because the language has such minimal syntax, those three classes
are enough to represent any lambda
calculus program.
Now we’re going to
give a small-step operational semantics for the lambda calculus by implementing
a#reduce
method on each syntax class. Small-step is an
attractive choice, because it allows us to see the individual steps of evaluation, which is
something we can’t easily do for Ruby expressions.
Before we can
implement#reduce
, we need another
operation called#replace
, which finds occurrences of a
particular variable inside an expression and replaces them with some other
expression:
class
LCVariable
def
replace
(
name
,
replacement
)
if
self
.
name
==
name
replacement
else
self
end
end
end
class
LCFunction
def
replace
(
name
,
replacement
)
if
parameter
==
name
self
else
LCFunction
.
new
(
parameter
,
body
.
replace
(
name
,
replacement
))
end
end
end
class
LCCall
def
replace
(
name
,
replacement
)
LCCall
.
new
(
left
.
replace
(
name
,
replacement
),
right
.
replace
(
name
,
replacement
))
end
end
This works in the obvious way on variables and calls:
>>
expression
=
LCVariable
.
new
(
:x
)
=> x
>>
expression
.
replace
(
:x
,
LCFunction
.
new
(
:y
,
LCVariable
.
new
(
:y
)))
=> -> y { y }
>>
expression
.
replace
(
:z
,
LCFunction
.
new
(
:y
,
LCVariable
.
new
(
:y
)))
=> x
>>
expression
=
LCCall
.
new
(
LCCall
.
new
(
LCCall
.
new
(
LCVariable
.
new
(
:a
),
LCVariable
.
new
(
:b
)
),
LCVariable
.
new
(
:c
)
),
LCVariable
.
new
(
:b
)
)
=> a[b][c][b]
>>
expression
.
replace
(
:a
,
LCVariable
.
new
(
:x
))
=> x[b][c][b]
>>
expression
.
replace
(
:b
,
LCFunction
.
new
(
:x
,
LCVariable
.
new
(
:x
)))
=> a[-> x { x }][c][-> x { x }]
For functions, the situation is more complicated.#replace
only acts on the body of a function, and it only replaces
free variables
—that is, variables that haven’t been
bound
to the function by being named as its parameter:
>>
expression
=
LCFunction
.
new
(
:y
,
LCCall
.
new
(
LCVariable
.
new
(
:x
),
LCVariable
.
new
(
:y
))
)
=> -> y { x[y] }
>>
expression
.
replace
(
:x
,
LCVariable
.
new
(
:z
))
=> -> y { z[y] }
>>
expression
.
replace
(
:y
,
LCVariable
.
new
(
:z
))
=> -> y { x[y] }
This lets us replace occurrences of a variable throughout an
expression without accidentally changing unrelated variables that
happen to have the same name:
>>
expression
=
LCCall
.
new
(
LCCall
.
new
(
LCVariable
.
new
(
:x
),
LCVariable
.
new
(
:y
)),
LCFunction
.
new
(
:y
,
LCCall
.
new
(
LCVariable
.
new
(
:y
),
LCVariable
.
new
(
:x
)))
)
=> x[y][-> y { y[x] }]
>>
expression
.
replace
(
:x
,
LCVariable
.
new
(
:z
))
=> z[y][-> y { y[z] }]
>>
expression
.
replace
(
:y
,
LCVariable
.
new
(
:z
))
=> x[z][-> y { y[x] }]
Our simple#replace
implementation won’t work on certain inputs. It doesn’t properly
handle replacements that contain free variables:
>>
expression
=
LCFunction
.
new
(
:x
,
LCCall
.
new
(
LCVariable
.
new
(
:x
),
LCVariable
.
new
(
:y
))
)
=> -> x { x[y] }
>>
replacement
=
LCCall
.
new
(
LCVariable
.
new
(
:z
),
LCVariable
.
new
(
:x
))
=> z[x]
>>
expression
.
replace
(
:y
,
replacement
)
=> -> x { x[z[x]] }
It’s not okay to just pastez[x]
into the body of-> x { … }
like that, because thex
inz[x]
is a free
variable and should remain free afterward, but here it gets accidentally
captured
by the function parameter with the same name.
[
50
]
We can ignore this deficiency, because we’ll only be evaluating expressions that
don’t contain any free variables, so it won’t actually cause a problem, but beware that
a more sophisticated implementation is needed in the general case.