Change events for AtomHashMap and the STM system (via Ref)
Pre-releaseAs requested by @CK-LinoPro in this Issue. AtomHashMap
and Ref
now have Change
events.
Change<A>
Change<A>
is a new union-type that represents change to a value, and is used by AtomHashMap
and TrackingHashMap
.
You can pattern-match on the Change
value to find out what happened:
public string WhatHappened(Change<A> change) =>
change switch
{
EntryRemoved<A> (var oldValue) => $"Value removed: {oldValue}",
EntryAdded<A> (var value) => $"Value added: {value}",
EntryMapped<A, A>(var from, var to) => $"Value mapped from: {from}, to: {to}",
_ => "No change"
};
EntryMapped<A, B>
is derived from EntryMappedFrom<A>
and EntryMappedTo<B>
, so for any A -> B
mapping change, you can just match on the destination value:
public string WhatHappened(Change<A> change) =>
change switch
{
EntryRemoved<A> (var oldValue) => $"Value removed: {oldValue}",
EntryAdded<A> (var value) => $"Value added: {value}",
EntryMappedTo<A>(var to) => $"Value mapped from: something, to: {to}",
_ => "No change"
};
That avoids jumping through type-level hoops to see any changes!
There are also various 'helper' properties and methods for working with the derived types:
Member | Description |
---|---|
HasNoChange |
true if the derived-type is a NoChange<A> |
HasChanged |
true if the derived-type is one-of EntryRemoved<A> or EntryAdded<A> or EntryMapped<_, A> |
HasAdded |
true if the derived-type is a EntryAdded<A> |
HasRemoved |
true if the derived-type is a EntryRemoved<A> |
HasMapped |
true if the derived-type is a EntryMapped<A, A> |
HasMappedFrom<FROM>() |
true if the derived-type is a EntryMappedFrom<FROM> |
ToOption() |
Gives the latest value from the Change , as long as the Change is one-of EntryAdded or EntryMapped or EntryMappedTo |
There are also constructor functions to build your own Change
values.
AtomHashMap<K, V>
and AtomHashMap<EqK, K, V>
The two variants of AtomHashMap
both now have Change
events that can be subscribed to. They emit a HashMapPatch<K, V>
value, which contains three fields:
Field | Description |
---|---|
From |
HashMap<K, V> that is the state before the change |
To |
HashMap<K, V> that is the state after the change |
Changes |
HashMap<K, Change<V>> that describes the changes to each key |
There are three related Rx.NET
extensions in the LanguageExt.Rx
package:
AtomHashMap Extension |
Description |
---|---|
OnChange() |
Observable stream of HashMapPatch<K, V> |
OnMapChange() |
Observable stream of HashMap<K, V> , which represents the latest snapshot of the AtomHashMap |
OnEntryChange() |
Observable stream of (K, Change<V>) , which represents the change to any key within the AtomHashMap |
Example
var xs = AtomHashMap<string, int>();
xs.OnEntryChange().Subscribe(pair => Console.WriteLine(pair));
xs.Add("Hello", 456);
xs.SetItem("Hello", 123);
xs.Remove("Hello");
xs.Remove("Hello");
Running the code above yields:
(Hello, +456)
(Hello, 456 -> 123)
(Hello, -123)
Swap
method (potential) breaking-change
The implementation of Swap
has changed. It now expects a Func<TrackingHashMap<K, V>, TrackingHashMap<K, V>>
delegate instead of a Func<HashMap<K, V>, HashMap<K, V>>
delegate. This is so the Swap
method can keep track of arbitrary changes during the invocation of the delegate, and then emit them as events after successfully committing the result.
Ref<A>
Refs are used with the atomic(() => ...)
, snapshot(() => ...)
, and serial(() => ...)
STM transactions. Their changes are now tracked during a transaction, and are then (if the transaction is successful) emitted on the Ref<A>.Change
event. These simply publish the latest value.
As before there's Rx
extensions for this:
Ref Extension |
Description |
---|---|
OnChange() |
provides an observable stream of values |
Example
var rx = Ref("Hello");
var ry = Ref("World");
Observable.Merge(rx.OnChange(),
ry.OnChange())
.Subscribe(v => Console.WriteLine(v));
atomic(() =>
{
swap(rx, x => $"1. {x}");
swap(ry, y => $"2. {y}");
});
This outputs:
1. Hello
2. World
TrackingHashMap
This is a new immutable data-structure which is mostly a clone of HashMap
; but one that allows for changes to be tracked. This is completely standalone, and not related to the AtomHashMap
in any way other than it's used by the AtomHashMap.Swap
method. And so, this has use-cases of its own.
Changes are tracked as a HashMap<K, Change<V>>
. That means there's at most one change-value stored per-key. So, there's no risk of an ever expanding log of changes, because there is no log! The changes that are tracked are from the 'last snapshot point'. Which is from the empty-map or from the last state where trackingHashMap.Snapshot()
is called.
Example 1
var thm = TrackingHashMap<int, string>();
Console.WriteLine(thm.Changes);
thm = thm.Add(100, "Hello");
thm = thm.SetItem(100, "World");
Console.WriteLine(thm.Changes);
This will output:
[]
[(100: +World)]
Note the +
, this indicates an Change.EntryAdded
. And so there has been a single 'add' of the key-value pair (100, World)
. The "Hello"
value is ignored, because from the point of the snapshot: a value has been added. That's all we care about.
If I take a snapshot halfway through, you can see how this changes the output:
var thm = TrackingHashMap<int, string>();
thm = thm.Add(100, "World");
thm = thm.Snapshot();
thm = thm.SetItem(100, "Hello");
Console.WriteLine(thm.Changes);
This outputs:
[(100: World -> Hello)]
So the snapshot is from when there was a (100, World)
pair in the map.
Hopefully that gives an indication of how this works!