[Перевод] Язык C# почти функционален


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

e1b694f554424d9c8bbbde5be94ddd91.jpeg

Многие программисты неявно подразумевают, что «функциональное программирование (ФП) должно реализовываться только на функциональном языке». C# — объектно-ориентированный язык, поэтому не стоит и пытаться писать на нем функциональный код.

Разумеется, это поверхностная трактовка. Если вы обладаете чуть более глубокими знаниями C# и представляете себе его эволюцию, то, вероятно, в курсе, что язык C# мультипарадигмальный (точно как и F#) и что, пусть он изначально и был в основном императивным и объектно-ориентированным, в каждой последующей версии добавлялись и продолжают добавляться многочисленные функциональные возможности.

Итак, напрашивается вопрос: насколько хорош нынешний язык C# для функционального программирования? Перед тем, как ответить на этот вопрос, я поясню, что понимаю под «функциональным программированием». Это парадигма, в которой:

  1. Делается акцент на работе с функциями
  2. Принято избегать изменения состояния

Чтобы язык способствовал программированию в таком стиле, он должен:
  1. Поддерживать функции как элементы 1-го класса; то есть, должна быть возможность трактовать функцию как любое другое значение, например, использовать функции как аргументы или возвращаемые значения других функций, либо хранить функции в коллекциях
  2. Пресекать всякие частичные «местные» замены (или вообще сделать их невозможными): переменные, объекты и структуры данных по умолчанию должны быть неизменяемыми, причем должно быть легко создавать модифицированные версии объекта
  3. Автоматически управлять памятью: ведь мы создаем такие модифицированные копии, а не обновляем данные на месте, и в результате у нас множатся объекты. Это непрактично в языке, где отсутствует автоматическое управление памятью

С учетом всего этого, ставим вопрос ребром:

Насколько язык C# — функциональный?

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

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

Func triple = x => x * 3;
var range = Enumerable.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); наиболее распространенные из подобных операций — отображение, сортировка и фильтрация. Вот пример, в котором представлены все три:

Enumerable.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)

© Habrahabr.ru