PyReactive - a silly reactive module for Python

Hello people! Let me introduce you to the wonderful world of reactive programming, and showcase to you a Python module I've authored over the last 3 weeks.

What is Reactive Programing?

Wikipedia defines Reactive Programming as

"In computing, reactive programming is a programming paradigm oriented around data flows and the propagation of change. This means that it should be possible to express static or dynamic data flows with ease in the programming languages used, and that the underlying execution model will automatically propagate changes through the data flow."  

I know, that seems like a lot of geek speak even to me. I'll explain with the help of an example. Look at the following code.

>>>a = 5
>>>b = 8
>>>sum = a + b
>>>print(sum)
13  
>>>a = 10
>>>b = 20
>>>print(sum)
13  

There's nothing out of the ordinary here. This is regulation code. sum will always remain 13 no matter what a and b are changed to, because at the time of declaration, 'sum' had evaluated to 13, hence, it will stay 13.

But what if we wanted sum to change according to the values of a and b? Or, to put it a little more formally, what if we wanted sum to SUBSCRIBE to the two variables a and b?

-->PLAY MINDLESS ENTRY MUSIC HERE

--> ENTER REACTIVE PROGRAMMING

In this paradigm, variables are OBSERVED and/or SUBSCRIBED to. What I mean by observed is that when a value is declared, a memory location is alloted and when the value changes, the memory location is overwritten, rather than having it assigned in a new memory slot. This means that a variable can only update until it is explicitly purged. This is pretty similar to what happens with languages that expose memory locations by the usage of pointers. What I mean by subscribed is that another variable subscribes to the observed variables, and this colloquially means that it always has the latest value of the observed variables. The following example should clear things up. The pseudo code is:

>>>a = Observe(5)
>>>b = Observe(8)
>>>sum = Subscribe(var=(a,b), op=('+',))
>>>print(sum)
13  
>>>a.changeTo(10)
>>>print(sum)        #sum should change to 10+8
18  
>>>b.changeTo(20)
>>>print(sum)        #sum should now become 10+20
30  

The above example shows how reactive programming works. It observes variables, and whenever there's a subscription, it automatically computes the operation everytime there's a change in the underlying value of the variable. This example shows how beautiful code can get by utilizing this wonderful paradigm. No more redundant declarations. Declare once, use forever. Okay, I might be getting carried away now.

Introducing PyReactive

PyReactive is a reactive module that I authored over the course of the last month. I had stumbled upon this wonderful new world of reactive programming when I was fiddling with JS, and somehow, I reached the website of Bacon JS. At first, I thought it was something very esoteric, and something that I couldn't wrap my head around. After a month, I came back to the same site because by then, I had discovered React JS, another wonderful library that the freaks at Facebook were working on and using. And again, when I saw this incredible library called Ractive JS. Now all of them had something do with reactive programming. And they grabbed my attention.

After looking at their examples, I wanted to implement something for our backend. I searched for reactive modules for python, but there were very few results. And they weren't being actively maintained. There was RxJS and RxPy (authored by Microsoft), but then again, I think Microsoft still has some way to travel before the open source community can trust them (purely a personal opinion). And so I thought, "How hard can it be to write my own module?" Turns out, not very easy, but not very tough either.

The goal behind writing PyReactive is that I wanted something that I could use. Just me. But then again, if there's anyone who has the time to kill on ironing out the bugs in my code, sure, be my guest. This is and will always be an open source project and it is available on github. This is also available on pypi and can be installed via pip. Kindly bear in mind that this is my first ever module, my first time fiddling with classes and objects and inheritance (I know, how I survived this long without classes is beyond me). So the code quality is poor. But that's the beauty of an open source project, isn't it? Anyone can pitch in.

Overview of PyReactive

PyReactive mainly leverages the power of directed graphs and callback mechanisms. What are they? I'm glad you asked!

Directed Graphs

If 'a' depends on 'b', and 'b' depends on 'c', we have a relationship that can be drawn as a --> b--> c. This means that when 'b' changes, a change is triggered in 'a', and when 'c' changes, 'b' changes and a corresponding change is triggered in 'a'. So effectively, 'a' depends on both 'b' and 'c'. In other words, a change in 'a' is seen whenever there's a change in either 'b' or 'c'. Now how that relationship is preserved is idiosyncratic to each language.

