Особенности функционального программирования
Функциональное программирование базируется на идеях Алонзо Чёрча и его λ-исчислении, которое опирается на функцию в его математическом смысле и значении, что отличает его от императивного программирования, которое описывает процесс вычисления как последовательность изменения состояния некоторого автомата.
λ-исчисление
λ-исчисление является формальной математической системой, разработанной Алонзо Чёрчем в первой половине XX века. Алонзо Чёрч и Алан Тьюринг доказали, что существуют задачи, которые невозможно решить механически за конечное число шагов (см. теорему Чёрча — Тьюринга и проблему разрешения).
Формальная система λ-исчисление записывается в следующей форме Бэкуса-Наура: , где s — выражение, а x — переменная. также называется абстракцией, а — аппликацией.
Я не буду подробно останавливаться на полном описании идей Чёрча. Следующие материалы могут быть полезны для дальнейшего изучения вопроса:
- Lambda Calculus and Types by Andrew D. Ker [pdf]
- What is Lambda Calculus and should you care? (пер. λ-исчисление. Часть первая: история и теория)
- Church numbers by Robert ”Corky” Cartwright [pdf]
Свойства функциональных языков
Часто к достоинствам функциональных языков (для удобство обзовём их свойствами) относят следующее [1]:
- лаконичность;
- функции как значения
- отсутствие побочных эффектов (сайд-эффектов)
- ленивые вычисления
- строгая типизация
- модульность
Лаконичность
Конструкции функциональных языков позволяют описывать решение задач, близкому к математическому описанию. Сравните описание функции возведения в степень на C++:
#include <iostream> int power(int a, int n) { if (n == 0) { return 1; } return a * power(a, n - 1); } int main(int argc, char *argv[]) { std::cout << power(3, 3); }
и на Хаскеле:
power a 0 = 1 power a n = a * power a (n - 1) main = do print (power 2 8)
Другой пример состоит в сортировке вставками. Код на Джава выглядит следующим образом:
import java.util.Arrays; public class Main { public void sort(int[] input) { for (int j = 1; j < input.length; j++) { int value = input[j]; for (int i = j - 1; i >= 0 && value < input[i]; i--) { input[i + 1] = input[i]; input[i] = value; } } } public static void main(String[] args) { int[] array = {5, 3, 9, 5, 6, 1, 2, 4, 8, 7}; new Main().sort(array); System.out.println(Arrays.toString(array)); } }
Возможности Хаскеля позволяют решить эту задачу быстрее:
import Data.List insertionSort = foldr insert [] main = do let xs = insertionSort [5, 3, 9, 5, 6, 1, 2, 4, 8, 7] print xs
Функции как значения
В функциональных языках программирования функции являются объектами первого класса. Другими словами, они могут быть переданы как значение параметров, возвращены как результат или же присвоены переменной. Возможность передавать функцию в качестве аргумента была перенесена в другие языки программирования, такие как Джава (начиная с Java 8), С++ (начиная c С++11) и др. как анонимные функции. Примитивная реализация функции fold
на Джаве:
public class Main { interface Function { int apply(int a, int b); } public static int fold(Function function, int acc, int[] xs) { for (int x : xs) { acc = function.apply(acc, x); } return acc; } public static void main(String[] args) { // До версии Java 8 используется анонимный класс // int result = fold(new Function() { // @Override // public int apply(int a, int b) { // return a + b; // } // }, 0, new int[]{1, 3, 2, 4}); // С версии Java 8 можно использовать анонимную функцию (лямбда-функцию) int result = fold((a, b) -> a + b, 0, new int[]{1, 3, 2, 4}); } }
Динамические языки, такие как Питон, JS или Руби имеют возможность присваивать функцию переменной:
def fold(f, acc, xs): for x in xs: acc = f(acc, x) return acc sum = lambda a, b : a + b print sum(2, 3) print fold(sum, 0, [1, 3, 2, 4])
Функции, которые принимают другие функции в качестве аргументов называют функциями высшего порядка или функционалами. Функция fold
в нашем примере является функционалом, так же, как и функция Хаскеля foldr
, используемая в примере выше для сортировки. Достаточно распространённая функция map
применяет некоторую функцию к элементам списка:
main = do let squares = map (\n -> n * n) [1, 2, 3, 4, 5, 6, 7, 8, 9] print squares
Отсутствие побочных эффектов
Побочными эффектами называют неявное изменение значений переменных, не являющимися локальными для выполняемой последовательности команд (функций или процедур). Так, в языке С имеется возможность изменения значений глобальных переменных, в том числе, определённых в других модулях, а в объектно-ориентируемых языках — значений переменной экземпляра класса. В функциональном программировании никакая функция не может изменить состояние. Такие функции называются чистыми, что даёт уверенность в том, что вызов одной и той же функции несколько раз будет возвращать одно и то же значение (Pure Functions, Laziness, I/O, and Monads). К плюсам относят возможность при выполнении программы параллельно выполнять отдельные функции. Наличие чистых функций хорошо для математиков, но в реальности становится неясным, как работать с внешним миром, например, записывать или читать файлы, отправлять какие-либо данные по сети и т.д.[2] Данная проблема будет подробней описана в другой статье.
Ленивые вычисления
Ленивые вычисления означают, что в ходе выполнения программы не будут вычислены те функции, результат которых не влияет на общий требуемый результат. Например, следующий код приводит к ошибке деления на 0:
main = do let x = 30 `div` 0 print (x) -- Main.hs: divide by zero
Но, к примеру, для того, чтобы вычислить длину списка, нет необходимости вычислять сами значения элементов списка. Следующий код не будет вычислять значение и ошибки не произойдёт:
main = do let x = 30 `div` 0 print (length [x])
Другие свойства
Такие свойства как строгая типизация или модульность не является уникальным для функционального программирования явлением. Более подробно о типизации можно прочитать в статье «Ликбез по типизации в языках программирования».