change

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 20, 2023 License: AGPL-3.0 Imports: 1 Imported by: 5

README

change

Easy tracking of value changes in Go programs – ranging from simple to full-featured GoF observable

Go Reference Go Action

import "github.com/fractalqb/change"

A Story of change

This project was started for a very specific need: An event-driven program that collects events to to build and constantly refine the data of a database. When this was working well, I wanted a Web UI to have an easy look at some specific queries about the data. – No need to mention that the data has a reasonably rich domain model.

Then came the crucial step: I wanted—tired of pressing F5—the Web UI to update with the incoming events. That was the moment when I was looking for some observer-like solutions to put into my program. But as a hobby-project that runs on rather small hardware, I also wanted it to stay modest in its use of resources. No need to fire and instantaneously process events on each and every updated value. It would be sufficient to collect the information which entities changed during the processing of an event, and then only send the entities of the current Web view via WebSocket. – The thing that now is the chgv (“changing values”) sub-package was born. It has simply no memory overhead for the change-detectable values and, as you can see from the benchmarks, minimal time overhead compared to Go's builtin types. But the story is not yet complete…

The DB and its data grew, DB indexes were optimised read-caches1 took away read-load from the DB server and change detection also reduced DB writes. The program ran nearly imperceptible along others on the small machine. In theory the world could have been fine! … If it weren't for this pathological core entity in the domain model, which is important for processing almost all events.

  • This entity has a permanent external key which, unfortunately, is not part of all events. But the entity has to be created, event if that external key is not yet known.

  • No problem, I use an internal surrogate key. But how are subsequent events correlated to new instances. Luckily…

  • …the entity also has a name that is almost unique. Far more than 99.9% of all instances will have a unique name. A name which may change over time in very rare cases. – Not exactly a dream if you aim for deterministic processing of the data.

Don't get me wrong! I do not complain. This entity makes one of the more interesting challenges in the business logic of the project. But runtime statistics show that all three keys, the external key, the surrogate key and the name, are similarly important for lookup during event processing.

Lookup by Count Hit Rate
External ID 62436 33.9%
Name 55761 29.7%
Surrogate ID 20114 65.4%

Now, combine that with a cache where you can lookup the same memory object by either external key, surrogate key or name! It will turn out that one can make that work if you invalidate the by-name-lookup each time the name of an instance changes. This goes beyond the capabilities of the chgv package. And—I think I don't have to tell you—spreading the code with if name changed then invalidate by-name-lookup is a no-go.

OK, back to observer libraries. Many things I found for Go told me in their docs, that Go chan is an important part of their concept. But my problem is not about concurrency but only about GoF observer2. Invalidating the cache is fast enough, no need to do that concurrently. What I wanted, was something similar to JavaFX Properties and I didn't find it. So, rolled my own… Why not? 'Twas for my own fun.

But still I couldn't resist to think about efficiency. A value has to know all its observers. Even when there is no observer, this will introduce a constant amount of extra bytes to any single value.

  • A slice of observers will add 3 machine words. E.g. on a 64-bit machine this would blow up all register types, 1–8 byte, to the size of 32 byte. A string would end up with 40 byte.

  • A function pointer would add 1 extra machine word. Again, on a 64-bit machine this would blow up register types to 16 byte and string to 24 byte. That's better!

The problem with function pointers is that you cannot transparently resister/unregister several functions as observers. One only can set/unset/reset their pointers. So, which way to go? … Both! 'Twas for my own fun, you remember? The result is:

  • onchg: Implements the function pointer idea. The name resembles the call to the hook function “on change”.

  • obsv: Implements the observer pattern, still with only 1 machine word per-value memory overhead when not used as observable. The name is short for “observable value”.

For my original project it is still sufficient to use the onchg.String for the name attribute of the beloved pathological entity.

Benchmark

As the benchmark shows there is a significant runtime penalty for having the luxury of a full observable implementation. The benchmark measures the time to access an int value and then to set it again – either with change detection or without it, i.e. using the value as nothing more than being a value.

Noramlized against the reference of a bare Go int we have the factors:

Package Benchmark ~Factor
chgv BenchmarkIntReference-4 1
chgv BenchmarkInt_noDetect-4 6
chgv BenchmarkInt_withDetect-4 6
onchg BenchmarkInt_noDetect-4 8
onchg BenchmarkInt_withDetect-4 23
obsv BenchmarkInt_noDetect-4 10
obsv BenchmarkInt_withDetect-4 381
obsv BenchmarkInt_withObserver-4 441

Here's the benchmark output:

goos: linux
goarch: amd64
pkg: github.com/fractalqb/change/chgv
cpu: Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz
BenchmarkIntReference-4     	1000000000	         0.3374 ns/op
BenchmarkInt_noDetect-4     	588945325	         2.023 ns/op
BenchmarkInt_withDetect-4   	587903571	         2.020 ns/op
PASS
ok  	github.com/fractalqb/change/chgv	3.197s
goos: linux
goarch: amd64
pkg: github.com/fractalqb/change/onchg
cpu: Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz
BenchmarkInt_noDetect-4     	439199455	         2.703 ns/op
BenchmarkInt_withDetect-4   	156135638	         7.592 ns/op
PASS
ok  	github.com/fractalqb/change/onchg	3.431s
goos: linux
goarch: amd64
pkg: github.com/fractalqb/change/obsv
cpu: Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz
BenchmarkInt_noDetect-4       	341003700	         3.374 ns/op
BenchmarkInt_withDetect-4     	 8994292	       128.7 ns/op
BenchmarkInt_withObserver-4   	 8412120	       148.7 ns/op
PASS
ok  	github.com/fractalqb/change/obsv	4.412s

Footnotes

  1. Yes, that caches kill horizontal scalability. But, remember, this thing has to run with minimal resources, not on an elastic cluster.

  2. Having a good GoF Observer lib would make it easy to create wrappers around the receiving or the sending end of a chan to make them Observers or Observable. The other way around is not that simple. To me, a clear case of abstraction inversion.

Documentation

Overview

Package change helps to keep track of changed values in Go programs. It has three different concepts to do so that vary in memory overhead, time overhead and features: The simple Val, the Do with a single callback hook and the full-featured observable Obs. All implement a common interface Changeable and are implemented as generics.

Compare the following plain-Go example with the examples from Val, On and Obs:

Example
user := struct {
	Name   string
	Logins int
}{
	Name:   "John Doe",
	Logins: 0,
}
// Tedious with plain Go, simpler with “change”:
var chg uint64
if user.Name != "John Doe" {
	user.Name = "John Doe"
	chg |= 1 // = (1<<0) = 0b01
}
if user.Logins != 1 {
	user.Logins = 1 // = (1<<1) = 0b10
	chg |= 2
}

fmt.Printf("Changes: 0b%b\n", chg)
if chg&1 == 0 {
	fmt.Println("Name did not change")
} else {
	fmt.Println("Name changed")
}
Output:

Changes: 0b10
Name did not change

Index

Examples

Constants

View Source
const AllFlags = ^Flags(0)

AllFlags is a Flags set with all flags set – just to have said that.

Variables

This section is empty.

Functions

This section is empty.

Types

type Changeable added in v0.4.0

type Changeable[T comparable] interface {
	Get() T
	Set(v T, chg Flags) Flags
}

type Changed added in v0.4.0

type Changed[T comparable] struct {
	// contains filtered or unexported fields
}

func (Changed) Chg added in v0.4.0

func (e Changed) Chg() Flags

func (Changed[T]) NewValue added in v0.4.0

func (ce Changed[T]) NewValue() any

Implement ValueChange

func (Changed[T]) OldValue added in v0.4.0

func (ce Changed[T]) OldValue() any

Implement ValueChange

func (Changed) Source added in v0.4.0

func (e Changed) Source() Observable

type Event added in v0.4.0

type Event interface {
	// Source returns the Observable that issued the Event.
	Source() Observable
	// Chg returns the change Flags for the state change.
	Chg() Flags
}

Event is the base interface for all state change events.

type FlagHook added in v0.4.0

type FlagHook[_ comparable] Flags

FlagHook's Flag method always returns the same Flags value.

func (FlagHook[T]) Func added in v0.4.0

func (h FlagHook[T]) Func(src *On[T], oldval, newval T, check bool) Flags

Flag is a HookFunc for On values.

type Flags

type Flags uint64

Flags is used by all Set methods to return to the caller if a Set operation did change a value. Generally Flags==0 means nothing changed. The flags from different Set calls can efficiently be combined with the '|' binary or operator. However there is room for no more than 64 different flags.

The specific flags for a call to Set are generally provided by the caller. It depends on the sub package what it means to pass 0 flags to a Set method. E.g. there may be defaults that kick in if one passes 0 flags to the actual call.

func (Flags) All

func (c Flags) All(bits Flags) bool

All tests if all bits are set in c.

func (Flags) Any

func (c Flags) Any(bits Flags) bool

Any tests if any of bits is set in c.

func (Flags) Map

func (c Flags) Map(bits ...Flags) (res Flags)

Map remaps bits of c to a new combination of bits.

