Рекурсивная привязка F # в вычислительных выражениях и хвостовая рекурсивность

Я пытаюсь понять, как правильно вызывать рекурсивные функции в вычислительных выражениях и не получать исключение переполнения стека. Насколько я понимаю, это довольно известная проблема, но до сих пор не могу понять суть. Может быть, у кого-нибудь есть простые объяснения или примеры для этого.

Вот мой пример. Я хочу, чтобы построитель трассировки имел поведение, подобное seq, но не работающее с монадой seq вместо какой-либо другой, например option, и возвращало только последнее значение, отличное от None, из рекурсивного цикла. Является ли это возможным ?

Это всего лишь пример, код будет работать бесконечно, но не должно быть исключения stackowerflow.

Насколько я понимаю, проблема с переполнением стека в методе Combine, код просто продолжает вызывать функцию f () в рекурсивном цикле, и я хочу избежать этого и сделать этот хвост вызова рекурсивным, т.е. код должен компилироваться в обычном цикле.

type TraceBuilder() = 

    member __.Bind (m: int Option, f: int -> int Option) : int Option =
         match m with
         | Some(v) -> f v
         | None -> None

    member this.Yield(x) = Some x

    member this.YieldFrom(x) = x

    member __.Delay(f) = f

    member __.Run(f) = f()

    member __.Combine (a, f) = 
        match a with
        | Some _ -> a    
        | None -> f()    

let trace = new TraceBuilder()

let rec iterRec (xopt: int Option) = 
    trace {        
        yield! None
        let! x = xopt        
        yield! iterRec(Some(x + 1))
    }


[<EntryPoint>]
let main argv = 
    let x = iterRec(Some(0))
    //x = startFrom(0) |> Seq.take(5000) |> Seq.toList  |> ignore
    printfn "%A" x

Код мышления в комп. выражение должно быть скомпилировано

let iterRec xopt = 
    combine (None, (fun () ->
              bind(xopt, fun x -> iterRec(Some(x+ 1)))

И похоже, что в этом случае вызов iterRec не является хвостовым рекурсивным, так что почему stackoveflow, возможно ли реализовать эту рекурсивную логику хвоста?

Прочтите эти ссылки, все еще не могу понять решение:

(Как) я могу сделать эту монадическую привязку хвостовой рекурсивной? < / а>

Вот предложение, как решить проблему с помощью FsControl lib, но можно ли решить проблему с помощью регулярных вычислительных выражений?

Рекурсивные функции в вычислительных выражениях

Предотвращение переполнения стека (с бесконечными последовательностями последовательностей F #)

https://fsharpforfunandprofit.com/posts/computation-expressions-builder-part5/ < / а>


person baio    schedule 16.10.2016    source источник
comment
Вызов f() в Combine уже хвостовой рекурсии. Не совсем понятно, в чем ваша проблема. Возможно, вам поможет, если вы сможете построить меньший пример.   -  person Fyodor Soikin    schedule 16.10.2016
comment
Я обновил фрагмент кода до максимально сжатого, который можно было скомпилировать.   -  person baio    schedule 16.10.2016
comment
Все еще не ясно, с какой проблемой вы столкнулись.   -  person Fyodor Soikin    schedule 17.10.2016
comment
Код вычислительного выражения сверху, не скомпилированный как хвостовая рекурсия, я не хочу этого делать ...   -  person baio    schedule 17.10.2016
comment
@baio - Эта iterRec функция мне кажется странной. В какой момент он когда-либо даст какое-либо значение, кроме None? Возможно, вам не хватает строки yield x сразу под строкой let! x = xopt?   -  person rmunn    schedule 17.10.2016
comment
@balo, откуда вы знаете, что он не компилируется как хвостовая рекурсия?   -  person Fyodor Soikin    schedule 17.10.2016
comment
@FyodorSoikin из-за исключения переполнения стека, и здесь явно iterRec не является последним вызовом в процедуре.   -  person baio    schedule 17.10.2016
comment
@rmunn Я знаю, что логика кода не имеет смысла, потому что это не важно, у меня просто бесконечный цикл, но этот цикл не должен вызывать stackoverflow   -  person baio    schedule 17.10.2016
comment
Да, iterRec последний вызов функции fun x -> iterRec( ... ). Итак, у вас переполнение стека? Хорошо, теперь мы к чему-то приближаемся. Где ТАК происходит? Какая функция является точкой рекурсии? Также интересно: какая у вас ОС и время выполнения?   -  person Fyodor Soikin    schedule 17.10.2016
comment
Обратите внимание, что по умолчанию, когда вы работаете в режиме отладки, хвостовые вызовы не включены, что может повлиять на ваши наблюдения.   -  person kvb    schedule 17.10.2016


Ответы (1)


Я удалил части кода, которые, как мне казалось, не были необходимы для решения проблемы. Обратите внимание, что я также нахожу ваше Combine определение сбивающим с толку. Это могло бы быть мило, но меня бы полностью сбило с толку, поскольку Combine должен вести себя аналогично Bind в том, что две операции связаны друг с другом. Ваша Combine операция близка к тому, что обычно является OrElse операцией.

Так или иначе:

module Trace =
  let treturn a = Some a
  let tbind a b =
      match a with
      | Some(v)  -> b v
      | None     -> None
  let (>>=) a b = tbind a b

open Trace

// Will throw on Debug (and most likely Mono)
let rec iterRec xopt l =
  xopt >>= fun x -> if x < l then iterRec(Some(x + 1)) l else Some x

[<EntryPoint>]
let main argv =
  let x = iterRec_(Some(0)) 100000
  printfn "%A" x
  0

iterRec вызывает StackOverflowException в отладке и дрожит, что не распознает атрибут .tail.

Немного легче понять, что происходит, посмотрев на iterRec в разобранном виде (например, используя ILSpy`)

iterRec равно:

public static FSharpOption<int> iterRec(FSharpOption<int> xopt, int l)
{
  return Program.Trace.tbind<int, int>(xopt, new Program.iterRec@13(l));
}


internal class iterRec@13 : FSharpFunc<int, FSharpOption<int>>
{
  public int l;

  internal iterRec@13(int l)
  {
    this.l = l;
  }

  public override FSharpOption<int> Invoke(int x)
  {
    if (x < this.l)
    {
      return Program.iterRec(FSharpOption<int>.Some(x + 1), this.l);
    }
    return FSharpOption<int>.Some(x);
  }
}

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

При разборке на IL виден атрибут .tail.

IL_0008: tail.
IL_000a: call class [FSharp.Core]Microsoft.FSharp.Core.FSharpOption`1<!!1> Program/Trace::tbind<int32, int32>(class [FSharp.Core]Microsoft.FSharp.Core.FSharpOption`1<!!0>, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!0, class [FSharp.Core]Microsoft.FSharp.Core.FSharpOption`1<!!1>>)

К сожалению, не все Jitters заботятся о .tail, поэтому я не решаюсь на него полагаться и перепишу iterRec в хвостовую рекурсивную функцию, которую F# может распаковать:

let rec iterRec_ xopt l =
  // This F# unpacks into a while loop
  let rec loop xo =
    match xo with
    | Some x  -> if x < l then loop(Some(x + 1)) else xo
    | None    -> None
  loop xopt

Проверка этой функции в ILSpy:

internal static FSharpOption<int> loop@17(int l, FSharpOption<int> xo)
{
  while (xo != null)
  {
    FSharpOption<int> fSharpOption = xo;
    int x = fSharpOption.Value;
    if (x >= l)
    {
      return xo;
    }
    int arg_1E_0 = l;
    xo = FSharpOption<int>.Some(x + 1);
    l = arg_1E_0;
  }
  return null;
}

Больше не рекурсивная, эта функция будет нормально работать как при Debug дрожании, так и при mono.

Другой подход - реализовать паттерн-трамплин для обмена пространства стека на пространство кучи.

person Just another metaprogrammer    schedule 18.10.2016
comment
Все еще блуждаю, как реализована монада seq, поскольку она не генерирует исключения в таких случаях, как при первом подходе, даже в режиме отладки. - person baio; 19.10.2016
comment
Я не могу проверить прямо сейчас, но я считаю, что это потому, что Seq - ленивая последовательность, которая по сути превращает ее в батут. Если вам интересно, я создал общий фрагмент батута: gist.github.com/mrange/63dffe4b525e88c846fd411f - person Just another metaprogrammer; 19.10.2016