When you learn programming, you're usually told that side-effects are not good. This is particularly true for the Petri nets annotations in SNAKES.
Consider this first example:
from snakes.nets import *
class Queue (object) :
def __init__ (self, values) :
self.v = list(values)
def done (self) :
return not self.v
def next (self) :
return self.v.pop(0)
def __str__ (self) :
return "Queue(%r)" % self.v
def __repr__ (self) :
return "Queue(%r)" % self.v
net = PetriNet("bad")
net.add_place(Place("input"))
net.add_place(Place("output"))
trans = Transition("t", Expression("not x.done()"))
net.add_transition(trans)
net.add_input("input", "t", Variable("x"))
net.add_output("output", "t", Expression("x.next()"))
net.place("input").add(Queue(range(10)))
It declares a class Queue
that stores a list from which we can
consume one element after the other. Then we put an instance of it
with integers from 0 to 9 in a place input, and a transition consumes
it to produce the first element picked from the queue in a place
output. Let's try it:
>>> execfile("side-effects.py")
>>> net.get_marking() #1
Marking({'input': MultiSet([Queue([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])])})
>>> m = trans.modes()
>>> m #2
[Substitution(x=Queue([1, 2, 3, 4, 5, 6, 7, 8, 9]))]
>>> trans.fire(m[0])
>>> net.get_marking() #3
Marking({'output': MultiSet([2])})
There are problems here: from #1
we see that the next value for the
Queue
instance should be 0
. But after #2
we see that it now
starts with 1
. Later, from #3
we can seen that 1
has also been
skipped and we get 2
instead.
The reason is that expressions are evaluated several time, and side-effects are remembered between two evaluations. This becomes explicit here:
class NotBetter (Queue) :
def next (self) :
print("%s.next()" % self)
return Queue.next(self)
net = PetriNet("not better")
net.add_place(Place("input"))
net.add_place(Place("output"))
trans = Transition("t", Expression("not x.done()"))
net.add_transition(trans)
net.add_input("input", "t", Variable("x"))
net.add_output("output", "t", Expression("x.next()"))
net.place("input").add(NotBetter(range(10)))
print("calling trans.modes()")
m = trans.modes()
print("calling trans.fire()")
trans.fire(m[0])
Let's run it:
$ python side-effects.py
calling trans.modes()
Queue([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).next()
calling trans.fire()
Queue([1, 2, 3, 4, 5, 6, 7, 8, 9]).next()
Queue([2, 3, 4, 5, 6, 7, 8, 9]).next()
Printing and more generally any input/output is another kind of side
effects. We discover here that method next
has been called actually
three times: once during trans.modes()
and twice during
trans.fire()
. The reason is as follows
(see also here):
trans.modes()
builds a binding for every combination of input tokens. Each of those bindings is a mode if it allows to evaluate the guard toTrue
and it allows to produce tokens that are accepted by the output places. This is exactly in this latter check that methodnext
has to be calledtrans.fire()
is given a binding and has to check this is actually a mode, so it does exactly the same checks astrans.modes()
and thus also evaluatesx.next()
. Then, the transition is actually fired and sox.next()
is called a third time to actually compute the tokens to be produced
While this process is largely suboptimal, it is on the other hand
simple to understand and to implement. We could imagine that
trans.modes()
returns bindings enriched with the information
computed for the output arcs, which would avoid so many evaluations.
But this would be somehow misleading for the user to get modes with a
richer content than expected; and it would also seriously complexify
the implementation. Moreover, it wouldn't solve everything. Imagine we
want to get rid of method done
and implement as follow:
class StillBad (Queue) :
def next (self) :
if self.v :
return self.v.pop(0)
else :
return None
net = PetriNet("still bad")
net.add_place(Place("input"))
net.add_place(Place("output"))
trans = Transition("t", Expression("x.next() is not None"))
net.add_transition(trans)
net.add_input("input", "t", Variable("x"))
net.add_output("output", "t", Expression("x.next()"))
Now, x.next()
is called twice explicitly. If it had no side-effect,
this would be actually correct.
So, the good solution is to have functional Python in nets annotations
(i.e., in every Expression
instance), in the sense that every
annotation is a "pure" expression with no side-effect. If no object if
ever mutated, or every mutation occurs on a copy, your nets will not
exhibit the kind of wrong behaviour we have observed here above. Take
care also to assignments to global/class/modules variables,
inputs/outputs, and everything that can be considered as a
side-effect.