from a functional programmer's perspective
I'll go thru a couple iterations of your code and explain the steps I'm taking as I go. Most of my decisions are born out of the need to always be reducing complexity. For example, when you use array_walk
, do you have to think about how to iterate thru the array? Do you think about array indexes or making sure you increment after each iteration? No. array_walk
is powerful because it hides those nasty details away from the programmer – however that stuff happens is not for us to worry about.
So why do you smash your brain with worries like this?
ucwords( str_replace( array( "_", "-" ), " ", $key ) )
That is a function of its own, let's call it humanIdentifier
. It takes some programmatic $key
value and returns a nice, human-friendly string
function humanIdentifier ($x) {
return ucwords(str_replace('-', ' ', $x));
}
With this one tiny change, we will have already simplified your code a lot – the complexity goes down because we no longer have to worry about how to make the key-to-human-readable-string conversion. Those yucky details have been abstracted away.
// things are improving ...
array_walk( $keys, function( &$val, $key ){ $val = humanIdentifier($val); } );
This is the approach I'm going to take as I continue to work on other parts of this answer
array_map
sucks
many of the functional residents (eg) array_reduce
, array_map
, array_filter
, array_walk
are a disaster in PHP. The interfaces are horribly inconsistent and the behaviours are sometimes just downright wonky. As you and others have identified, array_map
does not give us a way to access the key, but there's nothing stopping you from making a generic function that makes it accessible.
function array_kmap (callable $f, iterable $xs) {
return array_reduce(array_keys($xs), function ($acc, $k) use ($xs, $f) {
return array_merge($acc, [$k => call_user_func($f, $k, $xs[$k], $xs)]);
}, []);
}
Using this combined with your humanIdentifier function, we could come up with a solution pretty easily
$a = [
['one' => 1, 'two-two' => 2, 'four' => 4],
['two-two' => 22, 'three' => 33, 'four' => 44]
];
$b = array_reduce($a, function ($acc, $x) {
return array_merge($acc, array_kmap(function ($k, $v) {
// we don't actually use `$v`, so we can ignore it
return humanIdentifier($k);
}, $x));
}, []);
print_r($b);
// Array
// (
// [one] => One
// [two-two] => Two Two
// [four] => Four
// [three] => Three
// )
hidden complexity
There's still some complexity hiding within the transformation of $a
to $b
. Can you spot it? array_reduce
is pretty much the grandfather of most functionals intended for use with iterables – it's immensely powerful, but it has to attribute that power to its extremely generic interface. Our use of array_reduce
is pretty much a wrapper around array_merge
and our mapping function, $f
. We can derive a new function that does just this that acts as a sort of specialized array_reduce
- most functional languages call this flat map.
function array_flatmap (callable $f, iterable $xs) {
return array_reduce(array_map($f, $xs), 'array_merge', []);
}
$a = [
['one' => 1, 'two-two' => 2, 'four' => 4],
['two-two' => 22, 'three' => 33, 'four' => 44]
];
$b = array_flatmap(function ($x) {
return array_kmap(function ($k) {
return humanIdentifier($k);
}, $x);
}, $a);
print_r($b);
// Array
// (
// [one] => One
// [two-two] => Two Two
// [four] => Four
// [three] => Three
// )
eta conversion
What? Eta conversion comes from lambda calculus and says that
function ($x) { return $f($x); } === $f
(function ($x) { return $f($x); })($y) === $f($y)
$f($y) === $f($y)
$f === $f
I mention this because we can eta-convert some of the code to reduce even more complexity. There are two eta conversions that could help our program. Do you see where?
array_kmap(function ($k) {
return humanIdentifier($k);
}, $x)
This dangling $k
can easily be removed – here is simplified but equivalent code (Note: this is possible because we're discarding the $v
value from your callable – only the key is necessary to compute our transformation)
array_kmap('humanIdentifier', $x)
Now, if we zoom out a little bit, we see this!
function ($x) {
return array_kmap('humanIdentifier', $x);
}
Another little dangling $x
on the end of our array_kmap
function! If we were to partially apply our array_kmap
function, we could remove the $x
point which would get rid of function ($x) { ... }
altogether.
Of course PHP doesn't have any means of partially applying a function, so we have to make that
function partial (callable $f, ...$xs) {
return function (...$ys) use ($f, $xs) {
return call_user_func($f, ...$xs, ...$ys);
};
}
And now our resulting transformation is a thing of beauty
$a = [
['one' => 1, 'two-two' => 2, 'four' => 4],
['two-two' => 22, 'three' => 33, 'four' => 44]
];
$b = array_flatmap(partial('array_kmap', 'humanIdentifier'), $a);
print_r($b);
// Array
// (
// [one] => One
// [two-two] => Two Two
// [four] => Four
// [three] => Three
// )
code reflects data reflects code
... data reflects code reflects data ... Take a look at our final bit of code there:
$b = array_flatmap(partial('array_kmap', 'humanIdentifier'), $a);
We're doing an array_map of an array_map - this makes sense because our initial data is an array of arrays! Here the design of our code is a reflection of the shape of the data it operates on.
This is great because even if we didn't write this, we could look at this code and immediately know the shape of the data it's mean to work on
putting it all together
Just to save you the time of gathering all of the snippets above, here's a complete runnable script with verified output
function array_kmap (callable $f, iterable $xs) {
return array_reduce(array_keys($xs), function ($acc, $k) use ($xs, $f) {
return array_merge($acc, [$k => call_user_func($f, $k, $xs[$k], $xs)]);
}, []);
}
function humanIdentifier ($x) {
return ucwords(str_replace('-', ' ', $x));
}
function array_flatmap (callable $f, iterable $xs) {
return array_reduce(array_map($f, $xs), 'array_merge', []);
}
function partial (callable $f, ...$xs) {
return function (...$ys) use ($f, $xs) {
return call_user_func($f, ...$xs, ...$ys);
};
}
$a = [
['one' => 1, 'two-two' => 2, 'four' => 4],
['two-two' => 22, 'three' => 33, 'four' => 44]
];
$b = array_flatmap(partial('array_kmap', 'humanIdentifier'), $a);
print_r($b);
// Array
// (
// [one] => One
// [two-two] => Two Two
// [four] => Four
// [three] => Three
// )
remarks
We wrote a lot of code here compared to the original code posted in your question. So maybe you're wondering how this is an improvement. Well, that's a qualitative measure, and in a lot of areas (eg speed, efficiency) this answer is probably worse. But in other areas (eg readability, maintainability) I see a dramatic improvement.
Like others, when I first read your code, I was scratching my head over what the heck it did. Looking at the resulting transformation, I can reason about what's happening more easily because I'm less focused on how things are being transformed and I can just focus on the parts that matter.
If you squint your eyes, this is basically all we have to be concerned about
// input is array of arrays
$a = array(array( ... ))
// output requires map of map of input
$b = map(map( ... humanIdentifier ))
We did a bunch of other stuff, too, like avoided unnecessary assignments, reassignments, or mutations. $a
is untouched as a result of creating $b
. Avoiding side-effects like this aids in increasing readability and decreasing complexity as our program continues to grow. Anyway, those are out-of-scope for this answer, but I figured I'd mention them.
Hope to have helped ^_^