Введение

В информатике проблема funarg (проблема аргумента функции) относится к сложности реализации функций первого класса (функций как объектов первого класса) в реализациях языков программирования с использованием стековой аллокации памяти для функций. Эта сложность возникает только в том случае, если тело вложенной функции ссылается непосредственно (то есть, не через передачу аргументов) на идентификаторы, определенные в области видимости, где функция определена, но не в области видимости вызова функции. Стандартным решением является либо запрет таких ссылок, либо создание замыканий. Существует два немного различающихся варианта проблемы funarg. Проблема funarg, направленная вверх, возникает при возврате (или иной передаче "вверх") функции из вызова функции. Проблема funarg, направленная вниз, возникает при передаче функции в качестве параметра другому вызову функции.

Вверх по лестнице

Когда одна функция вызывает другую во время выполнения типичной программы, локальное состояние вызывающей функции (включая параметры и локальные переменные) должно быть сохранено, чтобы выполнение продолжилось после возврата вызванной функции. В большинстве компилируемых программ это локальное состояние хранится в стеке вызовов в структуре данных, называемой стековой рамкой или записью активации. Эта стековая рамка помещается в стек (выделяется) перед вызовом другой функции и извлекается из стека (деаллоцируется), когда вызванная функция возвращается в функцию, которая её вызвала. Проблема «funarg вверх» возникает, когда вызывающая функция обращается к состоянию вызванной функции после её возврата. Следовательно, стековая рамка, содержащая переменные состояния вызванной функции, не должна быть деаллоцирована при возврате функции, что нарушает парадигму вызова функций на основе стека. Одно из решений проблемы «funarg вверх» заключается в том, чтобы выделять все записи активации из кучи, а не из стека, и полагаться на сборку мусора или подсчёт ссылок для их деаллокации, когда они больше не нужны. Исторически управление записями активации в куче считалось менее эффективным, чем в стеке (хотя это частично опровергается), и предполагало значительную сложность реализации. Большинство функций в типичных программах (в меньшей степени в программах на функциональных языках программирования) не создают «funarg вверх», что усиливает опасения по поводу потенциальных накладных расходов, связанных с их реализацией. Кроме того, этот подход затруднителен в языках, которые не поддерживают сборку мусора. Некоторые компиляторы, ориентированные на эффективность, используют гибридный подход, при котором записи активации для функции выделяются из стека, если компилятор может определить с помощью статического анализа программы, что функция не создаёт «funarg вверх». В противном случае записи активации выделяются из кучи. Другое решение — просто скопировать значения переменных в замыкание в момент его создания. Это приведёт к другому поведению в случае изменяемых переменных, поскольку состояние больше не будет общим для замыканий. Но если известно, что переменные являются константами, то этот подход будет эквивалентным. Языки ML используют этот подход, поскольку переменные в этих языках связаны со значениями, то есть переменные нельзя изменить. Java также использует этот подход в отношении анонимных классов (и лямбда-выражений с Java 8), поскольку он позволяет ссылаться только на переменные в окружающей области видимости, которые являются фактически финальными (то есть константами). Некоторые языки позволяют программисту явно выбирать между двумя вариантами поведения. Анонимные функции PHP 5.3 требуют указания переменных, которые следует включить в замыкание, с помощью ключевого слова `use`; если переменная указана по ссылке, то включается ссылка на исходную переменную; в противном случае передаётся значение. В анонимных функциях Apple Blocks захваченные локальные переменные по умолчанию захватываются по значению; если требуется общий доступ к состоянию между замыканиями или между замыканием и внешней областью видимости, переменная должна быть объявлена с модификатором `block`, в этом случае эта переменная выделяется в куче.

Проблема с посадкой вниз

Нисходящий funarg может также относиться к состоянию функции, когда она фактически не выполняется. Однако, поскольку по определению существование нисходящего funarg подразумевается выполнением функции, которая его создает, стек-фрейм для этой функции обычно все еще может храниться в стеке. Тем не менее, наличие нисходящих funarg подразумевает древовидную структуру замыканий и стековых фреймов, что может затруднить понимание состояния программы как для человека, так и для машины. Проблема нисходящих funarg усложняет эффективную компиляцию хвостовых вызовов и кода, написанного в стиле передачи продолжений. В этих особых случаях программист (как правило) предполагает, что функция должна выполняться с ограниченным использованием стека, поэтому более "быстрое" поведение может оказаться нежелательным.

Практические последствия

Исторически проблема обратного funarg оказалась более сложной. Например, язык программирования Pascal позволяет передавать функции в качестве аргументов, но не возвращать их в качестве результатов; таким образом, реализации Pascal требуется решать проблему funarg вниз, но не вверх. Языки программирования Modula 2 и Oberon (потомки Pascal) позволяют использовать функции как в качестве параметров, так и в качестве возвращаемых значений, но присваиваемая функция не может быть вложенной. Язык программирования C исторически избегает основной сложности проблемы funarg, не допуская вложенных определений функций; поскольку окружение каждой функции одинаково и содержит только статические глобальные переменные и функции, указатель на код функции полностью определяет функцию. Компания Apple предложила и реализовала синтаксис замыканий для C, который решает проблему обратного funarg, динамически перемещая замыкания из стека в кучу по мере необходимости. Язык программирования Java решает эту проблему, требуя, чтобы контекст, используемый вложенными функциями в анонимных внутренних и локальных классах, был объявлен final, а контекст, используемый лямбда-выражениями, был эффективно final. В C# и D есть лямбды (замыкания), которые инкапсулируют указатель функции и связанные переменные. В функциональных языках функции являются значениями первого класса и могут передаваться где угодно. Таким образом, реализации Scheme или Standard ML должны решать как проблему обратного, так и проблему прямого funarg. Обычно это достигается путем представления значений функций как замыканий, выделенных в куче, как описано ранее. Компилятор OCaml использует гибридный метод (основанный на статическом анализе программы) для максимизации эффективности.