Crowdhailer

Crowdhailer

Creator of Raxx

Functionaly pure message passing

The Gen* behaviours in Elixir (and erlang) provide a pure interface.

One of the benefits of this is business logic is easy to test. No need to start processes just pass the arguments you want to test to handle_call for example and assert on one of the results.

I’m sure I read in a book that a pure interface was an important part of the setup, unfortunately I can’t remember which one so if anyone can point me to that again I’d be grateful.

Unfortunately with GenServer implemented as it is several things can’t be done in a pure fashion. For example sending a reply to a call in response to a later message.

It’s a fairly contrived example but I don’t believe this can be implemented in a pure manner.

defmodule PairUp do
  use GenServer

  def handle_call({:pair, pid}, from, :none) do
    {:noreply, {:waiting, pid, from}}
  end
  def handle_call({:pair, pid2}, from2, {:waiting, pid1, from1}) do
    GenServer.reply(from1, {:other, pid2})
    {:reply, {:other, pid1}, :none}
  end
end

I was thinking with some small changes to the GenServer design purity could be regained.

defmodule PairUp do
  use AltServer

  def handle_call({:pair, pid}, from, :none) do
    {[], {:waiting, pid, from}}
  end
  def handle_call({:pair, pid2}, from2, {:waiting, pid1, from1}) do
    messages = [
      {from2, {:other, pid1}},
      {from1, {:other, pid2}},
    ]
    {messages, :none}
  end
end

The key changes to this interface is that :send/:nosend are replaced by a list of {target, message} pairings, an empty list giving a same behaviour as no send.

I think the structure {[{target, message], state} could be treated as a writer monad. This might even be a helpful model to add type safety to message sending.

This post is really just me musing. my questions are?

  • Is this a great idea or a horrible idea
  • Does something similar exist already

Most Liked

peerreynders

peerreynders

Don’t put might-on-will.
Alan Cooper, About Face (1995), p.138

This design rule was specified in connection to user interfaces but over the years I’ve found it to be a good design rule in general. OO often pushed for more generic solutions - frequently in the name of improved reusability - without necessarily accounting for the increase in complexity or decrease in transparency that sometimes results from trying to account for another 5%(? or less) of use cases.

So while your alternate solution may seem more “generic” most use cases only ever need one or no message - not many.

  • Your concern can be easily addressed by composing the logic in handle_call with pure functions which can be tested separately instead of testing handle_call directly.

  • If you are finding that you need to quite often dispatch multiple replies simply go with something like

defmodule PairUp do
  use GenServer
  
  defp multi_reply(msgs), 
    do: 
      Enum.each(msgs, fn({from, msg}) ->
        GenServer.reply(from, msg)
      end)

  def handle_call({:pair, pid}, from, :none) do
    {:noreply, {from, {:other, pid}}
  end
  def handle_call({:pair, pid}, from, msg) do
    multi_reply([{from, {:other, pid}}, msg])
    {:noreply, :none}
  end
end

provide a pure interface.

Can you elaborate on what this means? In connection to functions “purity” is a well defined concept. Going from your post you seem to be largely concerned with message dispatch that isn’t handled via callback return values.

My typical solution is to implement the “business logic” in an entirely separate module and have the GenServer callbacks simply invoke those module functions. So PairUp would be simply GenServer interaction logic (and GenServer related helper functions) while some PairUpLogic module would contain the actual “pure business functions”. So testing would focus on PairUpLogic while PairUp callbacks would mostly just call PairUpLogic functions.

michalmuskala

michalmuskala

Being idealistic, I think this could be an improvement, but on the other hand, being realistic I know there’s just no chance to change GenServer (and probably for a good reason).

Fortunately, what you propose is already implemented in gen_statem which accepts a list of “actions” in return from state functions:

defmodule Statem do
  @behaviour :gen_statem

  def callback_mode(), do: :state_functions
  
  def init(_args) do
    {:ok, :none, :no_data}
  end

  def none({:call, from1}, {:pair, pid1}, _data) do
    {:next_state, :waiting, {pid1, from1}}
  end

  def waiting({:call, from2}, {:pair, pid2}, {pid1, from1}) do
    {:next_state, :none, :no_data, [{:reply, from1, pid2}, {:reply, from2, pid1}]}
  end
end

Last Post!

Crowdhailer

Crowdhailer

Creator of Raxx

Not yet but I would very much like too.

Where Next?

Popular in Discussions Top

AstonJ
Are there any Elixir or Erlang libraries that help with this? I’ve been thinking how streaming services like twitch have exploded recentl...
New
chulkilee
Here are the list of HTTP client libraries/wrappers, and some thoughts on HTTP client in general. I’d like to hear from others how they w...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 40082 209
New
RudManusachi
What configs will make sense to put to runtime.exs? – A bit of how I configure apps: I have generic configs in config/config.exs, dev...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
crispinb
On reading dhh’s latest The One Person Framework it strikes me that Phoenix with LiveView is already pretty much this. However, never hav...
New
AstonJ
Can you believe the first professionally published Elixir book was published just 8 years ago? Since then I think we’ve seen more books f...
New

Other popular topics Top

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
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 49134 226
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New

We're in Beta

About us Mission Statement