-
Notifications
You must be signed in to change notification settings - Fork 0
Maybe Type Extension Methods
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<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
(removesMaybe<TIn>
wrapper) -
Null Safety: Guarantees a
non-nullable
return (assuming substitute isn'tnull
) -
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.
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 outputMaybe<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
toMaybe<T>.Some
) simplify returning values in the examples.
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);
Inspired by LanguageExt, this library offers a more compact and user-friendly alternative with extensive examples and tutorials.
There is a very detailed YouTube channel with a dedicated video tutorial playlist for this library.
Star this repository and follow me on GitHub to stay informed about new releases and updates. Your support fuels this project's growth!
If my content adds value to your projects, consider supporting me via crypto.
- Bitcoin: bc1qlfljm9mysdtu064z5cf4yq4ddxgdfztgvghw3w
- USDT(TRC20): TJFME9tAnwdnhmqGHDDG5yCs617kyQDV39
Thank you for being part of this community—let’s build smarter, together