defmodule Foo do
def foo do
state = %{state | a: a(state)}
state = %{state | b: b(state)}
state = %{state | c: c(state)}
state
end
def a(s)…
def b(s)…
def c(s)…
end
Yeah that’s definitely an option. But I need to refactor quite a bit for that because the functions are used in other places as well that don’t need the full state. So I was first looking at some other options.
I see kinda XY Problem here to begin with. Functions retrieving the partial state from the full state could be either trivial wrappers for destructuring (def a(%{a: value}), do: value) or in-place calculations of some additional values based on the whole state. From your comments, I understood we are dealing with the latter.
That said, you potentially have inconsistencies in the state (when a bare a(state) gets called without updating the state.) The arguably best property of Erlang/Elixir (of the actor model in general) is its proven responsibility to keep the state consistent no matter what. That basically suggests to calculate changes on update, store them within the state, and retrieve them with a bare dot notation.
I understand that the refactor involving changes in many places is something you want to avoid, but I’d better refactor the state to keep the value you are to retrieve later and amend it in the single place in the way which does not depend on the call order (your example does,) and does not use readers.
Let me check the requirements: You want to update a map repetedly, specifying the key, and a function that has the same name as the key, where each function accepts the full state and returns the new value for the corresponding key. Each update should use the latest version of the state.
def foo do
state
|> my_update(:a, &a/1)
|> my_update(:b, &b/1)
|> my_update(:c, &c/1)
end
You could even go further an make a macro that ensures the key and function name are the same, but that’s probably making things more confusing rather than simpler.
In the end I went ahead and refactored the code and make each function have a state → state signature. This cleans things up quite a bit.
I’m still thinking about the code though because the correctness obviously relies on the order of execution of the functions and that is quite implicit in the current implementation. I don’t have good solutions for that. So while it works it feels a bit brittle. But that’s probably my bias
defmodule Foo do
defmacro black_magic(state, keyfun) do
fun = Macro.var(keyfun, __MODULE__)
quote do
%{unquote(state) | unquote(keyfun) => unquote(__MODULE__).unquote(fun)(unquote(state))}
end
end
def foo do
%{a: "A", b: "B", c: "C"}
|> black_magic(:a)
|> black_magic(:c)
end
def a(_s), do: "a was updated with black magic"
def b(_s), do: "b was updated with black magic"
def c(_s), do: "c was updated with black magic"
end
Then:
iex(1)> Foo.foo
%{
c: "c was updated with black magic",
a: "a was updated with black magic",
b: "B"
}
I’m not sure if unquote(__MODULE__).unquote(fun)(unquote(state)) is the best way to call a function in the same module when the function name is specified by an atom…
defmacrop black_magic(state, keyfun) do
fun = Function.capture(__MODULE__, keyfun, 1)
quote do
%{unquote(state) | unquote(keyfun) => unquote(fun).(unquote(state))}
end
end
No worries, did not miss it. “For fun” coding is so much fun sometimes. Just had a laugh seeing you tinkering about code optimization in code that would never see a production server.