Functional Programming in C#: Advanced Level
Functional programming (FP) is a paradigm that has been actively penetrating the world of C# in recent years. Thanks to the capabilities of .NET, developers can combine object-oriented and functional approaches, creating more reliable, testable, and predictable code. In this article, we will explore advanced FP techniques in C# that will help you go beyond simple LINQ usage.
1. Pure Functions and Side Effects: Strict Control
A pure function is a function whose result depends only on its input arguments and which causes no side effects (modifying global state, input-output, exceptions). In C#, we can approach this ideal by using immutable types and avoiding parameter modification.
// Pure function: result depends only on x and ypublic static int Add(int x, int y) => x + y;
// Impure function: depends on external stateprivate static int _counter = 0;public static int IncrementCounter() => ++_counter;At an advanced level, it is important to strictly separate pure computations from operations with side effects. For this, you can use the IO pattern (in functional languages) or its simplified analogs in C# via Func<T> and lazy evaluation.
Practical Technique: "Lazy Execution"
Instead of immediately performing an action with a side effect, return a delegate that will execute it:
public static Func<int> CreateFileReadOperation(string path){ return () => File.ReadAllText(path).Length; // Side effect is deferred}
// Usagevar operation = CreateFileReadOperation("data.txt");// ... pure logic ...int length = operation(); // Side effect is executed here2. Function Composition and Currying
Composition is combining two or more functions to create a new one. Currying is transforming a function of several arguments into a chain of functions of one argument. These techniques make code more modular and reusable.
Composition via LINQ and Custom Methods
In C#, composition can be implemented using extension methods:
public static class FuncExtensions{ public static Func<T, TResult> Compose<T, TIntermediate, TResult>( this Func<T, TIntermediate> first, Func<TIntermediate, TResult> second) => x => second(first(x));}
// Example: f(x) = x + 2, g(x) = x * 3, h = f ∘ gFunc<int, int> addTwo = x => x + 2;Func<int, int> multiplyByThree = x => x * 3;var composed = multiplyByThree.Compose(addTwo); // h(x) = (x*3) + 2Console.WriteLine(composed(5)); // 17Currying in C#
Currying allows partial application of a function:
public static Func<int, Func<int, int>> CurriedAdd = x => y => x + y;
var addFive = CurriedAdd(5); // Partial applicationConsole.WriteLine(addFive(10)); // 15
// More complex example: logging function with a prefixpublic static Func<string, Func<string, string>> CreateLogger = prefix => message => $"[{prefix}] {message}";
var infoLogger = CreateLogger("INFO");Console.WriteLine(infoLogger("System started")); // [INFO] System started3. Monads and Working with Option (Maybe)
A monad is a pattern that allows sequentially applying functions to values wrapped in a context (e.g., the possibility of absence of a value or asynchrony). In C#, the Option<T> or Maybe<T> implementation is popular for avoiding null.
Simple Option Implementation
public class Option<T>{ private readonly T _value; private readonly bool _hasValue;
pri