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:

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.