Функциональное программирование в C#: продвинутый уровень
Функциональное программирование (ФП) — это парадигма, которая в последние годы активно проникает в мир C#. Благодаря возможностям .NET, разработчики могут сочетать объектно-ориентированный и функциональный подходы, создавая более надёжный, тестируемый и предсказуемый код. В этой статье мы разберём продвинутые приёмы ФП в C#, которые помогут вам выйти за рамки простого использования LINQ.
1. Чистые функции и побочные эффекты: строгий контроль
Чистая функция — это функция, результат которой зависит только от входных аргументов и которая не вызывает побочных эффектов (изменение глобального состояния, ввод-вывод, исключения). В C# мы можем приблизиться к этому идеалу, используя неизменяемые типы и избегая модификации параметров.
// Чистая функция: результат зависит только от x и y
public static int Add(int x, int y) => x + y;
// Нечистая функция: зависит от внешнего состояния
private static int _counter = 0;
public static int IncrementCounter() => ++_counter;
На продвинутом уровне важно строго отделять чистые вычисления от операций с побочными эффектами. Для этого можно использовать паттерн IO (в функциональных языках) или его упрощённые аналоги в C# через Func<T> и отложенные вычисления.
Практический приём: «Отложенное выполнение»
Вместо того чтобы сразу выполнять действие с побочным эффектом, возвращайте делегат, который его выполнит:
public static Func<int> CreateFileReadOperation(string path)
{
return () => File.ReadAllText(path).Length; // Побочный эффект отложен
}
// Использование
var operation = CreateFileReadOperation("data.txt");
// ... чистая логика ...
int length = operation(); // Побочный эффект выполняется здесь
2. Композиция функций и каррирование
Композиция — это объединение двух или более функций для создания новой. Каррирование — преобразование функции от нескольких аргументов в цепочку функций от одного аргумента. Эти техники делают код более модульным и переиспользуемым.
Композиция через LINQ и собственные методы
В C# композицию можно реализовать через методы расширения:
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));
}
// Пример: f(x) = x + 2, g(x) = x * 3, h = f ∘ g
Func<int, int> addTwo = x => x + 2;
Func<int, int> multiplyByThree = x => x * 3;
var composed = multiplyByThree.Compose(addTwo); // h(x) = (x*3) + 2
Console.WriteLine(composed(5)); // 17
Каррирование в C#
Каррирование позволяет частично применять функцию:
public static Func<int, Func<int, int>> CurriedAdd = x => y => x + y;
var addFive = CurriedAdd(5); // Частичное применение
Console.WriteLine(addFive(10)); // 15
// Более сложный пример: функция логирования с префиксом
public static Func<string, Func<string, string>> CreateLogger =
prefix => message => $"[{prefix}] {message}";
var infoLogger = CreateLogger("INFO");
Console.WriteLine(infoLogger("Система запущена")); // [INFO] Система запущена
3. Монады и работа с Option (Maybe)
Монада — это паттерн, который позволяет последовательно применять функции к значениям, обёрнутым в контекст (например, возможность отсутствия значения или асинхронность). В C# популярна реализация Option<T> или Maybe<T> для избежания null.
Простая реализация Option
public class Option<T>
{
private readonly T _value;
private readonly bool _hasValue;
private Option(T value, bool hasValue) => (_value, _hasValue) = (value, hasValue);
public static Option<T> Some(T value) => new Option<T>(value, true);
public static Option<T> None() => new Option<T>(default, false);
public Option<TResult> Bind<TResult>(Func<T, Option<TResult>> func)
=> _hasValue ? func(_value) : Option<TResult>.None();
public TResult Match<TResult>(Func<T, TResult> some, Func<TResult> none)
=> _hasValue ? some(_value) : none();
}
// Пример использования: безопасное деление
public static Option<int> Divide(int numerator, int denominator)
=> denominator == 0 ? Option<int>.None() : Option<int>.Some(numerator / denominator);
var result = Option<int>.Some(10)
.Bind(x => Divide(x, 2)) // Some(5)
.Bind(x => Divide(x, 0)) // None
.Match(
some: val => $"Результат: {val}",
none: () => "Ошибка деления на ноль"
);
Console.WriteLine(result); // Ошибка деления на ноль
Более мощные реализации можно найти в библиотеках LanguageExt или Optional.
4. Неизменяемость (Immutability) и record-типы
Неизменяемые объекты — краеугольный камень ФП. C# 9+ предоставляет record типы, которые по умолчанию неизменяемы и поддерживают неразрушающую мутацию через with.
public record Person(string Name, int Age);
var person1 = new Person("Alice", 30);
var person2 = person1 with { Age = 31 }; // Создаётся новый объект
// Для коллекций используйте Immutable collections:
using System.Collections.Immutable;
var list = ImmutableList.Create(1, 2, 3);
var newList = list.Add(4); // list остаётся неизменным, newList = [1,2,3,4]
Продвинутый приём — структуры как value objects:
public readonly struct Money : IEquatable<Money>