Одной из основ функционального программирования является тот факт, что функция (сама по себе, а не только результат ее вычисления) рассматривается как объект, с которым можно работать - сохранять, преобразовывать, вычислять. В 1С такого нет - нет указателей на функцию (как в Си), нет ссылок на функции, нельзя создать объект-функцию. Хотя вроде бы есть платформенные функции Выполнить(<Строка>) и Вычислить(<Строка>), с помощью которых можно вызывать функции по имени, но проблем с их использованием столько, что такой подход вряд ли удобен. Так что же, других вариантов нет? Обычно мы вызываем функцию следующим образом <Модуль>.<Имя функции>. Посмотрим на это выражение с другой стороны. Мы не можем использовать <Имя функции> как объект, но ведь <Модуль> - можем! Таким образом, используя в качестве "функций" общие модули 1С и разместив в них алгоритм вычисления, мы можем использовать эти "функции" как объекты.
В 1С есть фундаментальная проблема - имена общих модулей могут конфликтовать с именами других идентификаторов в программе. Добавление знака "_" в имя общего модуля, хоть это и противоречит стандартам 1С, позволит снизить остроту этой проблемы.
Чтобы избежать путаницы, далее в статье и в примерах под понятием "функция" будет пониматься "функция ФП" (функционального программирования) в отличие от "функция 1С" - элемента языка 1С. Функцию, реализованную в виде общего модуля назовем "оператор" - такие функции послужат "кирпичиками" для построения более сложных функций.
1. Функции высшего порядка и каррирование
Рассмотрим обычное сложение Сумма(10, 1). Это функция с двумя переменными, возвращающая при вычислении число 11. А что будет, если мы попытаемся вычислить Сумма(1)? В мире 1С такая конструкция даст ошибку, а в мире ФП - вернет объект - функцию увеличения на 1, т.е. инкремент: Инкремент = Сумма(1). В вызов полученной функции (уже только от одного аргумента) мы можем подставить любое число, или, как принято говорить, применить функцию к любому числу: Инкремент(10) // =11, Инкремент(27) // =28
Вот так это выглядит в прилагаемой конфигурации:
ФункцияСложения = НоваяФункция(_Сложить,10,1);
Результат = ВыполнитьФункцию(ФункцияСложения);
Сообщить(Результат); // =11
// ... или
ФункцияСложения = НоваяФункция(_Сложить);
Инкремент = ПрименитьФункцию(_Сложить,1); // в Инкремент записалась функция
Результат = ПрименитьФункцию(Инкремент, 10);
Сообщить(Результат); // =11
С технической точки зрения функция здесь - это массив, первым элементом которого выступает операция или другая функция, а остальные элементы - ее фактические аргументы.
Такое преобразование функции от нескольких аргументов в функции от одного аргумента называется каррирование, или карринг, а функции, принимающие или возвращающие другие функции как объекты, - функции высшего порядка.
Продолжаем.
Функция, умножающая аргумент на 3:
Функция3x = НоваяФункция(_Умножить, 3);
Функция, делящая аргумент на 2:
Функция05x = НоваяФункция(_Умножить, 0.5);
Функция, вычисляющая 3*x+1. Здесь нам надо результат умножения подать на "вход" функции инкремента, как на конвейере:
Функция3x1 = НоваяФункция(_Конвейер, Функция3x, Инкремент);
Более сложная функция - проверяет аргумент на четность:
ФункцияЭтоЧетное = НоваяФункция(_Конвейер, НоваяФункция(_Остаток, 2), НоваяФункция(_Равно, 0));
Обратите внимание: все вышеприведенные примеры возвращают функцию, которую потом можно применить к любому аргументу.
А вот функция, возвращающая другую функцию в зависимости от аргумента:
ФункцияПоУсловию = НоваяФункция(_Конвейер, ФункцияЭтоЧетное, НоваяФункция(_Условие, Функция05x, Функция3x1));
Если аргумент четный - то вернется функция деления на 2, если нечетный - функция 3*x+1.
Теперь у нас все готово, чтобы заняться сиракузской проблемой (она, конечно же, не имеет отношение к ФП, а используется только для примера):
ФункцияСложения = НоваяФункция(_Сложить);
Инкремент = ПрименитьФункцию(_Сложить, 1);
Функция3x = НоваяФункция(_Умножить, 3);
Функция05x = НоваяФункция(_Умножить, 0.5);
Функция3x1 = НоваяФункция(_Конвейер, Функция3x, Инкремент);
ФункцияЭтоЧетное = НоваяФункция(_Конвейер, НоваяФункция(_Остаток, 2), НоваяФункция(_Равно, 0));
ФункцияПоУсловию = НоваяФункция(_Конвейер, ФункцияЭтоЧетное, НоваяФункция(_Условие, Функция05x, Функция3x1));
Значение = 27;
ПрименитьФункцию(_ВывестиЗначение, Значение);
Пока Значение > 1 Цикл
Значение = ПрименитьФункцию(ПрименитьФункцию(ФункцияПоУсловию, Значение), Значение);
ПрименитьФункцию(_ВывестиЗначение, Значение);
КонецЦикла;
Здесь в цикле на основе переменной-аргумента Значение выбирается одна из двух функций: 3*x+1 или x/2, и она же применяется к этому аргументу.
2. Массивы и map, filter, reduce.
Так как мы научились работать с функциями высшего порядка, нам ничего не мешает реализовать и обычные для современных языков функции map, filter, reduce. Но поскольку мы пишем все-таки на русском языке, назовем их ПрименитьДляКаждого, Фильтр, Свернуть. Напомню, все эти функции применяются для коллекций (в нашем случае для массива). ПрименитьДляКаждого (map) возвращает массив, к каждому элементу которого применена функция-аргумент. Фильтр (filter) - возвращает массив, в котором содержатся только элементы, удовлетворяющие преданному в аргументе условию. Свернуть (reduce) возвращает значение, полученное обработкой каждого элемента массива с накоплением промежуточного "итога", так можно подсчитать, например, сумму элементов.
Реализация этих функций несложна:
Функция ПрименитьДляКаждого(Знач Массив, Знач ФункцияФП)Экспорт
Результат = Новый Массив;
Для каждого Элемент Из Массив Цикл
Результат.Добавить(ПрименитьФункцию(ФункцияФП, Элемент));
КонецЦикла;
Возврат Результат;
КонецФункции
Функция Фильтр(Знач Массив, Знач ФункцияФП)Экспорт
Результат = Новый Массив;
Для каждого Элемент Из Массив Цикл
РезультатСравнения = ПрименитьФункцию(ФункцияФП, Элемент);
Если ЗначениеЗаполнено(РезультатСравнения) И РезультатСравнения <> Ложь И РезультатСравнения <> 0 Тогда
Результат.Добавить(Элемент);
КонецЕсли;
КонецЦикла;
Возврат Результат;
КонецФункции
Функция Свернуть(Знач Массив, Знач ФункцияФП, Знач НачальноеЗначениеИтога=NULL)Экспорт
Результат = НачальноеЗначениеИтога;
Для каждого Элемент Из Массив Цикл
Результат = ПрименитьФункцию(ВставитьПервыйАргументВФункцию(ФункцияФП, Результат), Элемент);
КонецЦикла;
Возврат Результат;
КонецФункции
Примеры использования:
Массив1 = ПримерЧисловогоМассива();
Сообщить("Исходный массив:");
ПрименитьДляКаждого(Массив1, _ВывестиЗначение);
Сообщить("Массив элементов, увеличенных на 1:");
Инкремент = ПрименитьФункцию(_Сложить, 1);
Массив2 = ПрименитьДляКаждого(Массив1, Инкремент);
ПрименитьДляКаждого(Массив2, _ВывестиЗначение);
Сообщить("Массив элементов, больших 5:");
Условие = НоваяФункция(_Больше, 5);
Массив2 = Фильтр(Массив1, Условие);
ПрименитьДляКаждого(Массив2, _ВывестиЗначение);
Сообщить("Сумма: " + Свернуть(Массив1, НоваяФункция(_Сложить), 0));
Сообщить("Минимум: " + Свернуть(Массив1, НоваяФункция(_Минимум), Массив1[0]));
Сообщить("Максимум: " + Свернуть(Массив1, НоваяФункция(_Максимум), Массив1[0]));
Обращаю ваше внимание, что Свернуть (reduce) может возвращать значение любого типа, а не только простого. В зависимости от переданной функции можно реализовать генерацию сложных структур - массивов, связанных списков, B-tree и т.п.
3. Сортировка
В 1С нет функции сортировки массива. Может быть потому, что элементами массива могут быть значения самых разных типов, например структуры? Используя функции высшего порядка, такую сортировку можно реализовать. Где здесь использовать такие функции? Во-первых, для элементов массива, в зависимости от их типов, нужна функция получения значения, пригодного для сравнения. А во-вторых, нужна функция сравнения.
Функция Сортировать(Знач Массив, Знач ФункцияПолученияЗначения, Знач ФункцияСравнения)Экспорт
Результат = Массив;
// проверим, может массив уже отсортирован, и если нет - найдем элемент, который точно находится не по порядку
НадоСортировать = Ложь;
Для Индекс = 0 По Массив.ВГраница() - 1 Цикл
Значение1 = ПрименитьФункцию(ФункцияПолученияЗначения, Массив[Индекс]);
Значение2 = ПрименитьФункцию(ФункцияПолученияЗначения, Массив[Индекс + 1]);
Если ВыполнитьФункцию(НоваяФункция(ФункцияСравнения, Значение1, Значение2)) Тогда
НадоСортировать = Истина;
Прервать;
КонецЕсли;
КонецЦикла;
Если НадоСортировать Тогда
// делим массив на две части относительно найденного элемента, сортируем эти части отдельно и объединяем их
ФункцияСравненияЗначения = НоваяФункция(_Конвейер, ФункцияПолученияЗначения, НоваяФункция(ФункцияСравнения, Значение2));
Массив1 = Фильтр(Массив, НоваяФункция(_Конвейер, ФункцияСравненияЗначения,_Не)); // Массив значений не "бОльших" заданного значения
Массив2 = Фильтр(Массив, ФункцияСравненияЗначения); // Массив значений "бОльших" заданного значения
Результат = Объединить(Сортировать(Массив1, ФункцияПолученияЗначения, ФункцияСравнения),
Сортировать(Массив2, ФункцияПолученияЗначения, ФункцияСравнения));
КонецЕсли;
Возврат Результат;
КонецФункции
Пример для массива чисел:
Массив1 = ПримерЧисловогоМассива();
Массив2 = Сортировать(Массив1, Неопределено,_Больше);
ПрименитьДляКаждого(Массив2, _ВывестиЗначение);
Здесь второй аргумент для сортировки говорит о том, что нам не нужна дополнительная функция получения значения.
А если в массиве будут не числа, а, например, Структура("Фамилия,Имя,Отчество")? Нет проблем, надо лишь определить функцию, которая из структуры получит значение, пригодное для сравнения и передать ее в функцию сортировки.
Массив1 = ПримерМассиваСтруктур();
Массив2 = Сортировать(Массив1, _ФИОВСтрокуВрег, _Больше);
ПрименитьДляКаждого(Массив2, _ВывестиСтруктуру);
В заключение надо сказать, что далеко не все принципы функционального программирования можно реализовать так легко. Отсутствует возможность реализовать анонимные лямбда функции (пожалуйста, не используйте для этого Выполнить, это не хорошо), есть проблема с определением передачи аргумента: передавать как функцию или же как результат вычисления функции (в предлагаемой реализации аргументы-функции не вычисляются, поэтому пришлось использовать конвейер), сложно красиво организовать рекурсию, да и вообще, отсутствие "синтаксического сахара" делает код далеким от лаконичности. Тем не менее, я думаю, часть этих принципов реализовать удалось.
И еще, важное замечание: в реальных проектах этот и подобные подходы используйте с большой осторожностью! Не надо этого делать без крайней необходимости, просто чтобы показать "посмотрите, как я могу!". Подумайте о тех, кто будет сопровождать такой проект.
К статье приложена конфигурация с "движком" и примерами. Предполагается, что код в ней - не более чем образец, вопросы оптимизации не рассматривались, "защиты от дурака" почти нет. Проверялась на платформе 8.3.18.1363.
Как всегда, приветствуются замечания / дополнения / комментарии.
Следующие статьи:
- Если хочется ООП с наследованием и полиморфизмом
- Если хочется низко-низкоуровневого программирования с битами и байтами