Tải bản đầy đủ (.pdf) (13 trang)

Recursion and Constructing Combinators An Intermediate+n

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (90.49 KB, 13 trang )

Recursion and Constructing Combinators
An Intermediate+n Tutorial by Jonathan Carlos
Baca
1. What Recusion Is and Is Not
This is a tutorial that will not teach you how to use a certain library, popular programming technique, or even make you a faster
coder. However, I maintain that this tutorial will make you a better coder; code is logic knit into forms by human fingertips
dancing on keyboards, and although you may just want to dab, why not learn ballet as well?
For this tutorial to help you out, you must know what recursion is. This is actually pretty simple. Suppose we have a function
called ‘foo’, defined as follows (I’m using Javascript, so haters; bear along. For some of you, I’ve also written the tutorial in
Lisp, so if you’re that type of purist, you’ll be pretty happy. For beginners: the first examples are always in Javascript, and the
second ones are in Lisp, but you’ll get a feel for which is which almost immediately):
Javascript
1
2
3
4
5
6

function foo(n) {
if(n == 0) {
return n;
}
return 1 + foo(n - 1);
}

JavaScript

Lisp
1
2


3
4

(defun foo (n)
(if (zerop n)
n
(1+ (foo (1- n)))))

What output will foo(3) produce? Well let’s walk through it. if n:=3, then n == 0 is false, so it will return 1 + foo(2). So what’s
foo(2)? In the same manner, it’s 1 + foo(1). What’s foo(1)? Again, 1 + foo(0). Now what’s foo(0)? Since here n:=0, we return n
(which is 0), so foo(0) = 0. So 1 + foo(0) = foo(1) = 1, 1 + foo(1) = foo(2) = 2, and 1 + foo(3) = foo(3) = 3. If you haven’t figured it
out yet, it just takes a positive integer and regurgitates it.
The question I’m posing seems like a simple one: can recursion exist without functions being able to refer back to themselves?
This may seem like an obvious ‘no’ to some of you, ‘yes’ to others, and to those of you in the back (who should give the other
students a chance) shouting about the Y-combinator: shut up a minute. Let me make my meaning more clear: suppose we’re
given a computer language (I’ve chosen javascript to write this tutorial in, although any functionalish language will do) that has
names for variables and functions and has a mechanism for passing those functions and variables into other functions. The
question is this: ‘Must a function refer to itself by the name it was given upon definition to calculate recursive functions in a
more-or-less direct way?’ It’s a nebulous question, to be sure.

2. Ouroboros (the snake eating its own tail)


Suppose we want to recurse without naming the recursor. We can start by writing a simple recursive function. I’ve chosen
multiplication. The identity we’ll be using is 0 * b = 0, but if a != 0, then a * b = (1 + (a - 1)) * b = b + (a - 1) * b (or, in English, zero
times anything is zero, and thing times second thing is equal to second thing plus first thing minus one. So 3 * 5 = 5 + (2 * 10),
and so on:
Javascript
1
2

3
4
5
6

function multiply(a, b) {
if(a == 0) {
return 0;
}
return b + multiply(a-1, b);
}

JavaScript

Lisp
1
2
3
4

(defun multiply (a b)
(if (zerop a)
0
(+ b (multiply (1- a) b))))

We could write this more succinctly (and without structures) as:
Javascript
1
2
3


function multiply(a, b) {
return a? b + multiply(a-1, b) : 0;
}

JavaScript

Now suppose we have a function called ‘doMultiply’, which does everything that multiply does, but has to be explicitly given
itself as an argument in order to recurse using itself. We could do something like this:
Javascript
1
2
3
4
5
6

function
return
}
function
return
}

doMultiply(dm, a, b) {
a? b + dm(dm, a-1, b) : 0;

JavaScript

multiply(a, b) {

doMultiply(doMultiply, a, b);

Lisp
1
2
3
4
5
6

(defun do-multiply (dm a b)
(if (zerop a)
0
(+ b (funcall dm a b))))
(defun multiply (a b)
(do-multiply #'do-multiply a b))

And to tidy things up, I’ve introduced a ‘recursor’ functino that just supplies a function with itself and its ‘base’ arguments:


Javascript
1
2
3
4
5
6
7
8
9


function
return
}
function
return
}
function
return
}

doMultiply(dm, a, b) {
a ? b + dm(dm, a-1, b) : 0;

JavaScript

recursor(fxn, a, b) {
fxn(fxn, a, b);
multiply(a, b) {
recursor(doMultiply, a, b);

Lisp
1
2
3
4
5
6
7
8

9
10

(defun do-multiply (dm a b)
(if (zerop a)
0
(+ b (funcall dm dm (1- a) b))))
(defun recursor (f a b)
(funcall f f a b))
(defun multiply (a b)
(recursor #'do-multiply a b))

In javascript, there’s a tidy little notation that I like to call parenfuncs or (..)=>….-style expressions. The way these work is that
you have a list of arguments followed by a block or expression. For the sake of beauty, the only operator we’re going to allow is
the ?: triadic operator (where a?b:c yields b if a evaluates to true/not-false, and b otherwise), and we’re not going to allow any
brackets. Our function is simple enough not to need a lot of reconfiguring, and the functions above could be defined as such:
Javascript
1
2
3

var doMultiply = (dm, a, b) => a ? b + dm(dm, a-1, b) : 0;
var recursor = (fxn, a, b) => fxn(fxn, a, b);
var multiply = (a, b) => recursor(doMultiply, a, b);

JavaScript

Now the beauty of the parenfuncs (especially with the guidelines I set forth) is that you can replace any occurrence of a named
parenfunc with the goop inside it. For example, if we’re given:
Javascript

1
2

var bar = (n) => n * n;
var foo = (a, b) => bar(a - 1) - b;

JavaScript

We can replace the second bar with what’s left of the =>, substituting ‘a - 1’ for ‘n’:
Javascript
1

var foo = (a, b) => (a - 1) * (a - 1) - b;

Verbose? Yes. Tedious? Yes. But please bear with me. Let’s ‘fold’ the recursor function into the multiply function:

JavaScript


Javascript
1
2

var doMultiply = (dm, a, b) => a ? b + dm(dm, a-1, b) : 0;
var multiply = (a, b) => ((fxn, a, b) => fxn(fxn, a, b))(doMultiply, a, b);

JavaScript

Lisp
1

2
3
4
5
6
7

(defun do-multiply (dm a b)
(if (zerop a)
0
(+ b (funcall dm dm (1- a) b))))
(defun multiply (a b)
(funcall #'do-multiply #'do-multiply a b))

And finally, let’s ‘fold’ the ‘doMultiply’ function into the multiply function.
Javascript
1
2

var multiply = (a, b) => ((fxn, a, b) => fxn(fxn, a, b))((dm, a, b) =>
a ? b + multiply(dm, a-1, b) : 0, a, b);

JavaScript

Lisp
1
2
3
4
5

6
7

(defun multiply (a b)
((lambda (fxn a b) (funcall fxn fxn a b))
#'(lambda (dm a b)
(if (zerop a)
0
(+ b (funcall dm dm (1- a) b))))
a b))

3. Down the Rabbit Hole
And just to be a little crazy, we’re not even gonna add automatically. We’ll go through the process somewhat quicker here. We
start with a recursive function:
Javascript
1
2
3

var add(c, d) {
return c ? 1 + add(c - 1, d) : d;
}

Lisp
1
2
3
4

(defun add (c d)

(if (zerop c)
d
(1+ (add (1- c) d))))

JavaScript


Redefine it to use the ‘recursor’ function (seen above):
Javascript
1
2
3

var doAdd = (da, c, d) => c ? 1 + da(da, c-1, d) : d;
var add = (c, d) => recursor(doAdd, c, d);
var recursor = (fxn, a, b) => fxn(fxn, a, b);

JavaScript

Lisp
1
2
3
4
5
6
7
8
9
10


(defun do-add (da c d)
(if (zerop c)
d
(1+ (funcall da da (1- c) d))))
(defun recursor (fxn a b)
(funcall fxn fxn a b))
(defun add (c d)
(recursor #'do-add c d))

So let’s look back at our ‘multiply’ function, and put in the ‘add’ function without folding it in:
Javascript
1
2
3

var multiply = (a, b) => ((fxn, a, b) =>
fxn(fxn, a, b))((dm, a, b) =>
a ? add(b, multiply(dm, a-1, b)) : 0, a, b);

JavaScript

Lisp
1
2
3
4
5
6
7

8
9

(defun multiply (a)
(lambda (b)
((lambda (f)
(funcall (funcall (funcall f f) a) b)) #'(lambda (dm)
(lambda (x)
(lambda (y)
(if (zerop x)
0
(add y (funcall (funcall (funcall dm dm) (1- x)) y))))))))

And then fold the add function together in a similar fashion.
Javascript
1
2

Lisp

var add = (c, d) => ((fxn, c, d) => fxn(fxn, c, d))
((da, c, d) => c ? 1 + da(da, c-1, d) : d, c, d);

JavaScript


1
2
3
4

5
6
7
8

(defun add (c d)
((lambda (fxn c d)
(funcall fxn fxn c d))
#'(lambda (da c d)
(if (zerop c)
d
(1+ (funcall da da (1- c) d))))
c d))

When we fold in the ‘add’ function; when c := b, and d := dm(dm, a-1, b), the add(b, dm(dm, a-1, b)) becomes:
Javascript
1
2
3

((fxn, c, d) => fxn(fxn, c, d))((da, c, d) => c ?
1 + da(da, c-1, d)
: d, b, dm(dm, a-1, b))

JavaScript

Lisp
1
2
3

4
5
6
7

((lambda (fxn c d)
(funcall fxn fxn c d))
#'(lambda (da c d)
(if (zerop c)
d
(1+ (funcall da da (1- c) d))))
b (funcall dm dm (1- a) b))

And putting it together, we get:
Javascript
1
2
3
4

Lisp

var multiply = (a, b) => ((fxn, a, b) =>
fxn(fxn, a, b))((dm, a, b) => a ?
((fxn, c, d) => fxn(fxn, c, d))((da, c, d) => c ? d
: 1 + da(da, c-1, d), b, dm(dm, a-1, b)) : 0, a, b);

JavaScript



1
2
3
4
5
6
7
8
9
10
11
12
13
14

(defun multiply (a b)
((lambda (fxn a b)
(funcall fxn fxn a b))
#'(lambda (dm a b)
(if (zerop a)
0
((lambda (fxn c d)
(funcall fxn fxn c d))
#'(lambda (da c d)
(if (zerop c)
d
(1+ (funcall da da (1- c) d))))
b (funcall dm dm (1- a) b))))
a b))


Or, putting it more one-letter-per-concept-y (i.e., cryptically):
Javascript
1
2
3
4
5
6
7
8
9
10
11

var multiply =
(a, b) =>
((f, a, b) =>
f(f, a, b))(
(f, a, b) =>
a ? ((g, c, d) =>
g(g, c, d))(
(g, c, d) =>
c == 0 ? d : 1 + g(g, c-1, d), b, f(f, a-1, b)) : 0,
a,
b)

JavaScript

Lisp
1

2
3
4
5
6
7
8
9
10
11
12
13
14

(defun multiply (a b)
((lambda (f a b)
(funcall f f a b))
#'(lambda (f a b)
(if (zerop a)
0
((lambda (g c d)
(funcall g g c d))
#'(lambda (g c d)
(if (zerop c)
d
(1+ (funcall g g (1- c) d))))
b (funcall f f (1- a) b))))
a b))

That’s a lot of layers! And now we do something interesting. Suppose we only allow monads (functions with one input, not an

ordered set of inputs). We can do this in the following way. Suppose we start with this:
Javascript


1
2
3

function foo(a, b, c) {
return 3 * a - (b - 1) * (c - 2);
}

JavaScript

Lisp
1
2

(defun foo (a b c)
(- (* 3 a) (* (1- b) (- c 2))))

We could have this return a function that takes only a, and returns a function which takes in (b, c) and outputs the same value.
This may seem like a lateral step, but notice that we’re getting rid of commas.
Javascript
1
2
3
4
5


function foo(a) {
return function(b, c) {
return 3 * a - (b - 1) * (c - 2);
}
}

JavaScript

Lisp
1
2
3

(defun foo (a)
(lambda (b c)
(- (* 3 a) (* (1- b) (- c 2)))))

One step further, we get:
Javascript
1
2
3
4
5
6
7

function foo(a) {
return function(b) {
return function(c) {

return 3 * a - (b - 1) * (c - 2);
}
}
}

Lisp
1
2
3
4

(defun foo (a)
(lambda (b)
(lambda (c)
(- (* 3 a) (* (1- b) (- c 2))))))

“Of course, a call with the original ‘foo’ would be something like:
Javascript

JavaScript


1

foo(3,4,6)

JavaScript

Lisp
1


(foo 3 4 6)

And get -3. With the latest foo, we have to oo:
Javascript
1

foo(3)(4)(6)

JavaScript

Lisp
1

(funcall (funcall (foo 3) 4) 6)

And still we get -3. Doing this with (..)=>….-style expressions, we get the following progression:
Javascript
1
2
3

var foo = (a, b, c) => 3 * a - (b - 1) * (c - 2);
var foo = (a) => (b, c) => 3 * a - (b - 1) * (c - 2);
var foo = (a) => (b) => (c) => 3 * a - (b - 1) * (c - 2);

JavaScript

And it works just like the final foo! Try it yourself. Using these principles (and, admittedly, a little bit of trial and error (I know
what I mean; why should the computer have to?), I derived the following expression for ‘multiply’:

Javascript
1
2
3
4
5
6
7
8
9

Lisp

var multiply =
(a)=>(b)=>
((f)=>(a)=>(b)=>
f(f)(a)(b))(
(f) => (a) => (b) =>
a ? ((g) => (c) => (d) =>
g(g)(c)(d))(
(g) => (c) => (d) =>
c == 0 ? d : 1 + g(g)(c-1)(d))(b)(f(f)(a-1)(b)) : 0)(a)(b)

JavaScript


1
2
3
4

5
6
7
8
9
10
11
12
13
14
15
16
17

(defun multiply (a)
(lambda (b)
((lambda (f)
(funcall (funcall (funcall f f) a) b)) #'(lambda (dm)
(lambda (x)
(lambda (y)
(if (zerop x)
0
(funcall ((lambda (c) (lambda (d)
(funcall (funcall ((lambda (fxn) (lambda (a) (lambda (b)
(funcall (funcall (funcall fxn fxn) a) b)))) #'(lambda (da)
(lambda (c)
(lambda (d)
(if (zerop c)
d
(1+ (funcall (funcall (funcall da da) (1- c)) d))))))) c) d))) y)

(funcall (funcall (funcall dm dm) (1- x)) y)))))))))

"Adding words back in, you can see that it’s still pretty cryptic.
Javascript
1
2
3
4
5
6
7
8
9
10
11

Lisp

var multiply =
(multiplier)=>(multiplicand)=>
((doMultiply)=>(multiplier)=>(multiplicand)=>
doMultiply(doMultiply)(multiplier)(multiplicand))(
(doMultiply) => (multiplier) => (multiplicand) =>
multiplier ? ((doAdd) => (adder) => (addend) =>
doAdd(doAdd)(adder)(addend))(
(doAdd) => (adder) => (addend) =>
adder == 0 ? addend : 1 + doAdd(doAdd)(adder-1)(addend))
(multiplicand)(doMultiply(doMultiply)(multiplier-1)(multiplicand))
: 0)(multiplier)(multiplicand)


JavaScript


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

28
29
30
31
32
33
34
35

(defun multiply (multiplier)
(lambda (multiplicand)
((lambda (multiplier-recursor)
(funcall (funcall
(funcall multiplier-recursor multiplier-recursor)
multiplier)
multiplicand))
#'(lambda (do-multiply)
(lambda (multiplier)
(lambda (multiplicand)
(if (zerop multiplier)
0
(funcall ((lambda (adder) (lambda (addend)
(funcall (funcall
((lambda (adder-recursor)
(lambda (multiplier)
(lambda (multiplicand)
(funcall (funcall
(funcall adder-recursor adder-recursor)
multiplier)
multiplicand))))

#'(lambda (do-add)
(lambda (adder)
(lambda (addend)
(if (zerop adder)
addend
(1+ (funcall (funcall
(funcall do-add do-add)
(1- adder))
addend))))))) adder) addend)))
multiplicand)
(funcall (funcall
(funcall do-multiply do-multiply)
(1- multiplier))
multiplicand)))))))))

