Saturday 27 September 2014

PHP: significant oversight in the implementation of array_reduce() (and I find the PHP bugbase...)

G'day:
I was fishing around in Stack Overflow this morning for a quick PHP exercise to do before getting out of bed (there's some imagery for you), seeking coffee, and getting on with the day.

I found this one: "Explode my Array, remove dash from key, then Implode it back together". It already had an accepted answer which involved just a coupla foreach() loops, but I thought I could come up with something more descriptive using array_reduce(). And I thought I'd post it anyway as an alternative approach.

However I ran into a problem.

First, a CFML solution to demonstrate how it should work. The problem is that some array keys (in PHP there's just arrays: I'm using a CFML struct here, but that's just an associative array in PHP), have dashes in them, and these need to be changed to spaces:

// stackoverflow.cfm

li_1 = "suffix 1"
li_2 = "suffix 2"
temp_content = "content"
    
pages = {
    'Administrator' = {
        'network-administrator' = {
            'title' = 'Network ' & li_1,
            'description' = 'Network ' & li_1 & ' ' & temp_content,
            'post' = '<p>Network ' & li_1 & ' ' & temp_content & '.</p>'
        },
        'database administrator' = {
            'title' = 'Database ' & li_1,
            'description' = 'Database ' & li_1 & ' ' & temp_content,
            'post' = '<p>Database ' & li_1 & ' ' & temp_content & '.</p>'
        }
    },
    'Analyst' = {
        'business systems analyst' = {
            'title' = 'Business Systems ' & li_2,
            'description' = 'Business Systems ' & li_2 & ' ' & temp_content,
            'post' = '<p>Business Systems ' & li_2 & ' ' & temp_content & '.</p>'
        },
        'data-analyst' = {
            'title' = 'Data ' & li_2,
            'description' = 'Data ' & li_2 & ' ' & temp_content,
            'post' = '<p>Data ' & li_2 & ' ' & temp_content & '.</p>'
        }
    }
}

newPages = pages.map(function(key,value){
    return value.reduce(function(reduction, key, value){
        key = key.replace("-", " ", "all")
        reduction[key] = value
        return reduction
    }, {})
})

dump([pages, newPages])

So I just use a map() call to traverse each element of the outer struct, and for each of those I perform a reduction which actually returns the same array again, just with the keys "fixed".

This works fine:

Array
1
Struct
Administrator
Struct
database administratorvar.database administrator
network-administrator
Struct
Analystvar.Analyst
2
Struct
Administrator
Struct
database administratorvar.database administrator
network administrator
Struct
Analystvar.Analyst

(I've collapsed a lot of it to just demonstrate that the "network-administrator" has been fixed to "network administrator").

So I set about doing the same with PHP's array_map() and array_reduce(). But it's not possible. Because the only thing passed into array reduce is the reduction (ie: the result of the previous iteration) and the current element's value. But not its key. And there's no way to access the element's key via some other circuitous route. This would be almost understandable if a PHP array was simply an indexed array like in CFML, but it's not. The keys can be a simple numeric index, but they can also be anything else as well (they're ordered structs with default sequential numeric keys, basically). And so the key is kinda important info to pass to the iteration methods. This is, btw, not limited to array_reduce(): array_map() also doesn't expose it, and I presume the rest of them don't too.

So I googled about and found the PHP bugbase. And indeed I found a ticket covering this very shortfall: "array_reduce() callback should receive current key/index" (65872). I've voted for it, and I'll probably stick a comment on it too, to suggest following CFML's lead here.

Now here's a question for you. I'm not entirely convinced by my approach to using reduce() to actually just modify the struct, rather than really "reduce" it, per se. I know it works, but I dunno if this solution actually does fall within my intention of making the code clearer in intent than just using a coupla generic loops? What I really want is a remap() method (so like a map(), except I can specify new key names, which map() won't allow. Thoughts? Am I getting too picky? (Sean, I hope you're reading this, as I know you'll have an informed opinion on this ;-).

Righto. Coffee time.

--
Adam