There are two types of signals. Read/write signals can be assigned values. Here we create one called counter which we initially a assign a value of 4
var counter = Signal.State(4);
this can be changed to a five etc using Set
.
counter.Set(5);
and read back using Get
Console.WriteLine(counter.Get()); // prints 5
The second type of signals are computed ones, these are based on other signals
var isEven = Signal.Computed(() => counter.Get() % 2 == 0 );
Console.WriteLine(isEven.Get()); // prints false as counter is 5
counter.Set(6);
Console.WriteLine(isEven.Get()); // prints true as counter is 6
Computed signals can also be based on other computed signals
var parity = Signal.Computed(() => isEven.Get() ? "Even" : "Odd";);
Console.WriteLine(parity.Get()); // prints Even as isEven is true`
counter.Set(9);
Console.WriteLine(parity.Get()); // prints odd as isEven is now false
or on multiple signals
var firstname = Signal.State("David");
var lastname = Signal.State("Betteridge");
var fullname = Signal.Computed(() => $"{firstname.Get()} {lastname.Get()}");
Effects can be added to signals which are automatically triggered when a value changes
counter.AddEffect((previous, current) => Console.WriteLine($"Counter changed {previous} to {current}"));
isEven.AddEffect((previous, current) => Console.WriteLine($"IsEven changed {previous} to {current}"));
parity.AddEffect((previous, current) => Console.WriteLine($"Parity changed {previous} to {current}"));
counter.Set(10);
// Counter changed 9 to 10
// IsEven changed False to True
// Parity changed Odd to Even
counter.Set(12);
// Counter changed 10 to 12
Effects can also be removed from a signal.
var effect = isEven.AddEffect((p, c) => { ... }));
isEven.RemoveEffect(effect);
In the previous example not only aren't the other two effects triggered, but parity isn't even computed.
var parity = SignalBuilder.DependsOn(isEven).ComputedBy(v =>
{
Console.WriteLine("Compute parity");
return v.Get() ? "Even" : "Odd";
});
// This changes the value for isEven so parity is computed
counter.Set(13);
Console.WriteLine(parity.Get());
// This doesn't change the value for isEven so parity isn't computed
counter.Set(15);
Console.WriteLine(parity.Get());
So far we have only looked at a signals based on a primitive type (integer). In the real world we need to cater for classes/records/arrays/lists etc. For example:
record People(string Name, int Age);
var data = Signal.State<List<People>>([new People("David", 48)]);
var adults = Signal.Computed(() => data.Get().Where(p => p.Age > 18).ToList());
var numberOfAdults = Signal.Computed(() => adults.Get().Count );
numberOfAdults.AddEffect((oldCount, newCount) =>
Console.WriteLine($"Number of adults changed from {oldCount} to {newCount}"));
The numberOfAdults signal only needs to be computed if adults changes.
In order to get this to work, we have to supply your own Equality function. This is provided using a second (optional) argument to the Signal.State and Signal.Computed methods.
var adults = Signal.Computed(() => data.Get().Where(p => p.Age > 18).ToList(),
CompareOrderedLists);
bool CompareOrderedLists<T>(IList<T> lhs, IList<T> rhs) where T : notnull
{
// T must be a record type as they generate the EqualityContract property
// There is no proper way to enforce this in C# however.
if (lhs.Count != rhs.Count)
return false;
for (var i = 0; i < lhs.Count; i++)
{
if (!lhs[i].Equals(rhs[i]))
return false;
}
return true;
}