Reputation: 4322
Just trying to understand what is and what isn't a side effect of a function in relation to functional programming. For example, if we have a very simple integer division function like this:
def integer_division(a, b, count=0):
if (a < 0) ^ (b < 0):
sign = -1
else:
sign = 1
if abs(a) - abs(b) == 0:
return (count + 1) * sign
elif abs(a) - abs(b) < 0:
return count * sign
else:
return integer_division((abs(a) - abs(b))*sign, b, count + 1)
if __name__ == '__main__':
print(integer_division(-5, 2))
Out: -2
Would sign
variable be considered a side effect? On one hand it is only defined within the function, but then on the other hand it is entirely based on the function parameters and as such shouldn't be one. While I was pondering the question I re-wrote the function as this...:
def integer_division_diff(a, b, count=0, sign=1):
if (a < 0) ^ (b < 0):
sign *= -1
if abs(a) - abs(b) == 0:
return (count + 1) * sign
elif abs(a) - abs(b) < 0:
return count * sign
else:
return integer_division((abs(a) - abs(b)) * sign, b, count + 1)
...which does exactly the same job and produces the same result, but which one is a more "correct" from a functional programming perspective?
EDIT: fixed a glaring issue when a is a multiple of b.
Upvotes: 0
Views: 178
Reputation: 2444
This function is not problematic, but there is a discussion to be had.
sign
has state and this state is managed by local side-effects in the shape of statements (which mutate the state). Now it's a very tiny local state, your brain is not going to explode, but there are other ways to write this function.
I am going to list some of them bellow and then I will try to make a statement. Note that I am not a pythonist myself so I let you judge what is more idiomatic.
def integer_division(a, b, count=0):
sign = -1 if (a < 0) ^ (b < 0) else 1
diff = abs(a) - abs(b)
return (
(count + 1) * sign if diff == 0
else count * sign if diff < 0
else integer_division(diff * sign, b, count + 1)
)
def integer_division(a, b, count=0):
sign = -1 if (a < 0) ^ (b < 0) else 1
diff = abs(a) - abs(b)
done = diff <= 0
if done:
return (count + (diff == 0)) * sign
else:
return integer_division(diff * sign, b, count + 1)
def integer_division(a, b, count=0):
sign = (1, -1)[(a < 0) ^ (b < 0)]
diff = abs(a) - abs(b)
done = diff <= 0
return (
lambda: integer_division(diff * sign, b, count + 1),
lambda: (count + (diff == 0)) * sign
)[done]()
(obviously don't create lambdas in a low level function, this is an example of how to use tuples as lazy conditional expressions)
Statements are not referentially transparent: a statement is not data, no variable can store what it evaluates to, you can't extract a single statement to a pure function, and when you are done reading a statement you can't move on to something else, because a statement is incomplete: it is linked to something else that you must look up and carry with you at the back of your head until it's finally out of scope.
Removing this kind of cognitive load is one of the things functional programming aims at, and expressions make it possible because they are self-contained.
Python doesn't have constants, but you can chose to never reassign variables as a convention and to prefer expressions over statements (with reason). Do this and your brain's backpack will get lighter because you will allow yourself to forget about how values came into existence. Your names will describe decisions that have been made, not decisions in the process of being made and that is invaluable.
Whether you find the propositions above to your liking or not, the fact is they make the key elements of the algorithm stand out. You can short-circuit your reading, trust that the right-hand side of assignments is correct and go on your merry way instead of digging for informations.
Upvotes: 1