A functional view -- find and replace
First, I'm going to write the fact function slightly differently, because it will make the first version of my explanation easier.
function fact(n)
return n == 0
and (1)
or (n * fact(n - 1))
end
This is using and/or as a "ternary" operator. The expression cond and a or b evaluates to a when cond is true and b when cond is false. So, the above does exactly the same thing as the original program, it's just reduced to a single return statement.
Now, how does Lua evaluate fact(3)? The simplest way to think of it is just replacing fact(3) with the body of fact, where all the ns have been replaced with 3s:
fact(3)
3 == 0 and (1) or (3 * fact(3 - 1))
3 * fact(3 - 1)
3 * fact(2)
3 * (2 == 0 and (1) or (3 * fact(3 - 1)))
3 * (2 * fact(4 - 1))
3 * (2 * fact(1))
3 * (2 * (1 == 0 and (1) or (1 * fact(1 - 1)))
3 * (2 * (1 * fact(1 - 1)))
3 * (2 * (1 * fact(0)))
3 * (2 * (1 * (0 == 0 and (1) or 0 * fact(0 - 1))))
3 * (2 * (1 * 1)))
6
That is, whenever Lua comes across a call expression, it simply plugs in the numbers to the function body. A recursive function call is no different -- it just may take a while to plug in to the resulting function calls!
An imperative view -- the stack
Expressions in Lua aren't actually evaluated by replacing a function expression with the body of a function.
Before Lua is executed, it's first "compiled" into a "bytecode" -- a much tinier programming language which is smaller and faster to execute than Lua, but much harder to read and write. That byte code contains only a small number of "instructions" which do a few things.
Let's approximate Lua's bytecode with the following operations:
v1 := (v2 + v3);
v1 := (v2 - v3);
v1 := (v2 * v3);
v4 := f(v5);
v6 := (v7 == v8);
if v9 {
codeblock_alpha
} else {
codeblock_beta
}
return (v10);
The function fact compiled into the above "bytecode" might look something like this:
fact(n) {
is_zero := (n == 0);
if is_zero {
return 1;
} else {
smaller := (n - 1);
recursive := f(smaller);
result := (n * recursive);
return (result);
}
}
Now, how do we execute this?
First, realize an important property of local variables in Lua. Two functions that call each other that use the same local variable name still get their own variables:
function beta()
local x = 2
local b = "bye"
end
function alpha()
local x = 1
local a = "hi"
beta()
print(x)
end
The way this works is by creating a new stack frame each time you start executing a function. All local variables live in the new stack frame, so each local x is creating an entry in different stack frames:
local x = 1
local a = "hi"
local x = 2
local b = "bye"
Let's return to the "bytecode" version of fact and execute it manually for fact(3). n is just a "regular" local variable, so we start like this:
==FRAME OF fact(3)==
local n = 3
executing line 2
After executing line 2, we get this state:
==FRAME OF fact(3)==
local n = 3
local is_zero = false
executing line 3
Next we hit the if. Since the condition is_zero is false, we'll take the else branch, and the next line to execute will be line 6:
==FRAME OF fact(3)==
local n = 3
local is_zero = false
executing line 6
Now we execute line 6, computing n - 1:
==FRAME OF fact(3)==
local n = 3
local is_zero = false
local smaller = 2
executing line 7
Now we get to the interesting line, a function call. Each function call needs its own local variables, so we push a new frame onto the stack. This new frame will start executing on line 2 with n=2, since smaller is 2 in the calling frame:
==FRAME OF fact(3)==
local n = 3
local is_zero = false
local smaller = 2
executing line 7
==FRAME OF fact(2)==
local n = 2
executing 2
Now we do a similar thing. We compute is_zero in the new frame (leaving the old frame alone):
==FRAME OF fact(3)==
local n = 3
local is_zero = false
local smaller = 2
executing line 7
==FRAME OF fact(2)==
local n = 2
local is_zero = false
executing 3
We take the else branch again
==FRAME OF fact(3)==
local n = 3
local is_zero = false
local smaller = 2
executing line 7
==FRAME OF fact(2)==
local n = 2
local is_zero = false
executing 6
We compute smaller again:
==FRAME OF fact(3)==
local n = 3
local is_zero = false
local smaller = 2
executing line 7
==FRAME OF fact(2)==
local n = 2
local is_zero = false
local smaller = 1
executing 7
and we reach another function call. This once again pushes a new stack frame:
==FRAME OF fact(3)==
......
==FRAME OF fact(2)==
local n = 2
local is_zero = false
local smaller = 1
executing 7
==FRAME OF fact(1)==
local n = 1
executing 2
once again!
==FRAME OF fact(3)==
......
==FRAME OF fact(2)==
local n = 2
local is_zero = false
local smaller = 1
executing 7
==FRAME OF fact(1)==
local n = 1
local is_zero = false
executing 3
==FRAME OF fact(3)==
......
==FRAME OF fact(2)==
local n = 2
local is_zero = false
local smaller = 1
executing 7
==FRAME OF fact(1)==
local n = 1
local is_zero = false
executing 6
==FRAME OF fact(3)==
......
==FRAME OF fact(2)==
local n = 2
local is_zero = false
local smaller = 1
executing 7
==FRAME OF fact(1)==
local n = 1
local is_zero = false
local smaller = 0
executing 7
==FRAME OF fact(3)==
......
==FRAME OF fact(2)==
......
==FRAME OF fact(1)==
local n = 1
local is_zero = false
local smaller = 0
executing 7
==FRAME OF fact(0)==
local n = 0
executing 2
Now we're in the final call, fact(0). This one looks slightly different:
==FRAME OF fact(3)==
local n = 3
......
==FRAME OF fact(2)==
......
==FRAME OF fact(1)==
local n = 1
local is_zero = false
local smaller = 0
executing 7
==FRAME OF fact(0)==
local n = 0
local is_zero = true
executing 3
==FRAME OF fact(3)==
......
==FRAME OF fact(2)==
......
==FRAME OF fact(1)==
local n = 1
local is_zero = false
local smaller = 0
executing 7
==FRAME OF fact(0)==
local n = 0
local is_zero = true
executing 4
Now we are at a return statement. This will remove the currently running stack, and write the returned value into the appropriate value. Since the previous frame is executing a function on line 7 that returns into recursive, we'll update the previous stack by writing into recursive with the returned value 1.
==FRAME OF fact(3)==
......
==FRAME OF fact(2)==
local n = 2
local is_zero = false
local smaller = 1
executing 7
==FRAME OF fact(1)==
local n = 1
local is_zero = false
local smaller = 0
local recursive = 1
executing 8
Now executing 8 is just computing a product:
==FRAME OF fact(3)==
......
==FRAME OF fact(2)==
local n = 2
local is_zero = false
local smaller = 1
executing 7
==FRAME OF fact(1)==
local n = 1
local is_zero = false
local smaller = 0
local recursive = 1
local result = 1
executing 9
and now we return result which is valued 1 to the calling stack frame's receiving variable recursive:
==FRAME OF fact(3)==
local n = 3
local is_zero = false
local smaller = 2
executing line 7
==FRAME OF fact(2)==
local n = 2
local is_zero = false
local smaller = 1
local recursive = 1
executing 8
and again:
==FRAME OF fact(3)==
local n = 3
local is_zero = false
local smaller = 2
executing line 7
==FRAME OF fact(2)==
local n = 2
local is_zero = false
local smaller = 1
local recursive = 1
local result = 2
executing 9
and returning the result, value 2, to recursive of fact(3)'s stack frame:
==FRAME OF fact(3)==
local n = 3
local is_zero = false
local smaller = 2
local recursive = 2
executing line 8
and finally evaluating the final product:
==FRAME OF fact(3)==
local n = 3
local is_zero = false
local smaller = 2
local recursive = 2
local result = 6
executing line 9
returning the value result which is 6:
result 6
This is more or less "exactly" how Lua makes recursive functions work. It has a stack of frames, and each call makes a new frame on top of all of the existing ones; a recursive call puts yet another frame after, and when it returns it goes back to its work from earlier on.
Notice how the list of frames got bigger as execution went forward. You only have so much space for the stack; if your code needs too many stack frames, it will eventually just fail because Lua didn't make enough space for it to do its figuring.
2 comments
huh?
this is a great question