In PyReactive, I've used a central dependency graph (which is a dictionary) that stores the dependency relationship everytime one is found. This dictionary consists of all (overriden) mutable datatypes (such as list, dict, set, and bytearray), observables, and subscriptions when they are declared. And when a new variable that depends on these is declared, it is stored as the 'value' of the corresponding variable (which acts as the 'key' to the graph). Confusing? This example should help understand.

>>>a = List([1,3,2]) #This creates an entry in the dependencyGraph.
>>>print(dependencyGraph)
{'a': [[1,3,2]]}
>>>b = Observe(a)    #This creates another entry
>>>print(dependencyGraph)
{'a': [[1,3,2]], 'b': ['a']}

Now, the names of the variables aren't stored there (obviously), but instead, their ids are stored. I've used UUID4 to assign a unique id to each object.

Callbacks

Now callbacks are something new to Python, and although I haven't used asyncio to handle callbacks (I will use them in the next update), I have just stopped short (because I didn't know how to use them).

I'm gonna use the definition from Wikipedia, since the folks there have lucidly defined them as

In computer programming, a callback is a piece of executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at some convenient time. The invocation may be immediate as in a synchronous callback, or it might happen at later time as in an asynchronous callback. In all cases, the intention is to specify a function or subroutine as an entity that is, depending on the language, more or less similar to a variable.  

In PyReactive, I've architected each object to have an 'update' method and an 'onchange' method, both of which are called in the mentioned order whenever there's a change in the underlying value. So, when the value changes, the 'update' method is triggered, which in turn triggers the 'onchange' method and the handling code can be written by overriding the 'onchange' method of the object.

The nuts and bolts (and other definitions)

The following section describes the various definitions of terms used in the module and the corresponding APIs.

Mutables

A mutable is any data type that can be altered in-place. The meaning of in-place is that the value is modified in the same memory location. In other words, if you're familiar with Python, the __new__ method isn't called when its value changes. In PyReactive, ByteArray, Dict, List, Set, Observe and Subscribe are mutables.

Immutables

An immutable is any object/data type that cannot be altered in-place, i.e., a new instantiation takes place when it is modified. In other words, the __new__ method is called every time the value changes. Or, once an immutable is assigned, the only way its value can be changed is by declaring a new immutable. In Python, int, str, tuple, etc. are immutables.

ByteArray, Dict, List, Set (BDLS)

These are bytearray, dict, list, and set on steroids. They are specific to PyReactive only and have a few overridden methods over their native equivalents. They can be accessed with the same Pythonic APIs, but whenever there's a change in their values, they begin to do some exotic things (Okay, may be not. Maybe they only check the dependencyGraph and issue callback updates to all mutables dependent on them).

Mind the CamelCasing in their names, though. This is what makes them unique. The usage is as follows:

>>>a = List([1,3,2])
>>>a[0]
1  
>>>b = Dict({1:a, 2:[3,2,1,3]})
>>>b[2]
[3,2,1,3]
>>>c = Set({1,2,3,1,2,4,2,1})
>>>c
{1,2,3,4}
>>>d = ByteArray('hello', 'UTF-8')
>>>d
bytearray(b'hello')  
Observe objects

Observe objects are the ones where the magic begins. In PyReactive, I've defined them as any data type that depends on only one operator, or method. In other words, they could be viewed as data types that have unary operands. Let's jump in to a few examples.

Use Case: str, tuple, frozenset (native python data types)

>>>a = Observe('hey')
>>>b = Observe(a)
>>>b
'hey'  

There's not much to do here, since they are immutable data types. But, although this is fairly redundant, there's a method that's allowed.

a) len - Holds the length of the data type

>>>a = Observe('hey')
>>>leng = Observe(a, method='len')
>>>leng
3  
>>>a.changeTo('hello there')
>>>leng
11  

Use Case: int, float, bool (native python data types)

>>>a = Observe(9)
>>>b = Observe(a)
>>>b
9  

There are 2 methods allowed here. They are:

a) not - this is the LOGICAL NOT operator

>>>a = Observe(2)
>>>b = Observe(a, method='not')
>>>b
False  
>>>a.changeTo(0)
>>>b
True  

b) '~' - this is the Ones COMPLEMENT operator

>>>a = Observe(1)
>>>b = Observe(a, method='~')
>>>b
-2
>>>a.changeTo(3)
>>>b
-4

Use Case: List

