Search code examples
erlanggen-server

Can I modify a map that is managed with a gen_server with external functions?


I have a map that is managed with a module with a gen_server behavior, where I'm able to add, remove and update key->values.

I also have a main module with some routines and subroutines where I act depending on the key->values that I have in the map. My problem is that I try to modify the map during the execution of my module, but I didn't get any answer.

This is an example of the structure of my main module:

-export([
    go/0, 
    add_belief/1
]).

go()->
    bs:start_link(),
    collect_bottles(0).

collect_bottles(Total) ->
    case {bs:is_belief(holding), bs:is_belief(over_drop)} of
        {true, true} -> drop_and_leave();
        {true,false} -> get_to_drop();
        {false, _} -> get_bottle()
    end.

get_bottle()->
io:format("Getting bottle.~n"),
case {bs:get_belief(see)} of
    {true} -> collect_bottles(bs:get_belief(collected)); 
    {false} ->move(),
              get_bottle()
end.

move(Dist)->
io:format("Start moving...~n"),
timer:sleep(5000).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%            GOD FUNCTIONS             %%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

add_belief(Belief)->
    bs:add_belief(Belief).

The code of bs:add_belief(Belief) is:

add_belief(Belief)->
    gen_server:cast(?MODULE,{add,Belief}).

And in the gen_server function:

handle_cast({add,{Key,Value}},State)->
    io:format("Belief added: ~p.~n",[{Key,Value}]),
    {noreply, maps:put(Key,Value,State)};

When I run my script, I get:

tr:go().
Getting bottle.
Start moving...
Getting bottle.
Start moving...

And I can't not use another function (I would like to use add_belief({see,bottle}) to get out of the loop.


Solution

  • Can I modify a map that is managed with a gen_server with external functions?

    Yes, here is proof:

    $ erl
    Erlang/OTP 19 [erts-8.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
    Eshell V8.2  (abort with ^G)
    
    1> c(tr).
    {ok,tr}
    
    2> c(bs).
    {ok,bs}
    
    3> c(env).
    {ok,env}
    
    4> tr:test().
    tr:get_bottle(): getting bottle
    <0.74.0>
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    
    5> env:get_state().
    env: get_state(): 10
    ok
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    
    6> bs:get_state().
    bs:get_state(): #{}
    ok
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle 
    
    7> bs:add_belief({holding, []}).
    Adding belief: {holding,[]}
    ok
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    
    8> bs:get_state().
    bs:get_state(): #{holding=>[]}
    ok
    tr:get_bottle(): getting bottle
    
    9> 
    BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
           (v)ersion (k)ill (D)b-tables (d)istribution
    $ 
    

    If instead I change my bs:erl file to call gen_server:start_link() like this:

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

    then this is what happens:

    1> c(tr).
    {ok,tr}
    
    2> c(bs).
    {ok,bs}
    
    3> c(env).
    {ok,env}
    
    4> tr:test().
    tr:get_bottle(): getting bottle
    <0.74.0>
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle 
    
    5> bs:add_belief({holding, []}).
    ok
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    
    6> bs:get_state().
    ** exception exit: {noproc,{gen_server,call,[bs,get_state]}}
         in function  gen_server:call/2 (gen_server.erl, line 204)
         in call from bs:get_state/0 (bs.erl, line 40)
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    tr:get_bottle(): getting bottle
    

    From the gen_server docs about gen_server:start_link():

    The first argument, {local, ch3}, specifies the name. The gen_server is then locally registered as ch3.

    If the name is omitted, the gen_server is not registered. Instead its pid must be used. The name can also be given as {global, Name}, in which case the gen_server is registered using global:register_name/2.

    If you don't specify {local, ?MODULE} as an argument to gen_server:start_link(), then you have to call gen_server:call() like this:

     gen_server:call(ServerPid, Request)
    

    To get the ServerPid, you need to do something like this:

    start_link() ->
        {ok, ServerPid} = gen_server:start_link(
                              %%{local, ?MODULE},
                              ?MODULE, [], []
                          ),
        ServerPid.
    

    From the last shell session above, it looks like if you don't specify {local, ServerName}--and thus you do not register the server name--and you call gen_server:cast(?MODULE, ...) it won't cause an error, but if you call gen_server:call(?MODULE...) you will get an error. To me it seems like it would be handy if you got an error in both cases when the server wasn't registered--the return value of ok for the cast is pretty misleading.


    tr.erl:

    -module(tr).
    %%-compile(export_all).
    -export([go/0, test/0]).
    
    go() ->
        bs:start_link(),
        env:start_link(),
        collect_bottles(0).
    
    collect_bottles(_Total) ->
        get_bottle().
    
    get_bottle() ->
        io:format("tr:get_bottle(): getting bottle~n"),
        timer:sleep(3000),
        get_bottle().
    
    test() ->
        spawn(tr, go, []).
    

    bs.erl:

    -module(bs).
    %%-compile(export_all).
    -export([init/1, handle_call/3, handle_cast/2]).
    -export([handle_info/2, terminate/2, code_change/3]).
    -export([start_link/0, add_belief/1, get_state/0, stop/0]).
    
    
    %%Internal server functions:
    init([]) ->
        {ok, #{}}.  %%<******** INITIALIZE STATE WITH AN EMPTY MAP
    
    handle_cast({add, {Key, Val}=Belief}, State) ->
        io:format("Adding belief: ~w~n", [Belief]),
        { noreply, maps:put(Key, Val, State) }.
    
    handle_call(get_state, _From, State) ->
        {reply, State, State}.    
    
    %  -----
    handle_info(_Info, State) ->
        {noreply, State}.
    
    terminate(_Reason, _State) ->
        ok.
    
    code_change(_OldVsn, State, _Extra) ->
        {ok, State}.
    
    %%External interface:
    start_link() ->
        gen_server:start_link(
          {local, ?MODULE},
          ?MODULE, [], []
        ).
    
    add_belief(Belief) ->  %%<******* EXTERNAL FUNCTION THAT MODIFIES A MAP
        gen_server:cast(?MODULE, {add, Belief}).
    
    get_state() ->
        State = gen_server:call(?MODULE, get_state),
        io:format("bs:get_state(): ~w~n", [State]).
    
    stop() ->
        gen_server:stop(?MODULE).
    

    env.erl:

    -module(env).
    %%-compile(export_all).
    -export([init/1, handle_call/3, handle_cast/2]).
    -export([handle_info/2, terminate/2, code_change/3]).
    -export([start_link/0, get_state/0, stop/0]).
    
    %%Internal server functions:
    init([]) ->
        {ok, 10}.   %%<***** INITIALIZE STATE WITH 10
    
    handle_call(get_state, _From, State) ->
        {reply, State, State}.
    
    %%     ------
    handle_cast(_Request, State) ->
        {noreply, State}.    
    
    handle_info(_Info, State) ->
        {noreply, State}.
    
    terminate(_Reason, _State) ->
        ok.
    
    code_change(_OldVsn, State, _Extra) ->
        {ok, State}.
    
    %%External interface:
    start_link() ->
        gen_server:start_link(
          {local, ?MODULE},
          ?MODULE, [], []
        ).
    
    get_state() ->
        State = gen_server:call(?MODULE, get_state),
        io:format("env: get_state(): ~w~n", [State]).
    
    stop() ->
        gen_server:stop(?MODULE).