【问题标题】:Transform array using a declarative approach使用声明性方法转换数组
【发布时间】:2018-09-23 00:03:49
【问题描述】:

给定以下代码:

$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 (
     ),
   ),
 )

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

【问题讨论】:

  • 什么是无状态的?
  • $flat 是什么?
  • 对不起,s/query/flat/
  • 我希望我在帖子本身上进一步澄清了我的意思。但总而言之:在不覆盖相同变量的情况下转换数据。
  • 嗯,避免可见的突变似乎不值得在 PHP 中努力?!?您通常使用递归和累加器来执行此操作。出于性能原因,您可以改变累加器,因为它隐藏在函数范围内。不幸的是,我几乎不知道 PHP...

标签: php functional-programming stateless


【解决方案1】:

创建和遍历不同形状的树最好使用函数来完成。下面,我们创建函数 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 的内置函数来与关联数组进行交互。我们可以抽象出 PHP 丑陋的、非功能性的接口以获得更有利的接口。

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 子级的逻辑移至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 操作。但是,这种方法可能会导致许多小的、分离的功能。另一种方法涉及发明您自己的满足您需求的数据类型。下面,我们废弃上面创建的分离函数,并将它们换成一个类,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 Node { ... }。这个练习留给读者。

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 ();
}

【讨论】:

  • 我不明白这与 OP 已经在做什么有什么不同。我的意思是,我知道你的做法不同(使用函数),但这怎么比原始代码更“无状态”?
  • @Anthony 我们的归约操作不会改变现有绑定或创建新绑定。它是一个纯函数式操作,以$flat 作为输入并产生输出$result
  • 它更具声明性。这才是我真正的意思。
  • 原来的也不创建新的绑定?什么是绑定?这有什么特别之处?这就是所有功能的工作方式。而且您的方法不采用$flat,它是一个匿名函数,恰好调用了非匿名函数。所有真正的工作都发生在那个匿名函数中。这与$result = myTransformFunction($flat); 有何不同?
  • 谢谢@user633183。你的回答很有教育意义。非常感谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-28
相关资源
最近更新 更多