Help me refactor chained map updates

Let’s say I have some code like this

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

is there a cleaner way to do that?

2 Likes

If the functions themselves updated the state you could pipe them.

def foo(state) do
  state
  |> a()
  |> b()
  |> c()
end

def a(state) do
  %{state | a: :baz}
end

I do this a lot in GenServers.

2 Likes

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.

Take a look at Map.update!/3.

def foo do
  state
  |> Map.update!(:a, &a)
  |> Map.update!(:b, &b)
  ...
end
3 Likes

Yeah, it’s contextual. I still do what you’ve done sometimes as well.

You could reduce

[a: &a/1, b: &b/1, c: &c/1]
|> Enum.reduce(state, fn {key, fun}, state -> Map.update!(state, key, fun) end)
5 Likes

Note that this is actually slightly different because in the OP the entire state is the input to the function.

1 Like

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.

5 Likes

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.

You could wrap this behaviour in a function:

def my_update(state, key, fun), do: %{state | key => fun.(state)}

Then call it like this:

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.

4 Likes

Thanks for all the ideas.

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

Just for fun:

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…

1 Like

It’s like questioning whether you use the right bolt for an airplane made of paper and candy sticks :slight_smile:

I think the state → state signature is the most common (and known) way.

1 Like

I’d go with captured function and private macro.

  defmacrop black_magic(state, keyfun) do
    fun = Function.capture(__MODULE__, keyfun, 1)
    quote do
      %{unquote(state) | unquote(keyfun) => unquote(fun).(unquote(state))}
    end
  end
2 Likes

Did you miss this bit?

:roll_eyes:

I’ll be in the field sticking bolts on my paper aeroplanes…

1 Like

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.

Ahh, add a helper like this:

def update_with(state, key, fun) do
  Map.put(state, key, fun.(state))
end

def foo(state) do
  state
  |> update_with(:a, &a/1)
  ...
end

Original functions don’t need to know where to put their output, foo contains all the rules for where to put results.

Why do I get the impression my posts are invisible sometimes :laughing:

2 Likes

Use the common language, not your barbarian Macroeneze tongue.

1 Like

lol, I admit I didn’t read the thread after Garrison’s reply, so you are in good invisible company.

1 Like

I feel like there has to be some profoundly simple reason nobody has suggested

  def foo do
    %{state | a: a(state),
              b: b(state),
              c: c(state)
    }
  end

that I’m just missing for whatever reason