Option<T> for High Performance C#

Ages ago, I wrote about how the concept of null is a million dollar mistake and tried to replicate the concept of Option from other languages into C#. We are heavily using this in Academia for newer code since then. We even refactored some of our old code that causes the most NullPointerExceptions. I’m making a pure DOTS game as programming practice and I realize that I want the same construct to represent the concept of none, nothing, or no result but for the C# subset called High Performance C# (HPC#). Our current implementation of Option<T> can’t be used in HPC# as it uses reference types like interfaces and delegates. This means that I have to write another kind of Option<T> but only for value types.

ValueTypeOption<T>

The name is a mouthful but feel free to use another name if you’re going to adopt this code. Here’s how it looks like:

public readonly struct ValueTypeOption<T> : IEquatable<ValueTypeOption<T>> where T : struct {
    // We use property here as static member variable doesn't work for Burst
    public static ValueTypeOption<T> None {
        get {
            return new ValueTypeOption<T>();
        }
    }
    
    public static ValueTypeOption<T> Some(T value) {
        return new ValueTypeOption<T>(value);
    }
    
    private readonly T value;
    private readonly byte hasValue;

    private ValueTypeOption(T value) {
        this.value = value;
        this.hasValue = 1;
    }

    public bool IsSome {
        get {
            return this.hasValue > 0;
        }
    }

    public bool IsNone {
        get {
            return this.hasValue <= 0;
        }
    }

    public void Match<TMatcher>(TMatcher matcher) where TMatcher : struct, IOptionMatcher<T> {
        if (this.IsSome) {
            matcher.OnSome(this.value);
        } else {
            matcher.OnNone();
        }
    }

    public TReturnType Match<TMatcher, TReturnType>(TMatcher matcher)
        where TMatcher : struct, IFuncOptionMatcher<T, TReturnType> {
        return this.IsSome ? matcher.OnSome(this.value) : matcher.OnNone();
    }

    public T ValueOr(T other) {
        return this.IsSome ? this.value : other;
    }

    public bool Equals(ValueTypeOption<T> other) {
        return this.hasValue == other.hasValue && this.value.Equals(other.value);
    }

    public override bool Equals(object obj) {
        return obj is ValueTypeOption<T> other && Equals(other);
    }

    public override int GetHashCode() {
        unchecked {
            return (this.hasValue.GetHashCode() * 397) ^ this.value.GetHashCode();
        }
    }

    public static bool operator ==(ValueTypeOption<T> left, ValueTypeOption<T> right) {
        return left.Equals(right);
    }

    public static bool operator !=(ValueTypeOption<T> left, ValueTypeOption<T> right) {
        return !left.Equals(right);
    }
}

ValueTypeOption<T> is implemented as a readonly struct to intentionally make it as an immutable value. It has the convenience property None to represent null, none, or no value. It’s implemented here as a property instead of a static value as Burst throws errors when the static value is used. A static Some() method is provided to denote a ValueTypeOption with a value. The constructor with specified value is set to private so that client code is forced to use Some(). Client code may use the default constructor but that defaults to a None value.

The method requirements to implement IEquatable<T> are auto generated by Rider. The equality methods are handy to compare ValueTypeOption values without having to write a matcher.

Here’s what it looks like to create ValueTypeOption values:

// A none target
ValueTypeOption<Entity> target = ValueTypeOption<Entity>.None;

// A ValueTypeOption of int2 with a value
ValueTypeOption<int2> tilePosition = ValueTypeOption<int2>.Some(new int2(x, y));

Accessors

For accessing the value, I made 3 ways to do this – a void matcher, a func matcher, and ValueOr() method. For the matchers, I just reused the interfaces I used for the original Option<T> implementation. The difference in this case is that the matchers can only be structs. They can’t be an interface or a delegate.

Here’s what a void matcher looks like:

// Let's just say, we don't know if there really was a target or not
ValueTypeOption<Entity> target = ResolveTarget();

// This is how to match
target.Match(new HitMatcher() {
    allHp = GetComponentDataFromEntity<HitPoints>(),
    damage = this.damage
});

// This is the struct matcher
private struct HitMatcher : IOptionMatcher<Entity> {
    private ComponentDataFromEntity<HitPoints> allHp;
    private int damage;

    public void OnSome(Entity targetEntity) {
        // Apply the damage
        HitPoints hp = this.allHp[targetEntity];
        hp.Value -= this.damage;
        
        // Modify value
        this.allHp[targetEntity] = hp;
    }

    public void OnNone() {
        // No target
    }
}

Here’s what a func matcher looks like (It’s like a void matcher but returns a value):

// Let's say weapons may or may not have and effect bounds
ValueTypeOption<Rect> effectBounds = weapon.EffectBounds;

float area = effectBounds.Match<ComputeArea, float>(new ComputeArea());
// ... do something with area

// Here's a sample func matcher
private readonly struct ComputeArea : IFuncOptionMatcher<Rect, float> {
    public float OnSome(Rect rect) {
        return rect.Width * rect.Height;
    }

    public float OnNone() {
        // There's no rect
        return 0;
    }
}

Using a func matcher is a little more verbose as we have to specify the type of the matcher and the return type in the method. This is needed to avoid boxing (aka avoid reference types) and in effect prevents garbage, too.

The last accessor is the convenience method called ValueOr(). It returns the value if it has a value. Otherwise, it returns the specified other value. Ideally, this is not advisable as it can be abused but since we are dealing with value types, there’s really no way to cause a NullPointerException. However, this should be used sparingly and should be code reviewed carefully when it is used. It’s usage should be justified.

// Let's say ObjectRenderingData contains the spriteIds for different orientations (up, down, left, right)
// but not all orientations are necessarily specified so it uses ValueTypeOption 
ObjectRenderingData renderingData = ResolveObjectRenderingData(this.objectId);
ValueTypeOption<FixedString64> orientationSpriteId = renderingData.GetSpriteId(this.orientation);

// Return empty if orientationSpriteId is None
FixedString64 spriteId = orientationSpriteId.ValueOr(new FixedString64());
if(spriteId.Length > 0) {
    // Process only if spriteId is not empty
    ...
}

Does it work with Burst?

Of course it does. The point of making this was to be able to make it work with DOTS. I’m already using it in my experiment pure DOTS project. Here’s a simple test system:

public class ValueTypeOptionBurstTest : SystemBase {
    // A component with a dummy ValueTypeOption variable
    public struct OptionData : IComponentData {
        public readonly ValueTypeOption<int> rawValue; // May be none
        public int computedValue;

        public OptionData(ValueTypeOption<int> rawValue) {
            this.rawValue = rawValue;
            this.computedValue = 0;
        }
    }
    
    protected override void OnCreate() {
        // Create a substantial amount of entities with OptionData
        // 30% of which may not have a raw value
        for (int i = 0; i < 10000; ++i) {
            Entity entity = this.EntityManager.CreateEntity(typeof(OptionData));
            float random = UnityEngine.Random.value;
            ValueTypeOption<int> rawValue = random > 0.7f ? ValueTypeOption<int>.None : ValueTypeOption<int>.Some(UnityEngine.Random.Range(1, 1000));
            this.EntityManager.SetComponentData(entity, new OptionData(rawValue));
        }
    }

    protected override void OnUpdate() {
        // Do some dummy computation
        this.Entities.ForEach(delegate(ref OptionData data) {
            data.computedValue = data.rawValue.Match<ComputeMatcher, int>(new ComputeMatcher());
        }).ScheduleParallel();
    }

    private readonly struct ComputeMatcher : IFuncOptionMatcher<int, int> {
        public int OnSome(int rawValue) {
            // Cube value
            return rawValue * rawValue * rawValue;
        }

        public int OnNone() {
            // No raw value
            return 0;
        }
    }
}

Why use this? It’s verbose.

It’s all about intent. I want to represent the concept of none, null, or nothing value but also force the user of the code to handle it properly meaning to handle both the presence or absence of value. A method or property returning a ValueTypeOption<T> clearly says that it may return nothing and the user of such code will be forced to handle the nothing value.

The other obvious reason is to reduce bugs.

That’s all I have for now. I hope that helps.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s