Skip to content

Maybe Type

Zeid Youssefzadeh edited this page Feb 25, 2025 · 12 revisions

Overview

The concepts such as - the "Optional Nomad" and using Option<T> or Maybe<T> instead of primitive nullable types like int? - are rooted in functional programming paradigms and best practices for writing robust, maintainable code, and It represents the presence or absence of a value explicitly, avoiding the pitfalls of null.

Why Use Maybe<T> Instead of Nullable?

Here are some disadvantages of Using Nullable:

  • Null Reference Exceptions: Forgetting to check for null can cause runtime crashes.
  • Ambiguity: null can signify missing data, errors, or uninitialized values, making code harder to understand.
  • Poor Readability: Excessive null checks clutter code and reduce readability.
  • Violates Functional Principles: null contradicts functional programming's emphasis on explicitness and immutability.

Here are some advantages of Using Maybe<T>:

  • Explicit Handling of Missing Values: Forces developers to handle the absence of a value explicitly.
  • Avoids Magic Numbers: Eliminates the need for special values (e.g., -1) to represent missing data which is error-prone and unclear.
  • Type Safety: Ensures compile-time handling of absent values, reducing runtime errors.
  • Functional Composition: Supports operations like Map and Bind, enabling chaining without null concerns.

Code examples of Maybe

This is a simple example of how to create the Maybe<T> instance:

 // Creating a Maybe with a value
 Maybe<int> someValue = Maybe.Some(10);
 Console.WriteLine(someValue.IsSome); // Output: true
 Console.WriteLine(someValue.IsNone); // Output: false

 // Creating a Maybe with a value implicitly
 Maybe<int> someValue2 = 10;
 Console.WriteLine(someValue2.IsSome); // Output: true
 Console.WriteLine(someValue2.IsNone); // Output: false

 // Creating a Maybe in the 'None' state
 Maybe<string> noneValue = Maybe.None<string>();
 Console.WriteLine(noneValue.IsSome); // Output: false
 Console.WriteLine(noneValue.IsNone); // Output: true

^ Back To Top

Extension Methods of Maybe<T> with Examples

The value of Maybe<T> instance is not accessible directly, therefore extension methods are provided to help interacting with the Maybe<T> type. This section provides a brief overview of each extension method available in the ZeidLab.ToolBox.Options namespace. For a list of extension methods that support both synchronous and asynchronous operations, please refer to the Maybe Extension Methods section.

^ Back To Top

ToSome

Converts a non-null object to a Maybe<TIn> instance in the 'Some' state.

var maybeInt = 10.ToSome(); // maybeInt is now a Maybe<int> with the 'Some' state.

^ Back To Top

ToNone

Creates a Maybe<TIn> instance in the None state, regardless of the input value.

var maybeInt = ((int?)null).ToNone(); // maybeInt is now a Maybe<int> with the 'None' state.

^ Back To Top

If

Checks whether a Maybe<TIn> value both exists and satisfies a condition. Designed as a terminal operation for validation scenarios.

Key Behavior:

  • Conditional Check: Only evaluates the predicate when the Maybe<TIn> instance is Some
  • Immediate Failure: Returns false without predicate evaluation if the Maybe<TIn> instance is None
  • Chain Termination: Cannot be chained further (must be last operation in a sequence)
Maybe<int> maybeInt1 = Maybe.Some(10);
bool result1 = maybeInt1.If(x => x > 5); // result1 is true.

Maybe<int> maybeInt2 = Maybe.None<int>();
bool result2 = maybeInt2.If(x => x > 5); // result2 is false.

Notes:

  • Think of this as a combination of "has value" and "value meets requirement"
  • For pure existence checks, without conditions, use Maybe<TIn>.IsSome instead

^ Back To Top

Where

whenever there is an instance of IEnumerable<Maybe<TIn>> you can use this method. Filters a collection of Maybe<TIn> values to include only Maybe<TIn>.IsSome instances that satisfy a specified condition. Automatically excludes Maybe<TIn>.IsNone values and applies the predicate to the unwrapped Maybe<TIn>.IsSome values.

Key Behavior:

  • None Exclusion: All Maybe<TIn>.IsNone entries are omitted from results
  • Predicate Filtering: Only Maybe<TIn>.IsSome values matching the condition are retained
  • LINQ-like Syntax: Designed for parity with standard collection filtering patterns
var maybeList = new List<Maybe<int>> 
{
    Maybe.Some(5),
    Maybe.None<int>(),
    Maybe.Some(10)
};
// filtered contains only Maybe.Some(10).
var filtered = maybeList.Where(x => x > 5); 

Notes:

  • Think of this as combination filter that:

    1. Removes Maybe<TIn>.IsNone values
    2. Applies your condition to remaining Maybe<TIn>.IsSome values
  • Returns IEnumerable<Maybe<TIn>> containing only qualifying Maybe<TIn>.IsSome instances

^ Back To Top

Filter

Applies a condition to a single Maybe<TIn> instance. Returns the original Maybe<TIn>.IsSome instance if it satisfies the predicate, otherwise returns Maybe<TIn>.IsNone.

Key Behavior:

  • Single-Value Operation: Works on individual Maybe<TIn> instances
  • State Transition: Converts Maybe<TIn>.IsSomeMaybe<TIn>.IsNone if condition fails
  • No Unwrapping: Maintains Maybe<TIn> type throughout
var maybe1 = Maybe.Some(42);
var maybe2 = Maybe.Some(10);
// Returns Maybe.Some(42)
var filtered1 = maybe1.Filter(value => value > 10); 
// Returns Maybe.None<int>()
var filtered1 = maybe1.Filter(value => value > 10); 

^ Back To Top

