Friday, March 03, 2023

Function purity and idempotence

 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.