• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Observers and subscribers - why not?

Status
Not open for further replies.
Level 15
Joined
Nov 30, 2007
Messages
1,202
This is a pretty simple and straightforward method to achieve data binding. One observer can have multiple subscribers listening for state changes. The observer holds the data and just notifies the subscribers if the value has changed. The subscriber can only watch one observer and manages the callback trigger.

The purpose of the library is to allow for asynchronous programming instead of doing a lookup for state changes. It's not as efficent in most cases I reckon but here it is. One possible use case is when you are designing interfaces of any kind, a menu for example. Instead of looping through and updating each field you check for state changes for when specific fields should be updated. It can also be used as a callback interface.

In this thread we can discuss the utility (if you think there is any) of observables in jass and what their use cases could possibly be and how to improve the library, or if there are any major flaws with the system that I am unaware of.

I would submit this as a code snippet but I'm not sure if such a resource already exists or not.

JASS:
library Observables uses Table optional PArrayList
/*
    ===============================================================================================
        Library:     Observables
        Version:     1.1
        Made by:     Pinzu
   ===============================================================================================
        This library provides a subscriber observable interface for data binding.
      
        The observable holds the data and can notify the subscriber of any changes. LiveData extends
        Observable and will automatically notify the subscribers of any changes when its value change,
        where as Observable requires the method notifyChanges() to be called. Note that the value must
        actually be assigned to a different value for changes to be detected.
      
        The subscriber can only observe one Observable at a time and is responsible for executing
        the callback trigger when changes are detected.
  
    ===============================================================================================
        Configuration
    ===============================================================================================
        Simply add any data types you wish to include below:
      
        */
        //! runtextmacro DEFINE_OBS("Integer", "integer", "0")
        //! runtextmacro DEFINE_OBS("Boolean", "boolean", "false")
        //! runtextmacro DEFINE_OBS("Real", "real", "0")
        //! runtextmacro DEFINE_OBS("String", "string", "null")
        //! textmacro_once DEFINE_OBS takes NAME, TYPE, NONE
        /*
      
    ===============================================================================================
       API
    ===============================================================================================
        *** Access Globals ***
      
            <NAME>Subscriber      last<NAME>Sub            Notified subscriber
            Observable<NAME>    lastObs<NAME>            Changed observer
            <TYPE>                last<NAME>Value            Changed value
  
  
        *** struct <NAME>Subscriber ***
            method operator callback= takes code c returns nothing
                Changes the callback function to the provided code
              
              
            method subscribe takes Observable<NAME> observer returns nothing
                Adds an observable to the subscription.
              
            method unsubscribe takes Observable<NAME> observer returns nothing
                Removes the observable from the subscription
              
            method clear takes nothing returns nothing
                Stops subscribing to all observables
              
            method destroy takes nothing returns nothing
                Destroys it
              
            static method create takes code callback returns thistype
                Creates it
       *** struct Observable<NAME> ***
  
        method operator value= takes <TYPE> newValue returns nothing
            Sets the value of the Observable
          
        method operator value takes nothing returns <TYPE>
            Gets the value of the Observable
          
        method hasChanged takes nothing returns boolean
            Returns true if the assigned value differs from the previous one
          
        method notifyChanges takes nothing returns nothing
            Notifies all current subscribers that changes have been made.
          
        method hasSubscriber takes <NAME>Subscriber subscriber returns boolean
            Returns true if the Subscriber is observing the Observable.
          
        method subscribe takes <NAME>Subscriber subscriber returns nothing
            Adds a Subscriber to the subscription
          
        method unsubscribe takes <NAME>Subscriber subscriber returns nothing
            Removes a Subscriber from the subscription
          
        method clear takes nothing returns nothing
            Removes all Subscribers from the subscription.
          
        method destroy takes nothing returns nothing
            Destroys it
          
        static method create takes <TYPE> newValue returns thistype
            Creates it with the assigned value.
      
      
        LiveData has the same methods as Observable, the only difference in behavior is that LiveData will
        automatically notify Subscribers whenever the value changes.
      
        *** struct <NAME>LiveData extends Observable<NAME> ***
    */
  
    globals   
        $NAME$Subscriber          last$NAME$Sub
        Observable$NAME$        lastObs$NAME$
        $TYPE$                    last$NAME$Value
    endglobals
    struct $NAME$Subscriber
        private trigger t
        private IntArrayList observables
      
        method operator callback= takes code c returns nothing
            call TriggerClearConditions(.t)
            if not (c == null) then
                call TriggerAddCondition(this.t, Condition(c))
            endif
        endmethod
      
        // Should not be used outside this library scope
        method observers takes nothing returns IntArrayList
            return observables
        endmethod
      
        static method create takes code callback returns thistype
            local thistype this = .allocate()
            set this.observables = IntArrayList.create()
            set this.t = CreateTrigger()
            call TriggerAddCondition(this.t, Condition(callback))
            return this
        endmethod
        method destroy takes nothing returns nothing
            call .clear()
            call .observables.destroy()
            call TriggerClearConditions(.t)
            call DestroyTrigger(.t)
            set .t = null
        endmethod
        // Should not be used outside this library scope
        method onChange takes nothing returns nothing
            set last$NAME$Sub  = this
            call TriggerEvaluate(.t)
        endmethod
      
        method subscribe takes Observable$NAME$ observer returns nothing
            call observer.subscribe(this)
        endmethod
  
        method unsubscribe takes Observable$NAME$ observer returns nothing
            call observer.unsubscribe(this)
        endmethod
      
        method clear takes nothing returns nothing
            local integer i = 0
            loop
                exitwhen i == .observables.length
                call Observable$NAME$(.observables[i]).unsubscribe(this)
                set i = i + 1
            endloop
        endmethod
      
    endstruct
  
    struct Observable$NAME$
  
        method operator value= takes $TYPE$ newValue returns nothing
            local boolean valueDiff = Table(this).$TYPE$[-1] != newValue
            set Table(this).$TYPE$[-1] = newValue
            set Table(this).boolean[-2] = valueDiff                  
            if valueDiff and .getType() == $NAME$LiveData.typeid then
                call .notifyChanges()
            endif
        endmethod
        method operator value takes nothing returns $TYPE$
            return Table(this).$TYPE$[-1]
        endmethod
      
        private method operator subscribers takes nothing returns IntArrayList
            return Table(this)[-3]
        endmethod
      
        method hasChanged takes nothing returns boolean
            return Table(this).boolean[-2]
        endmethod
  
        static method create takes $TYPE$ newValue returns thistype
            local Observable$NAME$  this = Table.create()
            set Table(this)[-3] = IntArrayList.create()
            set this.value = newValue
            return this
        endmethod
      
        method destroy takes nothing returns nothing
            call .clear()
            call .subscribers.destroy()
            call Table(this).destroy()
        endmethod
      
        method clear takes nothing returns nothing
            local integer i = .subscribers.length - 1
            loop
                exitwhen i < 0
                call .unsubscribe(.subscribers[i])
                set i = i - 1
            endloop
        endmethod
      
        method notifyChanges takes nothing returns nothing
            local integer i = 0
            set lastObs$NAME$ = this
            set last$NAME$Value = .value
            loop
                exitwhen i == .subscribers.length
                call $NAME$Subscriber(.subscribers[i]).onChange()
                set i = i + 1
            endloop
        endmethod
      
        method hasSubscriber takes $NAME$Subscriber subscriber returns boolean
            return .subscribers.contains(subscriber)
        endmethod
        method subscribe takes $NAME$Subscriber subscriber returns nothing
            if hasSubscriber(subscriber) then
                return
            endif
            call .subscribers.add(0, subscriber)
            call subscriber.observers().add(0, this)
        endmethod
  
        method unsubscribe takes $NAME$Subscriber subscriber returns nothing
            local integer index = .subscribers.indexOf(subscriber, true)
            local IntArrayList subObservers
            if index != -1 then
                call subscribers.remove(index)
                set subObservers = subscriber.observers()
                call subObservers.remove(subObservers.indexOf(this, true))
            endif
        endmethod
    endstruct
  
    struct $NAME$LiveData extends Observable$NAME$
        /* This is empty due to the difference in behavior is being managed inside the parent instead. */
    endstruct
