I'm looking for a solution for subscribing to attribute changes in Go. Given the below structs I'd like to implement a derived attribute which subscribes to its source attributes, and only when being read it would reevaluate itself. It would know to do so due to being notified, or by checking a "dirty flag" (channel?) if one or more of the sources had changed. Edit: I'm not looking for a "getter" function, which would not cache the fetched values, instead fethcing them every time being read). See also an added DeriveAndSubscribe-method far below, illustrating what the derived FullName would do).
I guess this resembles a fairly typical case. Se example below:
type Person struct {
/FullName string // Derived, from the two below:
FirstName string // persistent
LastName string // persistent
}
The concept must be "visible" for far subscriptions/fetches too, for example a User object deriving it's detailed user info from an underlaying Person object:
type User struct {
person *Person
/FullName string // Derived from person.FullName above
}
(Ok, people's names don't change very often but the example must be simple).
My own first thoughts on this is,
Pull - The derived attributes (FullName) is to be "lazy" (evaluated only if someone is reading it/them). Therefore it seems the most natural to "pull" any subscriptions ("dirty" flag/notification) only when evaluating the Fullname string, that is, "ask" if any change has occured.
Cache - when the value has been derived, then store it in a (hidden) field (_fullName) so that the string can be reused at next read, if its subscribed values hasn't changed.
Lazy subscription - Not only the derive operation should be "lazy" when someone reads the FullName attribute, but also the subscription itself should be laid only the first time it is evaluated (when someone reads the attribute).
Good reasons for pull instead of push seems to be that the subscribing attribute(s) may or may not be present when the underlaying attributes change. If there's no "send-list" at the source then there's no need to "unregister" either if/when the end subscribing attributes/objects goes away. And further; in a distributed scenario (User and Person on different machines) it's better to update things only if the data is actually explicitly asked for (that goes for the subscription as well, which can be laid only at the first read of FullName).
Luxury would be if a goroutine (optionally) could update (reevaluate) the FullName attribute(s) when CPU isn't very busy, while reevaluation would be immediately enforced if someone reads the FullName attribute (could both be achieved in one solution?).
In any case, here's the subscriptions that needs to be laid (ASCII model):
[Person]./FullName --> [Person].FirstName // Subscribe 1
[Person].LastName // Subscribe 2
and
[User]./FullName --> [User].person./FullName // Subscribe 3
That is, alltogether three (3) subscriptons to keep the User.FullName attrib updated. (Disregard for now the [User].person-link). Can something like this be achieved using channels, and if so, um... how?
Below the above structs with the hidden fields inserted (used for caching the derived result until next time the source attributes gets "dirty"):
type Person struct {
/FullName string // Derived
_fullName string // "cache"
FirstName string
LastName string
}
and:
type User struct {
person *Person
/FullName string // Derived
_fullName string // "cache"
}
Edit: The Person-FullName-attribute could be served by a method like this one (it could be packaged into typed attribute objects (structs) later):
func (p *Person) _FullName_DeriveAndSubscribe(Subscriber chan) string {
if /* check if channel(s) is "dirty" */ {
//
// Keep an internal channel, and get hold of the channel, or
// Chan of Chan(?) wich can notify us if any underlaying values change:
//
// _subscr = Subscriber
//
// Now, update the cache
_fullName = FirstName + " " + LastName
}
return _fullName // return the cached value
}
Just make functions out of these derived attributes unless benchmarking shows that this is a bottleneck. In that case, you could still insert hidden (lowercase) fields to implement whatever sort of caching.
type Person struct {
FirstName, LastName string
}
func (p *Person) FullName() string {
return p.FirstName + " " + p.LastName
}
http://play.golang.org/p/THNb3C-TLq
package main
import (
"fmt"
)
type ChangeHandler func(interface{})
type EventedChanger interface {
Get(name string) interface{}
Set(name string, value interface{}) EventedChanger
OnChange(name string, listener ChangeHandler) EventedChanger
}
type MyChanger struct {
data map[string]interface{}
listeners map[string][]ChangeHandler
}
func (m *MyChanger) Get(name string) interface{} {
val, ok := m.data[name]
if !ok {
return nil
}
return val
}
func (m *MyChanger) Set(name string, value interface{}) EventedChanger {
m.data[name] = value
if listeners, ok := m.listeners[name]; ok {
for _, l := range listeners {
l(value)
}
}
return m
}
func (m *MyChanger) OnChange(name string, listener ChangeHandler) EventedChanger {
m.listeners[name] = append(m.listeners[name], listener)
return m
}
func NewMyChanger() *MyChanger {
return &MyChanger{
make(map[string]interface{}),
make(map[string][]ChangeHandler),
}
}
func main() {
c := NewMyChanger()
h := func(value interface{}) {
c.Set("fullname", fmt.Sprint(c.Get("firstname"), c.Get("lastname")))
}
q := func(value interface{}) {
fmt.Println("Full name:", value)
}
c.OnChange("firstname", h).OnChange("lastname", h).OnChange("fullname", q)
c.Set("firstname", "Walter").Set("lastname", "Smith")
}
Output is:
Full name: Walter <nil>
Full name: Walter Smith
Program exited.
You could improve it by making it concurrent and/or execute handlers in parallel for instance.
EDIT:
http://play.golang.org/p/msgaBXQwt_
I have made a more generic version of this to comply with your requirements of being lazy and cached:
package main
import (
"fmt"
)
type Getter func(string) interface{}
type Setter func(string, interface{})
type GetSetter interface {
Get(string) interface{}
Set(string, interface{}) GetSetter
RegisterGetter(string, Getter) GetSetter
RegisterSetter(string, Setter) GetSetter
}
type LazyGetSetter struct {
data map[string]interface{}
getters map[string]Getter
setters map[string]Setter
}
func NewLazyGetSetter() *LazyGetSetter {
return &LazyGetSetter{
make(map[string]interface{}),
make(map[string]Getter),
make(map[string]Setter),
}
}
func (l *LazyGetSetter) Get(name string) interface{} {
if getter, ok := l.getters[name]; ok {
return getter(name)
}
if val, ok := l.data[name]; ok {
return val
}
return nil
}
func (l *LazyGetSetter) Set(name string, value interface{}) *LazyGetSetter {
if setter, ok := l.setters[name]; ok {
setter(name, value)
} else {
l.data[name] = value
}
return l
}
func (l *LazyGetSetter) RegisterGetter(name string, getter Getter) *LazyGetSetter {
l.getters[name] = getter
return l
}
func (l *LazyGetSetter) RegisterSetter(name string, setter Setter) *LazyGetSetter {
l.setters[name] = setter
return l
}
type CachedLazyGetSetter struct {
*LazyGetSetter
cache map[string]interface{}
}
func NewCachedLazyGetSetter() *CachedLazyGetSetter {
return &CachedLazyGetSetter{
NewLazyGetSetter(),
make(map[string]interface{}),
}
}
func (c *CachedLazyGetSetter) Cache(name string, value interface{}) *CachedLazyGetSetter {
c.cache[name] = value
return c
}
func (c *CachedLazyGetSetter) FetchCache(name string) interface{} {
if val, ok := c.cache[name]; ok {
return val
}
return nil
}
func main() {
l := NewCachedLazyGetSetter()
l.RegisterGetter("fullname", func(name string) interface{} {
if cached := l.FetchCache(name); cached != nil {
return cached
}
f := fmt.Sprintf("%s %s", l.Get("firstname"), l.Get("lastname"))
l.Cache(name, f)
return f
})
l.Set("firstname", "Walter").Set("lastname", "Smith")
fmt.Println(l.Get("fullname"))
}
As to your comment: a map lookup will outperform reflection by orders of magnitude.
Cheers!