Deprecated: Function get_magic_quotes_gpc() is deprecated in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 99

Deprecated: The each() function is deprecated. This message will be suppressed on further calls in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 619

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1169

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176
8000 GitHub - pimbrouwers/Danom: Option and Result types for C# to simplify functional programming.
Nothing Special   »   [go: up one dir, main page]

Skip to content

pimbrouwers/Danom

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Danom

NuGet Version build license aot net8.0 net6.0 netstandard2.1

Option and Result types for C# to simplify functional programming.


Danom provides Option and Result types for C#, inspired by F#. It’s designed to be easy to use, efficient, and compatible with existing C# codebases. These types offer a type-safe way to represent nullable values and expected errors, while also supporting a fluent API (e.g., map, bind) for chaining operations and value transformations.


Key Features

Design Goals

  • Provide a safe and expressive way to handle nullable values.
  • Efficient implementation to minimize overhead.
  • Enforce exhaustive matching.
  • Enhance functional programming in C#.
  • Opionated monads to encourage consistent use.
  • Support for both synchronous and asynchronous operations.
  • netstandard2.1 compatible.

Getting Started

Install the Danom NuGet package:

dotnet add package Danom

OR

PM>  Install-Package Danom

Looking for Validation or ASP.NET Core Integration?

Quick Start

using Danom;

// Options
Option.Some(5)
    .Map(x => x * 2)
    // ^--- transform the value
    .Bind(x => x > 5 ? Option.Some(x) : Option<int>.None())
    // ^--- chain another operation that returns an Option
    .Match(
        some: x => Console.WriteLine("Value: {0}", x),
        none: () => Console.WriteLine("No value"));

// Results
public Result<int, string> TryDivide(int numerator, int denominator) =>
    denominator == 0
        ? Result<int, string>.Error("Cannot divide by zero")
        : Result<int, string>.Ok(numerator / denominator);

TryDivide(10, 2)
    .Map(x => x + 1)
    // ^--- transform the value
    .Bind(x => TryDivide(x, 0))
    // ^--- chain another operation that returns a Result
    .MapError(e => $"Error occurred: {e}")
    // ^--- transform the error
    .Match(
        ok: x => Console.WriteLine("Result: {0}", x),
        error: e => Console.WriteLine("Error: {0}", e));

Option

Options have an underlying type and can optionally hold a value of that type. Options are a much safer way to handle nullable values, they virtually eliminate null reference exceptions. They also provide a fantastic means of reducing primitive congestion in your code.

Creating Options

var option = Option<int>.Some(5);

// or, with type inference
var optionInferred = Option.Some(5);

// or, with no value
var optionNone = Option<int>.NoneValue;

// also returns none
var optionNull = Option<object>.Some(default!);

Using Option

Options are commonly used when a operation might not return a value. For example, the method below tries to find a number in a list that satisfies a predicate. If the number is found, it is returned as a Some, otherwise, None is returned.

using Danom;

public Option<int> TryFind(IEnumerable<int> numbers, Func<int, bool> predicate) =>
    numbers.FirstOrDefault(predicate).ToOption();

With this method defined we can begin performing operations against the Option result:

using Danom;

IEnumerable<int> nums = [1,2,3];

// Exhaustive matching
TryFind(nums, x => x == 1)
    .Match(
        some: x => Console.WriteLine("Found: {0}", x),
        none: () => Console.WriteLine("Did not find number"));

// Mapping the value (i.e., I want to access the value)
Option<int> optionSum =
    TryFind(nums, x => x == 1)
        .Map(x => x + 1);

// Binding the option (i.e., when a nested operation also returns an Option)
Option<int> optionBindSum =
    TryFind(nums, x => x == 1)
        .Bind(num1 =>
            TryFind(nums, x => x == 2)
                .Map(num2 => num1 + num2));

// Handling "None"
Option<int> optionDefault =
    TryFind(nums, x => x == 4)
        .DefaultValue(99);

Option<int> optionDefaultWith =
    TryFind(nums, x => x == 4)
        .DefaultWith(() => 99); // useful if creating the value is expensive

Option<int> optionOrElse =
    TryFind(nums, x => x == 4)
        .OrElse(Option<int>.Some(99));

Option<int> optionOrElseWith =
    TryFind(nums, x => x == 4)
        .OrElseWith(() => Option<int>.Some(99)); // useful if creating the value is expensive

Result

Results are used to represent a success or failure outcome. They provide a more concrete way to manage the expected errors of an operation, then throwing exceptions. Especially in recoverable or reportable scenarios.

Creating Results

using Danom;

var result = Result<int, string>.Ok(5);

