Система Orphus
Версия для печати

LINQ как шаг к функциональному программированию

Автор: Чистяков Влад (VladD2)
The RSDN Group

Источник: RSDN Magazine #2-2008
Опубликовано: 28.08.2008
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Базис ФП – функция
Манипуляция функциями (ссылки на функции)
Тип функции
Делегаты
Анонимные методы и лямбда-выражения (или просто «лямбды»)
Зачем нужны лямбда-выражения?
ФП и SQL
Немного о проблемах делегатов
Кирпичики – или базовые «Функции высшего порядка» (ФВП)
Работа со списками
Объединяем все вместе
Вместо заключения

Введение

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

Меж тем, функциональный подход может сделать ваш код более кратким и понятным. Это позволит решать более сложные задачи и делать при этом меньше ошибок. Но чтобы иметь возможность читать (и писать) функциональный код надо быт знакомыми с основными приемами ФП.

Цель этой статьи максимально просто объяснить императивному программисту основы функционального программирования (далее ФП).

Примеры в этой статье будут даваться в основном на C#.

ПРЕДУПРЕЖДЕНИЕ

В этой статье будут приводиться примеры, в основном связанные с «LINQ to object», так как именно LINQ to object является прямым аналогом функционального программирования. «LINQ to SQL» использует похожий код, но реально генерирует аналог абстрактного синтаксического дерева (эдакого CodeDom), по которому специализированными драйверами генерируется код SQL-запросов. Это выходит за рамки этой статьи, так что если вы встретите пример, реализацию некого метода или термин «LINQ», то знайте, что по умолчанию, речь идет о «LINQ to object».

Базис ФП – функция

Даже из названия «функциональное программирование» ясно, что основной упор в нем делается на функции. Функциональное вычисление – функция (или если быть точнее – «чистая функция», pure function) – должна принимать некоторые аргументы (входящие данные) на входе, производить вычисление и возвращать некоторый результат. При этом функция не должна создавать никаких побочных эффектов. Под побочными эффектами понимается:

  1. Изменять глобальные (статические в терминах C#) переменные.
  2. Вызывать другие функции которые могут создать побочный эффект.
  3. Заниматься любым вводом/выводом (да-да, не удивляйтесь).
  4. Посылать или принимать некие сообщения.

По сути, пункты 2-4 представляют собой разновидности одного и того же – изменение состояния посредством вызова.

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

У такого подхода есть одна замечательная особенность. Функция всегда должна возвращать один и тот же результат для одних и тех же аргументов. Замечательна эта особенность сразу по нескольким причинам.

  1. Легкость отладки: А. Такую функцию проще проверить тестами, так как она зависит только от параметров. Б. Все состояние (если его так можно назвать) при функциональных вычислениях располагается в стеке, так что его легко анализировать, и даже можно производить пошаговую отмену и повтор действий.
  2. Результаты работы функции можно кэшировать, что позволяет существенно повысить скорость работы некоторых алгоритмов.
  3. Легкость распараллеливания. Два вызова одной и той же функции совершенно независимы и могут выполняться параллельно. Кроме того, потенциально выполнение функции может быть автоматически распараллелено компилятором.
  4. Имеется потенциальная возможность доказать правильность функции. Данная область хорошо изучена математиками, и найдены формальные механизмы доказательства корректности функциональных программ. К сожалению, на практике такого ПО немного, и оно в основном носит исследовательский характер.
  5. Высокая повторная используемость функций. Независимость опять же играет на руку, и однажды написанную функцию легче использовать в другом месте программы (или в другой программе).

Фактически, отсутствие побочных эффектов – это заслуга не самой функции, а содержащихся в них выражений. Ведь именно в них требуется не допускать побочных эффектов. Меж тем в ИЯ имеется множество конструкций создающих побочные эффекты. Главная из них, я бы даже сказал, основополагающая – это присвоение. Именно присвоение приводит к изменению памяти. Остальные конструкции, в основном, только способствуют или не способствуют применению присвоения. Так, среди способствующих конструкций можно перечислить:

  1. Out- и ref-параметры.
  2. Циклы.
  3. Условные выражения, не позволяющие возвратить значения (а это все выражения C#, за исключением оператора «условие ? выражение1 : выражение2».
  4. Способствует присвоениям и тот факт, что в C# нельзя возвратить из функции множество разнотипных значений, если только не поместить все их в некий объект (что приводит к множеству лишних действий и загромождению кода) или массив с элементами типа object (что совсем не здорово).

Впрочем, изменение локальных переменных не создают столько проблем, как изменение глобальных переменных и вообще изменение глобального состояния.

Манипуляция функциями (ссылки на функции)

Даже очень старые ИЯ, например, C и Паскаль, поддерживают самую базовую идею ФП – возможность манипуляции функциями. Однако в большинстве даже современных ИЯ реализована эта возможность совсем слабо. Обычно все ограничивается возможностью передать указатель на тело функции. Скажем, в С и C++ имя глобальной функции интерпретируется как указатель на нее.

ФЯ развивают эту идею, возводя ее в принцип – функция является в программе таким же полноценным объектом, как и экземпляр любого другого типа (например, экземпляр строки). Ей должно быть так же удобно манипулировать, как обычными объектами. Скажем, в С мы можем передать указатель на функцию другой функции, но составить из двух функций третью мы не в силах. Невозможно в С и объявить функцию по месту. В ФЯ же все это возможно. Манипулировать функциями в ФЯ очень просто и удобно.

Тип функции

Первое, что требуется для манипуляции функциями – это иметь возможность описать их тип. В ФЯ для функций был введен специальный тип – функциональный тип. Обычно он описывается как список типов параметров функции и список ее возвращаемых значений. Впрочем, тут возможны варианты, так как функция с несколькими параметрами может быть описана как функция с одним параметром, возвращающая функцию с другим параметром. Скажем, если для описания возвращаемого значения функции использовать знак «->», а для перечисления значений - «*», то функцию с сигнатурой:

        string Function(int x, string y);

Можно описать как:

        int * string -> string

или как:

        int -> string -> string

то есть как функцию, которая получает int и возвращает функцию, которая получает string и возвращает тоже string. Я понимаю, что это очень непривычно и непонятно (на первый взгляд), но поверьте, что есть целая теория обосновывающая это (теория «лямбда-исчислений» Чёрча). Нам все эти тонкости не важны. Зато нам важно, как обстоят дела с аналогом функционального типа в C#.

Делегаты

В C# аналогом ссылки на функцию являются делегаты. Сама по себе функция (метод) или ее имя не является чем-то, чем можно было бы манипулировать, но если поместить ее в делегат, появится возможность ссылаться на нее, а значит передавать в другие функции и возвращать функции из других функций. Таким образом, можно (с большой натяжкой) сказать, что делегаты являются аналогами функциональных типов в ФЯ. Однако они не являются полными аналогами, что создает некоторые проблемы при использовании делегатов в качестве функционального типа. Чуть позже я опишу проблемы делегатов, но сначала я расскажу о нововведении C# 3.0 – о лямбда-выражениях.

Анонимные методы и лямбда-выражения (или просто «лямбды»)

Зачем нужны лямбда-выражения?

ФП и SQL

Немного о проблемах делегатов

Кирпичики – или базовые «Функции высшего порядка» (ФВП)

Работа со списками

Свертка: Fold, FoldLeft, FoldRight, Reduce, Aggregate

Другие агрегатные функции

Отображение: Map, Convert, ConvertAll, Select

Фильтрация: Filter, Where

Разворот списка: Rev, Reverse

Сортировка: Sort, (в LINQ: OrderBy, OrderByDescending, ThenBy и ThenByDescending)

Спрямление: Flatten, SelectMany

Группирование: Group, GroupBy

Объединяем все вместе

Вместо заключения


Полная версия этой статьи опубликована в журнале RSDN Magazine #2-2008. Информацию о журнале можно найти здесь