通过引用更新树结构中的项目并返回更新的树结构 [英] Update item in tree structure by reference and return updated tree structure

查看:52
本文介绍了通过引用更新树结构中的项目并返回更新的树结构的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我目前正在学习使用HyperappJS(V2)和RamdaJS进行的函数式编程.我的第一个项目是一个简单的博客应用程序,用户可以在其中评论帖子或其他评论.注释以树结构表示.

我的状态看起来像这样:

 //state.js导出默认值{帖子:[{主题:主题A",注释: []},{主题:主题B",注释: [{文字:评论",注释: [/* ... */]}]},{主题:主题C",注释: []}],其他的东西: ...} 

当用户想要添加评论时,我将当前树项传递给我的addComment-action.在那里,我将注释添加到引用的项目中,并返回一个新的状态对象以触发视图更新.

所以,目前我正在这样做,并且工作正常:

 //actions.js从"ramda"导入{concat}导出默认值{addComment:(state,args)=>{args.item.comments = concat(args.item.comments,[{text:args.text,注释:[]}])返回{... state}}} 

我的问题:这种方法正确吗?有什么方法可以清理此代码并使它更具功能性?我正在寻找的将是这样的:

  addComment:(state,args)=>({...状态,帖子:addCommentToReferencedPostItemAndReturnUpdatedPosts(args,state.posts)}) 

解决方案

