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:
- The Contract and the Implementation are defined in the same file. I really don’t like this.
- 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
- Have you read Dave’s small article? What do you think?
- How can I improve this module and this code? Let me know !
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #ai
- #elixirconf-us
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #elixirconf-eu
- #metaprogramming
- #hex











Showing Posts 26 to 17- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
sasajuric
Roughly speaking yeah. My position is that lifecycle functions (
childspecand optionalstart_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
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
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,initand callback functions fromGenServerinto 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
Fantastic!
Carry on, everyone. Nothing to see here.
sasajuric
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
Not all all.
I’m suggesting separating lifecycle concerns from functional concerns within the same codebase, that’s all.
sasajuric
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
In addition, it’s worth remembering that
child_spec/1thing 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. Withchild_spec& support by ElixirSupervisor, a component/service can fully encapsulate the details of how its started, which makes things simpler for the user. StickFooor{Foo, arg}where you want it in the supervision tree, and you can now interact withFoousing the functions from the same module (or “submodules”). To me that seems very straightforward. Again, YMMVWell, you said earlier in this thread that it’s important that we’re discussing it, so here I am, discussing it
pragdave
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
sasajuric
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
Registrydocs still need to instruct me to startRegistry.Serverbefore I can useRegistryfunctions. 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 forServerexplain 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
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_linkandchild_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.