Опираясь на ответ Томаса, давайте определим два модуля:
module Kurt =
type Gen<'a> = Gen of (int -> 'a)
let unit x = Gen (fun _ -> x)
let bind k (Gen m) =
Gen (fun n ->
let (Gen m') = k (m n)
m' n)
type GenBuilder() =
member x.Return(v) = unit v
member x.Bind(v,f) = bind f v
let gen = GenBuilder()
module Tomas =
type Gen<'a> = Gen of (int -> ('a -> unit) -> unit)
let unit x = Gen (fun _ f -> f x)
let bind k (Gen m) =
Gen (fun n f ->
m n (fun r ->
let (Gen m') = k r
m' n f))
type GenBuilder() =
member x.Return v = unit v
member x.Bind(v,f) = bind f v
let gen = GenBuilder()
Чтобы немного упростить ситуацию, давайте перепишем вашу оригинальную функцию последовательности как
let rec sequence = function
| [] -> gen { return [] }
| m::ms -> gen {
let! x = m
let! xs = sequence ms
return x::xs }
Теперь sequence [for i in 1 .. 100000 -> unit i]
будет выполняться до завершения независимо от того, определен ли sequence
в терминах Kurt.gen
или Tomas.gen
. Проблема не в том, что sequence
вызывает переполнение стека при использовании ваших определений, а в том, что функция, возвращаемая из вызова к sequence
, вызывает переполнение стека при вызове it .
Чтобы понять, почему это так, давайте расширим определение sequence
в терминах основных монадических операций:
let rec sequence = function
| [] -> unit []
| m::ms ->
bind (fun x -> bind (fun xs -> unit (x::xs)) (sequence ms)) m
Вставляя значения Kurt.unit
и Kurt.bind
и упрощая как сумасшедшие, мы получаем
let rec sequence = function
| [] -> Kurt.Gen(fun _ -> [])
| (Kurt.Gen m)::ms ->
Kurt.Gen(fun n ->
let (Kurt.Gen ms') = sequence ms
(m n)::(ms' n))
Теперь, надеюсь, понятно, почему вызов let (Kurt.Gen f) = sequence [for i in 1 .. 1000000 -> unit i] in f 0
переполняет стек: f
требует нерекурсивно-рекурсивного вызова последовательности и вычисления результирующей функции, поэтому для каждого рекурсивного вызова будет один кадр стека.
Вставив Tomas.unit
и Tomas.bind
в определение sequence
, вместо этого мы получаем следующую упрощенную версию:
let rec sequence = function
| [] -> Tomas.Gen (fun _ f -> f [])
| (Tomas.Gen m)::ms ->
Tomas.Gen(fun n f ->
m n (fun r ->
let (Tomas.Gen ms') = sequence ms
ms' n (fun rs -> f (r::rs))))
Рассуждать об этом варианте сложно. Вы можете эмпирически проверить, что он не унесет стек для некоторых произвольно больших входных данных (как показывает Томас в своем ответе), и вы можете пройти оценку, чтобы убедиться в этом. Тем не менее, потребление стека зависит от Gen
экземпляров в переданном списке, и позволяет перебросить стек для входов, которые сами по себе не являются рекурсивными:
// ok
let (Tomas.Gen f) = sequence [for i in 1 .. 1000000 -> unit i]
f 0 (fun list -> printfn "%i" list.Length)
// not ok...
let (Tomas.Gen f) = sequence [for i in 1 .. 1000000 -> Gen(fun _ f -> f i; printfn "%i" i)]
f 0 (fun list -> printfn "%i" list.Length)