-
Notifications
You must be signed in to change notification settings - Fork 0
State
Access and mutation of stored Data
The abstract class State<T>
and the interfaces it inherits provide ways of accessing and mutating the internal Data value held by a state.
States are only allowed as members of Logic classes and parameteres to Logic classes constructors.
T is assumed to be Data (immutable with value semantics).
Concrete classes deriving from State<T>
define side effects for access/mutation such as thread safety via locking and journaling changes.
State is accessed in one of two ways:
1 Using the Val
method of the IStateVal
interface:
public interface IStateVal<T> {
T Val();
}
Usage:
var currentValue = SomeState.Val();
Val()
is used to read the state value. As T
is Data, the result is immutable and so it is not possible to mutate the state using Val()
result.
Side effects defined by concrete classes deriving from State<T>
(i.e locking) are invoked.
Receiving IStateVal
reference gives a function readonly access to the state.
2 Using the Val
method of the IStateRef
interface:
public delegate void ActionRef<T>(ref T r1);
public delegate RES FuncRef<T, RES>(ref T r1);
public interface IStateRef<T> {
T Val();
void Val(ActionRef<T> f);
TRES Val<TRES>(FuncRef<T, TRES> f);
}
Usage:
var currentValue = SomeState.Val();
SomeState.Val((ref SomeType data) => { /* do something with data including assigning to it; */ });
var x= SomeState.Val((ref SomeType data) => { /* do something with data including assigning to it and return some value; */ });
Val()
is used to mutate the state. It accepts a function that takes the state value as a ref. Assigning to the state value inside the lambda modifies the internal state. Side effects defined by concrete classes deriving from State<T>
(i.e locking) are invoked.
Receiving IStateRef
reference allows a function to mutate a state.
To create a new type of state derive a concrete class from State<T>
and implement the methods:
virtual protected void PreVal() { }
virtual protected void PostVal() { }
virtual protected object? PreRef(in T preState) => null; // result will be passed to PostRef
virtual protected void PostRef(in T preState, in T postState, object? PreData) { }
PreVal
and preRef
can do something before Val
and Ref
access the state - ie acquire a lock, or do some journaling.
PostVal
and postVal
can do something after Val
and Ref
access the state - ie release lock, or do some journaling.
Note that these are called even in the case of an exception thrown.
State<T>
also define an internal abstract class Combine<T2>
which is used by concrete classes to combine two states into one.
Three concrete implementation of State<T>
are provided as part of F:
LockedState
Acquire/release a lock before/after executing Ref
lambdas assuring they are thread save. Note that Val
is still thread safe (as T is a Data) but can become stale.
LockedState<T1,T2>
- combine two states into one, acquiring/releasing both locks:
LockedState<AddressData> Address = new(new AddressData("33 Victoria St"));
LockedState<int> Age = new(25);
LockedState<AddressData, int> Details = new(Address, Age); // combine locks
Details.Ref((ref AddressData address, ref Age age) => {
// both locks for Address and Age are now locked
address.Street = "st";
age = 56;
});
JournalingState
Same as LockedState
but also includes journaling state changes. As the state mutates a public Seq<T>
accumulate the history of previous values which can then be inspected or archived.
ObserverState
Allows registering events that get invoked before/after a mutation:
var EmployeesObserverState = new ObserverState<EmployeesMapData>(new());
EmployeesObserverState.PostRefEvent += (preVal, postVal) => Log($"EmployeesObserverState {preVal} => {postVal}");
EmployeesObserverState.Ref((ref EmployeesMapData map) => { map += ("Dave", dave); });
Lenses