[eeps] EEP 049: Value-Based Error Handling Mechanisms

Fred Hebert mononcqc@REDACTED
Fri Dec 7 14:45:44 CET 2018


On 12/07, Kenneth Lundin wrote:
>
>We agree about that it would be nice to be able to replace or simplify
>deeply-nested case ... end expressions except that we don't see a problem
>with using throw (a mechanism to make a non local return) as long as it is
>done in a safe way. We don't regard throw as an exception, and it is
>documented as "a non local return".
>

As Anthony mentioned, it is literally defined as an exception. But an 
exception specifically used for control-flow.

I would also object to the notion that "using a mechanism to make a non 
local return in a safe way". This is a bit like "there is no danger to 
holding a knife by the blade, as long as it is done in a safe way". The 
only way to know that what you're doing is unsafe is to have cut 
yourself with it too many times before.

If the objection to safety is "you just have to be careful", it's 
essentially just saying "we don't think it's a problem, deal with it", 
which is what I think should be said here instead of pretend concern. I 
am okay with the more honest and direct approach.

>Comments on the solution
>
>In most of the examples where we find usage of nested case, the unwanted
>result ({error, Reason} in this case) is not just returned, there is some
>other actions performed as well before returning. In those cases the
>proposed solution does not help.
>
>   - We don't like a language construct which is hard coded to support
>ok,{ok,Result},
>   {error,Reason}.
>   - the use of underscore _ <~ to mean a match with ok is not a hit, it
>   will make programs harder to read
>   - We are against the introduction of *unwrapexprs* that cannot be used
>   everywhere where expressions are allowed.
>   - The *unwrapexpr* changes the scoping rules and can not be used in
>   nested expressions and not outside begin ... end.

This is understandable.

>
>It is perfectly possible to use throw and try catch to replace or simplify
>deeply-nested case ... end expressions in the same way as the proposed
>language extension does.
>Example
>
>commit_write2(OpaqueData) ->
>    Ref = erlang:make_ref(),
>    Ok = fun(ok) -> ok; ({ok,R}) -> R; ({error,_Reason} = E) -> throw(
>{Ref,E}) end,
>    try
>        Ok(disk_log:sync(OpaqueData#backup.file_desc)),
>        Ok(disk_log:close(OpaqueData#backup.file_desc)),
>        Ok(file:rename(OpaqueData#backup.tmp_file, OpaqueData#backup.file)),
>        {ok, OpaqueData#backup.file}
>    catch
>        {Ref,E} ->
>            E
>    end.


Please, just encourage people to use nested cases and never encourage 
them to use this god-awful construct. What you presented here:

- requires a full synchronisation of processes to allocate a unique ref
- requires reimplementing a closure in scope of every place that wants 
  to handle errors
- does not prevent handling the 'unwrapexppr' differently since it 
  supports no nesting: the only handling is done literally at the 
  current scope since it relies on 'Ref' being in context
- any support for error propagation at a deeper level requires to pass 
  the Ref explicitly
- any support for multilayered error handling requires to literally have 
  a try ... catch ... end block at every level which may or may not 
  error

If I could ask for someone to redact and retract an e-mail, this would 
be it. I think this is plainly a terrible idea to recommend to people 
and would rather thave 6 levels of nesting than having to maintain code 
using this.

If it were a good pattern, we would all have been using it already since 
the 20+ years of open source Erlang that exist, and I have _never_ seen 
it used once seriously.

There has to be a reason.

>
>%% We could simplify for the user even more and at the same time encourage the
>%% standard pattern ok, {ok,Result}, {error,Reason} by creating a library
>%% function like this (and perhaps place it in stdlib). To start with
>the user can
>%% make his own function or fun for this.
>
>ok() ->
>    Ref = erlang:make_ref(),
>    Ok = fun(ok) -> ok;
>            ({ok,R}) -> R;
>            ({error,_Reason} = E) ->
>                 throw( {Ref,E})
>         end,
>    {Ref,Ok}.
>
>
>%% Here is the same example using the library function
>commit_write3(OpaqueData) ->
>    {Ref,Ok} = ok(),
>    try
>        Ok(disk_log:sync(OpaqueData#backup.file_desc)),
>        Ok(disk_log:close(OpaqueData#backup.file_desc)),
>        Ok(file:rename(OpaqueData#backup.tmp_file, OpaqueData#backup.file)),
>        {ok, OpaqueData#backup.file}
>    catch
>        {Ref,E} ->
>            E
>    end.
>

This is as bad and has all the same limitations, it just prevents having 
to declare the closure yourself.

>
>%% This is another example from EEP 49
>maybe() ->
>    case file:get_cwd() of
>        {ok, Dir} ->
>            case
>                file:read_file(
>                  filename:join([Dir, "demo", "data.txt"]))
>            of
>                {ok, Bin} ->
>                    {ok, {byte_size(Bin), Bin}};
>                {error, Reason} ->
>                    {error, Reason}
>            end;
>        {error, Reason} ->
>            {error, Reason}
>    end.
>
>
>%% The example above can be written like this without any new language
>%% constructs and the ok() function as a library function
>%%
>-spec maybe2() -> {ok, non_neg_integer()} | {error, term()}.
>maybe2() ->
>    {Ref,Ok} = ok(),
>    try
>        Dir = Ok(file:get_cwd()),
>        Bin = Ok(file:read_file(filename:join([Dir, "demo", "data.txt"]))),
>        {ok, {byte_size(Bin), Bin}}
>    catch
>        {Ref,ErrorReason} ->
>            ErrorReason
>    end.
>

The same criticism I had earlier still applies. It's just that now you 
need a multi-way construct (ok() + try ... catch + Ok() + {Ref, Err}) 
rather than a language construct.

case ... end expressions that are nested are a much better and 
definitely safer approach.


>Summary
>
>   - We say no to the proposed language extensions. We don't think they are
>   general enough and we also see some problems with them.
>   - The same effect can be achieved safely with the current language using
>   throw, try...catch.
>   - Encouraging ok, {ok,Result}, {error,Reason} as results from functions
>   can be done in other ways, for example through library functions. These
>   values should not be special to the *language*.
>   - We also want to thank the author for a very well thought through and
>   well documented proposal which has triggered us to think about possible
>   solutions in this area. We really appreciate the effort.
>
>/Kenneth, Erlang/OTP Ericsson

- I am okay with the OTP team denying the change
- The same effect cannot be achieved safely with try ... catch as you 
  have accidentally demonstrated here. The solutions you proposed in any 
  form of generalization turn out to be in no way safer than using a 
  bare throw since you have to remember to handle all the Refs 
  explicitly, and can introduce tricky exception leaks where someone 
  mis-handles an exception because your error signaling of unknown cases 
  is basically in the same channel as your non-exceptional signaling.

  If you want equivalent safety, use nested case ... end expressions as 
  cumbersome as they are. At least the data flow is obvious, and 
  unhandled cases result in exceptions rather than just being raised up 
  a level through the same handling pipeline
- Sure, I can understand that position
- No problem

But please, please, please. Do not try to implement any kind of 
throw-based generalisation as a response to this EEP. It is a terrible 
idea.  Nobody does it because nobody likes it. We all collectively 
thought about it already and decided it sucks compared to nested cases.  
We've had about 20 years for that.

Regards,
Fred.



More information about the eeps mailing list