Skip to main content

A simple Python library to provide an API to implement the Reactive Object Pattern (ROP).

Project description

pytest PyPI version

AlleyCat - Reactive

A part of the AlleyCat project which supports the Reactive Object Pattern.

Introduction

AlleyCat Reactive is a project to explore the possibility of bridging the gap between the two most widely used programming paradigms, namely, the object-oriented programming (OOP), and functional programming (FP).

It aims to achieve its goal by proposing a new design pattern based on the Reactive Extensions (Rx).

Even though it is already available on PyPI repository as alleycat-reactive package, the project is currently at a proof-of-concept stage and highly experimental.

As such, there can be significant changes in the API at any time in future. Furthermore, the project may even be discontinued in case the idea it is based upon proves to be an infeasible one.

So, please use it at your discretion and consider opening an issue if you encounter a problem.

Reactive Object Pattern

AlleyCat Reactive provides an API to implement what we term as Reactive Object Pattern or ROP for short. Despite its rather pretentious name, it merely means defining class properties which can also serve as an Observable in Rx.

But why do we need such a thing?

We won't delve into this subject too much since you can learn about the concept from many other websites.

In short, it's much better to define data in a declarative manner, or as "data pipelines", especially when it's changing over time. And Rx is all about composing and manipulating such pipelines in a potentially asynchronous context.

But what if the data do not come from an asynchronous source, like Tweets or GUI events, but are just properties of an object? Of course, Rx can handle synchronous data as well, but the cost of using it may outweigh the benefits in such a scenario.

In a traditional OOP system, properties of an object are mere values which are often mutable, but not observable by default. To observe the change of a property over time, we must use an observer pattern, usually in the form of a separate event.

Some implementation of Rx allows converting such an event into an Observable. Still, it can be a tedious task to define an event for every such property, and it requires a lot of boilerplate code to convert them into Rx pipelines.

More importantly, the resulting pipelines and their handling code don't have any clear relationship with the original object or its properties from which they got derived. They are just a bunch of statements which you can put anywhere in the project, and that's not how you want to design your business logic in an OOP project.

In a well-designed OOP project, classes form a coherent whole by participating in inheritance hierarchies. They may reference, extend, or redefine properties of their parents or associated objects to express the behaviours and traits of the domain concepts they represent.

And there is another problem with using Observables in such a manner. Once you build an Rx pipeline, you can't retrieve the value flowing inside unless you write still more boilerplate code to subscribe to the stream and store its value to an outside variable.

This practice is usually discouraged as an anti-pattern, and so is the use of Subjects. However, the object is not observing some outside data (e.g. Tweets) but owns it, which is one of the few cases where the use of a Subject can be justified. In such a context, it's perfectly reasonable to assume that an object always has access to a snapshot of all the states it owns.

So, what if we can define such state data of an object as Observables which can also behave like ordinary properties? Wouldn't it be nice if we can easily access them as in OOP while still being able to observe and compose them like in Rx?

And that is what this project is trying to achieve.

Usage

To achieve the goal outlined in the previous section, we provide a way to define a property which can also turn into an Observable. And there are two different types of such property classes you can use for that purpose.

Reactive Property

Firstly, there is a type of property that can manage its state, which is implemented by ReactiveProperty[T] class. To define such a property using an initial value, you can use a helper function from_value as follows:

from alleycat.reactive import RP
from alleycat.reactive import functions as rv

class Toto:

    # Create an instance of ReactiveProperty[int] with an initial value of '99':
    value: RP[int] = rv.from_value(99) # You know the song, don't you? 

Note that RP[T] is just a convenient alias for ReactiveProperty[T] which you can use for type hinting. As with other type annotations in Python, it is not strictly necessary. But it can be particularly useful when you want to lazily initialize the property.

You can declare an 'empty' property using new_property and initialize it later as shown below. Because it may be difficult to see what type of data the property expects without an initial value, using an explicit type annotation can make the code more readable:

from alleycat.reactive import RP
from alleycat.reactive import functions as rv

class MyClass:

    # Declare an empty property first.
    value: RP[int] = rv.new_property()

    def __init__(self, init_value: int):

        # Then assign a value as you would do to an ordinary property.
        self.value = init_value

A ReactiveProperty can be can be read and modified like an ordinary class attribute. And also you can make it a read-only property by setting the read_only argument to True like the following example:

from alleycat.reactive import RP
from alleycat.reactive import functions as rv

class ArcadiaBay:

    writeable: RP[str] = rv.from_value("life is strange")

    read_only: RP[str] = rv.from_value("the past", read_only=True)


place = ArcadiaBay()

print(place.writeable) # "life is strange"
print(place.read_only) # "the past"

place.writeable = "It's awesome"

print(place.writeable) # "It's awesome"

place.read_only = "Let me rewind." # Throws an AttributeError

# Of course, you can't change the past. But the game is hella cool!

But haven't we talked about Rx? Of course, we have! And that's the whole point of the library, after all.

To convert a reactive property into an Observable of the same type, you can use observe method like this:

from alleycat.reactive import RP
from alleycat.reactive import functions as rv

class Nena:

    ballons: RP[int] = rv.from_value(98)


nena = Nena()

luftballons = []

rv.observe(nena.ballons).subscribe(luftballons.append) # Returns a Disposable. See Rx API.

print(luftballons) # Returns [98].

nena.ballons = nena.ballons + 1

print(luftballons) # [98, 99] # Ok, I lied before. It's about Nena. not Toto :P

If you are familiar with Rx, you may notice the similarities between ReactiveProperty with BehaviorSubject. In fact, the former is a wrapper around the latter, and observe returns an Observable instance backed by such a subject.

To learn about all the exciting things we can do with an Observable, you may want to read the official documentation of Rx. We will introduce a few examples later, but before that, we better learn about the other variant of the reactive value first.

Reactive View

ReactiveView[T](or RV[T] for short) is another derivative of ReactiveValue, from which ReactiveProperty is also derived (hence, the alias of functions module used above, "rv").

The main difference is that while the latter owns a state value itself, a reactive view reflects it from an outside source specified as an Observable. To create a reactive view from an instance of Observable, you can use from_observable function like this:

import rx
from alleycat.reactive import RV
from alleycat.reactive import functions as rv

class Joni:

    big: RV[str] = rv.from_observable(rx.of("Yellow", "Taxi"))

If you are familiar with Rx, you may see it as a wrapper around an Observable, while a reactive property can be seen as one around a Subject.

Like its counterpart, you can initialize a reactive view either eagerly or lazily. In order to create a lazy-initializing view, you can use new_view, and later provide an Observable as shown below:

import rx
from alleycat.reactive import RV
from alleycat.reactive import functions as rv

class BothSides:

    love: RV[str] = rv.new_view()

    def __init__(self):
        self.love = rx.of("Moons", "Junes", "Ferris wheels")

It also accepts read_only option from its constructor (default to True, in contrast to the case with ReactiveProperty) setting of which will make it 'writeable'.

It may sound unintuitive since a 'view' usually implies immutability. However, what changes when you set a value of a reactive view is the source Observable that the view monitors, not the data itself, as is the case with a reactive property.

Lastly, you can convert a reactive property into a view by calling its as_view method. It's a convenient shortcut to call observe to obtain an Observable of a reactive property so that it can be used to initialize an associated view.

The code below shows how you can derive a view from an existing reactive property:

from alleycat.reactive import RP, RV
from alleycat.reactive import functions as rv

class Example:

    value: RP[str] = rv.from_value("Boring!") # I know. But it's not easy to make it interesting, alright?  

    view: RV[str] = value.as_view()

Operators

As we know how to create reactive properties and values, now it's time to learn how to transform them. Both variants of ReactiveValue provides map method, with which you can map an arbitrary function or lambda expression over each value in the pipeline:

from alleycat.reactive import RP, RV
from alleycat.reactive import functions as rv

class Counter:

    word: RP[str] = rv.new_property()

    count: RV[str] = word.as_view().map(len).map(lambda o, c: f"The word has {c} letter(s)")
    # The first argument 'o' is the current instance of Counter class.

counter = Counter()

counter.word = "Supercalifragilisticexpialidocious!"

print(counter.count) # Prints "The word has 35 letter(s)". Wait, did you actually count that?

You can also use pipe to chain arbitrary Rx operators to build a more complex pipeline like this:

from rx import operators as ops
from alleycat.reactive import RP, RV
from alleycat.reactive import functions as rv

class CrowsCounter:

    animal: RP[str] = rv.new_property()

    # The first argument 'o' is the current instance of Counter class.
    crows: RV[str] = animal.as_view().pipe(lambda o: (
        ops.map(str.lower),
        ops.filter(lambda v: v == "crow"),
        ops.map(lambda _: 1),
        ops.scan(lambda v1, v2: v1 + v2, 0)))

counting = CrowsCounter()

counting.animal = "cat"
counting.animal = "Crow"
counting.animal = "CROW"
counting.animal = "dog"

print(counting.crows) # Returns 2.

There are also convenient counterparts to merge, combine_latest, and zip from Rx API, which you can use to combine two or more reactive values in a more concise manner:

from alleycat.reactive import RP, RV
from alleycat.reactive import functions as rv

class Rectangle:

    width: RP[int] = rv.from_value(100)

    height: RP[int] = rv.from_value(200)

    area: RV[int] = rv.combine_latest(width, height)(lambda v: v[0] * v[1])

rectangle = Rectangle()

print(rectangle.area) # Prints 20,000... you do the math!

rectangle.width = 150

print(rectangle.area) # Prints 30,000.

rectangle.height = 50

print(rectangle.area) # Prints 750.

Install

The library can be installed using pip as follows:

pip install alleycat-reactive

License

This project is provided under the terms of MIT License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

alleycat-reactive-0.4.8.tar.gz (16.9 kB view details)

Uploaded Source

Built Distribution

alleycat_reactive-0.4.8-py3-none-any.whl (14.6 kB view details)

Uploaded Python 3

File details

Details for the file alleycat-reactive-0.4.8.tar.gz.

File metadata

  • Download URL: alleycat-reactive-0.4.8.tar.gz
  • Upload date:
  • Size: 16.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.7.0 importlib_metadata/4.8.2 pkginfo/1.8.2 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.9

File hashes

Hashes for alleycat-reactive-0.4.8.tar.gz
Algorithm Hash digest
SHA256 0501b6daf2a6f0525ba4e4ebff434b76f9701f30cc3f3a509ed0cc9002e967ee
MD5 169dba47e7becd553f8f72ce3a6134a6
BLAKE2b-256 ea7bc06d54edc6dfcebef0b2ea70b45697bb0747ef20a2f353debd171a1eb31c

See more details on using hashes here.

File details

Details for the file alleycat_reactive-0.4.8-py3-none-any.whl.

File metadata

  • Download URL: alleycat_reactive-0.4.8-py3-none-any.whl
  • Upload date:
  • Size: 14.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.7.0 importlib_metadata/4.8.2 pkginfo/1.8.2 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.9

File hashes

Hashes for alleycat_reactive-0.4.8-py3-none-any.whl
Algorithm Hash digest
SHA256 3379b40f3b67ef96b048abff83bec154b69754cdc71d190ef10a0c9b9e594f36
MD5 995a1c57668680d544ca325e7d6dda7a
BLAKE2b-256 64d714f404ddd2188aa6b482ffe1fa99773b0be98f62fcaed7bf98b23a9ce3e4

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page