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

Kenneth Lundin kenneth@REDACTED
Thu Oct 22 12:52:49 CEST 2020


See embedded comments

On Thu, Oct 15, 2020 at 4:58 PM Fred Hebert <mononcqc@REDACTED> wrote:

>
> 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.
>
> I think the middle bullet (above this) is the most interesting. The LHS <~
RHS expression should be thought of as MATCH_OR_BREAK or MATCH_OR_RETURN a
conditional return as I think you mention somewhere.
The scope to break out from is function clause, begin/end, case clause and
probably something else which I have not thought of yet.
If we introduce this maybe it is strange to not introduce an unconditional
return as well.
Will take a closer look into the 'with' construct in Elixir and see if
there is anything more we could copy.
Note, I have discussed this briefly with some of the OTP team members, see
this as an initial view point not written in stone.

>
>    - 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.
>

/Kenneth, Erlang/OTP Ericsson

> _______________________________________________
> eeps mailing list
> eeps@REDACTED
> http://erlang.org/mailman/listinfo/eeps
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://erlang.org/pipermail/eeps/attachments/20201022/00abf191/attachment.htm>


More information about the eeps mailing list