Closure Binding as an alternative to “use” variables

As a general rule when creating a Closure, arguments are passed when the function is called, but “use” variables (I’m sure that they have a formal name, but have no idea what it might be, so I just refer to them as “use” variables because they’re passed to the Closure through a “use” clause) are fixed as the value that they already contain when the Closure is defined, and the variables themselves must already exist within that scope. This isn’t normally a problem where we’re defining the Anonymous function inline, because we can specify the values directly in the callback function itself:


$filteredArrayData = array_filter(
    $arrayData,
    function($value) { return $value->price >= 120.00 && $value->price < 250.00; }
);

Writing our callbacks like this has the big benefit of being easy to read and understand exactly what the callback is doing.

Of course, the drawback of this approach is that when we need to change the price minimum and maximum values for filtering, they’re hard-coded in the callback.
For those array functions that use callbacks, such as array_filter(), we can’t simply pass the values as extra parameters directly to the function; although we can define the price range values as variables that can then be passed to the callback function as “use” variables. Perhaps a more practical approach than hard-coding them if we get the minimum and maximum values for filtering from user input, or if they need to be calculated elsewhere in our code.


$minimumPrice = 120.00;
$maximumPrice = 250.00;

$filteredArrayData = array_filter(
    $arrayData,
    function($value) use ($minimumPrice, $maximumPrice) {
        return $value->price >= $minimumPrice && $value->price < $maximumPrice;
    }
);

But what if we want to assign that Closure to a variable, as a lambda function, so that we can use it in several places in our code? Generally, we would assign “use” variables for our minimum and maximum price values


$minimumPrice = 120.00;
$maximumPrice = 250.00;

$priceRange = function($value) use ($minimumPrice, $maximumPrice) {
    return $value->price >= $minimumPrice && $value->price < $maximumPrice;
}

$filteredArrayData = array_filter(
    $arrayData,
    $priceRange
);

When we create the Closure in this way, the variables have to be predefined, and are then set for the duration of the lambda’s existence; we can’t change them (easily), and the values aren’t directly evident in our call to array_filter(), so the code is less readable and intuitive.

One workround for this problem is to define our “use” variables “by reference” rather than “by value”. If we do that, then we don’t even need to define the “use” variables in advance, because creating the Closure will create them for us (with null values), and we can set the actual values that we want to use later in our script, closer in our code to the array_filter() call; but we still don’t have that immediate readability.


$priceRange = function($value) use (&$minimumPrice, &$maximumPrice) {
    return $value->price >= $minimumPrice && $value->price < $maximumPrice;
}

$minimumPrice = 120.00;
$maximumPrice = 250.00;

$filteredArrayData = array_filter(
    $arrayData,
    $priceRange
);

And this does give us scope problems, because we need to ensure that the same original variables that are created when we create the Closure remain in scope for us to change them when we need to use the callback. It would be easier to maintain that scope if it related to object properties, and could reference those properties using $this in our Closure. Normally using $this and/or self within a Closure would only be referencing the Closure object itself, but in a recent blog post, I spoke about binding Closures to objects. So let’s build ourselves an filter object, where we can store the maximum and minimum values for the price range as properties; and that we can use to create the Closure, bound to the object so that those additional filter arguments are in scope.


class PriceFilter {
    private $minimumValue;
    private $maximumValue;
    private $filter;

    protected function getFilter() {
        if ($this->filter === null) {
            $this->filter = function($value) {
                return $value->price >= $this->minimumValue &&
                    $value->price < $this->maximumValue;
            };
        }
        return $this->filter;
    }

    public function inRange($minimumValue, $maximumValue) {
        $this->minimumValue = $minimumValue;
        $this->maximumValue = $maximumValue;
        return $this->getFilter();
    }
}

$priceFilter = new PriceFilter();

$filteredArrayData = array_filter(
    $bookData,
    $priceFilter->inRange(5.00, 15.00)
);

We don’t even need to bind the Closure to the PriceFilter object, because a Closure is automatically bound to the object in which it is created.