Try it yourself. multiply(2)(3) yields 6. This is a slow algorithm; it only counts one at a time. So while we’re writing toy programs,
why not go full hog with the crypticality? We don’t even need names (except fluid names to refer to variables within the current
context – no ‘external’ variables):
Javascript
1

Lisp

JavaScript

((c)=>(a)=>(b)=>c(c)(a)(b))((c)=>(a)=>(b)=>a?((d)=>(e)=>(f)=>d(d)(e)(f))((d)=>(e)=>(f)=>e==0?f:1+d(d)(e-1)


1
2

3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

33
34
35
36

(funcall ((lambda (multiplier)
(lambda (multiplicand)
((lambda (multiplier-recursor)
(funcall (funcall
(funcall multiplier-recursor multiplier-recursor)
multiplier)
multiplicand)) #'(lambda (do-multiply)
(lambda (multiplier)
(lambda (multiplicand)
(if (zerop multiplier)
0
(funcall ((lambda (adder) (lambda (addend)
(funcall (funcall
((lambda (adder-recursor)
(lambda (multiplier)
(lambda (multiplicand)
(funcall (funcall
(funcall adder-recursor adder-recursor)
multiplier)
multiplicand)))) #'(lambda (do-add)
(lambda (adder)
(lambda (addend)
(if (zerop adder)
addend
(1+

(funcall (funcall (funcall do-add do-add)
(1- adder))
addend)))))))
adder)
addend)))
multiplicand)
(funcall (funcall
(funcall do-multiply do-multiply)
(1- multiplier))
multiplicand)))))))))
2) 3)

