Wednesday, March 31, 2010

The perils of side effects, an example

I've just spent two hours of my life debugging a problem that demonstrates perfectly the problem of side effects in functions. The code in question was fairly unremarkable:

print convert_to_customer_format(data, default_item)
customer_interface.send(convert_to_customer_format(data, default_item))

the print line had been innocently added for debug, but it caused an error in the customer interface component, as the line had been added to assist with debug on the customer interface component it was a while before the error was traced back to the code above. We log all calls and returns from functions, so it didn't take long to realise that what we were printing was different to what we were trying to send, so suspicion quickly fell on the conversion function:

def convert_to_customer_format(data, default_item):
#turn into list
for key in data.keys():
data[key].insert(0, key)
data = data.values()
#pad with empty items
default_item.insert(0, 'Empty')
number_of_pads = range(len(data), 16)
for x in number_of_pads:
return data

It's not a very pretty function, but you if you run it like so:

default_item = [0, 0, 0, 0]
data = defaultdict(lambda:default_item)
data['foo'] = [1, 2, 3, 4]
data['bar'] = [5, 6, 7, 8]
x = side_effect(data, default_item)
print x
y = side_effect(data, default_item)
print y

You'll see that it changes the data each time. You wouldn't have this kind of trouble in Haskell! It got me thinking though, could I make a decorator that would force a function to not have side effects, or at least raise an exception if it did. I think I've done it, i'd appreciate comments:

def no_side_effects(func):
def inner_func(*args, **kwargs):
pre_call_args = copy.deepcopy(args)
pre_call_kwargs = copy.deepcopy(kwargs)
print 'pre_call: %s| %s' % (pre_call_args, pre_call_kwargs)
result = func(*args, **kwargs)
print 'post call: %s| %s' % (args, kwargs)
if args == pre_call_args and kwargs == pre_call_kwargs:
return result
raise Exception('Side effect found: Function altered the arguments')
return inner_func

No comments:

Post a Comment