As promised, we’re writingONE
instead of-> p { -> x { p[x] } }
, and so on, to
make the code clearer.
Unfortunately, this program doesn’t work anymore, because we’re now using operations
like..
and%
on the
proc-based implementations of numbers. Because Ruby doesn’t know how to treat these as
numbers it’ll just blow up:TypeError: can't iterate from
,
ProcNoMethodError: undefined method `%'
, and so on. We need to replace all of the
for #
operations to work with these representations—and we can only use procs to do it.
Before we can reimplement any of the operations, though, we need
implementations oftrue
andfalse
.
How can we
represent Booleans using only procs? Well, Booleans exist solely to be used in
conditional statements, and in general, a conditional says “if some Boolean then
this
else
that
”:
>>
success
=
true
=> true
>>
if
success
then
'happy'
else
'sad'
end
=> "happy"
>>
success
=
false
=> false
>>
if
success
then
'happy'
else
'sad'
end
=> "sad"
So the real job of a Boolean is to allow us to choose between two options, and we can
take advantage of this by representing a Boolean as a proc that chooses one of two values.
Instead of thinking of a Boolean as a lifeless piece of data that can be read by some future
code to decide which of two options to choose, we’ll just implement it directly as a piece
of code that, when called with two options, either chooses the first option or chooses the
second option.
Implemented as methods, then,#true
and#false
could be:
def
true
(
x
,
y
)
x
end
def
false
(
x
,
y
)
y
end
#true
is a method that takes
two arguments and returns the first one, and#false
takes two arguments and returns the
second. This is enough to give us crude conditional behavior:
>>
success
=
:true
=> :true
>>
send
(
success
,
'happy'
,
'sad'
)
=> "happy"
>>
success
=
:false
=> :false
>>
send
(
success
,
'happy'
,
'sad'
)
=> "sad"
As before, it’s straightforward to translate these methods into
procs:
TRUE
=
->
x
{
->
y
{
x
}
}
FALSE
=
->
x
{
->
y
{
y
}
}
And just as we defined#to_integer
as a sanity check, to make sure it
was possible to convert proc-based numbers into Ruby numbers, so we can
define a#to_boolean
method that can
turn theTRUE
andFALSE
procs into Ruby’s nativetrue
andfalse
objects:
def
to_boolean
(
proc
)
proc
[
true
][
false
]
end
This works by taking a proc that represents a Boolean and calling it withtrue
as its first argument andfalse
as its second.TRUE
just returns its
first argument, soto_boolean(TRUE)
will returntrue
, and likewise forFALSE
:
>>
to_boolean
(
TRUE
)
=> true
>>
to_boolean
(
FALSE
)
=> false
So representing Booleans with procs is surprisingly easy, but for FizzBuzz, we don’t
just need Booleans, we need a proc-only implementation of Ruby’sif
-elsif
-else
. In fact, because of the way these Boolean implementations work, it’s easy
to write an#if
method too:
def
if
(
proc
,
x
,
y
)
proc
[
x
][
y
]
end
And that’s easy to translate into a proc:
IF
=
->
b
{
->
x
{
->
y
{
b
[
x
][
y
]
}
}
}
ClearlyIF
doesn’t need to do any useful work,
because the Boolean itself picks the right argument—IF
is
just sugar—but it looks more natural than calling the Boolean directly:
>>
IF
[
TRUE
][
'happy'
][
'sad'
]
=> "happy"
>>
IF
[
FALSE
][
'happy'
][
'sad'
]
=> "sad"
Incidentally, this means we can revise the definition of#to_boolean
to useIF
:
def
to_boolean
(
proc
)
IF
[
proc
]
[
true
][
false
]
end
While we’re refactoring, it’s worth noting that the implementation ofIF
can be cleaned up significantly, because it contains some
procs that are equivalent to simpler ones, as discussed in
Equality
.
For example, look atIF
’s innermost proc:
->
y
{
b
[
x
][
y
]
}
This code means:
Take an argumenty
.
Callb
withx
to get a proc.
Call that proc withy
.
Steps 1 and 3 are dead wood: when we call this proc with an
argument, it just passes it on to another proc. So the whole thing is
equivalent to just step 2,b[x]
, and
we can remove the dead wood in the implementation ofIF
to make it simpler:
IF
=
->
b
{
->
x
{
b
[
x
]
}
}
We can see the same pattern again in what’s now the innermost
proc:
->
x
{
b
[
x
]
}
For the same reason, this proc is the same as justb
, so we can simplifyIF
even further:
IF
=
->
b
{
b
}
We’re not going to be able to simplify it any more than
that.
IF
doesn’t do anything
useful—it’sTRUE
andFALSE
that do all the work—so we
could
simplify further by getting rid of it
altogether. But our goal is to translate the original FizzBuzz
solution into procs as faithfully as possible, so it’s convenient to
useIF
to remind us where theif
-elsif
-else
expression appeared in the original,
even though it’s purely decorative.
Anyway, now that we haveIF
, we
can go back to the FizzBuzz program and replace
the Rubyif
-elsif
-else
with nested calls toIF
:
(
ONE
.
.
HUNDRED
)
.
map
do
|
n
|
IF
[
(
n
%
FIFTEEN
)
.
zero?
][
'FizzBuzz'
][
IF
[
(
n
%
THREE
)
.
zero?
][
'Fizz'
][
IF
[
(
n
%
FIVE
)
.
zero?
][
'Buzz'
][
n
.
to_s
]]]
end
Our next job is to
replaceFixnum#zero?
with a proc-based implementation that will work with proc-based numbers.
The underlying algorithm of#zero?
for Ruby values is something like this:
def
zero?
(
n
)
if
n
==
0
true
else
false
end
end
(This is more verbose than is necessary, but it’s explicit about what happens: compare
the number with0
; if it’s equal, then returntrue
; otherwise, returnfalse
.)
How can we adapt this to handle procs instead of Ruby numbers?
Look at our implementation of numbers again:
ZERO
=
->
p
{
->
x
{
x
}
}
ONE
=
->
p
{
->
x
{
p
[
x
]
}
}
TWO
=
->
p
{
->
x
{
p
[
p
[
x
]]
}
}
THREE
=
->
p
{
->
x
{
p
[
p
[
p
[
x
]]]
}
}
⋮
Notice thatZERO
is the only number that doesn’t callp
—it just returnsx
—whereas all of the other numbers callp
at
least once. We can take advantage of this: if we call an unknown number withTRUE
as its second argument, it’ll returnTRUE
immediately if the number isZERO
. If it’s notZERO
, then it’ll return
whatever callingp
returns, so if we makep
a proc that always returnsFALSE
, we’ll get the behavior we want:
def
zero?
(
proc
)
proc
[->
x
{
FALSE
}
][
TRUE
]
end
Again, it’s easy to rewrite this as a proc:
IS_ZERO
=
->
n
{
n
[->
x
{
FALSE
}
][
TRUE
]
}
We can use#to_boolean
on the
console to check that it works:
>>
to_boolean
(
IS_ZERO
[
ZERO
]
)
=> true
>>
to_boolean
(
IS_ZERO
[
THREE
]
)
=> false
That’s working fine, so in FizzBuzz, we can replace all of the calls to#zero?
withIS_ZERO
:
(
ONE
.
.
HUNDRED
)
.
map
do
|
n
|
IF
[
IS_ZERO
[
n
%
FIFTEEN
]
][
'FizzBuzz'
][
IF
[
IS_ZERO
[
n
%
THREE
]
][
'Fizz'
][
IF
[
IS_ZERO
[
n
%
FIVE
]
][
'Buzz'
][
n
.
to_s
]]]
end