There are some quite important functions that are being very commonly used to transform data, even across languages, that modern approaches to solving problems greatly prefer. Many things in theory could fit such definition, but right now I am talking about map, reduce or even filter functions, all of which are being increasingly preferred to plain while, for and foreach loops, wherever applicable. Of course Laravel offers it's flavor of these functions that work on data in Collections. I will not detail on how to use them as the official documentation for filter, map, and reduce respectively is detailed enough. Instead I want to focus here on a small bit that is omitted in the docs. Using keys with reduce.
Reduce
From the official docs mentioned above, the example for reduce
looks like
this:
<?php
$collection = collect([1, 2, 3]);
$total = $collection->reduce(function ($carry, $item) {
return $carry + $item;
});
// 6
And in fact, doing a sum of values is one of the most used example for reduce usage there is. Basically the book example. You will probably find similar examples for other languages too.
Reduce with an arrow function
For the sake of improvement, let's rewrite the above using arrow function, a feature that had been added into PHP as a part of anonymous functions. They are available in javascript as well and I love using them there, so let's try:
<?php
$collection = collect([1, 2, 3]);
$total = $collection->reduce(fn ($carry, $item) =>
$carry + $item;
);
// 6
Saves a few keystrokes, too. By now, it should be clear even to the young
Padawans that the reduce
function for it's first argument accepts a
callback function, that has two arguments, a $carry
and the actual
$item
being iterated, many times referred to as a value. If we really
just want to do a sum of values, this is all we need. What about situations
where it is not enough?
Reduce with keys
Imagine we have a Collection of cities and we want to use reduce to
calculate the total distance between them using reduce
alone. This is
little bit tricky because the distance is a relation between two cities so
we have to have away to access it within a callback. Not being able to find
reliably the documentation for this, I decided to quickly write it down, so
here it is:
<?php
$cities = City::all();
$total = $cities->reduce(function ($carry, $city, $key) use ($cities) {
$next = $key + 1;
if (isset($cities[$next]) {
$carry += $city->distanceTo($cities[$next]);
}
return $carry;
});
The most important bit here is that the callback can actually have more
than two parameters, the third one is being the $key
. We also have to
make $cities
available in the local scope via use
keyword and need to
check if the end of the array was not reached beforehand.
Show me them arrows
Arrow functions in javascript can have many statements. In PHP, only a single assignment per arrow function is permitted. Rewriting the above with an arrow function is trickier, but possible.
<?php
$cities = City::all();
$total = $cities->reduce(fn ($carry, $city, $key) =>
isset($cities[$key + 1])
? $carry += $city->distanceTo($cities[$key + 1])
: $carry
);
A ternary operator is used here. It is up to the reader to judge if this is
an improvement or a hit to the readability. Also, there seems to exist too
many ways people prefer to see the above code formatted, so it might even
look scary or ugly to some. With arrow function however, outside variables
are available in the local scope. Thus $cities
are available without the
need for the use
keyword.
Better to split up
Using keys with reduce
function, a part of Laravel Collections can be
useful in some situations. Documentation does not explicitly mention the
third parameter for the callback function and since, as demonstrated above,
the code that makes use of it is not that elegant, maybe it is omitted for
a good reason.
Is there another way? Well, as with anything programming related, the
answer is yes. The $key
is actually just the second attribute to the
map
function, and mentioned in the docs, go check it. In many situations
using map
with the keys in the similar fashion as above is better, as it
would enable us running reduce
on the mapped values, for example
distances. It requires two functions instead of single concise one, but the
resulting code might be more explicit. See below:
<?php
$cities = City::all();
$distances = $cities->map(fn ($city, $key) =>
isset($cities[$key + 1])
? $city->distanceTo($cities[$key + 1])
: 0
);
$total = $distances->reduce(fn ($carry, $distance) =>
$carry += $distance
);
This is how I like to write it with my current style. What would be your preferred way?