>>>a = List([1,3,2])
>>>b = Observe(a)
>>>b
[1,3,2]
>>>a.append(9)
>>>b
[1,3,2,9]
>>>a.insert(0,-20)
>>>b
[-20,1,3,2,9]

As you can see, every change on the list propogates in to a change on the observing object.

An Observe object also takes in an optional method. The legal keywords for the optional method are: count, reverse, sort, firstel, lastel, slice, set, len, sum, max and min.

a) count - always holds the number of occurrences of the value passed with the methodParameter option.

>>>a = list([1,1,1,4,3,5,1,1])
>>>b = Observe(a, method='count', methodParameter=1)
>>>b    #Stores the number of 1s
5  
>>>a.extend([1,1])
>>>a
[1,1,1,4,3,5,1,1,1,1]
>>>b    #Automatically updates the number of 1s
7  

b) reverse - holds a copy of the reversed List

>>>a = List([1,3,2])
>>>b = Observe(a, method='reverse')
>>>b
[2,3,1]
>>>a.append(9)
>>>a
[1,3,2,9]
>>>b    #holds the reverse of the list
[9,2,3,1]

c) sort - holds a copy of the sorted List

>>>a = List([1,3,2])
>>>b = Observe(a, method='sort')
>>>b
[1,2,3]
>>>a.extend([-1,-9,0,8])
>>>b    #prints the sorted list
[-9,-1,0,1,2,3,8]
>>>a
[1,3,2,-1,-9,0,8]

d) firstel - holds the first element of the List

>>>a = List([1,3,2])
>>>b = Observe(a, method='firstel')
>>>b
1  
>>>a.insert(0,-100)
>>>b
-100

An example that combines sort and firstel to always holds the least element of a List

>>>a = List([1,3,2])
>>>b = Observe(a, method='sort')
>>>leastEl = Observe(b, method='firstel')
>>>leastEl
1  
>>>a.append(-9)
>>>leastEl
-9
>>>a
[1,3,2,-9]
>>>b
[-9,1,2,3]

e) lastel - always holds the last element of the List

>>>a = List([1,3,2])
>>>b = Observe(a, method='lastel')
>>>b
2  
>>>a.append(9)
>>>b
9  
>>>a
[1,3,2,9]

f) slice - always holds the sliced part of the List. methodParameter is a tuple and can compose of PyReactive Observe Objects, e.g. methodParameter = (0, 4, 1), or (a, b), where a and b are PyReactive Observe objects. It also takes an optional step as the third argument. This needs to be an integer.

>>>a
[1, 2, 3, 4]
>>>slicedList = Observe(a, method='slice', methodParameter=(0, 2))
>>>slicedList
[1, 2]
>>>a.insert(0, -1)    #Inserts -1 at 0th position
>>>slicedList
[-1, 1]
>>>a
[-1, 1, 2, 3, 4]
>>>b = Observe(0)
>>>c = Observe(2)
>>>d = Observe(a, method='slice', methodParameter=(b, c))
>>>d
[-1, 1]
>>>c.changeTo(4)
>>>d
[-1, 1, 2, 3]

g) set - holds only the unique elements of the List

>>>a = List([1,3,2,2,4,1,5,2])
>>>b = Observe(a, method='set')
>>>b
{1,2,3,4,5}
>>>a.extend([5,5,5,6,7,7,6])
>>>b
{1,2,3,4,5,6,7}

h) len - holds the length of the List

>>>a = List([1,2,4,3,1])
>>>length = Observe(a, method='len')
>>>length
5  
>>>a.pop()
1  
>>>length
4  

i) sum - holds the sum of all elements of the List

>>>a = List([1, 2, 3, 4])
>>>listSum = Observe(a, method='sum')
>>>listSum
10  
>>>a.extend([5, 6, 7])
>>>listSum
28  

j) max - holds the maximum value of the List

>>>a = List([1, 2, 3, 4])
>>>listMax = Observe(a, method='max')
>>>listMax
4  
>>>a.extend([5, -1, 5, 7])
>>>listMax
7  

k) min - holds the minimum value of the List

>>>a = List([1, 2, 3, 4])
>>>listMin = Observe(a, method='min')
>>>listMin
1  
>>>a.extend([-1, -2, 5, 9, -5])
>>>listMin
-5

Use Case: Dict

>>>a = Dict({1: [12,3,65], 2: [43,23,1]})
>>>b = Observe(a)
>>>a[3] = [78,54,23]
>>>b
{1: [12,3,65], 2: [43,23,1], 3: [78,54,23]}

A change in the underlying Dict triggers a change in the Observe object. The optional method keywords are:

a) key - holds the current value of the 'key' passed in as the methodParameter

>>>a = Dict({1: [12,3,65], 2: [43,23,1]})
>>>b = Observe(a, method='key', methodParameter=1)
>>>b
[12,3,65]
>>>a[1] = [5,2]
>>>b
[5,2]

b) len - holds the length of the Dict

>>>a = Dict({1:2, 2:3})
>>>length = Observe(a, method='len')
>>>length
2  
>>>a[3] = 4
>>>length
3  

c) sum - holds the sum of all the keys in the Dict

>>>a = Dict({1: 2, 2: List([3, 4, 5])})
>>>dictSum = Observe(a, method='sum')
>>>dictSum
3  
>>>a[4] = {4, 5, 6, 7}
>>>dictSum
7   # 1 + 2 + 4  

d) max - holds the maximum of all the keys in the Dict

>>>a = Dict({1: 2, 2: List([3, 4, 5])})
>>>maxDict = Observe(a, method='max')
>>>maxDict
2  
>>>a[3] = [4, 5, 6, 7]
>>>maxDict
3  

e) min - holds the minimum of all the keys in the Dict

>>>a = Dict({1: 2, 2: List([3, 4, 5])})
>>>minDict = Observe(a, method='min')
>>>minDict
1  
>>>a[-1] = {1, 2}
>>>minDict
-1

Use Case: Set

>>>a = Set({1,2,3,4,1,1,4})
>>>a
Set({1,2,3,4})  
>>>b = Observe(a)
>>>a.update({9})
>>>b
Set({1,2,3,4,9})  
>>>a
Set({1,2,3,4,9})  

Just like in the previous case, any change to the Set data type percolates to the Observe object.

The Observe object in this case also takes a few optional methods along with a few methodParameters. The legal keywords for the optional method are: len, difference, intersection, symmetric_difference, union, isdisjoint, issubset, issuperset, sum, max and min.

a) len - holds the length of the Set

>>>a = Set({1,3,4,2,1})
>>>b = Observe(a, method='len')
>>>b
4  
>>>a.update({5})
>>>b
5  

b) difference - calculate the set difference of S1 and S2, which is the elements that are in S1 but not in S2

>>>S1 = Set({1,2,3})
>>>S2 = Set({2,3,4})
>>>diff = Observe(S1, method='difference', methodParameter=S2)
>>>diff
Set({1})  
>>>S1.update({5})
>>>diff
Set({1,5})  

c) intersection - holds elements that have a presence in both S1 and S2

>>>S1 = Set({1,2,3})
>>>S2 = Set({2,3,4})
>>>intersect = Observe(S1, method='intersection', methodParameter=S2)
>>>intersect
Set({2,3})  
>>>S2.update({1})
>>>intersect
Set({1,2,3})  

d) symmetric_difference - holds the set of elements which are in one of either set, but not in both

>>>S1 = Set({1,2,3})
>>>S2 = Set({2,3,4})
>>>symm_diff = Observe(S1, method='symmetric_difference', methodParameter=S2)
>>>symm_diff
Set({1,4})  
>>>S2.update({1})
>>>symm_diff
Set({4})  

e) union - holds the merger of the two sets

>>>S1 = Set({1,2,3})
>>>S2 = Set({5,7,8})
>>>union = Observe(S1, method='union', methodParameter=S2)
>>>union
Set({1,2,3,5,7,8})  
>>>S1.update({0,9})
>>>union
Set({0,1,2,3,5,7,8,9})  

f) isdisjoint - returns True if S1 is disjoint with S2, False otherwise

>>>S1 = Set({1,2,3})
>>>S2 = Set({4,5,6})
>>>check = Observe(S1, method='isdisjoint', methodParameter=S2)
>>>check
True  
>>>S2.update({3})
>>>check
False  
>>>S1.remove(3)
>>>check
True  

g) issubset - returns True if S1 is a subset of S2, False otherwise

>>>S1 = Set({1,2,3})
>>>S2 = Set({4,5,6})
>>>check = Observe(S1, method='issubset', methodParameter=S2)
>>>check
False  
>>>S2.update({1,2,3})
>>>check
True  

