Options, results, and eithers
The Bodu.Functional railway primitives make the shape of an outcome part of a signature: Option<T> for a value that may be absent, Result<T> (and the void Result) for an operation that succeeds with a value or fails with a described error, and Either<TLeft, TRight> for a value that is exactly one of two things. All three are readonly structs - no allocation on the happy path - and compose through Map / Bind / Match chains instead of null checks and try/catch control flow.
A few contract points worth keeping in mind:
- Present values are never
null.Option<T>.Some,Result.Success<T>, and theEitherfactories all rejectnullwith ArgumentNullException. Absence is expressed by the type (None,Failure, or the other side), never by a smuggled null. - The lenient lift is explicit. Converting a
T?to anOption<T>implicitly mapsnulltoNone;Option.FromNullabledoes the same for nullable value types. Only the strictSomefactory throws. - Each type documents its
default.default(Option<T>)isNone;default(Result)/default(Result<T>)are failures carrying an empty ResultError;default(Either<,>)is an explicit uninitialized state - neitherIsLeftnorIsRight, and side-requiring operations throw InvalidOperationException (there is no honest side to default to). Result<T>owns the railway bias.Either<TLeft,TRight>is deliberately symmetric -MapLeft/MapRight, no unqualifiedMaporBind- so a disjoint union is never mistaken for a success/failure pipeline.
Pattern 1 - replacing null returns with Option<T>
using Bodu.Functional;
Option<Customer> FindCustomer(string id) =>
_store.TryGetValue(id, out var customer) ? Option.Some(customer) : Option.None<Customer>();
var greeting = FindCustomer("42")
.Map(c => c.DisplayName)
.Filter(name => name.Length > 0)
.Match(name => $"Hello, {name}!", () => "Hello, guest!");
Pattern 2 - a success/failure pipeline with Result<T>
using Bodu.Functional;
Result<Order> ParseOrder(string json) =>
TryParse(json, out var order)
? Result.Success(order)
: Result.Failure<Order>(ResultError.FromMessage("The payload is not a valid order.", code: "ORD001"));
var confirmation = ParseOrder(payload)
.Bind(Validate) // Result<Order> -> Result<Order>
.Map(o => o.ConfirmationNumber) // failure skips the projection
.TapError(e => _log.Warn(e.Message))
.GetValueOrDefault("(none)");
GetValueOrThrow() converts the failure rail back into an exception at the boundary of railway code: it throws InvalidOperationException carrying the error's message, with ResultError.Exception (when present) attached as InnerException - the captured exception is never rethrown directly, so its stack trace stays intact.
Pattern 3 - a true disjoint union with Either<TLeft,TRight>
using Bodu.Functional;
Either<StreetAddress, PostOfficeBox> destination = useBox
? Either<StreetAddress, PostOfficeBox>.Right(box)
: Either<StreetAddress, PostOfficeBox>.Left(address);
var label = destination.Match(
a => a.ToPostalLabel(),
b => $"PO Box {b.Number}");
Crossing between the types
Option<T>.ToResult(error) upgrades absence to a described failure, and Result<T>.ToOption() discards the error when only presence matters:
var result = FindCustomer("42").ToResult(ResultError.FromMessage("Unknown customer.", code: "CUST404"));
var option = ParseOrder(payload).ToOption();
Async composition
OptionAsyncExtensions and ResultAsyncExtensions provide Task-based MapAsync / BindAsync / MatchAsync (plus TapAsync / TapErrorAsync for results) over both Task<Option<T>> / Task<Result<T>> sources and sync sources with async selectors. Guards throw synchronously at the call site, selectors are never invoked on the empty/failure rail, and every await uses ConfigureAwait(false). The combinators take no CancellationToken - they perform no I/O of their own; cancellation belongs inside the caller's delegates.
var name = await LoadCustomerAsync(id) // Task<Option<Customer>>
.MapAsync(c => c.DisplayName)
.MatchAsync(n => n, () => "guest");
When you need memoization rather than outcome modelling, see Memoization; for collection-shaped absence (lookups that return collections), the choosing-a-collection guide covers the dictionary surfaces.