Ответ 1
Вы можете тривиально сделать это, включив функцию в CPS (Continuation Passing Style). Идея состоит в том, что вместо вызова depth left
, а затем вычисления вещей на основе этого результата вы вызываете depth left (fun dleft -> ...)
, где второй аргумент - "что нужно вычислить, когда результат (dleft
) доступен".
let depth tree =
let rec depth tree k = match tree with
| Leaf x -> k 0
| Node(_,left,right) ->
depth left (fun dleft ->
depth right (fun dright ->
k (1 + (max dleft dright))))
in depth tree (fun d -> d)
Это хорошо известный трюк, который может сделать любую функцию хвостовой рекурсивной. Voilà, это хвост.
Следующий известный трюк в сумке - "defunctionalize" результат CPS. Представление продолжений (части (fun dleft -> ...)
) как функции опрятно, но вы можете посмотреть, как это выглядит как данные. Поэтому мы заменяем каждое из этих замыканий конкретным конструктором типа данных, который захватывает свободные переменные, используемые в нем.
Здесь мы имеем три закрытия продолжения: (fun dleft -> depth right (fun dright -> k ...))
, который только повторно использует переменные среды right
и k
, (fun dright -> ...)
, которые повторно используют k
и теперь доступный левый результат dleft
и (fun d -> d)
, начальное вычисление, которое ничего не фиксирует.
type ('a, 'b) cont =
| Kleft of 'a tree * ('a, 'b) cont (* right and k *)
| Kright of 'b * ('a, 'b) cont (* dleft and k *)
| Kid
Дефункрированная функция выглядит следующим образом:
let depth tree =
let rec depth tree k = match tree with
| Leaf x -> eval k 0
| Node(_,left,right) ->
depth left (Kleft(right, k))
and eval k d = match k with
| Kleft(right, k) ->
depth right (Kright(d, k))
| Kright(dleft, k) ->
eval k (1 + max d dleft)
| Kid -> d
in depth tree Kid
;;
Вместо того, чтобы строить функцию k
и применяя ее на листах (k 0
), я строю данные типа ('a, int) cont
, которые должны быть позже eval
uated для вычисления результата. eval
, когда он получает a Kleft
, выполняет то, что делал закрытие (fun dleft -> ...)
, то есть рекурсивно вызывает depth
в правом поддереве. eval
и depth
являются взаимно рекурсивными.
Теперь посмотрите на ('a, 'b) cont
, что это за тип данных? Это список!
type ('a, 'b) next_item =
| Kleft of 'a tree
| Kright of 'b
type ('a, 'b) cont = ('a, 'b) next_item list
let depth tree =
let rec depth tree k = match tree with
| Leaf x -> eval k 0
| Node(_,left,right) ->
depth left (Kleft(right) :: k)
and eval k d = match k with
| Kleft(right) :: k ->
depth right (Kright(d) :: k)
| Kright(dleft) :: k ->
eval k (1 + max d dleft)
| [] -> d
in depth tree []
;;
И список - это стек. То, что мы имеем здесь, является на самом деле овеществлением (преобразованием в данные) стека вызовов предыдущей рекурсивной функции с двумя разными случаями, соответствующими двум различным типам не-tailrec-вызовов.
Обратите внимание, что деинтернализация существует только для удовольствия. В правильной версии версия CPS коротка, легко получается вручную, довольно легко читается, и я бы рекомендовал ее использовать. Замыкания должны быть выделены в памяти, но также элементы ('a, 'b) cont
- хотя они могут быть представлены более компактно ". Я бы придерживался версии CPS, если нет веских причин сделать что-то более сложное.