Paul King created GROOVY-12326:
----------------------------------

             Summary: 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


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)

Reply via email to