Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

compose and transform without context managers? #17

Open
belm0 opened this issue Apr 6, 2021 · 0 comments
Open

compose and transform without context managers? #17

belm0 opened this issue Apr 6, 2021 · 0 comments
Labels
API breaker change that could potentially break the API

Comments

@belm0
Copy link
Contributor

belm0 commented Apr 6, 2021

Currently compose and transform functionality take the form of context managers. This is the orderly way, ensuring that derived values don't outlive the scope of their parents, and that callbacks (e.g. the transform function) won't be called after the corresponding AsyncValue has been finalized.

with compose_values(x=async_x, y=async_y) as async_xy, \
        async_xy.open_transform(lambda val: val.x * val.y) as x_mul_y \
        async_xy.open_transform(lambda val: val.x / val.y) as x_div_y:
    ...

This form is a bit uncomfortable at first, and requires creative use of ExitStack()-- but it works.

How would things be without context managers?

async_xy = compose_values(x=async_x, y=async_y)
x_mul_y = async_xy.transform(lambda val: val.x * val.y)
x_div_y = async_xy.transform(lambda val: val.x / val.y)

The concern is about lifetimes, and avoiding cycles that might keep objects around forever.

Once transform() works without a context manager, compose_values() could be implemented on top of it by taking advantage of callback side effects.

approach A: weak collection of transform funcs in parent, strong ref in child

The parent AsyncValue would need a weak collection of transform functions, and the child AsyncValue would hold the main ref to the function. The transform callback may be called for a while even after the child expires, until the GC cleans up the weak ref. (Users may be surprised by this, especially if the transform function has side effects.)

However using a static transform function would be problematic, as the function could live for the entire program session:

foo = async_xy.transform(foo_transform)

approach B: weak collection of children in parent

The parent has a weak-key dict of child:transform.

Like (A), the transform callback may be called for a while even after the child expires, until the GC cleans up the weak ref. (Users may be surprised by this, especially if the transform function has side effects.)

approach C: child has a ref to parent and unregisters transform in finalizer

The parent would prepare a finalizer callback on child AsyncValue that would unregister the transform.

However, now it's a situation where the parent could be finalized, and silently the child would stop receiving updates. Adding an additional ref from parent to child isn't too attractive, as it would cause cycles in the garbage graph.

@belm0 belm0 added the API breaker change that could potentially break the API label Apr 6, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API breaker change that could potentially break the API
Projects
None yet
Development

No branches or pull requests

1 participant