{"id":20361023,"url":"https://github.com/mysticfall/alleycat-reactive","last_synced_at":"2025-05-08T06:32:42.433Z","repository":{"id":45621973,"uuid":"278980868","full_name":"mysticfall/alleycat-reactive","owner":"mysticfall","description":"A simple Python library to provide an API to implement the Reactive Object Pattern (ROP).","archived":false,"fork":false,"pushed_at":"2022-12-27T16:17:37.000Z","size":200,"stargazers_count":18,"open_issues_count":3,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-08-11T11:14:48.194Z","etag":null,"topics":["fp","library","oop","python","rx"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mysticfall.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-07-12T03:03:19.000Z","updated_at":"2024-03-26T09:43:36.000Z","dependencies_parsed_at":"2023-01-31T04:45:56.615Z","dependency_job_id":null,"html_url":"https://github.com/mysticfall/alleycat-reactive","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mysticfall%2Falleycat-reactive","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mysticfall%2Falleycat-reactive/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mysticfall%2Falleycat-reactive/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mysticfall%2Falleycat-reactive/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mysticfall","download_url":"https://codeload.github.com/mysticfall/alleycat-reactive/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224708957,"owners_count":17356521,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fp","library","oop","python","rx"],"created_at":"2024-11-14T23:44:22.663Z","updated_at":"2024-11-14T23:44:23.309Z","avatar_url":"https://github.com/mysticfall.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"![pytest](https://github.com/mysticfall/alleycat-reactive/workflows/pytest/badge.svg)\n[![PyPI version](https://badge.fury.io/py/alleycat-reactive.svg)](https://badge.fury.io/py/alleycat-reactive)\n# AlleyCat - Reactive\n\nA part of the AlleyCat project which supports the _Reactive Object Pattern_.\n\n## Introduction\n\nAlleyCat Reactive is a project to explore the possibility of bridging the gap between the \ntwo most widely used programming paradigms, namely, the object-oriented programming (OOP), \nand functional programming (FP).\n\nIt aims to achieve its goal by proposing a new design pattern based on the \n[Reactive Extensions (Rx)](http://reactivex.io/).\n\nEven though it is already available on [PyPI repository](https://pypi.org/project/alleycat-reactive/) \nas `alleycat-reactive` package, the project is currently at a proof-of-concept stage \nand highly experimental.\n\nAs such, there can be significant changes in the API at any time in future. Furthermore, the project \nmay even be discontinued in case the idea it is based upon proves to be an infeasible one.\n\nSo, please use it at your discretion and consider opening an issue if you encounter a problem.\n\n## Reactive Object Pattern\n\nAlleyCat Reactive provides an API to implement what we term as _Reactive Object Pattern_ or \n_ROP_ for short. Despite its rather pretentious name, it merely means defining class properties \nwhich can also serve as an _Observable_ in Rx.\n\nBut why do we need such a thing?\n\nWe won't delve into this subject too much since you can learn about the concept from many \nother websites.\n\nIn short, it's much better to define data in a declarative manner, or as \"data pipelines\", \nespecially when it's changing over time. And Rx is all about composing and manipulating such \npipelines in a potentially asynchronous context.\n\nBut what if the data do not come from an asynchronous source, like Tweets or GUI events, \nbut are just properties of an object? Of course, Rx can handle synchronous data as well, but the \ncost of using it may outweigh the benefits in such a scenario.\n\nIn a traditional OOP system, properties of an object are mere values which are often \nmutable, but not observable by default. To observe the change of a property over time, we \nmust use an observer pattern, usually in the form of a separate event.\n\nSome implementation of Rx allows converting such an event into an _Observable_. Still, it \ncan be a tedious task to define an event for every such property, and it requires a lot of \nboilerplate code to convert them into Rx pipelines.\n\nMore importantly, the resulting pipelines and their handling code don't have any clear \nrelationship with the original object or its properties from which they got derived. They \nare just a bunch of statements which you can put anywhere in the project, and that's not \nhow you want to design your business logic in an OOP project.\n\nIn a well-designed OOP project, classes form a coherent whole by participating in \ninheritance hierarchies. They may reference, extend, or redefine properties of their \nparents or associated objects to express the behaviours and traits of the domain concepts \nthey represent.\n\nAnd there is another problem with using _Observables_ in such a manner. Once you build an Rx \npipeline, you can't retrieve the value flowing inside unless you write still more boilerplate \ncode to subscribe to the stream and store its value to an outside variable.\n\nThis practice is usually discouraged as an anti-pattern, and so is the use of _Subjects_. \nHowever, the object is not observing some outside data (e.g. Tweets) but owns it, which is \none of the few cases where the use of a _Subject_ can be justified. In such a context, it's \nperfectly reasonable to assume that an object always has access to a snapshot of all the \nstates it owns.\n\nSo, what if we can define such state data of an object as _Observables_ which can also \nbehave like ordinary properties? Wouldn't it be nice if we can easily access them as in OOP \nwhile still being able to observe and compose them like in Rx?\n\nAnd that is what this project is trying to achieve.\n\n## Usage\n\nTo achieve the goal outlined in the previous section, we provide a way to define a property \nwhich can also turn into an _Observable_. And there are two different types of such \nproperty classes you can use for that purpose.\n\n### Reactive Property\n\nFirstly, there is a type of property that can manage its state, which is implemented by \n`ReactiveProperty[T]` class. To define such a property using an initial value, you can use \na helper function `from_value` as follows:\n\n```python\nfrom alleycat.reactive import RP\nfrom alleycat.reactive import functions as rv\n\nclass Toto:\n\n    # Create an instance of ReactiveProperty[int] with an initial value of '99':\n    value: RP[int] = rv.from_value(99) # You know the song, don't you? \n```\n\nNote that `RP[T]` is just a convenient alias for `ReactiveProperty[T]` which you can use for \ntype hinting. As with other type annotations in Python, it is not strictly necessary. But it \ncan be particularly useful when you want to lazily initialize the property.\n \nYou can declare an 'empty' property using `new_property` and initialize it later as shown \nbelow. Because it may be difficult to see what type of data the property expects without an \ninitial value, using an explicit type annotation can make the code more readable:  \n\n```python\nfrom alleycat.reactive import RP\nfrom alleycat.reactive import functions as rv\n\nclass MyClass:\n\n    # Declare an empty property first.\n    value: RP[int] = rv.new_property()\n\n    def __init__(self, init_value: int):\n\n        # Then assign a value as you would do to an ordinary property.\n        self.value = init_value\n```\n\nA `ReactiveProperty` can be can be read and modified like an ordinary class attribute. And \nalso you can make it a read-only property by setting the `read_only` argument to `True` like the \nfollowing example:\n\n```python\nfrom alleycat.reactive import RP\nfrom alleycat.reactive import functions as rv\n\nclass ArcadiaBay:\n\n    writeable: RP[str] = rv.from_value(\"life is strange\")\n\n    read_only: RP[str] = rv.from_value(\"the past\", read_only=True)\n\n\nplace = ArcadiaBay()\n\nprint(place.writeable) # \"life is strange\"\nprint(place.read_only) # \"the past\"\n\nplace.writeable = \"It's awesome\"\n\nprint(place.writeable) # \"It's awesome\"\n\nplace.read_only = \"Let me rewind.\" # Throws an AttributeError\n\n# Of course, you can't change the past. But the game is hella cool!\n```\n\nBut haven't we talked about Rx? Of course, we have! And that's the whole point of \nthe library, after all.\n\nTo convert a reactive property into an _Observable_ of the same type, you can use observe \nmethod like this:\n\n```python\nfrom alleycat.reactive import RP\nfrom alleycat.reactive import functions as rv\n\nclass Nena:\n\n    ballons: RP[int] = rv.from_value(98)\n\n\nnena = Nena()\n\nluftballons = []\n\nrv.observe(nena.ballons).subscribe(luftballons.append) # Returns a Disposable. See Rx API.\n\nprint(luftballons) # Returns [98].\n\nnena.ballons = nena.ballons + 1\n\nprint(luftballons) # [98, 99] # Ok, I lied before. It's about Nena. not Toto :P\n```\n\nIf you are familiar with Rx, you may notice the similarities between `ReactiveProperty` \nwith _BehaviorSubject_. In fact, the former is a wrapper around the latter, and `observe` \nreturns an _Observable_ instance backed by such a subject.\n\nTo learn about all the exciting things we can do with an _Observable_, you may want to \nread the official documentation of Rx. We will introduce a few examples later, but before \nthat, we better learn about the other variant of the reactive value first.\n\n### Reactive View\n\n`ReactiveView[T]`(or `RV[T]` for short) is another derivative of `ReactiveValue`, from \nwhich `ReactiveProperty` is also derived (hence, the alias of `functions` module used \nabove, _\"rv\"_).\n\nThe main difference is that while the latter owns a state value itself, a reactive view \nreflects it from an outside source specified as an _Observable_. To create a reactive \nview from an instance of _Observable_, you can use `from_observable` function like this:\n\n```python\nimport rx\nfrom alleycat.reactive import RV\nfrom alleycat.reactive import functions as rv\n\nclass Joni:\n\n    big: RV[str] = rv.from_observable(rx.of(\"Yellow\", \"Taxi\"))\n```\n\nIf you are familiar with Rx, you may see it as a wrapper around an _Observable_, while \na reactive property can be seen as one around a _Subject_.\n\nLike its counterpart, you can initialize a reactive view either eagerly or lazily. In order \nto create a lazy-initializing view, you can use `new_view`, and later provide an _Observable_ \nas shown below:\n\n```python\nimport rx\nfrom alleycat.reactive import RV\nfrom alleycat.reactive import functions as rv\n\nclass BothSides:\n\n    love: RV[str] = rv.new_view()\n\n    def __init__(self):\n        self.love = rx.of(\"Moons\", \"Junes\", \"Ferris wheels\")\n```\n\nIt also accepts `read_only` option from its constructor (default to `True`, in contrast to the case \nwith `ReactiveProperty`) setting of which will make it 'writeable'.\n\nIt may sound unintuitive since a 'view' usually implies immutability. However, what changes \nwhen you set a value of a reactive view is the source _Observable_ that the view monitors, \nnot the data itself, as is the case with a reactive property.\n\nLastly, you can convert a reactive property into a view by calling its `as_view` method. \nIt's a convenient shortcut to call `observe` to obtain an _Observable_ of a reactive property \nso that it can be used to initialize an associated view.\n\nThe code below shows how you can derive a view from an existing reactive property:\n\n```python\nfrom alleycat.reactive import RP, RV\nfrom alleycat.reactive import functions as rv\n\nclass Example:\n\n    value: RP[str] = rv.from_value(\"Boring!\") # I know. But it's not easy to make it interesting, alright?  \n\n    view: RV[str] = value.as_view()\n```\n\n### Operators\n\nAs we know how to create reactive properties and values, now it's time to learn how to \ntransform them. Both variants of `ReactiveValue` provides `map` method, with which you \ncan map an arbitrary function or lambda expression over each value in the pipeline:\n\n```python\nfrom alleycat.reactive import RP, RV\nfrom alleycat.reactive import functions as rv\n\nclass Counter:\n\n    word: RP[str] = rv.new_property()\n\n    count: RV[str] = word.as_view().map(len).map(lambda o, c: f\"The word has {c} letter(s)\")\n    # The first argument 'o' is the current instance of Counter class.\n\ncounter = Counter()\n\ncounter.word = \"Supercalifragilisticexpialidocious!\"\n\nprint(counter.count) # Prints \"The word has 35 letter(s)\". Wait, did you actually count that?\n```\n\nYou can also use `pipe` to chain arbitrary Rx operators to build a more complex pipeline like this: \n\n```python\nfrom rx import operators as ops\nfrom alleycat.reactive import RP, RV\nfrom alleycat.reactive import functions as rv\n\nclass CrowsCounter:\n\n    animal: RP[str] = rv.new_property()\n\n    # The first argument 'o' is the current instance of Counter class.\n    crows: RV[str] = animal.as_view().pipe(lambda o: (\n        ops.map(str.lower),\n        ops.filter(lambda v: v == \"crow\"),\n        ops.map(lambda _: 1),\n        ops.scan(lambda v1, v2: v1 + v2, 0)))\n\ncounting = CrowsCounter()\n\ncounting.animal = \"cat\"\ncounting.animal = \"Crow\"\ncounting.animal = \"CROW\"\ncounting.animal = \"dog\"\n\nprint(counting.crows) # Returns 2.\n```\n\nThere are also convenient counterparts to `merge`, `combine_latest`, and `zip` from Rx API, \nwhich you can use to combine two or more reactive values in a more concise manner:\n\n```python\nfrom alleycat.reactive import RP, RV\nfrom alleycat.reactive import functions as rv\n\nclass Rectangle:\n\n    width: RP[int] = rv.from_value(100)\n\n    height: RP[int] = rv.from_value(200)\n\n    area: RV[int] = rv.combine_latest(width, height)(lambda v: v[0] * v[1])\n\nrectangle = Rectangle()\n\nprint(rectangle.area) # Prints 20,000... you do the math!\n\nrectangle.width = 150\n\nprint(rectangle.area) # Prints 30,000.\n\nrectangle.height = 50\n\nprint(rectangle.area) # Prints 750.\n```\n\n## Install\n\nThe library can be installed using `pip` as follows:\n```shell script\npip install alleycat-reactive\n```\n\n## License\nThis project is provided under the terms of _[MIT License](LICENSE)_.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmysticfall%2Falleycat-reactive","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmysticfall%2Falleycat-reactive","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmysticfall%2Falleycat-reactive/lists"}