tcoopman

tcoopman

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?

First 10 of 20 Posts! Switch mode

garrison

garrison

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.

tcoopman

tcoopman

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.

windexoriginal

windexoriginal

Take a look at Map.update!/3.

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

garrison

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

Zurga

Zurga

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)
garrison

garrison

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

mudasobwa

mudasobwa

Creator of Cure

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.

adamu

adamu

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.

tcoopman

tcoopman

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

adamu

adamu

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…

Last Post!

Zurga

Zurga

State might have changed in a way after a(state) that affects the outcome from b(state)

Where Next?

Trending in Questions Top

jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
silverdr
Using Phoenix.LiveView.TagEngine as an EEx.Engine is deprecated! To compile HEEx, use Phoenix.LiveView.TagEngine.compile/2 instead. Sta...
New
saveman71
Hello ! We want new/edit form pages to POST/PUT to their own URL rather than the resources REST defaults (post /things, put /things/:id)...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
michallepicki
I am using Oban and occasionally, shortly after a deployment, a handful of jobs can fail because of dependency on other parts of the syst...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New

We're in Beta

About us Mission Statement