h) issuperset - returns True if S1 is superset of S2, False otherwise

>>>S1 = Set({1,2,3})
>>>S2 = Set({4,5,6})
>>>check = Observe(S1, method='issuperset', methodParameter=S2)
>>>check
False  
>>>S1.update({4,5,6})
>>>check
True  

i) sum - holds the sum of all the elements in the Set

>>>a = Set({1, 2, 3, 4})
>>>setSum = Observe(a, method='sum')
>>>setSum
10  
>>>a.update({5})
>>>setSum
15  

j) max - holds the element with the maximum value in the Set

>>>a = Set({1, 2, 3, 4})
>>>setMax = Observe(a, method='max')
>>>setMax
4  
>>>a.update({5})
>>>setMax
5  

k) min - holds the element with the minimum value in the Set

>>>a = Set({1, 2, 3, 4})
>>>setMin = Observe(a, method='min')
>>>setMin
1  
>>>a.update({-1, 5})
>>>setMin
-1

Now, it's true that many of the above optional methods could've been made as Subscribe operators, but since PyReactive doesn't support parantheses yet, there's no way to ensure the precedence of set operators. To avoid ambiguity (since in this case only one operation can be performed at a time), chaining of set operations can be used to solve complex and intricate set equations.

Use Case: ByteArray

>>>a = ByteArray('hello','UTF-8')
>>>b = Observe(a)
>>>b
bytearray(b'hello')  
>>>a[0] = 120
>>>b
bytearray(b'xello')  

Again, the change percolates to cause a change in the Observe object. The optional methods are:

a) len - Holds the length of the ByteArray

>>>a = ByteArray('hello', 'UTF-8')
>>>length = Observe(a, method='len')
>>>a.pop()
111  
>>>length
4  
>>>a
bytearray(b'hell')  

b) count - counts the number of occurrences of the value passed as the methodParameter in the ByteArray

>>>a = ByteArray("Hi There", "UTF-8")
>>>count = Observe(a, method='count', methodParameter=b'e')
>>>count
2  
>>>a.extend(b'! Evening!')
>>>count
4  

c) decode - holds the decoded ByteArray according to the decoding passed as the methodParameter. There is no default decoding. methodParameter is necessary

>>>a = ByteArray("Hi There", "UTF-8")
>>>decoded = Observe(a, method='decode', methodParameter='UTF-8')
>>>decoded
Hi There  
>>>a.extend(b"! How are you?")
>>>decoded
Hey There! How are you?  

d) endswith - holds a boolean value. Becomes True if the ByteArray ends with the parameter passed in methodParameter. methodParameter is compulsory

>>>a = ByteArray("Hi There", "UTF-8")
>>>endswith = Observe(a, method='endswith', methodParameter=b'e')
>>>endswith
True  
>>>a.extend(b'!')
>>>endswith
False  

e) find - holds the first location of the value passed in methodParameter. Holds -1 if value is not found. methodParameter is the search parameter and is compulsory. Currently, only the first location is supported.

>>>a = ByteArray("Hi There", "UTF-8")
>>>find = Observe(a, method='find', methodParameter=b'k')
>>>find
-1
>>>a.extend(b' king')
>>>find
9  

f) index - holds the first location of the value passed in methodParameter. Raises ValueError if not found. methodParameter is the search parameter and is compulsory. Currently, only the first location is supported.

>>>a = ByteArray("Hi There", "UTF-8")
>>>index = Observe(a, method='index', methodParameter=b'e')
>>>index
5  
>>>a.replace(b'H', b'e')
>>>index
0  

g) isalnum - Returns True if the ByteArray is alnum. False otherwise.

>>>a = ByteArray("Hi There", "UTF-8")
>>>isalnum = Observe(a, method='isalnum')
>>>isalnum
False  

h) isalpha - Returns True if the ByteArray is alpha. False otherwise.

>>>a = ByteArray("Hi", "UTF-8")
>>>isalpha = Observe(a, method='isalpha')
>>>isalpha
True  

i) isdigit - Returns True if the ByteArray is digit. False otherwise.

>>>a = ByteArray("12345", "UTF-8")
>>>>isdigit = Observe(a, method='isdigit')
>>>isdigit
True  

j) islower - Returns True if the ByteArray is lower. False otherwise.

>>>a = ByteArray("hi there", "UTF-8")
>>>islower = Observe(a, method='islower')
>>>islower
True  

k) isupper - Returns True if the ByteArray is upper. False otherwise.

>>>a = ByteArray("HI THERE", "UTF-8")
>>>isupper = Observe(a, method='isupper')
>>>isupper
True  

l) lower - This holds the ByteArray in its lower case

>>>a = ByteArray("Hi There", "UTF-8")
>>>lower = Observe(a, method='lower')
>>>lower
bytearray(b'hi there')  

m) upper - This holds the ByteArray in its upper case

>>>a = ByteArray("Hi There", "UTF-8")
>>>upper = Observe(a, method='upper')
>>>upper
bytearray(b'HI THERE)  

n) replace - This holds the ByteArray with the replaced byte passed in the methodParameter. methodParameter is a tuple with the first element being the byte to replace and the second element being the byte that needs to replace.

>>>a = ByteArray("Hi There", "UTF-8")
>>>replace = Observe(a, method='replace', methodParameter=(b'e', b'l'))
>>>replace
bytearray(b'Hi Thlrl)  

o) reverse - This holds the reversed ByteArray

>>>a = ByteArray("Hi There", "UTF-8")
>>>reverse = Observe(a, method='reverse')
>>>reverse
bytearray(b'erehT iH')  

p) slice - This holds the sliced ByteArray. methodParameter is a tuple and can compose of PyReactive Observe Objects, e.g. methodParameter = (0, 4, 1), or (a, b), where a and b are PyReactive Observe objects. It also takes an optional step as the third argument. This needs to be an integer.

>>>a = ByteArray("Hi There", "UTF-8")
>>>a
bytearray(b'Hi There')  
>>>sliced = Observe(a, method='slice'. methodParameter=(-3, -1))
>>>sliced
bytearray(b'er')  
>>>a.extend(b' Again')
>>>sliced
bytearray(b'ai')  
>>>a
bytearray(b'Hi There Again')  
>>>b = Observe(2)
>>>c = Observe(5)
>>>sliced = Observe(a, method='slice', methodParameter=(b, c))
>>>sliced
bytearray(b' Th')  
>>>c.changeTo(7)
bytearray(b' Ther')  

q) startswith - Returns True if the ByteArray starts with the value passed in as the methodParameter. False otherwise.

>>>a = ByteArray("Hi There", "UTF-8")
>>>startswith = Observe(a, method='startswith', methodParameter=b'H')
>>>startswith
True  

r) max - This holds the maximum value in the ByteArray. Holds an integer.

>>>a = ByteArray("Hi There", "UTF-8")
>>>maxBA = Observe(a, method='max')
>>>maxBA
114  

s) min - This holds the minimum value in the ByteArray. Holds an integer.

>>>a = ByteArray("Hi There", "UTF-8")
>>>minBA = Observe(a, method='min')
>>>minBA
32  #The UTF-8 code for blank-space in integer  
Observe class methods

Each Observe object has a few fancy methods too.

a) modifyMethod - this method modifies the current method to something different. Also takes in an optional methodParameter that acts in tandem with the method.

>>>a = List([1,3,2,4,9])
>>>b = Observe(a, method='sort')
>>>b
[1,2,3,4,9]
>>>b.modifyMethod(method='firstel')
>>>b
1  

b) notify - this, arguably, is one of the coolest piece of code I've ever imagined! This method needs to be overridden if you want something exotic to happen whenever the Observe object changes. Every time that the value of the object changes, the notify method is called. An e.g.: Let's say that we want to push the updated value via a WebSocket, all that we have to do is override the notify method to push the value via the WebSocket. It takes fewer lines than this description. Seriously.

class ObserveSocket(Observe):  
    def notify(self):
        ws.send(self)       #Where ws is the WebSocket object
>>>a = List([1,2])
>>>b = ObserveSocket(a)
>>>a.append(9)
#The updated value of b is sent via the WebSocket
>>>

c) changeTo - this method is used to change the value of the Observe object, in case it observes an immutable data type such as int, str, etc. Like in all other cases, a change here would trigger a change in all the dependents on this object.

>>>a = Observe(9)
>>>a
9  
>>>a.changeTo(19)
>>>a
19  
>>>b = Observe(a)
>>>b
19  
>>>a.changeTo(10)
>>>b
10  
>>>b.changeTo(1000)
InvalidSubscriptionError: changeTo method not permitted on mutables.  
Subscribe Objects

