<div dir="ltr"><div dir="ltr"></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Thu, Oct 15, 2020 at 3:31 AM Kenneth Lundin <<a href="mailto:kenneth@erlang.org">kenneth@erlang.org</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr"><div dir="ltr"><div><div><div>
<div>
<div style="color:black;font-size:12pt;font-family:Calibri,Arial,Helvetica,sans-serif">We welcome initiatives like this and are positive to revisit this.</div>
<div style="color:black;font-size:12pt;font-family:Calibri,Arial,Helvetica,sans-serif">
A proposal for something closer to <i>with</i> in Elixir looks interesting.
</div></div></div></div></div></div></div></blockquote><div><br></div><div>Alright. So before I get into the big details, here are a few things / variables I'm considering if we are to redesign this. <br></div><div><br></div><div>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.<br></div><div> <br>First, dropping the normative return values of <span style="font-family:monospace">ok | {ok, T} | {error, R}</span>:</div><div><br></div><div><span style="font-family:monospace">begin<br> {ok, A} <~ exp(),<br> {ok, B} <~ exp(),<br> {ok, A+B}<br>end.</span><br></div><div><br></div><div>This has interesting impacts in some of the examples given in the EEP, specifically in that <span style="font-family:monospace">_ <~ RHS</span> now means as much as <span style="font-family:monospace">_ = RHS</span>, but also that it allows rewriting some forms. The RFC looked at expressions such as:</div><div><br></div><div><span style="font-family:monospace">backup_releases(Dir, NewReleases, Masters, Backup, Change, RelFile) -><br> case at_all_masters(Masters, ?MODULE, do_copy_files, [RefFile, [Backup, Change]]) of</span></div><div><span style="font-family:monospace"> ok -></span></div><div><span style="font-family:monospace"> ok;<br></span></div><div><span style="font-family:monospace"> {error, {Master, R}} -><br> remove_files(Master, [Backup, Change], Masters)<br> end. </span><br></div><div><br></div><div>Could now be written the following way:<br></div><div><br></div><div><span style="font-family:monospace">backup_releases(Dir, NewReleases, Masters, Backup, Change, RelFile) -><br> begin<br> {error, {Master, R}} <~ at_all_masters(Masters, ?MODULE, do_copy_files, [RefFile, [Backup, Change]]), <br> remove_files(Master, [Backup, Change], Masters)<br> end.</span><br></div><div><br></div><div>Which looks a bit funny because the main path is now the error path and the happy-path is fully removed from the situation.</div><div><br></div><div>In any case, this is the most minimal rework required, has some edge cases pointed out by the EEP already (a match for <span style="font-family:monospace">{ok, [_|_]=L} <~ RHS</span> can end up doing a short return for <span style="font-family:monospace">{ok, Tuple}</span> for example, and interfere with expected values). I'll call this <b>proposal rewrite 1. </b><br></div><div><br></div><div>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 <span style="font-family:monospace">else</span> construct in the elixir <span style="font-family:monospace">with</span>:</div><div><br></div><div><span style="font-family:monospace">%% because of `f()', this returns `{ok, "hello"}' instead of `{ok, <<"Hello">>}'<br>%% or a badmatch error.<br>f() -> {ok, "hello"}.<br>validate(IoData) -> size(IoData) > 0.<br>sanitize(IoData) -> string:uppercase(IoData).<br><br>fetch() -><br> begin<br> {ok, B = <<_/binary>>} <~ f(),<br> ok <~ validate(B),<br> {ok, sanitize(B)}<br> end.</span></div><div><span style="font-family:monospace"><br></span></div><div><span style="font-family:monospace">%% Only workaround:<br>fetch_workaround() -><br> begin<br> {ok, B = <<_/binary>>} <~ f(),<br> ok <~ validate(B),<br> {ok, sanitize(B)}<br> else<br> {ok, [_|_]} -> ...<br> end.</span></div><div><span style="font-family:monospace"><br></span></div><div>This format might work, but requires introducing new extensions to the <span style="font-family:monospace">begin ... end</span> form (or new things like<span style="font-family:monospace"> maybe ... end</span>), regardless of terms. In terms of semantics, a <span style="font-family:monospace">catch</span>, <span style="font-family:monospace">of</span>, or <span style="font-family:monospace">after</span> 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 <b>proposal rewrite 2</b>. <b>Variant A</b> would be to keep it as described above, and <b>Variant B</b> would include the potential options with other alternative keywords and blocks.<br></div><div><br></div><div>Either way, dropping the pattern and changing constructs maintains the overall form and patterns described in the EEP. They however still keep <span style="font-family:monospace">LHS <~ RHS</span> 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.</div><div><br></div><div>First, we need to define where a free-standing <span style="font-family:monospace">LHS <~ RHS</span> 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. <br></div><div><br></div><div>We'd first have to choose which scope nested expressions would need to return to:</div><div><br></div><div> <span style="font-family:monospace"> a() -><br> V = case f() of<br> true -> <br> ok <~ g(),<br> h();<br> false -><br> {ok, X} <~ i(),<br> k(element(2, {ok, Y} <~ j(X)))<br> end,<br> handle(V).</span><br></div><div><br></div><div>This is an interesting test bed for some possible execution locations where the new operator could be bound. We could pick:</div><div><ul><li>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 <span style="font-family:monospace">{ok, X} <~ i()</span> implies that <span style="font-family:monospace">a()</span> itself can return <span style="font-family:monospace">i()</span>'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.</li><li>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 <span style="font-family:monospace">{ok, X} <~ i()</span> failing implies that <span style="font-family:monospace">V</span> gets bound to the return value of <span style="font-family:monospace">i()</span>, and similarly for the return value of <span style="font-family:monospace">j()</span> if it were to fail. Upon seeing a <span style="font-family:monospace">LHS <~ RHS</span> 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 <span style="font-family:monospace">[,]</span>), 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.</li><li>Something else I haven't thought of<br></li></ul><div>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 <b>proposal rewrite 3</b>. 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 <span style="font-family:monospace">return</span> somewhat scoped like a <span style="font-family:monospace">continue</span> in an imperative language) rather than a new operator within a bound construct.<br></div><div><br></div><div>An interesting <b>Variant B</b> for this one would be that since we make the expression general, we could change the <span style="font-family:monospace">LHS <~ RHS</span> expression to instead be <span style="font-family:monospace">LHS <- RHS</span> 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 <span style="font-family:monospace">[handle(X) || {ok, X} <- [i()]]</span>.</div><div><br></div><div>Let me know what you think about these.<br></div></div></div></div>