不带 for.in.do 的扩展计算表达式



我所说的扩展计算表达式是指通过自定义操作属性定义的自定义关键字的计算表达式。

在阅读有关扩展计算表达式的信息时,我遇到了非常酷的IL DSL,@kvb:

let il = ILBuilder()
// will return 42 when called
// val fortyTwoFn : (unit -> int)
let fortyTwoFn = 
    il {
        ldc_i4 6
        ldc_i4_0
        ldc_i4 7
        add
        mul
        ret
    }

我想知道在不使用构造的情况下如何编写操作for..in..do。我的直觉是它以x.Zero成员开头,但我还没有找到任何参考来验证这一点。

如果上面的示例技术性太强,下面是一个类似的DSL,其中列出了幻灯片的组件,但没有for..in..do

page {
      title "Happy New Year F# community"
      item "May F# continue to shine as it did in 2012"
      code @"…"
      button (…)
} |> SlideShow.show

我有几个密切相关的问题:

  • 如何在没有成员的情况下定义或使用扩展计算表达式For(即提供一个完整的小示例(?如果它们不再是 monad,我不会太担心,我对它们开发 DSL 很感兴趣。
  • 我们可以将扩展计算表达式与let!return!一起使用吗?如果是,有什么理由不这样做吗?我问这些问题是因为我没有遇到任何使用 let!return! 的例子。

我很高兴你喜欢IL的例子。 了解表达式如何脱糖的最好方法可能是查看规范(尽管它有点密集......

在那里我们可以看到类似的东西

C {
    op1
    op2
}

脱糖如下:

T([<CustomOperator>]op1; [<CustomOperator>]op2, [], fun v -> v, true) ⇒
CL([<CustomOperator>]op1; [<CustomOperator>]op2, [], C.Yield(), false) ⇒
CL([<CustomOperator>]op2, [], 〚 [<CustomOperator>]op1, C.Yield() |][], false) ⇒
CL([<CustomOperator>]op2, [], C.Op1(C.Yield()), false) ⇒
〚 [<CustomOperator>]op2, C.Op1(C.Yield()) 〛[] ⇒
C.Op2(C.Op1(C.Yield()))

至于为什么使用Yield()而不是Zero,这是因为如果作用域中有变量(例如,因为你使用了某些lets,或者在for循环中,等等(,那么你会得到Yield (v1,v2,...)Zero显然不能这样使用。 请注意,这意味着在 Tomas 的 lr 示例中添加一个多余的let x = 1将无法编译,因为将使用类型 int 而不是 unit 的参数调用Yield

还有另一个技巧可以帮助理解计算表达式的编译形式,即 (ab( 对 F# 3 中的计算表达式使用自动引号支持。 只需定义一个不执行任何操作Quote成员并Run返回其参数:

member __.Quote() = ()
member __.Run(q) = q

现在,您的计算表达式将计算为其脱糖形式的报价。 这在调试时非常方便。

我不得不承认,当您使用CustomOperation属性等查询表达式功能时,我不完全了解计算表达式的工作原理。但这里有一些来自我的实验的评论,可能会有所帮助......

首先,我认为不可能将标准计算表达式特征(return!等(与自定义操作自由组合。某些组合显然是允许的,但不是全部。例如,如果我定义自定义操作leftreturn!那么我只能在return!之前使用自定义操作:

// Does not compile              // Compiles and works
moves { return! lr               moves { left 
        left }                           return! lr }

至于仅使用自定义操作的计算,最常见的cusotom操作(orderByreverse和此类操作(都有一个类型M<'T> -> M<'T>其中M<'T>是代表我们正在构建的东西的一些(可能是泛型(类型(例如列表(。

例如,如果我们想构建一个表示左/右移动序列的值,我们可以使用以下Commands类型:

type Command = Left | Right 
type Commands = Commands of Command list

然后,自定义操作(如 leftright(可以将Commands转换为Commands并将新步骤追加到列表末尾。像这样:

type MovesBuilder() =
  [<CustomOperation("left")>]
  member x.Left(Commands c) = Commands(c @ [Left])
  [<CustomOperation("right")>]
  member x.Right(Commands c) = Commands(c @ [Right])

请注意,这与仅返回单个操作或命令yield不同,因此yield需要使用自定义操作Combine来组合多个单独的步骤,则永远不需要组合任何内容,因为自定义操作会逐渐构建整个Commands值。它只需要一些在开始时使用的初始Commands值......

现在,我希望在那里看到Zero,但它实际上调用了带有 unit 的Yield作为参数,因此您需要:

member x.Yield( () ) = 
  Commands[]

我不确定为什么会这样,但Zero经常被定义为Yield (),所以也许目标是使用默认定义(但正如我所说,我也希望在这里使用Zero......

我认为将自定义操作与计算表达式相结合是有意义的。虽然我对如何使用标准计算表达式有强烈的看法,但我对自定义操作的计算没有任何很好的直觉 - 我认为社区仍然需要弄清楚这一点:-(。但例如,您可以像这样扩展上述计算:

member x.Bind(Commands c1, f) = 
  let (Commands c2) = f () in Commands(c1 @ c2)
member x.For(c, f) = x.Bind(c, f)
member x.Return(a) = x.Yield(a)

(在某些时候,翻译将开始需要ForReturn,但在这里它们可以像BindYield一样定义 - 我不完全了解何时使用哪种替代方案(。

然后你可以写这样的东西:

let moves = MovesBuilder()
let lr = 
  moves { left
          right }    
let res =
  moves { left
          do! lr
          left 
          do! lr }

相关内容

  • 没有找到相关文章

最新更新