Saturday 1 November 2014

PHP & CFML: mapping and reducing

I'm trying my hardest to answer PHP questions on StackOverflow these days, but the PHP community is so large and my PHP knowledge is so neophytic that I am never an early-enough bird to catch the worm for the easy questions. Plus a lot of the "PHP" questions are actually asking framework specific questions, and I have no idea about Laravel, Zend etc.

Still: I found an interesting question today with a pedestrian answer, so I decided to offer my own answer (to the actual question as asked). The answer would have been easy in CFML, but it was a bit of a ball-ache in PHP.

The question is "Convert multidimensional array", and this is a paraphrase:

Given this array:

$awards = [
    "award_year" => ["1999-01", "2010-12"],
    "award_title_user" => ["2", "tt"],
    "award_description_user" => ["2", "ddd"]

Translate it into this array:

$remappedAwards = [[
    "award_year" => "2010-12",
    "award_title_user" => "tt",
    "award_description_user" => "ddd"
    "award_year" => "1999-01",
    "award_title_user" => "2",
    "award_description_user" => "2"

Try to use array_map()

Note that the initial array is indexed by category, and as a sub-array of values; the remapped version is indexed by the sub-array's elements' positions, and then each category's element is explicitly associated with the category. And the order of the elements is reversed. Basically the array is being turned inside out and upside down.

The person who had answered the question eschewed the notion of using array_map(), and just used nested foreach() loops. This works fine, and from a PHP perspective, is the correct answer.

However I wanted to look at the array_map() approach and see if I could do it. TBH - and I said this in my answer - array_map() is not really the tool for the job here. A mapping exercise reuses the same index values/keys / general structure, it simply replaces existing values with new ones. And this is not what we want here.

I contrived a way to do it though:

// array_map.php

$awards = [
    "award_year" => ["1999-01", "2010-12"],
    "award_title_user" => ["2", "tt"],
    "award_description_user" => ["2", "ddd"]

$remappedAwards = array_map(function($year, $title, $description){
    return [
        "award_year" => $year,
        "award_title_user" => $title,
        "award_description_user" => $description
}, $awards["award_year"], $awards["award_title_user"], $awards["award_description_user"]);

$remappedAwards = array_reverse($remappedAwards);

echo "<pre>";
echo "</pre>";

Here I'm leveraging a handy "feature" (really a mis-implementation IMO, but still: handy for my purposes here) of array_map(): one can pass it multiple arrays, and the current element of each array is passed to the callback as an argument. So I'm actually mapping the inner arrays, not the outer one.

This results in a correctly-structured array, but in the wrong order so I then need to reverse it.

Job done.

Why do I say the multi-array handling of array_map() is a mis-implementation? Because the function name is array_map(). Not array_merge_and_map() (or PHP would probably call it arr_mm() or something). It should do one job and do it well. Not two different jobs shoddily. This is a common thread I have encountered in PHP though. Oh well.

Another observation I made in my answer is that I feel this is more a job for a .reduce() sort of operation, not a .map() one. However it'd be awkward in PHP due to the implementation of array_reduce() being "incomplete" (or just half-arsed) in that it only passes the value into the callback, not the index/key as well: and we need the keys here too. It'd be a piece of piss in CFML, as this code demonstrates:

// reduce.cfm

awards = {
    award_year = ["1999-01", "2010-12"],
    award_title_user = ["2", "tt"],
    award_description_user = ["2", "ddd"]

remappedAwards = awards.reduce(function(reduction,key,values){
    var offset = values.len() + 1
        reduction[offset-index][key] = value
    return reduction

dump([awards, remappedAwards])

So I reduce() the outer array (well: struct in CFML), and in the callback I also iterate over each() element of the inner array too. Then I populate the "reduction" result back-to-front to implement the reordering.

This got me wondering... exactly how much of a mess would the equivalent code look in PHP? For one thing, I'd need to pass an iterator separately to the callback so I can fish the keys out whilst I reduce()... and I'm using closure here, and PHP's implementation of closure is a bit shit in that one has to manually specify which variables to enclose, and would add even more clutter. But I managed it:

// reduce.php

// [$awards initialisation omitted for brevity]

$awardsIterator = new ArrayIterator($awards);
$remappedAwards = array_reduce($awards, function($reduction, $current) use ($awardsIterator){
    $key = $awardsIterator->key();
    $offset = count($current) - 1;

    array_walk($current, function($value, $index) use (&$reduction, $offset, $key){
        $reduction[$offset-$index][$key] = $value;
    }, true);
    return $reduction;
}, [[],[]]);

Note I have to pass in the array structure as the starting value for the reduction. I got tripped over by the fact that if one specifies by hand the index value of a PHP array when populating it, then the elements are populated in chronological order, not numeric order. Here I'm populating index 1 before I populate index 0, which means I ended up with an array like this:

    [1] = stuff
    [0] = stuff

Obviously the intent is this:

    [0] = stuff
    [1] = stuff

I find PHP's array handling mind boggling at times (those times being "all of them", TBH).

However if I give it its initial structure, PHP managed to work out that [0] is the first element, and [1] is the second one.

I think we can all agree that just using nested foreach() loops here is just better. Pity. PHP kinda let itself down here. It was an interest exercise, nevertheless.

I'm pleased to see CFML being made to look good though. Even if using PHP as the benchmark is damning with faint praise indeed.