具有延迟求值的订阅者

huangapple go评论107阅读模式
英文:

Subscriber with lazy evaluation

问题

我正在寻找一个在Go中订阅属性更改的解决方案。给定下面的结构体,我想实现一个派生属性,它订阅其源属性,只有在被读取时才重新评估自己。它会通过被通知或通过检查一个“脏标志”(通道?)来知道是否需要重新评估自己,如果其中一个源发生了更改。

我猜这类似于一个相当典型的情况。请参见下面的示例:

type Person struct {
    /FullName  string  // 从下面的两个属性派生而来:
    FirstName string  // 持久化
    LastName  string  // 持久化
}

这个概念也必须对于远程订阅/获取来说是“可见”的,例如一个User对象从底层的Person对象派生其详细用户信息:

type User struct {
    person *Person
    /FullName string // 从person.FullName派生而来
}

(好吧,人们的名字不经常改变,但这个例子必须简单)。

我自己对此的第一个想法是:

  1. 拉取 - 派生属性(FullName)应该是“惰性”的(只有在有人读取它们时才计算)。因此,当计算FullName字符串时,似乎最自然的是“拉取”任何订阅(“脏”标志/通知),也就是说,询问是否发生了任何更改。

  2. 缓存 - 当派生值已经计算出来后,将其存储在一个(隐藏的)字段(_fullName)中,以便在下次读取时可以重用该字符串,如果其订阅的值没有发生更改。

  3. 惰性订阅 - 不仅派生操作应该是“惰性”的,当有人读取FullName属性时,订阅本身也应该只在第一次评估时进行。

  4. 拉取而不是推送的好处似乎是,订阅的属性可能存在也可能不存在,当底层属性发生更改时。如果源没有“发送列表”,那么当最终订阅的属性/对象消失时,也没有必要“取消注册”。而且,在分布式场景中(User和Person在不同的机器上),只有在实际显式请求数据时才更新事物是更好的(这也适用于订阅,它只能在第一次读取FullName时进行)。

  5. 奢侈的事情是,如果一个goroutine(可选地)可以在CPU不太繁忙时更新(重新评估)FullName属性,而如果有人读取FullName属性,则会立即强制重新评估(这两个可以在一个解决方案中实现吗?)。

无论如何,下面是需要进行订阅的结构体(ASCII模型):

[Person]./FullName --> [Person].FirstName // 订阅1
                      [Person].LastName  // 订阅2

[User]./FullName --> [User].person./FullName // 订阅3

也就是说,总共有三个订阅来保持User.FullName属性的更新。(暂时忽略[User].person-link)。使用通道是否可以实现这样的功能,如果可以,那么如何实现?

下面是插入了隐藏字段的上述结构体(用于缓存派生结果,直到下次源属性变“脏”):

type Person struct {
    /FullName  string  // 派生
    _fullName string  // “缓存”
    FirstName string  
    LastName  string  
}

type User struct {
    person *Person
    /FullName  string  // 派生
    _fullName string  // “缓存”
}

编辑:Person-FullName属性可以通过以下方法提供(它可以稍后打包到类型化的属性对象(结构体)中):

func (p *Person) _FullName_DeriveAndSubscribe(Subscriber chan) string {
    if /* 检查通道是否“脏” */ {
        //
        // 保持一个内部通道,并获取通道,或者
        // Chan of Chan(?) 可以通知我们任何底层值的更改:
        //
        // _subscr = Subscriber 
        //
        // 现在,更新缓存
        _fullName = FirstName + " " + LastName
    }
    return _fullName   // 返回缓存的值
}
英文:

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,

  1. 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.

  2. 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.

  3. 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).

  4. 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).

  5. 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
}

答案1

得分: 4

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")
}

英文:

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!

答案2

得分: 0

只需将这些派生属性转换为函数,除非基准测试显示这是一个瓶颈。在这种情况下,您仍然可以插入隐藏(小写)字段来实现任何类型的缓存。

type Person struct {
    FirstName, LastName string
}

func (p *Person) FullName() string {
    return p.FirstName + " " + p.LastName
}
英文:

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
}

huangapple
  • 本文由 发表于 2013年6月19日 22:11:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/17193513.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定