// or, with an error
var resultError = Result<int, string>.Error("An error occurred");

Using Results

Results are commonly used when an operation might not succeed, and you want to manage or report back the expected errors. For example:

Let's create a simple inline function to divide two numbers. If the denominator is zero, we want to return an error message.

using Danom;

Result<int, string> TryDivide(int numerator, int denominator) =>
    denominator == 0
        ? Result<int, string>.Error("Cannot divide by zero")
        : Result<int, string>.Ok(numerator / denominator);

With this method defined we can begin performing operations against the result:

using Danom;

// Exhaustive matching
TryDivide(10, 2)
    .Match(
        ok: x => Console.WriteLine("Result: {0}", x),
        error: e => Console.WriteLine("Error: {0}", e));

// Mapping the value
Result<int, string> resultSum =
    TryDivide(10, 2)
        .Map(x => x + 1);

// Binding the result (i.e., when a nested operation also returns a Result)
Result<int, string> resultBindSum =
    TryDivide(10, 2)
        .Bind(num1 =>
            TryDivide(20, 2)
                .Map(num2 =>
                    num1 + num2));

// Handling errors
Result<int, string> resultDefault =
    TryDivide(10, 0)
        .DefaultValue(99);

Result<int, string> resultDefaultWith =
    TryDivide(10, 0)
        .DefaultWith(() => 99); // useful if creating the value is expensive

Result<int, string> resultOrElse =
    TryDivide(10, 0)
        .OrElse(Result<int, string>.Ok(99));

Result<int, string> resultOrElseWith =
    TryDivide(10, 0)
        .OrElseWith(() =>
            Result<int, string>.Ok(99)); // useful if creating the value is expensive

Result Errors

Danom provides a built-in error type, ResultErrors, to simplify the creation of results with multiple errors. This type can be initialized with a single string, a collection of strings, or a key-value pair. It can be thought of as a domain-specific dictionary of string keys and N string values.

using Danom;

var resultOk = Result.Ok(5); // or, Result<int>.Ok(5);

var resultErrors =
    Result<int>.Error("An error occurred");

var resultErrorsMultiError =
    Result<int>.Error(["An error occurred", "Another error occurred"]);

var resultErrorsKeyed =
    Result<int>.Error("error-key", "An error occurred");

var resultErrorsKeyedMulti =
    Result<int>.Error("error-key", ["An error occurred", "Another error occurred"]);

Unit

Danom provides a Unit type to represent the absence of a value. This type is useful in functional programming as a way to represent the absence of a meaningful value. It acts as a placeholder when a function needs to return something, but there is no actual data to return—similar to void in C#, but as a real type that can be used in generic code, composed in monads like Option and Result, or passed as a value. This enables more consistent and expressive APIs, especially when working with functional patterns, pipelines, or asynchronous workflows where a type is always required.

using Danom;

void Log(string message) => Console.WriteLine(message);

// Convert an Action to a Func that returns Unit
Func<string, Unit> logFunc = Log.ToUnitFunc();

// Use in a functional pipeline
Option<string> maybeMessage = Option.Some("Hello, world!");

maybeMessage
    // Logs the message and returns Option<Unit>
    .Map(logFunc);

Procedural Programming

Inevitably you'll need to interact with these functional types in a procedural way. Both Option and Result provide a TryGet method to retrieve the underlying value. This method will return a bool indicating whether the value was successfully retrieved and the value itself as an output parameter.

Option TryGet

using Danom;

var option = Option<int>.Some(5);

if (option.TryGet(out var value)) {
    Console.WriteLine("Value: {0}", value);
}
else {
    Console.WriteLine("No value");
}

Result TryGet and TryGetError

using Danom;

var result = Result<int, string>.Ok(5);

if (result.TryGet(out var value)) {
    Console.WriteLine("Result: {0}", value);
}
else if (result.TryGetError(out var error)) {
    Console.WriteLine("Error: {0}", error);
}
else {
    Console.WriteLine("No value or error");
}

String Parsing

Most applications will at some point need to parse strings into primitives and value types. This is especially true when working with external data sources.

Option provides a natural mechanism to handle the case where the string cannot be parsed. The "TryParse" API is provided to simplify the process of parsing strings into .NET primitives and value types.

using Danom;

// a common pattern
var x = int.TryParse("123", out var y) ? Option<int>.Some(y) : Option<int>.NoneValue;

// or, more simply using the TryParse API
var myInt = intOption.TryParse("123"); // -> Some(123)
var myDouble = doubleOption.TryParse("123.45"); // -> Some(123.45)
var myBool = boolOption.TryParse("true"); // -> Some(true)