If the number of bits elements is odd then the last element is added to res if c is not zero. The even number of leading elements is treated as a list of pairs. The first element of a pair is checked to have any bits common with c. If so, the second element of the pair is added to res.

Example
fmt.Println(Flags(0xf0).Map(4))          // 0xf0 != 0 → 4
fmt.Println(Flags(0xf0).Map(0x0f, 2, 1)) // 0xf0 & 0x0f == 0 → 0 and 0x0f != 0 → 1 ⇒ 0|1 = 1
fmt.Println(Flags(0x18).Map(0x0f, 2, 1)) // 0x18 & 0x0f != 0 → 2 and 0x0f != 0 → 1 ⇒ 2|1 = 3
fmt.Println(Flags(0x18).Map(0x0f, 2))    // 0x18 & 0x0f != 0 → 2
Output:

4
1
3
2

type HookFunc added in v0.4.0

type HookFunc[T comparable] func(src *On[T], oldval, newval T, check bool) Flags

HookFunc functions can be hooked into On values. They get passed the On object src for which the Set method was called, the old value odlval and the to be set value newval and the check flag. See also the description of On.

type Obs added in v0.4.0

type Obs[T comparable] struct {
	ObservableBase
	// contains filtered or unexported fields
}

Obs implements Changeable as a full featured Observable.

Example (String)
str := NewObs("", "example string", 4711)
str.ObsRegister(0, nil, UpdateFunc(func(tag any, e Event) {
	chg := e.(Changed[string])
	fmt.Printf("Tag: %+v\n", tag)
	fmt.Printf("Event: '%s'→'%s': %d\n",
		chg.OldValue(),
		chg.NewValue(),
		e.Chg())
}))
fmt.Println(str.Set("Observe this change!", 0), str.ObsLastVeto())
Output:

Tag: example string
Event: ''→'Observe this change!': 4711
4711 <nil>

func NewObs added in v0.4.0

func NewObs[T comparable](init T, defaultTag any, defaultChg Flags, os ...Observer) Obs[T]

func (Obs[T]) Get added in v0.4.0

func (ov Obs[T]) Get() T

func (*Obs[T]) Set added in v0.4.0

func (ov *Obs[T]) Set(v T, chg Flags) Flags

type Observable added in v0.4.0

type Observable interface {
	// ObsDefaults returns the default tag and change Flags of the observable.
	ObsDefaults() (tag any, chg Flags)
	// SetObsDefaults sets the default tag and change Flags of the observable.
	SetObsDefaults(tag any, chg Flags)
	// ObsRegister registres a new Observer with this Observable. If tag is not
	// nil, the Observer will be notified with this specific tag, not the
	// default tag.
	ObsRegister(prio int, tag any, o Observer)
	// ObsUnregister removes the Observer o from the observable if it is
	// registered. Otherwise ObsUnregister odes nothing.
	ObsUnregister(o Observer)
	// ObsLastVeto returns the Veto from the last Set call, if any.
	ObsLastVeto() *Veto
	// ObsEach calls do for all registered Observers in noitification order
	// with the same tag that would be used for notifications.
	ObsEach(do func(tag any, o Observer))
}

Observable objects will notify registered Observers about state changes. Observables use tags to let Observers easily distinguish the different subject they observe. In addition to change Events, Observables also use change Flags to descibe chages – much like Val and On values. Observers are notified in order of their registered priority, highest first. Observers with the same priority are notified in order of their registration.

type ObservableBase added in v0.4.0

type ObservableBase struct {
	// contains filtered or unexported fields
}

func (ObservableBase) ObsDefaults added in v0.4.0

func (b ObservableBase) ObsDefaults() (tag any, chg Flags)

func (*ObservableBase) ObsEach added in v0.4.0

func (b *ObservableBase) ObsEach(do func(tag any, o Observer))

func (*ObservableBase) ObsLastVeto added in v0.4.0

func (b *ObservableBase) ObsLastVeto() *Veto

func (*ObservableBase) ObsRegister added in v0.4.0

func (b *ObservableBase) ObsRegister(prio int, tag any, o Observer)

func (*ObservableBase) ObsUnregister added in v0.4.0

func (b *ObservableBase) ObsUnregister(o Observer)

func (*ObservableBase) SetObsDefaults added in v0.4.0

func (b *ObservableBase) SetObsDefaults(tag any, chg Flags)

type Observer added in v0.4.0

type Observer interface {
	// Check lets the observer inspect the hypothetical change Event e
	// before it is executed.  By returning a non-nil veto the
	// observer can block the change.  Check also can override the
	// default change Flags used for the Set operation by returning
	// chg != 0.
	Check(tag any, e Event) *Veto
	// Update notifies the observer about a change Event e.
	Update(tag any, e Event)
}

