Functional C# with Style: Mapping, Binding, and Chaining

Introduction

In modern C# development, we often find ourselves lost in “If-Else” chains, nested blocks of null checks and error handling which can obscure the actual business logic. Inspired by Rust and functional programming, the Esox.SharpAndRusty library introduces a way to write cleaner, more resilient code, using Railroad Oriented Programming.

Here is how you can transform your C# code into a fluent, type-safe pipeline.

Getting Rid of “If-Chains”

Tradition code can often look like staircase of if statements. If any steps fails, you return early or throw an exception as the case may be. This “Happy Path” is buried under boilerplate. Here is an example:

public string GetUserDisplay(int id) {
    var user = database.Find(id);
    if (user != null) {
        var profile = service.LoadProfile(user);
        if (profile != null) {
            return $"User: {profile.DisplayName}";
        }
    }
    return "Unknown";
}

The Solution: Mapping and Binding

Using Map and `Bind`, we treat the operation as a flow.

  • Map transforms the value inside a Result or Option if it is successful.
  • Bind is used when the transformation itself could fail, returning another Result.
public string GetUserDisplay(int id) =>
    database.Find(id) // Returns Option<User>
        .Map(user => service.LoadProfile(user))
        .Match(
            some: profile => $"User: {profile.DisplayName}",
            none: () => "Unknown"
        );

Railroad Oriented Programming

Try to think of your logic as a two-track railroad. One track is the Success Track, and the other is the Failure Track.

When you use Bind(), you are moving along the Success Track. If a function returns an Err, the switch is flipped and the execution jumps to the Failure Track, bypassing all subsequent logic until it reaches an error handler.

Let’s have a look at a concrete example:

public Result<Invoice, Error> ProcessOrder(OrderRequest request) =>
    ValidateRequest(request)
        .Bind(req => CalculateTax(req))
        .Bind(order => SubmitToGateway(order))
        .Map(receipt => GenerateInvoice(receipt));

If CalculateTax() fails, SubmitToGateway() and GenerateInvoice() are never even called. The error slides down the failure track to the caller.

Side Effects without Breaking the Chain

Sometimes you need to perform an action – like logging or analytics – that does not change the data. Normally, this requires breaking your fluent chain to write a line of code.

Inspect() and Tap() allow you to perform these side effects, while keeping the track moving:

  • Inspect: Run an action on the success value.
  • InspectErr: Run an action on the success value.
  • Tap: Executes actions on both success and failure, again without transforming the result.

A concrete example:

var result = ParseInt(input)
    .Inspect(val => Logger.Info($"Processing number: {val}"))
    .Bind(val => Divide(100, val))
    .InspectErr(err => Logger.Error($"Failed to divide: {err}"));

The Unit Type: Handling Void Elegantly

In standard C#, void is a special case that does not play well with generics or functional chains. In SharpAndRusty, we use the Unit type to represent “no value” while still maintaining the Result context.

This allows you to chain methods, that do something, but return nothing, for example saving to a file or a database, without losing the ability to track errors. Here is an example:

public Result<Unit, Error> SaveToDatabase(User user) {
    try {
        db.Save(user);
        return Result<Unit, Error>.Ok(Unit.Default);
    } catch (Exception ex) {
        return Result<Unit, Error>.Err(Error.FromException(ex));
    }
}

var finalResult = ValidateUser(user)
    .Bind(u => SaveToDatabase(u)) // Returns Result<Unit, Error>
    .Map(_ => "User saved successfully!");

A small cheat sheet

MethodPurposeScenario
MapTransforms the value inside.You have a successful result and want to change its type or value (e.g., UserDisplayName).
BindChains a new failable operation.The next step could also fail (e.g., UserLoadProfileFromDB). Prevents nested Results.
MatchResolves the flow.The final step: define what to return for both the Success and Failure paths.
InspectExecutes a side effect.You want to log a value or trigger an event without changing the data in the pipeline.
TapRuns actions on both tracks.Ideal for logging “Processing started” or “Operation finished,” regardless of the outcome.
UnitRepresents “void.”Used when a method performs an action (like saving to a DB) but doesn’t return a value.

Conclusion

Transitioning from imperative “if-else” chains to Railroad Oriented Programming is more than just a syntactic change; it is a shift in how we reason about the reliability of our software. By treating our logic as a continuous flow rather than a series of hurdles, we make the “Happy Path” the primary focus of our code while ensuring that error are handled with the same level of type-safety as our data.

Why this matters?

Adopting libraries like Esox.SharpAndRusty provides three (3!) immediate benefits to virtually any C# codebase:

  • Readability: The business logic reads like a story from top to bottom, unencumbered by repetitive null checks or try-catch blocks. Mind you, exceptions are still needed for non-recoverable errors, like a stack overflow or an out of memory exception.
  • Safety: By replacing null with Option and exceptions with Result, you force the compiler to remind you of potential failure states, reducing runtime crashes.
  • Maintainability: Side effects are isolated using Inspect() and Tap(), and the Unit type ensures that even “void” operations remain first-class citizens in your pipeline.

Functional C# does not require you to abandon everything you know about the language. Instead, it invites you to use tools, like Mapping, Binding, and Chaining to write code that is as expressive as it is robust. By switching the tracks from nested statements to a linear pipeline, you spend less time debugging the “staircase of doom” and more time delivering value.

The Code Nomad
The Code Nomad
Articles: 165

Leave a Reply

Your email address will not be published. Required fields are marked *