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 BadRange (object) :
def __init__ (self, *args) :
self.v = range(*args)
def done (self) :
return not self.v
def next (self) :
return self.v.pop(0)
def __str__ (self) :
return str(self.v)
def __repr__ (self) :
return repr(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(BadRange(10))
Let’s try it under IPython:
In [1]: %run side-effects.py
In [2]: net.get_marking()
Out[2]: Marking({'input': MultiSet([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])})
In [3]: m = trans.modes()
In [4]: m
Out[4]: [Substitution(x=[1, 2, 3, 4, 5, 6, 7, 8, 9])]
In [5]: trans.fire(m[0])
In [6]: net.get_marking()
Out[6]: Marking({'output': MultiSet([2])})
There are two problems here: Out[2]
shows that the next value for
the BadRange
instance should be 0
. But in Out[4]
we see that it
now starts with 1
. Later, in Out[6]
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 NotBetterRange (BadRange) :
def next (self) :
print("%s.next()" % self)
return BadRange.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(NotBetterRange(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()
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].next()
calling trans.fire()
[1, 2, 3, 4, 5, 6, 7, 8, 9].next()
[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:
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 StillBadRange (object) :
def __init__ (self, *args) :
self.v = range(*args)
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.
A quick and (not so) dirty way to do so is to copy the object and let the side-effect take place on the copy:
class BetterRange (object) :
def __init__ (self, *args) :
self.v = range(*args)
def next (self) :
other = self.__class__(0)
other.v = list(self.v)
return other.v.pop(0)
def done (self) :
return not self.v
This can be simplified with a decorator that tries to call
self.copy()
and falls back to calling deepcopy(self)
if such a
method is not available. As a result, a method decorated with @copy
operates on a copy of self
and not on self
itself so that
side-effects take place on the copy and leave the original object
untouched.
from copy import deepcopy
from functools import wraps
def copy (method) :
@wraps(method)
def newmethod (self, *l, **k) :
if hasattr(self, "copy") :
other = self.copy()
else :
other = deepcopy(self)
return method(other, *l, **k)
return newmethod
class SimplerRange (object) :
def __init__ (self, *args) :
self.v = range(*args)
@copy
def next (self) :
return self.v.pop(0)
def done (self) :
return not self.v
Note that this works only for side-effects on the object itself, not for more general side-effects like assignment to a global variables or inputs/outputs. So, as usual, there is no silver bullet and you just have to avoid side-effects to be safe.