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

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

问题描述

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

我的状态如下所示:

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

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

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

//actions.js从 'ramda' 导入 {concat}导出默认{addComment: (state, args) =>{args.item.comments = concat(args.item.comments,[{文本:args.text,评论:[]}])返回 {...状态}}}

我的问题:这种方法是否正确?有什么办法可以清理这段代码并使其功能更强大吗?我正在寻找的是这样的:

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 = identity, path = []){ 如果 (!isObject(o))返回如果 (f (o))屈服路径for (const [ k, v ] of Object.entries(o))yield* 搜索 (v, f, [ ...path, k ])}

让我们定位id大于1的所有对象-

for (const path of search (state, ({ id = 0 }) => id > 1))控制台 .log(路径)//[ "posts", "1" ]//[ "posts", "2" ]

这些路径"指向 state 树中的对象,其中谓词 ({ id = 0 }) =>身份证号1),是真的.即,

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

我们将使用search来编写像searchById这样的高阶函数,它更清楚地编码了我们的意图-

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

<小时>

转换

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

const None =象征 ()const 变换 =( o = {}, [ q = 无, ...路径] = [], t = 身份) =>q === 无//1?到): isObject (o)//2?对象分配( isArray (o) ? [] : {}, o, { [q]: transformAt (o[q], path, t) }): raise (Error ("transformAt: invalid path"))//3

这些要点对应于上面编号的评论 -

  1. 当查询 qNone 时,路径已用完,是时候在路径上运行转换 t输入对象,o.
  2. 否则,通过归纳q为空.如果输入 o 是一个对象,则使用 Object.assign 创建一个新对象,其中它的新 q 属性是其旧属性的转换q 属性,o[q].
  3. 否则,通过归纳q not 为空,o not> 一个物体.我们不能指望在一个非对象上查找 q,因此 raise 一个错误来表明 transformAt 被赋予了一个无效的路径.

现在我们可以很容易地编写appendComment,它接受一个输入,state,一个评论的id,parentId,和一个新的评论,c -

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

Recall search 生成谓词查询返回 true 的所有 可能路径.您必须选择如何处理查询返回多个结果的场景.考虑像这样的数据 -

const otherState ={ 帖子: [ { 类型: "帖子", id: 1, ... }, ... ], 图像: [ { 类型: "图像", id: 1, ... }, ... ]}

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

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

但在这种情况下,我们可能不希望在我们的应用中出现重复的评论.search 之类的任何查询函数都可能返回零个、一个或多个结果,必须决定您的程序在每种情况下的响应方式.><小时>

把它放在一起

这里是剩余的依赖项 -

const isArray =数组.isArrayconst isObject = x =>对象 (x) === xconst raise = e =>{扔e}const 身份 = x =>X

让我们将第一条新评论附加到 id = 2, 主题 B" -

const state1 =附加注释( 状态, 2, { id: 4, text: "好文章!", 评论: [] })

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

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

我们将附加另一条评论,嵌套在该评论上 -

