Reputation: 13632
(I want to cite another question as a reference: How do I elegantly check many conditions in Erlang?)
The generic form of "success case code separated from error handling" seems to be:
try
ok = do_x(),
...
ok = do_y()
catch
error:{badmatch, x_failure} -> do_something1();
...
error:{badmatch, y_failure} -> do_something2();
How does one use this pattern when the functions in the try clause do something with a side effect, such as write a file, sent a network packet, write a line into a database, etc? Is there a generic pattern for a "rollback" in the catch clause? Example:
try
%How to make this whole block a transaction?
ok = step_1_write_file(),
ok = step_2_write_database(),
ok = step_3_send_packet(),
...
catch
error:{badmatch, database_failure} -> clean_up_step_1() %delete file?
error:{badmatch, sendpacket_failure} -> clean_up_step_1_and_2() %??
It seems like the error handling gets onerous, where the cleanup that needs to be performed is dependent on the step in the try
block that failed.
Is there a general programming pattern that treats this as a transaction, whereas the succeeded steps in the try block preceding the failed clause are ``unwound"?
Upvotes: 4
Views: 245
Reputation: 7102
I personally learned to program such a algorithms by passing a list of 'validators' and, optionaly, 'finalizers' to some generic iterative function.
So, your case may be programmed like this:
noop() -> ok.
transaction([{Fun, Rollback} | Rest]) ->
try
{ok, Result} = Fun(),
[Result | transaction(Rest)]
catch Type:Reason ->
Rollback(),
erlang:raise(Type, Reason, erlang:get_stacktrace())
end;
transaction([Fun | Rest]) ->
% not every action require cleanup on error
transaction([{Fun, fun noop/0} | Rest]);
transaction([]) -> [].
main() ->
Actions = [
{fun write_file/0, fun cleanup_file/0},
{fun write_database/0, fun cleanup_database/0},
fun do_safe_thing/0,
{fun send_packet/0, fun cancel_send_packet/0},
],
transaction(Actions).
As you can see, since this list evaluated using body recursion, iteration through this list will form a stack of calls and, if at some step one of these functions will fall, stack will be unwinded and each cleanup function will be called in reverse order.
For example, if do_safe_ting/0
will fall, cleanup functions noop/0
, cleanup_database/0
and cleanup_file/0
will be called in this order.
Of course, this may be programmed in different way, not by re-throwing exceptions, but by, for example, returning {ok, Result}
and {error, Reason}
. It's just implementation details.
Upvotes: 4