Access first level keys with array_map() without calling `array_keys()`
Asked Answered
C

18

312

Is there a way of doing something like this:

$test_array = array(
    "first_key" => "first_value", 
    "second_key" => "second_value"
);

var_dump(
    array_map(
        function($a, $b) {
            return "$a loves $b";
        }, 
        array_keys($test_array), 
        array_values($test_array)
    )
);

But instead of calling array_keys and array_values, directly passing the $test_array variable?

The desired output is:

array(2) {
  [0]=>
  string(27) "first_key loves first_value"
  [1]=>
  string(29) "second_key loves second_value"
}
Chaldron answered 23/10, 2012 at 17:40 Comment(0)
V
292

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"
// }
Viewpoint answered 23/10, 2012 at 17:51 Comment(10)
Except that in this case, you want $a = "$b loves $a", to match the OP's desired output.Stuckup
correct, changed :) it is nice how different they've made array_map from array_walk.Viewpoint
Nice, thanks. In order to avoid messing the original array, here's what I eventually did (look my answer below)Yare
This is not "functional-programming" though since array_walk() does not return the resulting array, but a bool instead.Mavis
@Mavis yes, like I wrote in the answer as well - instead of returning the value it changes the parameterViewpoint
array_map will handle keys and anything else you provide it. It's the most idiomatic solution.Bournemouth
it does not return desired output , instead it gives array:2 [ "first_key" => "first_key loves first_value" "second_key" => "second_key loves second_value" ]Popish
@AndrewVakhniuk correct, it only changes the values, not the keys. however I think the main point of this question were the values. You can alter the keys as well in the function given as parameter if needed.Viewpoint
@Viewpoint i tried with that function, i can not change the key, instead i am unsetting the element from the array and adding new one with needed keyPopish
ah. I also checked and apparently you really can't change the key in the way code is written in this answer. Good point.Viewpoint
B
224

This is probably the shortest and easiest to reason about:

$states = array('az' => 'Arizona', 'al' => 'Alabama');

array_map(function ($short, $long) {
    return array(
        'short' => $short,
        'long'  => $long
    );
}, array_keys($states), $states);

// produces:
array(
     array('short' => 'az', 'long' => 'Arizona'), 
     array('short' => 'al', 'long' => 'Alabama')
)
Bournemouth answered 29/5, 2015 at 21:28 Comment(6)
I just realized that the question specifically said not to use array_keys(). That seems like a silly requirement, though.Bournemouth
The question provided a solution using array_keys(), it would be silly to provide an answer that has no advantage (eg, calling less functions) over the current solution.Kehoe
The answer to the original question is NO, and this is the most appropriate solution.Linnie
This answer has ignored the sample data and desired output of the asked question.Farfetched
How could this get to many upvotes? This returns an array of arrays, which is NOT the expected output (an array of 2 items). It's very different structures so this answer is wrong.Clydeclydebank
@V.Högman That's so funny. I never noticed that before. My reading comprehension is not as good as I thought, haha. I must have just read the title. Maybe the people upvoting also came here because of the title (which used to be different).Bournemouth
P
96

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.

Usage

$ordinals = [
    'first' => '1st',
    'second' => '2nd',
    'third' => '3rd',
];

$func = function ($k, $v) {
    return ['new ' . $k, 'new ' . $v];
};

var_dump(array_map_assoc($func, $ordinals));

Output

array(3) {
  ["new first"]=>
  string(7) "new 1st"
  ["new second"]=>
  string(7) "new 2nd"
  ["new third"]=>
  string(7) "new 3rd"
}

Partial application

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.)


Alternative

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 of array_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;
    }, []);
}

Usage

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.


 Summary

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.

Paleo answered 24/3, 2017 at 16:49 Comment(6)
array_column is PHP 5.5+ only, so this answer is not compatible with PHP 5.3Oates
I'll be THAT guy. PHP 5.3 should no longer be a requirement for the date of this answer. IMHO.Eruptive
@Eruptive Sure, I just wanted to provide as versatile an answer as I could. I do like running new code on old systems though. I have Netscape 1.0 running on on an old Mac with System 7.1 which I occasionally go browsing the web with, just to give sys admins a fright when they look in their server logs :-)Paleo
Your first alternative solution in invalid. you must replace array_merge by array_replace to preserve keys that would be integers.Mohammadmohammed
@Mohammadmohammed Thanks! Though I'd say "invalid' is slightly misleading — it is fine if you do not have any integer keys (as was true in my own case).Paleo
If some interested in array-mapping function that preserves keys and at the same time it's callback has access to both key and value, and can return just a value (as normal mapping function do) instead of "specially formatted" value (an associative array with one row), then here is slightly modified php 5.6 solution: function array_map_assoc(callable $f, array $a) { return array_merge(...array_map(function ($k, $v) use ($f) { return [ $k => $f($k, $v) ]; }, array_keys($a), $a)); }. For this to work, the $f must be wrapped with another function, that maps new value to [key=>new_value].Ofilia
H
64
$array = [
    'category1' => 'first category',
    'category2' => 'second category',
];

$new = array_map(function ($key, $value) {
    return "{$key} => {$value}";
}, array_keys($array), $array);
Hanson answered 5/5, 2020 at 11:49 Comment(0)
F
21

With PHP5.3 or later:

$test_array = array("first_key" => "first_value", 
                    "second_key" => "second_value");

var_dump(
    array_map(
        function($key) use ($test_array) { return "$key loves ${test_array[$key]}"; },
        array_keys($test_array)
    )
);
Farrow answered 28/1, 2014 at 13:45 Comment(1)
I think the requirement was "instead of calling array_keys and array_values, directly passing the $test_array variable", can this be used without array_keys?Viewpoint
R
8

I'll add yet another solution to the problem using version 5.6 or later. Don't know if it's more efficient than the already great solutions (probably not), but to me it's just simpler to read:

$myArray = [
    "key0" => 0,
    "key1" => 1,
    "key2" => 2
];

array_combine(
    array_keys($myArray),
    array_map(
        function ($intVal) {
            return strval($intVal);
        },
        $myArray
    )
);

Using strval() as an example function in the array_map, this will generate:

array(3) {
  ["key0"]=>
  string(1) "0"
  ["key1"]=>
  string(1) "1"
  ["key2"]=>
  string(1) "2"
}

Hopefully I'm not the only one who finds this pretty simple to grasp. array_combine creates a key => value array from an array of keys and an array of values, the rest is pretty self explanatory.

Rubidium answered 2/10, 2018 at 10:44 Comment(3)
Thank you! I was looking for array_combine() to combine with array_map() to generate new key => value pairs: array_combine(array_values($arr), array_map(..., $arr))Lanta
[Simpler version of this answer ](3v4l.org/36r4j)Farfetched
This answer has ignored the sample data and desired output of the asked question.Farfetched
T
6

Look here! There is a trivial solution!

function array_map2(callable $f, array $a)
{
    return array_map($f, array_keys($a), $a);
}

As stated in the question, array_map already has exactly the functionality required. The other answers here seriously overcomplicate things: array_walk is not functional.

Usage

Exactly as you would expect from your example:

$test_array = array("first_key" => "first_value", 
                    "second_key" => "second_value");

var_dump(array_map2(function($a, $b) { return "$a loves $b"; }, $test_array));
Tambourine answered 5/9, 2018 at 10:27 Comment(1)
the other answers overcomplicate things because the question specified qrrqy_keys() shouldn't be used for #reasonsDrawers
R
3

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);
}
Ries answered 23/10, 2012 at 17:45 Comment(6)
looping array with reference, can cause spooky things to happenForestry
It's not spooky, it just means you forgot to unset( $value ) because it still exists in the defined scope.Itis
@azis, was kidding about the spookiness, referring to the article. It will create unexpected effects if you forget to unset.Forestry
Thanks for the answer, but I thought it was pretty clear that I didn't want to use a traditional loop.Yare
@Forestry See edited answer for clarification. I meant looping in a clean variable scope.Ries
Your parameters should be passed in the other way around. It is more versatile, and standard practice in FP, to have the 'data' last and the 'operation' first.Paleo
P
3

This is how I've implemented this in my project.

function array_map_associative(callable $callback, $array) {
    /* map original array keys, and call $callable with $key and value of $key from original array. */
    return array_map(function($key) use ($callback, $array){
        return $callback($key, $array[$key]);
    }, array_keys($array));
}
Piperpiperaceous answered 6/3, 2018 at 21:57 Comment(1)
Very clean, and doesn't alter the original array!Octangular
J
3

A closure would work if you only need it once. I'd use a generator.

$test_array = [
    "first_key" => "first_value", 
    "second_key" => "second_value",
];

$x_result = (function(array $arr) {
    foreach ($arr as $key => $value) {
        yield "$key loves $value";
    }
})($test_array);

var_dump(iterator_to_array($x_result));

// array(2) {
//   [0]=>
//   string(27) "first_key loves first_value"
//   [1]=>
//   string(29) "second_key loves second_value"
// }

For something reusable:

function xmap(callable $cb, array $arr)
{
    foreach ($arr as $key => $value) {
        yield $cb($key, $value);
    }
}

var_dump(iterator_to_array(
    xmap(function($a, $b) { return "$a loves $b"; }, $test_array)
));
Judy answered 8/9, 2020 at 22:57 Comment(0)
C
1

Based on eis's answer, here's what I eventually did in order to avoid messing the original array:

$test_array = array("first_key" => "first_value",
                    "second_key" => "second_value");

$result_array = array();
array_walk($test_array, 
           function($a, $b) use (&$result_array) 
           { $result_array[] = "$b loves $a"; }, 
           $result_array);
