ZeroNull 1.0.1
dotnet add package ZeroNull --version 1.0.1
NuGet\Install-Package ZeroNull -Version 1.0.1
<PackageReference Include="ZeroNull" Version="1.0.1" />
<PackageVersion Include="ZeroNull" Version="1.0.1" />
<PackageReference Include="ZeroNull" />
paket add ZeroNull --version 1.0.1
#r "nuget: ZeroNull, 1.0.1"
#:package ZeroNull@1.0.1
#addin nuget:?package=ZeroNull&version=1.0.1
#tool nuget:?package=ZeroNull&version=1.0.1
ZeroNull
Explicit, composable control flow for C#
What is this library?
ZeroNull brings functional control-flow patterns to C#. It provides lightweight, immutable structs like Option<T>, Result<T, TError>, and Either<TLeft, TRight> that let you model absence, errors, and branching logic explicitly in your type signatures—without exceptions, nulls, or hidden side-effects.
What is it for?
- Replace
nullwithOption<T>– no moreNullReferenceException. - Replace exceptions with
Result<T, TError>– every failure is visible in the return type. - Model mutually exclusive choices with
Either<TLeft, TRight>– a value can be one of two possible types. - Chain operations safely with
Map,Bind, and LINQ query syntax. - Force exhaustive handling using
Match– you can’t forget to handle any case.
Who is it for?
- C# developers who want more reliable, self‑documenting code.
- Teams adopting functional programming techniques in a pragmatic way.
- Anyone tired of unexpected nulls or hidden exception paths.
Types & Features
| Type | Purpose | Key methods |
|---|---|---|
Option<T> |
A value that may be present (Some) or absent (None). |
Map, Bind, Match, ValueOr, LINQ support (Select, SelectMany) |
Result<T, TError> |
A value that is either successful (Ok) or carries an error (Error). |
Map, Bind, MapError, Match, LINQ support (Select, SelectMany) |
Either<TLeft, TRight> |
A value that can be one of two possible types. By convention, Left represents failure/alternative, Right represents success. |
IsLeft, IsRight, Left, Right, Match<br/><br/>Extension methods:<br/>• Select / SelectMany – LINQ query syntax support (works on the Right side)<br/>• Where – filter Right (with custom Left on false) or filter Left<br/>• MapLeft / MapRight – transform one side while keeping the other<br/>• BindLeft / BindRight – bind over one side to produce a new Either |
Either‑specific extension method details
| Method | Description |
|---|---|
Select(selector) |
Projects the Right value when the Either is in the Right state. Enables LINQ query syntax. |
SelectMany(binder, projector) |
Chains two Either operations that work on Right values. Short‑circuits on the first Left. |
Where(predicate, leftOnFalse) |
Filters the Right value; if the predicate fails, becomes Left(leftOnFalse). |
Where(leftPredicate, leftOnFalse) |
Filters the Left value; if the predicate fails, becomes Left(leftOnFalse). |
MapLeft(mapper) |
Applies mapper to the Left value if present; otherwise preserves the Right. |
MapRight(mapper) |
Applies mapper to the Right value if present; otherwise preserves the Left. |
BindLeft(binder) |
If Left, applies binder to produce a new Either (can change Left type, or even become Right). |
BindRight(binder) |
If Right, applies binder to produce a new Either (can change Right type, or even become Left). |
Core features:
- Immutable,
readonly struct– zero heap allocation in hot paths. - LINQ query comprehension (
from...select...). - Exhaustive pattern matching with
Match. - Async extensions (optional – available in a separate package).
Examples
Using Option<T> – avoid null
Option<User> FindUser(int id) =>
_database.ContainsKey(id)
? Option<User>.Some(_database[id])
: Option<User>.None();
// Chain safely without null checks
string greeting = FindUser(42)
.Map(user => $"Hello, {user.Name}!")
.Match(
some: msg => msg,
none: () => "User not found."
);
Using Result<T, TError> - explicit error handling
Result<int, string> ParseInt(string input) =>
int.TryParse(input, out int value)
? Result<int, string>.Ok(value)
: Result<int, string>.Error($"'{input}' is not a valid integer.");
Result<double, string> Compute(string input) =>
from number in ParseInt(input)
from reciprocal in number == 0
? Result<double, string>.Error("Cannot divide by zero")
: Result<double, string>.Ok(1.0 / number)
select reciprocal;
var outcome = Compute("42").Match(
ok: result => $"Result: {result}",
error: err => $"Error: {err}"
);
Using Either<TLeft, TRight> – represent a value that can be one of two types
// A function returning an Either type. Here, Left is an error message, Right is a valid User.
Either<string, User> GetUser(int id) =>
_database.ContainsKey(id)
? Either<string, User>.Right(_database[id])
: Either<string, User>.Left($"User {id} not found.");
var message = GetUser(42).Match(
left: error => $"Error: {error}",
right: user => $"Success: {user.Name}"
);
For error handling (using Select)
// A simple Either: Left = error message (string), Right = valid integer
Either<string, int> ParseInt(string input) =>
int.TryParse(input, out int value)
? Either<string, int>.Of(value)
: Either<string, int>.Of($"Invalid number: '{input}'");
// Use Select to transform the Right value
var result = ParseInt("42").Select(x => x * 2);
result.Match(
onLeft: err => Console.WriteLine($"Error: {err}"),
onRight: val => Console.WriteLine($"Doubled value: {val}")
);
// Output: Doubled value: 84
Chaining two operations with SelectMany (LINQ query syntax)
Either<string, int> Divide(int numerator, int denominator) =>
denominator == 0
? Either<string, int>.Of("Division by zero")
: Either<string, int>.Of(numerator / denominator);
var query = from a in ParseInt("10")
from b in ParseInt("2")
from result in Divide(a, b)
select result;
query.Match(
onLeft: err => Console.WriteLine($"Failed: {err}"),
onRight: val => Console.WriteLine($"Result: {val}")
);
// Output: Result: 5
If any step fails (e.g., ParseInt("abc")), the whole chain short‑circuits to Left.
Mixed operations – parsing and validation (using Where explicitly)
Since where clause in query syntax would need a parameterless Where, we use the explicit Where method after the query.
// Parse age, then ensure it's between 0 and 120
var ageResult = ParseInt("150")
.Select(age => age) // identity, just to show flow
.Where(age => age >= 0 && age <= 120, "Age must be between 0 and 120");
ageResult.Match(
onLeft: err => Console.WriteLine($"Validation error: {err}"),
onRight: age => Console.WriteLine($"Valid age: {age}")
);
// Output: Validation error: Age must be between 0 and 120
Roadmap / Future Features
This library is actively evolving. Below are the planned types and capabilities for future releases.
Phase 1: Core Enhancements (v2.0)
| Feature | Description |
|---|---|
| Validation Builder | Accumulate multiple errors during complex validation (like an applicative Result). |
NonEmptyList<T> |
A compile-time guarantee that a collection has at least one element. |
Try<T> |
A specialized Result<T, Exception> for safely executing exception‑throwing code. |
Unit type |
Represents a void return, enabling generic monadic code. |
| Async variants | OptionAsync<T>, ResultAsync<T,TError>, EitherAsync<TLeft,TRight> with LINQ support. |
| IEnumerable interop | .ToOption(), .ToResult(), .ToEnumerable() extensions. |
| Discriminated Union source generator | Generate exhaustive matching for sum types via a simple attribute. |
Phase 2: Advanced Control‑Flow Monads (v3.0)
| Type | Purpose |
|---|---|
Reader<TEnv, T> |
Share read‑only environment (config, context) implicitly through a computation. |
Writer<TLog, T> |
Accumulate a log (strings, metrics, etc.) alongside the main result. |
State<TState, T> |
Pure, functional state transitions without ref or out parameters. |
| Monad Transformers | Combine effects, e.g., OptionT<Result<...>> – computations that can fail, log, or read state simultaneously. |
Phase 3: Interoperability & Polish (v3.x / v4.0)
| Feature | Description |
|---|---|
| Task / ValueTask integration | Seamless conversion between Task<T> and Result<T, TError>. |
| Standard interfaces | Implement IEquatable<T>, IComparable<T>, and System.Text.Json converters for all types. |
| High‑performance optimizations | Ensure all types remain readonly struct with zero heap allocation. |
| Source‑generated matching | For DUs and sum types, generate exhaustive Match and Switch methods at compile time. |
Release Timeline
- v2.0 – Phase 1 features (Validation,
NonEmptyList,Try,Unit, async variants, interop). - v3.0 – Phase 2 monads (
Reader,Writer,State, transformers). - v3.x / v4.0 – Phase 3 interoperability and performance.
Contributions and feedback are welcome! If you have a specific feature request, please open an issue.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.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.