Defensive programming
Samuel Rivas
samuel@REDACTED
Thu Mar 30 10:32:32 CEST 2006
Joe Armstrong (AL/EAB) wrote:
> > Today I've found this piece of code I wrote some months ago:
> >
> > acceptor(tcp, Module, LSocket) ->
> > case gen_tcp:accept(LSocket) of
> > {ok, Socket} ->
> > case Module:start() of
> > {ok, Pid} ->
> > ok = gen_tcp:controlling_process(Socket, Pid),
> > gen_server:cast(Pid, {connected, Socket}),
> > acceptor(tcp, Module, LSocket);
> > {error, Error} ->
> > {stop, {Module, LSocket, Error}}
> > end;
> > {error, Reason} ->
> > {stop, {Module, LSocket, Reason}}
> > end;
>
> 14 lines of complex code - with a doubly indented case clause
>
> >
> > is that too defensive ? should I write it this way
> >
> > acceptor(tcp, Module, LSocket) ->
> > {ok, Socket} = case gen_tcp:accept(LSocket),
> > {ok, Pid} = Module:start()
> > ok = gen_tcp:controlling_process(Socket, Pid),
> > gen_server:cast(Pid, {connected, Socket}),
> > acceptor(tcp, Module, LSocket);
> >
>
> vs 6 lines of linear code with no conditional structure.
>
> How can you ask the question - you KNOW the answer.
>
> Six lines of linear code is far better that 14 lines of code with
> conditional structure.
>
> [ ... snip ...]
>
> I just say:
>
> "Let it crash"
>
> [ ... snip ...]
>
> Error handling in Erlang is based on the idea of "workers" and
> "observers"
> where both workers and observers are processes.
>
> +-----------+ +-----------+
> | Worker |------->---------| Observer |
> +-----------+ error signal +-----------+
>
I have a doubt about that:
In my view, the former piece of code is _almost_ fine. It's fine
because on absence on errors it does exactly what you want; no more, no
less. I said "almost" because on errors it does not behave exactly as
I'd like.
Elaboration: the error the observer gets is not the actual error, but
a "badmatch". Thus, I usually wrap OTPish functions like that:
-module(foo)
bar(Anything, AnotherThing) ->
case an_otp_lib:bar(Anything, AnotherThing) of
{ok, Value} ->
Value;
{error, Reason} ->
erlang:error(Reason)
end.
Now, my code would read almost as proposed (getting rid of the
{ok,Result} matches) and the observer can try-catch errors if needed,
obtaining the actual exit reason. This way, functions always return a
valid term, failing otherwise.
However, that comes with some problems:
- I have to wrap a lot of things
- I can not easily distinguish expected errors from bugs (badmathces,
undefs, etc) any more, since they fall in the same catch category.
- I feel going against the tide since supervisors and gen_servers are
not particulary happy with this philosophy (they stubbornly catch
error signals on startups and return {error, Reason} tuples; among
other nuisances).
I can solve the second point changing erlang:error() with exit() thus
catching exit signals, but it leaves a dirty feeling (I don't want to
exit really, I want to signal an error). The other two are solved with
a fair amount of adapting code. For example, I build my gen_servers using
these utility functions:
-module(foo_lib).
server_call(Server, Request) ->
case gen_server:call(Server, Request) of
{error, Reason} ->
exit(Reason);
Value ->
Value
end.
secure_call(Request, From, State, Module, CallFunc) ->
try Module:CallFunc(Request, From, State) of
Result when is_tuple(Result), size(Result) == 3 ->
Result;
_ ->
%% Pretty defensive against annoying bugs
erlang:error(bad_call_return)
catch
error:Reason ->
{reply, {error, Reason}, State};
end.
So in the server module, handle_call is implemented like this:
handle_call(Request, From, State) ->
foo_lib:secure_call(Request, From, State, ?MODULE, handle_call2).
handle_call2({bar, Args}, _From, State) ->
{Reply, NewState} = handle_bar(Args),
{reply, Reply, NewState};
I wonder whether this "make it crash (TM)" approach is somewhat overkill.
Regards
--
Samuel
More information about the erlang-questions
mailing list