«Анонимные функции, функции как переменные, методы для структур и соответствий, классы/прототипы, наследование! Где это всё?!» — спросите вы меня. И я уверенно отвечу: «Здесь». В этой теме ключ ко всему. Решение небольшое, элегантное, исключительно на встроенном языке 1С и как прививка от коронавируса: молодое, обнадеживающее, многообещающее, но еще не протестированное на массах. Поэтому прошу пока воспринимать всё, как альфа-версию, или даже, как концепцию. Кстати, под словом "Функция" здесь и ниже понимаются и процедуры тоже.
Имея опыт программирования на Python и JavaScript, страдаю, когда в 1С функция не может быть объектом первого класса. То есть, функцию нельзя поместить в переменную. Не результат выполнения функции, а саму функцию. Это лишает возможности передавать её, как параметр, в другие функции или программно создавать объекты c методами. Мы не можем ждать милости от 1С, и добавим нужные механизмы сами!
Материал будет разбит на несколько публикаций. Сегодня рассмотрим основу всей затеи — анонимную функцию.
Анонимные функции
Их еще называют лямбдами (λ). В отличие от привычных функций не имеют имен и определены непосредственно в месте вызова. В остальном точно такие же. Есть случаи, когда их удобнее использовать, чем именованные. Забегая вперед, покажу пару примеров.
//Каждое четное число в массиве умножить на 3
ForEach(Массив, "Если _1 % 2 = 0 Тогда Возврат _1 * 3; КонецЕсли;");
//Отобрать элементы, состоящие из заглавных букв:
НовыйМассив = Filter(Массив, "Возврат _1 = ВРЕГ(_1);");
ForEach и Filter — обычные функции. Как именно они написаны, покажу ниже. Сейчас важно отметить, что они обрабатывают массив с помощью анонимных функций (второй параметр). Это, во-первых, позволяет сократить код без потери читаемости, и, во-вторых, хранить и передавать тело анонимной функции куда нужно.
Пару дней назад я захотел в режиме предприятия написать произвольный код в поле ввода, нажать на кнопку, и чтобы этот код выполнился над списком отобранных объектов. Должен признаться, иногда я тайком использую команды Выполнить/Вычислить и в этот раз на них рассчитывал. Каково же было мое изумление, когда узнал, что эти конструкции не понимают таких важных вещей как: Возврат, Процедура, Функция! Тогда я создал функцию Lambda, которая умеет выполнять код с возвратами.
Lambda(Код, [Аргументы: _1, _2, … _n])
Суть такова. В эту функцию подается код в виде строки и аргументы по необходимости. К аргументам можно обращаться по номерам с подчеркиванием (например, _1). К сожалению, из-за особенности языка 1С максимальное количество аргументов определено заранее (в данных примерах — максимум 4). Все возвраты перед выполнением заменяются на конструкцию с использованием б-гомерзких (да будут они преданы забвению) операторов goto. Таким образом, результат возврата помещается в служебную переменную (ProgmaLambdaResult) и тут же осуществляется переход к концу функции Lambda, где происходит обычный возврат результата. В тексте переданного кода для повышения читаемости допускается заменять две двойные кавычки на символ ` (на клавиатуре, где Ё). Код считается "скомпилированным", если первый символ #.
//Пример использования:
// Lambda вызывается с тремя параметрами:
// Код,
// _1 = СписокОбъектов
// _2 = "Слава Тьюрингу!"
// Результат (Истина или Ложь) вернется в переменную ЕстьПрославлениеТьюринга
//
ЕстьПрославлениеТьюринга = Lambda("
|СтрокаПоиска = ВРЕГ(_2);
|Для каждого Значение из _1 Цикл
| Если Найти(ВРЕГ(Значение.Наименование), СтрокаПоиска) <> 0 Тогда
| Возврат Истина;
| КонецЕсли;
|КонецЦикла;
|Возврат Ложь;
|", СписокОбъектов, "Слава Тьюрингу!"
);
Вот так это сейчас реализовано:
//Функция возвращает результат выполнения Кода. Нумерованные параметры можно использовать для передачи произвольных значений,
// необходимых для выполнени кода. Возвращает Null по умолчанию.
//
// !!! Команда "Возврат" всегда должна писаться с заглавной буквы и закрываться ";" (точкой с запятой)
// !!! В Коде не должно быть команд: Функция, Процедура и их концов. Только тело.
//
//Параметры:
// Код - Строка - тело процедуры или функции
// _1 - произвольное значение (то же самое для _2, _3 ... _n)
//
Функция Lambda(знач Код, _1=Неопределено, _2=Неопределено, _3=Неопределено, _4=Неопределено)
ProgmaLambdaResult = null; // переменная, в которую вернется результат функции (если нужно)
// Код, начинающийся с символа # считается уже скомпилированным
Выполнить ?(Лев(Код, 1) = "#", Сред(Код, 2), CompileCode(Код));
~ProgmaLambdaReturn: // метка, к которой переходит алгоритм при возвратах
Возврат ProgmaLambdaResult;
КонецФункции
//Функция подготавливает код для выполнения в Lambda()
//
//Параметры:
// Код - Строка - код на языке 1С.
// Допускается для обозначений строк внутри кода вместо "" использовать ` (где ё).
// Код, начинающийся с # считается уже скомпилированным.
// Команда "Возврат" всегда должна писаться с заглавной буквы и закрываться ";" (точкой с запятой).
// Все Возвраты вне строк заменяются на специальные метки, необходимые для функции Lambda().
// В Коде не должно быть команд: Функция, Процедура и их концов. Только тело.
//
Функция CompileCode(знач Код)
Если Лев(Код, 1) = "#" Тогда
Возврат Код;
КонецЕсли;
Код = toCode(Код);
_Возврат = "Возврат";
ДлинаСловаВозврат = СтрДлина(_Возврат);
ПозицияВозврата = FindWithinCode(Код, _Возврат);
Пока ПозицияВозврата <> 0 Цикл
КодДо = Лев(Код, ПозицияВозврата-1);
КодПосле = Сред(Код, ПозицияВозврата + ДлинаСловаВозврат);
КонецКоманды = FindWithinCode(КодПосле, ";");
ЗначениеВозврата = Лев(КодПосле, КонецКоманды-1);
Если ПустаяСтрока(ЗначениеВозврата) Тогда // Процедуры
Код = КодДо + "Перейти ~ProgmaLambdaReturn" + КодПосле;
Иначе // Функции
Код = КодДо + "ProgmaLambdaResult =" + ЗначениеВозврата
+ "; Перейти ~ProgmaLambdaReturn;" + Сред(КодПосле, КонецКоманды+1);
КонецЕсли;
ПозицияВозврата = FindWithinCode(Код, _Возврат);
КонецЦикла;
Возврат Код;
КонецФункции
//Функция переводит Значение в вид, необходимый для выполнения в Lambda()
//
//Параметры:
// Значение - произвольное значение
//
Функция toCode(знач Значение)
Если Значение = Неопределено Тогда
Возврат "Неопределено";
КонецЕсли;
Если ТипЗнч(Значение) = Тип("Строка") Тогда
// Декодирование синтаксического сахара
Возврат СтрЗаменить(СокрЛП(Значение), "`", """");
КонецЕсли;
Возврат Значение;
КонецФункции
//Аналог функции Найти, только игнорирует текст внутри кавычек "" и комментарии.
//
//Параметры:
// Стр - Строка - Строка, в которой проводится поиск
// Подстрока - Строка - Строка, которую нужно найти
//
Функция FindWithinCode(Стр, Подстрока)
Накопитель = "";
ДлинаНакопителя = 0;
ДлинаПодстроки = СтрДлина(Подстрока);
ВнутриКавычек = Ложь;
ВнутриКомментрия = Ложь;
Для i = 1 по СтрДлина(Стр) Цикл
Символ = Сред(Стр, i, 1);
Если ВнутриКомментрия Тогда
Если Символ = Символы.ПС или Символ = Символы.ВК Тогда
ВнутриКомментрия = Ложь;
КонецЕсли;
ИначеЕсли Символ = """" Тогда
ВнутриКавычек = не ВнутриКавычек;
ИначеЕсли ВнутриКавычек Тогда
Продолжить;
ИначеЕсли Символ = "/" и Сред(Стр, i+1, 1) = "/" Тогда
ВнутриКомментрия = Истина;
i = i + 1;
Иначе
Накопитель = Накопитель + Символ;
ДлинаНакопителя = ДлинаНакопителя + 1;
Если ДлинаНакопителя = ДлинаПодстроки и Накопитель = Подстрока Тогда
Возврат i - ДлинаПодстроки + 1;
ИначеЕсли Накопитель <> Лев(Подстрока, ДлинаНакопителя) Тогда
Накопитель = "";
ДлинаНакопителя = 0;
КонецЕсли;
КонецЕсли;
КонецЦикла;
Возврат 0;
КонецФункции
Функции CompileCode и toCode выделены из Lambda, потому что будут нужны отдельно. Кроме этого используется вспомогательная функция FindWithinCode, которая работает как команда Найти, но игнорирует стрóки внутри строки и комментарии. Например, в возможной конструкции Возврат "Была выполнена команда Возврат;" при "компиляции" часть "Была выполнена команда Возврат;" должна остаться как есть.
Новые возможности
Используя функцию Lambda можно делать универсальные инструменты.
//Функция вызывает Код для каждого элемента Коллекции. Текущий элемент в Коде находится в переменной _1
//Если Код возвращает значение отличное от Null, в коллекции элемент заменяется.
//
//Например:
// Сообщить каждое значение: ForEach({Массив}, "Сообщить(_1)");
// Умножить каждое четное значение на 3: ForEach({Массив}, "Если _1 % 2 = 0 Тогда Возврат _1 * 3; КонецЕсли;");
//Параметры:
// Коллекция - Массив - пока только массив
// Код - Строка - текущий элемент коллекции находится в переменной _1.
// Возврат значения отличного от Null изменит элемент в коллекции
// _2 - произвольное значение - ..._n Нумерованные параметры можно использовать в Коде
//
Функция ForEach(Коллекция, знач Код, _2=Неопределено, _3=Неопределено, _4=Неопределено)
Код = "#" + CompileCode(Код);
Для i = 0 по Коллекция.ВГраница() Цикл
Значение = Lambda(Код, Коллекция[i], _2, _3, _4);
Если Значение <> null Тогда
Коллекция[i] = Значение;
КонецЕсли;
КонецЦикла;
Возврат Коллекция;
КонецФункции
//Функция возвращает коллекцию, состоящую из элементов Источника, которые удовлетворяют условию описанному в Коде.
//Текущий элемент в Коде находится в переменной _1
//
//Например:
// Отобрать четные числа: Filter({Массив}, "Возврат _1 % 2 = 0;");
// Отобрать строки, набранные заглавными буквами: Filter({Массив}, "Возврат _1 = ВРЕГ(_1);");
// Отобрать элементы, между параметрами _2 и _3 включительно: Filter({Массив}, "Возврат _2 <= _1 и _1 <= _3;", {Значение _2}, {Значение _3});
//
//Параметры:
// Источник - Массив - пока только массив
// Код - Строка - код должен возвращать значение, которое можно преобразовать в Булево
// _2 - произвольное значение - ..._n Нумерованные параметры можно использовать в Коде
//
Функция Filter(Источник, знач Код, _2=Неопределено, _3=Неопределено, _4=Неопределено)
Результат = новый Массив;
Код = "#" + CompileCode(Код);
Для каждого Элемент из Источник Цикл
Если Булево(Lambda(Код, Элемент, _2, _3, _4)) Тогда
Результат.Добавить(Элемент);
КонецЕсли;
КонецЦикла;
Возврат Результат;
КонецФункции
Цена
Заметно облегчая написание и чтение кода, данные анонимные функции, конечно, замедляют работу программы. Разница между вызовом классического кода и такого же, но командой Выполнить, минимум в 10 раз! На моих тестах замедление доходило почти до 25 раз. Но это всего лишь означает, что функции, созданные по данной технологии должны вызываться меньшее количество раз. Например, мою функцию ForEach можно переписать так, чтобы цикл был внутри лямбда-кода, тогда лишние сотые доли секунды, нужные директиве Выполнить на интерпретацию станут незаметными.
Отладка лямбда-кода затруднительна. Но анонимные функции не должны быть сложными.
«Боже! Боже! Это настолько нестандартно! Так на 1С не пишут! Кто нам позволит?» — скажут зануды. Вообще-то платформа 1С ценна не красотой и богатством синтаксиса, а распространенностью. А программирование само по себе подразумевает создание нужных абстракций, упрощающих работу. Я всего лишь планирую добавить десяток маленьких, но мощных функций, которые сильно увеличат возможности выражения мысли при кодировании.
Перспектива
Головокружительная! Уже сейчас я закончил функции, которые позволяют создавать специальные структуры, которые можно поместить в переменные и вызвать в другом месте. Эти структуры-функции можно вставлять в другие универсальные коллекции и вызывать с доступом к самому объекту-носителю (как self в Python или this в JS). Таким образом остается написать инструменты, которые будут понимать, что делать со свойством "Prototype" у объекта, и здравствуй, ООП!
// Тестируемый сейчас вариант создания и вызова функций
// newFunction(Параметры, Код) - создает специальную структуру
// Call(Структура, [Параметры]) - вызывает переданную функцию либо функцию из свойства Структуры
// Можно создать "функцию" и поместить её в переменную. Обычный код был бы таким:
// Функция ПервыйВВыборке(Выборка, ЗначениеПоУмолчанию=Неопределено)
// Возврат ?(Выборка.Следующий(), Выборка, ЗначениеПоУмолчанию);
// КонецФункции
//
ПервыйВВыборке = newFunction("Выборка, ЗначениеПоУмолчанию=Неопределено", // параметры функции
"Возврат ?(Выборка.Следующий(), Выборка, ЗначениеПоУмолчанию);" // код функции
);
Выборка = КакойТоКодДляПолученияДанных();
Call(ПервыйВВыборке, Выборка); // Данные выборки или Неопределено
Call(ПервыйВВыборке, Выборка, Ложь); // Данные выборки или Ложь
// Можно назначить структуре "метод"
_Объект = новый Структура("Имя, Фамилия, Представиться",
"Anton",
"Progma",
newFunction("Начало=`Hello! `", "Сообщить(Начало + `My name is ` + _.Имя + ` ` + _.Фамилия);"
)
Call(_Объект, "Представиться", "Hi."); // Hi.My name is Anton Progma
Call(_Объект, "Представиться"); // Hello! My name is Anton Progma
Пока этому всему пара суток отроду. Буду рад любым объективным мнениям. Прикрепил обработку для тестирования. Попрошу модераторов сделать её бесплатной для скачивания. Описание файла: обработка на управляемых формах с примером использования Lambda, ForEach и Filter (кнопки в командной панели формы). Слева — список элементов, который будут использовать ForEach и Filter. Посередине — область для лямбда-кода (оформляется по правилам 1С). Над областью кода — поле для списка параметров. Справа — лог, куда будет выводиться результат выполнения кода. Файл тестировался на платформе 8.3.16.1814
Планирую выпустить еще минимум 3 статьи по данной теме:
- Вспомогательные инструменты для упрощения кодирования и чтения.
- Функции первого класса, прототипы, наследование и, возможно, замыкания.
- Модули.
В перерывах жонглеры и фокусы!