rogach

rogach

Is mutating "parent" state in a module an anti-pattern?

I often need to abstract some common functionality into a separate module. In cases when this functionality requires keeping some state, I use the following pattern: I keep that state under a separate key in the “parent” module state (usually a GenServer), and to simplify the code I pass “parent” state into the functions of common module.

I’ll give a specific example to make the idea clearer. In the example, there is a GenServer module that handles packets coming from a TCP socket, and there is LineReader module that handles aggregating the raw data packets and extracting lines from them.

Here’s the code (I’ve attempted to omit most of irrelevant parts):

defmodule ClientHandler do
  use GenServer

  def init(%{socket: socket}) do
    state = %{
      socket: socket,
      lr_state: LineReader.initial_state(),
    }
    {:ok, state}
  end

  def handle_info({:tcp, _socket, data}, state) do
    state = LineReader.put_packet(state, data)
    {lines, state} = LineReader.recv_lines(state)
    # ... do something with lines
    {:noreply, state}
  end
end

defmodule LineReader do
  def initial_state() do
    %{lines: [], trailing: ""}
  end

  def put_packet(parent_state, packet) do
    lr_state = parent_state.lr_state
    # ... process the new data packet, update lr_state
    %{parent_state | lr_state: lr_state}
  end

  def recv_lines(parent_state) do
    lr_state = parent_state.lr_state
    lines = lr_state.lines
    lr_state = %{lr_state | lines: []}
    {lines, %{parent_state | lr_state: lr_state}}
  end
end

I feel like this code is a little smelly, because it ties into specific field name in parent state. However, it saves a lot of code on the caller’s side and makes the code much cleaner.

Is such approach an anti-pattern? Are there some better alternatives?

Most Liked

LostKobrakai

LostKobrakai

Imo processes should only be introduced if there are runtime level reasons for them - failure isolation, asynchronous processing, … That is completely separate from code level abstractions, where multiple module collaborate to build up logic.

dimitarvp

dimitarvp

A super generic answer: be explicit. Implicitness is rarely worth it unless it’s seriously getting in the way. One example of “getting in the way”, subjectively for me, is a lot of artifacts that Phoenix generates should start off being referenced as defaults in other modules / libraries and only put inside your code base when you need to modify them and they are no longer the defaults.

In your case however, it does seem you want to save just a little typing which is IMO not worth.

And this is also very related to feature envy and the single responsibility principle. Simplified: put things where they semantically belong. Which of course is the entire problem, sometimes we have a problem nailing the semantics – and is ironically one of the things we are actually being paid to do, not the code flinging itself.

LostKobrakai

LostKobrakai

It’s a little unclear why LineReader would need access to parent_state. I’d argue that LineReader should only be involved with the state it takes care of and not care that it’s nested within the ClientHandler. Yes this might be a “bit more characters” in ClientHandler, but it’ll be less in LineReader and responsibilities are properly aligned.

derek-zhou

derek-zhou

Smelly code is not clean code. My advise is to make the code not smelly first. We can then help you to refactor the code to be more concise and readable.

codeanpeace

codeanpeace

Yeah, agreed – it’s what I was trying to suss out with “what else it’s being used for and how else it’s being used”. It’d be the code organization by process anti-pattern otherwise.

You can spin up a temporary process “on demand” e.g. Task.async/1 and “if you spawn a task inside a GenServer, then the GenServer will automatically await for you and call GenServer.handle_info/2 with the task response and associated :DOWN message.”

If the data packet processing logic is brittle and the client handler needs to be robust, a nice runtime reason could be failure isolation so that packet processing can fail without affecting the client handler. You’d likely want to only pass the necessary information.

Where Next?

Popular in Questions Top

albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
dokuzbir
Hello, I am trying to convert my lists to string without losing brackets.For start i have 3 map. They look like these buyer = %{ id: ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
gonzofish
I’m currently trying to understand how to join three tables using Ecto. All the examples I’ve seen use 2, so maybe I’m just missing somet...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
sabri
Can someone explain the settings of pool_size of Ecto in config file? and what is the recommend size? Thanks
New

Other popular topics Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement