In the previous chapter, the concept and use of linearity was explained. Making PHP code linear makes it easier to convert to Node.js code. But what if PHP code cannot be made linear?
A good example of PHP code that cannot simply be made linear is a
for
statement that writes to multiple
files. The PHP code remains stubbornly linear because the for
statement cannot be eliminated and the
blocking PHP API calls cannot be moved outside of it:
for
(
$i
=
0
;
$i
<
$n
;
++
$i
)
{
$fp
=
fopen
(
'fp.txt'
+
$i
,
'w'
);
fwrite
(
$fp
,
$data
);
fclose
(
$fp
);
}
The Node.js “callback chain” illustrates how the Node.js code will work. A callback chain is formed where each subsequent nonblocking Node.js API call is nested inside the callback of the previous nonblocking Node.js API call, forming an arbitrarily long chain:
fs
.
open
(
'fp.txt0'
,
'w'
,
0666
,
function
(
error
,
fp
)
{
fs
.
write
(
fp
,
data
,
null
,
'utf-8'
,
function
()
{
fs
.
close
(
fp
,
function
(
error
)
{
fs
.
open
(
'fp.txt1'
,
'w'
,
0666
,
function
(
error
,
fp
)
{
fs
.
write
(
fp
,
data
,
null
,
'utf-8'
,
function
()
{
fs
.
close
(
fp
,
function
(
error
)
{
// and on and on for a total of "n" times
});
});
});
});
});
});
Although this example illustrates how the Node.js code needs to
function, it is not practical because the value of the n
variable might only be known at runtime. To
actually implement the callback chain in Node.js, the code must use three
relatively sophisticated JavaScript features: anonymous functions, lambdas,
and closures.
An anonymous function is a function without a name. Named functions are general-purpose tools, ready and available for other JavaScript code to invoke at any time. In contrast, anonymous functions are usually special purpose and specific to the code that they are declared in.
How can you call a function without a name? An anonymous function is often also a lambda function. A lambda function is a function that can be assigned to a variable and that variable functions very much like any other variable. A lambda function can also be invoked using the usual function call syntax with the variable that the lambda function is assigned to. An anonymous function is assigned to a variable (or passed to a function where it is assigned to one of the function arguments) and can be invoked using the variable and the function call syntax. For example:
var
f
=
function
(
a
)
{
// lambda function
return
a
+
1
;
};
var
b
=
f
(
4
);
// b takes the value of 5
The word “lambda" may sound highfalutin and complicated but it hides the simple concept that functions are first-class values, like numbers and strings, and can mostly be treated and manipulated in the same ways that numbers and strings can.
To make anonymous functions easier and more capable, JavaScript supports closure. Closure is a language feature where, when the function is defined, the outer context of a function is preserved and is made available to the function when it is invoked. The value of any variables in the saved context is persistent over time and there is only one value at any given time; all invocations of the function share the same context and refer to the same variables. An example of closure is:
function
f
()
{
var
b
=
6
;
function
g
()
{
// b is closed over by this function
++
b
;
return
b
;
}
return
g
;
}
var
h
=
f
();
var
c
=
h
();
// c is 7
var
d
=
h
();
// d is 8
In this example, the b
variable, even though
it is not a local variable of the g()
function, is still available to the g()
function even after the f()
function
has exited. The b
variable (also called
an “upvalue” in closure terminology) behaves sort of like a private global
variable. It behaves like a global variable to the g()
function but is hidden from all other code
that is outside of the f()
function.
Anonymous functions, lambdas, and closures are needed to implement a callback chain that can be arbitrarily long:
var
f
=
function
()
{
};
for
(
var
i
=
n
-
1
;
i
>=
0
;
--
i
)
{
f
=
function
(
g
,
j
)
{
return
function
()
{
fs
.
open
(
'fp.txt'
+
j
,
'w'
,
0666
,
function
(
error
,
fp
)
{
fs
.
write
(
fp
,
data
,
null
,
'utf-8'
,
function
()
{
fs
.
close
(
fp
,
function
(
error
)
{
g
();
}
});
});
};
}
(
f
,
i
);
}
f
();
The Node.js code is quite complicated. The overall algorithm is to build the callback chain in reverse order, from the most deeply nested call to the outermost call, and then to kick off the callback chain by calling the first one. Let us go through it, step by step.
A do-nothing, default lambda function is created that will be the
“no-op” (empty) callback for the innermost fs.close()
call:
var
f
=
function
()
{
};
In the PHP code, the for
statement goes from
0
to n-1
but, since callbacks need to be built from
the inside out, Node.js requires the for
statement to
go from n-1
to 0
:
for
(
var
i
=
n
-
1
;
i
>=
0
;
--
i
)
{
// create the callback
}
The current callback is held in the f
variable to capture the value during this
specific iteration of the for
loop. The
callback just outside the current callback will be created by using a new
lambda function that does the work for the outer callback and then calls
the current (inner) callback. The new lambda function is a return value
for syntax reasons that will be explained next:
return
function
()
{
fs
.
open
(
'fp.txt'
+
j
,
'w'
,
0666
,
function
(
error
,
fp
)
{
fs
.
write
(
fp
,
data
,
null
,
'utf-8'
,
function
()
{
fs
.
close
(
fp
,
function
(
error
)
{
g
();
// current callback
}
});
});
}
The newly created callback function is returned as an anonymous function. An anonymous function is an unnamed function that can be used as a value. The inner anonymous function takes zero arguments and is used as the return value for an outer function that takes two arguments. The outer function is needed to defeat closure.
In this case, closure must be defeated so that the values of the
f
and i
variables during this iteration of the loop
are preserved, instead of their values at some later time, such as when
the f
function is invoked. Closure is
defeated by making new variables, actually, arguments, that will always
have the same values. The g
variable
holds the value of the f
variable
during this iteration of the for
loop;
likewise, the j
variable holds the
value of the i
variable during this
iteration of the for
loop. The outer
anonymous function (which takes two arguments) is invoked immediately, as
indicated by the (f, i)
string at the
end of the function definition. When invoked, it captures the f
and i
values and returns the inner anonymous function. The inner anonymous
function actually contains the callback code:
f
=
function
(
g
,
j
)
{
return
function
()
{
// implement outer callback
// call g() to invoke inner callback
};
}
(
f
,
i
);
The for
statement loops from
n-1
to 0
, creating inner callbacks and nesting them
into outer callbacks. When the for
statement exits, the
f
variable contains the outermost
callback, which will properly invoke its subsequent callback, which will
invoke its subsequent callback, and so on, all in the correct order. All
that is left is to invoke the outermost callback:
f
();
This Node.js code works but it has two drawbacks. First, the PHP code looks nothing like the Node.js code. If callback chains occur often (which is common), maintaining and improving the PHP version and the Node.js version simultaneously is going to be very difficult if they are very different. Second, the Node.js code by itself is pretty complicated and requires three advanced JavaScript concepts: lambdas, anonymous functions, and closure.
PHP 5.3 introduced anonymous functions, lambdas, and closure to the PHP language. To make the PHP similar to the Node.js code, the PHP code could be refactored from its step-by-step algorithm to the complicated algorithm used by the Node.js function.
Lambdas and anonymous functions in PHP 5.3 are very similar to
lambdas in Node.js. The only difference is syntactic. PHP variables use
the dollar sign ($) while Node.js variables do not; Node.js variables are
required to be declared using the var
keyword:
$f
=
function
(
$a
)
{
// lambda function
return
$a
+
1
;
};
$b
=
$f
(
4
);
// $b takes the value of 5
PHP 5.3 also supports closure. Unlike Node.js, the entire outer
context is not automatically preserved in PHP 5.3 like it is in Node.js.
In PHP 5.3, the use
keyword must be
used to indicate the specific variables (upvalues) that will be captured
(closed over) by the function. Variables can be captured in two ways: they
can be captured by value or by reference.
If a PHP variable is captured by value, each invocation of the lambda function will reset the variable to its value when the lambda function was created. A variable captured by value freezes its value in time.
If a PHP variable is captured by reference, each invocation of the lambda function shares the same variable. If one invocation modifies the variable, future invocations will see the modification. Capturing by reference might be considered real or true closure and is equivalent to how closure works in Node.js:
function
f
()
{
$b
=
6
;
$g
=
function
()
use
(
&
$b
)
{
// b is closed over by this function
++
$b
;
return
$b
;
}
return
$g
;
}
$h
=
f
();
$c
=
$h
();
// c is 7
$d
=
$h
();
// d is 8
This example captures the $b
variable by reference. Since the $b
variable will be preserved across invocations, the $d
variable is assigned the value of 8
. Initially, the $b
variable is set to 6
. Then, when the $c
variable is assigned, the $b
variable is incremented to 7
. Finally, when the $d
variable is assigned, the $b
variable is incremented to 8
.
If the $b
variable was
captured by value instead of by reference, any call to the $g
function would always return 7
. In that case, when the $c
variable is assigned, the $b
variable is incremented to 7
. But when the $d
variable is assigned, the $b
variable is reset to 6
and then incremented to 7
. The $d
variable would receive a value of 7
instead of a value of 8
.
What is the purpose of capturing by value if it is not really closure? Capturing by value is how PHP 5.3 defeats closure.
Earlier, it was shown that Node.js needs to defeat closure in order to take the current value of a variable when a function is defined, instead of its current value at the time of invocation. Here’s a simple example of defeating closure in Node.js:
for
(
var
i
=
0
;
i
<
n
;
++
i
)
{
f
[
i
]
=
function
(
j
)
{
return
function
()
{
return
j
;
};
}
(
i
);
}
In Node.js, an outer function and an inner function are used to
defeat closure. The outer function, which takes a single argument, is
declared and then immediately invoked as indicated by the (i)
string at the end of the function
definition. When it is invoked, the value of the i
variable is “frozen” in the j
argument. The inner function captures the
j
variable instead of the i
variable. Even though the j
variable is captured (essentially by
reference), a new j
variable is
declared each time through the loop. The j
variable is many variables that take the value
of the i
variable when the function is
defined; the i
variable itself is a
single variable with a single value that is shared across all
invocations.
Consider alternative Node.js code where closure is not defeated:
for
(
var
i
=
0
;
i
<
n
;
++
i
)
{
f
[
i
]
=
function
()
{
return
i
;
};
}
In this Node.js example, a single i
variable is shared across all the functions.
The i
variable breaks out of the loop
when the i
variable reaches the value
of n
. Since the i
variable now has the value of n
, all the functions that were generated will
return the current value of i
, which is
n
.
PHP 5.3 is much simpler. Instead of declaring two functions where an outer function is required to defeat closure in the inner function, PHP 5.3 allows closure values to be captured by value, instead of reference. Defeating closure is a language feature that results in simpler code:
for
(
$i
=
0
;
$i
<
$n
;
++
$i
)
{
$f
[
$i
]
=
function
()
use
(
$i
)
{
return
$i
;
};
}
Closure (i.e., true closure) is enabled by using the ampersand
(&) in the use
clause of the code;
removing the ampersand (&) defeats closure. The extra complication of
outer and inner functions and immediate invocation in Node.js is not
needed in PHP 5.3.
Using anonymous functions, lambdas, and closure by value (i.e., defeating closure), the Node.js callback chain algorithm can be implemented in PHP 5.3. The new PHP 5.3 code is more complex than the original code, but by design, it is very similar to the Node.js code:
$f
=
function
()
{
};
for
(
$i
=
$n
-
1
;
$i
>=
0
;
--
$n
)
{
$f
=
function
()
use
(
$f
,
$i
)
{
$fp
=
fopen
(
'fp.txt'
+
$i
,
'w'
);
fwrite
(
$fp
,
$data
);
fclose
(
$fp
);
$f
();
}
}
$f
();
The corresponding Node.js code is:
var
f
=
function
()
{
};
for
(
var
i
=
n
-
1
;
i
>=
0
;
--
i
)
{
f
=
function
(
g
,
j
)
{
return
function
()
{
fs
.
open
(
'fp.txt'
+
j
,
'w'
,
0666
,
function
(
error
,
fp
)
{
fs
.
write
(
fp
,
data
,
null
,
'utf-8'
,
function
()
{
fs
.
close
(
fp
,
function
(
error
)
{
g
();
}
});
});
};
}
(
f
,
i
);
}
f
();
Since the PHP 5.3 code now looks so similar to the Node.js code, it is much easier to improve the code and add features while maintaining both the PHP and Node.js versions simultaneously.
The PHP 5.3 code can be improved to parallel the Node.js code
more closely. The definition of the f
function inside the for
statement looks
like this:
$fp
=
fopen
(
'fp.txt'
+
$i
,
'w'
);
fwrite
(
$fp
,
$data
);
fclose
(
$fp
);
$f
();
The corresponding Node.js code looks like this:
fs
.
open
(
'fp.txt'
+
j
,
'w'
,
0666
,
function
(
error
,
fp
)
{
fs
.
write
(
fp
,
data
,
null
,
'utf-8'
,
function
()
{
fs
.
close
(
fp
,
function
(
error
)
{
g
();
}
});
});
Notice anything familiar? The linearity-based conversion recipes
from Chapter 3 can be applied here! The PHP 5.3
implementation of the f
function can be
made linear and converted to Node.js code easily and independently. The
outer context of the function is irrelevant and is totally orthogonal to
converting the implementation of function f
.
One annoyance is that the PHP 5.3 code uses $i
and $f
,
the original names of the variables captured using closure by value,
whereas the Node.js renames these variables to j
and g
. This
annoyance is easily remedied by creating temporary variables, $j
and $g
, in
the PHP 5.3 code:
$g
=
$f
;
$j
=
$i
;
$fp
=
fopen
(
'fp.txt'
+
$j
,
'w'
);
fwrite
(
$fp
,
$data
);
fclose
(
$fp
);
$g
();
The final PHP 5.3 implementation of the callback chain algorithm is:
$f
=
function
()
{
};
for
(
$i
=
$n
-
1
;
$i
>=
0
;
--
$n
)
{
$f
=
function
()
use
(
$f
,
$i
)
{
$g
=
$f
;
$j
=
$i
;
$fp
=
fopen
(
'fp.txt'
+
$j
,
'w'
);
fwrite
(
$fp
,
$data
);
fclose
(
$fp
);
$g
();
}
}
$f
();
So, a useful conversion recipe is to rewrite PHP 5.3 code from a step-by-step algorithm to a callback chain algorithm.
A pattern is developing. Converting PHP code to Node.js code often involves refactoring the PHP code so it is more like Node.js, not simply taking the PHP code as is and grinding through a bunch of conversion steps to output Node.js code at the other end. Refactoring the PHP code can even be seen as converting Node.js features and algorithms to PHP such that when the “official” conversion process begins, the PHP code is already primed for easy conversion to Node.js code. The conversion process is two-way: the PHP code is converted to Node.js, but in some sense, the Node.js code is converted to PHP code. An acceptable medium is developed between the two languages and the two codebases. Most likely, the developer will decide to compromise the PHP code the most but there will be some cases where the Node.js code makes some minor compromises to keep the PHP code from being too difficult.
PHP 5.3 introduced anonymous functions, lambdas, and closure, which are all needed to easily create Node.js-style callback chain algorithms. What about PHP 4 and other PHP versions below PHP 5.3? Does PHP code that is converted to Node.js have to drop support for PHP versions below 5.3?
Many web hosting companies no longer support or provide PHP 4 or PHP versions below 5.3. Similarly, as cloud computing increases in popularity and virtual machines become the de facto standard for delivering cloud computing, the user can choose any version of PHP that he wants to run and there really is not any reason to choose a PHP version less than 5.3. If your PHP code is targeted for specific users, perhaps you can require those users to install PHP 5.3 and above. If your PHP code is targeted at the general public, perhaps it is not a serious burden or inconvenience to require them to install PHP 5.3 or above or to insist that your users select only services that support PHP 5.3 and above.
Still, there are some good reasons to support PHP 4 and PHP versions below 5.3. One reason is to remove the PHP version as a requirement. Why have a requirement when you do not have to? If your PHP code supports PHP versions below 5.3, you can loosen the requirement from “PHP 5.3 and above” to just “PHP.” The user no longer needs to figure out the PHP version; he only needs to know that he can serve PHP code. Another reason is that the codebase will rely on fewer language features. If you decide to convert your PHP code to even more languages, do you really want to require those languages to support advanced language features like anonymous functions, lambdas, and closure and assume that they are supported in a similar way? In the past, software was written and functioned in languages without these advanced features, so it is probably doable. It may even be easier for novice developers to follow and understand your code. A final reason is that there may be some legacy or custom requirement that forces your users to use a PHP version below 5.3.
For the rest of this chapter, I will present a way to support PHP 4 and PHP versions below 5.3 that will still allow conversion to Node.js code. With this method, the code can still be improved and features can be added while maintaining both the PHP and Node.js versions simultaneously. For simplicity, I will use the phrase “PHP 4” to loosely refer to “PHP 4 and PHP versions below 5.3.”
Anonymous functions, lambdas, and closure are not supported in PHP 4. However, these language features can be simulated in PHP 4 and even the simulations of these features can be converted to Node.js code. Instead of using its built-in versions of anonymous functions, lambdas, and closure, the Node.js code can convert the algorithms that implements the simulations from PHP and run them. Both the PHP and Node.js code will eschew language support for anonymous functions, lambdas, and closure so their code can closely parallel each other.
A simulation of a language feature always uses a subset of the language. Basic features of the language are built upon to accomplish what the more advanced feature could accomplish simply and directly. The Node.js code will always be able to use its basic features to simulate its more advanced features. With trivial differences, it will always be possible for the PHP code that simulates an anonymous function, a lambda, or closure to be directly converted to Node.js code that supports it in the same way. The basic features of PHP and Node.js are nearly identical in functionality, though not in syntax, such that it can confidently be said that “whatever can be built with PHP’s basic features can be built with Node.js’ basic features.” As a result, the PHP code that simulates anonymous functions, lambdas, and closures can be converted and will work the same way in Node.js.
In Node.js, this anonymous function increments its argument and returns the incremented value as its return value:
function
(
a
)
{
return
a
+
1
;
};
In PHP 4, this anonymous function can be simulated by giving it a name. It is now a “simulated anonymous PHP function”:
function
anon_increment
(
$a
)
{
return
$a
+
1
;
}
This simulated anonymous PHP function can be converted back to Node.js:
function
anon_increment
(
a
)
{
return
a
+
1
;
}
If the Node.js code has 20 anonymous functions, they can be simulated by making 20 named functions in PHP and those 20 named PHP functions can be converted back to Node.js as 20 named Node.js functions. For now, it may seem pointless but it can be done.
A lambda function, which is generally an anonymous function, is
stored in a lambda variable. The f
variable stores a lambda function in the following Node.js code:
var
f
=
function
(
a
)
{
return
a
+
1
;
};
In PHP 4, a lambda variable can be simulated using a PHP array. A PHP array can store all the information to “freeze” a function call. Instead of passing around a lambda variable, a PHP array variable can be passed around. If supported properly, the PHP array can do everything that a lambda variable can do:
function
anon_increment
(
$a
)
{
return
$a
+
1
;
}
$f
=
array
(
'func'
=>
'anon_increment'
);
The $f
variable above
encapsulates a lambda variable by storing the name of the function that it
represents. The func
key indicates
which lambda function it is pointing to.
This simulated lambda PHP variable and function can be converted back to Node.js:
function
anon_increment
(
a
)
{
return
a
+
1
;
}
var
f
=
{
'func'
:
'anon_increment'
};
In Node.js, the PHP array becomes a Node.js object. Node.js objects work similarly to PHP arrays and have a similar syntax.
In Node.js, a lambda function can be invoked:
var
f
=
function
(
a
)
{
return
a
+
1
;
};
var
b
=
f
(
4
);
// b takes the value of 5
To simulate the invocation of a lambda function in PHP 4, the target function must accept a call object, a PHP array that represents the lambda function call. The invoking code will create this call object before it invokes the simulated lambda function. The call object combines both the lambda variable and the details of the call, such as the argument values.
The call object will contain several properties. These properties are:
func
A string with the name of the function to invoke
args
An array of objects that are the arguments to the call
ret
An object that is the return value of the call
Each function that will be a target of a simulated lambda
function call will need to have a version of itself that accepts a call
object instead of ordinary arguments and return values. Accepting a call
object will give the utmost flexibility to the target function in terms of
handling the call. By convention, the lf_
prefix is added to the function name to
indicate that it is a “lambda function” handler. The lambda function
handler will unpack the call arguments and repack the return value into
the call object.
A side-by-side comparison of the PHP anon_increment()
function and the PHP lf_anon_increment()
lambda function will
demonstrate how the $a
argument is
unpacked from the lambda call object, the $lc
argument, and how the return value is
repacked into the same:
function
anon_increment
(
$a
)
{
return
$a
+
1
;
}
function
lf_anon_increment
(
$lc
)
{
$a
=
$lc
[
'args'
][
'a'
];
// unpack the arguments
$ret
=
$a
+
1
;
$lc
[
'ret'
]
=
$ret
;
// pack the return value
};
At the start of the function, the argument array is unpacked into individual variables that have the same name. When the function exits, the return value is packed into the call object.
Of course, the simulated lambda function can be converted to Node.js:
function
anon_increment
(
a
)
{
return
a
+
1
;
}
function
lf_anon_increment
(
lc
)
{
var
a
=
lc
[
'args'
][
'a'
];
// unpack the arguments
var
ret
=
a
+
1
;
lc
[
'ret'
]
=
ret
;
// pack the return value
};
A helper function is needed to translate the simulated lambda
call object into an actual function call. This PHP function works like a
very poor man’s JavaScript eval()
function call. It takes a string and turns it into running code. In our
case, it takes a string and turns it into a single function
call:
function
lc_call
(
$lc
)
{
if
(
$lc
[
'func'
]
==
'anon_increment'
)
{
lf_anon_increment
(
$lc
);
}
return
$lc
;
}
For convenience of the callers, the lc_call()
function returns the same call object,
which is passed as an argument. The calling code can sometimes have more
compact code by taking advantage of the returned call object.
The lc_call()
function will
grow as you add new lambda functions. You simply add more if…else
clauses. If you have many lambda
functions, the lc_call()
function will
become very long.
The lc_call()
function can be
converted to Node.js:
function
lc_call
(
lc
)
{
if
(
lc
[
'func'
]
==
'anon_increment'
)
{
lf_anon_increment
(
lc
);
}
return
lc
;
}
To simulate a lambda function call, the call object is created by
merging the function object with the arguments. The PHP array_merge()
function takes the $f
array variable, which simulates the lambda
variable and merges or adds an array that represents the function
arguments to be applied. Then, the lc_call()
function is called on the call object
to execute the call. The lc_call()
function finds the function to call and calls it. The target function, the
lf_anon_increment()
function in our
previous example, unpacks the arguments, increments the argument, and then
packs the return value in the ret
key.
The lf_anon_increment()
function
returns to lc_call()
, which returns the
call object to the caller. Since the call object is returned from the
lc_call()
function, the ret
key can be accessed inline to get the return
value. Here’s the “long” form of the code to invoke the simulated lambda
function:
$b
=
lc_call
(
array_merge
(
$f
,
array
(
'args'
=>
array
(
'a'
=>
4
))))[
'ret'
];
Although this PHP code makes it as straightforward as possible to
trace the code through the lc_call()
function call then through the lf_anon_increment()
function call and back
again, it is unnecessarily verbose in practice. A helper function, the
lf_call()
function, can shorten the
code for lambda functions to be turned into calls:
function
lf_call
(
$lf
,
$a
)
{
$lf
[
'args'
]
=
$a
;
return
lc_call
(
$lf
);
}
$b
=
lf_call
(
$f
,
array
(
'a'
=>
4
))[
'ret'
];
Instead of calling the array_merge()
PHP API, assigning the arguments
array to the args
key of the lambda
variable works just as well. In practice, it is harmless to reuse a lambda
function as a call object. lf_call()
is
much simpler than lc_call()
.
Of course, this PHP code can be converted to Node.js code easily:
function
lf_call
(
lf
,
a
)
{
lf
[
'args'
]
=
a
;
return
lc_call
(
lf
);
}
var
b
=
lf_call
(
f
,
array
(
'a'
=>
4
))[
'ret'
];
We now have everything needed to simulate the Node.js example
code. If you will recall, the Node.js code created an anonymous function,
assigned it to the f
lambda variable,
invoked the f
lambda function, and
assigned the return value to the b
variable:
var
f
=
function
(
a
)
{
// anonymous function
return
a
+
1
;
};
var
b
=
f
(
4
);
// b takes the value of 5
The four pieces of the simulated lambda call are: the lf_anon_increment()
function, the lc_call()
helper function, the lf_call()
convenience function, and the calling
code where the $f
lambda variable is
invoked. The following code puts all four pieces together for a complete
demonstration of how PHP 4 can support lambda (callback)
functions:
function
lf_anon_increment
(
$lc
)
{
// anonymous function
$a
=
$lc
[
'args'
][
'a'
];
$ret
=
$a
+
1
;
$lc
[
'ret'
]
=
$ret
;
};
function
lc_call
(
$lc
)
{
if
(
$lc
[
'func'
]
==
'anon_increment'
)
{
lf_anon_increment
(
$lc
);
}
return
$lc
;
}
function
lf_call
(
$lf
,
$a
)
{
$lf
[
'args'
]
=
$a
;
return
lc_call
(
$lf
);
}
$f
=
array
(
'func'
=>
'anon_increment'
);
$b
=
lf_call
(
$f
,
array
(
'a'
=>
4
))[
'ret'
];
This PHP 4 code can be converted to Node.js in a straightforward way:
function
lf_anon_increment
(
lc
)
{
// anonymous function
var
a
=
lc
[
'args'
][
'a'
];
var
ret
=
a
+
1
;
lc
[
'ret'
]
=
ret
;
};
function
lc_call
(
lc
)
{
if
(
lc
[
'func'
]
==
'anon_increment'
)
{
lf_anon_increment
(
lc
);
}
return
lc
;
}
function
lf_call
(
lf
,
a
)
{
lf
[
'args'
]
=
a
;
return
lc_call
(
lf
);
}
var
f
=
{
'func'
:
'anon_increment'
};
var
b
=
lf_call
(
f
,
{
'a'
:
4
})[
'ret'
];
This example shows definitively that Node.js anonymous functions and lambda variables can work in PHP 4 code and be converted to Node.js code. Now, let us turn our attention to the real-world example presented at the start of this chapter. In PHP, writing multiple files was straightforward:
for
(
$i
=
0
;
$i
<
$n
;
++
$i
)
{
$fp
=
fopen
(
'fp.txt'
+
$i
,
'w'
);
fwrite
(
$fp
,
$data
);
fclose
(
$fp
);
}
Converting this PHP code to Node.js was problematic. The code was nonlinear and could not be made linear. As a result, the Node.js code had to be completely rewritten with a callback chain algorithm:
var
f
=
function
()
{
};
for
(
var
i
=
n
-
1
;
i
>=
0
;
--
i
)
{
f
=
function
(
g
,
j
)
{
return
function
()
{
fs
.
open
(
'fp.txt'
+
j
,
'w'
,
0666
,
function
(
error
,
fp
)
{
fs
.
write
(
fp
,
data
,
null
,
'utf-8'
,
function
()
{
fs
.
close
(
fp
,
function
(
error
)
{
g
();
}
});
});
};
}
(
f
,
i
);
}
f
();
To effectively maintain and improve the PHP and Node.js code
simultaneously, it was unacceptable that the two codebases be so
dramatically different. Since the PHP algorithm, a simple
for
statement, could not be converted into Node.js
code, the Node.js callback chain algorithm was converted to PHP 5.3
code:
$f
=
function
()
{
};
for
(
$i
=
$n
-
1
;
$i
>=
0
;
--
$n
)
{
$f
=
function
()
use
(
$f
,
$i
)
{
$g
=
$f
;
$j
=
$i
;
$fp
=
fopen
(
'fp.txt'
+
$j
,
'w'
);
fwrite
(
$fp
,
$data
);
fclose
(
$fp
);
$g
();
}
}
$f
();
To convert this PHP 5.3 code to PHP 4, the first step is to
identify and implement the anonymous functions. At the top of the code,
there is a “do nothing” anonymous function; a good name for this function
is lf_anon_noop()
because it is a “no
operation” function or “noop” for short. Inside the for
statement, there is a “write file” anonymous
function; a good name for this function is lf_anon_writefile()
. Here’s the code for these
functions:
function
lf_anon_noop
(
$lc
)
{
}
function
lf_anon_writefile
(
$lc
)
{
$j
=
$lc
[
'args'
][
'j'
];
$g
=
$lc
[
'args'
][
'g'
];
$fp
=
fopen
(
'fp.txt'
+
$j
,
'w'
);
fwrite
(
$fp
,
$data
);
fclose
(
$fp
);
lc_call
(
$g
);
}
In the PHP 4 if_anon_writefile()
function, the arguments are
unpacked from the lambda call object. After that, the file is written; the
PHP 4 code for that is unsurprising. Finally, the $g
lambda variable is invoked to execute the
next step in the callback chain.
These PHP 4 anonymous functions can be converted to Node.js:
function
lf_anon_noop
(
lc
)
{
}
function
lf_anon_writefile
(
lc
)
{
var
j
=
lc
[
'args'
][
'j'
];
var
g
=
lc
[
'args'
][
'g'
];
fs
.
open
(
'fp.txt'
+
j
,
'w'
,
0666
,
function
(
error
,
fp
)
{
fs
.
write
(
fp
,
data
,
null
,
'utf-8'
,
function
()
{
fs
.
close
(
fp
,
function
(
error
)
{
lc_call
(
g
);
}
});
});
}
Notice how the implementation of the lf_anon_writefile()
function in Node.js can have
the linearity-based conversion rules from Chapter 3 applied independently of the outer context
of the function. A callback chain algorithm works because it forces the
nonblocking Node.js API calls into an anonymous function that prevents
those APIs from affecting the surrounding code. The anonymous function
serves as a “prison” for the nonlinear code, breaking it apart from the
for
statement that causes it to be
nonlinear. Since it is effectively no longer in a for
statement, the for
statement becomes linear.
The second step to make the PHP 4 code work is to implement the
lc_call()
and lf_call()
functions. The lc_call()
function is unsurprising; it is
modified from the example to execute the lf_anon_noop()
and lf_anon_writefile()
functions instead of the
lf_anon_increment()
function from the
previous example. The lf_call()
function is unchanged. It is completely independent of the specifics of
the rest of the PHP 4 code:
function
lc_call
(
$lc
)
{
if
(
$lc
[
'func'
]
==
'anon_noop'
)
{
lf_anon_noop
(
$lc
);
}
else
if
(
$lc
[
'func'
]
==
'anon_writefile'
)
{
lf_anon_writefile
(
$lc
);
}
return
$lc
;
}
function
lf_call
(
$lf
,
$a
)
{
$lf
[
'args'
]
=
$a
;
return
lc_call
(
$lf
);
}
The PHP 4 lf_anon_noop()
and
lf_anon_writefile()
functions are
converted to Node.js with very few changes:
function
lc_call
(
lc
)
{
if
(
lc
[
'func'
]
==
'anon_noop'
)
{
lf_anon_noop
(
lc
);
}
else
if
(
lc
[
'func'
]
==
'anon_writefile'
)
{
lf_anon_writefile
(
lc
);
}
return
lc
;
}
function
lf_call
(
lf
,
a
)
{
lf
[
'args'
]
=
a
;
return
lc_call
(
lf
);
}
The third and final step is to convert the PHP 5.3 calling code into PHP 4. To more clearly see the PHP 5.3 calling code, the implementations of the anonymous functions have been removed and replaced with comments here:
$f
=
function
()
{
// the lf_anon_noop() anonymous function
};
for
(
$i
=
$n
-
1
;
$i
>=
0
;
--
$n
)
{
$f
=
function
()
use
(
$f
,
$i
)
{
// the lf_anon_writefile() anonymous function
}
}
$f
();
This PHP 5.3 calling code will be translated line by line to PHP 4 and Node.js.
The following PHP 5.3 code assigns a “noop” (no operation) lambda
function to the $f
lambda
variable:
$f
=
function
()
{
// the lf_anon_noop() anonymous function
};
In PHP 4, the $f
lambda
variable becomes an array variable that points to the lf_anon_noop()
simulated anonymous
function:
$f
=
array
(
'func'
=>
'anon_noop'
);
Next, in PHP 5.3, the for
statement builds the
callback chain by repeatedly redefining the $f
lambda variable in terms of its initial value
or its value from the last time through the loop. The use
keyword is used in such a way to defeat
closure for the $f
and $i
variables. The $f
lambda variable is used inside the anonymous
function (i.e., lf_anon_writefile()
) as
the next link in the chain:
for
(
$i
=
$n
-
1
;
$i
>=
0
;
--
$n
)
{
$f
=
function
()
use
(
$f
,
$i
)
{
// the lf_anon_writefile() anonymous function
}
}
In PHP 4, the for
statement
remains the same. The inside of the for
statement is very interesting, though:
for
(
$i
=
$n
-
1
;
$i
>=
0
;
--
$n
)
{
$f
=
array
(
'func'
=>
'anon_writefile'
,
'args'
=>
array
(
'j'
=>
$i
,
'g'
=>
$f
));
}
The $f
lambda variable is
redefined as a call object that passes the current value of the $i
index variable and the value of the $f
lambda variable from the previous time
through the loop as arguments. But in the PHP 5.3 code, these variables
are not passed as arguments; they are passed to the use
keyword in such as a manner as to defeat
closure. Closure and arguments are two different things. What happened to
closure?
With Node.js and PHP 5.3 lambda variables, only a function can be stored in a variable, not any predefined arguments. Arguments can only be applied when the lambda variable is invoked, not at the time of definition. In PHP 4, though, lambda variables are simulated using an array. Since lambda variables are just arrays, arguments can be added at the time of definition or at the time of invocation or any other time. In some sense, simulated lambda variables are more powerful than real lambda variables because they can add their arguments at any time, not just at the time of invocation.
In light of this idea, closure can be seen as a way to add arguments at the time of definition. When using closure, arguments are added to the lambda function by reference. When defeating closure, arguments are added by value. Simulating a lambda variable using an array and manipulating the arguments at definition time is a drop-in replacement for defeating closure.
To simulate the PHP 5.3 use clause in PHP 4, a call object is
assigned to the $f
lambda variable with
the use
variables passed as arguments.
It is that simple:
$f
=
array
(
'func'
=>
'anon_writefile'
,
'args'
=>
array
(
'j'
=>
$i
,
'g'
=>
$f
));
Once the callback chain has been created, it needs to be
triggered. A PHP 4 lc_call()
call
starts the callback chain:
lc_call
(
$f
);
Put together, here’s the PHP 4 calling code:
$f
=
array
(
'func'
=>
'anon_noop'
);
for
(
$i
=
$n
-
1
;
$i
>=
0
;
--
$n
)
{
$f
=
array
(
'func'
=>
'anon_writefile'
,
'args'
=>
array
(
'j'
=>
$i
,
'g'
=>
$f
));
}
lc_call
(
$f
);
The Node.js version is similar:
f
=
{
func
':'
anon_noop
'};
for (var i=n-1; i >= 0; --n) {
f = {'
func
':'
anon_writefile
', '
args
':{'
j
':i, '
g
'
:
f
}};
}
lc_call
(
f
);
Although using arrays directly will work, some convenience functions would be nice for PHP 4 production code.
A PHP 4 lf_create()
convenience function is useful for creating lambda functions instead of
direct array definition:
function
lf_create
(
$name
)
{
return
array
(
'func'
=>
$name
);
}
Here’s the Node.js version of the lf_create()
function:
function
lf_create
(
name
)
{
return
{
'func'
:
name
};
}
A PHP 4 lc_create()
convenience function is useful for creating call objects:
function
lc_create
(
$name
,
$args
)
{
return
array
(
'func'
=>
$name
,
'args'
=>
$args
)
}
The lc_create()
function is
easily converted to Node.js:
function
lc_create
(
name
,
args
)
{
return
{
'func'
:
name
,
'args'
:
args
};
}
With the three PHP 5.3 steps converted to PHP 4, plus the two convenience functions, the PHP 4 code can be put together.
Here’s the PHP 5.3 version:
$f
=
function
()
{
};
for
(
$i
=
$n
-
1
;
$i
>=
0
;
--
$n
)
{
$f
=
function
()
use
(
$f
,
$i
)
{
$g
=
$f
;
$j
=
$i
;
$fp
=
fopen
(
'fp.txt'
+
$j
,
'w'
);
fwrite
(
$fp
,
$data
);
fclose
(
$fp
);
$g
();
}
}
$f
();
Here’s the PHP 4 version, which also works on PHP 5.3:
function
lf_anon_noop
(
$lc
)
{
}
function
lf_anon_writefile
(
$lc
)
{
$j
=
$lc
[
'args'
][
'j'
];
$g
=
$lc
[
'args'
][
'g'
];
$fp
=
fopen
(
'fp.txt'
+
$j
,
'w'
);
fwrite
(
$fp
,
$data
);
fclose
(
$fp
);
lc_call
(
$g
);
}
function
lf_create
(
$name
)
{
return
array
(
'func'
=>
$name
);
}
function
lf_call
(
$lf
,
$a
)
{
$lf
[
'args'
]
=
$a
;
return
lc_call
(
$lf
);
}
function
lc_create
(
$name
,
$args
)
{
return
array
(
'func'
=>
$name
,
'args'
=>
$args
)
}
function
lc_call
(
$lc
)
{
if
(
$lc
[
'func'
]
==
'anon_noop'
)
{
lf_anon_noop
(
$lc
);
}
else
if
(
$lc
[
'func'
]
==
'anon_writefile'
)
{
lf_anon_writefile
(
$lc
);
}
return
$lc
;
}
$f
=
lf_create
(
'anon_noop'
);
for
(
$i
=
$n
-
1
;
$i
>=
0
;
--
$n
)
{
$f
=
lc_create
(
'anon_writefile'
,
array
(
'j'
=>
$i
,
'g'
=>
$f
));
}
lc_call
(
$f
);
Here’s the PHP 4 version converted to Node.js:
function
lf_anon_noop
(
lc
)
{
}
function
lf_anon_writefile
(
lc
)
{
var
j
=
lc
[
'args'
][
'j'
];
var
g
=
lc
[
'args'
][
'g'
];
fs
.
open
(
'fp.txt'
+
j
,
'w'
,
0666
,
function
(
error
,
fp
)
{
fs
.
write
(
fp
,
data
,
null
,
'utf-8'
,
function
()
{
fs
.
close
(
fp
,
function
(
error
)
{
lc_call
(
g
);
}
});
});
}
function
lf_create
(
name
)
{
return
{
'func'
:
name
};
}
function
lf_call
(
lf
,
a
)
{
lf
[
'args'
]
=
a
;
return
lc_call
(
lf
);
}
function
lc_create
(
name
,
args
)
{
return
{
'func'
:
name
,
'args'
:
args
};
}
function
lc_call
(
lc
)
{
if
(
lc
[
'func'
]
==
'anon_noop'
)
{
lf_anon_noop
(
lc
);
}
else
if
(
lc
[
'func'
]
==
'anon_writefile'
)
{
lf_anon_writefile
(
lc
);
}
return
lc
;
}
var
f
=
lf_create
(
'anon_noop'
);
for
(
var
i
=
n
-
1
;
i
>=
0
;
--
n
)
{
f
=
lc_create
(
'anon_writefile'
,
{
'j'
:
i
,
'g'
:
f
});
}
lc_call
(
f
);
The simulated anonymous functions and lambda variables are compatible with everything: PHP 4, PHP 5.3, and Node.js. These simulation techniques allow arbitrary PHP 4 code to be refactored such that it can be converted to Node.js and both the PHP and Node.js codebases kept closely synchronized while both are improved and new features added.
Some optional improvements are worth mentioning.
Although implementing simulated anonymous functions as standalone functions works fine, there may be cases when anonymous functions either need to be attached to existing classes or a class full of anonymous functions is useful for organizational reasons.
Adding an obj
key to the
lambda variable allows simulated anonymous functions to be added to
classes. The lfo_create()
and lco_create()
convenience functions, where
the “o” stands for “object,” are object-based
versions of the lf_create()
and
lc
_create()
convenience functions. When these alternative functions are used,
the lc_call()
function can invoke simulated anonymous functions that are attached to
objects. Here’s the PHP 4 code:
function
lfo_create
(
$obj
,
$name
)
{
return
array
(
'obj'
=>
$obj
,
'func'
=>
$name
);
}
function
lco_create
(
$obj
,
$name
,
$args
)
{
return
array
(
'obj'
=>
$obj
,
'func'
=>
$name
,
'args'
=>
$args
);
}
function
lc_call
(
$lc
)
{
if
(
$lc
[
'func'
]
==
'anon_noop'
)
{
$lc
[
'obj'
]
->
lf_anon_noop
(
$lc
);
}
else
if
(
$lc
[
'func'
]
==
'anon_writefile'
)
{
$lc
[
'obj'
]
->
lf_anon_writefile
(
$lc
);
}
return
$lc
;
}
These PHP 4 functions can be converted to Node.js:
function
lfo_create
(
obj
,
name
)
{
return
{
'obj'
:
obj
,
'func'
:
name
};
}
function
lco_create
(
obj
,
name
,
args
)
{
return
{
'obj'
:
obj
,
'func'
:
name
,
'args'
:
args
};
}
function
lc_call
(
lc
)
{
if
(
lc
[
'func'
]
==
'anon_noop'
)
{
lc
[
'obj'
].
lf_anon_noop
(
$lc
);
}
else
if
(
lc
[
'func'
]
==
'anon_writefile'
)
{
lc
[
'obj'
].
lf_anon_writefile
(
lc
);
}
return
lc
;
}
Chapter 8 shows how to convert PHP classes to Node.js classes. The implementation and usefulness of adding simulated anonymous functions to classes will become more apparent in that chapter.
In the next chapter, the differences between sending an HTTP response in PHP and in Node.js will be addressed. As might be expected, HTTP responses in PHP are sent using a blocking technique whereas HTTP responses in Node.js are sent using a nonblocking technique. The linearity-based conversion recipes from Chapter 3 and the simulation-based conversion recipes from this chapter will be the foundation for refactoring PHP HTTP response code to be easily converted to Node.js.
Get Node.js for PHP Developers now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.