Fl4m3Ph03n1x

Fl4m3Ph03n1x

Background

After reading Elixir in Action I came with a (slightly modified for my evil purposes) module that I very originally call the ProcessRegistry. Now this module works and is fine, it also has a behaviour defined, but everything is in the same file and it feels tangled to me, specially after reading this post from pragmatic Dave Splitting APIs, Servers, and Implementations in Elixir.

Code

So, following is the file called ProcessRegistry:


defmodule Clint.IProcessRegistry do
  @type via_tuple_type  ::
    {:via, module, {module, {module, any}}} |
    {:via, module, {module, {module, any}, any}}

  @callback lookup(tuple)                  :: [{pid, any}]
  @callback update_with_value(tuple, any)  :: {any, any} | :error
  @callback via_tuple(tuple)               :: via_tuple_type
  @callback via_tuple(tuple, any)          :: via_tuple_type
end

defmodule Clint.ProcessRegistry do
  @behaviour Clint.IProcessRegistry

  alias Clint.IProcessRegistry

  ###############
  # Public API  #
  ###############

  @impl IProcessRegistry
  def lookup(key), do: Registry.lookup(__MODULE__, key)

  @impl IProcessRegistry
  def update_with_value(key, new_value), do:
    Registry.update_value(__MODULE__, key, fn _old_value -> new_value end )

  @impl IProcessRegistry
  def via_tuple(key), do:
    {:via, Registry, {__MODULE__, key}}

  @impl IProcessRegistry
  def via_tuple(key, value), do:
    {:via, Registry, {__MODULE__, key, value}}

  @spec start_link() :: {:ok, pid} | {:error, any}
  def start_link, do:
    Registry.start_link(keys: :unique, name: __MODULE__)

  #############
  # Callbacks #
  #############

  @spec child_spec(any) :: Supervisor.child_spec
  def child_spec(_) do
    Supervisor.child_spec(
      Registry,
      id: __MODULE__,
      start: {__MODULE__, :start_link, []}
    )
  end
end

It’s a fairly small file with 53 lines, but it has a couple of things that bother me:

  1. The Contract and the Implementation are defined in the same file. I really don’t like this.
  2. According to pragmatic Dave, the API and the implementation are mixed together.

For the first point, I don’t really think there will ever be another implementation of ProcessRegistry that doesn’t use Elixir’s Registry. Yes, I could go on an adventure and re-invent the wheel, but what for?

How many of you had to re-implement registry’s functionality for your own projects after Elixir 1.4? What is so bad about the current implementation of Registry (or so lacking) that would make you re-implement it? Short summary, I don’t really think I need a Contract for this, because I don’t see a future where ProcessRegistry will have (or need) an implementation that doesn’t use Elixir’s Registry.

The second point is even more contentious. From what I understand, even without the behaviour, Dave would not approve of my “mixing of concerns” between the API and the Registry module. He would define a module called ProcessRegistry with the API, and then inside a folder with the same name he would create a module with a completely useless name like impl and there I would have the calls to Registry.

I attack this idea based on the fact Dave likes to complain about useless folders and boilerplaty code, but I find that in my specific case, this is what I would be creating - more boilerplate and indirection.

Opinions

  1. Have you read Dave’s small article? What do you think?
  2. How can I improve this module and this code? Let me know !

Showing Posts 26 to 17

sasajuric

sasajuric

Author of Elixir In Action

Roughly speaking yeah. My position is that lifecycle functions (childspec and optional start_link) are a part of the component/service API. I’m not saying this should always be in the same file though. I do concede that lifecycle can be thought of as a separate API concern, so in the case of a larger API, we might decide to move lifecycle and some other API concerns into separate modules (in which case I’d try to keep lifecycle in the top-level module). In this particular example, I feel that the module is too small, and IMO splitting it causes more confusion with little to no tangible benefits.

I feel that the mainstream microservices approach (i.e. the approach based on one OS process per microservice) is an improvisation around deficiencies in the first chosen technology (the language of choice + runtime). I’m happy to discuss this in further details, but it’s probably better done in another thread.

peerreynders

peerreynders

Perhaps the former emphasizes the client point of view (related to interface segregation); the latter emphasizes the maintenance point of view - being able to see everything the process needs to function in one place to make it easier to reason about the whole - up to a point. When the capabilities of the process grow large enough it is time to push those into separate modules (or perhaps even separate processes), in effect composing the capabilities to realize the process.

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

So, just to see if I understand both points of view clearly:

@pragdave defends we should split functional concerns from lifecycle concerns. In his opinion, such can be achieved by separating the start_link, init and callback functions from GenServer into a separate file, while keeping the API that is actually used, in another file.

@sasajuric defends that both lifecycle and functional concerns belong together in the same file, because you need both to make effective use of an API. It is not clear to me if @sasajuric likes the micro-services approach we have seen recently, but it looks like he prefers the way Elixit’s community is dealing with the way APIs are organized anyway.

I apologize if I have miss interpreted any comments, but I feel this is a succinct summary of both points of view.

pragdave

pragdave

Author of Programming Elixir

Fantastic!

Carry on, everyone. Nothing to see here.

sasajuric

sasajuric

Author of Elixir In Action

Right, and this is what I believe the whole industry has been doing for decades, and it’s still doing it now with the mainstream microservices approach. Thus, I actually think that the Erlang way, despite its age, is in fact avangarde, and the Elixir way with childspecs is even more progressive.

pragdave

pragdave

Author of Programming Elixir

Not all all.

I’m suggesting separating lifecycle concerns from functional concerns within the same codebase, that’s all.

sasajuric

sasajuric

Author of Elixir In Action

The style might not have changed much within BEAM languages (it actually has, but more on that later), but I feel that this style is mostly unknown to non-BEAM languages. Since in those languages people typically don’t have lightweight processes, they usually resort to running multiple OS processes to split runtime activities. So for example, to start an external process registry, we would have a registry as an external OS process (e.g. etcd). To start/stop it, we would use some CLI API, probably in combination with an external lifecycle manager, such as systemd. Then we’d use some internal API (e.g. REST, possibly wrapped with some library functions) to interact with the running component. Such approach is in my view close to what you seem to be suggesting.

After using that approach myself for many years, and then also using the “BEAM way” for quite some time too, I empirically came to conclusion that I prefer the latter. I find it simpler, and at the same time more powerful and flexible. YMMV of course :slight_smile:

In addition, it’s worth remembering that child_spec/1 thing is Elixir specific, and it’s in fact a fairly new addition to Elixir. FWIW, I think it’s one of the more interesting changes introduced by Elixir, because it makes things closer to the way I personally think about code. With child_spec & support by Elixir Supervisor, a component/service can fully encapsulate the details of how its started, which makes things simpler for the user. Stick Foo or {Foo, arg} where you want it in the supervision tree, and you can now interact with Foo using the functions from the same module (or “submodules”). To me that seems very straightforward. Again, YMMV :slight_smile:

Well, you said earlier in this thread that it’s important that we’re discussing it, so here I am, discussing it :slight_smile:

pragdave

pragdave

Author of Programming Elixir

If it’s a “mechanical detail” then I’d argue that it’s definitely something that isn’t important when reading. It’s just housekeeping noise.

That’s why I have the ability in the Component library to have globals started automatically.

But it’s all just style, and there’s nothing but time and experience to judge which is preferable. And indeed, as you say, there’s no one right answer.

I push these points, though, because the community seems to be very happy to continue to write code as it has been written for the last 20 years, and that sets off little alarm bells in the agile part of my brain. Unless we try new ways of doing things (rather than just talking about them) we’ll never learn if the current way is indeed the best. I don’t want to debate. I want to code :slight_smile:

sasajuric

sasajuric

Author of Elixir In Action

I regard the fact that these funs are (usually) not called directly as a mechanical detail. To me they are still first and foremost a part of the same API, because these functions cover one aspect of how we use the abstraction. IMO starting a thing is a part of using the thing, and so it’s a part of the API of that thing.

Separating into two modules doesn’t change that - the Registry docs still need to instruct me to start Registry.Server before I can use Registry functions. So the API surface remains the same, except now it’s split across two modules. Now I need to read docs of both modules because they cross-reference themselves (e.g. docs for Server explain how some childspec/start_link option affects the behaviour of some function in the “API” module and vice-versa). I personally find this confusing.

I can agree that starting might be considered as a separate API concern, so splitting these two concerns might sometimes be valid, but IMO that’s not the case in this simple example. I feel that such split here brings more usage/reading distractions with little to no practical gain.

pragdave

pragdave

Author of Programming Elixir

Thanks for highlighting this, because I think it’s at the core of much of my thinking.

The Beam is a pretty unique environment, and it makes us think about many things differently. One major difference is in the way we have to think about process lifecycle as well as process functionality.

Take this particular example. We have a global registry with an API (lookup, update, and so on). In most other languages, that would be the end of it. But on the Beam the conventional way to have global state is to have a named process, and so we now have lifecycle code that starts this process.

But I suggest that this code (the start_link and child_spec) are not part of the API of the Registry. They are part of the lifecycle, the scaffolding needed to get the Registry working on the Beam. 99% of the time, it will only be invoked as a tuple passed as a child spec to some top-level supervisor. It never gets called by people using the module.

So that’s at the root of my concern. We mix traditional APIs and BEAM lifecycle management code into the same bucket of functions. And, as a result, I think we often conflate supervision with functionality. But the two are separate: the supervision tree is an expression of process lifecycle, and not process functionality.

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
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
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
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
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
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
apz
I’m new to elixir and just tried to install the elixirLS extension for VScode(ium) and it is throwing some errors that I would like help ...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
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
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
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
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

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews