Функциональное программирование в C#: продвинутые приёмы и практики

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Функциональное программирование в 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>                

Блоги

Рекомендаций по книгам