更新递归类型的值 - elm lang
Update value in a recursive type - elm lang
我有一个树状文本节点结构,它可能有另一个文本节点作为子节点,我需要更新其中的一个值。更新位于该树深处(或根本不在该树中)的文本节点的最简单方法是什么?
在非不可变语言中,我会简单地更改该项目的值,仅此而已,但在像 Elm 这样的不可变语言中,这非常棘手。
type alias Item =
{ id: String
, text: String
, children: ChildItems
}
type ChildItems = ChildItems (List Item)
type alias Model =
{ rootItem: Item
}
updateItem: Item -> Item -> Item
updateItem: rootItem item =
-- TODO
...
update model =
case msg of
UpdateItem item updatedText ->
let
updatedItem = { item | text = updatedText }
in
({ model | rootItem = (updateItem model.rootItem updatedItem) }, Cmd.none)
这是我想出来的
updateItem: Item.Item -> Item.Item -> Item.Item
updateItem rootItem updatedItem =
if rootItem.id == updatedItem.id then
updatedItem
else
case rootItem.children of
Item.ChildItem [] ->
rootItem
Item.ChildItem children ->
let
updatedChildren =
case children of
[] ->
[]
children ->
List.map (\item ->
updateItem rootItem item) children
in
{ rootItem | children = Item.ChildItem updatedChildren }
但我遇到 Maximum call stack size exceeded
错误
你得到堆栈溢出的原因是因为你返回 rootItem
而不是 Item.ChildItems []
情况下的 []
。
我将稍微修改一下您的代码,因为我们可以提取一些常见的模式。首先,让我们采用底层 tree-ish 结构并使其更通用,以便它可以适应任何类型的事物,而不仅仅是 Item
:
type Node a
= Node a (List (Node a))
这给了我们一个结构,它总是有一个根节点,并且可以有任意数量的 children,每个人也可以有任意数量的 children.
如果我们考虑您要使用的算法,我们可以推断出一个熟悉的模式。您有一个包含多个项目的结构,并且您想要一个访问每个项目并有选择地更改它的算法。这听起来很像 List.map
。这是一个很常见的习语,调用我们的广义函数 map
:
是个好主意
map : (a -> b) -> Node a -> Node b
map f (Node item children) =
Node (f item) (List.map (map f) children)
(旁注:我们刚刚偶然发现 functors!)
由于我采用了 children 的想法并将其放入 Node
类型,我们需要像这样修改别名 Item
:
type alias Item =
{ id: String
, text: String
}
现在我们有了一个 Item
,如果 id
匹配某个值,那么有一个可以更新它的函数会很有帮助。由于以后你可能会有更多的更新功能要执行,所以最好将查找和 ID 匹配部分与你实际要执行的功能分开:
updateByID : String -> (Item -> Item) -> Item -> Item
updateByID id f item =
if item.id == id then
f item
else
item
现在要在树中的任何位置对与 id 匹配的项目执行更新,您可以简单地执行以下操作:
map (updateByID "someID" (\x -> { x | text = "Woohoo!" })) rootNode
我有一个树状文本节点结构,它可能有另一个文本节点作为子节点,我需要更新其中的一个值。更新位于该树深处(或根本不在该树中)的文本节点的最简单方法是什么?
在非不可变语言中,我会简单地更改该项目的值,仅此而已,但在像 Elm 这样的不可变语言中,这非常棘手。
type alias Item =
{ id: String
, text: String
, children: ChildItems
}
type ChildItems = ChildItems (List Item)
type alias Model =
{ rootItem: Item
}
updateItem: Item -> Item -> Item
updateItem: rootItem item =
-- TODO
...
update model =
case msg of
UpdateItem item updatedText ->
let
updatedItem = { item | text = updatedText }
in
({ model | rootItem = (updateItem model.rootItem updatedItem) }, Cmd.none)
这是我想出来的
updateItem: Item.Item -> Item.Item -> Item.Item
updateItem rootItem updatedItem =
if rootItem.id == updatedItem.id then
updatedItem
else
case rootItem.children of
Item.ChildItem [] ->
rootItem
Item.ChildItem children ->
let
updatedChildren =
case children of
[] ->
[]
children ->
List.map (\item ->
updateItem rootItem item) children
in
{ rootItem | children = Item.ChildItem updatedChildren }
但我遇到 Maximum call stack size exceeded
错误
你得到堆栈溢出的原因是因为你返回 rootItem
而不是 Item.ChildItems []
情况下的 []
。
我将稍微修改一下您的代码,因为我们可以提取一些常见的模式。首先,让我们采用底层 tree-ish 结构并使其更通用,以便它可以适应任何类型的事物,而不仅仅是 Item
:
type Node a
= Node a (List (Node a))
这给了我们一个结构,它总是有一个根节点,并且可以有任意数量的 children,每个人也可以有任意数量的 children.
如果我们考虑您要使用的算法,我们可以推断出一个熟悉的模式。您有一个包含多个项目的结构,并且您想要一个访问每个项目并有选择地更改它的算法。这听起来很像 List.map
。这是一个很常见的习语,调用我们的广义函数 map
:
map : (a -> b) -> Node a -> Node b
map f (Node item children) =
Node (f item) (List.map (map f) children)
(旁注:我们刚刚偶然发现 functors!)
由于我采用了 children 的想法并将其放入 Node
类型,我们需要像这样修改别名 Item
:
type alias Item =
{ id: String
, text: String
}
现在我们有了一个 Item
,如果 id
匹配某个值,那么有一个可以更新它的函数会很有帮助。由于以后你可能会有更多的更新功能要执行,所以最好将查找和 ID 匹配部分与你实际要执行的功能分开:
updateByID : String -> (Item -> Item) -> Item -> Item
updateByID id f item =
if item.id == id then
f item
else
item
现在要在树中的任何位置对与 id 匹配的项目执行更新,您可以简单地执行以下操作:
map (updateByID "someID" (\x -> { x | text = "Woohoo!" })) rootNode