Bogoware.Monads 0.1.7

There is a newer version of this package available.
See the version list below for details.
dotnet add package Bogoware.Monads --version 0.1.7                
NuGet\Install-Package Bogoware.Monads -Version 0.1.7                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Bogoware.Monads" Version="0.1.7" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Bogoware.Monads --version 0.1.7                
#r "nuget: Bogoware.Monads, 0.1.7"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Bogoware.Monads as a Cake Addin
#addin nuget:?package=Bogoware.Monads&version=0.1.7

// Install Bogoware.Monads as a Cake Tool
#tool nuget:?package=Bogoware.Monads&version=0.1.7                

Bogoware Monads

Nuget

Yet another functional library for C#

Quickstart

Install from Nuget and enjoy!

dotnet add package Bogoware.Monads

Introduction to Monads

Monads are a powerful tool for modeling operations in a functional way, making them a cornerstone of functional programming. While we won't delve into a detailed explanation of monads and their inner workings in this document, there are numerous resources available online that approach the topic from different perspectives.

For the purpose of this introduction, we can consider monads as a type of "safe container" that encapsulates the result of an operation. They provide methods that enable manipulation of the result in a safe manner, ensuring that the operation executes only if it succeeds.

By employing monads, code can be protected from further processing in case of errors or missing data. Adopting a functional approach offers benefits such as increased readability, improved reasoning capabilities, and more robust and error-resistant code.

Functional Challenges in C#

C# offers good support for functional programming, but there are certain limitations that necessitate careful design decisions.

Bogoware Monads

This library provides two well-known monads: Result and Maybe monads (also referred to as Either, Optional, Option in other contexts):

The Result<T> monad is used to model operations that can fail.

The Maybe<T> monad is used to model operations that can either return a value or be empty.

Additionally, the library provides the Error abstract class, which complements the Result<T> monad and offers an ergonomic approach to error management at an application-wide scale.

Design Goals for Result<T>

The Result<T> monad is designed for modeling operations that can either fail or return a value. It is a generic type, with T representing the type of the value returned by the successful operation.

Result<T> provides a set of methods that facilitate chaining operations in a functional manner:

  • Map: Allows transformation of the value returned by the operation, representing the "happy" flow.
  • MapError: Allows transformation of the error returned by the operation, representing the "unhappy" flow.
  • Bind: Enables chaining of operations that return a Result<T>.
  • Match: Facilitates handling of the operation's result by providing separate paths for the "happy" and "unhappy" flows.
  • RecoverWith: Provides a way to recover from an error by returning a Result<T>
  • Ensure: Allows asserting a condition on the value returned by the operation.
  • ExecuteIfSuccess: Executes an action if the operation succeeds.
  • ExecuteIfFailure: Executes an action if the operation fails.

There are also some unsafe methods intended to support developers who are less familiar with the functional approach and may need to resort to a procedural style to achieve their goals. These methods should be used sparingly, as they deviate from the functional paradigm and make the code less robust, potentially leading to unexpected exceptions:

  • GetValueOrThrow: Extracts the value from the Result<T> monad.
  • GetErrorOrThrow: Extracts the error from the Result<T> monad.

By adhering to the Result<T> monad, code can be modeled in a more readable and reasoned manner. It also contributes to writing more robust code with reduced error-proneness.

Result Helper Methods

The Result class provides a set of helper methods that facilitate the creation of Result<T> instances or make the code more readable.

  • Result.Success: Creates a successful Result<T> instance with the specified value.
  • Result.Failure: Creates a failed Result<T> instance with the specified error.
  • Result.Ensure: Creates a successful Result<Unit> instance if the specified condition is true, otherwise creates a failed instance with the specified error.
  • Result.Bind: Creates a Result<T> instance from a delegate. This method is particularly useful when you need to start a chain of operations with a Result<T> instance and you like to have a consistent syntax for all the steps of the chain.

For example, instead of writing:

/// Publishes the project
public Result<Unit> Publish() {
    if (PublishingStatus == PublishingStatus.Published)
        return new InvalidOperationError("Already published");
    
    return ValidateCostComponents() // Note the explicit invocation of the method
        .Bind(ValidateTimingComponents)
        // ... more binding to validation methods
        .ExecuteIfSuccess(() => PublishingStatus = PublishingStatus.Published);
}

You can write:

/// Publishes the project
public Result<Unit> Publish() => Result
    .Ensure(PublishingStatus != PublishingStatus.Published, () => new InvalidOperationError("Already published")
    .Bind(ValidateCostComponents)
    .Bind(ValidateTimingComponents)
    // ... more binding to validation methods
    .ExecuteIfSuccess(() => PublishingStatus = PublishingStatus.Published);

Design Goals for Error

The Error class is used for modeling errors and works in conjunction with the Result<T> monad.

There are two types of errors:

  • LogicError: These errors are caused by application logic and should be programmatically handled. Examples include InvalidEmailError, InvalidPasswordError, InvalidUsernameError, etc.
  • RuntimeError: These errors are caused by external sources and are unrelated to domain logic. Examples include DatabaseError, NetworkError, FileSystemError, etc.

Distinguishing between LogicErrors and RuntimeErrors is important, as they require different handling approaches:

  • LogicErrors should be programmatically handled and can be safely reported to the user in case of a malformed request.
  • RuntimeErrors should be handled by the infrastructure and should not be reported to the user.

For example, in a typical ASP.NET Core application, LogicErrors can be handled by returning a BadRequest response to the client, while RuntimeErrors can be handled by returning an InternalServerError response.

Error Hierarchy Best Practices

Each application should model its own logic errors by deriving from a root class that represents the base class for all logic errors. The root class should derive from the LogicError class.

For different kinds of logic errors that can occur, the application should derive specific classes, each modeling a particular logic error and providing the necessary properties to describe the error.

In the following example, we model two logic errors: NotFoundError and InvalidOperationError:


public abstract class ApplicationError: LogicError
{
	
	public int ErrorCode { get; }

	protected ApplicationError(string message, int errorCode)
		: base(message)
	{
		ErrorCode = errorCode;
	}
}

public class NotFoundError : ApplicationError
{
	
	public string ResourceName { get; }
	public string ResourceId { get; }
	public NotFoundError(string message, int errorCode, string resourceName, string resourceId)
		: base(message, errorCode)
	{
		ResourceName = resourceName;
		ResourceId = resourceId;
	}
}

public class InvalidOperationError : ApplicationError
{
	
	public string OperationName { get; }
	public string Reason { get; }
	public InvalidOperationError(string message, int errorCode, string operationName, string reason)
		: base(message, errorCode)
	{
		OperationName = operationName;
		Reason = reason;
	}
}

As demonstrated in the project FluentValidationSample the FluentValidation library can be used to model validation errors.

In contrast to LogicErrors, RuntimeErrors are generated by the Result.Execute() methods to encapsulate exceptions thrown by the application.

Design Goals for Maybe<T>

Before discussing what can be achieved with the Maybe<T> monad, let's clarify that it is not intended as a replacement for Nullable<T>. This is mainly due to fundamental libraries, such as Entity Framework, relying on Nullable<T> to model class attributes, while support for structural types remains limited. A pragmatic approach involves using Nullable<T> for modeling class attributes and Maybe<T> for modeling return values and method parameters.

The advantage of using Maybe<T> over Nullable<T> is that Maybe<T> provides a set of methods that enable chaining operations in a functional manner. This becomes particularly useful when dealing with operations that can either return a value or be empty, such as querying a database.

The implicit conversion from Nullable<T> to Maybe<T> allows for lifting Nullable<T> values to Maybe<T> values and utilizing Maybe<T> methods for chaining operations.

Practical rule: Use Nullable<T> to model class attributes and Maybe<T> to model return values and method paramethers.

Product Compatible and additional computed target framework versions.
.NET net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net7.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
9.0.9 453 9/12/2024
9.0.9-alpha.0.1 47 9/12/2024
9.0.7 87 7/30/2024
9.0.5 383 2/15/2024
9.0.4 133 2/12/2024
9.0.3 102 2/12/2024
9.0.2 111 2/12/2024
9.0.1 119 2/12/2024 9.0.1 is deprecated.
9.0.0 117 2/12/2024 9.0.0 is deprecated.
8.0.2 115 1/31/2024
8.0.1 96 1/31/2024
8.0.0 218 12/12/2023
0.2.1 128 12/12/2023
0.2.0 501 9/21/2023
0.1.19 523 7/6/2023
0.1.18 211 7/5/2023
0.1.17 195 7/5/2023
0.1.16 221 7/1/2023
0.1.15 218 6/30/2023
0.1.14 242 6/30/2023
0.1.13 235 6/28/2023
0.1.12 205 6/28/2023
0.1.11 231 6/27/2023
0.1.10 221 6/27/2023
0.1.9 213 6/27/2023
0.1.8 203 6/27/2023
0.1.7 211 6/27/2023
0.1.6 201 6/26/2023
0.1.5 206 6/26/2023
0.1.4 216 6/24/2023
0.1.3 192 6/24/2023
0.1.2 203 6/23/2023
0.1.1 209 6/23/2023
0.1.0 209 6/23/2023
0.0.3 213 6/23/2023
0.0.3-alpha.0.30 79 4/27/2023
0.0.3-alpha.0.25 80 4/25/2023
0.0.3-alpha.0.23 86 4/24/2023
0.0.3-alpha.0.10 82 4/19/2023
0.0.3-alpha.0.9 78 4/19/2023
0.0.3-alpha.0.3 85 4/18/2023