Is there a way of doing something like this:
$test_array = array(\"first_key\" => \"first_value\",
\"second_key\" => \"second_valu
I'd do something like this:
<?php
/**
* array_map_kv()
* An array mapping function to map with both keys and values.
*
* @param $callback callable
* A callback function($key, $value) for mapping values.
* @param $array array
* An array for mapping.
*/
function array_map_kv(callable $callback, array $array) {
return array_map(
function ($key) use ($callback, $array) {
return $callback($key, $array[$key]); // $callback($key, $value)
},
array_keys($array)
);
}
// use it
var_dump(array_map_kv(function ($key, $value) {
return "{$key} loves {$value}";
}, array(
"first_key" => "first_value",
"second_key" => "second_value",
)));
?>
Results:
array(2) {
[0]=>
string(27) "first_key loves first_value"
[1]=>
string(29) "second_key loves second_value"
}
Here's my very simple, PHP 5.5-compatible solution:
function array_map_assoc(callable $f, array $a) {
return array_column(array_map($f, array_keys($a), $a), 1, 0);
}
The callable you supply should itself return an array with two values, i.e. return [key, value]
. The inner call to array_map therefore produces an array of arrays. This then gets converted back to a single-dimension array by array_column.
$ordinals = [
'first' => '1st',
'second' => '2nd',
'third' => '3rd',
];
$func = function ($k, $v) {
return ['new ' . $k, 'new ' . $v];
};
var_dump(array_map_assoc($func, $ordinals));
array(3) {
["new first"]=>
string(7) "new 1st"
["new second"]=>
string(7) "new 2nd"
["new third"]=>
string(7) "new 3rd"
}
In case you need to use the function many times with different arrays but the same mapping function, you can do something called partial function application (related to ‘currying’), which allows you to only pass in the data array upon invocation:
function array_map_assoc_partial(callable $f) {
return function (array $a) use ($f) {
return array_column(array_map($f, array_keys($a), $a), 1, 0);
};
}
...
$my_mapping = array_map_assoc_partial($func);
var_dump($my_mapping($ordinals));
Which produces the same output, given $func
and $ordinals
are as earlier.
NOTE: if your mapped function returns the same key for two different inputs, the value associated with the later key will win. Reverse the input array and output result of array_map_assoc
to allow earlier keys to win. (The returned keys in my example cannot collide as they incorporate the key of the source array, which in turn must be unique.)
Following is a variant of the above, which might prove more logical to some, but requires PHP 5.6:
function array_map_assoc(callable $f, array $a) {
return array_merge(...array_map($f, array_keys($a), $a));
}
In this variant, your supplied function (over which the data array is mapped) should instead return an associative array with one row, i.e. return [key => value]
.
The result of mapping the callable is then simply unpacked and passed to array_merge
. As earlier, returning a duplicate key will result in later values winning.
n.b. Alex83690 has noted in a comment that using
array_replace
here in the stead ofarray_merge
would preserve integer keys.array_replace
does not modify the input array, so is safe for functional code.
If you are on PHP 5.3 to 5.5, the following is equivalent. It uses array_reduce
and the binary +
array operator to convert the resulting two-dimensional array down to a one-dimensional array whilst preserving keys:
function array_map_assoc(callable $f, array $a) {
return array_reduce(array_map($f, array_keys($a), $a), function (array $acc, array $a) {
return $acc + $a;
}, []);
}
Both of these variants would be used thus:
$ordinals = [
'first' => '1st',
'second' => '2nd',
'third' => '3rd',
];
$func = function ($k, $v) {
return ['new ' . $k => 'new ' . $v];
};
var_dump(array_map_assoc($func, $ordinals));
Note the =>
instead of ,
in $func
.
The output is the same as before, and each can be partially applied in the same way as before.
The goal of the original question is to make the invocation of the call as simple as possible, at the expense of having a more complicated function that gets invoked; especially, to have the ability to pass the data array in as a single argument, without splitting the keys and values. Using the function supplied at the start of this answer:
$test_array = ["first_key" => "first_value",
"second_key" => "second_value"];
$array_map_assoc = function (callable $f, array $a) {
return array_column(array_map($f, array_keys($a), $a), 1, 0);
};
$f = function ($key, $value) {
return [$key, $key . ' loves ' . $value];
};
var_dump(array_values($array_map_assoc($f, $test_array)));
Or, for this question only, we can make a simplification to array_map_assoc()
function that drops output keys, since the question does not ask for them:
$test_array = ["first_key" => "first_value",
"second_key" => "second_value"];
$array_map_assoc = function (callable $f, array $a) {
return array_map($f, array_keys($a), $a);
};
$f = function ($key, $value) {
return $key . ' loves ' . $value;
};
var_dump($array_map_assoc($f, $test_array));
So the answer is NO, you can't avoid calling array_keys
, but you can abstract out the place where array_keys
gets called into a higher-order function, which might be good enough.
By "manual loop" I meant write a custom function that uses foreach
. This returns a new array like array_map
does because the function's scope causes $array
to be a copy—not a reference:
function map($array, callable $fn) {
foreach ($array as $k => &$v) $v = call_user_func($fn, $k, $v);
return $array;
}
Your technique using array_map
with array_keys
though actually seems simpler and is more powerful because you can use null
as a callback to return the key-value pairs:
function map($array, callable $fn = null) {
return array_map($fn, array_keys($array), $array);
}
You can use map method from this array library to achieve exactly what you want as easily as:
Arr::map($test_array, function($a, $b) { return "$a loves $b"; });
also it preserves keys and returns new array, not to mention few different modes to fit your needs.
I see it's missing the obvious answer:
function array_map_assoc(){
if(func_num_args() < 2) throw new \BadFuncionCallException('Missing parameters');
$args = func_get_args();
$callback = $args[0];
if(!is_callable($callback)) throw new \InvalidArgumentException('First parameter musst be callable');
$arrays = array_slice($args, 1);
array_walk($arrays, function(&$a){
$a = (array)$a;
reset($a);
});
$results = array();
$max_length = max(array_map('count', $arrays));
$arrays = array_map(function($pole) use ($max_length){
return array_pad($pole, $max_length, null);
}, $arrays);
for($i=0; $i < $max_length; $i++){
$elements = array();
foreach($arrays as &$v){
$elements[] = each($v);
}
unset($v);
$out = call_user_func_array($callback, $elements);
if($out === null) continue;
$val = isset($out[1]) ? $out[1] : null;
if(isset($out[0])){
$results[$out[0]] = $val;
}else{
$results[] = $val;
}
}
return $results;
}
Works exactly like array_map. Almost.
Actually, it's not pure map
as you know it from other languages. Php is very weird, so it requires some very weird user functions, for we don't want to unbreak our precisely broken worse is better
approach.
Really, it's not actually map
at all. Yet, it's still very useful.
First obvious difference from array_map, is that the callback takes outputs of each()
from every input array instead of value alone. You can still iterate through more arrays at once.
Second difference is the way the key is handled after it's returned from callback; the return value from callback function should be array('new_key', 'new_value')
. Keys can and will be changed, same keys can even cause previous value being overwritten, if same key was returned. This is not common map
behavior, yet it allows you to rewrite keys.
Third weird thing is, if you omit key
in return value (either by array(1 => 'value')
or array(null, 'value')
), new key is going to be assigned, as if $array[] = $value
was used. That isn't map
's common behavior either, yet it comes handy sometimes, I guess.
Fourth weird thing is, if callback function doesn't return a value, or returns null
, the whole set of current keys and values is omitted from the output, it's simply skipped. This feature is totally unmap
py, yet it would make this function excellent stunt double for array_filter_assoc
, if there was such function.
If you omit second element (1 => ...
) (the value part) in callback's return, null
is used instead of real value.
Any other elements except those with keys 0
and 1
in callback's return are ignored.
And finally, if lambda returns any value except of null
or array, it's treated as if both key and value were omitted, so:
null
is used as it's value WARNING:
Bear in mind, that this last feature is just a residue of previous features and it is probably completely useless. Relying on this feature is highly discouraged, as this feature is going to be randomly deprecated and changed unexpectedly in future releases.
NOTE:
Unlike in array_map
, all non-array parameters passed to array_map_assoc
, with the exception of first callback parameter, are silently casted to arrays.
EXAMPLES:
// TODO: examples, anyone?
Not with array_map, as it doesn't handle keys.
array_walk does:
$test_array = array("first_key" => "first_value",
"second_key" => "second_value");
array_walk($test_array, function(&$a, $b) { $a = "$b loves $a"; });
var_dump($test_array);
// array(2) {
// ["first_key"]=>
// string(27) "first_key loves first_value"
// ["second_key"]=>
// string(29) "second_key loves second_value"
// }
It does change the array given as parameter however, so it's not exactly functional programming (as you have the question tagged like that). Also, as pointed out in the comment, this will only change the values of the array, so the keys won't be what you specified in the question.
You could write a function that fixes the points above yourself if you wanted to, like this:
function mymapper($arrayparam, $valuecallback) {
$resultarr = array();
foreach ($arrayparam as $key => $value) {
$resultarr[] = $valuecallback($key, $value);
}
return $resultarr;
}
$test_array = array("first_key" => "first_value",
"second_key" => "second_value");
$new_array = mymapper($test_array, function($a, $b) { return "$a loves $b"; });
var_dump($new_array);
// array(2) {
// [0]=>
// string(27) "first_key loves first_value"
// [1]=>
// string(29) "second_key loves second_value"
// }