如何创建一个类似单步调试的F#工作流程?

3
我希望创建一个构建器,用于构建表达式,每个步骤后返回类似续延的东西。像这样:
```html

我想创建一个构建器,构建表达式,每一步之后都会返回类似续延的东西。

例如:

```
module TwoSteps = 
  let x = stepwise {
    let! y = "foo"
    printfn "got: %A" y
    let! z = y + "bar"
    printfn "got: %A" z
    return z
  }

  printfn "two steps"
  let a = x()
  printfn "something inbetween"
  let b = a()

'let a'这行返回包含待稍后评估的其余表达式的内容。

为每个步骤使用单独的类型来完成此操作很简单,但显然不是特别有用:

type Stepwise() =
  let bnd (v: 'a) rest = fun () -> rest v
  let rtn v = fun () -> Some v
  member x.Bind(v, rest) = 
    bnd v rest
  member x.Return v = rtn v

let stepwise = Stepwise()

module TwoSteps = 
  let x = stepwise {
    let! y = "foo"
    printfn "got: %A" y
    let! z = y + "bar"
    printfn "got: %A" z
    return z
  }

  printfn "two steps"
  let a = x()
  printfn "something inbetween"
  let b = a()

module ThreeSteps = 
  let x = stepwise {
    let! y = "foo"
    printfn "got: %A" y
    let! z = y + "bar"
    printfn "got: %A" z
    let! z' = z + "third"
    printfn "got: %A" z'
    return z
  }

  printfn "three steps"
  let a = x()
  printfn "something inbetween"
  let b = a()
  printfn "something inbetween"
  let c = b()

结果符合我的预期:

two steps
got: "foo"
something inbetween
got: "foobar"
three steps
got: "foo"
something inbetween
got: "foobar"
something inbetween
got: "foobarthird"

但我无法确定这种情况的一般性。

我想要的是能够将事件输入到此工作流中,这样你可以编写类似以下的内容:

let someHandler = Stepwise<someMergedEventStream>() {
  let! touchLocation = swallowEverythingUntilYouGetATouch()
  startSomeSound()
  let! nextTouchLocation = swallowEverythingUntilYouGetATouch()
  stopSomeSound()
}

并且使事件触发工作流程的下一步。(特别是,我想在MonoTouch - F# on the iPhone中尝试这种操作。传递objc选择器让我感到疯狂。)

2个回答

2
你的实现问题在于每次调用Bind都返回“unit -> 'a”,因此不同步骤数会得到不同类型的结果(通常,这是monad /计算表达式的可疑定义)。
正确的解决方案应该使用其他类型,可以表示具有任意步骤的计算。您还需要区分两种类型的步骤 - 一些步骤仅评估计算的下一步,而一些步骤通过“return”关键字返回结果。我将使用类型“seq >”。这是一个惰性序列,因此读取下一个元素将评估计算的下一步。该序列将包含除最后一个值外的“None”值,“Some(value)”将是最后一个值,表示使用“return”返回的结果。
你的实现中另一个可疑的事情是非标准类型的Bind成员。您的bind采用值作为第一个参数的事实意味着您的代码看起来更简单(您可以编写“let!a = 1”),但是您无法组合逐步计算。您可能希望能够编写:
let foo() = stepwise { 
  return 1; }
let bar() = stepwise { 
  let! a = foo()
  return a + 10 }

我描述的类型将允许您编写此内容。一旦您拥有了这种类型,您只需要按照实现中的BindReturn的类型签名即可得到以下结果:
type Stepwise() = 
  member x.Bind(v:seq<option<_>>, rest:(_ -> seq<option<_>>)) = seq {
    let en = v.GetEnumerator()
    let nextVal() = 
      if en.MoveNext() then en.Current
      else failwith "Unexpected end!" 
    let last = ref (nextVal())
    while Option.isNone !last do
      // yield None for each step of the source 'stepwise' computation
      yield None
      last := next()
    // yield one more None for this step
    yield None      
    // run the rest of the computation
    yield! rest (Option.get !last) }
  member x.Return v = seq { 
    // single-step computation that yields the result
    yield Some(v) }

let stepwise = Stepwise() 
// simple function for creating single-step computations
let one v = stepwise.Return(v)

现在,让我们看一下如何使用这种类型:
let oneStep = stepwise {
  // NOTE: we need to explicitly create single-step 
  // computations when we call the let! binder
  let! y = one( "foo" ) 
  printfn "got: %A" y 
  return y + "bar" } 

let threeSteps = stepwise { 
  let! x = oneStep // compose computations :-)
  printfn "got: %A" x 
  let! y = one( x + "third" )
  printfn "got: %A" y
  return "returning " + y } 

如果您想逐步运行计算,可以简单地迭代返回的序列,例如使用 F# 的 for 关键字。以下代码还会打印每个步骤的索引:
for step, idx in Seq.zip threeSteps [ 1 .. 10] do
  printf "STEP %d: " idx
  match step with
  | None _ -> ()
  | Some(v) -> printfn "Final result: %s" v

希望这可以帮到你! 附注:我觉得这个问题非常有趣!如果我将我的回答改编成博客文章放在我的博客(http://tomasp.net/blog)上,你介意吗?谢谢!

只是一个补充,这也非常类似于所谓的Resumption monad。但我相信使用序列的实现在F#中可能会更容易使用。 - Tomas Petricek
附言:我觉得这个问题非常有趣!如果我把我的回答改编成一篇博客文章放到我的博客(http://tomasp.net/blog)上,你介意吗?当然啦,请随便。谢谢你的回答,我正在研究如何将其应用到iPhone事件系统中。 - James Moore

1

Monad和计算构建器让我非常困惑,但我已经适应了我在早期的SO帖子中制作的一些东西。也许其中一些部分可以派上用场。

下面的代码包含一个操作队列和一个表单,其中Click事件侦听操作队列中的下一个操作。下面的代码是连续4个操作的示例。在FSI中执行它并开始点击表单。

open System.Collections.Generic
open System.Windows.Forms

type ActionQueue(actions: (System.EventArgs -> unit) list) =
    let actions = new Queue<System.EventArgs -> unit>(actions) //'a contains event properties
    with
        member hq.Add(action: System.EventArgs -> unit) = 
           actions.Enqueue(action)
        member hq.NextAction = 
            if actions.Count=0 
                then fun _ -> ()
                else actions.Dequeue()

//test code
let frm = new System.Windows.Forms.Form()

let myActions = [
    fun (e:System.EventArgs) -> printfn "You clicked with %A" (e :?> MouseEventArgs).Button
    fun _ -> printfn "Stop clicking me!!"
    fun _ -> printfn "I mean it!"
    fun _ -> printfn "I'll stop talking to you now."
    ]

let aq = new ActionQueue(myActions)

frm.Click.Add(fun e -> aq.NextAction e)

//frm.Click now executes the 4 actions in myActions in order and then does nothing on further clicks
frm.Show()

您可以点击表单4次,然后再进行进一步的点击时没有反应。

现在执行以下代码,表单将再次响应两次:

let moreActions = [
    fun _ -> printfn "Ok, I'll talk to you again. Just don't click anymore, ever!"
    fun _ -> printfn "That's it. I'm done with you."
    ]

moreActions |> List.iter (aq.Add)

1
某种程度上,我想要的是利用工作流来构建你手动完成的操作。该工作流将生成一系列函数,然后您只需遍历该序列即可。 - James Moore

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接