Думаю, что практически все слышали о существовании функционального подхода в программировании. Многие даже пытались понять, что это такое и чем он отличается от привычного народным массам императивного подхода. Однако только немногие решились реально разобраться в нем, и уж совсем немногие действительно разобрались и освоили функциональный подход.
Меж тем, функциональный подход может сделать ваш код более кратким и понятным. Это позволит решать более сложные задачи и делать при этом меньше ошибок. Но чтобы иметь возможность читать (и писать) функциональный код надо быт знакомыми с основными приемами ФП.
Цель этой статьи максимально просто объяснить императивному программисту основы функционального программирования (далее ФП).
Примеры в этой статье будут даваться в основном на C#.
ПРЕДУПРЕЖДЕНИЕ В этой статье будут приводиться примеры, в основном связанные с «LINQ to object», так как именно LINQ to object является прямым аналогом функционального программирования. «LINQ to SQL» использует похожий код, но реально генерирует аналог абстрактного синтаксического дерева (эдакого CodeDom), по которому специализированными драйверами генерируется код SQL-запросов. Это выходит за рамки этой статьи, так что если вы встретите пример, реализацию некого метода или термин «LINQ», то знайте, что по умолчанию, речь идет о «LINQ to object». |
Даже из названия «функциональное программирование» ясно, что основной упор в нем делается на функции. Функциональное вычисление – функция (или если быть точнее – «чистая функция», pure function) – должна принимать некоторые аргументы (входящие данные) на входе, производить вычисление и возвращать некоторый результат. При этом функция не должна создавать никаких побочных эффектов. Под побочными эффектами понимается:
По сути, пункты 2-4 представляют собой разновидности одного и того же – изменение состояния посредством вызова.
В общем, функция не имеет право делать ничего, что могло бы изменить состояние чего бы то ни было. Все, что может сделать функция – это произвести вычисления и выдать результат.
У такого подхода есть одна замечательная особенность. Функция всегда должна возвращать один и тот же результат для одних и тех же аргументов. Замечательна эта особенность сразу по нескольким причинам.
Фактически, отсутствие побочных эффектов – это заслуга не самой функции, а содержащихся в них выражений. Ведь именно в них требуется не допускать побочных эффектов. Меж тем в ИЯ имеется множество конструкций создающих побочные эффекты. Главная из них, я бы даже сказал, основополагающая – это присвоение. Именно присвоение приводит к изменению памяти. Остальные конструкции, в основном, только способствуют или не способствуют применению присвоения. Так, среди способствующих конструкций можно перечислить:
Впрочем, изменение локальных переменных не создают столько проблем, как изменение глобальных переменных и вообще изменение глобального состояния.
Даже очень старые ИЯ, например, C и Паскаль, поддерживают самую базовую идею ФП – возможность манипуляции функциями. Однако в большинстве даже современных ИЯ реализована эта возможность совсем слабо. Обычно все ограничивается возможностью передать указатель на тело функции. Скажем, в С и C++ имя глобальной функции интерпретируется как указатель на нее.
ФЯ развивают эту идею, возводя ее в принцип – функция является в программе таким же полноценным объектом, как и экземпляр любого другого типа (например, экземпляр строки). Ей должно быть так же удобно манипулировать, как обычными объектами. Скажем, в С мы можем передать указатель на функцию другой функции, но составить из двух функций третью мы не в силах. Невозможно в С и объявить функцию по месту. В ФЯ же все это возможно. Манипулировать функциями в ФЯ очень просто и удобно.
Первое, что требуется для манипуляции функциями – это иметь возможность описать их тип. В ФЯ для функций был введен специальный тип – функциональный тип. Обычно он описывается как список типов параметров функции и список ее возвращаемых значений. Впрочем, тут возможны варианты, так как функция с несколькими параметрами может быть описана как функция с одним параметром, возвращающая функцию с другим параметром. Скажем, если для описания возвращаемого значения функции использовать знак «->», а для перечисления значений - «*», то функцию с сигнатурой:
string Function(int x, string y); |
Можно описать как:
int * string -> string |
или как:
int -> string -> string |
то есть как функцию, которая получает int и возвращает функцию, которая получает string и возвращает тоже string. Я понимаю, что это очень непривычно и непонятно (на первый взгляд), но поверьте, что есть целая теория обосновывающая это (теория «лямбда-исчислений» Чёрча). Нам все эти тонкости не важны. Зато нам важно, как обстоят дела с аналогом функционального типа в C#.
В C# аналогом ссылки на функцию являются делегаты. Сама по себе функция (метод) или ее имя не является чем-то, чем можно было бы манипулировать, но если поместить ее в делегат, появится возможность ссылаться на нее, а значит передавать в другие функции и возвращать функции из других функций. Таким образом, можно (с большой натяжкой) сказать, что делегаты являются аналогами функциональных типов в ФЯ. Однако они не являются полными аналогами, что создает некоторые проблемы при использовании делегатов в качестве функционального типа. Чуть позже я опишу проблемы делегатов, но сначала я расскажу о нововведении C# 3.0 – о лямбда-выражениях.