使用声明性方法转换数组

Transform array using a declarative approach

给定以下代码:

$flat = [
    [ '10', 'hoho'],
    [ '10', null],
    [ '13', null],
    [ '10', 'ahha']
];

//imperative, procedural approach
$hierarchical = [];
foreach ($flat  as $entry) {
    $id = $entry[0];

    $hierarchical[$id]['id'] = $id;
    $hierarchical[$id]['microtags'] = $hierarchical[$id]['microtags'] ?? [];
    if ($entry[1] != null)
        array_push($hierarchical[$id]['microtags'], $entry[1]);
}

及其结果($hierarchical):

 array (
   10 => 
   array (
     'id' => '10',
     'microtags' => 
     array (
       0 => 'hoho',
       1 => 'ahha',
     ),
   ),
   13 => 
   array (
     'id' => '13',
     'microtags' => 
     array (
     ),
   ),
 )

是否可以将其重构为合理有效的 declarative/functional 方法?喜欢使用数组转换函数(map、reduce、filter 等)?也无需更改引用或更改相同的变量。如果可以,怎么做?

创建和遍历不同形状的树最好使用函数来完成。下面,我们创建函数 node_createnode_add_child 来编码我们的意图。最后,我们使用array_reduce来完成转换。 $flat保持不变;我们的归约操作仅 从输入数据中读取

function node_create ($id, $children = []) {
  return [ "id" => $id, "children" => $children ];
}

function node_add_child ($node, $child) {
  return node_create ($node['id'], array_merge ($node['children'], [ $child ]));
}

$flat =
  [ [ '10', 'hoho' ]
  , [ '10', null ]
  , [ '13', null ]
  , [ '10', 'ahha' ]
  ];

$result =
  array_reduce ($flat, function ($acc, $item) {
    list ($id, $value) = $item;
    if (! array_key_exists ($id, $acc))
      $acc [$id] = node_create ($id);
    if (! is_null ($value))
      $acc [$id] = node_add_child ($acc [$id], $value);
    return $acc;
  }, []);

结果

print_r ($result);
// Array
// (
//     [10] => Array
//         (
//             [id] => 10
//             [children] => Array
//                 (
//                     [0] => hoho
//                     [1] => ahha
//                 )
//         )
//     [13] => Array
//         (
//             [id] => 13
//             [children] => Array
//                 (
//                 )
//         )
// )

上面,我们为$acc使用关联数组,这意味着我们必须使用PHP的built-in函数来与关联数组交互。我们可以抽象掉 PHP 的丑陋接口,non-functional 接口以获得更有利的接口。

function has ($map, $key) {
  return array_key_exists ($key, $map);
}

function get ($map, $key) {
  return $map [$key];
}

function set ($map, $key, $value = null) {
  $map [$key] = $value;
  return $map;
}

我们将添加 null children 的逻辑移动到 node_add_child

function node_create ($id, $children = []) {
  return [ "id" => $id, "children" => $children ];
}

function node_add_child ($node, $child = null) {
  if (is_null ($child))
    return $node;
  else
    return node_create ($node['id'], array_merge ($node['children'], [ $child ]));
}

现在我们可以看到更具声明性的 reduce

function make_tree ($flat = []) {
  return 
    array_reduce ($flat, function ($acc, $item) {
      list ($id, $value) = $item;
      return 
          set ( $acc
              , $id
              , has ($acc, $id)
                  ? node_add_child (get ($acc, $id), $value)
                  : node_add_child (node_create ($id), $value)
              );
    }, []);
}

print_r (make_tree ($flat));
// same output as above

上面,我们看到了 hasgetset 如何简化我们的 reduce 操作。然而,这种方法会导致许多小的、分离的功能。另一种方法涉及发明满足您需要的您自己的数据类型。下面,我们废弃上面创建的分离函数并将它们换成 class、MutableMap

class MutableMap {
  public function __construct ($data = []) {
    $this->data = $data;
  }
  public function has ($key) {
    return array_key_exists ($key, $this->data);
  }
  public function get ($key) {
    return $this->has ($key)
      ? $this->data [$key]
      : null
    ;
  }
  public function set ($key, $value = null) {
    $this->data [$key] = $value;
    return $this;
  }
  public function to_assoc () {
    return $this->data;
  }
}

现在,我们不必将 $acc 传递给每个函数,而是将其换成 $map,这是我们新类型

的一个实例
function make_tree ($flat = []) {
  return 
    array_reduce ($flat, function ($map, $item) {
      list ($id, $value) = $item;
      return
        $map -> set ( $id
                    , $map -> has ($id)
                        ? node_add_child ($map -> get ($id), $value)
                        : node_add_child (node_create ($id), $value)
                    );
    }, new MutableMap ())
    -> to_assoc ();
}

当然,您可以将 node_createnode_add_child 换成 class-based 实现,class Node { ... }。此练习留给 reader.

function make_tree ($flat = []) {
  return 
    array_reduce ($flat, function ($map, $item) {
      list ($id, $value) = $item;
      return
        $map -> set ( $id
                    , $map -> has ($id)
                        ? $map -> get ($id) -> add_child ($value)
                        : (new Node ($id)) -> add_child ($value)
                    );
    }, new MutableMap ())
    -> to_assoc ();
}