Мова C# майже функціональний

Здрастуйте, шановні читачі! Наші пошуки в області мови C# серйозно перегукуються з цією статтею, автор якої — фахівець з функціонального програмування на C#. Стаття — уривок з підготовлюваної книги, тому в кінці посту пропонуємо проголосувати за цю книгу.



Багато програмісти неявно передбачають, що «функціональне програмування (ФП) має реалізовуватися тільки на функціональному мовою». C# — об'єктно-орієнтована мова, тому не варто і намагатися писати на ньому функціональний код.

Зрозуміло, це поверхнева трактування. Якщо ви володієте трохи більш глибокими знаннями C# і уявляєте собі його еволюцію, то, ймовірно, в курсі, що мова C# мультипарадигмальный (точно як і F#) і що, нехай він спочатку і був переважно імперативним і об'єктно-орієнтованим, у кожної наступної версії додавалися і продовжують додаватися численні функціональні можливості.

Отже, напрошується питання: наскільки хороший нинішній мова C# для функціонального програмування? Перед тим, як відповісти на це питання, я поясню, що розумію під «функціональним програмуванням». Це парадигма, в якій:

  1. Робиться акцент на роботі з функціями
  2. Прийнято уникати зміни стану
Щоб мова сприяв програмування в такому стилі, він повинен:

  1. Підтримувати функції як елементи 1-го класу; тобто, повинна бути можливість трактувати функцію як будь-яке інше значення, наприклад, використовувати функції як аргументи або повертаються значення інших функцій, або зберігати функції в колекціях
  2. Припиняти всілякі часткові «місцеві» заміни (або взагалі зробити їх неможливими): змінні, об'єкти і структури даних повинні бути незмінними, причому повинно бути легко створювати модифіковані версії об'єкта
  3. Автоматично управляти пам'яттю: адже ми створюємо такі модифіковані копії, а не оновлюємо дані на місці, і в результаті у нас множаться об'єкти. Це непрактично у мові, де відсутнє автоматичне керування пам'яттю
З урахуванням всього цього, ставимо питання руба:

Наскільки мова C# — функціональний?

Ну… давайте подивимося.

1) Функції в C# — дійсно значення першого класу. Розглянемо, наприклад,
наступний код:

Func<int, int> triple = x => x * 3;
var range = Перечіслімого.Range(1, 3);
var triples = range.Select(triple);
triples // => [3, 6, 9]

Тут видно, що функції – дійсно значення першого класу, і можна присвоїти функцію змінної triple, після чого поставити її в якості аргументу Select.

Насправді, підтримка функцій як значень першого класу існувала в C# з перших версій мови, це робилося за допомогою типу Delegate. Згодом були введені лямбда-виразу, і підтримка цієї можливості на рівні синтаксису тільки покращилася.

У мові виявляються деякі примхи і обмеження, коли мова заходить про виведення типів (особливо, якщо ми хочемо передавати многоаргументные функції інших функцій в якості аргументів); про це ми поговоримо в розділі 8. Але, взагалі, підтримка функцій як значень першого класу тут достатньо хороша.

2) В ідеалі, мова також повинен припиняти місцеві заміни. Тут – найбільший недолік C#; всі за замовчуванням изменяемо, і програмісту потрібно чимало потрудитися, щоб забезпечити незмінність. (Порівняйте з F#, де змінні за замовчуванням можна буде змінити, і, щоб змінну можна було міняти, її потрібно спеціально позначити як mutable.)

Що щодо типів? У фреймворку є кілька незмінних типів, наприклад, string і DateTime, але користувацькі змінні типи в мові підтримуються погано (хоча, як буде показано нижче, ситуація трохи виправилася в C#, і в наступних версіях також повинна поліпшуватися). Нарешті, колекції у фреймворку є змінними, але вже є солідна бібліотека незмінних колекцій.

3) З іншого боку, в C# виконується більш важлива вимога: автоматичне керування пам'яттю. Таким чином, хоча мову і не стимулює стиль програмування, не допускає місцевих замін, програмувати в такому стилі на C# зручно завдяки збірці сміття.

Отже, в C# дуже добре підтримуються деякі (але не всі) прийоми функціонального програмування. Мова еволюціонує, і підтримка функціональних прийомів у нього поліпшується.

Далі розглянемо кілька рис мови C# з минулого, сьогодення і недалекого майбутнього – мова піде про можливості, особливо важливих у контексті функціонального програмування.

Функціональна сутність LINQ

Коли вийшов мова C# 3 одночасно з фреймворком .NET 3.5, там виявилася маса можливостей, по суті запозичених з функціональних мов. Частина з них увійшла в бібліотеку (
System.Linq
), а деякі інші можливості забезпечували або оптимізували ті чи інші риси LINQ – наприклад, методи розширення і дерева виразів.

В LINQ пропонуються реалізації багатьох поширених операцій над списками (або, в більш загальному вигляді, над «послідовностями», саме так з технічної точки зору треба називати
IEnumerable
); найбільш поширені з подібних операцій – відображення, сортування і фільтрація. Ось приклад, у якому представлені всі три:

Перечіслімого.Range(1, 100).
Where(i => i % 20 == 0).
OrderBy(i => -i).
Select(i => $"{i}%")
// => ["100%", "80%", "60%", "40%", "20%"]

Зверніть увагу, як Where, OrderBy і Select приймають інші функції в якості аргументів і не змінюють отриманий IEnumerable, а повертають новий IEnumerable, ілюструючи обидва принципу ФП, згадані мною вище.

LINQ дозволяє запитувати не тільки об'єкти, що знаходяться в пам'яті (LINQ to Objects), але і різні інші джерела даних, наприклад, SQL-таблиці і дані у форматі XML. Програмісти, які працюють з C#, визнали LINQ в якості стандартного інструментарію для роботи зі списками і реляційними даними (а на таку інформацію припадає істотна частина будь-якої бази коду). З одного боку це означає, що ви вже трохи уявляєте, який з себе API функціональної бібліотеки.

З іншого боку, при роботі з іншими типами фахівці по C# зазвичай дотримуються імперативного стилю, висловлюючи задумане поведінку програми у вигляді послідовних інструкцій управління потоком. Тому більшість баз коду на C#, які мені доводилося бачити – це черезсмужжя функціонального (робота з
IEnumerable
та
IQueryable
) та імперативного стилю (все інше).

Таким чином, хоча C#-програмісти і в курсі, які переваги роботи з функціональної бібліотекою, наприклад, з LINQ, вони недостатньо щільно знайомі з принципами влаштування LINQ, що заважає їм самостійно використовувати такі прийоми при проектуванні.
Це – одна з проблем, вирішувати які покликана моя книга.

Функціональні можливості в C#6 та C#7

Нехай C#6 та C#7 і не такі революційні, як C#3, ці версії привносять в мову безліч дрібних змін, які в сукупності значно підвищують зручність роботи і ідіоматичність синтаксису при написанні коду.

ПРИМІТКА: Більшість нововведень в C#6 та C#7 оптимізують синтаксис, а не доповнюють функціонал. Тому, якщо ви працюєте зі старою версією C#, то все одно зможете користуватися всіма прийомами, описаними в цій книзі (хіба що ручної роботи буде трохи більше). Однак, нові можливості значно підвищують читабельність коду, і програмувати в функціональному стилі стає приємніше.

Розглянемо, як ці можливості реалізовані в нижчеподаному лістингу, а потім обговоримо, чому вони важливі в ФП.

Лістинг 1. Можливості C#6 та C#7, важливі в контексті функціонального програмування

using static System.Math; <1>
public class Circle
{
public Circle(double radius) 
=> Radius = radius; <2>
public double Radius { get; } <2>
public double Circumference <3>
=> PI * 2 * Radius; <3>

public double Area
{
get
{
double Square(double d) => Pow(d, 2); <4>
return PI * Square(Radius);
}
}
public (double Circumference, double Area) Stats <5>
=> (Circumference, Area);
}

  1. using static
    забезпечує некваліфікований доступ до статичних членам
    System.Math
    , наприклад
    PI
    та
    Pow
    нижче
  2. Авто-властивість
    getter-only
    можна встановити тільки в конструкторі
  3. Властивість в тілі вираження
  4. Локальна функція – це метод, оголошений всередині іншого методу
  5. Синтаксис кортежів C#7 допускає імена членів
Імпорт статичних членів за допомогою «using static»

Інструкція
using static
в C#6 дозволяє "імпортувати" статичні члени класу (в даному випадку мова йде про клас
System.Math
). Таким чином, в нашому випадку можна викликати члени
PI
та
Pow
Math
без додаткової кваліфікації.

using static System.Math;
//...
public double Circumference
=> PI * 2 * Radius;

Чому це важливо? В ФП пріоритет віддається таким функціям, поведінка яких залежить лише від їх вхідних аргументів, оскільки можна окремо протестувати кожну таку функцію і міркувати про неї поза контекстом (порівняйте з методами примірників, реалізація кожного з них залежить від членів примірника). Ці функції в C# реалізуються як статичні методи, тому функціональна бібліотека в C# буде складатися в основному з статичних методів.

Інструкція
using static
полегшує споживання таких бібліотек і, хоча зловживання нею може призводити до забруднення простору імен, помірне використання дає чистий, легкий для читання код.

Більш прості незмінні типи з getter-only авто-властивостями

При оголошенні
getter-only
авто-властивості, наприклад,
Radius
, компілятор неявно оголошує readonly резервне поле. В результаті значення цим властивостям може бути присвоєно лише в конструкторі або внутристрочно.

public Circle(double radius) 
=> Radius = radius;
public double Radius { get; }

Getter-only
автосвойства в C#6 полегшують визначення незмінних типів. Це видно на прикладі класу Circle: в ньому є всього одне поле (резервне поле
Radius
), призначений тільки для читання; отже, створивши
Circle
, ми вже не можемо його змінити.

лаконічніші функції з членами в тілі вираження

Властивість
Circumference
оголошується разом з «тілом вираження», яке починається з
=>
, а не з звичайним тілом інструкції», що укладається в
{ }
. Зверніть увагу, наскільки лаконічніше цей код у порівнянні з властивістю Area!

public double Circumference
=> PI * 2 * Radius;

В ФП прийнято писати безліч простих функцій, серед яких повно однорядкових. Потім такі функції компонуються в більш складні робочі керуючі потоки. Методи, декларовані в тілі вирази, в такому випадку зводять до мінімуму синтаксичні перешкоди. Це особливо наочно, якщо спробувати написати функцію, яка повертала б функцію – у книзі це робиться дуже часто.

Синтаксис з оголошенням в тілі вираження з'явився в C#6 для методів і властивостей, а в C#7 став більш універсальним і застосовується також з конструкторами, деструкторами, геттерами і сеттерами.

Локальні функції

Якщо доводиться писати безліч простих функцій, це означає, що часто функція викликається всього з одного місця. В C#7 це можна запрограмувати явно, оголошуючи методи в області видимості методу; наприклад, метод Square оголошується в області видимості геттера
Area
.

get
{
double Square(double d) => Pow(d, 2);
return PI * Square(Radius);
}

Оптимізований синтаксис кортежів

Мабуть, у цьому полягає найважливіша властивість C#7. Тому можна з легкістю створювати і споживати кортежі і, що найважливіше, привласнювати їх елементів значущі імена. Наприклад, властивість
Stats
повертає кортеж типу
(double, double)
, але додатково задає значущі імена для елементів кортежу, і по них можна звертатися до цих елементів.

public (double Circumference, double Area) Stats
=> (Circumference, Area);

Причина важливості кортежів ФП, знову ж таки, пояснюється все тією ж тенденцією: розбивати завдання на як можна більш компактні функції. У може вийти на тип даних, що застосовується для захоплення інформації, що повертається лише однією функцією і приймається іншою функцією як введення. Було б нераціонально визначати для таких структур виділені типи, які не відповідають ніяким абстракцій з предметної області – саме в таких випадках і знадобляться кортежі.

В майбутньому мову C# стане функціональніше?

Коли на початку 2016 року я писав чернетка цієї глави, я з цікавістю відзначив, що всі можливості, викликали у команди розробників «сильний інтерес», що традиційно асоціюються з функціональними мовами. Серед цих можливостей:

  • Реєстровані типи (незмінні типи без трафаретного коду)
  • Алгебраїчні типи даних (потужне додаток до системі типів)
  • Зіставлення з шаблоном (нагадує оператор `switch` перемикаючий форму даних, наприклад, їх тип, а не тільки значення)
  • Оптимізований синтаксис кортежів
Однак, довелося задовольнятися лише останнім пунктом. В обмеженому обсязі реалізовано і зіставлення з шаблоном, але поки це лише бліда тінь зіставлення з шаблоном, доступного в функціональних мовах, і на практиці такої версії зазвичай недостатньо.

З іншого боку, такі можливості плануються в наступних версіях, і вже йде опрацювання відповідних пропозицій. Таким чином, в майбутньому ми, ймовірно, побачимо у C# реєстровані типи і зіставлення за шаблоном.

Отже, C# і далі буде розвиватися як мультипарадигмальный мову зі все більш вираженим функціональним компонентом.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.