February 26, 2026

Functional Programming & Monads in C#

🧠 Introduction

Functional programming (FP) is a paradigm focused on pure functions, immutability, and composable transformations. Modern C# supports many FP concepts, making it possible to write expressive, predictable, and safe code without leaving the .NET ecosystem. This guide walks through the core FP ideas, monads, immutability, partial functions, and practical C# examples — all in clean Markdown for your blog.

1. Core Concepts of Functional Programming

1.1 First-Class & Higher-Order Functions

In FP, functions are values. You can:

  • Functions are treated as values: you can pass them around, store them in variables, and return them from other functions.
  • Higher-order functions take other functions as parameters or return them.

Example:

Func<int, int, int> add = (x, y) => x + y;

// Higher-order function: takes a function as input
int ApplyOperation(int a, int b, Func<int, int, int> operation)
{
    return operation(a, b);
}

var result = ApplyOperation(5, 3, add); // result = 8

1.2 Immutability

  • FP emphasizes immutable data, once created, values don’t change.
  • In C#, you can use readonly fields, record types, or avoid mutating collections
  • Data does not change after creation. Instead, you create new values.

Example using records:

record Person(string Name, int Age);
...
var p1 = new Person("Isabel", 30);
// Instead of mutating, create a new instance
var p2 = p1 with { Age = 31 }; // p2 is a new object

1.3 Pure Functions

  • A pure function always returns the same output for the same input and has no side effects (like modifying global state or I/O).
  • This makes code predictable and testable.

Pure:

int Square(int x) => x * x; // Pure function

Not pure:

int counter = 0;
int Increment() => ++counter; // Not pure (depends on external state)

1.4 Function Composition

Combine small functions into larger ones.

Func<int, int> doubleIt = x => x * 2;
Func<int, int> squareIt = x => x * x;

// Compose manually
Func<int, int> doubleThenSquare = x => squareIt(doubleIt(x));

var result = doubleThenSquare(3); // (3*2)^2 = 36

1.5 Declarative Style (LINQ)

  • FP favors describing what to do rather than how to do it.
  • LINQ is a great example of declarative programming in C#.
var numbers = new[] { 1, 2, 3, 4, 5 };

// Declarative: filter and transform
var evensSquared = numbers
    .Where(n => n % 2 == 0)
    .Select(n => n * n);

foreach (var n in evensSquared)
    Console.WriteLine(n); // Output: 4, 16

1.6 Lazy Evaluation

  • FP often defers computation until needed.
  • In C#, IEnumerable with yield return or LINQ queries are (mostly) lazily evaluated.
IEnumerable<int> Squares()
{
    int i = 1;
    while (true)
        yield return i * i++;
}

🔑 Summary Functional programming in C# revolves around:

  • Treating functions as values
  • Using immutability
  • Writing pure functions
  • Composing small functions
  • Favoring declarative style (LINQ)
  • Leveraging lazy evaluation

2. Monads in C#

2.1 What Is a Monad?

A monad is a design pattern from functional programming that:

  • Wraps a value in a context (e.g., "maybe this value exists", "this value is asynchronous", "this value is a sequence").
  • Provides a way to chain operations on that value without breaking the context.
  • Ensures consistent handling of side effects (nulls, errors, async, logging, etc.). Think of it as a container + rules for chaining.

A monad must support:

  • Return/Unit → wrap a value (handle the cases where the function has no valid value, eg null pointer, ...)
  • Bind → (flatMap / SelectMany in LINQ) Chain operations (so it can be chained through functions) and everything works even if the value is null/none, etc. Make functions act like pure functions by handling the bad cases through the Monad.

2.2 Common Monads in C#

Monad Meaning
Task<T> Asynchronous computation
IEnumerable<T> Sequence computation
Nullable<T> Optional values
Result<T> Success/failure pipeline

2.3.1 Example: Option Monad

public class Option<T>
{
    private readonly T _value;
    public bool HasValue { get; }

    private Option() { HasValue = false; }
    private Option(T value) { _value = value; HasValue = true; }

    public static Option<T> Some(T value) => new(value);
    public static Option<T> None() => new();

    public Option<TResult> Bind<TResult>(Func<T, Option<TResult>> f)
        => HasValue ? f(_value) : Option<TResult>.None();

    public Option<TResult> Map<TResult>(Func<T, TResult> f)
        => HasValue ? Option<TResult>.Some(f(_value)) : Option<TResult>.None();