//! endtextmacro
/*
    ===============================================================================================
        Injected library if the map does not already have it
    ===============================================================================================
*/
    // library PArrayList uses Table
    /*
        Made by: Pinzu
        This is a very basic list using Table for storage.
        Requirements:
        Table by Bribe
            hiveworkshop.com/threads/snippet-new-table.188084/
    */  
  
    static if not LIBRARY_PArrayList then
        //! runtextmacro DEFINE_ARRAY_LIST("Int", "integer", "0")  
        //! textmacro_once DEFINE_ARRAY_LIST takes NAME, TYPE, NONE
      
        struct $NAME$ArrayList
          
            static method create takes nothing returns thistype
                local thistype this = Table.create()
                set this.length = 0  
                return this
            endmethod
          
            implement optional $NAME$ArrayListSortModule
          
            method destroy takes nothing returns nothing
                call Table(this).destroy()
            endmethod
          
            method clear takes nothing returns nothing
                call Table(this).flush()
                set .length = 0
            endmethod
            private method operator length= takes integer index returns nothing
                set Table(this).integer[-1] = index
            endmethod
          
            method operator length takes nothing returns integer
                return Table(this).integer[-1]
            endmethod
          
            // Getter: list[index]
            method operator [] takes integer index returns $TYPE$
                if (index < 0 or index >= .length) then
                    debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60, "$NAME$ArrayList.get:IndexOutOfBounds")
                    return $NONE$
                endif
                return Table(this).$TYPE$[index]
            endmethod
          
            // Setter: list[index] = element
            method operator []= takes integer index, $TYPE$ element returns nothing
                if (index < 0 or index >= .length) then
                    debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60, "$NAME$ArrayList.set:IndexOutOfBounds")
                    return
                endif
                set Table(this)[index] = element
            endmethod
          
            method add takes integer index, $TYPE$ element returns boolean
                local integer i
                if (index < 0 or index > .length) then
                    debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60, "$NAME$ArrayList.add:IndexOutOfBounds")
                    return false
                endif
                set i = .length
                loop
                    exitwhen i == index
                    set Table(this).$TYPE$[i] = Table(this).$TYPE$[i - 1]
                    set i = i - 1
                endloop
                set Table(this).$TYPE$[index] = element
                set .length = .length + 1
                return true
            endmethod
          
            method remove takes integer index returns $TYPE$
                local integer i
                local $TYPE$ returnValue
                if (index < 0 or index >= .length) then
                    debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60, "$NAME$ArrayList.remove:IndexOutOfBounds")
                    return $NONE$
                endif
                set i = index
                set .length = .length - 1
                loop
                    exitwhen i == .length
                    set Table(this).$TYPE$[i] = Table(this).$TYPE$[i + 1]
                    set i = i + 1
                endloop
                set returnValue = Table(this)[i]
                call Table(this).$TYPE$.remove(.length)
                return returnValue
            endmethod
          
            method contains takes $TYPE$ value returns boolean
                return .indexOf(value, true) != -1
            endmethod
          
            method indexOf takes $TYPE$ value, boolean ascending returns integer
                local integer i
                if ascending then
                    set i = 0
                    loop
                        exitwhen i == .length
                        if Table(this).$TYPE$[i] == value then
                            return i
                        endif
                        set i = i + 1
                    endloop
                else
                    set i = .length - 1
                    loop
                        exitwhen i < 0
                        if Table(this).$TYPE$[i] == value then
                            return i
                        endif
                        set i = i - 1
                    endloop
                endif
                return -1
            endmethod
          
            method isEmpty takes nothing returns boolean
                return .length == 0
            endmethod
          
            method size takes nothing returns integer
                return .length
            endmethod
          
            method swap takes integer indexA, integer indexB returns nothing
                local $TYPE$ temp
                if (indexA < 0 or indexA >= .length or indexB < 0 or indexB >= .length) then
                    debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60, "$NAME$ArrayList.swap:IndexOutOfBounds")
                    return
                endif
                set temp = Table(this).$TYPE$[indexA]
                set Table(this).$TYPE$[indexA] = Table(this).$TYPE$[indexB]
                set Table(this).$TYPE$[indexB] = temp
            endmethod
          
            static method copy takes $NAME$ArrayList src, integer srcPos, $NAME$ArrayList dest, integer destPos, integer range returns nothing
                local integer i = 0
                if srcPos < 0 or srcPos == src.length or destPos < 0 or destPos > dest.length then
                    debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60, "$NAME$ArrayList.copy:IndexOutOfBounds")
                    return
                endif
                loop
                    exitwhen i == range or srcPos >= src.length
                    call dest.add(destPos, src[srcPos])
                    set destPos = destPos + 1
                    set srcPos = srcPos + 1
                    set i = i + 1
                endloop
            endmethod
          
    debug    method print takes nothing returns nothing
    debug         local integer i = 0
    debug        local string s = "[" + I2S(.length) + "]: "
    debug         loop
    debug             exitwhen i == .length
    debug            set s = s + I2S(this[i]) + ", "
    debug             set i = i + 1
    debug         endloop
    debug         call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0,60, s)
    debug    endmethod
        endstruct
    //! endtextmacro
    endif
    // endlibrary PArrayList uses Table
endlibrary

Test environment:

JASS:
scope Example1
    private function OnChange takes nothing returns nothing
        // These are the globals available to us on callback
        call BJDebugMsg("Notified Subscriber: " + I2S(lastIntegerSub))    // Notified subscriber
        call BJDebugMsg("Changed Observable: " + I2S(lastObsInteger))    // Changed Observable
        call BJDebugMsg("Changed Value: " + I2S(lastIntegerValue))        // Observable Value
    endfunction
 
    function RunEx1 takes nothing returns nothing
        // Here we create an observable integer with value 0 and then we bind it to our subscriber
        local ObservableInteger obs = ObservableInteger.create(0) 
     
        // Here we are creating a subscriber with a callback function
        local IntegerSubscriber sub = IntegerSubscriber.create(function OnChange)
        // We can always change the callback function
        set sub.callback = function OnChange 
     
        // Here we bind the observer to the subscriber
        call sub.subscribe(obs)
     
        call BJDebugMsg("Ex 1")
     
        set obs.value = 10 
        // This is required to execute the callback, without it nothing happens
        call obs.notifyChanges() 
    endfunction
endscope
scope Example2
    // This works exactly as before
    private function OnChange takes nothing returns nothing
        call BJDebugMsg("Changed Value: " + I2S(lastIntegerValue))
    endfunction
 
    function RunEx2 takes nothing returns nothing
        // Now lets try using live data instead, and this time create 2!
        local IntegerLiveData liveData1 = IntegerLiveData.create(5) 
        local IntegerLiveData liveData2 = IntegerLiveData.create(10)
     
        // Again we subscribe to the LiveData the same way as we did before
        local IntegerSubscriber sub =  IntegerSubscriber.create(function OnChange)
        call sub.subscribe(liveData1)
        call sub.subscribe(liveData2)
     
        call BJDebugMsg("Ex 2")
     
        // The difference between LiveData and observables is that they will automatically
        // trigger a callback each time their value change
     
        set liveData1.value = 100    // this will trigger a callback
        set liveData1.value = 100    // this will not, as the value is unchanged
     
        set liveData2.value = 900    // We can watch multiple observers from the same subscriber
     
    endfunction
endscope
scope Example3
    private function OnChange takes nothing returns nothing
        call BJDebugMsg("Changed Value: " + I2S(lastIntegerValue))
    endfunction
 
    function RunEx3 takes nothing returns nothing
     
        local IntegerLiveData liveData = IntegerLiveData.create(19) 
        local IntegerSubscriber sub =  IntegerSubscriber.create(function OnChange)
     
        // You can also subscribe using the observer directly
        call liveData.subscribe(sub)
     
        call BJDebugMsg("Ex 3")
     
        set liveData.value = liveData.value *100    // Will trigger a callback
    endfunction
endscope
scope Example4
 
    // We can also extend Observables (Live Data behavior can not be extended however)
    struct Talker extends ObservableInteger
        private string str
     
        static method create takes nothing returns thistype
            local thistype this = .allocate(0)    // We have now created an Observable with value 0
            return this
        endmethod
     
        method message takes string s returns nothing
            set this.str = s
            set .value = GetRandomInt(0, 100)
            call this.notifyChanges() 
        endmethod
     
        // A new message was received, display it!
        method display takes nothing returns nothing
            call BJDebugMsg(this.str + " " + I2S(lastIntegerValue))
        endmethod
    endstruct
 
    private function OnChange takes nothing returns nothing
        call Talker(lastObsInteger).display()
    endfunction
 
    function RunEx4 takes nothing returns nothing
        local Talker talker = Talker.create()
 
        local IntegerSubscriber sub =  IntegerSubscriber.create(function OnChange)
        call sub.subscribe(talker)
     
        call BJDebugMsg("Ex 4")
        call talker.message("Hello!")
 
    endfunction
 
