Let’s add Lazy Evaluation to PHP?

I’ve been learning a bit about Haskell recently and it has me pining for lazy evaluation in PHP without all the effort of doing it manually.

As I envision it lazy evaluation in PHP would allow annotating a class property or even a variable with a typehint-like keyword lazy and then when that property or variable is assigned to the right-hand-side expression would not be evaluated until the property or variable are actually accessed later in the program.

In general a lazy property would be syntax sugar around variable and/or property assignment and then subsequent access. Let’s take a look at code we can run today in PHP 8.x that can implement lazy evaluation:

<?php

class OrderItem {
    public int $order_id;
    public string $sku;
    public int $quantity;
}
class OrderItems {
    static function get(int $order_id):array {
        static $times = 0;
        if ( 0 !== $times ) {
            $msg = 'ERROR: The second call to callAndSet() evaluated the closure again!!!'
            trigger_error($msg);
            $times = 1;
        }
        // Simulating a two item order
        $item = new OrderItem();
        $item->order_id = $order_id;
        $item->sku = 'ABC-123';
        $item->quantity = 12;
        $item2 = new OrderItem();
        $item2->order_id = $order_id;
        $item2->sku = 'XYZ-987';
        $item2->quantity = 5;
        return [$item,$item2];
    }
}
function callAndSet(mixed &$lazy) {
    return $lazy instanceof Closure
        ? $lazy = $lazy()
        : $lazy;
}
class Order {

    public int $order_id;
    public Closure|array $order_items;
    function __construct(int $order_id) {
        $this->order_id = $order_id;
        $this->order_items = fn() => OrderItems::get($order_id);
    }
}
$order = new Order(123);
foreach( callAndSet($order->order_items) as $order_item) {
    printf("%d of %s\n",
        $order_item->quantity,
        $order_item->sku);
}
echo "---\n";
foreach( callAndSet($order->order_items) as $order_item) {
    printf("%d of %s\n",
        $order_item->quantity,
        $order_item->sku);
}
echo "---\n";
foreach( $order->order_items as $order_item) {
    printf("%d of %s\n",
        $order_item->quantity,
        $order_item->sku);
}

In the preceding code we have the $order_items property in the Order class which gets a closure assigned it in the __construct() method: fn() => OrderItems::get($order_id).

Later in the foreach we call callAndSet($order->order_items) to evaluate the closure and update the parameter passed with the return value of the evaluated closure so that next time callAndSet() is called it will just return the value passed to it.

This example runs the foreach loop three times: 1.) to show that we do indeed evaluates the closure, 2.) to show that we did not reevaluate the closure (see the trigger_error() in OrderItems::get()), and 3.) to show that we did indeed assign the array returned by the closure to $order->order_items.

To confirm the above code actually works as promised click here to see in action on 3v4l.org.

What would it look like with lazy loading? it would be simple. Below I am only showing the code that would change from above example (note the lazy keyword on the 3rd line.)

class Order {
    public int $order_id;
    public lazy array $order_items;
    function __construct(int $order_id) {
        $this->order_id = $order_id;
        $this->order_items = OrderItems::get($order_id);
    }
}
$order = new Order(123);
foreach( $order->order_items as $order_item) {
    printf("%d of %s\n",
        $order_item->quantity,
        $order_item->sku);
}
echo "---\n";
foreach( $order->order_items as $order_item) {
    printf("%d of %s\n",
        $order_item->quantity,
        $order_item->sku);
}

This time when we access $order->order_items in the first foreach it evaluates the closure to get the list of order items, and the second time the property already has the value.

I would give you a link to 3v4l.org showing this new code running, but, well, it is not a feature of PHP yet. Of course.

In addition, if we had this feature in PHP we’d probably also need a way to access the closure and not trigger the evaluation, so we would probably need a function like get_lazy_closure($lazy_var_or_property).

So what do you think? Should PHP add lazy evaluation like this? Would it even be possible given the internals of PHP?

So what do you think? Should PHP add lazy evaluation like this? Would it even be possible given the internals of PHP?

Cover from Unsplash by photographer Graham Holtshausen.

Leave a Reply

Your email address will not be published. Required fields are marked *