Because the Closure is bound to the PriceFilter object, we can access the properties of that object directly within the Closure function using $this, without any need for “use” variables; and we set the values in the call to return and use the Closure.
And by giving our filter class and method appropriate and meaningful names, our callback is as easy to read and understand as the original Anonymous function that we used at the beginning of this article (if not more so), and our values are directly visible within the array_filter() call again; but we’ve also created a Closure that is flexible (it can accept arguments that have been calculated or derived from user input) and can be reused elsewhere in our code, passing the argument values whenever we need to call it.

Advertisements
This entry was posted in PHP and tagged , , , . Bookmark the permalink.

11 Responses to Closure Binding as an alternative to “use” variables

  1. Pingback: PHP Annotated Monthly – April 2017 | PhpStorm Blog

  2. Ryan Hayle says:

    Why not just use a static factory function instead? Something like this:

    public static function inRange($minimumValue, $maximumValue) {
    $filter = new static($minimumValue, $maximumValue);

    return $filter->getFilter();
    }

    Like

    • Mark Baker says:

      I could…. but I wanted to show that there was an alternative to “use variables” that are only set at creation time; the whole pint of the post is to show that additional arguments to a Closure can be passed at runtime

      Like

  3. Pingback: Closure Binding as an alternative to “use” variables - murze.be

  4. Michali Sarris says:

    I wouldn’t recommend doing it this way, because it can get confusing because of the side effects of inRange, which changes the context object instead of returning a new one. For example it will be counter intuitive when you use it like this:

    $between5and15 = $priceFilter->inRange(5.00, 15.00);
    $between15and25 = $priceFilter->inRange(15.00, 25.00);

    $filteredArrayData1 = array_filter($bookData, $between5and15);
    $filteredArrayData2 = array_filter($bookData, $between15and25);

    You would expect both filters to return different results, but after the 2nd call they’re both actually the same filter because of the side effects of inRange.

    A better way would be to create a new object, maybe by implementing __invoke or just a simple closure with a use(), but then we’re back at where we started.

    Like

    • Mark Baker says:

      It will always get confusing if dynamic values that are assigned to variables change, and we aren’t aware that they can change. That applies to objects, or to “by reference” assignments. It’s up to us as developers to be aware when we’re assigning something dynamic to a variable…. and yes, creating a new variable for everything is an alternative approach; but I wanted to demonstrate that Closures have an automatic binding, and that there are ways in which we can take advantage of that

      Like

  5. joshadell says:

    As an extension of Scott’s answer, you could also use an invokable object to create on-the-fly filters. This gives all the advantages of having a closure with dynamic properties, as well as allowing extension using OOP features like composition and inheritance. https://3v4l.org/iHpuS

    Like

    • Mark Baker says:

      __invoke definitely has benefits, especially combined with binding to the ValueFilter object, giving readability without having the entire Closure defined in inline code

      Like

  6. slifin says:

    http://www.phptherightway.com/pages/Functional-Programming.html

    Have you considered partial application in this context?

    Like

  7. Scott Aubrey says:

    Hi Mark,

    Closures offer enormous flexibility in PHP now, and although a little clunky in place – I genuinely think they are one of the better improvements in “modern” PHP – and keep getting better. Your posts on the more corner cases and uses are really helpful to the community – thank you!

    I would say, for your example of a closure for array_filter with reusable scope, I’d probably go more for the closure in closure option to create a scope, just because there’s less code to it, and it avoids “object call” like syntax at call site. Observe: https://3v4l.org/5Z1mY It would depend on the actually situation though. Sometimes classes offer more in way of “documentation” – that can be really handy when talking about reuse across different scopes.

    Thanks!

    – Scott

    Like

    • Mark Baker says:

      Either is usable; and I just wanted to remind people that there was an alternative to “use variables”. Choice of which approach to use will always depend on specific use cases, and of course it’s always possible to combine both “use variables” and bindings… of course, those bindings could also be methods in their own right, and using an object with bindings then would probably help readability

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s