Working with Optional values in C# using LanguageExt

29. May 2023 Functional C# 0

Handling optional values is a common task in software development, and it often involves dealing with null references or exceptions. To provide a more elegant and functional approach to handling optional values, the LanguageExt library for C# offers the Option type. In this blog post, we will explore how to use Option and its various operations to work with optional values effectively.

What is Option?

Option<T> is a monadic type provided by the LanguageExt library that represents an optional value of type T. It allows you to handle scenarios where a value may or may not exist, eliminating the need for explicit null checks and reducing the risk of null reference exceptions.

Creating an Option

To create an Option, you can use one of the following methods:

Option<T>.Some(value): Creates an Option<T> with a value.
Option<T>.None: Creates an empty Option<T> with no value.
Let’s see some examples:

Option someOption = Option.Some("Hello");
Option noneOption = Option.None;

Pattern Matching with Option

To work with the value contained within an Option, you can use pattern matching to handle both cases: when a value is present (Some) and when it is absent (None).

void ProcessOption(Option<string> option)
{
    option.Match(
        some: value => Console.WriteLine("Value exists: " + value),
        none: () => Console.WriteLine("Value does not exist")
    );
}

ProcessOption(someOption); // Output: Value exists: Hello
ProcessOption(noneOption); // Output: Value does not exist

Transforming the Option Value

The Option type provides several methods to transform the wrapped value or perform computations on it.

Map: Applies a transformation function to the value inside the Option and returns a new Option with the transformed value.

Option numberOption = Option.Some(42);

Option resultOption = numberOption.Map(num => num.ToString());

resultOption.Match(
some: str => Console.WriteLine("Transformed value: " + str),
none: () => Console.WriteLine("Value does not exist")
);
// Output: Transformed value: 42

Bind: Applies a transformation function that returns another Option, and flattens the result.

Option numberOption = Option.Some(42);

Option resultOption = numberOption.Bind(num => Option.Some(num * 2));

resultOption.Match(
some: value => Console.WriteLine("Transformed value: " + value),
none: () => Console.WriteLine("Value does not exist")
);
// Output: Transformed value: 84

Filter: Checks a predicate against the value inside the Option and returns the original Option if the predicate is true, otherwise returns Option<T>.None.

Option numberOption = Option.Some(42);

Option filteredOption = numberOption.Filter(num => num % 2 == 0);

filteredOption.Match(
some: value => Console.WriteLine("Filtered value: " + value),
none: () => Console.WriteLine("Value does not exist or does not meet the condition")
);
// Output: Filtered value: 42

Unwrapping the Option

To extract the value from an Option, you can use the following methods:

Value: Retrieves the value from the Option. However, if the Option is None, it throws a ValueEmptyException.

Option<int> numberOption = Option<int>.Some(42);

int value = numberOption.Value;
Console.WriteLine("Extracted value: " + value);
// Output: Extracted value: 42

ValueOr: Retrieves the value from the Option, or returns a default value if the Option is None.

Option<int> numberOption = Option<int>.None;

int value = numberOption.ValueOr(0);
Console.WriteLine("Extracted value: " + value);
// Output: Extracted value: 0

Match: Provides a way to handle both cases of the Option in a concise manner by supplying separate functions for Some and None cases.

Option<int> numberOption = Option<int>.Some(42);

numberOption.Match(
    some: value => Console.WriteLine("Value exists: " + value),
    none: () => Console.WriteLine("Value does not exist")
);
// Output: Value exists: 42

Chaining Operations with Option

One of the key benefits of using Option is the ability to chain operations on optional values in a concise and readable way. Since Option supports the monadic pattern, you can use the Bind method to chain computations.

Option<int> CalculateSquareRoot(double value)
{
    if (value >= 0)
        return Option<int>.Some((int)Math.Sqrt(value));
    else
        return Option<int>.None;
}

Option<double> inputOption = Option<double>.Some(16);

Option<int> resultOption = inputOption.Bind(CalculateSquareRoot);

resultOption.Match(
    some: result => Console.WriteLine("Square root: " + result),
    none: () => Console.WriteLine("Invalid input or no square root exists")
);
// Output: Square root: 4

By using Bind to chain the CalculateSquareRoot function, we handle the possibility of invalid input or the absence of a square root gracefully, without resorting to null references or exceptions.


Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.