Как стать автором
Обновить

Ваш C# уже «функциональный», просто позвольте ему это

Время на прочтение 4 мин
Количество просмотров 19K
Привет, Хабр! Представляю вашему вниманию перевод оригинальной статьи «Your C# is already functional, but only if you let it» автора Igal Tabachnik.

Несколько дней назад я написал в Твиттере фрагмент кода C#, реализующий FizzBuzz, используя некоторые из новых «фичи» в C# 8.0 . Твит “стал вирусным”, несколько человек восхищались его лаконичностью и функциональностью, в то время как другие спрашивали меня, почему я не написал его на F#?

Прошло уже более 4 лет с тех пор, как я в последний раз писал на C#, и то, что я обычно использую функциональное программирование, явно повлияло на то, как я пишу код сегодня. Фрагмент, который я написал, кажется очень аккуратным и естественным, однако некоторые люди выразили опасения, что он не похож на код на C#.
«Он выглядит слишком функциональным.» – писали мне они.
В зависимости от того, кого вы спрашиваете, «функциональное программирование» означает разные вещи для разных людей. Но вместо того, чтобы обсуждать семантику, я хотел бы предложить объяснение того, почему эта реализация FizzBuzz кажется функциональной.

Для начала давайте разберем что же делает этот код:

public static void Main(string[] args)
{
      string FizzBuzz(int x) => 
            (x % 3 == 0, x % 5 == 0) switch
            {  
                  (true, true) => "FizzBuzz", 
                  (true, _) => "Fizz", 
                  (_, true) => "Buzz", 
                  _ => x.ToString()
            };
    
      Enumerable.Range(1, 100 ) 
            .Select(FizzBuzz).ToList() 
            .ForEach(Console.WriteLine); 
}

Здесь мы создаем локальный метод, представленный лямбда-выражением, результат которого вычисляется с помощью кортежа.

Новизна здесь заключается в использовании кортежа (пары) для работы с результатом вычисления двух выражений вместе (x % 3 = = 0 и x % 5 = = 0). Это позволяет использовать сопоставление шаблонов для определения итогового результата. Если ни один из вариантов не совпадает, то по умолчанию будет возвращено строковое представление числа.

Однако, ни одна из многих “функциональных” «фич», используемых в этом фрагменте кода (включая циклы foreach в стиле LINQ), не делает этот код функциональным сам по себе. Что делает его функциональным, так это тот факт, что за исключением вывода результата на консоль, все методы, используемые в этой программе, являются выражениями (expressions).

Проще говоря, выражение — это вопрос, на который всегда есть ответ. В терминах программирования выражение (expression) представляет собой комбинацию констант, переменных, операторов и функций, вычисляемых средой выполнения для вычисления (“возврата”) значения. Чтобы проиллюстрировать разницу с инструкциями (statements), давайте напишем более привычный для программистов C# вариант FizzBuzz:

public static void Main(string[] args) 
{
      foreach( int x in Enumerable.Range(1, 100)) 
      {
             FizzBuzz(x);
      }
} 

public static void FizzBuzz( int x) 
{
      if (x % 3 == 0  && x % 5 == 0) 
             Console.WriteLine("FizzBuzz"); 
      else if (x % 3 == 0 ) 
             Console.WriteLine("Fizz"); 
      else if (x % 5 == 0 ) 
             Console.WriteLine("Buzz"); 
      else
             Console.WriteLine(x);
}

Естественно, это далеко не «чистый» код, и его можно улучшать, но нельзя не согласиться, что это обычный код на C#. При ближайшем рассмотрении метода FizzBuzz, даже с учетом его простоты, в нем явно видно несколько проблем с дизайном.

Прежде всего, эта программа нарушает первый из принципов SOLID – принцип единой ответственности. Он смешивает логику вычисления выходного значения на основе числа и вывод этого значения на консоль. Как следствие, он нарушает принцип инверсии зависимостей (последний из SOLID), плотно связываясь с выводом результата на консоль. Наконец, такая реализация программы затрудняет повторное использование и изолированное тестирование кода. Конечно, для такой простой программы как эта, особо нет смысла вдаваться в тонкости проектирования иначе может получится что-то вроде этого.

Все вышеперечисленные проблемы можно решить разделением получения значения и вывода на консоль. Даже без использования причудливых языковых конструкций, простой возврат полученного значения вызывающему коду освобождает нас от ответственности за использование этого значения.

public static string FizzBuzz(int x)
{
      if (x % 3 == 0 && x % 5 == 0)
            return "FizzBuzz";
      else if (x % 3 == 0)
            return "Fizz";
      else if (x % 5 == 0)
            return "Buzz";
      else
            return x.ToString();
}

Конечно это не кардинальные изменения, но этого уже достаточно:

  1. Метод FizzBuzz теперь является выражением (expression), которое получает числовое значение и возвращает строку.
  2. У него нет никаких других обязанностей и скрытых эффектов, что превращает его в чистую функцию.
  3. Он может использоваться и тестироваться самостоятельно, без каких-либо дополнительных зависимостей и настроек.
  4. Код, вызывающий данную функцию волен делать с результатом все что угодно – теперь это не наша ответственность.

И в этом заключается суть функционального программирования — программа состоит из выражений, результатом которых являются какие-либо значения, и эти значения возвращаются вызывающему коду. Эти выражения, как правило, являются полностью самостоятельными, а результат их вызова зависит только от входных данных. На самом верху, в точке входа (или, иногда называемой «концом света»), значения, возвращаемые функциями, собираются и взаимодействуют с остальным миром. На объектно-ориентированном жаргоне это иногда называют «луковой архитектурой” (или „портами и адаптерами“) — чистым ядром, состоящим из бизнес-логики и императивной внешней оболочки, отвечающей за взаимодействие с внешним миром.

Использование выражений (expressions) вместо инструкций (statements) в некоторой степени используются всеми языками программирования. C# эволюционировал с течением времени, чтобы ввести функции, облегчающие работу с выражениями: LINQ, методы, основанные на выражениях, сопоставление шаблонов и многое другое. Эти «фичи» часто называют «функциональными», потому что они есть — в таких языках, как F# или Haskell, в котором практически невозможно существование чего- либо, кроме выражений.

На самом деле, этот стиль программирования теперь поощряется командой C#. В недавнем выступлении в NDC London Билл Вагнер призывает разработчиков изменить свои (императивные) привычки и принять современные методы:


C# (и другие императивные языки, такие как Java) можно использовать функционально, но это требует немалых усилий. Эти языки делают функциональный стиль исключением, а не нормой. Я призываю вас изучать функциональные языки программирования, чтобы стать первоклассным специалистом.
Теги:
Хабы:
+20
Комментарии 127
Комментарии Комментарии 127

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн