Skip to content

Question: Is there a recommended way to achieve type-safety in effect handlers? #36

@nth-commit

Description

@nth-commit

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 Eff method

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions