没有for..in..do的扩展计算表达式

时间:2013-01-01 12:37:53

标签: f# dsl computation-expression query-expressions

扩展计算表达式的意思是使用CustomOperation属性定义的自定义关键字的计算表达式。

在阅读extended computation expressions时,我遇到了@kvb非常酷的IL DSL:

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!的例子。

2 个答案:

答案 0 :(得分:14)

我很高兴你喜欢IL的例子。理解表达式如何被贬低的最好方法可能是查看spec(虽然它有点密集......)。

我们可以看到像

这样的东西
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的let x = 1示例中添加多余的lr将无法编译,因为Yield将使用int类型的参数而不是unit进行调用}。

还有另一个技巧可以帮助理解计算表达式的编译形式,即(ab)使用F#3中计算表达式的自动引用支持。只需定义一个不做任何事情Quote成员并制作Run只返回其论点:

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

现在,您的计算表达式将评估其desugared形式的引用。这在调试时非常方便。

答案 1 :(得分:9)

我必须承认,当您使用查询表达式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,但它实际上以单位作为参数调用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 }