如何创建支持单步执行等功能的 F# 工作流程?

发布于 2024-08-17 21:19:06 字数 1776 浏览 3 评论 0 原文

我想创建一个构建器来构建表达式,该表达式在每个步骤后返回类似于延续的内容。

像这样:

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()
}

让事件触发工作流程中的下一步。 (特别是,我想在 iPhone 上的 MonoTouch - F# 中玩弄这种东西。传递 objc 选择器让我发疯。)

I'd like to create a builder that builds expressions that returns something like a continuation after each step.

Something like this:

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()

Where the 'let a' line returns something containing the rest of the expressions to be evaluated later on.

Doing this with a separate type for each number of steps is straightforward but of course not particularly useful:

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()

And the results are what I'm looking for:

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

But I can't figure out what the general case of this would be.

What I'd like is to be able to feed events into this workflow, so you could write something like:

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

And have events trigger a move to the next step in the workflow. (In particular, I want to play with this sort of thing in MonoTouch - F# on the iPhone. Passing around objc selectors drives me insane.)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

最好是你 2024-08-24 21:19:06

您的实现的问题在于,每次调用 Bind 都会返回“unit -> 'a”,因此您将针对不同数量的步骤获得不同类型的结果(一般来说,这是 monad/ 的可疑定义)计算表达式)。

正确的解决方案应该是使用其他类型,它可以表示具有任意步骤数的计算。您还需要区分两种类型的步骤 - 某些步骤仅评估计算的下一步,而某些步骤则返回结果(通过 return 关键字)。我将使用类型 seq>。这是一个惰性序列,因此读取下一个元素将评估下一步的计算。该序列将包含 None 值,但最后一个值除外,该值将为 Some(value),表示使用 return 返回的结果。

您的实现中的另一个可疑之处是 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

希望这有帮助!

PS:我发现这个问题很有趣!您介意我将我的答案添加到我的博客的博客文章中吗(http://tomasp.net/blog)?谢谢!

the problem with your implementation is that it returns "unit -> 'a" for each call to Bind, so you'll get a different type of result for different number of steps (in general, this is a suspicious definition of monad/computation expression).

A correct solution should be to use some other type, which can represent a computation with arbitrary number of steps. You'll also need to distinguish between two types of steps - some steps just evaluate next step of the computation and some steps return a result (via the return keyword). I'll use a type seq<option<'a>>. This is a lazy sequence, so reading the next element will evaluate the next step of the computation. The sequence will contain None values with the exception of the last value, which will be Some(value), representing the result returned using return.

Another suspicious thing in your implementation is a non-standard type of Bind member. The fact that your bind takes a value as the first parameter means that your code looks a bit simpler (you can write let! a = 1) however, you cannot compose stepwise computation. You may want to be able to write:

let foo() = stepwise { 
  return 1; }
let bar() = stepwise { 
  let! a = foo()
  return a + 10 }

The type I described above will allow you to write this as well. Once you have the type, you just need to follow the type signature of Bind and Return in the implementation and you'll get this:

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)

Now, let's look at using the type:

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 } 

If you want to run the computation step-by-step, you can simply iterate over the returned sequence, for example using the F# for keyword. The following also prints the index of the step:

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

Hope this helps!

PS: I found this problem very interesting! Would you mind if I addapted my answer into a blog post for my blog (http://tomasp.net/blog)? Thanks!

梦年海沫深 2024-08-24 21:19:06

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)

Monads and computation builders confuse the hell out of me, but I've adapted something I've made in an earlier SO post. Maybe some bits and pieces can be of use.

The code below contains an action queue, and a form where the Click event listens to the next action available in the action queue. The code below is an example with 4 actions in succession. Execute it in FSI and start clicking the form.

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()

You can click the form 4 times and then nothing happens with further clicks.
Now execute the following code, and the form will respond two more times:

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)
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文