Higher Order Functions in PHP
This article will look at how higher order functions work in PHP and how to use them.
What is a higher order function
The general definition of a higher order function is a function that either takes another function as input or returns another function as output (Source). There are several examples in the PHP standard library that are higher order functions like array_map
, array_filter
, array_reduce
etc.
Currying
One very simple example for a higher order function is currying. This is a technique that originated in functional programming languages like Haskel.
For this example let's suppose we have a function that multiplies two numbers like so:
function multiply(int $x, int $y): int {
return $x * $y;
}
But say we use this function very often to multiply a number by 2. So what we could do is define a new function that creates a function that multiply by 2 like this:
function doubler(): callable {
return function(int $x): int {
return multiply(2, $x);
};
}
The function call now looks like this:
$doublerFunction = doubler();
$doublerFunction(20);
This way we implicitly call multiply but now with one argument being preset to 2. Of course this is a very simple example and hardly useful. So let's look at something more interesting.
Use cases
The first use case I'd like to bring up is Importing or reading from a file. When working with large data you might also want to consider a Generator but that is a topic for a different post.
The file we are reading is CSV (Comma Separated Values) file with headers. So for a file that looks like this:
Name | Age |
---|---|
Peter | 20 |
Paul | 30 |
Marry | 40 |
We want to return an array like this:
[
0 => ['Peter' => '20'],
1 => ['Paul' => '30'],
2 => ['Marry' => '40'],
]
Assuming the file is properly formatted this is a possible solution:
declare(strict_types=1);
class CSVReader {
public function read(string $fileName): array {
// Read the raw content from the file into an array of strings
$rawFileContent = file($fileName, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// Parse every line as a csv string
$csvFileContent = array_map('str_getcsv', $rawFileContent);
// Remove the first line (as this is the header row)
$header = array_shift($csvFileContent);
// Iterate over the remaining rows with the combine function to use the header as keys and the row as values
return array_map(function (array $row) use ($header): array {
return array_combine($header, $row);
}, $csvFileContent);
}
}
Yes, this works. But this is very inefficient. The array of lines needs to be traversed twice. Once for the CSV parsing and once for the array parsing.
In this piece of code you might also have noticed that you can invoke functions from a string like 'str_getcsv'
. This is only required for global functions. For functions on objects you can do $object->funcName
likewise for static methods SomeClass::staticMethod
.
Scopes and other issues
PHP doesn't have the concept of block scopes like other languages. But it has a function scope. You might have noticed that use ($header)
in the code. This means when calling the function use the $header
variable from outside. Now you can read the content of the variable. If you want to modify it you need to reference it with &$header
if header is a primitive datatype.
Another thing that PHP doesn't do is type checking of those function arguments. So for example the array $a = [1, 2, true, 3]
can be mapped with array_map(function (int $i) { return $i + 1; }, $a);
without any complaints. (true + 1 is 2 even with strict types)
Last words
Functional programming in PHP is possible but without a fancy syntax for those inline functions like the Arrow Notation in Javascript and a proper type checking, this is not always a good solution. Sometimes those functions are even slower than their loop equivalent. However if properly implemented like in the doctrine https://github.com/doctrine/collections/blob/master/lib/Doctrine/Common/Collections/ArrayCollection.php#L305
module those can be quite fun to work with.Hello