I tend to agree that a construct like begin *expressions1* else _ -> *expression* end is attractive and copies the essence of the with construct in Elixir. The use of '<-' makes sense as well. I know we had an objection in the OTP team for introducing an operator '<~' that was not usable everywhere but in this case we are reusing '<-' that is already used in list comprehensions (as the only place).
In that case LHS '<-' RHS should only be allowed in the begin else _ -> ... end construct. I don't know if we have a problem with introducing 'else' as a keyword. Will that potentially break old existing code? On Thu, Oct 22, 2020 at 2:56 PM José Valim <[email protected]> wrote: > My initial thoughts about an approach where <~ is not lexically bound by > any construct is that it may make moving code around harder. > > For example, if you have this function: > > foo() -> > {ok, Value} <~ file:read_file("somefile.erl"), > Another = do_something_with(Value), > do_something_else(Another). > > If you are going to move the first two lines to a private function then > the automatic extraction does not work: > > foo() -> > Another = bar(), > do_something_else(Another). > > bar() -> > {ok, Value} <~ file:read_file("somefile.erl"), > do_something_with(Value). > > You have to do this: > > foo() -> > {ok, Another} <~ bar(), > do_something_else(Another). > > bar() -> > {ok, Value} <~ file:read_file("somefile.erl"), > do_something_with(Value). > > Which is doable but not obvious because the scope <~ applies to is not > immediately clear. I think something like this: > > foo() -> > begin > {ok, Value} <~ file:read_file("somefile.erl"), > Another = do_something_with(Value), > do_something_else(Another) > end. > > provides a clearer indicator of the scope and more clues that moving code > around requires extra work. The other benefit of having a delimiter is that > you can include else clauses that Fred mentioned, and I believe you will > quickly find out they are a must have. Imagine you want to perform many > operations that may fail and, if they do, you want to raise an error. > Without else, you have to do this: > > change_and_backup(File) -> > Res = > begin > {ok, Value} <~ file:read_file(File), > NewValue = do_something_with_value(Value), > ok <~ file:write_file(File ++ ".backup", Value), > ok <~ file:write_file(File, NewValue), > {ok, NewValue} > end, > > case Res of > {ok, NewValue} -> NewValue; > {error, Reason} -> erlang:error({backup_error, Reason}, [File]) > end. > > With else, you do this: > > change_and_backup(File) -> > begin > {ok, Value} <~ file:read_file(File), > NewValue = do_something_with_value(Value), > ok <~ file:write_file(File ++ ".backup", Value), > ok <~ file:write_file(File, NewValue), > NewValue > else > {error, Reason} -> erlang:error({backup_error, Reason}, [File]) > end. > > Note the compiler can raise if else is given but no <~ is used. > > If I remember correctly, Elixir's with originally started without else, > but else was added in the next release because it was seen as a very clear > extension of the original mechanism. I have included some examples from the > Elixir codebase where we use "with". I think the first example is really > clear on the benefit of usng "with" to perform validation, compared to > something like using try/catch: > > * > https://github.com/elixir-lang/elixir/blob/272303e558286dff35ace00eada6d2030431874d/lib/elixir/lib/dynamic_supervisor.ex#L355-L361 > * > https://github.com/elixir-lang/elixir/blob/272303e558286dff35ace00eada6d2030431874d/lib/elixir/lib/file.ex#L560-L574 > * > https://github.com/elixir-lang/elixir/blob/272303e558286dff35ace00eada6d2030431874d/lib/elixir/diff.exs#L83-L104 > * > https://github.com/elixir-lang/elixir/blob/272303e558286dff35ace00eada6d2030431874d/lib/elixir/lib/version.ex#L536-L546 > > Finally, another potential benefit of having a explicit delimiter is that > you don't need a new operator as you can re-use <- if you want to. > Especially because <- inside a comprehesion already has a "soft match" > semantics (i.e. it doesn't raise if it doesn't match). > > On Thu, Oct 22, 2020 at 12:53 PM Kenneth Lundin <[email protected]> > wrote: > >> See embedded comments >> >> On Thu, Oct 15, 2020 at 4:58 PM Fred Hebert <[email protected]> wrote: >> >>> >>> On Thu, Oct 15, 2020 at 3:31 AM Kenneth Lundin <[email protected]> >>> 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 >> >>> >>> /Kenneth, Erlang/OTP Ericsson
_______________________________________________ eeps mailing list [email protected] http://erlang.org/mailman/listinfo/eeps