var_dump($result_array);
Chaldron answered 23/10, 2012 at 21:10 Comment(4)
Why is this easier than just passing the array values and keys directly to array_map? It's slower and more complicated, I'm not seeing the advantage.Flareup
@Flareup can you back up the claim that it would be slower, even with big numbers? It needs to iterate the array only once, so I think it should be magnitudes faster in big O notation. I agree about complexity though.Viewpoint
@Viewpoint It's slower because it's creating the result array one at a time in PHP, instead of en masse in C. It does avoid the array_keys call though (although that's fast since it's in C). Benchmark it - see which is faster, I'm not really certain, but usually more code = slower code. In complexity it's definitely worse though, and that's more important than speed most of the time.Flareup
You do not need to send the third arg to array_walk as you are not referencing it in the closure.Bumptious
C
1

I made this function, based on eis's answer:

function array_map_($callback, $arr) {
    if (!is_callable($callback))
        return $arr;

    $result = array_walk($arr, function(&$value, $key) use ($callback) {
        $value = call_user_func($callback, $key, $value);
    });

    if (!$result)
        return false;

    return $arr;
}

Example:

$test_array = array("first_key" => "first_value", 
                "second_key" => "second_value");

var_dump(array_map_(function($key, $value){
    return $key . " loves " . $value;
}, $arr));

Output:

array (
  'first_key' => 'first_key loves first_value,
  'second_key' => 'second_key loves second_value',
)

Off course, you can use array_values to return exactly what OP wants.

array_values(array_map_(function($key, $value){
    return $key . " loves " . $value;
}, $test_array))
Cologarithm answered 1/4, 2015 at 20:25 Comment(2)
@KevinBeal I use this function a lot in my work. Could you point where is the errors?Cologarithm
Firstly, the code as it stands is missing a check that $arr is of type array, however if you type-hint your arguments as callable and array you can instead drop the check to is_callable. Next, you make an assignment to $value that is then unused. You should just ignore the return value. Thirdly, it would be better to throw an exception in the callback than to return false. You would then either always return a valid value or always throw.Paleo
C
1

YaLinqo library* is well suited for this sort of task. It's a port of LINQ from .NET which fully supports values and keys in all callbacks and resembles SQL. For example:

$mapped_array = from($test_array)
    ->select(function ($v, $k) { return "$k loves $v"; })
    ->toArray();

or just:

$mapped_iterator = from($test_array)->select('"$k loves $v"');

Here, '"$k loves $v"' is a shortcut for full closure syntax which this library supports. toArray() in the end is optional. The method chain returns an iterator, so if the result just needs to be iterated over using foreach, toArray call can be removed.

* developed by me

Counterproof answered 4/6, 2015 at 13:36 Comment(0)
C
1

I always like the javascript variant of array map. The most simple version of it would be:

/**
 * @param  array    $array
 * @param  callable $callback
 * @return array
 */
function arrayMap(array $array, callable $callback)
{
    $newArray = [];

    foreach( $array as $key => $value )
    {
        $newArray[] = call_user_func($callback, $value, $key, $array);
    }

    return $newArray;
}

So now you can just pass it a callback function how to construct the values.

$testArray = [
    "first_key" => "first_value", 
    "second_key" => "second_value"
];

var_dump(
    arrayMap($testArray, function($value, $key) {
        return $key . ' loves ' . $value;
    });
);
Condign answered 9/1, 2018 at 14:40 Comment(6)
It is more useful to have the data as the last argument to any function you write, as you can then create a new function that bakes-in some specific callback (behaviour) — i.e. you get function composition: h(g(f($data))) applies f, then g, then h to your data. It is generally considered more versatile in functional programming to have a function that performs the same operation on divers data, than to have a function that applies divers functions to a fixed data set.Paleo
In your example you have only 1 argument to the function. I find it easier to put the data as first argument, like array_filter, array_reduce and the array functions in javascript.Condign
That's my point! By passing the data last, it allows you to curry the function (create a new function that combines the looping with the specific operation) and apply that to the data by calling the new function with a single parameter. This principal is explained better than I can here, in this answer: stackoverflow.com/a/5863222Paleo
Is using a compose function in a language like PHP not a better solution to this problem?Condign
It's an alternative but requires substantially more investment in FP, for example this: github.com/nickshanks/fp-php-talk/blob/master/lib.php#L24 or this: github.com/nickshanks/php-fp/blob/master/src/fp.php#L62Paleo
foreach isn't convoluted enough.Judy
A
1

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"
}
Astra answered 15/8, 2018 at 3:50 Comment(0)
C
1

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.

Clintonclintonia answered 26/7, 2019 at 19:47 Comment(1)
Why using a library also array_map function with key do the job ?Tansy
M
0

Another way of doing this with(out) preserving keys:

$test_array = [
    "first_key"     => "first_value",
    "second_key"    => "second_value"
];

$f = function($ar) {
    return array_map(
        function($key, $val) {
            return "{$key} - {$val}";
        },
        array_keys($ar),
        $ar
    );
};

#-- WITHOUT preserving keys
$res = $f($test_array);

#-- WITH preserving keys
$res = array_combine(
    array_keys($test_array),
    $f($test_array)
);
Manned answered 21/11, 2018 at 0:8 Comment(0)
D
-2

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 unmappy, 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:

    1. new key for element is assigned
    2. 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?

Danieldaniela answered 22/7, 2013 at 20:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.