[vJASS] Observables

Level 15
Joined
Nov 30, 2007
Messages
1,202
This is a subscriber observable-interface you can use to manage all your callback methods to achieve data binding, should you need it. It essentially does what real variable change events does but for any data type.

Observers hold active Subscribers and data that can change. Whenever the value change the subscribers can be notified of updates that has occured.

LiveData is an extension of Observable and will automatically notify Subscribers whenever the data changes value. Note that assigning the same value does not trigger any callbacks.

Subscribers can observe one or more Observables and executes a callback whenever it is notified of state changes from the Observable.

The library includes an ArrayList that was used to reduce the code amount.

One of the strengths of this is that you can Create a Many-to-Many map, meaning: one Subscriber can watch many Observers, and equally one Observer can have many Subscribers.

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 = .observables.length -1
            loop
                exitwhen i < 0
                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 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

I've provided a Lab to show how this library can be used:

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

Known Issues:
- LiveData does not work as intended when a struct extends it. The child struct has the same behavior as a regular Observable would (changes are not automatically notified).
 
Last edited:
If calling func by default, or method operator through extra struct.. maybe making it operator compatible in first place? ;p

Those thistype this = Table.create() seems abusive, as they are initially meant to return indices out of bounds ( > max array size), due to that Table works special in this regard. Is there a reason why no "normal way" with a member table variable is used?^^
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
If calling func by default, or method operator through extra struct.. maybe making it operator compatible in first place? ;p

You mean getting rid of the extra struct and instead using a method call for the auto-notice, obs.setValue(5) or obs.setLiveValue(5) for instance? I didn't like the syntax behind it and wanted the user to have control over when to send notifications in the default case :<

Those thistype this = Table.create() seems abusive, as they are initially meant to return indices out of bounds ( > max array size), due to that Table works special in this regard. Is there a reason why no "normal way" with a member table variable is used?^^

This allows for breaking the limit of observables that can be allocated. Maybe one should drag Bribe over here to ask him about such struct allocation, but I've never seen any complaints on the subject before. I could however make this optional.
 
This allows for breaking the limit of observables that can be allocated. Maybe one should drag Bribe over here to ask him about such struct allocation, but I've never seen any complaints on the subject before. I could however make this optional.

Using struct instance higher than array limit will not work correctly for normal struct usage. Table itself fakes struct usgage, starts higher JASS_MAX_ARRAY_SIZE for this, but at same time it doesn't work with struct (array) syntax, but with hashtable internally.. so it can work properly with this higher JASS_MAX_ARRAY_SIZE.

But in the current code above, a normal struct syntax is used, like..
set this.length = 0
..so it's limited to JASS_MAX_ARRAY_SIZE.

The reason it currently works should be only because Table's internal array limit is not up to date. It thinks the array limit is 8192, and not 32,768 (since patch 1.29). When it works as intended, it should return indices higher than array_limit, and then the code above would break.

JASS:
// currently:

this = Table.create() // -> 8191
this.value = 1        // -> works currently, because 8191 is in current array size bounds


// after array size is updated in Table:

this = Table.create() // -> 32,767
this.value = 1        // -> malfunction, because of MAX_ARRAY_SIZE
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
.length is an operator faking normal struct syntax. I've created more than 32,768 instances using this method before. I don't see how changing the constant for MAX_ARRAY_SIZE that Table uses would change anything? I'm assuming it was set to 8191 to avoid collision but I don't think there is any risk in that in this case since it is only allocated as a table and never as a struct.

But I don't mind changing it to a normal struct either, but then the user will have no be careful with allocated instances, though 32,700 does seem like it would be enough xD
 
Yes, you actually don't use array syntax, I have overlooked the operator. It should work fine then with bigger array size.

Edit:

I'm assuming it was set to 8191 to avoid collision
so you can make inits in globals block:
JASS:
globals
    key k
    Table table = k
endglobals
so they are reserved for those constants.

edit:

key isn't related to JASS_MAX_ARRAY_SIZE, it seems, so the number 8190 from Table is seemingly an arbitary a bit higher value.^^
 
Last edited:
Top