Subscribe objects are similar to Observe objects, but the difference is that they take in multiple operands and operators. Subscribe objects look and behave like mathematical equations. Let's look at the API and a few use cases.

API: Subscribe(expression, name='')

The expression is written in INFIX notation. The operator precedence followed is that of Python's. Once initialized, the expression is stored in the postfix notation, and hence parantheses and unary operators can be used.

If c is to subscribe to a + b, the API is:

>>>c = Subscribe('a+b')     #This also follows that a and b are the name arguments of the Observe objects a and b. Otherwise, an error is thrown

If result is to subscribe to a + b * 5 - c ** 0.87 + d - e/6, the same API looks like this:

>>>result = Subscribe('a+b*5-c**0.87+d-e/6')

As of this moment, the supported binary operators are:
1. + (Addition),
2. - (Subtraction),
3. / (Division),
4. * (Multiplication),
5. ** (Exponent),
6. % (Modulus),
7. // (Floor Division),
8. << (Binary Left Shift),
9. >> (Binary Right Shift),
10. & (Binary/Bitwise AND),
11. | (Binary/Bitwise OR),
12. ^ (Binary/Bitwise XOR),
13. 'and' (Logical AND),
14. 'or' (Logical OR).

The supported unary operators are:

  1. 'not' - Boolean NOT
  2. '~' - Bitwise NOT
  3. 'sin' - math.sin (Returns the sine of the value)
  4. 'cos' - math.cos (Returns the cosine of the value)
  5. 'tan' - math.tan (Returns the tan of the value)
  6. 'round' - round (Rounds to the nearest integer; does not round to a particular precision as of yet)
  7. 'ceil' - math.ceil (Rounds to the lowest integer greater than the value)
  8. 'floor' - math.floor (Rounds to the greatest integer lower than the value)
  9. 'abs' - math.fabs (Returns the absolute value)

Additionally, one can subscribe to other data types such as ByteArrays, Lists, Dicts, Sets, Observe objects, Subscribe objects.

Subscribe class methods

Each Subscribe object has a few fancy methods too.

a) equation - displays the current expression subscribed to.

>>>c = Subscribe('9<<10')
>>>c
9216  
>>>c.equation()
9<<10  

b) append - appends variables and their corresponding operators to the existing expression. The API is same as the one used during initialization.

>>>a = Observe(12, name='a')
>>>b = Observe(16, name='b')
>>>subs = Subscribe('a*b')
>>>subs
192  
>>>c = Observe(20, name='c')
>>>subs.append('-c')
>>>subs
172  
>>>subs.equation()
a*b-c  

c) notify - Similar to the notify method on an Observe object, this method too needs to be overridden to do something meaningful. The notify method is called every time there's a change in the underlying value of the Subscribe object.

>>>a = Observe(10, name='a')
>>>b = Observe(11, name='b')
>>>class SubNotify(Subscribe):
    def notify(self):
        if self.value > 23:
            print("%s hit the upper limit!"%self.name)
>>>c = SubNotify('a+b', name='c')
>>>a.changeTo(11)
>>>b.changeTo(12)
>>>a.changeTo(12)
c hit the upper limit!  
>>>
Example Subscribe API

Simple arithmetic

>>>a = Observe(5, name='a')
>>>b = Observe(8, name='b')
>>>c = Observe(25, name='c')
>>>sub = Subscribe('a+b*c-c/(a*b)')
>>>sub.value
204.375  
>>>c.changeTo(15)
>>>sub.value
124.625  
>>>sub.append('-ceil(b/a)+floor(a/b)')
>>>sub.value
122.625  

Trigonometric equations

>>>pi = Observe(math.pi, name='pi')
>>>alpha = Observe(2, name='alpha')
>>>beta = Observe(3, name='beta')
>>>gamma = Observe(4, name='gamma')
>>>sub = Subscribe('sin(pi/alpha) + cos(pi/beta) + tan(pi/gamma)')
>>>sub.value
2.5  
>>>gamma.changeTo(6)
>>>sub.value
2.0773502691896257  
>>>sub.name = 'sub'
>>>rounded = Subscribe('round(sub)')
>>>rounded.value
2  
>>>ceiled = Subscribe('ceil(sub)')
>>>ceiled.value
3  
>>>floored = Subscribe('floor(sub)')
>>>floored.value
2  
A few use cases
  1. Let's say you're trying to build an awesome timeline. Now, every item on the timeline has a timestamp (obviously). Of course, there's a million ways you could go about it, but I feel this can be achieved in an easier way by simply observing a timer object (although this isn't supported by PyReactive as of yet). Or, we could have the timer object to append the current time after every 5 secs to a List object, which is being observed by an Observe object with the methodParameter being 'lastel'.
    So, this Observe object always holds the current time (max delta being 5 secs). This object could be pushed to the frontend. Now sure, this is an elementary example, and there are a thousand better ways to go about this.

  2. What if you want to be notified when a stock hits a certain value? Simple! Retrieve the current value of the stock and push it into a List object. Now, define an Observe object over this List. Also, override the onchange() method on the Observe object to include the handling code for the case when it hits that value. And voilĂ !
    I'll show this using pseduo code.

def ObserveStock(Observe):  
    def onchange(self):
        if self.value > 50:
            print("SELL THE STOCK")
>>>stockvalues = List()
>>>monitorStock = ObserveStock(stockvalues, method='lastel')
>>>for time in timeOut(5):        #Repeat this code every 5 secs
... valueRetrieved = requestCurrentStockValue
... stockValues.append(valueRetrieved)

Now, when the stock hits any value greater than 50, the print statement will execute. No sweat!

I shall keep adding example use cases as and when I get ideas.

Efficiency

Now, like I mentioned at the beginning of the blogpost, this is my first ever trial at writing something whose usefulness lasts beyond the life of a program. There's a very good chance that I might have not followed the best use cases. Also, I'm not one of those coders who write succinct code. There could've been many mistakes, and I'm more than willing to rectify them if you point them out.

What about the execution time, since there are so many changes that need to be taken care of, in case there are many dependencies? Great question! Observe and Subscribe objects do not poll for changes in the underlying data. Rather, they are notified by the data types themselves when they change. In other words, these objects change only when the underlying objects change. These are called callbacks that I had described at the beginning, and hence, are pretty efficient.

Known Issues

a)

>>>a = List([1,3,2])
>>>b = Dict({1:a})
>>>c = Observe(b)
>>>b[1].append(9)
>>>a
[1,3,2,9]
>>>b
{1: [1,3,2,9]}
>>>c
{1: [1,3,2,9]}

Although c works as expected, the change isn't triggered in c because of the change in b. So, overriding notify method of c wouldn't work in this case.
v0.2.0 has resolved issue a). Support for deep Lists, Dicts, ByteArrays, Sets, Observe objects has been introduced. This issue still persists in all versions <= v0.1.6

b) If an error occurs when variables are being updated, then they might go out of sync. As long as no errors are thrown, the module does what it is told to. A workaround could be to isolate each updation, try to update, and commit the update when there are no errors. Will see to it in the next version.

c) There is no way to delete PyReactive objects as of now. Using del variableName might remove the variable from the environment, but it does not remove the dependencies. Will provide an API for deletion in the next update.

Major Changes
  • As of v0.2.3, the notify method has turned silent in case its value does not change. So, it only notifies when there's a tangible change in the value. The below example should help..
>>>l = List([1, 2, 3])
>>>class Trial(Observe):
...     def notify(self):
...         print("Updated %s"%self.value)
>>>a = Trial(l, method='max')
Updated 3  
>>>b = Trial(l, method='min')
Updated 1  
>>>a
3  
>>>b
1  
>>>l.append(-1)
Updated -1  
>>>b
-1
>>>l.append(4)
Updated 4  
>>>a
4  
>>>l.append(2.5)
>>>#No update to either a or b

In the above example, the notify method on a is called only when the max value in the List changes. Similarly, the notify method on b is called only when the min value in the List changes. In versions prior to 0.2.3, the notify method was called irrespective of whether its value changed. This has been altered so that no unnecessary function calls are made.

  • As of v0.3.0, the Subscribe API has been altered to make it intuitive. Now accepts infix expressions as strings, parses and stores them as postfix expressions. Hence, unary, binary operators and parantheses can now be used.
Further work:

1) Open up access to other data types and objects such as those of numpy/scipy, etc.
2) Extend this module such that user-defined operators can be included.
3) Write this using asyncio, if needed.

If I was able to capture your attention for this long, please leave a comment (it'd be an achievement). I would go treat myself to a burger. I shall update this as and when the module changes. Again, the github link is https://github.com/apratheek/PyReactive. Cheers!