Skip to content
/ mutable Public

A runtime call graph analysis decorator which lets you manipulate function results like values

License

Notifications You must be signed in to change notification settings

jburgy/mutable

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mutable (python module)

mutable.mutates is a runtime call graph analysis decorator which lets you manipulate function results like values. Think of it like a memoize decorator which remains consistent when you update cached results. Using it to decorate some of your functions is an easy way to give your script reactive semantics.

Pretend you are studying the Australian Broadcasting Corporation logo in parametric form:

from mutable import mutates, scope
from math import sin, cos, atan2, pi

@mutates
def x(t): return cos(t)

@mutates
def y(t): return sin(3*t)

@mutates
def phi(t): return atan2(x(t), y(t))

Now pretend you need to calculate the following partial derivative:

mutates lets you write

def dphi_by_dx(t, epsilon=1e-6):
    x_t = x.ref(t)
    with scope:
        x_t += epsilon
        phi_plus = phi(t)
    with scope:
        x_t -= epsilon
        phi_minus = phi(t)
    return (phi_plus - phi_minus)/(epsilon + epsilon)

which is an obvious use of finite difference.

This contrived example illustrates three things mutates does for you:

  1. mutates caches (y was not invoked by dphi_by_dx)
  2. mutates identifies dependencies automatically (phi was recalculated after each update to x)
  3. mutates cleans up after itself (scope left the original state unperturbed)

Here is, at a high level, how it works. mutates does not create one cache per decorated function like the minimalistic memoization recipe. It defines a unique global list of caches instead to simplify scope management. The global list of caches is a ChainMap and a context manager. Its only public API is that of a context manager.

Because of Python's very dynamic nature, mutates detects dependencies between values of decorated functions at runtime. Call graph analysis is a side-effect of every decorated call, even those that hit the cache. It is also scope-sensitive by necessity. This point is key to understand how mutable avoids inconsistencies between pure and mutated cache entries. Any operations which modifies a cache entry shadows all entries which depend on it directly or indirectly. Shadowing creates blank entries in the innermost scope while leaving alone those in outer scopes.

Decorated functions implement a ref method which accepts the same signature as them and returns a reference to the corresponding cache entries. The cache entry can be retrieved in the current scope by calling the reference object, just like the standard weakref model. The call returns None if the entry has not yet been created in the current scope. References delegate all in-place operators as well as a write-only 'value' property. As mentioned earlier, any modification (setting value or invoking in-place operators) shadows the entry and the transitive closure of its callers in the current scope.

This interactive session breaks down each step:

>>> t = pi/4
>>> phi(t)  # establishes dependencies as side-effect
0.7853981633974483
>>> x_t = x.ref(t)  # x_t is a reference to x(t) in cache
>>> with scope:  # create fresh scope to avoid trampling results and dependencies
...     x_t += 1e-6 # shadow x(t) and phi(t), update x(t)
...     phi_plus = phi(t)  # evaluate phi using updated x(t) and original y(t)
...

>>> phi_plus - phi(t)  # exiting scope recovers original values
7.071062811947471e-07

About

A runtime call graph analysis decorator which lets you manipulate function results like values

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages