- URL: https://www.laruence.com/en/2022/05/10/6192.html
- Please include attribution when republishing.
A while back I spent some time improving Yar's performance, but I also ran into a legacy design wart that had always bothered me: for parallel RPC calls, the current method prototype is:
public static Yar_Concurrent_Client::call(string $uri, string $method, ?array $arguments = NULL, ?callable $callback = NULL, ?callable $error_callback = NULL, ?array $options = NULL):null|int|bool {}
Doesn't your head hurt just looking at it?
Because in real usage the callback and error callback are very often empty — they can be specified globally in the loop when the calls are actually dispatched:
Yar_Concurrent_Client::loop(?callable $callback = NULL, ?callable $error_callback = NULL, ?array $options = NULL):?bool {}
And $options is often useful, varying per call. So in practice, a lot of parallel-call code ends up littered with NULL arguments, like:
Yar_Concurrent_Clinet::call("https://xxx.com/api", "method", array("arguments"), NULL, NULL, array(YAR_OPT_HEADER=>array("header:val1"));
Yar_Concurrent_Clinet::call("https://xxx.com/api", "method", array("arguments"), NULL, NULL, array(YAR_OPT_HEADER=>array("header:val2"));
Yar_Concurrent_Clinet::call("https://xxx.com/api", "method", array("arguments"), NULL, NULL, array(YAR_OPT_HEADER=>array("header:val2"));
Yar_Concurrent_Clinet::call("https://xxx.com/api", "method", array("arguments"), NULL, NULL, array(YAR_OPT_HEADER=>array("header:val4"));
So I'd long been wondering how to make calls like this more elegant. At one point I considered polymorphism, or adding a new API like:
public static Yar_Concurrent_Client::callArray(array $arguments):null|int|bool {}
But my OCD told me that would sow endless trouble. Then this morning I suddenly remembered an RFC I'd once seen; after digging around, I found it had been committed as far back as PHP 5.6. But I'm fairly old-school and don't track new features closely, so I'd never really used it — not sure whether you have. 🙂
That's the first feature to introduce today: argument unpacking.
Argument unpacking
We know PHP supports variadic functions. For example, given this function definition:
function variadic(...$arguments) {
var_dump($arguments);
}
Note the parameter definition uses three dots … (the ellipsis symbol), meaning however many arguments you pass when calling this function, they all get packed into an array named $arguments:
variadic();
//output: array(0) { }
variadic(NULL);
//output: array(1) { [0]=> NULL }
variadic("foo", "bar");
//output: array(2) { [0]=> string(3) "foo" [1]=> string(3) "bar" }
variadic(NULL, array(), "dummy");
//output: array(3) { [0]=> NULL [1]=>[] [2]=> string(5) "dummy" }
Of course, that's not what we need today. This feature has a sibling form used at the call site, called argument unpacking:
For instance, for my problem above, suppose we define a function
function dummy($a, $b = NULL, $c = NULL, $d = NULL, $e = NULL) {
var_dump($a, $b, $c, $d, $e);
}
If most of the time parameters b, c and d are NULL but e needs to be passed, we can use argument unpacking to avoid all those NULL arguments in the code, like:
$arguments = array(
"First argument",
NULL, NULL, NULL,
"Fifth argument",
);
dummy(...$arguments);
//output:
// string(14) "First argument"
// NULL
// NULL
// NULL
// string(14) "Fifth argument"
Note I also used … at the call site: it means unpack the array after the … and pass its elements to the callee in order — the first element maps to the first parameter, the second to the second.
But note that position here is about fill order, not array index. That is:
$arguments = array(
4=> "First argument",
0=> "Fifth argument"
),
in this form, the entry with index 4 is still treated as the first parameter.
Realizing this, I suddenly saw that Yar needs nothing new at all — the opening example can become:
$arguments = array(
"https://xxx.com/api",
"method",
array("arguments"),
NULL, NULL,
"options" => array(YAR_OPT_HEADER => array("header:val1")
)
Yar_Concurrent_Clinet::call(...$arguments);
$arguments["options"][YAR_OPT_HADER] = ["header:val2"];
Yar_Concurrent_Clinet::call(...$arguments);
$arguments["options"][YAR_OPT_HADER] = ["header:val3"];
Yar_Concurrent_Clinet::call(...$arguments);
$arguments["options"][YAR_OPT_HADER] = ["header:val4"];
Yar_Concurrent_Clinet::call(...$arguments);
Think that's the end of it?
Consider the code above: there's still one issue — you have to build an intermediate array. For the OCD among us, that still feels a bit… well, something.
But we can also lean on another RFC introduced in PHP 8.0: named parameters.
Named Parameter
Since PHP 8.0, you may specify parameter names when passing arguments. For the same example function:
function dummy($a, $b = NULL, $c = NULL, $d = NULL, $e = NULL) {
var_dump($a, $b, $c, $d, $e);
}
We can now name the parameters we want to pass at the call site, for example:
dummy(a:"dummy", e:"foo"); //output: // string(5) "dummy" // NULL // NULL // NULL // string(3) "foo"
That is, I specified values for a and e; everything unspecified takes its default. You can even ignore the declaration order:
dummy(e:"foo", a:"dummy");
The output is the same.
With this, the opening code becomes:
Yar_Concurrent_Client::call("https://xxx.com/api", "method", arguments:array("arguments"), options:array(YAR_OPT_HEADER=>array("header:val1")));
Yar_Concurrent_Client::call("https://xxx.com/api", "method", arguments:array("arguments"), options:array(YAR_OPT_HEADER=>array("header:val2")));
Yar_Concurrent_Client::call("https://xxx.com/api", "method", arguments:array("arguments"), options:array(YAR_OPT_HEADER=>array("header:val3")));
Yar_Concurrent_Client::call("https://xxx.com/api", "method", arguments:array("arguments"), options:array(YAR_OPT_HEADER=>array("header:val4")));
Now you can pass whichever parameter you want, however you want, right at the call site.
The code is still a bit longer than with argument unpacking, but it removes the sea of NULLs without introducing any intermediate variable.
Problem solved perfectly — and no new API needed. :)
Be First to Comment