Created
April 30, 2016 18:44
-
-
Save usrlocalben/a3debdbc23e917fbc2fa7320395372ee to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
python + redux == pydux | |
Benjamin Yates, 2016 | |
Redux: http://redux.js.org | |
A somewhat literal translation of Redux & counter demo, | |
incl. apply_middleware, logging middleware, and thunks. | |
Tornado is used to demonstrate thunks and async actions. | |
Closures in Python are over references (as opposed to | |
names as in JavaScript) so single-element arrays are | |
used to create read/write closures. | |
extend() provides an alternative to the es6 ...spread | |
operator, or es5's Object.assign({} ...) | |
Ensuring that action.type always exists helps minimize | |
excessive .get()ing (and precious line-length) | |
""" | |
from datetime import timedelta | |
from tornado.ioloop import IOLoop | |
def main(): | |
"""demo based on the redux counter example. | |
demonstrates: | |
- multiple subscribed listeners | |
- detecting state change in a listener | |
- unsubscribing a listener | |
- async 'work' using thunks | |
- logging middleware | |
""" | |
main_loop = IOLoop.instance() | |
store = create_store( | |
combine_reducers({'counter': counter}), | |
enhancer=apply_middleware(thunk_middleware, log_middleware), | |
) | |
dispatch, subscribe = store['dispatch'], store['subscribe'] | |
def log_counter(): | |
state = store['get_state']() | |
msg = '>>> Active Tasks: {}, Counter: {}'.format( | |
state['counter']['task_count'], | |
state['counter']['value']) | |
print msg | |
last_value = [None] # r/w closure | |
def four_alert(): | |
state = store['get_state']() | |
cur_value = state['counter']['value'] | |
if last_value[0] != cur_value: | |
last_value[0] = cur_value | |
if cur_value == 4: | |
print '----- counter is now 4 -----' | |
subscribe(four_alert) | |
unsubscribe_log = subscribe(log_counter) | |
log_counter() | |
dispatch(increment_counter()) | |
dispatch(increment_counter()) | |
dispatch(increment_counter()) | |
unsubscribe_log() | |
dispatch(increment_counter()) # not shown | |
dispatch(increment_counter()) # not shown | |
unsubscribe_log = subscribe(log_counter) | |
dispatch(wait_then_decrement_counter(1)) | |
dispatch(wait_then_decrement_counter(2)) | |
dispatch(wait_then_decrement_counter(3)) | |
dispatch(wait_then_decrement_counter(4)) | |
dispatch(wait_then_decrement_counter(5)) # should be back to zero... | |
dispatch(wait_then_terminate(10)) | |
main_loop.start() | |
print 'Terminating.' | |
# demo constants | |
INCREMENT_COUNTER = 'INCREMENT_COUNTER' | |
DECREMENT_COUNTER = 'DECREMENT_COUNTER' | |
INCREMENT_TASK_COUNT = 'INCREMENT_TASK_COUNT' | |
DECREMENT_TASK_COUNT = 'DECREMENT_TASK_COUNT' | |
# demo reducer | |
def counter(state=None, action=None): | |
if state is None: | |
state = {'value': 0, 'task_count': 0} | |
if action['type'] == INCREMENT_COUNTER: | |
return extend( | |
state, | |
{'value': state['value'] + 1}, | |
) | |
elif action['type'] == DECREMENT_COUNTER: | |
return extend( | |
state, | |
{'value': state['value'] - 1}, | |
) | |
elif action['type'] == INCREMENT_TASK_COUNT: | |
return extend( | |
state, | |
{'task_count': state['task_count'] + 1}, | |
) | |
elif action['type'] == DECREMENT_TASK_COUNT: | |
return extend( | |
state, | |
{'task_count': state['task_count'] - 1}, | |
) | |
else: | |
return state | |
# demo actions | |
def increment_counter(): | |
return {'type': INCREMENT_COUNTER} | |
def decrement_counter(): | |
return {'type': DECREMENT_COUNTER} | |
def increment_task_count(): | |
return {'type': INCREMENT_TASK_COUNT} | |
def decrement_task_count(): | |
return {'type': DECREMENT_TASK_COUNT} | |
def wait_then_decrement_counter(seconds): | |
def inner(dispatch, get_state): | |
main_loop = IOLoop.instance() | |
dispatch(increment_task_count()) | |
def then(): | |
dispatch(decrement_counter()) | |
dispatch(decrement_task_count()) | |
main_loop.add_timeout(timedelta(seconds=seconds), then) | |
return inner | |
def wait_then_terminate(seconds): | |
def inner(dispatch, get_state): | |
main_loop = IOLoop.instance() | |
def then(): | |
main_loop.stop() | |
main_loop.add_timeout(timedelta(seconds=seconds), then) | |
return inner | |
# pydux | |
def create_store(reducer, enhancer=None): | |
"""redux in a nutshell. replace_reducer omitted""" | |
if enhancer is not None: | |
return enhancer(create_store)(reducer) | |
state = [{}] # r/w closure | |
listeners = [[]] # r/w closure | |
def get_state(): | |
return state[0] | |
def dispatch(action): | |
state[0] = reducer(state[0], action) | |
[listener() for listener in listeners[0]] | |
def subscribe(listener): | |
def unsubcribe(): | |
listeners[0] = [item for item in listeners[0] if item != listener] | |
listeners[0].append(listener) | |
return unsubcribe | |
dispatch({'type': None}) | |
return { | |
'dispatch': dispatch, | |
'get_state': get_state, | |
'subscribe': subscribe, | |
} | |
def combine_reducers(defs): | |
"""helper for composing the state-tree of reducers""" | |
def reducer(state, action): | |
return {key: subreducer(state.get(key), action) | |
for key, subreducer in defs.iteritems()} | |
return reducer | |
def apply_middleware(*middlewares): | |
"""returns an enhancer function composed of middleware""" | |
def inner(create_store): | |
def create_wrapper(reducer, enhancer=None): | |
store = create_store(reducer, enhancer) | |
dispatch = store['dispatch'] | |
middlewareAPI = { | |
'get_state': store['get_state'], | |
'dispatch': lambda action: dispatch(action), | |
} | |
chain = [mw(middlewareAPI) for mw in middlewares] | |
dispatch = compose(*chain)(store['dispatch']) | |
return extend(store, {'dispatch': dispatch}) | |
return create_wrapper | |
return inner | |
def compose(*funcs): | |
"""compose(a,b,c) returns f, where f(x) = a(b(c(x)))""" | |
if not funcs: | |
return lambda *args: args | |
last = funcs[-1] | |
rest = funcs[0:-1] | |
return lambda *args: reduce(lambda ax, func: func(ax), | |
reversed(rest), last(*args)) | |
def thunk_middleware(store): | |
"""thunks, from https://github.com/aearon/redux-thunk""" | |
dispatch, get_state = store['dispatch'], store['get_state'] | |
def wrapper(next): | |
def thunk_dispatch(action): | |
if hasattr(action, '__call__'): | |
return action(dispatch, get_state) | |
return next(action) | |
return thunk_dispatch | |
return wrapper | |
def log_middleware(store): | |
"""log all actions to console as they are dispatched""" | |
def wrapper(next): | |
def log_dispatch(action): | |
print 'Dispatch Action:', action | |
return next(action) | |
return log_dispatch | |
return wrapper | |
# extra | |
def extend(*items): | |
"""fold-left shallow dictionary merge""" | |
def merge(ax, item): | |
ax.update(item) | |
return ax | |
return reduce(merge, items, {}) | |
if __name__ == '__main__': | |
main() | |
# vim: tabstop=4 shiftwidth=4 softtabstop=4 expandtab |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment