Petri Nets Log #009

siiky

2024/07/14

2024/07/15

en

On the previous log I wrote about token philosophy and how I made a bad implementation decision.

However, not all is lost if the option is given the the programmer: the same strategy that is implemented in gen_pnet can be offered as an alternative, on a transition-by-transition basis. It should be used only as a last resort, because it's slow and most often it'll only be used for "RPC"-related paths, in the input interface places and internally. Another alternative is to choose a field of a tuple (e.g. the 1st) to serve as the "key" to group tokens of different places.

The current PNEE:consume/1 function has a very simple interface, similar to a DNF: it must return a list of alternative sets of places to consume from (optionally with a multiplicity, and optionally with a token-predicate).

consume(a) -> [[coin_slot]];
consume(b) -> [[signal, storage]].

This option should be the most painless to use, and I believe it covers the vast majority of all transitions. The implementation is pretty simple, and the performance is linear. The other alternatives could be provided with a helper function, something like this:

consume(send_result) -> pnee:over_all(
  [get_res_reqs, done_reqs],
  % The tokens should be accepted only if they're of the same request
  fun(#{get_res_reqs:=[{ReqId, _Foo}], done_reqs:=[{ReqId, _Result}]}) ->
    true
  end
).

% OR

% Use erlang:element/2 to get each token's request ID
consume(send_result) -> pnee:by_key(1, [get_res_reqs, done_reqs]).

Compared to the simplest (default) consume algorithm, pnee:by_key/2 shouldn't be much slower, and seems to cover a large number of common cases, although it still isn't generic enough to offer the full high-level Petri net experience. pnee:over_all/2 is there for that.

With this ease-of-use hierarchy I expect the programmer will be lead to the best performance-wise option on each case.