Не претендую на превращение 1С в функциональный язык... но функцию первого класса удалось соорудить.
И нет, здесь не будет ничего про лямбды. И никакого "кода в кавычках".
(Но если вам интересны лямбды - пишите в комментариях, у меня и лямбды имеются)
Немного теории
(Те, кто знаком с функциями первого класса, могут пропустить этот раздел)
Всем известно, что мы можем хранить данные в переменных и передавать их в параметрах функций. Также известно, что концепция объекта позволяет нам хранить и передавать не только данные, но и поведение, определенное в виде методов объекта.
В большинстве современных языков есть еще один способ хранить и передавать поведение - это так называемые "функции первого класса" (first-class functions).
Первое, что отличает функцию 1-го класса от обычной функции: ее можно передать в параметре другой функции или присвоить переменной. Именно саму функцию, а не ее результат. По сути, это означает передачу ссылки на функцию, а далее можно вызвать функцию по этой ссылке, передав ей требуемые параметры, и получить результат. В функциональных языках все функции по умолчанию обладают этим свойством. Но и во многих императивных языках есть такая возможность.
В терминах ООП можно сказать, что функция 1-го класса - это эквивалент объекта с одним единственным методом. Но при этом не требуется специально описывать отдельный класс - достаточно описать только функцию.
Второе важное понятие - замыкание (closure). Это способность функции 1-го класса использовать переменные окружающего контекста, существовавшие в момент ее создания, даже когда она вызывается в совершенно другом контексте. В языках, поддерживающих замыкание на уровне синтаксиса, это действительно выглядит так, что функция В, созданная внутри функции А, ссылается на локальные переменные функции А. Хотя реально вызов функции В может происходить где-то в функции С, куда функцию В передали в качестве параметра.
Если отбросить синтаксический сахар, то суть замыкания в том, что сторона, создавшая функцию, может параметризировать ее алгоритм своими локальными значениями. Но не путем передачи их в параметрах функции (поскольку она лишь объявляет, но не вызывает функцию), а путем инкапсуляции этих значений вместе с функцией в единый объект.
Таким образом, второе важное свойство функции 1-го класса: ее алгоритм может получать одну часть параметров от стороны, создающей функцию, а другую часть параметров - от стороны, вызывающей функцию.
Функтор
Мое решение предназначено для использования на сервере, а синтаксически максимально приближено к "функциональному" виду. Я назвал его "Функтор", чтобы не путать с традиционными функциями. К тому же, мой функтор более всего похож на класс-функтор в С++, хотя и имеет от него ряд отличий. Впрочем, в разных языках программирования термин Функтор обозначает совершенно разные концепции, вот и у меня будет свой функтор с блэкджеком и гуриями.
Создается функтор так:
Функ = Функтор(<Объект>, <ИмяФункции>, <Параметр>, <Параметр>, ...);
<Объект> - Любой объект, содержащий функции. Это может быть общий модуль, модуль менеджера, экземпляр объекта конфигурации или объекта платформы.
<ИмяФункции> - Строка - Имя функции в объекте. Важно, чтобы это была именно функция. Использование процедуры приведет к ошибке при попытке ее вызова.
<Параметр>, <Параметр>, ... - Произвольные параметры, максимум 8 штук. Это на 1 больше, чем максимальное количество параметров, рекомендуемое стандартами 1С (но если мало, можете добавить).
Параметры соответствуют параметрам функции и служат для создания контекста замыкания.
На этом следует остановиться подробнее.
В 1С нет другого способа передать функции параметры, кроме как через параметры (ваш Кэп). Поэтому контекст замыкания мы тоже передаем через параметры. В определении функции должны быть предусмотрены соответствующие параметры для этого. Значения параметров, переданные при создании функтора, будут в нем инкапсулированы, а затем использованы при вызове функции.
При определении функции рекомендуется в начале списка параметров располагать параметры, которые будут передаваться вызывающей стороной, а в конце списка - параметры контекста замыкания. Соответственно, если функция имеет параметры, передаваемые вызывающей стороной, то при создании функтора мы их можем пропустить, оставив их значения неопределенными. Например, если вызывающая сторона передает 2 параметра, а создающая сторона - еще 2 параметра контекста, создание функтора будет выглядеть так:
Функ = Функтор(<Объект>, <ИмяФункции>, ,, <ПараметрКонтекста1>, <ПараметрКонтекста2>);
А вот так происходит вызов функтора:
Результат = Функ.Вызвать(<Параметр>, <Параметр>, ...);
Здесь также можно передать до 8 параметров, они соответствуют все тем же параметрам функции. Но имеют приоритет перед параметрами контекста замыкания: если значение параметра указано и при создании функтора, и при его вызове, то фактически функции будет передано значение, указанное при вызове. Такой подход позволяет при создании функтора определить для параметров значения по умолчанию, которые затем могут быть переопределены вызывающей стороной (по сути, все параметры контекста - это и есть значения по умолчанию).
Если функция имеет параметры, передаваемые "по ссылке" (т.е. без Знач), то она может возвращать значения в параметры контекста, инкапсулированные в функторе. Таким образом функция может передавать значения стороне, создавшей функтор (впрочем, их может получать любая сторона, владеющая ссылкой на функтор).
Получать эти значения можно так:
Значение = Функ.Значение(<Индекс>);
<Индекс> - индекс параметра функции в диапазоне [0...7].
Нужно иметь в виду, что значения возвращаемых параметров, используемых вызывающей стороной, попадут не в контекст, а в переменные вызывающей стороны.
Есть одна неочевидная особенность. Если в каком-то параметре передать значение Неопределено, то функтор будет считать этот параметр неиспользуемым. Это вынужденная мера, т.к. функтор не знает, сколько в действительности параметров у вызываемой функции. Если функция имеет возвращаемый параметр, и мы передадим в него переменную, содержащую Неопределено, в ожидании, что в эту переменную что нибудь вернется, то нас ждет облом: ничего туда не вернется, т.к. функтор увидит только значение Неопределено и посчитает параметр неиспользуемым. В таких случаях нужно обязательно инициализировать переменную каким-нибудь определенным значением.
ДОПОЛНЕНО. В комментариях tormozit предложил иедю, как купировать эту проблему: в реализации функтора заменить все проверки на Неопределено проверками на Null.
Давайте перейдем к примерам практического применения функторов.
Пример 1. Ленивые вычисления
Я не буду здесь приводить навязший в зубах пример с бесконечными последовательностями, так любимый функциональщиками. Возьмем что-нибудь, более приближенное к практике 1С.
Пускай у нас есть функция в общем модуле, выполняющая некоторый общий алгоритм, так и назовем ее - ОбщийАлгоритм. Вызывающая сторона должна передать ей в параметре значение. Предположим, что для получения этого значения нужно перелопатить достаточно большой объем данных из базы. Другими словами, вычисление этого значения обходится дорого. Но ОбщийАлгоритм таков, что использует это значение только при определенных условиях. Предположим, что статистически вероятность использования этого значения - 40%. Если мы будем заранее вычислять значение, чтобы передать его в ОбщийАлгоритм, то в 60% случаев мы будем делать это зря.
Решение: оформим тяжелое вычисление в виде функции и будем передавать ее в ОбщийАлгоритм как функтор.
Общий модуль:
Функция ОбщийАлгоритм(КонтейнерЗначения) Экспорт
//...
Если УсловиеВыполняется Тогда
Значение = КонтейнерЗначения.Вызвать();
//
КонецЕсли;
//...
КонецФункции
Вызывающий модуль:
Функция ТяжелаяФункция(Парам1, Парам2) Экспорт
//...массивные вычисления...
КонецФункции
Процедура ...()
Результат = Вычисления.ОбщийАлгоритм(Функтор(ЭтотОбъект, "ТяжелаяФункция", Знач1, Знач2));
КонецПроцедуры
В этом примере мы используем функтор как контейнер ленивого вычисления. Все параметры задает создающая сторона, а вызывающая сторона просто получает результат - но только тогда, когда он действительно понадобится.
Обратите внимание, что ТяжелаяФункция объявлена как экспортная, т.к. она должна быть доступна для вызова из другого модуля.
Пример 2. Универсальный обход дерева.
Предположим, мы активно работаем с деревом значений. Время от времени нам нужно рекурсивно обходить все узлы дерева и с каждым узлом выполнять некоторое действие. Дерево одно, а действий много и все разные.
Пускай наше дерево содержит, среди прочих, колонки с числами и колонку Пометка (Булево). В примере реализуем несколько операций. Одна будет вычислять максимум значения численной колонки по всем узлам дерева. Вторая будет собирать массив всех узлов, где Пометка = Истина. Третья будет устанавливать значение Пометка во всех узлах.
Начнем с универсальной функции рекурсивного обхода дерева:
// Это универсальная функция, ее можно было бы переместить в общий модуль
Функция ОбходДерева(Узел, Действие)
Для каждого Строка Из Узел.Строки Цикл
Действие.Вызвать(Строка);
ОбходДерева(Строка, Действие);
КонецЦикла;
Возврат Действие;
КонецФункции
Функция обхода получает на вход дерево и функтор действия, который применяет к каждому узлу дерева. В качестве значения возвращает тот же функтор действия, это повышает удобство пользования (ниже станет понятно, почему).
Теперь функции, реализующие действия со строкой дерева:
Функция ДействиеМаксимумПоКолонке(Строка, Колонка, Максимум) Экспорт
Максимум = Макс(Строка[Колонка], Максимум);
Возврат Неопределено;
КонецФункции
Функция ДействиеПомеченныеУзлы(Строка, Узлы) Экспорт
Если Строка.Пометка Тогда
Узлы.Добавить(Строка);
КонецЕсли;
Возврат Неопределено;
КонецФункции
Функция ДействиеУстановитьПометку(Строка, Пометка) Экспорт
Строка.Пометка = Пометка;
Возврат Неопределено;
КонецФункции
И, наконец, фронтальные операции:
Функция МаксимумПоКолонке(Дерево, Колонка) Экспорт
Возврат ОбходДерева(Дерево, Функтор(ЭтотОбъект, "ДействиеМаксимумПоКолонке",, Колонка, 0))
.Значение(2); // параметр Максимум
КонецФункции
Функция ПомеченныеУзлы(Дерево) Экспорт
Возврат ОбходДерева(Дерево, Функтор(ЭтотОбъект, "ДействиеПомеченныеУзлы",, Новый Массив))
.Значение(1); // параметр Узлы
КонецФункции
Процедура УстановитьПометку(Дерево, Пометка) Экспорт
ОбходДерева(Дерево, Функтор(ЭтотОбъект, "ДействиеУстановитьПометку",, Пометка));
КонецПроцедуры
В этом примере мы используем функтор не для возврата значения вызывающей стороне, а для накопления некоторых данных в контексте замыкания, с последующим их возвратом создающей стороне, либо для модификации переданного параметра.
Пример выглядит тривиальным, но представьте, что таких действий с деревом нужно реализовать десяток. А если нужно при обходе еще и фильтровать узлы дерева по некоторому условию? Которое можно передать универсальной процедуре обхода в виде еще одного функтора. А можно передать функтор условия не процедуре обхода, а функтору действия...
В общем, функторы позволяют реализовывать сложное поведение путем комбинирования его из простых элементов. Способы комбинирования ограничены только вашим воображением.
Что под капотом?
Под спойлером реализация функтора, совершенно бесплатно.
Вступайте в нашу телеграмм-группу Инфостарт