Вы можете сделать это тривиально, превратив функцию в CPS (стиль продолжения продолжения).Идея состоит в том, что вместо вызова 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)
Это хорошо известный трюк, который может сделать любую функцию рекурсивной.Вуаля, это хвостовая рекомендация.
Следующая известная хитрость в пакете - это «нефункционализация» результата 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
вычислил результат.eval
, когда ему передается 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, если нет веских причин сделать что-то более сложное.