実例に見る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
- persimmon-projects/Persimmon: Persimmonのソースコード
- Persimmonの特徴: Persimmonの特徴について
- 詳説コンピュテーション式 - ぐるぐる~: コンピュテーション式の解説
- コンピュテーション式のSourceメソッドを試す - pocketberserkerの爆走: Sourceメソッドについてのエントリ
- F# でのテスト用 DSL について考える - a wandering wolf: Persimmon誕生までの経緯
- BasisLib/Basis.Core: 自称「唯一きちんとreturnするコンピュテーション式を持ったライブラリ」
明日のF# Advent Calendar
日本語版はkos59125さん、英語版はSteve Shogrenさんです。 Steve Shogrenさんは「F# Polymorphism」とありますね。楽しみです!