    // Extract the value with a fallback
    public T GetValueOrDefault(T fallback = default)
        => HasValue ? _value : fallback;
}

2.3.2 Using this Monad

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

var result = maybeNumber
    .Bind(x => Option<int>.Some(x * 2))
    .Bind(x => Option<int>.Some(x + 10));

// result = Some(20)

This avoids null checks everywhere — the monad handles the "no value" case.

2.4.1 Task Monad (async) Example

Task in C# is essentially a monad:

  • Task.FromResult(value) → wraps a value.
  • await / ContinueWith → bind operations.
async Task<int> DoubleAsync(int x) => x * 2;

var result = await Task.FromResult(5)
    .ContinueWith(t => DoubleAsync(t.Result))
    .Unwrap();

Here Task ensures async chaining without manually handling threads.

2.5.1 LINQ Query Syntax (Enumerable Monad)

LINQ’s SelectMany is the bind operation for sequences.

var numbers = new[] { 1, 2, 3 };
var doubled = from n in numbers
              from m in new[] { n * 2 }
              select m;

// doubled = {2, 4, 6}

LINQ query comprehension is syntactic sugar for monadic chaining.

⚡ Summary In C#:

  • Option (or Nullable) → Maybe Monad
  • Task → Async Monad
  • IEnumerable → Sequence Monad
  • LINQ query syntax → Monadic chaining (SelectMany) 👉 The purpose: monads let you build pipelines of computation while hiding the messy details of context management (nulls, async, errors, etc.).

🧩 Scenario Imagine a WPF app where the ViewModel fetches a user profile. Sometimes the profile data might be missing (e.g., network error, no record). Instead of sprinkling if (profile != null) everywhere, we’ll use an Option Monad.

  1. Define a Simple Option<T> Monad. See the class above

3. Monads in MVVM (Practical Example)

public class ProfileViewModel : ObservableObject
{
    private Option<UserProfile> _profile = Option<UserProfile>.None();

    public string DisplayName =>
        _profile.Map(p => $"Welcome, {p.Name}!").GetValueOrDefault("Guest");

    public async Task LoadAsync()
    {
        var fetched = await FetchProfileAsync();
        _profile = fetched is null
            ? Option<UserProfile>.None()
            : Option<UserProfile>.Some(fetched);

        OnPropertyChanged(nameof(DisplayName));
    }
}

4. Partial Functions

4.1 What Is a Partial Function?

A function that is not defined for all inputs. Example:

int Divide(int x, int y) => x / y; // undefined when y ** 0

4.2 Partial Application (Different Concept)

Fixing some arguments of a function.

Func<int, int, int> add = (x, y) => x + y;
Func<int, int> addFive = y => add(5, y);

4.3 Lifting a Partial Function into a Monad

Option<int> SafeDivide(int x, int y)
{
    if (y == 0) return Option<int>.None();
    return Option<int>.Some(x / y);
}

6. Immutability in Functional Programming

6.1 Why It Matters

Immutability provides:

  • predictability
  • pure functions
  • thread safety
  • referential transparency
  • easier debugging
  • safe composition
  • undo/redo and time-travel debugging

6.2 Example: Immutable State

record AppState(int Count);

var state1 = new AppState(0);
var state2 = state1 with { Count = state1.Count + 1 };

7. Using Monads for Unit-Safe Values

7.1 Example: Length Monad

public class Length
{
    private readonly double _meters;

    private Length(double meters) => _meters = meters;

    public static Length FromMeters(double m) => new(m);
    public static Length FromKilometers(double km) => new(km * 1000);

    public Length Map(Func<double, double> f) => new(f(_meters));
    public Length Bind(Func<double, Length> f) => f(_meters);

    public double ToMeters() => _meters;
}

8. Practice Quiz (Optional)

1. What is a pure function?

Answer: A function with no side effects that always returns the same output for the same input.

2. Which LINQ method corresponds to monadic bind?

Answer: Select Many

3. What is the purpose of a monad?

Answer: To wrap values in context and allow safe, composable operations.

4. Which C# type is an example of a monad?

Answer: Task

5. How does Option help in MVVM?

Answer: Eliminates null checks by encapsulating optional values.

9. Summary

Functional programming in C# gives you:

  • safer code
  • predictable behavior
  • composable pipelines
  • easier testing
  • fewer bugs

Monads, immutability, and pure functions work together to create a clean, expressive, and maintainable architecture.

Copied and edited from a Copilot chat

No comments: