[
https://issues.apache.org/jira/browse/GROOVY-12326?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18110477#comment-18110477
]
ASF GitHub Bot commented on GROOVY-12326:
-----------------------------------------
paulk-asert merged PR #2853:
URL: https://github.com/apache/groovy/pull/2853
> ChannelSelect: guard an offer directly with receive(c).when(cond)
> -----------------------------------------------------------------
>
> Key: GROOVY-12326
> URL: https://issues.apache.org/jira/browse/GROOVY-12326
> Project: Groovy
> Issue Type: Improvement
> Reporter: Paul King
> Priority: Major
>
> h2. Summary
> GROOVY-12324 added a precondition mask to {{ChannelSelect}}, and it works.
> Building a real guarded ALT
> against it surfaced a second, separable gap: the mask binds each flag to an
> offer *positionally and
> anonymously*.
> {code:groovy}
> def alt = ChannelSelect.from(put, get) // written here
> ...
> def r = await alt.select(counter < cap, counter > 0) // ...guarded here, by
> position only
> {code}
> Nothing at the selection site says which channel {{counter < cap}} guards.
> The reader counts arguments and
> matches them against a {{from(...)}} written elsewhere -- possibly far away,
> since the instance is normally
> hoisted so that {{fair()}} keeps its rotation state (GROOVY-12320). That is
> the same class of fragility the
> mask was introduced to remove; GROOVY-12324 stopped the *index* from moving,
> and left the *flag*'s
> association to a branch just as implicit.
> This proposes attaching the guard to the offer itself, which is what occam,
> Ada, Erlang and Kotlin all do.
> h2. Proposal
> {code:java}
> // on ChannelSelect.Offer
> public Offer when(java.util.function.BooleanSupplier enabled)
> {code}
> {code:groovy}
> def alt = ChannelSelect.offers(receive(put).when { counter < cap },
> receive(get).when { counter > 0 }).fair()
> while (true) {
> def r = await alt.select()
> ...
> }
> {code}
> h3. Why a supplier and not a plain boolean
> This is the part that only shows up when you build with it. A guard must be
> re-evaluated on every
> selection, because the condition it tests changes every iteration -- that is
> the whole point of a bounded
> buffer. An {{Offer}} is documented as "Immutable and freely reusable across
> selects", and a select instance
> is normally built ONCE and hoisted out of the loop so that {{fair()}} can
> keep its rotation state.
> So {{when(boolean)}} would be wrong in two different ways:
> * built once and hoisted, it freezes the guard at construction and every
> later selection uses a stale flag;
> * rebuilt each iteration to avoid that, it discards the {{lastWinner}}
> rotation -- reintroducing exactly the
> bug GROOVY-12320 was filed to fix, and which the checker already refuses as
> "fair() on a fresh
> ChannelSelect instance each iteration keeps no rotation state (priority in
> effect)".
> A {{BooleanSupplier}} evaluated at each {{select()}} call is the only form
> that keeps a held instance and a
> per-iteration guard at the same time. In Groovy it reads as a closure, and a
> closure over a mutable local
> sees the updates, so the bounded-buffer idiom above works as written.
> h3. Interaction with select(boolean...)
> An offer's {{when}} and a flag passed to {{select(boolean...)}} should be
> *conjoined*: the offer is enabled
> when both hold. That is the only reading that does not silently ignore one of
> the two. (Rejecting the
> combination outright is also defensible; what should not happen is one
> quietly winning.)
> Everything else is inherited unchanged: indices are preserved, and if every
> offer ends up disabled the
> returned awaitable already fails with {{IllegalStateException}}.
> h2. Open question -- worth settling before the API ships
> Nothing carrying GROOVY-12324 has been released yet, so the shape is still
> free. That makes this the moment
> to ask a question that cannot be asked afterwards: *do we want both forms, or
> should {{when}} be the only
> one?*
> The argument for keeping both is that they serve different entry points
> rather than duplicating each other:
> * {{select(boolean...)}} works directly with {{from(channels...)}}, which is
> what most code writes, and is
> the direct port of JCSP's {{priSelect(boolean\[\])}} and GPars'
> {{select(List<Boolean>)}} -- useful to
> anyone migrating;
> * {{when}} needs {{offers(receive(c)...)}}, which is more verbose for the
> common all-receives case, but
> binds guard to branch syntactically.
> If both are kept, the javadoc should lead with {{when}} as the idiomatic form
> and present the mask as the
> positional/compatibility one. If only one survives, it should be {{when}}.
> h2. Prior art
> Attaching the guard to the branch is the majority design; the positional mask
> is the minority one, and it
> exists mainly where the result is a bare index.
> || System || Guard is written... || Bound to its branch by ||
> | occam / occam-pi | {{bool & c ? x}} | syntax |
> | Ada | {{when Cond =>}} on a select alternative; entry barriers | syntax |
> | Erlang / Elixir | {{Pattern when Guard ->}} | syntax |
> | Kotlin coroutines | conditional clause registration in {{select \{ \}}} |
> the clause itself |
> | Go | {{nil}} channel in the case expression | the case |
> | JCSP | {{priSelect(boolean\[\] preCondition)}} | position |
> | GPars | {{select(List<Boolean> mask)}} | position |
> | Groovy 6 today | {{select(boolean... enabled)}} | position |
> h2. Implementation sketch
> Small, and contained to {{Offer}} plus one line in the registration loop.
> * {{Offer}} gains a {{BooleanSupplier enabled}} field, null meaning "always".
> {{when(...)}} returns a new
> {{Offer}} with the same channel/value/send and the given supplier,
> preserving immutability and
> reusability.
> * {{registrationOrder}} already skips offers the mask disables; extend its
> test from
> {{enabled == null || enabled\[i\]}} to also consult
> {{offers.get(i).enabled}}, evaluating the supplier
> once per {{select()}} call. The existing empty-order path then produces the
> {{IllegalStateException("every offer of the select is disabled")}} with no
> further change.
> * No change to {{Result}}, the claim protocol, {{withdraw}} or {{resend}}.
> * {{from(...)}} is unaffected: it builds plain receive offers with no guard.
> h2. Provenance
> Found by building a static verifier's model of GROOVY-12324 against Jon
> Kerridge's UCaPE c05 {{Queue}}
> (the bounded circular buffer that masks both of its guards) -- the same tool
> and the same corpus behind
> GROOVY-12320, GROOVY-12323 and GROOVY-12324. The mask itself is not at fault
> and needs no change; this is
> about where the guard is written.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)