4. Behind the Scenes
If you try this with large numbers like 35,543,512 and 578,512, you’ll get either a stack error after a second or two, or if you
have an insane amount of RAM and a great javascript interpreter, you might just freeze your computer for a day or so on a fast
processor. This is because, at a base level, what you are doing when you multiply 2 and 3 is this:


1
2
3
4
5
6
7
8
9
10
11
12

13
14
15
16
17
18

mult(2, 3)
add(3, mult(1, 3))
add(3, add(3, mult(0, 3)))
add(3, add(3, 0))
add(3, 1+ add(2, 0))
add(3, 1+ (1+ add(1, 0)))
add(3, 1+ (1+ (1+ add(0, 0))))
add(3, 1+ (1+ (1+ 0)))
add(3, 1+ (1+ 1))
add(3, 1+ 2)
add(3, 3)
1+ add(2, 3)
1+ (1+ add(1, 3))
1+ (1+ (1+ add(0, 3)))
1+ (1+ (1+ 3))
1+ (1+ 4)
1+ 5
6

So to get six, our final answer, we basically had to count one-by-one.
You may be wondering why we’re allowing the 1+x operation (such as (1 + 3) -> 4) to take place without deconstruction. The
reason why is because there has to be some atomic operation that changes numbers. In Peano arithmentic (where this tutorial
took inspiration from), this is known as the successor function. That is to say, there is a ‘0’ (by law), and a ’S(uccessor)‘ function

(by law). The 'meaning’ of the successor function of x (S(x)) is, intuitively, ‘one plus x’, but the axioms never state this (because
they are an attempt at symbolic purity). So ‘1’ is just shorthand for S(0), ‘2’ is just S(1) = S(S(0)), and something like ‘11’ is just
our place-value shorthand for S(S(S(S(S(S(S(S(S(S(S(0))))))))))) (whew).

5. Recursion: Every Change Unquestionably Recurrant; Self Instantiation Over Nothing
So what have we learned here? We’ve learned that recursion is possible without referring (explicitly) back to the original
function, but we’ve also learned a valuable lesson about assumptions. Even with something that beginning students find
challenging (such as recursion), there is a wealth of knowledge to be found by asking even the most basic of questions.”



×