BenSchZA

BenSchZA

Creating a GenServer `handle_call` macro / decorator

I’ve created a custom GenServer macro, to abstract the logic from the user of a project I’m working on, Cadex. It’s used for modelling and simulating basic differential equation based models and eventually complex systems.

I’ve been experimenting with different ways I can create state update functions like below, and make it a bit cleaner - so the user only has to implement the core logic. Essentially I want to take the handle_call function and insert code before and after, adapt the function interface, but I’m struggling to get my head around it. You can see my WIP here: https://github.com/BenSchZA/cadex/tree/robots-marbles-7/lib/cadex in decorators.ex and model.ex.

def handle_call(
        {:update, var},
        _from,
        state = %Cadex.Types.State{current: current, delta: delta}
      )
      when var == :box_A do
    increment =
      &(&1 +
          cond do
            current[var] > current[:box_B] -> -1
            current[var] < current[:box_B] -> 1
            true -> 0
          end)

    delta_ = %{var => increment}

    state_ =
      state
      |> Map.put(:delta, Map.merge(delta, delta_))

    {
      :reply,
      state_,
      state_
    }
  end

Rather something cleaner like this:

@state_update(:box_A)
def update_box_A(
        {:update, var},
        state = %Cadex.Types.State{current: current, delta: delta}
      ) do
    increment =
      &(&1 +
          cond do
            current[var] > current[:box_B] -> -1
            current[var] < current[:box_B] -> 1
            true -> 0
          end)

    {var: increment}
  end

In the GenServer macro, I’d then like to insert the rest of the code, and make sure the handle_call behaviour(?) is still implemented correctly and returns the right result. Is this possible?

I think I’ve messed around enough trying to implement a solution, and maybe some feedback would help me on my way! Appreciate any guidance :slight_smile:

Marked As Solved

lucaong

lucaong

If I understand, you want to allow the user of your library to specify how the state should be updated, without requiring them to implement the whole GenServer and without leaking private implementation details that should not affect the user. Is that correct?

If so, initially leave aside the GenServer and its state and think about a functional core for the state update logic. I do not understand the details of your example, but it seems that the user should specify how to update the :delta given some information in var and current. If that is true, you could let the user supply a module that fulfills a specified behavior, something like this:

# Behavior (adjust the types to the correct ones)
defmodule YourBehaviour do
  @callback update(var, current) :: delta
end

# User defined functional implementation
defmodule UserImplementation do
  @behaviour YourBehaviour

  @impl true
  def update(var, current) do
    # compute and return delta
  end
end

# Provide the implementation, something like:
{:ok, pid} = YourLibrary.start_link(impl: UserImplementation)

I might have gotten the details wrong, but the important thing is that you define a functional behavior so that the user defined functions receive all the needed data and no more than that, and return the updated value.

Your GenServer will then maintain the state and use the module supplier by the user to update it. This way, the user does not access implementation details or private data structures, and your GenServer logic can be tested with your own test implementation. Runtime concerns like handling concurrency or supervision are left outside of the functional core. Also, no macro is needed.

The GenServer is a runtime concern that is better separating from the actual update logic. This popular blog post elaborates more on the idea (the author, @sasajuric, hangs out in this forum).

Also Liked

al2o3cr

al2o3cr

Also check out gen_statem - it’s an example of an abstraction on top of the GenServer behavior that abstracts away state-updating and event routing.

LukeWood

LukeWood

I wrote a library to do this recently -

I didn’t like how it turned out so I never pushed forward - but you might find some value in the source code

ityonemo

ityonemo

If you want a gen_statem that looks like genserver, I wrote https://hexdocs.pm/state_server/StateServer.html

BenSchZA

BenSchZA

I joined the Elixir Forum just over a week ago, and already I can say the Elixir community is one of the best! :slight_smile: Thanks for all the suggestions, going to accept @lucaong 's answer, but they’ve all helped answer my question.

I’m excited to keep learning - just got a client wanting a data ETL pipeline, that is going to be the perfect match for the Elixir/Phoenix ecosystem, GenServer, GenStage, all the rest. First opportunity to use Elixir in production :clap:

BenSchZA

BenSchZA

After refactoring to use behaviours:

A model is defined.

defmodule Marbles do
  @behaviour Cadex.Behaviour

  @initial_conditions %{
    box_A: 11,
    box_B: 0
  }

  @partial_state_update_blocks [
    %Cadex.Types.PartialStateUpdateBlock{
      policies: [
        :robot_1,
        :robot_2
      ],
      variables: [
        :box_A,
        :box_B
      ]
    }
  ]

  @simulation_parameters %Cadex.Types.SimulationParameters{
    T: 10
  }

  @impl true
  def config do
    %Cadex.Types.State{
      sim: %{
        simulation_parameters: @simulation_parameters,
        partial_state_update_blocks: @partial_state_update_blocks
      },
      current: @initial_conditions
    }
  end

  @impl true
  def update(var = :box_A, _state = %Cadex.Types.State{current: current}) do
    increment =
      &(&1 +
          cond do
            current[var] > current[:box_B] -> -1
            current[var] < current[:box_B] -> 1
            true -> 0
          end)

    {:ok, increment}
  end

  @impl true
  def update(var = :box_B, _state = %Cadex.Types.State{current: current}) do
    increment =
      &(&1 +
          cond do
            current[var] > current[:box_A] -> -1
            current[var] < current[:box_A] -> 1
            true -> 0
          end)

    {:ok, increment}
  end
end

The model is run (down the line there will be settings for Monte carlo simulations etc.)

{:ok, pid} = Cadex.start(Marbles)
Cadex.update(:box_A)
Cadex.update(:box_B)
state = Cadex.state()

# Or, to run a full model loop
Cadex.run(optional_args)

This looks much better.

Where Next?

Popular in Questions Top

vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
Fl4m3Ph03n1x
Background Let’s assume I have a typical GenServer that receives messages as requests, does some operation in a DB and returns responses....
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
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
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
sabri
Can someone explain the settings of pool_size of Ecto in config file? and what is the recommend size? Thanks
New
Mooodi
Given a string, how can I get access to its character by index? Enum.at("my_string", 2) doesn't work. Or rather, not char, but a substr...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New

Other popular topics Top

chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30048 115
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
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
273 38985 115
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New

We're in Beta

About us Mission Statement