Является ли отсутствие оптимизации хвостового оперения препятствием при использовании технологического процесса "В конечном счете"?
Я использую модифицированную версию рабочего процесса Eventually из спецификации F # для моей разработки на Xbox. Кажется, инфраструктура .net на Xbox не поддерживает хвостовые звонки. Из-за этого я должен отключить оптимизацию хвостового вызова при компиляции.
Хотя сначала может показаться, что это ограничение не позволит использовать любую форму цикла в вычислениях, я изначально думал, что "степпинг" позволит избежать этой проблемы: рекурсивная функция f в выражении вычисления не вызывает себя непосредственно, вместо этого он возвращает значение Eventually, содержащее лямбда, которое вызывает f.
Эксперименты показывают, что я был прав в циклах (они не вызывают переполнение стека при использовании в выражениях вычислений), но не о рекурсивных функциях.
Чтобы уточнить, это работает:
// Wait until "start" or "A" is pressed on one of the gamepads.
// Set "player" when that happens.
let player : PlayerIndex option ref = ref None
while (!player).IsNone do
for p in all_players do
let state = GamePad.GetState(p)
if state.IsConnected
&& (state.Buttons.Start = ButtonState.Pressed
|| state.Buttons.A = ButtonState.Pressed) then
player := Some p
do! sys.WaitNextFrame()
Это вызывает переполнение стека:
// Wait until "start" is pressed on the controlling gamepad.
let rec wait() = task {
input.Update()
if not (input.IsStartPressed()) then
do! sys.WaitNextFrame()
do! wait()
}
Во втором случае трассировка стека показывает длинную последовательность вызовов "bind @17-1", код которой показан ниже. Номер строки, отображаемой в трассировке стека, - строка 17.
let rec bind k e =
match e with
| Completed r ->
Running(fun () -> k r)
| Running f ->
Running(fun () -> f() |> bind k) // line 17
| Blocked(dt, f) ->
Blocked(dt, fun () -> f() |> bind k)
| BlockedNextFrame f ->
BlockedNextFrame(fun () -> f() |> bind k)
| Yield f ->
Yield(fun () -> f() |> bind k)
Где я ошибаюсь? Является ли мое рассуждение о "степной рекурсии" безвредным w.r.t. неправильный переполнение стека? Что-то не так с моей реализацией bind?
О, моя голова! Продолжение-прохождение с рекурсией дает мне головные боли...
ИЗМЕНИТЬ: "Уступчивая рекурсия безвредная w.r.t. переполнение стека" получила имя, я только что узнал. Это называется батут.
Ответы
Ответ 1
замените последний do! на return!:
// Wait until "start" is pressed on the controlling gamepad.
let rec wait() = task {
input.Update()
if not (input.IsStartPressed()) then
do! sys.WaitNextFrame()
return! wait()
}
ИЗМЕНИТЬ
согласно спецификации F #, сделать! wait() будет преобразован в Bind (wait(), fun() → Zero()), поэтому каждый рекурсивный вызов увеличит уровень вложенности Bind (как вы видите в stacktrace)
в обратном возврате! wait() немедленно вернет новое вычисление "В конечном счете", которое может быть использовано на следующем шаге.
Ответ 2
Один из способов понять, что происходит, чтобы посмотреть на подписи типов.
type TaskBuilder() =
// do!
// Eventually<'a> * ('a -> Eventually<'b>) -> Eventually<'b>
member x.Bind(e, f) = bind f e
// return!
// Eventually<'a> -> Eventually<'a>
member x.ReturnFrom(r : Eventually<_>) = r
// return
// 'a -> Eventually<'a>
member x.Return(r) = result r
let result r = Completed(r)
Все функции в f # должны что-то возвращать. Таким образом, следующий код
let rec wait() = task {
input.Update()
if not (input.IsStartPressed()) then
do! sys.WaitNextFrame()
do! wait()
}
эквивалентно
let rec wait() = task {
input.Update()
if not (input.IsStartPressed()) then
do! sys.WaitNextFrame()
do! wait()
return ()
}
Если мы посмотрим на определение Return, он вызывает результат, который в свою очередь возвращает Completed (r).
Я сделал два небольших теста для задачи.
let test7() =
let sch = new Scheduler()
let sys = new Environment(sch)
let rec hop i = task {
printfn "%i: Hop!" i
//do! sys.Wait(1.0f)
if i > 0 then
do! hop (i - 1)
return ()
}
runAllFixed sch 0.1f [| hop 3 |]
let test8() =
let sch = new Scheduler()
let sys = new Environment(sch)
let rec hop i = task {
printfn "%i: Hop!" i
//do! sys.Wait(1.0f)
if i > 0 then
return! hop (i - 1)
}
runAllFixed sch 0.1f [| hop 3 |]
test7()
printfn "\n"
test8()
С помощью некоторых инструментов он печатает.
Delay 3: Hop!
Delay Bind Running 2: Hop!
Delay Bind Running Running 1: Hop!
Delay Bind Running Running Running 0: Hop!
Zero Completed Running Running Return Completed Running Return Completed Return
Delay 3: Hop!
Delay ReturnFrom 2: Hop!
Delay ReturnFrom 1: Hop!
Delay ReturnFrom 0: Hop!
Zero
MSDN doc в Вычисления Вычисления.