Observers can be registered with Observables to be notified about state changes of the Observable. An Observer must be comparable to make Observable.ObsRegister() and Observable.ObsUnregister() work.

func UpdateFunc added in v0.4.0

func UpdateFunc(f func(any, Event)) Observer

Use UpdateFunc to wrap an update function so that it can be used as an Observer. Note that the pointer to the UpdateFunc object will be used to implement equality. This is because Go functions are not comparable.

type On added in v0.4.0

type On[T comparable] struct {
	// contains filtered or unexported fields
}

On implements Changeable with a hook that is called on value changes. When the Set method is called, first the hook is called before a change is made with check=true. With check==true the hook decides if it blocks the set operation. To block the set operation, the hook returns 0. Otherwise the hook provides the change flags for the Set method. The flags from the hook are overridden if non-zero flags are passed to the value's Set method. If the hook is nil, On behave exactly like Val.

Example
package main

import "fmt"

var _ Changeable[int] = (*On[int])(nil)

func main() {
	user := struct {
		Name   On[string]
		Logins On[int]
	}{
		Name: NewOn("John Doe", FlagHook[string](1).Func),
		Logins: NewOn(0, func(_ *On[int], o, n int, check bool) Flags {
			if !check {
				fmt.Printf("changed logins from %d to %d\n", o, n)
			}
			return 2
		}),
	}

	chg := user.Name.Set("John Doe", 0)
	chg |= user.Logins.Set(1, 0)

	fmt.Printf("Changes: 0b%b\n", chg)
	if chg&1 == 0 {
		fmt.Println("Name did not change")
	} else {
		fmt.Println("Name changed")
	}
}
Output:

changed logins from 0 to 1
Changes: 0b10
Name did not change

func NewOn added in v0.4.0

func NewOn[T comparable](init T, hook HookFunc[T]) On[T]

func (On[T]) Get added in v0.4.0

func (cv On[T]) Get() T

func (*On[T]) Reset added in v0.4.0

func (cv *On[T]) Reset(v T, hook HookFunc[T]) (T, HookFunc[T])

func (*On[T]) Set added in v0.4.0

func (cv *On[T]) Set(v T, chg Flags) Flags

type Val added in v0.4.0

type Val[T comparable] struct {
	// contains filtered or unexported fields
}

Val is a Changeable that tracks changes with a simple set of Flags. The flag(s) passed to a Set method call are returned if the underlying value changed. Otherwise the passed value is not assigned and 0 is returned. Note that if one passes flag=0 to Set the returned value will always be 0, which makes it rather uninteresting. However this will not affect the actual value change.

While these changeable values are rather bare bones they come without memory overhead – unlike most observable libraries.

Example
user := struct {
	Name   Val[string]
	Logins Val[int]
}{
	Name:   NewVal("John Doe"),
	Logins: NewVal(0),
}
chg := user.Name.Set("John Doe", 1) // 1 = (1<<0) = 0b01
chg |= user.Logins.Set(1, 2)        // 2 = (1<<1) = 0b10

fmt.Printf("Changes: 0b%b\n", chg)
if chg&1 == 0 {
	fmt.Println("Name did not change")
} else {
	fmt.Println("Name changed")
}
Output:

Changes: 0b10
Name did not change

func NewVal added in v0.4.0

func NewVal[T comparable](init T) Val[T]

func (Val[T]) Get added in v0.4.0

func (cv Val[T]) Get() T

Get returns the current value.

func (*Val[T]) Set added in v0.4.0

func (cv *Val[T]) Set(v T, chg Flags) Flags

Set checks whether v is equal to the current value and returns 0 if it matches. Otherwise v is set as cv's value and chg is returned.

type ValueChange added in v0.4.0

type ValueChange interface {
	Event
	// OldValue returns the value befor the change.
	OldValue() any
	// NewValue returns the current value, i.e. after the change.
	NewValue() any
}

type Veto added in v0.4.0

type Veto struct {
	// contains filtered or unexported fields
}

Veto keeps the information why a Set operation was stopped by which Obersver. The Veto can be requested from each Observable until the next call to its Set method.

Veto also implements an unwrappable Go error.

func (*Veto) Error added in v0.4.0

func (v *Veto) Error() string

func (*Veto) StoppedBy added in v0.4.0

func (v *Veto) StoppedBy() Observer

StoppedBy returns the Observer that stopped the Set operation with its Veto.

func (*Veto) Unwrap added in v0.4.0

func (v *Veto) Unwrap() error

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL