Skip to content

Instantly share code, notes, and snippets.

@Refsa
Created January 9, 2021 02:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Refsa/1bde25277cb63e0e4f6956827453ed80 to your computer and use it in GitHub Desktop.
Save Refsa/1bde25277cb63e0e4f6956827453ed80 to your computer and use it in GitHub Desktop.

Option for C#

Simple utility to wrap your objects in an Option type to avoid and handle common issues with null.

It is a common pattern seen in languages such as Rust, F# and Haskell in order to force handling of scenarios where a variable might have a value.

This implementation is mimicking the API found in Rust's Option type.

Examples

I'll use this class in the examples, but value types such as struct and float also works.

class SomeClass 
{
    public float Value;
    public SomeClass(float value) => Value = value;
}

Creating an Option type

SomeClass someObject = new SomeClass(10f);

// It supports implicit conversion into an Option
Option<SomeClass> asSome = someObject;
asSome.IsSome() == true;

Option<SomeClass> asNone = null;
asNone.IsNone() == true;

// You can explicitly turn it into `Some` 
var asSome = Option<SomeClass>.Some(someObject);

// or create it as `None`
var asNone = Option<SomeClass>.None();

Checking the state

You can check which state an Option is in

Option<SomeClass> option = new SomeClass(10f);
if (option.IsSome()) {// true}
if (option.IsNone()) {// false}

Getting the value

Several methods exists to unwrap an Option value, some will throw an exception when the value is None.

Unwrap

Throws an exception on None, should obviously be avoided for most use cases

Option<SomeClass> option = new SomeClass(10f);
SomeClass value = option.Unwrap();

Expect

Throws an exception, but prefixes it with the given string, useful if you know the value should not be None.

Option<SomeClass> option = new SomeClass(10f);
SomeClass value = option.Expect("Some descriptive prefix");

TryUnwrap

Safe unwrap using the Try pattern.

Option<SomeClass> option = new SomeClass(10f);
if (option.TryUnwrap(out SomeClass value)) { }

UnwrapOr

Eager Or, gives the variable passed in the parameter if option is None.
Use this with caution, as the parameter variable is created even if it's not used.

Option<SomeClass> option = new SomeClass(10f);
SomeClass value = option.UnwrapOr(new SomeClass(0f));
UnwrapOrElse

Lazy Or, uses the delegate pass in the parameter to produce a value if option is None.
This is the preferred way for producing a value on None as it is only created when needed.

Option<SomeClass> option = null;
SomeClass value = option.UnwrapOrElse(() => new SomeClass(0f));

UnwrapOrDefault

Returns default(T) when option is None

Option<SomeClass> option = null;
SomeClass value = option.UnwrapOrDefault();

Unwrap to Callbacks

Some utility to run a callback when unwrapping a value.

Option<SomeClass> option = new SomeClass(10f);

// Runs when option is `Some`
option.WhenSome(value => { });

// Runs when option is `None`
option.WhenNone(() => { });

// if you want something similar to a `match` statement in Rust you could use this
option.Match(
    some: value => {}, 
    none: () => {}
);

// Only run when option is `Some` and the containing value is equal to the one given
option.SomeEqualsThen(new SomeClass(10f), (value) => { });

Advanced methods

Some additional methods are supplied for more in-depth use cases

Map

For this example we will map SomeClass into OtherClass.

class OtherClass 
{
    public int Value;
    public OtherClass(int value) => Value = value;
}
Option<SomeClass> option = new SomeClass(10f);

Option<OtherClass> = option.Map(origin => {
    return Option<OtherClass>.Some((int)origin);
})

And

Gives the value of other if both are Some.

Option<SomeClass> option1 = new SomeClass(10f);
Option<SomeClass> option2 = new SomeClass(20f);
Option<SomeClass> option3 = null;

// This variable will be equal to `option2`
var some = option1.And(option2); 

// This variable will be equal to "Option<SomeClass>.None"
var none = option1.And(option2).And(option3);

Or

Gives the first value that is Some or None if both are None

Option<SomeClass> option1 = new SomeClass(10f);
Option<SomeClass> option2 = null;

// This will be equal to `option1`
var or = option1.Or(option2);

// This will be equal to `option1` as well
var or = option2.Or(option1);

Filter

If value is Some it will use the given delegate to filter out unwanted values, similar to SomeEqualsThen but will instead return the Option<T> object. Also allows you to filter against inner values inside of T.

Option<SomeClass> option1 = new SomeClass(10f);

// Filtered will be equal to `option1`
var filtered = option1.Filter(value => value.Value == 10f);

// Filtered will be equal to "Option<SomeClass>.None"
var filtered = option1.Filter(value => value.Value == 5f);
public class OptionException : System.Exception
{
public OptionException(string message) : base(message) { }
}
public struct Option<T>
{
T _value;
/// <summary>
/// Wrap value in Option
/// </summary>
public static Option<T> Some(T value)
{
return new Option<T> { _value = value };
}
/// <summary>
/// Set to None with T as backing value
/// </summary>
public static Option<T> None()
{
return new Option<T>();
}
/// <summary>
/// Checks is Option is of None type
/// </summary>
/// <returns>True if Option is None</returns>
public bool IsNone()
{
return _value.Equals(null);
}
/// <summary>
/// Checks if Option is of Some type
/// </summary>
/// <returns>True of Option is Some</returns>
public bool IsSome()
{
return _value != null && !_value.Equals(null);
}
/// <summary>
/// Safe unwrap of value
/// </summary>
/// <param name="value">Spits out value, if Option is Some</param>
/// <returns>True if Option is Some</returns>
public bool TryUnwrap(out T value)
{
value = _value;
return IsSome();
}
/// <summary>
/// Unsafe unwrap of Option
///
/// Throws OptionException if Option is None
/// </summary>
/// <returns>Value if Option is Some</returns>
public T Unwrap()
{
if (IsNone())
{
throw new OptionException("Called unwrap on a None value");
}
return _value;
}
/// <summary>
/// Safe unwrap into "other" on None, else it gives Value
/// </summary>
/// <param name="other">Value returned if Option is None</param>
/// <returns>Value on Some, "other" on None</returns>
public T UnwrapOr(T other)
{
if (IsSome())
{
return _value;
}
return other;
}
/// <summary>
/// Returns value from "producer" on None
/// </summary>
/// <param name="producer">Delegate to produce else value with</param>
/// <returns>Value on Some, result from "producer" on None</returns>
public T UnwrapOrElse(System.Func<T> producer)
{
if (IsNone())
{
return producer.Invoke();
}
return _value;
}
/// <summary>
/// Unwraps to default(T) on None
/// </summary>
/// <returns>Value on Some, default(T) on None</returns>
public T UnwrapOrDefault()
{
if (IsSome())
{
return _value;
}
return default(T);
}
public T Expect(string onNone)
{
if (IsSome())
{
return _value;
}
throw new OptionException($"{this.ToString()}: {onNone}");
}
/// <summary>
/// Runs delegate when Option is Some
/// </summary>
/// <param name="callback">Callback to run, takes T as parameter</param>
/// <returns>this, unmodified</returns>
public Option<T> WhenSome(System.Action<T> callback)
{
if (IsSome())
{
callback.Invoke(_value);
}
return this;
}
/// <summary>
/// Runs a callback delegate if Value is equal to "other"
/// </summary>
/// <param name="other">Object to check equality against</param>
/// <param name="callback">Callback to run if Value is equal to "other"</param>
/// <returns></returns>
public Option<T> SomeEqualsThen(T other, System.Action<T> callback)
{
if (IsNone())
{
return this;
}
if (_value.Equals(other))
{
callback(_value);
}
return this;
}
public bool SomeEquals(T other)
{
if (IsNone())
{
return false;
}
if (_value.Equals(other))
{
return true;
}
return false;
}
/// <summary>
/// Runs callback when Option is None
/// </summary>
/// <param name="callback">callback to run on None</param>
/// <returns>this, unmodified</returns>
public Option<T> WhenNone(System.Action callback)
{
if (IsNone())
{
callback.Invoke();
}
return this;
}
/// <summary>
/// Simplified way to mimick "match" functionality of Rust/F#
/// </summary>
/// <param name="some">callback to run when Some</param>
/// <param name="none">callback to run when None</param>
/// <returns>this, unmodified</returns>
public Option<T> Match(System.Action<T> some, System.Action none)
{
if (IsSome())
{
some.Invoke(_value);
}
else
{
none.Invoke();
}
return this;
}
/// <summary>
/// Maps Value into U through "mapper" delegate, wraps new Value Option
/// </summary>
/// <param name="mapper">delegate to map Value T into U</param>
/// <typeparam name="U">Type that Value is mapped into</typeparam>
/// <returns>Option U.Some if this is Some, Option.None if None</returns>
public Option<U> Map<U>(System.Func<T, Option<U>> mapper)
{
if (TryUnwrap(out T value))
{
return mapper.Invoke(value);
}
return Option<U>.None();
}
/// <summary>
/// And conditional, results in None if both are not Some
/// </summary>
/// <param name="other">other Option to check against</param>
/// <returns>Some of "other" if both are SOme, None if not</returns>
public Option<T> And(Option<T> other)
{
if (IsSome() && other.IsSome())
{
return other;
}
return Option<T>.None();
}
/// <summary>
/// Or conditional, result is None if both are None
/// </summary>
/// <param name="other">other Option to check against</param>
/// <returns>Some of value that is Some or Some of this, None if both are None</returns>
public Option<T> Or(Option<T> other)
{
if (IsSome())
{
return this;
}
if (other.IsSome())
{
return other;
}
return Option<T>.None();
}
/// <summary>
/// Produces a new option if this Option is None
/// </summary>
/// <param name="producer">delegate to produce new Option from</param>
/// <returns>this if it is Some, Option from "producer" if not</returns>
public Option<T> OrElse(System.Func<Option<T>> producer)
{
if (IsSome())
{
return this;
}
return producer.Invoke();
}
/// <summary>
/// If this is Some it will run "filter" against it,
/// if filter returns in "false" it returns None
/// </summary>
/// <param name="filter">delegate to filter Value with</param>
/// <returns>None if this is None or "filter" returns false</returns>
public Option<T> Filter(System.Func<T, bool> filter)
{
if (IsNone())
{
return this;
}
if (filter.Invoke(_value))
{
return this;
}
return Option<T>.None();
}
public static implicit operator Option<T>(T value)
{
if (value == null || value.Equals(null))
{
return Option<T>.None();
}
else
{
return Option<T>.Some(value);
}
}
public static explicit operator T(Option<T> value)
{
return value.UnwrapOrDefault();
}
public override string ToString()
{
if (IsSome())
{
return $"Some<{typeof(T)}>({_value.ToString()})";
}
else
{
return $"None<{typeof(T)}>";
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment