Программирование на Athanor - 9 (косвенные ссылки и вызовы)

Цикл: "Программирование на Athanor": Часть 9 (Ссылки на функторы и функциональные значения)
15 февраля 2026, воскресенье 23:05
trilirium для раздела Блоги

Ссылки на функторы, косвенные вызовы и лямбда-выражения

Все вызовы функторов — как системных, так и пользовательских — которые мы пока рассматривали, были непосредственными, или прямыми. Это означает, что аргументы могут меняться — а вот сам вызываемый функтор жёстко прописан в коде. Намного большую гибкость обеспечивает механизм косвенных вызовов. Но для этого, нужен новый тип значений — функциональные, т.е. ссылки на функторы.

Нам уже прекрасно знакомы прямые вызовы — в самом общем случае, их синтаксис выглядит так:

  • func^arguments

Здесь func — это вызываемый функтор, а arguments — его аргумент(ы). Очень часто аргументом вызова является список в круглых скобках — и в этом случае можно опустить символ "^" между func и списком (что мы всегда и делали). Однако, этот символ необходим, если аргументом является что-то отличное от списка в круглых скобках. Например, нередко необходимо вызвать функтор с единственным операндом: блоком. В статье 4 мы уже рассматривали встроенный итератор ever, реализующий бесконечный цикл (а точнее, цикл без явного условия завершения). А его единственным операндом чаще всего является блок: конечно, можно написать и ever ({ S0; ... SN }) — но ever^{ S0; ... SN } чуть компактнее, и избавляет нас от ненужной пары круглых скобок.

Но наиболее существенный вклад в борьбу с ненужными скобками — эта запись вносит в ситуации, когда аргументом функтора также является вызов (и эти вызовы могут быть вложенными очень глубоко). В статье 5 мы уже рассматривали такую нетипичную операцию, как редукция (и приводили примеры: reduce (add (List)) — возвращает сумму всех элементов в списке List, интерпретируемых, как числовые скаляры). Однако, это же удобнее записать, как reduce^add (List). Это особенно полезно и при обращении к сложным списковым структурам, с использованием l_head и l_tail. Например, сложные операции доступа вроде l_head(l_tail(l_head (L))) — безусловно, удобнее записывать как l_head^l_tail^l_head (L).

И ещё немного про полезные сокращения. В случае, когда операндом для унарного функтора (неважно, встроенного или пользовательского!) является литерал (числовой или строковый) — можно опустить и "^", и круглые скобки (нужен лишь минимум один пробельный символ). Например: sqr 2.0 (возвращает 1.1412136) или s_len "Hello!" (возвращает 6). Разумеется, всё рассмотренное — исключительно синтаксические трюки: внутри интерпретатора нет никакой разницы между всеми формами вызова, и работают они совершенно одинаково. И, разумеется, все эти вызовы — исключительно прямые.

Как реализовать косвенный вызов? Для этого имеется специальный синтаксис:

  • ! func

Так можно получить косвенную ссылку на функтор func. Все ссылки на функторы — это новый тип значений языка: функциональный.

Зачем здесь нужен восклицательный знак? Дело в том, что в Athanor (как и в некоторых LISP-диалектах), переменные и функции — всегда принадлежат к разным пространствам имён. Они никогда не пересекаются! Соответственно, интерпретация любого идентификатора выполняется во время синтаксического анализа: если он является первым операндом в вызове func^args, или следует после "!" (и ещё в некоторых экзотических случаях, которые мы рассмотрим позднее...) — он считается именем функтора, и ищется в соответствующем пространстве имён. В противном случае — это имя переменной, и ищется оно в других местах. Поэтому, можно иметь одновременно функтор func и переменную func — они никогда не конфликтуют. И далее (как и раньше) мы будем выделять все имена функторов полужирным шрифтом.

Ссылки на функторы во многом подобны скалярам. Разумеется, большинство скалярных операций (вроде сложения) к ним неприменимо. Но их можно: присваивать переменным, включать как элементы в списки (как и другие, более сложные, структуры данных), передавать другим функторам как параметры или возвращать из них в качестве значения. Но самое главное: ссылки на функторы можно вызывать, передавая им произвольные аргумент(ы):

FuncRef ! Arguments
apply (FuncRef, Arguments)
Косвенный функциональный вызов FuncRef, с аргумент(ами) Arguments

Как видите, это уже "полноценный" встроенный функтор apply! Если его первый операнд (FuncRef) является ссылкой на функтор — последний будет вызван (с передачей ему аргументов Arguments), а результатом будет то, что он вернул бы при прямом вызове. Если FuncRef не является ссылкой на функтор — интерпретатор сообщает об ошибке (и, разумеется, ничего не вызывается).

Итак, практически любой прямой вызов функтора — например, functor (Arg0, Arg1, ... ArgN) — можно заменить на косвенный: (! functor) ! (Arg0, Arg1, ... ArgN). Ясно, что когда каждый раз вызывается один и тот же функтор — в этом нет смысла (и это не рекомендуется, т.к. косвенный вызов немного медленнее прямого). Смысл появляется, когда вызываются разные функторы (но с теми же аргументами). Например:

  • (flag ? ! add : ! sub) ! (350, 200)

Если значение переменной flag истинное (ненулевое) — аргументы складываются (результатом будет 550), иначе они вычитаются (результатом будет 150). Понятно, что flag может быть не только переменной: допустимо любое выражение.

Конечно, так можно вызывать не только функторы, которые что-то "вычисляют" — но и "управляющие" (вроде итераторов). Например:

  • (dir ? ! l_loop_r : ! l_loop) ! (Var, List, <: Var)

Здесь выводятся все элементы списка List, их порядок определяется значением dir: если оно истинно, то в обратном порядке; если ложно, в прямом.

Рассмотрим теперь пример, в котором наш функтор выводит таблицы значений для заданного набора функций. Мы определим его, как:

  • print_funcs_table (caption low_val high_val total_count col_wdt func_names func_refs)

где параметров довольно много:

  • caption: общий заголовок таблицы (выводится в начале)
  • low_val, high_val: начальное и конечное значения (предполагается low_val < high_val)
  • total_count: на сколько интервалов делится отрезок low_val..high_val
  • col_wdt: ширина всех колонок в таблице
  • func_names: список, имена функций (выводятся как подзаголовки)
  • func_refs: список, реализация функций

Предполагается, что длины списков func_names и func_refs равны. В выводимой таблице будет total_count+1 строка, включая граничные значения (не считая заголовков).

! print_funcs_table (caption low_val high_val total_count col_wdt func_names func_refs) : [I value step_val name func] = {
	` основной заголовок: `
	<: (caption, "\n");

	` подзаголовки (названия функций): `
	<: str_pad ("#", col_wdt);
	l_loop (name, func_names, <: (" ", str_pad (name, col_wdt)));
	<: "\n";

	` результаты: (total_count + 1) строк `
	step_val = (high_val - low_val) / total_count;

	for_inc (I, total_count + 1, {
		value = low_val + (I * step_val);
		<: str_pad (value, col_wdt);

		l_loop (func, func_refs, <: (" ", str_pad (func ! value, col_wdt)));

		<: "\n";
		});

	};	` -- print_funcs_table `

В приведённом коде — всё (надеюсь) не вызывает вопросов, кроме нестандартного функтора str_pad, выводящего своё операнд в поле заданной ширины. Он нужен в основном для того, чтобы колонки таблицы выглядели аккуратно, и не "расползались". Его надо определить отдельно — проще всего так:

! str_pad (value width) = value$[width];

Строго говоря, value$[width] — это начальный отрезок строки value с длиной width (описана ещё в статье 3). Однако, если длина строки была меньше width, она автоматически дополняется до width пробелами в конце (что нам в основном здесь и требуется). Если операнд не строка, а числовой скаляр — он автоматически приводится к строке (поскольку это значение нам нужно только для вывода, нас это тоже устраивает).

Теперь попробуем print_funcs_table в деле: например, выведем таблицу квадратных и кубических корней. В отличие от (встроенного) квадратного корня (sqr), встроенного кубического корня в языке нет. Нам сперва придётся его определить (что, впрочем, тоже совсем несложно):

! cbr (Х) = exp_by (Х, 1/3);

Теперь выведем параллельную таблицу квадратных и кубических корней (от 0 до 4, шаг 0.2 / 20 интервалов):

print_funcs_table ("Квадратные и кубические корни", 0.0, 4.0, 20, 15, ['sqr root' 'cube root'], (!sqr, !cbr));

Результат:

#               sqr root        cube root      
0.              0.              0.             
0.2             0.4472136       0.58480355     
0.4             0.63245553      0.7368063      
0.6             0.77459667      0.84343267     
0.8             0.89442719      0.92831777     
1.              1.              1.             
1.2             1.0954451       1.0626586      
1.4             1.183216        1.1186889      
1.6             1.2649111       1.1696071      
1.8             1.3416408       1.2164404      
2.              1.4142136       1.259921       
2.2             1.4832397       1.3005914      
2.4             1.5491933       1.3388659      
2.6             1.6124515       1.3750689      
2.8             1.6733201       1.4094597      
3.              1.7320508       1.4422496      
3.2             1.7888544       1.4736126      
3.4             1.8439089       1.5036946      
3.6             1.8973666       1.5326189      
3.8             1.9493589       1.5604908      
4.              2.              1.5874011      

Немного тригонометрии: синус, косинус и тангенс, в диапазоне от -π/3 до +π/3 с 12 интервалами (шаг π/18):

print_funcs_table ("Тригонометрические", pi (-1/3), pi (1/3), 12, 15, ['sin' 'cos' 'tan'], (!sin, !cos, !tan));

Результат:

Тригонометрические
#               sin             cos             tan            
-1.0471976      -0.8660254      0.5             -1.7320508     
-0.87266463     -0.76604444     0.64278761      -1.1917536     
-0.6981317      -0.64278761     0.76604444      -0.83909963    
-0.52359878     -0.5            0.8660254       -0.57735027    
-0.34906585     -0.34202014     0.93969262      -0.36397023    
-0.17453293     -0.17364818     0.98480775      -0.17632698    
0.              0.              1.              0.             
0,17453293      0.17364818      0.98480775      0.17632698     
0.34906585      0.34202014      0.93969262      0.36397023     
0.52359878      0.5             0.8660254       0.57735027     
0.6981317       0.64278761      0.76604444      0.83909963     
0.87266463      0.76604444      0.64278761      1.1917536      
1.0471976       0.8660254       0.5             1.7320508      

Обратные тригонометрические — арксинус, арккосинус и арктангенс, в диапазоне от -1 до +1, с 10 интервалами (шаг 0.2):

print_funcs_table ("Обратно-тригонометрические", -1, 1, 10, 15, ['arcsin' 'arccos' 'arctan'], (!asin, !acos, !atan));

Результат:

Обратно-тригонометрические
#               arcsin          arccos          arctan         
-1.             -1.5707963      3.1415927       -0.78539816    
-0.8            -0.92729522     2.4980915       -0.67474094    
-0.6            -0.64350111     2.2142974       -0.5404195     
-0.4            -0.41151685     1.9823132       -0.38050638    
-0.2            -0.20135792     1.7721542       -0.19739556    
0.              0.              1.5707963       0.             
0.2             0.20135792      1.3694384       0.19739556     
0.4             0.41151685      1.1592795       0.38050638     
0.6             0.64350111      0.92729522      0.5404195      
0.8             0.92729522      0.64350111      0.67474094     
1.              1.5707963       0.              0.78539816     

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

Функциональное выражение — это не обязательно ссылка на уже определённый функтор. Можно определить функтор без имени (т.е. анонимный), сделав это непосредственно там, где он нужен. (Анонимные определения также часто называют "лямбда-выражениями".) В общем, синтаксис практически идентичен обычному определению функтора — за исключением того, что имя у него отсутствует:

  • ! (par1 par2 par3 … parM) : [loc1 loc2 loc3 … locN] = (body)

Здесь списки параметров (par1 … parM) и локальных переменных (loc1 … locN) — совершенно аналогичны тем же у обычного определения функтора, а тело (body) — также выполняет всю работу, и (чаще всего) возвращает значение. Есть одна (важная) разница: в лямбда-выражении тело функтора должно иметь замкнутый синтаксис. Практически, обычно это значит, что оно должно быть в скобках (не обязательно круглых)! В остальном, никакой разницы между именованными и анонимными функторами нет — но вторые часто употребляются в ситуации, когда некое функциональное выражение употребляется только в одной точке программы, и придумывать для него отдельное имя нецелесообразно. Например, если нам нигде больше не нужно вычислять кубические корни — вывод для таблицы корней мог бы выглядеть и так:

print_funcs_table ("Квадратные и кубические корни", 0.0, 4.0, 20, 15, ['sqr root' 'cube root'], (!sqr, !(X) = (exp_by (Х, 1/3))));

(Но обратите внимание на внешние круглые скобки в (exp_by (Х, 1/3)) — здесь они нужны.)

Наконец, иногда надо проверить некое значение на "функциональность". Для всех типов данных в языке есть свои встроенные функторы-предикаты — и функциональные ссылки не исключение.

  • is_func (X): истина (1), если X – ссылка на функтор (иначе 0)

Например: is_func (!add), is_func (!sin) или is_func (!(X) = (2 * X + 3)) – все возвращают 1. Для любого другого значения (которое не является законной ссылкой на функтор) возвращается 0.

Мы разобрались с тем, как можно косвенно вызывать встроенные функторы. Возможна и обратная (в какой-то степени) ситуация: т.е. когда встроенный функтор принимает (как аргумент) ссылку на другой функтор (возможно даже пользовательский, или анонимный). Таких тоже имеется немало, но их мы подробно рассмотрим уже в следующей статье.

Теги