fluid – Fluid Bindings

The fluid module implements fluid bindings for Python. Fluid cells are bindings in the dynamic environment. The dynamic environment is like the lexical environment, but the value of a binding depends on the dynamic extent of a runtime context rather than lexical scope. The dynamic environment interacts with threads like the Unix shell environment interacts with processes: fluid bindings may be inherited from the parent thread.

Fluid bindings are safer and more convenient than global variables. They can often be used instead of a singleton. Dynamic parameters are described in more detail in SRFI 39.

>>> from md import fluid

Fluid Cells

cell([value, validate, type]) → Cell

Create a new fluid Cell with global value. If no value is given, the cell is bound to an undefined value.

If validate is given, it should be a callable that accepts a new value and returns a value (maybe the same value or something derived from it) or raises an exception.

The type can be used to specialize the type of cell created. The default is shared.

>>> def valid_base(value):
...     assert 2 <= value <= 36, 'base must be betwee 2 and 36'
...     return value

>>> BASE = fluid.cell(10, validate=valid_base)

>>> def convert(str):
...     return int(str, BASE.value)

>>> convert("11")
11

>>> with BASE.let(2):
...     print convert("11"), '(a)'
...     with BASE.let(8):
...         print convert("11"), '(b)'
...     print convert("11"), '(c)'
3 (a)
9 (b)
3 (c)

>>> BASE.set(40)
...
AssertionError: base must be betwee 2 and 36
class Cell([value, validate])

A cell is assigned a (possibly undefined) value when it is created. If the optional validate is given, it should be a procedure that takes a value and returns another value. This can be used to check or coerce values assigned to the cell.

value

Get or set the current value of this cell.

>>> BASE.value
10
>>> BASE.value = 16
let(value) → context

Bind the cell to value for the extent of the context.

>>> with BASE.let(8):
...     BASE.value
8
>>> BASE.value
16

Dynamic Environment

The dynamic environment is propagated to threads when they are started by snapshotting the environment of the parent thread. The propagated value depends on the type of the cell(); shared is the default.

>>> import threading, time

>>> def show(name, status, *cells):
...     print name, (' '.join(str(c.value) for c in cells)), '(%s)' % status

>>> def worker1(cell):
...     show('worker1', 'wait for change', cell)
...     time.sleep(0.01)
...     show('worker1', 'after change', cell)

>>> def worker2(cell):
...     time.sleep(0)
...     cell.value = 'banana'
...     show('worker2', 'changed', cell)

>>> def demo(cell):
...     t1 = threading.Thread(target=lambda: worker1(cell))
...     t2 = threading.Thread(target=lambda: worker2(cell))
...     with cell.let('pineapple'):
...          t1.start(); t2.start()
...          t1.join()
...          show('parent', 'workers done', cell)
class shared

The current binding in the dynamic environment is shared with the new environment. Mutating any cell with this shared binding affects all cells using that binding. This is most similar to a global variable.

>>> P1 = fluid.cell('apple')
>>> demo(P1)
worker1 pineapple (wait for change)
worker2 banana (changed)
worker1 banana (after change)
parent banana (workers done)
class acquired

The current value is acquired from the dynamic environment, but the cell is bound to a new location containing the value. Mutations of the original location will have no effect on the new location.

>>> P2 = fluid.cell('apple', type=fluid.acquired)
>>> demo(P2)
worker1 pineapple (wait for change)
worker2 banana (changed)
worker1 pineapple (after change)
parent pineapple (workers done)
class private

No binding is acquired from the dynamic environment. The cell is bound to a new location containing its global value.

>>> P3 = fluid.cell('apple', type=fluid.private)
>>> demo(P3)
worker1 apple (wait for change)
worker2 banana (changed)
worker1 apple (after change)
parent pineapple (workers done)
class copied
class deepcopied

These types behave like acquired, but they also copy (or deepcopy) the value of the binding in addition to creating a new location. This is useful if the value of the cell is mutable and the new environment should acquire a snapshot of the value.

>>> P4 = fluid.cell(type=fluid.acquired)
>>> P5 = fluid.cell(type=fluid.copied)

>>> def worker3(a, b):
...     a.value[0] = 'radish'
...     b.value[0] = 'mango'
...     show('worker3', 'changed', a, b)

>>> with fluid.let((P4, ['acorn']), (P5, ['grape'])):
...     t3 = threading.Thread(target=lambda: worker3(P4, P5))
...     t3.start(); t3.join()
...     show('parent', 'workers done', P4, P5)
worker3 ['radish'] ['mango'] (changed)
parent ['radish'] ['grape'] (workers done)

Utilities

let(*bindings) → context

This is a shortcut for parameterizing several fluid cells at the same time.

>>> MULTIPLIER = fluid.cell(2)
>>> BASE.value = 10

>>> def multiply(str):
...     return convert(str) * MULTIPLIER.value

>>> multiply("11")
22

>>> with fluid.let((BASE, 2), (MULTIPLIER, 3)):
...     multiply("11")
9
accessor(cell[, name]) → access

The two most common actions on a fluid cell are getting its value or creating a binding in a new dynamic context. An accessor closes over a cell. When it is called with no arguments, the value of the cell is returned. When called with one argument (a new value), a context manager is returned that binds the cell to the new value.

When a cell is accessed and it has never been assigned a value, a ValueError is raised. The optional name parameter is used to enhance the ValueError.

>>> multiplier = fluid.accessor(MULTIPLIER, name='multiplier')
>>> with multiplier(20):
...    multiplier()
20

Example: a parameterized database connection

A database connection is a good use-case for a fluid cell. Instead of requiring each query-method to accept a connection parameter, the connection is parameterized through the dynamic environment.

>>> import sqlite3
>>> from contextlib import contextmanager

>>> connection = fluid.accessor(fluid.cell(), name='CONNECTION')

>>> @contextmanager
... def autocommitted():
...     conn = connection()
...     yield conn.cursor()
...     conn.commit()

>>> def create_schema():
...     with autocommitted() as cursor:
...         cursor.execute('CREATE TABLE data (value text);')

>>> def add_data(values):
...     with autocommitted() as cursor:
...         cursor.executemany(
...             'INSERT INTO data VALUES (?);',
...             ((v,) for v in values)
...         )

>>> def get_data():
...     cursor = connection().cursor()
...     cursor.execute('SELECT value from data ORDER BY value;')
...     return (r[0] for r in cursor)

>>> @contextmanager
... def snapshot(dest):
...     exported = get_data()
...     with connection(dest):
...         create_schema()
...         add_data(exported)
...         yield

>>> create_schema()
...
ValueError: CONNECTION is undefined

>>> import sqlite3
>>> with connection(sqlite3.connect(':memory:')):
...     create_schema()
...     add_data(['foo', 'bar', 'baz'])
...     with snapshot(sqlite3.connect(':memory:')):
...         add_data(['mumble', 'quux'])
...         print list(get_data()), '(nested)'
...     print list(get_data()), '(outer)'
[u'bar', u'baz', u'foo', u'mumble', u'quux'] (nested)
[u'bar', u'baz', u'foo'] (outer)

Table Of Contents

Previous topic

expect – Primitive Type Expectations

Next topic

stm – Software Transactional Memory

This Page