Skip to content
Kofifus edited this page May 11, 2024 · 20 revisions

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

Clone this wiki locally