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

Fred Hebert mononcqc@REDACTED
Thu Oct 15 16:58:23 CEST 2020


On Thu, Oct 15, 2020 at 3:31 AM Kenneth Lundin <kenneth@REDACTED> wrote:

> We welcome initiatives like this and are positive to revisit this.
> A proposal for something closer to *with* in Elixir looks interesting.
>

Alright. So before I get into the big details, here are a few things /
variables I'm considering if we are to redesign this.

I'm labelling them as below into proposal rewrites 1 through 3, some with
variants. Let me know if some of them sound more interesting.



First, dropping the normative return values of ok | {ok, T} | {error, R}:

begin
    {ok, A} <~ exp(),
    {ok, B} <~ exp(),
    {ok, A+B}
end.

This has interesting impacts in some of the examples given in the EEP,
specifically in that _ <~ RHS now means as much as _ = RHS, but also that
it allows rewriting some forms. The RFC looked at expressions such as:

backup_releases(Dir, NewReleases, Masters, Backup, Change, RelFile) ->
    case  at_all_masters(Masters, ?MODULE, do_copy_files, [RefFile,
[Backup, Change]]) of
        ok ->
            ok;
        {error, {Master, R}} ->
            remove_files(Master, [Backup, Change], Masters)
    end.

Could now be written the following way:

backup_releases(Dir, NewReleases, Masters, Backup, Change, RelFile) ->
    begin
        {error, {Master, R}} <~ at_all_masters(Masters, ?MODULE,
do_copy_files, [RefFile, [Backup, Change]]),
        remove_files(Master, [Backup, Change], Masters)
    end.

Which looks a bit funny because the main path is now the error path and the
happy-path is fully removed from the situation.

In any case, this is the most minimal rework required, has some edge cases
pointed out by the EEP already (a match for {ok, [_|_]=L} <~ RHS can end up
doing a short return for {ok, Tuple} for example, and interfere with
expected values). I'll call this *proposal rewrite 1. *

We can't avoid the above escaping mechanism without normalizing over what
is an acceptable or unacceptable good match value, and proposal rewrite 1
makes this impossible. This requires adding something akin to the else
construct in the elixir with:

%% because of `f()', this returns `{ok, "hello"}' instead of `{ok,
<<"Hello">>}'
%% or a badmatch error.
f() -> {ok, "hello"}.
validate(IoData) -> size(IoData) > 0.
sanitize(IoData) -> string:uppercase(IoData).

fetch() ->
    begin
        {ok, B = <<_/binary>>} <~ f(),
        ok <~ validate(B),
        {ok, sanitize(B)}
    end.

%% Only workaround:
fetch_workaround() ->
    begin
        {ok, B = <<_/binary>>} <~ f(),
        ok <~ validate(B),
        {ok, sanitize(B)}
    else
        {ok, [_|_]} -> ...
    end.

This format might work, but requires introducing new extensions to the begin
... end form (or new things like maybe ... end), regardless of terms. In
terms of semantics, a catch, of, or after block might reuse existing
keywords but wouldn't be as clear in terms of meaning. Specifically
addressing this requirement that comes from relaxing semantics for proposal
1 is *proposal rewrite 2*. *Variant A* would be to keep it as described
above, and *Variant B* would include the potential options with other
alternative keywords and blocks.

Either way, dropping the pattern and changing constructs maintains the
overall form and patterns described in the EEP. They however still keep LHS
<~ RHS as a special expression type that is always contextual, which was
pointed out to be a thing the OTP team did not like. Making it apply
everywhere is a particularly tricky bit, but I think it might be possible.

First, we need to define where a free-standing LHS <~ RHS is going to
return. If it's free-standing it can't be a sort of macro trick for a case
expression, and it can't also be based on a throw, since throws can't
clearly disambiguate the control flow required for this construct vs.
random exceptions people could be handling at lower levels.I've seen in
EEP-52 that there is a core-erlang construct as a letrec_goto, and using it
we might be able to work with that.

We'd first have to choose which scope nested expressions would need to
return to:

        a() ->
            V = case f() of
                true ->
                    ok <~ g(),
                    h();
                false ->
                    {ok, X} <~ i(),
                    k(element(2, {ok, Y} <~ j(X)))
            end,
            handle(V).

This is an interesting test bed for some possible execution locations where
the new operator could be bound. We could pick:

   - shortcut the lexical scope: since case expressions and any other
   construct share the parent lexical scope and can export variables, we would
   have to expect that {ok, X} <~ i() implies that a() itself can return i()'s
   value directly if it doesn't strictly match the form, regardless of how
   deeply nested we are in the conditional. This is unlikely to be practical
   or expected to people, but would nest appropriately within funs. It may
   have very funny effects on list comprehensions when used as part of
   generators and that likely will need special treatment.
   - shortcut to the parent control flow construct / end of current
   sequence of expression: I don't know how to word this properly, but the
   idea would be to limit the short-circuit return to the prior branching or
   return point in the language. This means that {ok, X} <~ i() failing
   implies that V gets bound to the return value of i(), and similarly for
   the return value of j() if it were to fail. Upon seeing a LHS <~ RHS
   expression, the compiler would need to insert a label at the end of the
   current sequence of expressions (which may conveniently going to be
   explained as "all of the current expressions separated by a comma [,]),
   and do a conditional jump to it if the expression fails to match. If it
   works it keeps chugging along, and the last expression in the sequence can
   just jump to the same label with the identified return value. To my
   understanding, this wouldn't interfere with LCO nor require more
   stackframes than any other conditional would ever require.
   - Something else I haven't thought of

I also assume that none of these expressions would ever be valid in guards
since they can't do assignment today. I'm also unsure of whether
letrec_goto can use a label with arguments in what to execute (which would
let it carry/return a variable), but I'm waiting to do research on that on
whether this idea looks good or not to the OTP team. I think the lexical
scope option is unacceptable. I call the sequence of expressions
approach *proposal
rewrite 3*. This is much more ambitious and could have a ton weirder
unexpected effects, but it drops all pretense and introduces a new
operation type/control flow mechanism (which is comparable to a conditional
return somewhat scoped like a continue in an imperative language) rather
than a new operator within a bound construct.

An interesting *Variant B* for this one would be that since we make the
expression general, we could change the LHS <~ RHS expression to instead be LHS
<- RHS expression; after all, there is no unwrap involved anymore, and the
logical handling of this thing is now much closer to what you'd see in a
list comprehension such as [handle(X) || {ok, X} <- [i()]].

Let me know what you think about these.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://erlang.org/pipermail/eeps/attachments/20201015/6bb95a6d/attachment.htm>


More information about the eeps mailing list