Skip to the content.

Part 2: Computed Values and Computed<T>

Video covering this part:

Fusion’s Computed<T> is a fairly unique abstraction describing the result of a computation. Here is how it compares with computed observables from Knockout.js and MobX:

We’re going to use the same CounterService and CreateServices helper as in Part 1:

``` cs –editable false –region Part02_CounterService –source-file Part02.cs public class CounterService : IComputeService { private readonly ConcurrentDictionary<string, int> _counters = new ConcurrentDictionary<string, int>();

[ComputeMethod]
public virtual async Task<int> Get(string key)
{
    WriteLine($"{nameof(Get)}({key})");
    return _counters.TryGetValue(key, out var value) ? value : 0;
}

public void Increment(string key)
{
    WriteLine($"{nameof(Increment)}({key})");
    _counters.AddOrUpdate(key, k => 1, (k, v) => v + 1);
    using (Computed.Invalidate())
        _ = Get(key);
} }

public static IServiceProvider CreateServices() { var services = new ServiceCollection(); var fusion = services.AddFusion(); fusion.AddService(); return services.BuildServiceProvider(); }


First, let's try to "pull" `Computed<T>` instance created behind the
scenes for a given call:

``` cs --region Part02_CaptureComputed --source-file Part02.cs
var counters = CreateServices().GetRequiredService<CounterService>();
var computed = await Computed.Capture(() => counters.Get("a"));
WriteLine($"Computed: {computed}");
WriteLine($"- IsConsistent(): {computed.IsConsistent()}");
WriteLine($"- Value:          {computed.Value}");

The output:

Get(a)
Computed: Computed`1(Intercepted:CounterService.Get(a) @xIs0saqEU, State: Consistent)
- IsConsistent(): True
- Value:          0

As you may notice, Computed<T> stores:

Overall, its key properties include:

Computed<T> implements a set of interfaces — most notably,

And finally, there are a few important methods:

A bit of code to help remembering this:

interface IResult<T> {
  T ValueOrDefault { get; } // Never throws an error
  T Value { get; } // Throws Error when HasError
  Exception? Error { get; }
  bool HasValue { get; } // Error == null
  bool HasError { get; } // Error != null

  void Deconstruct(out T value, out Exception? error);
  bool IsValue(out T value);
  bool IsValue(out T value, out Exception error);
  
  Result<T> AsResult();  // Result<T> is a struct implementing IResult<T>
  Result<TOther> Cast<TOther>();
}

// CancellationToken argument is removed everywhere for simplicity
interface Computed<T> : IResult<T> {
  ConsistencyState ConsistencyState { get; } 
  
  event Action Invalidated; // Event, triggered on the invalidation
  Task WhenInvalidated(); // Async way to await for the invalidation
  void Invalidate();
  Task<Computed<T>> Update(); // Notice it returns a new instance!
  Task<T> Use();
}

A diagram showing how ConsistencyState transition works:

Since every Computed<T> is almost immutable, a new instance of IComputed gets created on recomputation if the most recent one is already invalidated at this point (otherwise there is no reason to recompute). Here is how 3 computations (of the same value) look on a Gantt chart:

An ugly visualization showing how multiple Computed<T> instances get invalidated and eventually replaced with their consistent versions:

Earlier Computed<T>.Update(...) was called Renew, so as you might guess, the animation was made before this rename :)

Ok, let’s get back to code and see how invalidation really works:

``` cs –region Part02_InvalidateComputed1 –source-file Part02.cs var counters = CreateServices().GetRequiredService(); var computed = await Computed.Capture(() => counters.Get("a")); WriteLine($"computed: {computed}"); WriteLine("computed.Invalidate()"); computed.Invalidate(); WriteLine($"computed: {computed}"); var newComputed = await computed.Update(); WriteLine($"newComputed: {newComputed}");


The output:

```text
Get(a)
computed: Computed`1(Intercepted:CounterService.Get(a) @1EhL08uaNN, State: Consistent)
computed.Invalidate()
computed: Computed`1(Intercepted:CounterService.Get(a) @1EhL08uaNN, State: Invalidated)
Get(a)
newComputed: Computed`1(Intercepted:CounterService.Get(a) @1EhL08uaPR, State: Consistent)

Compare the above code with this one:

``` cs –region Part02_InvalidateComputed2 –source-file Part02.cs var counters = CreateServices().GetRequiredService(); var computed = await Computed.Capture(() => counters.Get("a")); WriteLine($"computed: {computed}"); WriteLine("using (Computed.Invalidate()) counters.Get(\"a\"))"); using (Computed.Invalidate()) // <- This line _ = counters.Get("a"); WriteLine($"computed: {computed}"); var newComputed = await Computed.Capture(() => counters.Get("a")); // <- This line WriteLine($"newComputed: {newComputed}");


The output:

```text
Get(a)
computed: Computed`1(Intercepted:CounterService.Get(a) @R0oNKnVbo, State: Consistent)
using Computed.Invalidate(() => counters.Get("a"))
computed: Computed`1(Intercepted:CounterService.Get(a) @R0oNKnVbo, State: Invalidated)
Get(a)
newComputed: Computed`1(Intercepted:CounterService.Get(a) @R0oNKnVds, State: Consistent)

The output is ~ identical. As you might guess,

And finally, let’s see how you can “observe” the invalidation to trigger the update:

``` cs –region Part02_IncrementCounter –source-file Part02.cs var counters = CreateServices().GetRequiredService();

_ = Task.Run(async () => { for (var i = 0; i <= 5; i++) { await Task.Delay(1000); counters.Increment(“a”); } });

var computed = await Computed.Capture(() => counters.Get(“a”)); WriteLine($”{DateTime.Now}: {computed.Value}”); for (var i = 0; i < 5; i++) { await computed.WhenInvalidated(); computed = await computed.Update(); WriteLine($”{DateTime.Now}: {computed.Value}”); }


The output:

```text
Get(a)
9/1/2020 5:08:54 PM: 0
Increment(a)
Get(a)
9/1/2020 5:08:55 PM: 1
Increment(a)
Get(a)
9/1/2020 5:08:56 PM: 2
Increment(a)
Get(a)
9/1/2020 5:08:57 PM: 3
Increment(a)
Get(a)
9/1/2020 5:08:58 PM: 4
Increment(a)
Get(a)
9/1/2020 5:08:59 PM: 5

So even though Fusion doesn’t update anything automatically, achieving exactly the same behavior with it is pretty straightforward.

A good question to ask is: but why it doesn’t, if literally everyone else does? E.g. MobX even tries to re-compute all the derivations atomically — so why Fusion does literally the opposite?

The answer is:

What’s totally possible though is to notify the code using certain computed value that it became obsolete and thus has to be recomputed. And this notification — plus maybe some other “knowns” about the domain would let the code to determine the right strategy for triggering the recomputation.

Think of these two cases:

Such an approach allows you to have nearly instant updates in most cases, but selectively (i.e. per piece of content + user) throttle the update rate (without affecting user-induced updates) in cases when instant updates create performance problems.

It worth mentioning that Fusion offers all the abstractions you need to have this behavior, and moreover, similarly to almost-invisible Computed<T>, you normally don’t even need to know these abstractions exist. But they’ll be ready to help you once you conclude you need throttling.

Next: Part 3 » | Tutorial Home