F...ancy PHP: higher-order functions


April 15, 2018

This series is a challenge: what features of the more well-regarded languages does PHP already have? What others can it emulate?

Since I've been thinking of functional programming a lot recently, let's tackle higher-order functions first.

First-class functions

We say that a language has first-class functions if you can perform the following operations:

  • assign a function to a variable
  • pass a function as an argument to another function
  • return a function from another function.

That is, first-class functions mean you can treat a function like any other kind of value.

PHP already has first-class functions. Observe:

$addOne = function ($x) { return $x + 1; };

$multiplyByTwo = function ($f, $x) { return $f($x) * 2; };

array_map($addOne, [1, 2, 3]);

Implementation details

Well, it kiiiinda has first-class functions. If you squint.

php > echo gettype($addOne);
object
php > echo get_class($addOne);
Closure
php > o_Õ;
PHP Notice:  Use of undefined constant o_Õ - assumed 'o_Õ' in php shell code on line 1

So yeah. Anonymous functions are actually objects! Because of course.

Historical notes

PHP 5.3 introduced anonymous function/closure syntax you can see in the example above. PHP 5.4 added methods to the class, which we'll come back to in future.

However, the first version of PHP with first-class functions was actually PHP 4. It added function create_function - a more constrained version of eval (oy). Nobody actually used it since it leaked memory quite badly, and it's been deprecated in PHP 7.2. Fuggetaboutit.

Originally, the term "first-class functions" comes from "functions as first-class citizens". (Much Animal Farm 🐕)

Higher-order functions

If a function takes another function as an argument, and/or returns a function, it's a higher-order function. Every other function is a first-order function. As you can see:

// higher-order function $personaliser

$personaliser = function ($template) {
    return function ($name) use ($template) {
        return str_replace(':name', $name, $template);
    };
};

// first-order function $greeter

$greeter = $personaliser('Hello, :name!');

// higher-order function array_map

$greetings = array_map($greeter, ['Alice', 'Bob', 'World']);

Built-in higher-order functions

In PHP standard library there already exist a number of useful functions that take other functions as arguments, among others:

  • array_map
  • array_reduce
  • array_filter
  • call_user_func (and its variant, call_user_func_array)

(Note that I purposefully skipped functions like usort and array_walk which modify their argument instead of returning its copy. Hint, hint.)

Type declarations

Brill. As it happens I'm a fan of applying static analysis to PHP code, so how do I indicate a function argument/return using type declarations?

Turns out all "things that can be called like a function" are typehinted callable in PHP. There are more ways to generate a callable than you would expect, some of which exist for historical reasons:

// function name, as string
is_callable('array_map');

// anonymous function
$f = function ($x) { return $x + 1; };
is_callable($f);

// array containing an object and its method name, as string
$rf = new ReflectionFunction($f);
is_callable([$rf, 'isClosure']);

// instances of classes with __invoke() magic method
class C { function __invoke() { return 0; } };
$c = new C;
is_callable($c);

// ...and you can also convert them all into Closure objects because why not
$cl = Closure::fromCallable($c);
is_callable($cl);

We could therefore rewrite the example $personaliser as:

$personaliser = function (string $template): callable {
    return function (string $name) use ($template): string {
        return str_replace(':name', $name, $template);
    };
};

"Recursive" type declarations

Q: I have an interface and one of the methods declares it will return a callable. How can I restrict it more and specify type declarations for arguments and returns of the callable?

A: The one time I wanted to do this, I hand-rolled extra checks using reflection (look at ReflectionFunctionAbstract class docs).

TLDR: you can't, PAH (PHP ain't Haskell) ¯\_(ツ)_/¯

Tags: hack php