実例に見るSource変換活用術

これはF# Advent Calendar 2014の5日目の記事です。 昨日の記事はyukitosさんによるF# Project Scaffoldを使ってプロジェクトを作成するでした。 F# Project Scaffold、便利そうですね。 Chocolateyみたいに導入が簡単だと、もっといい感じになるような気がします。

さて、コンピュテーション式おじさんです、こんにちは。 最近はPersimmonというF#用のテスティングフレームワークを作っています。 Persimmon自体はまだベータ版ですが、今回はこのプロジェクトで得た知見の一つを紹介したいと思います。 誰得エントリですね。

続行可能なアサーション

Persimmonでのアサーションは、続行可能なアサーションと続行不可能なアサーションに分類されます。 例えば、

test "sample test" {
  do! assertEquals x y
  do! assertEquals a b
}

とあった時、最初のアサーションに通らなくても次のアサーションは実行できるとうれしいです。

それに対して、

let originalAssert x y = test "" {
  do! assertEquals x y
  return x
}

test "sample test" {
  let! b = originalAssert x y
  do! assertEquals a b
}

とあった時、二番目のアサーションは一番目のアサーションの結果を使っていますから、次のアサーションは実行しようがありません。

これを実現するためにはどうすればいいでしょうか?

Bindのオーバーロード

最初に考えたのは、Bindオーバーロードしてしまうというものでした。 その時に書いたのが以下のコードです。

アイディアとしては、Bindの第二引数として渡される関数(後続の処理)が必要としている型がunitかそれ以外かでBindを分けてしまう、というものです。 しかしこれは、コメントにもあるようにコンパイルエラーになってしまいます。

Sourceメソッドオーバーロードによる疑似的なBindのオーバーロードの実現

let!(やdo!)は、コンパイラによってBindメソッドに変換されますが、 ビルダーにSourceメソッドが定義されている場合、第一引数がSourceに渡されます。

Sourceがある場合のlet!の変換

T(let! p = e in ce, C) = T(ce, fun v -> C(b.Bind(b.Source(e), fun p -> v)))

Sourceオーバーロードし、中で同じ型にしてしまえばBindオーバーロードを間接的に実現できそうです。

やってみた

以下、単純化した例です。

type BindingValue<'T> =
  | UnitValue of 'T (* always unit *)
  | NonUnitValue of 'T

type SomeBuilder() =
  member __.Source(x: unit) = UnitValue x
  member __.Source(x: _) = NonUnitValue x
  member __.Bind(x: BindingValue<'T>, f) =
    match x with
    | UnitValue x ->
        (* unitの場合の処理を書く
           Persimmonの場合、アサーションに通らなくても以降の処理を続行するようなコードになっている
           アサーションの結果は、BindingValueが直接'Tを保持するのではなく、
           AssertionResult<'T>を保持することで持ちまわすようにしている *)
        assert (typeof<'T> = typeof<unit>)
        (* ここで直接()を渡してしまうと、fがunit -> 'Uのように推論されてしまうので、
           Unchecked.defaultof<'T>を使うことで回避 *)
        f Unchecked.defaultof<'T>
    | NonUnitValue x ->
        (* unit以外の場合の処理を書く
           Persimmonの場合、アサーションに通らない場合は以降の処理を続行せずに結果を返し、
           アサーションを通っていた場合は後続の処理を続行するようなコードになっている *)
        f x

このように、Sourceメソッドの引数で分岐し、戻り値をBindingValueという同じ型にするようなオーバーロードを用意することで、 Bindメソッドの中で処理を分岐できるようになります。 ちなみにreturn!を提供する場合、ReturnFromでもSourceメソッドが呼び出されるため、ReturnFromメソッドの中でBindingValueを剥いでやる必要があります。

このように、Sourceメソッドを使うとBindオーバーロードが疑似的に実現できます。

Persimmonのほかのコンピュテーション式

Persimmonでは、他にもパラメタライズテスト用のコンピュテーション式と、例外のテスト用のコンピュテーション式を用意しています。 パラメタライズドテスト用のコンピュテーション式は、シンプルなカスタムオペレーションの実装例となっています。 例外のテスト用のコンピュテーション式は、シンプルな割に便利なものになっています。 興味のある人は見てみるといいでしょう。

みなさんも、PersimmonやBasis.Coreのコンピュテーション式を参考に、 色々なコンピュテーション式を作ってみましょう!

参考URL

明日のF# Advent Calendar

日本語版はkos59125さん、英語版はSteve Shogrenさんです。 Steve Shogrenさんは「F# Polymorphism」とありますね。楽しみです!