This is the twelfth part of the Async Wandering series. For your convenience you can find other parts in the table of contents in Part 1 – Why creating Form from WinForms in unit tests breaks async?
Today we are going to color our functions in different way.
Last time we saw how to return values from each function using monads. However, all we need is just an ability to run the code in some context if we expect that code may be asynchronous. If we know it’s going to be synchronous then there is no reason to go through any monads. Instead of reverse-colouring functions, we may use generics to propagate the context and call it as needed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
public abstract class Builder { public abstract Monad Build(); } public class IdBuilder : Builder { public override Monad Build() { return new Id(); } } public class AsyncBuilder : Builder { public override Monad Build() { return new Async(); } } public interface Monad { U Map(T value, Func lambda); void Complete(object t); } public class Id : Monad { private object t; public U Map(T value, Func lambda) { this.t = value; lock (this) { while (t == null) { Monitor.Wait(this); } } return lambda((T)this.t); } public void Complete(object t) { lock (this) { this.t = t; Monitor.PulseAll(this); } } } public class Async : Monad { private object t; private int current; public U Map(T value, Func lambda) { this.t = value; if (t == null) { this.current = HKTMonadFiberAsync.current; byte b; HKTMonadFiberAsync.readyToGo.TryRemove(this.current, out b); HKTMonadFiberAsync.helper.Switch(0); } return lambda((T)this.t); } public void Complete(object t) { this.t = t; HKTMonadFiberAsync.readyToGo.TryAdd(this.current, 0); } } |
Super similar to the code from the last part. However, this time we don’t hold the value in the monad, we pass it as a parameter and run it through the context.
How do we use it? This way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
private static void RunInternal() { WhereAmI("Before nesting"); RunInternalNested<AsyncBuilder>(); WhereAmI("After nesting"); } private static void RunInternalNested() where T: Builder, new() { WhereAmI("Before creating delay"); Delay<T>(2000); WhereAmI("After sleeping"); var data = Data<T>("Some string"); WhereAmI($"After creating data {data}"); } private static void Delay(int timeout) where T : Builder, new() { var context = new T().Build(); var timer = new Timer(_ => context.Complete(new object()), null, timeout, Timeout.Infinite); GC.KeepAlive(timer); context.Map((object)null, _ => timeout); } private static U Data(U d) where T: Builder, new() { var context = new T().Build(); return context.Map(d, _ => d); } |
notice that call to Delay
passes the generic parameter indicating the context. We can also wrap any value through the context, just like Task.FromResult
if needed. And the output is as expected:
1 2 3 4 5 6 7 8 |
Thread 1 Time 8/12/2020 5:16:21 PM: Start - HKTMonadFiberAsync Thread 1 Time 8/12/2020 5:16:21 PM: Before nesting Thread 1 Time 8/12/2020 5:16:21 PM: Before creating delay Thread 1 Time 8/12/2020 5:16:21 PM: Side job Thread 1 Time 8/12/2020 5:16:23 PM: After sleeping Thread 1 Time 8/12/2020 5:16:23 PM: After creating data Some string Thread 1 Time 8/12/2020 5:16:23 PM: After nesting Thread 1 Time 8/12/2020 5:16:23 PM: End - HKTMonadFiberAsync |
See that the side job was executed when we were sleeping. But if we change line 5 to RunInternalNested< IdBuilder>();
, we get this:
1 2 3 4 5 6 7 8 |
Thread 1 Time 8/12/2020 5:17:10 PM: Start - HKTMonadFiberAsync Thread 1 Time 8/12/2020 5:17:10 PM: Before nesting Thread 1 Time 8/12/2020 5:17:10 PM: Before creating delay Thread 1 Time 8/12/2020 5:17:12 PM: After sleeping Thread 1 Time 8/12/2020 5:17:12 PM: After creating data Some string Thread 1 Time 8/12/2020 5:17:12 PM: After nesting Thread 1 Time 8/12/2020 5:17:12 PM: Side job Thread 1 Time 8/12/2020 5:17:12 PM: End - HKTMonadFiberAsync |
So the side job is executed after the main one finishes which is a synchronous execution.
This way we have no colors, no static state, just a generic parameter which could be optimized by the compiler. We can go even further and get rid of boxing:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
public abstract class Builder { public abstract Monad Build(); } public class IdBuilder : Builder { public override Monad Build() { return new Id(); } } public class AsyncBuilder : Builder { public override Monad Build() { return new Async(); } } public interface Monad { U Map<U>(T value, Func lambda); void Complete(T t); } public class Id : Monad { private T t; public U Map<U>(T value, Func lambda) { this.t = value; lock (this) { while (t == null) { Monitor.Wait(this); } } return lambda(this.t); } public void Complete(T t) { lock (this) { this.t = t; Monitor.PulseAll(this); } } } public class Async : Monad { private T t; private int current; public U Map<U>(T value, Func lambda) { this.t = value; if (t == null) { this.current = HKTMonadFiberAsync.current; byte b; HKTMonadFiberAsync.readyToGo.TryRemove(this.current, out b); HKTMonadFiberAsync.helper.Switch(0); } return lambda(this.t); } public void Complete(T t) { this.t = t; HKTMonadFiberAsync.readyToGo.TryAdd(this.current, 0); } } |
Bonus points for getting sort of Higher Kinded Type in C# without doing the Brand transformation.