endscope
// This example didnt turn out as well as I hoped, but my point is that you can create funny design :d
scope Example5
    struct Sender
        ObservableString msg
        ObservableString error
     
        static method create takes nothing returns thistype
            local thistype this = .allocate()
            set this.msg = ObservableString.create("")
            set this.error = ObservableString.create("")
            return this
        endmethod
     
        method work takes nothing returns nothing
            local integer rdm = GetRandomInt(0, 100)
            if rdm < 50 then
                set msg.value = "We have a random number: " + I2S(rdm) + "!"
                call msg.notifyChanges()
            else
                set error.value = "We failed to get a random number :("
                call error.notifyChanges()
            endif
        endmethod  
    endstruct
    struct Reciever extends StringSubscriber
        private static method manageChanges takes nothing returns nothing
            call BJDebugMsg("Msg recieved: " + lastStringValue)
        endmethod
 
        static method create takes ObservableString o1, ObservableString o2 returns thistype
            local thistype this = .allocate(function thistype.manageChanges)
            call this.subscribe(o1)
            call this.subscribe(o2)
            return this
        endmethod
    endstruct
 
    function RunEx5 takes nothing returns nothing
        local Sender sender = Sender.create()
        local Reciever reciever = Reciever.create(sender.msg, sender.error)
        call BJDebugMsg("Ex 5")
        call sender.work()
    endfunction
 
endscope
scope TestObserver initializer Init
    private function Init takes nothing returns nothing
        call ExecuteFunc("RunEx1")
        call ExecuteFunc("RunEx2")
        call ExecuteFunc("RunEx3")
        call ExecuteFunc("RunEx4")
        call ExecuteFunc("RunEx5")
    endfunction
endscope
 
Last edited:
Level 31
Joined
Jul 10, 2007
Messages
6,306
Observables are more useful in a UI setting with rendering. A new render should only take place when the value actually changes. From a data perspective, we care more about when the value was assigned and less about when the value actually changed. Detecting changes in values rather than assignments can lead to unintended behavior.

My two cents <3
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
Observables are more useful in a UI setting with rendering. A new render should only take place when the value actually changes. From a data perspective, we care more about when the value was assigned and less about when the value actually changed. Detecting changes in values rather than assignments can lead to unintended behavior.

You're refering to the if-statement? I wanted to keep it as a way to reduce trigger overhead, to me it is counter intuitive to the concept that an unchanged variable should notify as changed even if it was assigned a value. Perhaps the observer could have a notifyChanges method for cases when it's assigned the same value or make the whole thing manual, removing the automatic notification / or make it something that is toggleable.

I guess this is more akin to LiveData than a observable as it currently is implemented.

What about having the observable be passive and the user have to manually post changes using "notifyChanges" and then having LiveData extending the observable and only overrides the setValue method?

*Edit

Update 1.1

The previous destinction that Nestharus pointed out have been addressed. The observable now have 2 new methods hasChanged() returns true if the assigned value was different from the previous value and notifyChanges(). This means that observables no longer automatically sends notifications to the subscribers. To keep the previous logic of automatic notifications a LiveData struct was added, extending the Observable. It will notify subscribers if and only if it's value has changed (when using the method setValue(value)).

Added a few more examples, 1 where the struct extends ObservableInteger to inform the callback that it a struct instance has changed and another which has multiple observable member variables.

Oh and I also made the Observer completely table based.
 
Last edited:
Level 31
Joined
Jul 10, 2007
Messages
6,306
Pinzu, you had the correct idea with Observables. I simply remarked on my experience working with observables for driving some data when I was working on a javascript client using mobx. The use of observables in my design had some unintended consequences : ).\

Have some UI libraries on THW now too. Observables would definitely be very useful for those! = ).
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
Pinzu, you had the correct idea with Observables. I simply remarked on my experience working with observables for driving some data when I was working on a javascript client using mobx. The use of observables in my design had some unintended consequences : ).

It always does, oh the errors I created using RxJava and MVVM... ^^

On-Topic:

I have a trouble with the LiveData inheritence, that is, if a struct extends LiveData it no longer will automatically notify of changes as it's parent would.

It's related to this part of the code inside the Observable, but I'm not sure how to change .getType() == $NAME$LiveData.typeid to also accept any structs that have extended $NAME$LiveData.

JASS:
method operator value= takes $TYPE$ newValue returns nothing
            local boolean valueDiff = Table(this).$TYPE$[-1] != newValue
            set Table(this).$TYPE$[-1] = newValue
            set Table(this).boolean[-2] = valueDiff            
            if valueDiff and .getType() == $NAME$LiveData.typeid then        // <---
                call .notifyChanges()
            endif
        endmethod
 
Last edited:
Status
Not open for further replies.
Top