在此方法中,我们1)在状态树中定位目标对象,然后2)转换找到的对象.假设您的树有某种方式 id 单个对象-

  const state ={个帖子:[{id:1//<-id,主题:主题A", 注释: []},{id:2//<-id,主题:主题B", 注释: []},{id:3//<-id,主题:主题C", 注释: []}],其他:[1,2,3]} 


搜索

您可以通过编写通用的 search 开始,该搜索会产生通往被查询对象的可能路径-

  const search = function *(o = {},f =身份,path = []){如果(!isObject(o))返回如果(f(o))屈服路径对于(Object.entries(o)的[const [k,v]])产量*搜索(v,f,[... path,k])} 

让我们找到 id 大于 1 -

的所有对象

 用于(搜索的常量路径(状态,({id = 0})=> id> 1))控制台.log(路径)//[帖子","1"]//[帖子","2"] 

这些路径"指向您的状态树中的对象,其中谓词({id = 0})=>编号>1),是真的.即

 <代码>//[帖子","1"]state.posts [1]//{id:2,主题:主题B",评论:[]}//[帖子","2"]state.posts [2]//{id:3,主题:主题C",评论:[]} 

我们将使用 search 编写像 searchById 这样的高阶函数,该函数可以更清晰地编码我们的意图-

  const searchById =(o = {},q = 0)=>搜索(o,({{id = 0})=> id === q)对于(searchById(state,2)的常量路径)控制台.log(路径)//[帖子","1"] 


转换

接下来,我们可以编写 transformAt ,它接受一个输入状态对象 o ,一个 path 和一个转换函数 t-

  const无=象征 ()const transformAt =(o = {},[q = None,... path] = [],t =身份)=>q ===无//1?至):isObject(o)//2?对象分配(isArray(o)?[]:{}o,{[q]:transformAt(o [q],path,t)}):提高(错误("transformAt:无效路径"))//3 

这些项目要点对应于以上编号的注释-

  1. 当查询 q None 时,路径已用尽,是时候在服务器上运行转换 t 了.输入对象 o .
  2. 否则,归纳 q 为空.如果输入 o 是对象,则使用 Object.assign 创建一个新对象,其新的 q 属性是其旧版本的转换 q 属性, o [q] .
  3. 否则,归纳 q 为空, o 对象.我们不能指望在非对象上查找 q ,因此 raise 会出现错误,以信号通知 transformAt 无效的路径.

现在,我们可以轻松编写 appendComment ,它接受输入,状态,注释的ID, parentId 和新的注释 c -

  const append = x =>a =>[... a,x]const appendComment =(状态= {},parentId = 0,c = {})=>{for(searchById(state,parentId)的常数路径)return transformAt//<-仅先转换;返回( 状态,[...路径,注释"],附加(c))返回状态//<--如果没有搜索结果,则返回未修改的状态} 

调用 search 会生成谓词查询返回true的 all 条可能路径.您必须做出选择,如何处理查询返回多个结果的情况.考虑像-

这样的数据

  const otherState ={posts:[{type:"post",id:1,...},...],图片:[{类型:图片",id:1,...},...]} 

使用 searchById(otherState,1)将获得两个对象,其中 id = 1 .在 appendComment 中,我们仅选择修改 first 匹配项.如果需要,可以修改 all search 结果-

 //,但实际上不这样做const appendComment =(状态= {},parentId = 0,c = {})=>大批.from(searchById(state,parentId))//<-所有结果.减少((r,path)=>transformAt//<-转换每个(,[...路径,注释"],附加(c)),状态//<-初始状态) 

但是在这种情况下,我们可能不想在我们的应用程序中重复注释.诸如 search 之类的任何查询功能都可能返回零,一个或多个结果,并且必须决定您的程序在每种情况下的响应方式.


放在一起

剩余的依赖项-

  const isArray =Array.isArrayconst isObject = x =>对象(x)=== xconst提高= e =>{投掷}const identity = x =>X 

让我们将第一个新注释附加到 id = 2 主题B" -

  const state1 =appendComment( 状态,2,{id:4,文字:不错的文章!",评论:[]}) 

我们的第一个状态修订版 state1 将是-

  {帖子:[{id:1,主题:主题A", 注释: []},{id:2,主题:主题B", 注释:[{id:4//,文字:不错的文章!"//<-新增,评论:[]//评论}//]},{id:3,主题:主题C", 注释: []}],其他:[1,2,3]} 

然后我们将添加另一个评论,该评论嵌套在该评论上-

  const state2 =appendComment( 状态,4//<-我们最后评论的ID,{id:5,文字:我同意!",评论:[]}) 

第二个修订版 state2 将是-

  {帖子:[{id:1,...},{id:2,主题:主题B", 注释:[{id:4,文字:不错的文章!", 注释:[{id:5//嵌套,文字:我同意!"//<-评论,评论:[]//添加}//]}]},{id:3,...}],...} 


代码演示

在此演示中,我们将

    通过修改 state 以添加第一条评论
  • 创建 state1
  • 通过修改 state1 以添加第二个(嵌套的)注释,
  • 创建 state2
  • 打印 state2 以显示预期状态
  • 打印 state 以显示原始状态未修改

展开以下代码段,以在您自己的浏览器中验证结果-

  const无=象征 ()const isArray =Array.isArrayconst isObject = x =>对象(x)=== xconst提高= e =>{投掷}const identity = x =>Xconst append = x =>a =>[... a,x]const搜索=函数*(o = {},f =身份,路径= []){如果(!isObject(o))返回如果(f(o))屈服路径对于(Object.entries(o)的[const [k,v]])产量*搜索(v,f,[... path,k])}const searchById =(o = {},q = 0)=>搜索(o,({{id = 0})=> id === q)const transformAt =(o = {},[q = None,... path] = [],t =身份)=>q ===无?至):isObject(o)?对象分配(isArray(o)?[]:{}o,{[q]:transformAt(o [q],path,t)}):提高(错误("transformAt:无效路径"))const appendComment =(状态= {},parentId = 0,c = {})=>{for(searchById(state,parentId)的常数路径)返回transformAt( 状态,[...路径,注释"],附加(c))返回状态}const状态={个帖子:[{id:1,主题:主题A", 注释: []},{id:2,主题:主题B", 注释: []},{id:3,主题:主题C", 注释: []}],其他:[1,2,3]}const state1 =appendComment( 状态,2,{id:4,文字:不错的文章!",评论:[]})const state2 =appendComment(state1,4,{id:5,文字:我同意!",评论:[]})console.log("state2",JSON.stringify(state2,null,2))console.log(原始",JSON.stringify(state,null,2)) 


替代方案

上述技术与使用Scott提供的镜头的其他(出色)答案是平行的.值得注意的区别是,我们从目标对象的未知路径开始,找到路径,然后在发现的路径上转换状态.

这两个答案中的技术甚至可以结合起来. search 生成可用于创建 R.lensPath 的路径,然后我们可以使用 R.over 更新状态.

更高级别的技术正潜伏在拐角处.这源于一种理解,即编写诸如 transformAt 之类的函数相当复杂,并且很难正确地实现它们.问题的核心是我们的状态对象是普通的JS对象 {...} ,它不提供不变更新等功能.嵌套在那些对象中的我们使用具有相同问题的数组 [...] .

Object Array 这样的数据结构在设计时考虑了可能与您自己不匹配的无数注意事项.因此,您有能力设计自己的数据结构,以所需的方式运行.这是一个经常被忽视的编程领域,但是在我们尝试编写自己的程序之前,让我们先看看明智的人是怎么做到的.

一个示例 ImmutableJS 解决了这个 exact >问题.该库为您提供了数据结构以及在这些数据结构上运行的函数的集合,所有这些保证了不可变的行为.使用该库很方便-

   const append = x => a => //<-未使用 [... a,x] const {fromJS} =要求(不可变")const appendComment =(状态= {},parentId = 0,c = {})=>{for(searchById(state,parentId)的常数路径)返回transformAt(fromJS(state)//<-1.从JS到不可变,[...路径,注释"],list => list .push(c)//<-2.不可变的推送).toJS()//<-3.从不可变到JS返回状态}  

现在,我们编写 transformAt 并期望它将被赋予不变的结构-

   const isArray = //<-未使用 Array.isArray  const isObject =(x)=> //<-未使用对象(x)=== x const {Map,isCollection,get,set} =要求(不变")const transformAt =(o = Map()//<-空的不可变对象,[q = None,... path] = [],t =身份)=>q ===无?至):isCollection(o)//<-不可变的对象??设置//<-不可变的设置(oq,transformAt(get(o,q)//<-不可变的get, 小路t)):提高(错误("transformAt:无效路径")) 

希望我们可以开始将 transformAt 视为通用函数.ImmutableJS包含用于实现此目的的函数并非巧合, getIn setIn -

 <代码>  const无= //<-未使用符号() const raise = e => //<-未使用 {throw e} const {Map,setIn,getIn} =要求(不可变")const transformAt =(o = Map()//<-空的Map,路径= [],t =身份)=>setIn//<-由路径设置(o, 小路,t(getIn(o,path))//<-按路径获取) 

令我惊讶的是,甚至 transformAt 也被精确地实现为 updateIn -

   const identity = x => //<-未使用 x  const transformAt = //(o = Map()//<-未使用,路径= [] //,t =身份//)=> ... //const {fromJS,updateIn} =要求(不可变")const appendComment =(状态= {},parentId = 0,c = {})=>{for(searchById(state,parentId)的常数路径)return updateIn//<-通过路径的不可变更新(fromJS(州),[...路径,注释"],list => list .push(c)).toJS()返回状态}  

这是高级数据结构的课程.通过使用为不变操作设计的结构,我们降低了整个程序的整体复杂性.结果,该程序现在可以用少于30行的简单代码编写-

 <代码>////使用ImmutableJS完成实现//const {fromJS,updateIn} =要求(不可变")const搜索=函数*(o = {},f =身份,路径= []){如果(对象(o)!== o)返回如果(f(o))屈服路径对于(Object.entries(o)的[const [k,v]])产量*搜索(v,f,[... path,k])}const searchById =(o = {},q = 0)=>搜索(o,({{id = 0})=> id === q)const appendComment =(状态= {},parentId = 0,c = {})=>{for(searchById(state,parentId)的常数路径)返回updateIn(fromJS(州),[...路径,注释"],列表=>列出.push(c)).toJS()返回状态} 

ImmutableJS只是这些结构的一个可能的实现.存在许多其他方法,每种方法都有其独特的API和折衷方案.您可以从预制的库中进行选择,也可以自定义定制自己的数据结构以满足您的确切需求.无论哪种方式,希望您都能看到经过精心设计的数据结构所带来的好处,并且也许可以深入了解为什么首先发明了当今流行的结构.

展开下面的代码片段,以在浏览器中运行程序的ImmutableJS版本-

  const {fromJS,updateIn} =一成不变的const搜索=函数*(o = {},f =身份,路径= []){如果(对象(o)!== o)返回如果(f(o))屈服路径对于(Object.entries(o)的[const [k,v]])产量*搜索(v,f,[... path,k])}const searchById =(o = {},q = 0)=>搜索(o,({{id = 0})=> id === q)const appendComment =(状态= {},parentId = 0,c = {})=>{for(searchById(state,parentId)的常数路径)返回updateIn(fromJS(州),[...路径,'评论'],列表=>列出.push(c)).toJS()返回状态}const状态={个帖子:[{id:1,主题:主题A", 注释: []},{id:2,主题:主题B", 注释: []},{id:3,主题:主题C", 注释: []}],其他:[1,2,3]}const state1 =appendComment( 状态,2,{id:4,文字:不错的文章!",评论:[]})const state2 =appendComment(state1,4,{id:5,文字:我同意!",评论:[]})console.log("state2",JSON.stringify(state2,null,2))console.log(原始",JSON.stringify(state,null,2)) 

 < script src ="https://unpkg.com/immutable@4.0.0-rc.12/dist/immutable.js></script>  

I am currently learning functional programming using HyperappJS (V2) and RamdaJS. My first project is a simple blog app where users can comment on posts or other comments. The comments are represented as a tree structure.

My state looks something like this:

// state.js
export default {
    posts: [
        {
            topic: `Topic A`, 
            comments: []
        },
        {
            topic: `Topic B`, 
            comments: [
                {
                    text: `Comment`, 
                    comments: [ /* ... */ ]
                }
            ]
        },
        {
            topic: `Topic C`, 
            comments: []
        }
    ],
    otherstuff: ...
}

When the user wants to add a comment I pass the current tree item to my addComment-action. There I add the comment to the referenced item and return a new state object to trigger the view update.

So, currently I'm doing this and it's working fine:

// actions.js
import {concat} from 'ramda'   
export default {
    addComment: (state, args) => {
        args.item.comments = concat(
            args.item.comments, 
            [{text: args.text, comments: []}]
        )
        return {...state}
    }
}

My question: Is this approach correct? Is there any way to clean up this code and make it more functional? What I am looking for would be something like this:

addComment: (state, args) => ({
    ...state,
    posts: addCommentToReferencedPostItemAndReturnUpdatedPosts(args, state.posts)
})

解决方案

Here's an approach where we 1) locate the target object in your state tree, and then 2) transform the located object. Let's assume that your tree has some way to id the individual objects -

const state =
  { posts:
      [ { id: 1              // <-- id
        , topic: "Topic A"
        , comments: []
        }
      , { id: 2              // <-- id
        , topic: "Topic B"
        , comments: []
        }
      , { id: 3              // <-- id
        , topic: "Topic C"
        , comments: []
        }
      ]
  , otherstuff: [ 1, 2, 3 ]
  }


search

You could start by writing a generic search which yields the possible path(s) to a queried object -

const search = function* (o = {}, f = identity, path = [])
{ if (!isObject(o))
    return

  if (f (o))
    yield path

  for (const [ k, v ] of Object.entries(o))
    yield* search (v, f, [ ...path, k ])
}

Let's locate all objects where id is greater than 1 -

for (const path of search (state, ({ id = 0 }) => id > 1))
  console .log (path)

// [ "posts", "1" ]
// [ "posts", "2" ]

These "paths" point to objects in your state tree where the predicate, ({ id = 0 }) => id > 1), is true. Ie,

// [ "posts", "1" ]
state.posts[1] // { id: 2, topic: "Topic B", comments: [] }

// [ "posts", "2" ]
state.posts[2] // { id: 3, topic: "Topic C", comments: [] }

We will use search to write higher-order functions like searchById, which encodes our intentions more clearly -

const searchById = (o = {}, q = 0) =>
  search (o, ({ id = 0 }) => id === q)

for (const path of searchById(state, 2))
  console .log (path)

// [ "posts", "1" ]


transform

Next we can write transformAt which takes an input state object, o, a path, and a transformation function, t -

const None =
  Symbol ()

const transformAt =
  ( o = {}
  , [ q = None, ...path ] = []
  , t = identity
  ) =>
    q === None                                  // 1
      ? t (o)
  : isObject (o)                                // 2
      ? Object.assign 
          ( isArray (o) ? [] : {}
          , o
          , { [q]: transformAt (o[q], path, t) }
          )
  : raise (Error ("transformAt: invalid path")) // 3

These bullet points correspond to the numbered comments above -

  1. When the query, q, is None, the path has been exhausted and it's time to run the transformation, t, on the input object, o.
  2. Otherwise, by induction, q is not empty. If the input, o, is an object, using Object.assign create a new object where its new q property is a transform of its old q property, o[q].
  3. Otherwise, by induction, q is not empty and o is not an object. We cannot expect to lookup q on a non-object, therefore raise an error to signal to that transformAt was given an invalid path.

Now we can easily write appendComment which takes an input, state, a comment's id, parentId, and a new comment, c -

const append = x => a =>
  [ ...a, x ]

const appendComment = (state = {}, parentId = 0, c = {}) =>
{ for (const path of searchById(state, parentId))
    return transformAt   // <-- only transform first; return
      ( state
      , [ ...path, "comments" ]
      , append (c)
      )
  return state // <-- if no search result, return unmodified state
}

Recall search generates all possible paths to where the predicate query returns true. You have to make a choice how you will handle the scenario where a query returns more than one result. Consider data like -

const otherState =
  { posts: [ { type: "post", id: 1, ... }, ... ]
  , images: [ { type: "image", id: 1, ... }, ... ]
  }

Using searchById(otherState, 1) would get two objects where id = 1. In appendComment we choose only to modify the first match. It's possible to modify all the search results, if we wanted -

// but don't actually do this
const appendComment = (state = {}, parentId = 0, c = {}) =>
  Array
    .from (searchById (state, parentId)) // <-- all results
    .reduce
        ( (r, path) =>
            transformAt  // <-- transform each
              ( r
              , [ ...path, "comments" ]
              , append (c)
              )
        , state // <-- init state
        )

But in this scenario, we probably don't want duplicate comments in our app. Any querying function like search may return zero, one, or more results and you have to decide how your program responds in each scenario.


put it together

Here are the remaining dependencies -

const isArray =
  Array.isArray

const isObject = x =>
  Object (x) === x

const raise = e =>
  { throw e }

const identity = x =>
  x

Let's append our first new comment to id = 2, "Topic B" -

const state1 =
  appendComment
    ( state
    , 2
    , { id: 4, text: "nice article!", comments: [] }  
    )

Our first state revision, state1, will be -

{ posts:
    [ { id: 1
      , topic: "Topic A"
      , comments: []
      }
    , { id: 2
      , topic: "Topic B"
      , comments:
          [ { id: 4                     //
            , text: "nice article!"     // <-- newly-added
            , comments: []              //     comment
            }                           //
          ]
      }
    , { id: 3
      , topic: "Topic C"
      , comments: []
      }
    ]
, otherstuff: [ 1, 2, 3 ]
}

And we'll append another comment, nested on that one -

const state2 =
  appendComment
    ( state
    , 4  // <-- id of our last comment
    , { id: 5, text: "i agree!", comments: [] }  
    )

This second revision, state2, will be -

{ posts:
    [ { id: 1, ...}
    , { id: 2
      , topic: "Topic B"
      , comments:
          [ { id: 4
            , text: "nice article!"
            , comments:
                [ { id: 5             //     nested
                  , text: "i agree!"  // <-- comment
                  , comments: []      //     added
                  }                   //
                ]
            }
          ]
      }
    , { id: 3, ... }
    ]
, ...
}


code demonstration

In this demo we will,

  • create state1 by modifying state to add the first comment
  • create state2 by modifying state1 to add the second (nested) comment
  • print state2 to show the expected state
  • print state to show that the original state is not modified

Expand the snippet below to verify the results in your own browser -

const None = 
  Symbol ()

const isArray =
  Array.isArray

const isObject = x =>
  Object (x) === x

const raise = e =>
  { throw e }

const identity = x =>
  x

const append = x => a =>
  [ ...a, x ]

const search = function* (o = {}, f = identity, path = [])
{ if (!isObject(o))
    return
  
  if (f (o))
    yield path
  
  for (const [ k, v ] of Object.entries(o))
    yield* search (v, f, [ ...path, k ])
}

const searchById = (o = {}, q = 0) =>
  search (o, ({ id = 0 }) => id === q)

const transformAt =
  ( o = {}
  , [ q = None, ...path ] = []
  , t = identity
  ) =>
    q === None
      ? t (o)
  : isObject (o)
      ? Object.assign
          ( isArray (o) ? [] : {}
          , o
          , { [q]: transformAt (o[q], path, t) }
          )
  : raise (Error ("transformAt: invalid path"))

const appendComment = (state = {}, parentId = 0, c = {}) =>
{ for (const path of searchById(state, parentId))
    return transformAt
      ( state
      , [ ...path, "comments" ]
      , append (c)
      )
  return state
}

const state =
  { posts:
      [ { id: 1
        , topic: "Topic A"
        , comments: []
        }
      , { id: 2
        , topic: "Topic B"
        , comments: []
        }
      , { id: 3
        , topic: "Topic C"
        , comments: []
        }
      ]
  , otherstuff: [ 1, 2, 3 ]
  }

const state1 =
  appendComment
    ( state
    , 2
    , { id: 4, text: "nice article!", comments: [] }  
    )

const state2 =
  appendComment
    ( state1
    , 4
    , { id: 5, text: "i agree!", comments: [] }  
    )

console.log("state2", JSON.stringify(state2, null, 2))
console.log("original", JSON.stringify(state, null, 2))


alternate alternative

The techniques described above are parallel to the other (excellent) answer using lenses provided by Scott. The notable difference here is we start with an unknown path to the target object, find the path, then transform the state at the discovered path.

The techniques in these two answers could even be combined. search yields paths that could be used to create R.lensPath and then we could update the state using R.over.

And a higher-level technique is lurking right around the corner. This one comes from the understanding that writing functions like transformAt is reasonably complex and it's difficult to get them right. At the heart of the problem, our state object is a plain JS object, { ... }, which offers no such feature as immutable updates. Nested within those object we use arrays, [ ... ], that have the same issue.

Data structures like Object and Array were designed with countless considerations that may not match your own. It is for this reason why you have the ability to design your own data structures that behave the way you want. This is an often overlooked area of programming, but before we jump in and try to write our own, let's see how the Wise Ones before us did it.

One example, ImmutableJS, solves this exact problem. The library gives you a collection of data structures as well as functions that operate on those data structures, all of which guarantee immutable behaviour. Using the library is convenient -

const append = x => a => // <-- unused
  [ ...a, x ]

const { fromJS } =
  require ("immutable")

const appendComment = (state = {}, parentId = 0, c = {}) =>
{ for (const path of searchById(state, parentId))
    return transformAt
      ( fromJS (state) // <-- 1. from JS to immutable
      , [ ...path, "comments" ]
      , list => list .push (c) // <-- 2. immutable push
      )
      .toJS () // <-- 3. from immutable to JS
  return state
}

Now we write transformAt with the expectation that it will be given an immutable structure -

const isArray = // <-- unused
  Array.isArray

const isObject = (x) => // <-- unused
  Object (x) === x

const { Map, isCollection, get, set } =
  require ("immutable")

const transformAt =
  ( o = Map ()             // <-- empty immutable object
  , [ q = None, ...path ] = []
  , t = identity
  ) =>
    q === None
      ? t (o)
  : isCollection (o)       // <-- immutable object?
      ? set                // <-- immutable set
          ( o
          , q
          , transformAt
              ( get (o, q) // <-- immutable get
              , path
              , t
              )
          )
  : raise (Error ("transformAt: invalid path"))

Hopefully we can begin to see transformAt as a generic function. It is not coincidence that ImmutableJS includes functions to do exactly this, getIn and setIn -

const None = // <-- unused
  Symbol ()

const raise = e => // <-- unused
  { throw e }

const { Map, setIn, getIn } =
  require ("immutable")

const transformAt =
  ( o = Map () // <-- empty Map
  , path = []
  , t = identity
  ) =>
    setIn // <-- set by path
      ( o
      , path
      , t (getIn (o, path)) // <-- get by path
      )

To my surprise, even transformAt is implemented exactly as updateIn -

const identity = x => // <-- unused
  x

const transformAt =  //
  ( o = Map ()       // <-- unused
  , path = []        //   
  , t = identity     // 
  ) => ...           //

const { fromJS, updateIn } =
  require ("immutable")

const appendComment = (state = {}, parentId = 0, c = {}) =>
{ for (const path of searchById(state, parentId))
    return updateIn // <-- immutable update by path
      ( fromJS (state)
      , [ ...path, "comments" ]
      , list => list .push (c)
      )
      .toJS ()
  return state
}

This the lesson of higher-level data structures. By using structures designed for immutable operations, we reduce the overall complexity of our entire program. As a result, the program can now be written in less than 30 lines of straightforward code -

//
// complete implementation using ImmutableJS
//
const { fromJS, updateIn } =
  require ("immutable")

const search = function* (o = {}, f = identity, path = [])
{ if (Object (o) !== o)
    return

  if (f (o))
    yield path

  for (const [ k, v ] of Object.entries(o))
    yield* search (v, f, [ ...path, k ])
}

const searchById = (o = {}, q = 0) =>
  search (o, ({ id = 0 }) => id === q)

const appendComment = (state = {}, parentId = 0, c = {}) =>
{ for (const path of searchById(state, parentId))
    return updateIn
      ( fromJS (state)
      , [ ...path, "comments" ]
      , list => list .push (c)
      )
      .toJS ()
  return state
}

ImmutableJS is just one possible implementation of these structures. Many others exist, each with their unique APIs and trade-offs. You can pick from a pre-made library or you can custom tailor your own data structures to meet your exact needs. Either way, hopefully you can see the benefits provided by well-designed data structures and perhaps gain insight on why popular structures of today were invented in the first place.

Expand the snippet below to run the ImmutableJS version of the program in your browser -

const { fromJS, updateIn } =
  Immutable

const search = function* (o = {}, f = identity, path = [])
{ if (Object (o) !== o)
    return
  
  if (f (o))
    yield path
  
  for (const [ k, v ] of Object.entries(o))
    yield* search (v, f, [ ...path, k ])
}

const searchById = (o = {}, q = 0) =>
  search (o, ({ id = 0 }) => id === q)

const appendComment = (state = {}, parentId = 0, c = {}) =>
{ for (const path of searchById(state, parentId))
    return updateIn
      ( fromJS (state)
      , [ ...path, 'comments' ]
      , list => list .push (c)
      )
      .toJS ()
  return state
}

const state =
  { posts:
      [ { id: 1
        , topic: 'Topic A'
        , comments: []
        }
      , { id: 2
        , topic: 'Topic B'
        , comments: []
        }
      , { id: 3
        , topic: 'Topic C'
        , comments: []
        }
      ]
  , otherstuff: [ 1, 2, 3 ]
  }

const state1 =
  appendComment
    ( state
    , 2
    , { id: 4, text: "nice article!", comments: [] }  
    )

const state2 =
  appendComment
    ( state1
    , 4
    , { id: 5, text: "i agree!", comments: [] }  
    )

console.log("state2", JSON.stringify(state2, null, 2))
console.log("original", JSON.stringify(state, null, 2))

<script src="https://unpkg.com/immutable@4.0.0-rc.12/dist/immutable.js"></script>

这篇关于通过引用更新树结构中的项目并返回更新的树结构的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