Option - Perustaja/Polyglot GitHub Wiki

Option

Inspired by Rust's Option monad which is very similar to Maybe in F# and Haskell. The Option monad represents a discriminated union type of either Some or None. For all implementations, they should act as close to possible as they do in Rust.

Instantiation

var something = Option<string>.Some("Hello");
var nothing = Option<string>.None;

Checking the underlying value

Two functions are provided to see check if it is Some or None.

var o = Option<string>.Some("Noniin");
Console.WriteLine(o.IsSome()); // Prints true
Console.WriteLine(o.IsNone()); // Prints false

Unwrapping - Returning the underlying value

Unwrap : Option<T> -> T

Unwrap() returns the underlying value if the value is Some, or throws an exception if it is None.

int some = Option<int>.Some(10).Unwrap(); // Returns 10
int none = Option<int>.None.Unwrap(); // Throws an exception!

UnwrapOr : Option<T>(T fallback) -> T

Provides a default value, if the default value is the result of a function call, it must be eagerly evaluted (the function must be invoked within the call toUnwrapOr()).

int some = Option<int>.Some(5).UnwrapOr(10); // Returns 5, the underlying value
int none = Option<int>.None.UnwrapOr(10); // Returns 10, the default value

UnwrapOrElse : Option<T>(Func<T> fallbackFunc) -> T

Provides a fallback function to lazily evaluate in a closure if the current is None.

int num = 10;
int some = Option<int>.Some(5).UnwrapOrElse(() => num * 5); // Returns 5, the underlying value
int none = Option<int>.None.UnwrapOrElse(() => num * 5); // Returns 50, the result of the default function

Mapping - Performing transformations

Map : Option<T>(Func<T, U> f) -> Option<U>

Map() is a function for mapping an Option<T> to an Option<U> by invoking a passed function. If the current is Some, the function is invoked with the underlying value, returning a new Some of a different type. If it is None, the result is still None.

var someOpt = Option<int>.Some(10).Map(o => o.ToString()); // Returns Some with underlying value "10"
var none = Option<int>.None.Map(o => o.ToString()); // Returns None

MapOr : Option<T>(U fallback, Func<T, U> f) -> U

Provides a fallback value to return in case the current is None.

string some = Option<int>.Some(10).MapOr("Default", o => o.ToString()); // Returns "10"
string none = Option<int>.None.MapOr("Default", o => o.ToString()); // Returns "Default"

MapOrElse : Option<T>(Func<U> fallbackFunc, Func<T, U> f) -> U

Provides a fallback function to lazily evaluate in a closure if the current is None.

string greeting = "hello";
string r = Option<int>.None.MapOrElse(
    () => greeting.ToUpper, 
    s => s.ToString()
    );
// r : "HELLO"

Combinators

AndThen : Option<T>(Func<T, Option<U>> f) -> Option<U>

AndThen() allows chaining. Each function in the chain returns an Option, calling the passed function on its underlying value if Some, or returning None if there isn't one. If any of the calls in the chain return None, the end result is None.

// Assume the following function exists
private Option<int> SquareIfEven(int n)
    => n % 2 == 0 
    ? Option<int>.Some(n * n)
    : Option<int>.None;

public void SomeOtherScope()
{
    var o = Option<int>.Some(2);
    var r = o.AndThen(SquareIfEven).AndThen(SquareIfEven).Unwrap(); // Returns 16

    o = Option<int>.Some(3);
    var r = o.AndThen(SquareIfEven).AndThen(SquareIfEven).Unwrap() // Throws an exception
}

Filtering and mapping to Results

Filter : Option<T>(Func<T, bool> predicate) -> Option<T>

Returns None if current option has no value or the option has a value but the predicate returns false when evaluated with the underlying Some value. Returns Some if the current is Some and the predicate evaluates to true.

var none = Option<int>.Some(3).Filter(n => n % 2 == 0); // is None
var some = Option<int>.Some(2).Filter(n => n % 2 == 0); // is Some with underlying value 2

Or : Option<T>(Option<T> other) -> Option<T>

Returns the current Option if Some, else returns the argument. Note that this does not guarantee the other is Some.

var original = Option<int>.None.Or(Option<int>.Some(10)); 
// original : Some(10)

OkOr : Option<T>(E error) -> Result<T, E>

Transforms the Option into a Result. If current is Some, returns Ok with the same underlying value, else returns Err with underlying value set to the argument.

var okay = Option<int>.Some(10).OkOr<string>("Error!");
// okay : Ok(10)
var notOkay= Option<int>.None.OkOr<string>("Error!");
// okay : Err("Error!")

OkOrElse : Option<T>(Func<E> f) -> Result<T, E>

Performs the same functionality as OkOr but lazily evaluates the error function to use it as the error value.

var okay = Option<int>.Some(10).OkOrElse<string>(() => 10.ToString());
// okay : Ok(10)
var notOkay = Option<int>.None.OkOrElse<string>(() => 10.ToString());
// notOkay : Err("10")

Matching - Side-effects or I/O from an Option

Match - Actions instead of Funcs

This can be used for I/O operations or when a side effect is desired. This is made to mimic the match block in Rust, but obviously has its downsides as you cannot handle many different values like in Rust.

var o = Option<string>.Some("10");
o.Match(
    s => Console.WriteLine(s),
    () => someFile.WriteLine("Empty!")
);
// Prints "10" to the console

var o = Option<string>.None;
o.Match(
    s => Console.WriteLine(s),
    () => someFile.WriteLine("Empty!")
);
// Prints "Empty!" to some file

Equity and Hashing

Options are safe to use in hashed collections. It is also safe to use the == operator on them.

⚠️ **GitHub.com Fallback** ⚠️