24

拡張計算式とは、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
    }

コンストラクトを使用せずに操作がどのように構成されているのだろうか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(つまり、小さな完全な例を提供します)? それらがもはやモナドでなくても、私はあまり心配していません。DSL の開発に興味があります。
  • let!とで拡張計算式を使用できますreturn!か? はいの場合、そうしない理由はありますか?let!と を使用した例に遭遇したことがないため、これらの質問をしreturn!ます。
4

2 に答える 2

14

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明らかにこの方法では使用できないためです。let x = 1これは、 Tomas の例に余分なものを追加するとコンパイルに失敗することを意味することに注意してlrください。Yieldintunit

コンパイルされた形式の計算式を理解するのに役立つ別のトリックがあります。これは、F# 3 で計算式の自動引用サポートを (ab) 使用することです。何もしないQuoteメンバーを定義し、Runその引数を返すだけにします。

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

これで、計算式はその脱糖形式の引用に評価されます。これは、デバッグ時に非常に便利です。

于 2013-01-02T22:13:38.567 に答える
9

属性のようなクエリ式機能を使用する場合、計算式がどのように機能するかを完全には理解していないことを認めなければなりませんCustomOperation。しかし、ここに役立つかもしれないいくつかの私の実験からのいくつかの意見があります....

return!第一に、標準的な計算式の特徴(など)をカスタム操作と自由に組み合わせることができないと思います。一部の組み合わせは明らかに許可されていますが、すべてではありません。たとえば、カスタム操作を定義leftした後、次の前にreturn!のみカスタム操作を使用できる場合: 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変換CommandsCommandsてリストの最後に追加できます。何かのようなもの:

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

これはyield、単一の操作(またはコマンド)のみを返すものとは異なり、カスタム操作を使用する場合は複数の個別のステップを組み合わせるyield必要があることに注意してください。カスタム操作は全体として値を徐々に構築するため、何も組み合わせる必要はありません。最初に使用される初期の空の値のみが必要です...CombineCommands 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)

(ある時点で、翻訳にはForandが必要になりますが、ここではandとReturn同じように定義できます。どの代替手段がいつ使用されるかは完全にはわかりません)。BindYield

次に、次のように書くことができます。

let moves = MovesBuilder()

let lr = 
  moves { left
          right }    
let res =
  moves { left
          do! lr
          left 
          do! lr }
于 2013-01-01T15:53:37.830 に答える