"ValueWithName"や"Auto-Quotation of Arguments at Method Calls"を使って変数名や関数名を取得する #FsAdvent
この記事はF# Advent Calendar 2015の一日目の記事です。
Expr.ValueWithName
の話
以下のようなコードがあるとします。
let f a = <@ a @> let hoge = "Hi!" f hoge
これはどう表示されるでしょうか?
F# 3系までは以下のとおり(VS2013のF# interactiveで確認したのでちょっと正確な言い方ではないかも)。
Value ("Hi!") {CustomAttributes = []; Raw = ...; Type = System.String;}
F# 4.0からは次のようになります(VS2015のF# interactiveで確認したのでちょっと正確な言い方ではないかも)。
ValueWithName ("Hi!", a) {CustomAttributes = []; Raw = ...; Type = System.String;}
Expr.ValueWithName
となり、束縛名が取れるようになりました。
ReflectedDefinitionとメソッド呼び出しでの引数の暗黙的なauto-quotation
上記ドキュメントを読めばわかるとおり
static member Plot([<ReflectedDefinition>] values:Expr<int>) = ...
と定義してあげると
Chart.Plot(f x + f y)
このようなコードは下記の形に暗黙的に変換されます。
Chart.Plot(<@ f x + f y @>)
ReflectedDefinitionのIncludeValueとExpr.WithValue
先ほどのドキュメントにも書かれているとおり
static member Plot([<ReflectedDefinition(true)>] values:Expr<X>) = ...
IncludeValueをtrueにすることで、quotationに変換する際に
Chart.Plot(Expr.WithValue(f x + f y, <@ f x + f y @>))
Expr.WithValue(obj, Expr)
の形に変換されるようになります。
Persimmon.Driedによる利用例
いきなり利用例の話…の前に、ちょっとした問題提起から。
今年に入って実装を進めているライブラリの一つにPersimmon.Driedというものがありますが、下記のブログにも述べられているとおり弱点が存在します。
Persimmon.Dried で性質をチェックする — a wandering wolf
今回の話に関連する部分を以下に引用します。
1つは apply した先でテストに失敗した時に、どの apply で失敗したかが明確ではない点です。大体はどこで失敗したか予想はつきますが、1つの property ブロックに複数の apply を入れていると、失敗結果から自明には分かりません。どの Prop が失敗したか、名前が分かるととても捗りますね。
具体的なコードはFuncyというライブラリのテストがわかりやすいと思います。
Persimmon.DriedのPropnにはラベルをつける機能があるのですが、今回のように変数束縛するのであれば変数名からいい感じに名前を取得して重複させないようにしたいですね? というわけで先ほど紹介した機能の出番となります。
成果物
コードは下記で公開しています。
ReflectedDefinition、WithValue, NameWithValueを使って名前を取得する
まず、既存のPropertyBuilderには手を入れたくないので別ライブラリとしてコンピュテーション式のビルダーをラップします。 その後、該当部分のメソッド引数にReflectedDefinitionをつけていきます。
// 定義部分だけてきとーに抜粋 member __.Apply<'T, 'U when 'U :> Prop>(s: PropertiesState<'T>, [<ReflectedDefinition(true)>] expr: Expr<'U>) = member __.ApplyReturn(s, [<ReflectedDefinition(true)>] expr: Expr<Prop<'T>>) =
使う側はモジュールを一つオープンするだけで、変数名がラベルとして表示されるようになります。
実際に名前を取得する部分ですが
アクティブパターンを使ってひたすら地道に名前を取得しています。
また、引数の型をExpr<'T>
にしておけば値は'T
なことが確定するので、安心してWithValue
の第一引数やPropertyInfoで取得できるobjを'T
にダウンキャストできます。
おわりに
Expr は 4.0 になってさらなる可能性をもたらしてくれました。
これらの機能とコンピュテーション式を組み合わせると無限に悪巧みができそう有効活用ができそうなので、もっと使い倒してみたいところですね。
おわりにその2(追記)
Persimmonを実装しているプロジェクトのGitter日本語部屋があるので興味のある型はぜひ。