9

I have a tree like structure of text nodes that might have another text nodes as children, and I need to update one value in it. What's the easiest way to update text node that's somewhere deep in that tree (or it is not in that tree at all)?

In a non-immutable language, I would simply change a value of that item, and that's it, but it's quite tricky in an immutable language like 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)

this is what I came up with

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 }

but I'm getting an Maximum call stack size exceeded error

ondrej
  • 967
  • 7
  • 26
  • On data manipulation patterns I highly recommend to check Haskell resources. Elm syntax is very close to that of Haskell and the Haskell community has devoted huge amounts of time on these matters. Look for higher order functions and functors. – Titou May 25 '18 at 07:51

1 Answers1

19

The reason you're getting the stack overflow is because you are returning rootItem instead of [] in the Item.ChildItems [] case.

I'm going to modify your code a bit because there are some common patterns we can pull out. First off, let's take the underlying tree-ish structure and make that more generic so that it could fit any type of thing, not just Item:

type Node a
  = Node a (List (Node a))

That gives us a structure that always has a root node and may have any number of children, each of who may also have any number of children.

If we think about the algorithm you were going for, we can extrapolate a familiar pattern. You have a structure with multiple items and you want an algorithm that visits each item and optionally changes it. That sounds a lot like List.map. It's such a common idiom that it's a good idea to call our generalized function map:

map : (a -> b) -> Node a -> Node b
map f (Node item children) =
  Node (f item) (List.map (map f) children)

(Side note: We've just stumbled into functors!)

Since I've taken the idea of children and placed it into the Node type, we need to modify the alias Item like so:

type alias Item =
  { id: String
  , text: String
  }

Now that we have an Item, it will be helpful to have a function that can update it if the id matches a certain value. Since in the future, you may have more update functions you want to perform, it's best to keep the lookup and ID matching portion separate from the function you actually want to perform:

updateByID : String -> (Item -> Item) -> Item -> Item
updateByID id f item =
  if item.id == id then
    f item
  else
    item

Now to perform an update on an item matching the id, anywhere within the tree, you can simply do this:

map (updateByID "someID" (\x -> { x | text = "Woohoo!" })) rootNode
Chad Gilbert
  • 36,115
  • 4
  • 89
  • 97
  • Excellent answer! You've made my day :) – ondrej Oct 06 '16 at 04:28
  • How would I update the nested list of Items using this pattern? The problem I'm hitting is that the record update expression is designed to be the return expression of any function it appears in.I n a function designed to create a new Item, the return expression should be the new item, not the item's parent's list of children, which is what I need to update. Do we have scripture on how to deal with recursive (aka "tree") structures? I've seen instructions on making the item a type alias and the collection a type; you offer a different protocol. – Richard Haven Dec 21 '16 at 06:47
  • @RichardHaven that is handled by the map function - notice that in its body it creates a Node made up of the item from the Node it was given, possibly modified by the function, and the children from the Node it was given, each also possibly modified by the function. Ultimately when feeding updateById into map it will recursively build up the tree of nodes using the same pieces, except for the one item that has been changed. – David Mason May 13 '17 at 15:47
  • What an absolutely awesome answer. – swelet Oct 18 '17 at 21:04