5
votes

EDIT:

I'm not looking to use parameters as a general purpose way to construct Erlang programs--I'm still learning the traditional design principles. I'm also not looking to emulate OOP. My only point here is to make my gen_server calls consistent across server instances. This seems more like fixing a broken abstraction to me. I can imagine a world where the language or OTP made it convenient to use any gen_server instance's api, and that's a world I want to live in.

Thanks to Zed for showing that my primary objective is possible.


Can anyone figure out a way to use parameterized modules on gen_servers? In the following example, let's assume that test_child is a gen_server with one parameter. When I try to start it, all I get is:

42> {test_child, "hello"}:start_link().
** exception exit: undef
     in function  test_child:init/1
        called as test_child:init([])
     in call from gen_server:init_it/6
     in call from proc_lib:init_p_do_apply/3

Ultimately, I'm trying to figure out a way to use multiple named instances of a gen_server. As far as I can tell, as soon as you start doing that, you can't use your pretty API anymore and have to throw messages at your instances with gen_server:call and gen_server:cast. If I could tell instances their names, this problem could be alleviated.

4
You want multiple gen_servers using the same callback module, each with a different name? Or multiple gen_servers with the same name?Jacob
Multiple gen_servers using the same callback module. You can pass start_link a name to register different "instances" with. When you've got a single instance, it seem like it's typical to give it the same name as the module, and then your public API works like: some_module:some_function(). ...but that seems to be just a convenience. If you register the gen_server with a different name, it doesn't work anymore. I'd like to end up with something like: 1> Mod:some_function(). 2> Mod1:some_function(). ...where each variable refers to different instances of the gen_server modulemwt
Or, alternatively, I'd like to know why I shouldn't care about this. Every gen_server intro I've seen sets up an API rather than just using gen_server casts/calls. Being new to Erlang, I expected to be able to easily clone many processes, and I'm surprised that the API feature breaks as soon as you rename your gen_servers.mwt
Looking at your exception, you forgot to pass your module in its parametrized form to gen_server:start_link. See my answer for an example.Zed

4 Answers

10
votes

I just want to say two things:

  • archaelus explains it correctly. As he says the final way he shows is the recommended way of doing it and does what you expect.

  • never, NEVER, NEVER, NEVER use the form you were trying! It is a left over from the old days which never meant what you intended and is strongly deprecated now.

10
votes

There are two parts to this answer. The first is that you probably don't want to use paramatized modules until you're quite proficient with Erlang. All they give you is a different way to pass arguments around.

-module(test_module, [Param1]).

some_method() -> Param1.

is equivalent to

-module(test_non_paramatized_module).

some_method(Param1) -> Param1.

The former doesn't buy you much at all, and very little existing Erlang code uses that style.

It's more usual to pass the name argument (assuming you're creating a number of similar gen_servers registered under different names) to the start_link function.

start_link(Name) -> gen_server:start_link({local, Name}, ?MODULE, [Name], []).

The second part to the answer is that gen_server is compatible with paramatized modules:

-module(some_module, [Param1, Param2]).

start_link() -> 
  PModule = ?MODULE:new(Param1, Param2),
  gen_server:start_link(PModule, [], []).

Param1 and Param2 will then be available in all the gen_server callback functions.

As Zed mentions, as start_link belongs to a paramatized module, you would need to do the following in order to call it:

Instance = some_module:new(Param1, Param2),
Instance:start_link().

I find this to be a particularly ugly style - the code that calls some_module:new/n must know the number and order of module parameters. The code that calls some_module:new/n also cannot live in some_module itself. This in turn makes a hot upgrade more difficult if the number or order of the module parameters change. You would have to coordinate loading two modules instead of one (some_module and its interface/constructor module) even if you could find a way to upgrade running some_module code. On a minor note, this style makes it somewhat more difficult to grep the codebase for some_module:start_link uses.


The recommended way to pass parameters to gen_servers is explicitly via gen_server:start_link/3,4 function arguments and store them in the state value you return from the ?MODULE:init/1 callack.

-module(good_style).

-record(state, {param1, param2}).

start_link(Param1, Param2) ->
  gen_server:start_link(?MODULE, [Param1, Param2], []).

init([Param1, Param2]) ->
  {ok, #state{param1=Param1,param2=Param2}}.

Using this style means that you won't be caught by the various parts of OTP that don't yet fully support paramatized modules (a new and still experimental feature). Also, the state value can be changed while the gen_server instance is running, but module parameters cannot.

This style also supports hot upgrade via the code change mechanism. When the code_change/3 function is called, you can return a new state value. There is no corresponding way to return a new paramatized module instance to the gen_server code.

3
votes

I think you shouldn't use this feature this way. Looks like you are going after a OO-like interface to your gen_servers. You are using locally-registered names for this purpose - this add a lot of shared state into your program, which is The Bad Thing. Only crucial and central servers should be registered with register BIF - let all the others be unnamed and managed by some kind of manager on top of them (which should probably be registered under some name).

-4
votes
-module(zed, [Name]).
-behavior(gen_server).

-export([start_link/0, init/1, handle_cast/2]).
-export([increment/0]).

increment() ->
    gen_server:cast(Name, increment).

start_link() ->
    gen_server:start_link({local, Name}, {?MODULE, Name}, [], []).

init([]) ->
    {ok, 0}.

handle_cast(increment, Counter) ->
    NewCounter = Counter + 1,
    io:format("~p~n", [NewCounter]),
    {noreply, NewCounter}.

This module is working fine for me:

Eshell V5.7.2  (abort with ^G)
1> S1 = zed:new(s1).
{zed,s1}
2> S1:start_link().
{ok,<0.36.0>}
3> S1:increment().
1
ok
4> S1:increment().
2
ok