A robust option type for C#
Optional is a robust option/maybe type for C#.
Version: 4.0.0
Optional is a strongly typed alternative to null values that lets you:
Simply reference Optional.dll and you are good to go!
Optional is also available via NuGet:
PM> Install-Package Optional Or visit: https://www.nuget.org/packages/Optional/
The core concept behind Optional is derived from two common functional programming constructs, typically referred to as a maybe type and an either type (referred to as Option and Option in Optional).
Many functional programming languages disallow null values, as null-references can introduce hard-to-find bugs. A maybe type is a type-safe alternative to null values.
In general, an optional value can be in one of two states: Some (representing the presence of a value) and None (representing the lack of a value). Unlike null, an option type forces the user to check if a value is actually present, thereby mitigating many of the problems of null values. Option is a struct in Optional, making it impossible to assign a null value to an option itself.
Further, an option type is a lot more explicit than a null value, which can make APIs based on optional values a lot easier to understand. Now, the type signature will indicate if a value can be missing!
An either type is conceptually similar to a maybe type. Whereas a maybe type only indicates if a value is present or not, an either type contains an auxiliary value describing how an operation failed. Apart from this exceptional value, an either-type behaves much like its simpler counterpart.
Working with maybe and either types is very similar, and the description below will therefore focus on the maybe type, and only provide a quick summary for the either type.
Finally, Optional offers several utility methods that make it easy and convenient to work with both of the above described optional values.
To use Optional simply import the following namespace:
using Optional;A few auxiliary namespaces are provided:
using Optional.Linq; // Linq query syntax support
using Optional.Unsafe; // Unsafe value retrievalThe most basic way to create optional values is to use the static Option class:
var none = Option.None();
var some = Option.Some(10);For convenience, a set of extension methods are provided to make this a little less verbose:
var none = 10.None(); // Creates a None value, with 10 determining its type (int)
var some = 10.Some();Note that it is also allowed (but hardly recommended) to wrap null values in an Option instance:
string nullString = null;
var someWithNull = nullString.Some();To make it easier to filter away such null values, a specialized extension method is provided:
string nullString = null;
var none = nullString.SomeNotNull(); // Returns None if original value is nullSimilarly, a more general extension method is provided, allowing a specified predicate:
string str = "abc";
var none = str.SomeWhen(s => s == "cba"); // Return None if predicate is violated
var none = str.NoneWhen(s => s == "abc"); // Return None if predicate is satisfiedClearly, optional values are conceptually quite similar to nullables. Hence, a method is provided to convert a nullable into an optional value:
int? nullableWithoutValue = null;
int? nullableWithValue = 2;
var none = nullableWithoutValue.ToOption();
var some = nullableWithValue.ToOption();When retrieving values, Optional forces you to consider both cases (that is if a value is present or not).
Firstly, it is possible to check if a value is actually present:
var hasValue = option.HasValue;If you want to check if an option contains a specific value, you can use the Contains or Exists methods. The former checks if the optional contains a specified value, the latter if the contained value satisfies some predicate:
var isThousand = option.Contains(1000);
var isGreaterThanThousand = option.Exists(val => val > 1000);The most basic way to retrieve a value from an Option is the following:
// Returns the value if present, or otherwise an alternative value (10)
var value = option.ValueOr(10);
var value = option.ValueOr(() => SlowOperation()); // Lazy variantIn more elaborate scenarios, the Match method evaluates a specified function:
// Evaluates one of the provided functions and returns the result
var value = option.Match(x => x + 1, () => 10);
// Or written in a more functional'ish style (think pattern matching)
var value = option.Match(
some: x => x + 1,
none: () => 10
);There is a similar Match function to simply induce side-effects:
// Evaluates one of the provided actions
option.Match(x => Console.WriteLine(x), () => Console.WriteLine(10));
// Or pattern matching'ish as before
option.Match(
some: x => Console.WriteLine(x),
none: () => Console.WriteLine(10)
);Finally, side-effect matching (that is matching without returning a value) can be carried out for each case separately:
// Evaluated if the value is present
option.MatchSome(x =>
{
Console.WriteLine(x)
});
// Evaluated if the value is absent
option.MatchNone(() =>
{
Console.WriteLine("Not found")
});In some cases you might be absolutely sure that a value is present. Alternatively, the lack of a value might be fatal to your program, in which case you just want to indicate such a failure.
In such scenarios, Optional allows you to drive without a seatbelt. However, to stress the lack safety, another namespace needs to be imported:
using Optional.Unsafe;When imported, values can be retrieved unsafely as:
var value = option.ValueOrFailure();
var anotherValue = option.ValueOrFailure("An error message"); In case of failure an OptionValueMissingException is thrown.
In a lot of interop scenarios, it might be necessary to convert an option into a potentially null value. Once the Unsafe namespace is imported, this can be done relatively concisely as:
var value = option.ValueOrDefault(); // value will be default(T) if the option is empty.Similarly, it is possible to convert an option into to a nullable (insofar as the inner value is a value type):
var nullable = option.ToNullable();As a rule of thumb, such conversions should be performed only just before the nullable value is needed (e.g. passed to an external library), to minimize and localize the potential for null reference exceptions and the like.
A few extension methods are provided to safely manipulate optional values.
The Or function makes it possible to specify an alternative value. If the option is none, a some instance will be returned:
var none = Option.None();
var some = none.Or(10); // A some instance, with value 10
var some = none.Or(() => SlowOperation()); // Lazy variantSimilarly, the Else function enables you to specify an alternative option, which will replace the current one, in case no value is present. Notice, that both options might be none, in which case a none-option will be returned:
var none = Option.None();
var some = none.Else(10.Some()); // A some instance, with value 10
var some = none.Else(Option.None()); // A none instance
var some = none.Else(() => Option.Some()); // Lazy variantThe Map function transforms the inner value of an option. If no value is present none is simply propagated:
var none = Option.None();
var stillNone = none.Map(x => x + 10);
var some = 10.Some();
var somePlus10 = some.Map(x => x + 10);The FlatMap function chains several optional values. It is similar to Map, but the return type of the transformation must be another optional. If either the resulting or original optional value is none, a none instance is returned. Otherwise, a some instance is returned according to the specified transformation:
var none = Option.None();
var stillNone = none.FlatMap(x => x.Some()); // Returns another Option
var some = 10.Some();
var stillSome = some.FlatMap(x => x.Some());
var none = some.FlatMap(x => x.None()); // Turns empty, as it maps to noneFlatMap is useful in combination with methods that return optional values themselves:
public static Option FindPersonById(int id) { ... }
public static Option GetHairstyle(Person person) { ... }
var id = 10;
var person = FindPersonById(id);
var hairstyle = person.FlatMap(p => GetHairstyle(p));
hairstyle.Match( ... );In case you end up with a nested optional (e.g. Option>), you might flatten it by flatmapping it onto itself, but a dedicated Flatten function is offered for convenience:
Option> nestedOption = ...
Option option = nestedOption.Flatten(); // same as nestedOption.FlatMap(o => o)Finally, it is possible to perform filtering. The Filter function returns none, if the specified predicate is not satisfied. If the option is already none, it is simply returned as is:
var none = Option.None();
var stillNone = none.Filter(x => x > 10);
var some = 10.Some();
var stillSome = some.Filter(x => x == 10);
var none = some.Filter(x => x != 10);A recurring scenario, when working with null-returning APIs, is that of filtering away null values after a mapping. To ease the pain, a specific NotNull filter is provided:
// Returns none if the parent node is null
var parent = GetNode()
.Map(node => node.Parent)
.NotNull(); An option implements GetEnumerator, allowing you to loop over the value, as if it was a collection with either a single or no elements.
foreach (var value in option)
{
Console.WriteLine(value);
}As you might have noticed, this is a nice and lightweight alternative to Match in cases where you only want to do something if the value is present. Also, you should use this instead of the more verbose and unsafe combination of option.HasValue and option.ValueOrFailure(), which you might otherwise be tempted to try.
Notice, however, that options don't actually implement IEnumerable, in order to not pollute the options with LINQ extension methods and the like. Although many LINQ methods share functionality similar to those offered by an option, they offer a more collection-oriented interface, and includes several unsafe functions (such as First, Single, etc).
Although options deliberately don't act as enumerables, you can easily convert an option to an enumerable by calling the ToEnumerable() method:
var enumerable = option.ToEnumerable();Optional supports LINQ query syntax, to make the above transformations somewhat cleaner.
To use LINQ query syntax you must import the following namespace:
using Optional.Linq;This allows you to do fancy stuff such as:
var personWithGreenHair =
from person in FindPersonById(10)
from hairstyle in GetHairstyle(person)
from color in ParseStringToColor("green")
where hairstyle.Color == color
select person;In general, this closely resembles a sequence of calls to FlatMap and Filter. However, using query syntax can be a lot easier to read in complex cases.
No open issues yet, or sync has not completed.