Skip to content

Maybe Type Extension Methods

Zeid Youssefzadeh edited this page Feb 24, 2025 · 1 revision

overview

The ZeidLab.ToolBox.Options namespace consolidates various extension methods that streamline conversion and error handling across monadic types. By leveraging these extension methods, developers can create robust, clear, and efficient code that gracefully handles both successful outcomes and error states, ultimately improving both maintainability and performance. The Maybe monad (represented by Maybe<TIn>) encapsulates an optional value and improves overall software performance by eliminating null reference issues. This design enhances the robustness of railway-oriented programming by clearly indicating whether a value is present (IsSome) or absent (IsNone).

Benefits of Using the Maybe Monad and These Extensions:

  • Robust Error Handling: Minimizes null reference exceptions by clearly delineating between present and absent values.
  • Improved Code Clarity: The explicit handling of optional values supports a more expressive, railway-oriented programming style.
  • Enhanced Performance: Eliminates unnecessary null checks and simplifies error-handling logic.
  • Versatility: Seamlessly supports both synchronous and asynchronous workflows with flexible default value strategies.

Reduce/ReduceAsync

Reduce<TIn> and ReduceAsync<TIn> simplify extracting a value from a Maybe<TIn> monad by returning its contained value when available (IsSome) or a specified default (or computed substitute) when absent(IsNone). Reduce<TIn> works synchronously, while ReduceAsync<TIn> integrates with async workflows, eliminating explicit null checks and streamlining error handling in railway-oriented programming. This method is the last member in the chain of extension methods.

Key Characteristics:

  • Final Unwrapping: Converts Maybe<TIn>TIn (removes Maybe<TIn> wrapper)
  • Null Safety: Guarantees a non-nullable return (assuming substitute isn't null)
  • Cost Control: Substitute method is never invoked if Maybe<TIn>.IsSome

The below table summarizes the types and parameters of each method overload and their respective applicability.

Method Applicable Types Accepted Parameter returns
Reduce<TIn> Maybe<TIn> TIn(default value) TIn
Reduce<TIn> Maybe<TIn> Func<TIn>(default provider) TIn
ReduceAsync<TIn> Maybe<TIn> Task<TIn>(asynchronous default) Task<TIn>
ReduceAsync<TIn> Task<Maybe<TIn>> TIn(default value) Task<TIn>
ReduceAsync<TIn> Task<Maybe<TIn>> Func<TIn>(default provider) Task<TIn>
ReduceAsync<TIn> Task<Maybe<TIn>> Task<TIn>(asynchronous default) Task<TIn>

Example:

Maybe<int> maybeInt = Maybe.Some(10);
int result = maybeInt.Reduce(0);  // Returns 10 

Maybe<int> maybeNone = Maybe.None<int>();
int noneResult = maybeNone.Reduce(-1);  // Returns -1

Maybe<int> maybeNone = Maybe.None<int>();
int result = maybeNone.Reduce(() => DateTime.Now.Second);  // returns current second

This unified approach provides consistent unwrapping behavior while offering both eager and lazy fallback strategies.

^ Back To Top

Bind/BindAsync

Transforms the value of a Maybe<TIn> into a new Maybe<TOut> using the specified mapping function. This method enables chaining operations while automatically propagating the None state. If the original Maybe<TIn> is None, it immediately returns Maybe<TOut>.None without invoking the mapping function, ensuring subsequent operations in the chain are skipped for efficiency.In other words, you need only to focus on the happy path, the other one is already taken care of.

Key Behavior:

  • Propagates None: If the input Maybe<TIn> is None, the output Maybe<TOut> will also be None, and no mapping function will execute.
  • Chaining Support: Designed for sequential operations where each step depends on the previous result, and you are absolutely sure your methods will receive a non-null value.
  • Asynchronous Support: Providing both synchronous and asynchronous variants to integrate seamlessly with modern asynchronous code patterns.

The below table summarizes the types and parameters of each method overload and their respective applicability.

Method Applicable Types Accepted Parameter returns
Bind<TIn, TOut> Maybe<TIn> Func<TIn, Maybe<TOut>> Maybe<TOut>
BindAsync<TIn, TOut> Maybe<TIn> Func<TIn, Task<Maybe<TOut>>> Task<Maybe<TOut>>
BindAsync<TIn, TOut> Task<Maybe<TIn>> Func<TIn, Maybe<TOut>> Task<Maybe<TOut>>
BindAsync<TIn, TOut> Task<Maybe<TIn>> Func<TIn, Task<Maybe<TOut>>> Task<Maybe<TOut>>

Example: Synchronous Binding

Maybe<int> maybeInt = Maybe.Some(5);
// maybeString is now a Maybe<string> containing "5".
Maybe<string> maybeString = maybeInt.Bind(x => x.ToString().ToSome());

Example: Asynchronous Binding on Maybe<TIn>

var maybeNumber = Maybe.Some(5);
Task<Maybe<string>> maybeStringTask = maybeNumber.BindAsync(async num => {
    await Task.Delay(100); // Simulate asynchronous work
    return Maybe.Some(num.ToString());
});
// Await the task to obtain the transformed value.
Maybe<string> maybeString = await maybeStringTask;

Example: Binding with Task<Maybe<TIn>> (Synchronous Mapping)

Task<Maybe<int>> maybeTask = Task.FromResult(Maybe.Some(20));
Task<Maybe<string>> maybeStringTask = maybeTask.BindAsync(num => Maybe.Some(num.ToString()));
// The task resolves to a Maybe containing "20"
Maybe<string> maybeString = await maybeStringTask;

Example: Binding with Task<Maybe<TIn>> (Asynchronous Mapping)

Task<Maybe<int>> maybeTask = Task.FromResult(Maybe.Some(30));
Task<Maybe<string>> maybeStringTask = maybeTask.BindAsync(async num => {
    await Task.Delay(50); // Simulate asynchronous work
    return Maybe.Some(num.ToString());
});
// The task resolves to a Maybe containing "30"
Maybe<string> maybeString = await maybeStringTask;

Notes:

  • The mapping function (Func<TIn, Maybe<TOut>) determines the next value in the chain.
  • Implicit conversions (e.g., from T to Maybe<T>.Some) simplify returning values in the examples.

^ Back To Top

Match/MatchAsync

The Match and MatchAsync methods provide a streamlined way to handle the presence or absence of a value within a Maybe<TIn> instance. In essence, Match allows you to execute one function if a valid value exists (the "some" branch) or another function if it does not (the "none" branch), thereby reducing explicit null checks and consolidating error handling into a single, clear control flow. Meanwhile, MatchAsync extends this concept into the asynchronous realm, enabling you to perform asynchronous operations based on whether the value is present or absent, which is particularly useful for integrating with modern, async-based workflows. Both methods embody the principles of Railway-Oriented Programming by providing a consistent and expressive way to manage success and failure scenarios.

The below table summarizes the types and parameters of each method overload and their respective applicability.

Method Applicable Types Some Parameter None Parameter returns
Match<TIn, TOut> Maybe<TIn> Func<TIn, TOut> Func<TOut> TOut
MatchAsync<TIn, TOut> Maybe<TIn> Func<TIn, Task<TOut>> Func<Task<TOut>> Task<TOut>
Match<TIn> Maybe<TIn> Action<TIn> Action void
MatchAsync<TIn> Maybe<TIn> Func<TIn, Task> Func<Task> Task
MatchAsync<TIn, TOut> Task<Maybe<TIn>> Func<TIn, TOut> Func<TOut> Task<TOut>
MatchAsync<TIn, TOut> Task<Maybe<TIn>> Func<TIn, Task<TOut>> Func<Task<TOut>> Task<TOut>
MatchAsync<TIn> Task<Maybe<TIn>> Func<TIn, Task> Func<Task> Task

Example: Synchronous Matching

Maybe<int> maybeInt = Maybe.Some(5);
// maybeString is now a string containing "5".
string maybeString = maybeInt.Match(x => x.ToString(), () => "No Input");
// Suppose GetMaybeValue returns a Maybe<int> containing Some(10) or None.
Maybe<int> maybeValue = GetMaybeValue();

// Multiply the contained value by 2 if present; otherwise, return 0.
int result = maybeValue.Match(
    some: value => value * 2,
    none: () => 0);

Console.WriteLine(result);

Example: Asynchronous Matching

// Assume GetMaybeValueAsync returns a Task<Maybe<int>>.
Maybe<int> maybeValue = await GetMaybeValueAsync();

// Multiply the contained value by 3 asynchronously if present; otherwise, return 100.
int result = await maybeValue.MatchAsync(
    some: value => Task.FromResult(value * 3),
    none: Task.FromResult(100));

Console.WriteLine(result);

Example: Task-Based Asynchronous Matching

// Assume GetMaybeNameAsync returns Task<Maybe<string>>.
Task<Maybe<string>> maybeNameTask = GetMaybeNameAsync();

// Convert the contained name to uppercase if present; otherwise, use "UNKNOWN".
string result = await maybeNameTask.MatchAsync(
    some: name => name.ToUpper(),
    none: () => "UNKNOWN");

Console.WriteLine(result);

^ Back To Top

TapIfSome/TapIfNone

Clone this wiki locally