Someone mentioned idempotence at work. I looked it up and noted that it too is a property of functions, like function purity.
I decided to see if I could write functions with combinations of those properties and embedded tests for those properties.
Running my resultant program produces this result:
Created on Fri Mar 3 18:04:09 2023
@author: Paddy3118
pure_idempotent.py
Explores Purity and idempotence with Python examples
Definitions:
Pure:
* Answer relies solely on inputs. Same out for same in.
I.E: `f(x) == f(x) == f(x) == ...`
* No side-effects.
Idempotent:
* The first answer from any input, if used as input to
subsequent runs of the function, will all yield the same answer.
I.E: `f(x) == f(f(x)) == f(f(f(x))) == ...`
* Any side effect of a first function execution is *preserved* on
subsequent runs of the function using the previous answer.
Side effect:
* A function is said to have side effects if it relies apon or modifies
state outside of that *given* by its arguments. Modifying mutable
arguments is also a side effect.
#--------
def remove_twos(arg: list[int]) -> list[int]:
"Returns a copy of the list with all twos removed."
return [x for x in arg if x != 2]
Function is:
Pure
Idempotent
#--------
def return_first_int(arg: int) -> int:
"Return the int given in its first call"
global external_state
if external_state is None:
external_state = arg
return external_state
Function is:
Impure! External state changed
Idempotent
#--------
def plus_one(arg: int) -> int:
"Add one to arg"
return arg + 1
Function is:
Pure
Non-idempotent! Output changes for nested calls
#--------
def epoc_plus_seconds(secs: float) -> float:
"Return time since epoch + seconds"
time.sleep(0.1)
return time.time() + secs
Function is:
Impure! Output changes for same input
Non-idempotent! Output changes for nested calls
Code
The code that produces the above (but not its arbitrary colourising), is the following:
# -*- coding: utf-8 -*-
"""
Created on Fri Mar 3 18:04:09 2023
@author: Paddy3118
pure_idempotent.py
Explores Purity and idempotence with Python examples
Definitions:
Pure:
* Answer relies solely on inputs. Same out for same in.
I.E: `f(x) == f(x) == f(x) == ...`
* No side-effects.
Idempotent:
* The first answer from any input, if used as input to
subsequent runs of the function, will all yield the same answer.
I.E: `f(x) == f(f(x)) == f(f(f(x))) == ...`
* Any side effect of a first function execution is *preserved* on
subsequent runs of the function using the previous answer.
Side effect:
* A function is said to have side effects if it relies apon or modifies
state outside of that *given* by its arguments. Modifying mutable
arguments is also a side effect.
"""
import inspect
print(__doc__)
# %% Pure, idempotent.
print('\n#--------')
def remove_twos(arg: list[int]) -> list[int]:
"Returns a copy of the list with all twos removed."
return [x for x in arg if x != 2]
print(f"\n{inspect.getsource(remove_twos)}")
arg0 = [1, 2, 3, 2, 4, 5, 2]
print('Function is:')
print(' Pure' if remove_twos(arg0.copy()) == remove_twos(arg0.copy())
else ' Impure')
print(' Idempotent' if (answer1:=remove_twos(arg0)) == remove_twos(answer1)
else 'Non-idempotent')
# %% Impure, idempotent.
print('\n#--------')
def return_first_int(arg: int) -> int:
"Return the int given in its first call"
global external_state
if external_state is None:
external_state = arg
return external_state
print(f"\n{inspect.getsource(return_first_int)}")
# Purity
external_state = initial_state = None
arg0 = 1
same_output = (return_first_int(arg0)) == return_first_int(arg0)
same_state = external_state == initial_state
print('Function is:')
if same_output and same_state:
print(' Pure')
else:
if not same_output:
print(' Impure! Output changes for same input')
if not same_state:
print(' Impure! External state changed')
# Idempotence
external_state = None
answer1, state1 = return_first_int(arg0), external_state
answer2, state2 = return_first_int(answer1), external_state
same_output = answer1 == answer2
same_state = state1 == state2
if same_output and same_state:
print(' Idempotent')
else:
if not same_output:
print(' Non-idempotent! Output changes for nested calls')
if not same_state:
print(' Non-idempotent! External state changes for nested calls')
# %% Pure, non-idempotent.
print('\n#--------')
def plus_one(arg: int) -> int:
"Add one to arg"
return arg + 1
print(f"\n{inspect.getsource(plus_one)}")
# Purity
arg0 = 1
same_output = (plus_one(arg0)) == plus_one(arg0)
print('Function is:')
if same_output:
print(' Pure')
else:
print(' Impure! Output changes for same input')
# Idempotence
answer1 = plus_one(arg0)
answer2 = plus_one(answer1)
same_output = answer1 == answer2
if same_output:
print(' Idempotent')
else:
print(' Non-idempotent! Output changes for nested calls')
# %% Impure, non-idempotent.
print('\n#--------')
import time
def epoc_plus_seconds(secs: float) -> float:
"Return time since epoch + seconds"
time.sleep(0.1)
return time.time() + secs
print(f"\n{inspect.getsource(epoc_plus_seconds)}")
# Purity
arg0 = 1
same_output = (epoc_plus_seconds(arg0)) == epoc_plus_seconds(arg0)
print('Function is:')
if same_output:
print(' Pure')
else:
print(' Impure! Output changes for same input')
# Idempotence
answer1 = epoc_plus_seconds(arg0)
answer2 = epoc_plus_seconds(answer1)
same_output = answer1 == answer2
if same_output:
print(' Idempotent')
else:
print(' Non-idempotent! Output changes for nested calls')
END.