28

I have an associative array $assoc, and need to reduce to it to a string, in this context

$OUT = "<row";
foreach($assoc as $k=>$v) $OUT.= " $k=\"$v\"";
$OUT.= '/>';

How to do in an elegant way the same thing, but using array_reduce()


Near the same algorithm (lower performance and lower legibility) with array_walk() function,

 array_walk(  $row, function(&$v,$k){$v=" $k=\"$v\"";}  );
 $OUT.= "\n\t<row". join('',array_values($row)) ."/>";

Ugly solution with array_map() (and again join() as reducer):

  $row2 = array_map( 
    function($a,$b){return array(" $a=\"$b\"",1);},
    array_keys($row),
    array_values($row)
  ); // or  
  $OUT ="<row ". join('',array_column($row2,0)) ."/>";

PS: apparently PHP's array_reduce() not support associative arrays (why??).

Community
  • 1
  • 1
Peter Krauss
  • 13,174
  • 24
  • 167
  • 304
  • array_reduce() doesn't care whether your array is associative or enumerated, or even a mix, so don't spread FUD.... or provide a genuine example where it fails.... note that the callback is purely values, which is the documented behaviour, that's not the same as not supporting associative arrays – Mark Baker Mar 23 '15 at 14:49
  • @MarkBaker yes, the function is honest about "no access to the current index of the current value"... And other languages ([like Python](https://docs.python.org/2/library/functions.html#reduce)) use the same semantic for *reduce()*... Is better to view *reduce()* first parameter as a [traversable](http://php.net/manual/en/class.traversable.php) (not an array), where only *current/next* are known. – Peter Krauss Aug 18 '15 at 09:42
  • 3
    It is not FUD. It can be badly phrased, but array_reduce doesn't support associative arrays properly or completely. It only supports them partially, treating them like non associative ones. The implementation for non associative arrays of PHP is actually an associative array, but since you discard the keys on array_reduce, it isn't much useful when you need one associative. That's the point. – Asrail Feb 19 '16 at 02:12
  • **CONCLUSION**: the `foreach` solution above is the best solution. Faster, simplest (so elegant) ... And there are **no (functional) elegant way to use `array_reduce`** for this kind of problem in PHP. – Peter Krauss Mar 01 '18 at 10:10
  • PS: see also [this discusstion about reduce function in Python](https://stackoverflow.com/a/13638960/287948) and general solutions to avoid reduce and lambda, the [list comprehension](https://www.python-course.eu/python3_list_comprehension.php). – Peter Krauss Oct 17 '18 at 17:35

6 Answers6

23

First, array_reduce() works with associative arrays, but you don't have any chance to access the key in the callback function, only the value.

You could use the use keyword to access the $result by reference in the closure like in the following example with array_walk(). This would be very similar to array_reduce():

$array = array(
    'foo' => 'bar',
    'hello' => 'world'
);

// Inject reference to `$result` into closure scope.
// $result will get initialized on its first usage.
array_walk($array, function($val, $key) use(&$result) {
    $result .= " $key=\"$val\"";
});
echo "<row$result />";

Btw, imo your original foreach solution looks elegant too. Also there will be no significant performance issues as long as the array stays at small to medium size.

hek2mgl
  • 152,036
  • 28
  • 249
  • 266
  • I would have preferred to see some sort of delimiter between k/v pairs (something that acts like `implode()` and doesn't need to be trimmed from the front or back of the string when done). For the OP's scenario, I guess you can just unconditionally prepend a space to all pairs and remove the space after `row`. – mickmackusa Sep 11 '20 at 04:37
16
$array = array(
    'foo' => 'bar',
    'hello' => 'world'
);

$OUT = join(" ", array_reduce(array_keys($array), function($as, $a) use ($array) {
    $as[] = sprintf('%s="%s"', $a, $array[$a]); return $as;
}, array()));
Just a student
  • 10,560
  • 2
  • 41
  • 69
6

I personally see nothing wrong with the foreach thing, but if you want one single expression, your map snippet can be simplified down to

$OUT = sprintf("<row %s/>",
    join(" ", array_map(
        function($a, $b) { return "$a=\"$b\""; },
        array_keys($assoc),
        array_values($assoc)
)));

Also, since you're generating XML, it's better to use a dedicated tool, for example:

$doc = new SimpleXMLElement("<row/>");
foreach($assoc as $k => $v)
    $doc->addAttribute($k, $v);
echo $doc->asXML();
georg
  • 211,518
  • 52
  • 313
  • 390
  • Good and elegant XML-dedicated solution! the question was a test for use "functional style" in PHP... no positive result. We have similar problem with SimpleXML of PHP that never implemented "cast to array" in SimpleXML or DOMDocument. – Peter Krauss Jun 05 '18 at 22:02
  • `array_values()` is an unnecessary function call -- as shown by ShaunCockerill. – mickmackusa Sep 11 '20 at 04:38
  • IMHO the most elegant solution to be used as an oneliner in an assertion context and also the one that is nearest to a "join with prepended keys" intention. I keep the `array_values()` call for better readability/comprehensibility. – spackmat Mar 15 '22 at 10:57
4

you can prepare your input array using array_chunk and use array_reduce like this:

$a = ["a" => 123, "b" => 234, "c" => 55]; 

echo array_reduce(
   array_chunk($a, 1, true), 
   function ($r, $i) { 
     return $r . key($i) ." = ". current($i) . PHP_EOL;
   },
   ""
);

will show - ideal for array-as-text representation:

a = 123
b = 234
c = 55

array_chunk will create an array of single associative array entries. please note that is is probably not the the most performant solution - just a rather short one.

lukas.j
  • 6,453
  • 2
  • 5
  • 24
Andy P
  • 306
  • 4
  • 10
  • Maybe not the most performant solution, but in my opinion the most readable and kind of similar to the `Object.entries` function in JavaScript, which I really like. I also try to avoid strong references in PHP as much as possible (which is why I don't like array_walk). – Terrabythia Jun 17 '19 at 11:08
1

If you are set on array_reduce, and the values of your array are unique, then you can access the key by passing the associative array to the callback function and using array_search.

// Pass $assoc to your anonymous function and use array_search to get the key.
$OUT .= array_reduce($assoc, function($o, $v) use($assoc) {
    return sprintf('%s %s="%s"', $o, array_search($v, $assoc), $v);
}, '');

Personally, I feel that array_map and join would be more effective in this situation.

$OUT .= join(' ', array_map(function($v, $k){
    return sprintf('%s="%s"', $k, $v);
}, $assoc, array_keys($assoc)));
Shaun Cockerill
  • 800
  • 8
  • 11
0

Strictly using array_reduce this is the simplest algorithm I can think of (also both anonymous functions are pure functions):

$out =
    '<row '.
        array_reduce(
            array_map  (
                function ($e, $k) { return [$e, $k];  },
                array_keys($assoc),
                $assoc
            ),
            function ( $props, $e ) { return $props." {$e[0]}=\"{$e[1]}\"";  }
        )
    .' />';

In one line...

$out = '<row '. array_reduce( array_map( function ($e, $k) { return [$e, $k];  }, array_keys($assoc), $assoc), function ( $props, $e ) { return $props." {$e[0]}=\"{$e[1]}\""; }).' />';
Beto Aveiga
  • 3,466
  • 4
  • 27
  • 39