const state2 =附加注释( 状态, 4//<-- 我们最后一条评论的id, { id: 5, text: "我同意!", 评论: [] })

第二次修订,state2,将 -

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

<小时>

代码演示

在这个演示中,我们将,

  • 通过修改state添加第一条注释来创建state1
  • 通过修改 state1 添加第二个(嵌套)注释来创建 state2
  • 打印 state2 以显示预期状态
  • 打印state,表示原始状态没有被修改

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

const None =象征 ()const isArray =数组.isArrayconst isObject = x =>对象 (x) === xconst raise = e =>{扔e}const 身份 = x =>Xconst append = x =>a =>[ ...a, x ]const search = function* (o = {}, f = identity, path = []){ 如果 (!isObject(o))返回如果 (f (o))屈服路径for (const [ k, v ] of Object.entries(o))yield* 搜索 (v, f, [ ...path, k ])}const searchById = (o = {}, q = 0) =>搜索 (o, ({ id = 0 }) => id === q)const 变换 =( o = {}, [ q = 无, ...路径] = [], t = 身份) =>q === 无?到): isObject (o)?对象分配( isArray (o) ? [] : {}, o, { [q]: transformAt (o[q], path, t) }):引发(错误(transformAt:无效路径"))const appendComment = (state = {}, parentId = 0, c = {}) =>{ for (const path of searchById(state, parentId))返回变换( 状态, [ ...路径, 评论"], 附加 (c))返回状态}常量状态 ={ 帖子:[ { id: 1,话题:话题A", 评论: []}, { id: 2,话题:话题B", 评论: []}, { id: 3,话题:话题C", 评论: []}], 其他: [ 1, 2, 3 ]}常量状态1 =附加注释( 状态, 2, { id: 4, text: "好文章!", 评论: [] })常量状态2 =附加注释( 状态 1, 4, { id: 5, text: "我同意!", 评论: [] })console.log("state2", JSON.stringify(state2, null, 2))console.log("original", JSON.stringify(state, null, 2))

<小时>

替代方案

上述技术与使用 Scott 提供的镜头的其他(优秀)答案平行.这里的显着区别是我们从一个未知路径到目标对象开始,找到路径,然后在发现的路径上转换状态.

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

一种更高级的技术就在眼前.这来自于这样一种理解,即编写像 transformAt 这样的函数相当复杂,而且很难把它们弄对.问题的核心是,我们的状态对象是一个普通的 JS 对象,{ ... },它不提供诸如不可变更新之类的功能.嵌套在这些对象中,我们使用数组 [ ... ],它们具有相同的问题.

ObjectArray 等数据结构的设计考虑了无数可能与您不匹配的考虑因素.正是由于这个原因,您才有能力设计自己的数据结构,使其按照您想要的方式运行.这是一个经常被忽视的编程领域,但在我们开始尝试自己编写之前,让我们看看我们之前的智者是如何做到的.

一个例子,ImmutableJS,解决了这个精确 问题.该库为您提供了一组数据结构以及对这些数据结构进行操作的函数,所有这些都保证了不可变行为.使用库很方便-

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

现在我们编写 transformAt 并期望它会被赋予一个不可变的结构 -

const isArray =//<-- 未使用Array.isArrayconst isObject = (x) =>//<-- 未使用对象(x) === xconst { Map, isCollection, get, set } =要求(不可变")const 变换 =( o = Map()//<-- 空的不可变对象, [ q = 无, ...路径] = [], t = 身份) =>q === 无?到):isCollection (o)//<-- 不可变对象??set//<-- 不可变集合( o, q, 变换( get (o, q)//<-- 不可变的 get, 小路, t)): raise (Error ("transformAt: invalid path"))

希望我们可以开始将 transformAt 视为一个通用函数.ImmutableJS 包含执行此操作的函数并非巧合,getInsetIn -

const None =//<-- 未使用符号()const raise = e =>//<-- 未使用{ throw e }const { 映射,setIn,getIn } =要求(不可变")const 变换 =( o = Map()//<-- 空映射, 路径 = [], t = 身份) =>setIn//<-- 按路径设置( o, 小路, t (getIn (o, path))//<-- 通过路径获取)

令我惊讶的是,即使 transformAt完全实现为 updateIn -

const identity = x =>//<-- 未使用xconst transformAt = //( o = Map()//<-- 未使用, path = []//, t = 身份//) => ...//const { fromJS, updateIn } =要求(不可变")const appendComment = (state = {}, parentId = 0, c = {}) =>{ for (const path of searchById(state, parentId))return updateIn//<-- 按路径不可变的更新(来自JS(状态), [ ...路径, 评论"], 列表 => 列表 .push (c)).toJS ()返回状态}

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

////使用 ImmutableJS 完成实现//const { fromJS, updateIn } =要求(不可变")const search = function* (o = {}, f = identity, path = []){如果(对象(o)!== o)返回如果 (f (o))屈服路径for (const [ k, v ] of Object.entries(o))yield* 搜索 (v, f, [ ...path, k ])}const searchById = (o = {}, q = 0) =>搜索 (o, ({ id = 0 }) => id === q)const appendComment = (state = {}, parentId = 0, c = {}) =>{ for (const path of searchById(state, parentId))返回更新输入(来自JS(状态), [ ...路径, 评论"], 列表 =>列表 .push (c)).toJS ()返回状态}

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

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

const { fromJS, updateIn } =不可变const search = function* (o = {}, f = identity, path = []){如果(对象(o)!== o)返回如果 (f (o))屈服路径for (const [ k, v ] of Object.entries(o))yield* 搜索 (v, f, [ ...path, k ])}const searchById = (o = {}, q = 0) =>搜索 (o, ({ id = 0 }) => id === q)const appendComment = (state = {}, parentId = 0, c = {}) =>{ for (const path of searchById(state, parentId))返回更新输入(来自JS(状态), [ ...路径, '评论' ], 列表 =>列表 .push (c)).toJS ()返回状态}常量状态 ={ 帖子:[ { id: 1, 主题: '主题 A', 评论: []}, { id: 2, 主题: '主题 B', 评论: []}, { id: 3, 主题: '主题 C', 评论: []}], 其他: [ 1, 2, 3 ]}常量状态1 =附加注释( 状态, 2, { id: 4, text: "好文章!", 评论: [] })常量状态2 =附加注释( 状态 1, 4, { id: 5, text: "我同意!", 评论: [] })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>

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天全站免登陆