Stackless trampoline Monad / Computation Expression

时间:2017-07-17 19:18:17

标签: f# functional-programming

我正在研究我自己设计的函数式编程语言,我偶然发现了一个超出我的技能要求解决的问题。我想知道是否有人有任何关于如何解决它的建议或者为什么不可能的原因。

以下代码概述了一种理想但不妥协的解决方案。

这个问题是我目前使用的运行时系统的核心。我没有依赖.Net堆栈,而是使用monad在蹦床上执行操作。这应该有助于逐步调试,并允许用户不必担心堆栈空间。这是我目前正在使用的monad的简化版本。

type 't StackFree =
    |Return of 't                                 //Return a value
    |StackPush of ('t->'t StackFree)*'t StackFree //Pushes a return handler onto the "Stack"
    |Continuation of (unit->'t StackFree)         //Perform a simple opperation
type StackFreeMonad() =
    member this.Delay(fn) = 
        Continuation(fn)
    member this.Bind(expr,fn) =
        StackPush(fn,expr)
    member this.Return(value) =
        Return(value)
    member this.ReturnFrom(x) =x
let stackfree = StackFreeMonad()

这不是最初的设计,但它是我在理想世界中使用F#计算表达式最好的,上面的计算表达式可以在这种类型上工作。

type 't Running = 
    |Result of 't
    |Step of (unit->'t Running)

因此,为了将StackFree转换为Running类型,我必须使用此转换函数

//this method loops through the StackFree structure finding the next computation and managing a pseudo stack with a list.
let prepareStackFree<'t> :'t StackFree->'t Running =
    let rec inner stack stackFree = 
        Step(fun ()-> 
            match stackFree with 
            //takes the return values and passes it to the next function on the "Stack"
            |Return(value)-> 
                match stack with 
                |[]->Result(value)
                |x::xs -> inner xs (x value)
            //pushes a new value on the the "Stack"
            |StackPush(ret,next) ->
                inner (ret::stack) next
            //performs a single step
            |Continuation(fn)->
                inner stack (fn()))
    inner []

以下是两种类型的简要示例。

let run<'t> :'t StackFree->'t = 
    let rec inner = function 
        |Step(x)-> inner (x())
        |Result(x)-> x
    stackFreeToRunning>>inner
//silly function to recompute an intiger value using recursion
let rec recompute number = stackfree {
    if number = 0 then return 0
    else 
        let! next = recompute (number-1)
        return next+1
}
let stackFreeValue = recompute 100000
let result = run stackFreeValue
do printfn "%i" result

我花了几个小时试图获得一个直接在Running类型上运行的计算表达式并切断中间人StackFree。但是我无法弄清楚如何做到这一点。在这一点上,我正在认真考虑解决这个问题的可能性是不可能的。但是,我无法弄清楚它是不可能的原因。

我已经接近了几次,但最终的解决方案最终以一些令人困惑的方式使用了堆栈。

是否有可能在不使用.Net堆栈的情况下运行在Running类型上运行的计算表达式?如果这不可能,为什么不可能。我必须要有一些简单的数学推理。

NB:这些不是我正在使用的实际类型,对于这些问题,它们是简化的,真实的类型跟踪脚本中的范围和位置。此外,我知道这种抽象的严重性能成本

修改:这是解决问题的另一种方法。这种实现有缺陷,因为它使用堆栈。无论如何在不使用堆栈的情况下获得下面的确切行为?

type RunningMonad() = 
    member this.Delay(fn) =
        Step(fun ()->fn ())
    member this.Bind(m, fn) = 
        Step(fun ()->
            match m with
            |Result(value)-> fn value
            //Here is the problem
            |Step(next)-> this.Bind(next(),fn))
    member this.Return(v) =
        Result(v)
    member this.ReturnFrom(x) = x

上面的计算表达式中的绑定实现创建了一个调用另一个函数的函数。因此,当你越来越深入并且越来越多地调用bind时,你必须追逐一堆函数调用,然后最终你遇到了stackoverflow异常。

Edit2:清晰度。

1 个答案:

答案 0 :(得分:6)

迟到总比没有好!

Stackless Scala with Free Monads第4节讨论了这个问题。 Bjarnason通过向Trampoline数据类型添加新构造函数来解决问题,表示对另一个trampoline的子例程调用。他将这个新构造函数保持为私有,以确保您不能构建左嵌套Bind(在执行trampoline时会溢出堆栈)。

我绝不是一个F#呃,但我会糊里糊涂。在WishF#ul中,我刚刚编写了一个假想的F#方言,你可以直接表达新的存在量化构造函数:

type Tram<'a> =
    | Done of 'a
    | Step of (unit -> Tram<'a>)
    | Call<'x> of Tram<'x> * ('x -> Tram<'a>)  // don't export this

type TramMonad() =
    member this.Return(x) = Done(x)
    member this.Bind(ma, f) = match ma with
        | Call(mx, k) -> Call(mx, fun x -> this.Bind(k(x), f))
        | _ -> Call(ma, f)
    // i confess to not quite understanding what your Delay and ReturnFrom methods are for

let tram = new TramMonad()

let rec runTram t =
    let next mx f = match mx with
        | Done(x) -> f x
        | Step(k) -> Step(fun () -> tram.Bind(k(), f))
        | Call(my, g) -> tram.Bind(my, fun x -> tram.Bind(g x, f))

    match t with
        | Done(x) -> x
        | Step(k) -> runTram(k())
        | Call(mx, f) -> runTram(next mx f)

请注意,runTram的所有递归调用都处于尾部位置。这需要一些令人费解的,但你可以说服自己Bind不会构建一个深度嵌套的延续,因此runT将始终在O(1)堆栈空​​间中运行。

可悲的是我们在F#工作,而不是WishF#ul,所以我们不得不求助于Call构造函数中存在类型的面向对象编码。这就是......

module rec Trampoline =
    type Call<'a> =
        abstract member Rebind<'b> : ('a -> Tram<'b>) -> Tram<'b>
        abstract member Next : unit -> Tram<'a>
    type Tram<'a> =
        | Done of 'a
        | Step of (unit -> Tram<'a>)
        | Call of Call<'a>

    type TramMonad() =
        member this.Return(x) = Done(x)
        member this.Bind(ma, f) =
            match ma with
                | Call(aCall) -> aCall.Rebind(f)
                | _ -> call ma f
    let tram = new TramMonad()

    let rec call<'a, 'x>(mx : Tram<'x>) (f : 'x -> Tram<'a>) : Tram<'a> = Call {
        new Call<'a> with
            member this.Rebind<'b>(g : 'a -> Tram<'b>) : Tram<'b> =
                call<'b, 'x> mx (fun x -> tram.Bind(f x, g) : Tram<'b>)
            member this.Next() =
                match mx with
                    | Done(x) -> f x
                    | Step(k) -> Step(fun () -> tram.Bind(k(), f))
                    | Call(aCall) -> aCall.Rebind(f)
    }

    let rec runTram t =
        match t with
            | Done(x) -> x
            | Step(k) -> runTram(k())
            | Call(aCall) -> runTram(aCall.Next())

我建议阅读the whole paper,继续将这种无叠加结构概括为任何免费的monad,而不仅仅是蹦床(Free (Unit -> _))。 Phil Freeman的Stack Safety for Free建立在这项工作的基础上,将蹦床论文的免费monad推广到免费的monad变换器。