Functional Programming in C#: Advanced Techniques and Patterns

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Functional Programming in C#: From Syntax to Architecture

\n\n

Modern C# has long moved beyond being a purely object-oriented language. Starting from version 3.0 (LINQ) and ending with the latest .NET releases, the language is actively absorbing functional programming (FP) paradigms. If you've mastered lambdas and LINQ but want to dive deeper — this article is for you. We'll break down advanced FP concepts that will make your C# code more predictable, testable, and concise.

\n\n

1. Pure Functions and Working with Side Effects

\n\n

A pure function is a function that:

\n
    \n
  • Always returns the same result for the same arguments (referential transparency).
  • \n
  • Has no side effects (does not modify global state, write to a file, or send HTTP requests).
  • \n
\n\n

In C#, the ideal example of a pure function is mathematical operations. The problem is that a real application cannot consist solely of pure functions: sooner or later you need to save data or send an email. The solution is separating logic from effects.

\n\n

Advanced technique: use functors and monads to containerize effects. Let's look at a simple example with Maybe<T> (an analogue of nullable, but functional):

\n\n
public class Maybe<T>\n{\n    private readonly T _value;\n    public bool HasValue { get; }\n    \n    private Maybe(T value, bool hasValue) => (_value, HasValue) = (value, hasValue);\n    \n    public static Maybe<T> Some(T value) => new Maybe<T>(value, true);\n    public static Maybe<T> None() => new Maybe<T>(default, false);\n    \n    // Functor: Select (Map)\n    public Maybe<TResult> Select<TResult>(Func<T, TResult> selector) =>\n        HasValue ? Maybe<TResult>.Some(selector(_value)) : Maybe<TResult>.None();\n    \n    // Monad: SelectMany (FlatMap/Bind)\n    public Maybe<TResult> SelectMany<TResult>(Func<T, Maybe<TResult>> binder) =>\n        HasValue ? binder(_value) : Maybe<TResult>.None();\n}\n\n// Usage\nMaybe<int> result = Maybe<int>.Some(10)\n    .Select(x => x * 2)\n    .SelectMany(x => x > 15 ? Maybe<int>.Some(x) : Maybe<int>.None());
\n\n

This code does not throw exceptions or check for null manually — all the logic for handling the absence of a value is encapsulated in the monad.

\n\n

2. Currying and Partial Application

\n\n

In classical FP, each function takes one argument. Currying is the transformation of a function with multiple parameters into a chain of nested functions, each taking one argument. Partial application is fixing some of the arguments.

\n\n

C# does not have built-in currying, but it is easy to implement via closures:

\n\n
// Regular function\nFunc<int, int, int, int> add = (a, b, c) => a + b + c;\n\n// Curried version\nFunc<int, Func<int, Func<int, int>>> curriedAdd = a => b => c => a + b + c;\n\n// Partial application: fix the first argument\nFunc<int, Func<int, int>> add5 = curriedAdd(5);\n\n// Usage\nint sum = add5(10)(20); // 35
\n\n

Advanced scenario: dependency injection via partial application. Instead of passing configuration or a service through a class constructor, you can fix them at the composition stage:

\n\n
public class ReportService\n{\n    public Func<string, string, string> FormatReport => \n        (template, data) => $"[{DateTime.Now}] {template}: {data}";\n}\n\n// Usage with partial application\nvar service = new ReportService();\nvar formatWithTemplate = service.FormatReport("Error"); // fix the template\nstring errorMessage = formatWithTemplate("Connection failed");                

Blogs

Book Recommendations