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

pad*_*pad 24 dsl f# query-expressions computation-expression

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

在阅读扩展计算表达式时,我遇到了@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
    }
Run Code Online (Sandbox Code Playgroud)

我想知道如何在不使用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
Run Code Online (Sandbox Code Playgroud)

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

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

kvb*_*kvb 14

我很高兴你喜欢IL的例子.了解表达式如何被去除的最佳方法可能是查看规范(虽然它有点密集......).

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

C {
    op1
    op2
}
Run Code Online (Sandbox Code Playgroud)

如下去了:

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()))
Run Code Online (Sandbox Code Playgroud)

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

还有另一个技巧可以帮助理解计算表达式的编译形式,即(ab)使用F#3中计算表达式的自动引用支持.只需定义一个do-nothing Quote成员并使其Run返回其参数:

member __.Quote() = ()
member __.Run(q) = q
Run Code Online (Sandbox Code Playgroud)

现在你的计算表​​达式将评估它的desugared形式的引用.这在调试时非常方便.


Tom*_*cek 9

我不得不承认,当你使用像属性这样的查询表达式功能时,我并不完全理解计算表达式是如何工作的CustomOperation.但是,我的一些实验可能会有所帮助......

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

// Does not compile              // Compiles and works
moves { return! lr               moves { left 
        left }                           return! lr }
Run Code Online (Sandbox Code Playgroud)

对于只使用自定义操作,最常见的cusotom操作的计算(orderBy,reverse而这种)有一个类型M<'T> -> M<'T>,其中M<'T>是代表我们正在构建的东西(如表)的一些(可能是通用)类型.

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

type Command = Left | Right 
type Commands = Commands of Command list
Run Code Online (Sandbox Code Playgroud)

自定义操作,如leftright然后可以转化CommandsCommands并追加了新的一步,以列表的末尾.就像是:

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

请注意,这是从不同的yield返回只是一个单一的操作-或命令-因此yield需要Combine,如果您使用自定义操作多个单独的步骤结合起来,那么你永远不需要任何合并,因为自定义操作逐步构建的Commands价值作为一个整体.它只需要在开始时使用的一些初始 Commands值...

现在,我希望看到Zero那里,但它实际上调用Yield单位作为参数,所以你需要:

member x.Yield( () ) = 
  Commands[]
Run Code Online (Sandbox Code Playgroud)

我不确定为什么会这样,但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)
Run Code Online (Sandbox Code Playgroud)

(在某些时候,翻译将开始要求ForReturn,但在这里,他们可以被定义喜欢BindYield-我不完全理解的时候是用来替代).

然后你可以这样写:

let moves = MovesBuilder()

let lr = 
  moves { left
          right }    
let res =
  moves { left
          do! lr
          left 
          do! lr }
Run Code Online (Sandbox Code Playgroud)