// if the string cannot be parsed
var myIntNone = intOption.TryParse("danom"); // -> None
var myDoubleNone = doubleOption.TryParse("danom"); // -> None
var myBoolNone = boolOption.TryParse("danom"); // -> None

// null strings are treated as None
var myIntNull = intOption.TryParse(null); // -> None

The full API is below:

public static class boolOption {
    public static Option<bool> TryParse(string? x); }

public static class byteOption {
    public static Option<byte> TryParse(string? x, IFormatProvider? provider = null); }

public static class shortOption {
    public static Option<short> TryParse(string? x, IFormatProvider? provider = null);
    public static Option<short> TryParse(string? x); }

public static class intOption {
    public static Option<int> TryParse(string? x, IFormatProvider? provider = null);
    public static Option<int> TryParse(string? x); }

public static class longOption {
    public static Option<long> TryParse(string? x, IFormatProvider? provider = null);
    public static Option<long> TryParse(string? x); }

public static class decimalOption {
    public static Option<decimal> TryParse(string? x, IFormatProvider? provider = null);
    public static Option<decimal> TryParse(string? x); }

public static class doubleOption {
    public static Option<double> TryParse(string? x, IFormatProvider? provider = null);
    public static Option<double> TryParse(string? x); }

public static class floatOption {
    public static Option<float> TryParse(string? x, IFormatProvider? provider = null);
    public static Option<float> TryParse(string? x); }

public static class GuidOption {
    public static Option<Guid> TryParse(string? x, IFormatProvider? provider = null);
    public static Option<Guid> TryParse(string? x);
    public static Option<Guid> TryParseExact(string? x, string? format); }

public static class DateTimeOffsetOption {
    public static Option<DateTimeOffset> TryParse(string? x, IFormatProvider? provider = null);
    public static Option<DateTimeOffset> TryParse(string? x);
    public static Option<DateTimeOffset> TryParseExact(string? x, string? format, IFormatProvider? provider = null, DateTimeStyles dateTimeStyles = DateTimeStyles.None); }

public static class DateTimeOption {
    public static Option<DateTime> TryParse(string? x, IFormatProvider? provider = null);
    public static Option<DateTime> TryParse(string? x);
    public static Option<DateTime> TryParseExact(string? x, string? format, IFormatProvider? provider = null, DateTimeStyles dateTimeStyles = DateTimeStyles.None); }

public static class DateOnlyOption {
    public static Option<DateOnly> TryParse(string? x, IFormatProvider? provider = null);
    public static Option<DateOnly> TryParse(string? x);
    public static Option<DateOnly> TryParseExact(string? x, string? format, IFormatProvider? provider = null, DateTimeStyles dateTimeStyles = DateTimeStyles.None); }

public static class TimeOnlyOption {
    public static Option<TimeOnly> TryParse(string? x, IFormatProvider? provider = null);
    public static Option<TimeOnly> TryParse(string? x);
    public static Option<TimeOnly> TryParseExact(string? x, string? format, IFormatProvider? provider = null, DateTimeStyles dateTimeStyles = DateTimeStyles.None); }

public static class TimeSpanOption {
    public static Option<TimeSpan> TryParse(string? x, IFormatProvider? provider = null);
    public static Option<TimeSpan> TryParse(string? x);
    public static Option<TimeSpan> TryParseExact(string? x, string? format, IFormatProvider? provider = null); }

public static class EnumOption {
    public static Option<TEnum> TryParse<TEnum>(string? x) where TEnum : struct; }

Input Validation

One of the places the Result type really shines is input validation. It's a natural step in most workflows to validate input data before processing it, and the Result type is a great way to handle this. The Danom.Validation library provides an API for defining validation rules and checking input data against those rules, returning a Result<T, ResultErrors> that contains either the validated data or an error message.

Documentation can be found here.

Integrations

Since Danom introduces types that are most commonly found in your model and business logic layers, external integrations are not only inevitable but required to provide a seamless experience when building applications.

These are completely optional, but provide a great way to integrate Danom with your codebase.

ASP.NET Core MVC & Razor Pages Integration

Danom is integrated with ASP.NET Core MVC (and Razor Pages) via Danom.Mvc. This library provides a set of utilities to help integrate the core types with common tasks in ASP.NET Core MVC applications.

ASP.NET Core Minimal API Integration

Danom is integrated with ASP.NET Core Minimal API via Danom.MinimalApi. This library provides a set of utilities to help integrate the core types with common tasks in ASP.NET Core Minimal API applications.

Find a bug?

There's an issue for that.

License

Licensed under MIT.

Contributors 2

  •  
  •  
0