-
Notifications
You must be signed in to change notification settings - Fork 14
Description
Hey folks, love the library - love that you've figured out how to do effect programming in C# with async.
While I obviously don't need to explain the benefit of effects to ya'll, I'm struggling to reconcile those advantages with the advantages of type-safety that you might get without the extra indirection. For example, if you were to use a–welp–abstract class and had virtual methods as hooks to run some "effects".
I know there are runtime checks if effects aren't satisfied, but compile time checks are a lot safer, and give faster feedback if something's missing.
The main thing I'm yearning for is:
Guaranteeing that the handler used to run an effect handles all the effects used in an
Effmethod
If this isn't feasible, then:
Guaranteeing that the types of different effect handlers designed to handle the same effects maintain the same contract. Then, at least, you don't need to keep multiple effect handlers in sync with one effects contract, and you'll get notified to amend the handlers if the contracts are updated.
I've got no idea if the former is even possible in C#. I don't know what the limitations are of custom async state machines are, and also I suspect one would eventually run up against .NET not having discriminated unions as a barrier for getting something truly type-safe.
In lieu of that, I've pushed an example up of what I'm using for the latter. I am a total noob though, and it would be amazing if someone could tell me if I'm doing something horribly wrong.
I've added an ITypedEffectHandler interface, which exposing a single method for handling a single effect type.
public interface ITypedEffectHandler<in TEffect, TResult> : ITypedEffectHandler
where TEffect : Effect<TResult>
{
public ValueTask<TResult> Handle(TEffect effect);
}
To use these interfaces, I've added a EffectHandler implementation, that checks if it handles a specific effect with the ITypedEffectHandler, and dispatches the handling of that effect to the interface method.
public abstract class DispatchingEffectHandler : EffectHandler
{
public override async ValueTask Handle<TResult>(EffectAwaiter<TResult> abstractAwaiter)
{
// Does this class implement `ITypedEffectHandler<TEffect, TResult>`?
// - Yes? Call the method and set the result
// - No? Throw unhandled exception
}
}
When I want to specify an effects "contract", I create an interface like this:
public interface ICasinoEffectHandler :
ITypedEffectHandler<CoinTossEffect, bool>,
ITypedEffectHandler<DiceRollEffect, int>,
ITypedEffectHandler<FlipTableEffect> { }
Then, I can implement multiple handler implementations that are guaranteed to abide by this contract. e.g.
public class RiggedDiceRollEffectHandler : DispatchingEffectHandler, ICasinoEffectHandler
{
private readonly Queue<int> _rolls;
private bool _isTableFlipped;
public RiggedDiceRollEffectHandler(params int[] rolls)
{
_rolls = new Queue<int>(rolls);
}
public ValueTask<bool> Handle(CoinTossEffect effect) => ValueTask.FromResult(Random.Shared.NextDouble() < 0.5);
public ValueTask<int> Handle(DiceRollEffect effect) => ValueTask.FromResult(_rolls.Dequeue());
public ValueTask Handle(FlipTableEffect effect)
{
_isTableFlipped = true;
return ValueTask.CompletedTask;
}
}
Full code example here
Sorry, I know that's a lot! Any guidance would be greatly appreciated.