Filter vs. Where

Feature Filter Where
Operates On SingleMaybe<TIn> IEnumerable<Maybe<TIn>>
Return Type Maybe<TIn> IEnumerable<Maybe<TIn>>
None Handling Converts Some → None Removes None entries
Typical Use Case Conditionally invalidate a value Filter collections of Maybe's

^ Back To Top

Flatten

Converts a sequence of Maybe<TIn> values into an IEnumerable<TIn>, handling Maybe<TIn>.IsNone cases through three strategies as three overloads.

Key Characteristics:

  • Type Conversion: Always returns IEnumerable<TIn> (never contains Maybe<TIn> instances)
  • Null Safety: Handles all Maybe<TIn>.IsNone cases explicitly through exclusion or substitution
  • Lazy Evaluation: All versions preserve LINQ-like deferred execution

Choosing an Overload:

graph TD
    A[Need to handle Nones?] --> |No| B[Use Overload 1]
    A --> |Yes| C{Substitution Type?}
    C --> |Fixed Value| D[Use Overload 2]
    C --> |Dynamic Value| E[Use Overload 3]
Loading

Overload 1: Basic Flattening (Excludes Maybe<TIn>.IsNone)

IEnumerable<TIn> Flatten<TIn>(this IEnumerable<Maybe<TIn>> self)
  • Behavior: Returns only the values from Maybe<TIn>.IsSome instances, excluding all Maybe<TIn>.IsNone entries
  • Use Case: When you want to discard missing values entirely.

Example:

var maybeList = new List<Maybe<int>> 
{
    Maybe.Some(1),
    Maybe.<int>None(),
    Maybe.Some(3)
};
var result = maybeList.Flatten(); // Output: [1, 3]

Overload 2: Static Substitute Value

IEnumerable<TIn> Flatten<TIn>(this IEnumerable<Maybe<TIn>> self, TIn substitute)
  • Behavior: Replaces Maybe<TIn>.IsNone entries with a specified default value
  • Use Case: When missing values should be represented with a known constant

Example:

var maybeList = new List<Maybe<int>> 
{
    Maybe.Some(1),
    Maybe.None<int>(), 
    Maybe.Some(3)
};
var result = maybeList.Flatten(0); // Output: [1, 0, 3]

Overload 3: Dynamic Substitute Generator

IEnumerable<TIn> Flatten<TIn>(this IEnumerable<Maybe<TIn>> self, Func<TIn> substitute)
  • Behavior: Uses a function to generate substitutes for Maybe<TIn>.IsNone entries
  • Use Case: When replacement values need dynamic computation (e.g., time-sensitive values)

Example:

var maybeList = new List<Maybe<int>> 
{
    Maybe.Some(1),
    Maybe.None<int>(),
    Maybe.Some(3)
};
var result = maybeList.Flatten(() => 0); // Output: [1, 0, 3]

^ Back To Top

TapIfSome / TapIfSomeAsync

Executes a synchronous side effect of Action<TIn> only if the Maybe<TIn> contains a value. Leaves the original Maybe<TIn> instance unchanged. Do not use this for asynchronous side effects.

Key Behavior:

  • Conditional Execution: Action runs if it is an instance of Maybe<TIn>.IsSome
  • No Value Return: Purely for synchronous side effects (synchronous logging,synchronous updates)
  • Chain-Friendly: Returns original Maybe<TIn> for continued processing

Example:

Maybe<int> maybeInt = Maybe.Some(10);
maybeInt.TapIfSome(x => Console.WriteLine(x));  // Output: 10

// Chaining example
Maybe.Some(5)
    .TapIfSome(x => SaveToDatabase(x))  // Side effect
    .Bind(x => TransformValue(x));     // Continue processing

^ Back To Top

TapIfNone / TapIfNoneAsync

Executes a parameterless action only when the instance is of type Maybe<TIn>.IsNone. Designed for handling missing values.

Key Behavior:

  • Absence Handler: Executes the provided action if the Maybe<TIn> instance is Maybe<TIn>.IsNone.
  • No Context Access: Action doesn't receive any value parameter.
  • Chain Position: Typically used mid-chain to handle failures.
Maybe<int> maybeNone = Maybe.None<int>();
maybeNone.TapIfNone(() => Console.WriteLine("No value."));  // Output: "No value."

// Practical usage
GetUserInput()
    .TapIfNone(() => ShowError("Input required"))  // UI feedback
    .Filter(IsValidEmail); // Continue validation

^ Back To Top

ValueOrThrow

Unwraps a Maybe<TIn> with no fallback, throwing an exception if the value is missing. Use cautiously.

Key Behavior:

  • Assumed Presence: Throws InvalidOperationException on Maybe<TIn>.IsNone.
  • Last-Resort Unwrap: Prefer Reduce() for safer alternatives.
  • Diagnostic Aid: Useful in contexts where Maybe<TIn>.IsNone indicates a code bug.

When to Use:

graph LR
    A[Should this code path ever receive `Maybe<TIn>.IsNone`?] --> |No| B[Use `ValueOrThrow`]
    A --> |Yes| C[Use `Reduce`]
Loading

Example:

Maybe<int> maybeInt = Maybe.Some(10);
int value = maybeInt.ValueOrThrow();  // Returns 10

// Risky usage - avoid without explicit checks
var dangerous = Maybe.None<int>().ValueOrThrow();  
// Throws InvalidOperationException

This structure maintains technical precision while emphasizing each method's distinct role:

  • TapIfSome/TapIfNone: Side-effect management
  • ValueOrThrow: Assertive unwrapping with safety tradeoffs
  • Reduce: Unwrapping with fallback options

^ Back To Top

Clone this wiki locally