BenSchZA

BenSchZA

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: cadex/lib/cadex at robots-marbles-7 · BenSchZA/cadex · GitHub 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:

Showing Posts 1 to 8

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

BenSchZA

BenSchZA OP

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?

That’s correct, and behaviors look like exactly what I need :slight_smile: I’m going to give this a try, and see if I understand how the core logic and GenServer are then used together. I’ll come back for another review.

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.

BenSchZA

BenSchZA OP

That library is almost exactly what I need! I might even try build on top of it.

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

https://github.com/LukeWood/GenClient

ityonemo

ityonemo

If you want a gen_statem that looks like genserver, I wrote StateServer — state_server v0.4.10

BenSchZA

BenSchZA OP

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 OP

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.

— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
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
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews