Странное поведение, вызванное debug.getinfo (1, "n"). Name - PullRequest
2 голосов
/ 09 июля 2019

Я узнал, как получить имя функции внутри функции, используя debug.getinfo(1, "n").name.

Используя эту функцию, я обнаружил странное поведение в Lua.

Вот мой код:

function myFunc()
  local name = debug.getinfo(1, "n").name
  return name
end

function foo()
  return myFunc()
end

function boo()
  local name = myFunc()
  return name
end

print(foo())
print(boo())

Результат:

nil
myFunc

Как видите, функции foo() и boo() вызывают одну и ту же функцию myFunc(), но возвращают разные результаты.

Если я заменим debug.getinfo(1, "n").name другой строкой, они вернут те же результаты, что и ожидалось, но я не понимаю неожиданное поведение, вызванное использованием debug.getinfo().

Можно ли исправить функцию myFunc(), поэтому вызов функций foo() и boo() возвращает один и тот же результат?

Ожидаемый результат:

myFunc
myFunc

Ответы [ 3 ]

1 голос
/ 09 июля 2019

Вы можете запустить свой код через luac -l -p

...

function <stdin:6,8> (4 instructions at 0x555f561592a0)
0 params, 2 slots, 1 upvalue, 0 locals, 1 constant, 0 functions
  1 [7] GETTABUP    0 0 -1  ; _ENV "myFunc"
  2 [7] TAILCALL    0 1 0
  3 [7] RETURN      0 0
  4 [8] RETURN      0 1

function <stdin:10,13> (4 instructions at 0x555f561593b0)
0 params, 2 slots, 1 upvalue, 1 local, 1 constant, 0 functions
  1 [11]    GETTABUP    0 0 -1  ; _ENV "myFunc"
  2 [11]    CALL        0 1 2
  3 [12]    RETURN      0 2
  4 [13]    RETURN      0 1

Эти две функции, которые нам интересны: foo и boo

Как видитекогда boo вызывает myFunc, это просто обычный CALL, так что ничего интересного там нет.

foo, однако, делает то, что называется tail call .Таким образом, возвращаемое значение foo является возвращаемым значением myFunc.

. Что делает этот тип вызова особенным, так это то, что программе не нужно переходить обратно в foo;как только foo звонит myFunc, он может просто передать ключи и сказать: «Вы знаете, что делать»;myFunc затем возвращает свои результаты непосредственно туда, куда был вызван foo.Это имеет два преимущества:

  • Кадр стека foo можно очистить до того, как myFunc будет вызван
  • , как только myFunc вернется, ему не нужно два прыжкавернуться в основной поток;только один

Оба примера незначительны в таких примерах, как ваш, но если у вас есть цепочка лотов и концевых вызовов, это становится значительным.

Недостатком этого являетсячто после очистки стека foo Lua также забывает всю информацию об отладке, связанную с ним;он только помнит, что myFunc был вызван как хвостовой вызов, но не от куда.


Интересное примечание, что boo почти также является хвостовым вызовом.Если у Lua не было нескольких возвращаемых значений, он был бы точно таким же, как foo, и более умный компилятор, такой как LuaJIT, мог бы скомпилировать его в хвостовой вызов.PUC Lua не хочет, хотя, так как для распознавания хвостового вызова требуется литерал return some_function().

Разница в том, что boo возвращает только первое значение, возвращаемое myFunc, и в то время как в вашем примере, будет только один, интерпретатор не может сделать такое предположение (LuaJIT может сделать это предположение во время компиляции JIT, но это за пределами моего понимания)


Также обратите внимание, что, технически, слово хвостовой вызов просто описывает функцию A, непосредственно возвращающую возвращаемое значение другой функции B.

Он часто используется взаимозаменяемо с оптимизацией хвостового вызова , что и делает компиляторкогда он повторно использует кадр стека и превращает вызов функции в скачок.

Строго говоря, C (например) имеет хвостовые вызовы , но не имеет хвостового вызоваОптимизация , то есть что-то вроде

int recursive(n) { return recursive(n+1); }

является допустимым кодом C, но в конечном итоге приведет к переполнению стека, тогда как в Lua

local function recursive(n) return recursive(n+1) end

просто запуститсянавсегда.Оба являются хвостовыми вызовами, но оптимизируется только второй.


РЕДАКТИРОВАТЬ: Как всегда с C, некоторые компиляторы могут самостоятельно реализовать оптимизацию хвостовых вызовов, так что не говорите всем, что«С никогда не делает это»;это просто не обязательная часть языка, в то время как в Lua это фактически определено в спецификации языка, так что это не Lua, пока у него нет TCO.

1 голос
/ 09 июля 2019

В Lua любой оператор возврата в форме return <expression_yielding_a_function>(...) является "хвостовым вызовом".Хвостовые вызовы по существу не существуют в стеке вызовов, поэтому они не занимают дополнительного пространства или ресурсов.Функция, которую вы вызываете, эффективно удаляется из отладочной информации.

Возможно ли исправить функцию myFunc(), поэтому вызов функций foo() и boo() возвращает один и тот же результат?

Хм ... да, но прежде чем я расскажу вам, как, позвольте мне попытаться убедить вас не делать этого .

Как упоминалось ранее, хвостовые вызовы являются частьюязыка Lua.Удаление хвостовых вызовов из стека больше не является «оптимизацией», это не «оптимизация» для цикла for для выхода при использовании break.Это часть грамматики Lua, и программисты Lua имеют такое же право ожидать, что хвостовой вызов будет хвостовым вызовом, и имеют право ожидать break выхода из циклов.

Lua, какязык, в частности, утверждает, что это:

local function recursive(...)
  --some terminating condition

  return recursive(modified_args)
end

никогда, ever не исчерпает пространство стека.Это будет так же эффективно, как и выполнение цикла.Это часть языка Lua, такая же его часть, как и поведение for и while.

Если пользователь хочет вызвать вашу функцию с помощью хвостового вызова, то это их право как пользователь языка, который произносит хвост.Отказ для пользователей языка в праве использовать функции этого языка - грубый .

Так что не делайте этого.

Более того, ваш код предполагает, что выпытаясь полагаться на функции, имеющие имена.Что вы делаете что-то значимое и значимое с этими именами.

Ну, Lua - это не Python ;Функции Lua не должны иметь имен, точка.Таким образом, вы не должны писать код, который в значительной степени опирается на имя функции.Для отладки или ведения журнала, хорошо.Но вы не должны нарушать ожидания пользователей только для отладки и регистрации.Так что, если пользователь совершил повторный вызов, просто примите то, что он хотел, и ваша отладка / запись будет немного пострадала.

ОК, так, мы согласны, что вы не должны этого делать?Что пользователи Lua имеют право на хвостовые звонки, а вы не имеете права их отрицать?Что функции Lua не названы, и вы не должны писать код, который требует, чтобы они поддерживали имя?ОК?


Что означает ужасный код, который вы никогда не должны использовать! (в Lua 5.3):

function bypass_tail_call(Func)
    local function tail_call_bypass(...)
        local rets = table.pack(Func(...))
        return table.unpack(rets, rets.n)
    end
    return tail_call_bypass
end

Тогдапросто замените вашу реальную функцию на возвращение байпаса:

function myFunc()
  local name = debug.getinfo(1, "n").name
  return name
end

myFunc = bypass_tail_call(myFunc)

Обратите внимание, что функция байпаса должна создать массив для хранения возвращаемых значений, а затем распаковать их в последний оператор возврата.Это, очевидно, требует дополнительного выделения памяти, что не должно происходить в обычном коде.

Так что есть еще одна причина не делать этого.

1 голос
/ 09 июля 2019

Это результат оптимизации хвостового вызова , что делает Lua.

В этом случае Lua переводит вызов функции в оператор "goto",и не использует никакого дополнительного фрейма стека для выполнения хвостового вызова.

Вы можете добавить оператор traceback, чтобы проверить его:

    function myFunc()
      local name = debug.getinfo(1, "n").name
      print(debug.traceback("Stack trace"))
      return name
    end

Оптимизация хвостового вызова происходит в Lua, когда вы возвращаетесь свызов функции:

-- Optimized
function good1()
    return test()
end

-- Optimized
function good2()
    return test(foo(), bar(5 + baz()))
end

-- Not optimised
function bad1()
    return test() + 1
end

-- Not optimised
function bad2()
    return test()[2] + foo()
end

Для получения дополнительной информации вы можете обратиться к следующим ссылкам: - Программирование на Lua - 6.3: Правильные вызовы Tail - Что такое оптимизация хвостового вызова?- переполнение стека

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...