Thanks for your elaborate examples, much appreciated.
Still I don’t quite make sense of this all. Quoting from the docs:
If a key is a function, the function will be invoked passing three arguments, the operation (:get_and_update), the data to be accessed, and a function to be invoked next. This means get_and_update_in/3 can be extended to provide custom lookups. The downside is that functions cannot be stored as keys in the accessed data structures.
It doesn’t explain what the operation parameter is about, what kind of “next” function we’re looking at, or what structure the expected return value should conform to.
What is the purpose or responsiblity of this function? Is it kind of a selector, something that would be equivalent to a map key, like :age? That seems to be the case, as Access.at
, Access.all
seem to be from that category. So if that’s true, how does the ‘selector function’ (for lack of it’s actual name) is supposed to treat each kind? Should the next
function be invoked for the matches only? That seems to be what you are doing.
After spending still more time it’s dawning on my what the ideas are.
Basically there are two very different use cases, :get and :get_and_update.
We are navigating a tree structure by specifying a path. The path is being represented as a list of keys. Each key can also be a function which adheres to a specific contract (signature?).
There are two contracts, as there are two quite distinct use-cases, the simpler nested access, and nested update. The use :get
and :get_and_update
respectively.
:get
just fetches some subset (of possibly one), but potentiall the whole set for values. It’s used for the nested access case. It is expected to return a single result, but that can be a collection, or a single element, and anything in between. It can be the subset that fits a certain condition of a predicate, e.g. to name one way I was thinking of.
Now we look at the update case. Keeping in mind that functional data structures need to handled in a copy-on-write style in order to update them. So when traversing them for update, we need to find the nodes that we follow until we reach the node to replace, and the return back up the call stack and structure in order to replace the newly updated node from the deeper position into the exiting structure. This needs to happen all the way up to the root. And that’s the reason we need two copies of the data on the stack, the updated subset, as well the original structure. On the way down the path we need to put the original data onto the stack, so that we have it for modification when traveling back up the callstack. At the same time we need the updated subset, because we need to combine the previous structure and the updated subset into the new version of the data structure.
:get_and_update
is supporting the copy-on-write use case. Only the subset that’s supposed to be modified is fed into the next
function.
Still, I’m not quite sure what the next function is. Is it the the function corresponding to the next key element?
FWIW, I had the idea to have path element functions for
- key-value pairing
- predicate functions
And I’ve managed to implement those. Check out this code, implementation and tests:
defmodule NestedUpdate do
def find(key, value) do
fn :get_and_update, data, next ->
Enum.map(data, fn(x) ->
if Map.get(x, key, :miss) == value do
next.(x)
else
{x, x}
end
end) |> :lists.unzip
end
end
def find(pred) when is_function(pred, 1) do
fn :get_and_update, data, next ->
Enum.map(data, fn(x) ->
if pred.(x) do
next.(x)
else
{x, x}
end
end) |> :lists.unzip
end
end
end
defmodule NestedUpdatedFindTest do
use ExUnit.Case
import Access, only: [key: 1, all: 0]
import NestedUpdate, only: [find: 1, find: 2]
defmodule Note do
defstruct [:id, :text, :sections]
end
defp data() do
%{
name: "Tom",
notes: [
%Note{id: 1, sections: [
%Note{id: 1, text: "Note 1"},
%Note{id: 2, text: "Note 2"},
%Note{id: 3, text: "Note 3"}
]},
%Note{id: 2, sections: [
%Note{id: 1, text: "Note 1"},
%Note{id: 2, text: "Note 2"},
%Note{id: 3, text: "Note 3"}
]},
%Note{id: 3, sections: [
%Note{id: 1, text: "Note 1"},
%Note{id: 2, text: "Note 2"},
%Note{id: 3, text: "Note 3"}
]}
]
}
end
setup do
[data: data()]
end
describe "find/1" do
test "update element identified by predicate as middle path element", %{data: data} do
updated = update_in(data, [
key(:notes),
find(&(&1.id == 1)),
key(:sections),
find(&(&1.id == 2))
], fn
note -> %{note | text: "UPDATE"}
end)
assert %{name: "Tom",
notes: [
%Note{id: 1,
sections: [
%Note{id: 1, text: "Note 1"},
%Note{id: 2, text: "UPDATE"},
%Note{id: 3, text: "Note 3"}
]},
%Note{id: 2,
sections: [
%Note{id: 1, text: "Note 1"},
%Note{id: 2, text: "Note 2"},
%Note{id: 3, text: "Note 3"}
]},
%Note{id: 3,
sections: [
%Note{id: 1, text: "Note 1"},
%Note{id: 2, text: "Note 2"},
%Note{id: 3, text: "Note 3"}
]
}
]
} = updated
end
end
describe "find/2" do
test "update multiple elements", %{data: data} do
updated = update_in(data, [key(:notes), all(), key(:sections), find(:id, 3)], fn
note -> %{note | text: "UPDATE"}
end)
assert %{name: "Tom",
notes: [
%Note{id: 1,
sections: [
%Note{id: 1, text: "Note 1"},
%Note{id: 2, text: "Note 2"},
%Note{id: 3, text: "UPDATE"}
]},
%Note{id: 2,
sections: [
%Note{id: 1, text: "Note 1"},
%Note{id: 2, text: "Note 2"},
%Note{id: 3, text: "UPDATE"}
]},
%Note{id: 3,
sections: [
%Note{id: 1, text: "Note 1"},
%Note{id: 2, text: "Note 2"},
%Note{id: 3, text: "UPDATE"}
]
}
]
} = updated
end
test "update element identified by id as middle path element", %{data: data} do
updated = update_in(data, [key(:notes), find(:id, 2), key(:sections), find(:id, 3)], fn
note -> %{note | text: "UPDATE"}
end)
assert %{name: "Tom",
notes: [
%Note{id: 1,
sections: [
%Note{id: 1, text: "Note 1"},
%Note{id: 2, text: "Note 2"},
%Note{id: 3, text: "Note 3"}
]},
%Note{id: 2,
sections: [
%Note{id: 1, text: "Note 1"},
%Note{id: 2, text: "Note 2"},
%Note{id: 3, text: "UPDATE"}
]},
%Note{id: 3,
sections: [
%Note{id: 1, text: "Note 1"},
%Note{id: 2, text: "Note 2"},
%Note{id: 3, text: "Note 3"}
]
}
]
} = updated
end
end
end
It would be cool to get feedback on any obvious problems, or hints at how to be smarter / simpler / more idiomatic here.