1. Вступ
Переходячи від теорії до практики, логічно запитати: «А навіщо вам, розробнику на .NET і C#, усі ці прийоми функціонального програмування?»
Справді, C# — не суто функціональна мова на кшталт F# чи Haskell. Але, починаючи з версії 3.0 і аж до C# 14, він набув чимало інструментів ФП, які можуть істотно підвищити якість і виразність коду.
Ось де вони особливо доречні:
- Робота з колекціями — LINQ, Map/Reduce, фільтрація, агрегація, сортування та інші зручні перетворення даних.
- Чисті функції — менше помилок, повʼязаних зі станом і побічними ефектами; простіше налагоджувати.
- Функції вищого порядку — універсальні, повторно використовувані компоненти, з якими приємно працювати.
- Незмінність у багатопоточності — одна з головних запорук безпечного коду в паралельних і асинхронних сценаріях.
- Композиція функцій — робить складну бізнес-логіку лаконічною, читабельною та зручною для тестування.
Таблиця: Порівняння ООП-підходу та ФП-підходу в C#
| Завдання | Імперативно (ООП/класичний підхід) | Функціонально (ФП) |
|---|---|---|
| Фільтрація списку | |
|
| Перетворення списку | |
|
| Пошук за критерієм | |
|
| Агрегування | |
|
| Кешування | |
|
2. LINQ: найфункціональніше в C#
Якщо вам здалося, що «функціональне програмування» — це про списки, фільтри та різні .Where, .Select, .Aggregate, — вітаю: так і є! LINQ — квінтесенція ФП у C#.
Згадаймо, як влаштований LINQ
LINQ оперує колекціями за допомогою ланцюжків методів, що приймають функції як параметри (наприклад, лямбди). Наприклад:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// Отримаємо лише парні числа і подвоїмо їх
var result = numbers
.Where(x => x % 2 == 0)
.Select(x => x * 2);
foreach (var number in result)
Console.WriteLine(number);
Що тут відбувається?
- .Where — функція вищого порядку: приймає функцію (x => x % 2 == 0) і повертає іншу колекцію.
- .Select — теж приймає функцію (x => x * 2).
- Ми не змінюємо вихідну колекцію, а отримуємо новий результат.
Цей стиль легко читати й розширювати (можна додати ще .OrderBy, .Take, .Distinct тощо).
Зверніть увагу! Лямбда-вирази — зручний спосіб створити «делегат на льоту». LINQ був би неможливий без підтримки ФП у C#.
Схема: Функціональна обробка колекції
Колекція --> Where(x => bool) --> Select(x => y) --> Новий результат
3. Композиція функцій і конвеєр обробки даних
У ФП часто використовують композицію: складна операція будується як ланцюжок невеликих функцій, і кожна виконує свою частину роботи.
Приклад: ланцюжок обробки рядка
Стиль зі зміною стану (ООП):
string s = " hello world ";
s = s.Trim();
s = s.ToUpper();
s = s + "!";
Console.WriteLine(s); // HELLO WORLD!
Функціональніше — як конвеєр функцій:
Func<string, string> trim = x => x.Trim();
Func<string, string> upper = x => x.ToUpper();
Func<string, string> addBang = x => x + "!";
// Композиція функцій — послідовно застосовуємо їх
Func<string, string> pipeline = x => addBang(upper(trim(x)));
Console.WriteLine(pipeline(" hello world ")); // HELLO WORLD!
Простіший комбінатор композиції:
Func<T, R> Compose<T, U, R>(Func<T, U> f, Func<U, R> g) =>
x => g(f(x));
// А тепер pipeline через Compose:
var pipeline2 = Compose(trim, upper);
pipeline2 = Compose(pipeline2, addBang);
Console.WriteLine(pipeline2(" hello again ")); // HELLO AGAIN!
4. Робота з незмінністю: захист від помилок
Імутабельність — базовий елемент ФП. Ми не змінюємо структуру даних, а повертаємо нову. Це особливо важливо в багатопоточних застосунках.
Приклад: «неправильно» (мутабельно)
List<int> numbers = new List<int> { 1, 2, 3 };
numbers[0] = 42;
Приклад: «правильно» (функціонально)
var numbers = new List<int> { 1, 2, 3 };
var newNumbers = numbers.Select((x, i) => i == 0 ? 42 : x).ToList();
У сучасному C# є колекції ImmutableList<T> та інші типи з простору імен System.Collections.Immutable:
using System.Collections.Immutable;
var immutableNumbers = ImmutableList.Create(1, 2, 3);
var changed = immutableNumbers.SetItem(0, 42); // Повертає новий список!
5. Функції вищого порядку в реальному житті
Функції вищого порядку — спосіб писати універсальні компоненти без великої кількості умовних операторів.
Приклад: Універсальний фільтр для користувачів
class User
{
public string Name { get; set; }
public int Age { get; set; }
}
var users = new List<User>
{
new User { Name = "Василь", Age = 26 },
new User { Name = "Катерина", Age = 17 },
new User { Name = "Олексій", Age = 35 }
};
List<User> FilterUsers(List<User> source, Predicate<User> predicate)
{
return source.Where(u => predicate(u)).ToList();
}
// Використання:
var adults = FilterUsers(users, u => u.Age >= 18);
var longNames = FilterUsers(users, u => u.Name.Length > 3);
6. Pattern matching і switch-вирази
Сучасний C# активно використовує зіставлення зі зразком: switch-вирази часто замінюють громіздкі ланцюжки if.
object value = 123;
string description = value switch
{
int i when i > 100 => "Велике число",
string s when s.Length > 3 => "Довгий рядок",
null => "Порожнє значення",
_ => "Невідомо"
};
Console.WriteLine(description); // Велике число
7. Мемоізація: кешування результатів функцій
Мемоізація — кешування результату функції для однакових аргументів. У C# її легко реалізувати самостійно.
Func<int, int> SlowFib = null; // Рекурсивна функція Фібоначчі
var cache = new Dictionary<int, int>();
SlowFib = n =>
{
if (cache.ContainsKey(n))
return cache[n];
if (n <= 1)
cache[n] = n;
else
cache[n] = SlowFib(n - 1) + SlowFib(n - 2);
return cache[n];
};
Console.WriteLine(SlowFib(40)); // Блискавично!
8. Карування і часткове застосування
Часткове застосування — фіксація частини аргументів функції. У C# це зручно робити лямбдами.
Func<int, int, int> add = (a, b) => a + b;
// Фіксуємо перший аргумент
Func<int, int> add10 = b => add(10, b);
Console.WriteLine(add10(5)); // 15
Console.WriteLine(add10(100)); // 110
9. Декларативний стиль із функціями
Імперативно:
var result = new List<int>();
foreach (var n in numbers)
{
if (n > 0)
result.Add(n * n);
}
Декларативно:
var result = numbers
.Where(n => n > 0)
.Select(n => n * n)
.ToList();
10. Практичне завдання
Реалізуємо модуль фільтрації завдань у «менеджері завдань для студентів».
Модель:
class StudentTask
{
public string Title { get; set; }
public bool IsCompleted { get; set; }
public int Priority { get; set; }
}
Початкові дані:
var tasks = new List<StudentTask>
{
new StudentTask { Title = "Виконати домашнє завдання", IsCompleted = false, Priority = 2 },
new StudentTask { Title = "Випити кави", IsCompleted = true, Priority = 3 },
new StudentTask { Title = "Подивитися лекцію", IsCompleted = false, Priority = 1 }
};
Універсальний фільтр:
List<StudentTask> FilterTasks(
List<StudentTask> all,
Predicate<StudentTask> predicate)
{
return all.Where(t => predicate(t)).ToList();
}
// Пошук незавершених завдань із пріоритетом > 1
var importantTasks = FilterTasks(tasks, t => !t.IsCompleted && t.Priority > 1);
// Виводимо результат
foreach (var task in importantTasks)
Console.WriteLine(task.Title);
Комбінатори предикатів:
Predicate<StudentTask> IsActive = t => !t.IsCompleted;
Predicate<StudentTask> IsHighPriority = t => t.Priority > 1;
// Об’єднуємо кілька критеріїв, варіант 1
var specialTasks = FilterTasks(tasks, t => IsActive(t) && IsHighPriority(t));
// Варіант 2: функція-комбінатор двох предикатів
Predicate<StudentTask> And(Predicate<StudentTask> a, Predicate<StudentTask> b) => t => a(t) && b(t);
var specialTasks2 = FilterTasks(tasks, And(IsActive, IsHighPriority));
11. Особливості та типові помилки під час застосування функціонального підходу в C#
По‑перше, пам’ятайте: C# — суворо типізована мова. Іноді потрібно явно вказувати типи, особливо коли функції повертають делегати або складні лямбди. Інакше можна отримати помилку компіляції через невиведений тип.
По‑друге, не передавайте функції з побічними ефектами туди, де очікуються чисті функції. Зміна зовнішніх змінних ламає передбачуваність. Намагайтеся, щоб ваші функції не змінювали стан поза власною областю видимості.
По‑третє, обережно із захопленням змінних зовнішньої області (замиканням), особливо в асинхронному й багатопоточному коді. Змінна, значення якої змінюється після передачі в лямбду (наприклад, у LINQ), може призводити до неочевидних помилок.
Нарешті, надмірний ФП‑стиль може ускладнити підтримку коду для команди, яка до нього не звикла. Використовуйте ФП там, де це справді спрощує розв’язання, а не заради «краси».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