読者です 読者をやめる 読者になる 読者になる

"ValueWithName"や"Auto-Quotation of Arguments at Method Calls"を使って変数名や関数名を取得する #FsAdvent

F#

この記事は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

https://github.com/fsharp/FSharpLangDesign/blob/5cec1d3f524240f063b6f9dad2f23ca5a9d7b158/FSharp-4.0/AutoQuotationDesignAndSpec.md

上記ドキュメントを読めばわかるとおり

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というライブラリのテストがわかりやすいと思います。

https://github.com/Gab-km/Funcy/blob/55b9ff30b6508b9089448ea4e54776450fc35af4/Funcy.Test/ApplicativeLawsCheck.fs#L80

Persimmon.DriedのPropnにはラベルをつける機能があるのですが、今回のように変数束縛するのであれば変数名からいい感じに名前を取得して重複させないようにしたいですね? というわけで先ほど紹介した機能の出番となります。

成果物

コードは下記で公開しています。

github.com

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>>) =

使う側はモジュールを一つオープンするだけで、変数名がラベルとして表示されるようになります。

実際に名前を取得する部分ですが

https://github.com/persimmon-projects/Persimmon.Dried.Quotations/blob/afc8893b0674902b95c8a08e5e70293e284b75d0/src/Persimmon.Dried.Quotations/Quotations.fs#L15

アクティブパターンを使ってひたすら地道に名前を取得しています。 また、引数の型をExpr<'T>にしておけば値は'Tなことが確定するので、安心してWithValueの第一引数やPropertyInfoで取得できるobjを'Tにダウンキャストできます。

おわりに

Expr は 4.0 になってさらなる可能性をもたらしてくれました。 これらの機能とコンピュテーション式を組み合わせると無限に悪巧みができそう有効活用ができそうなので、もっと使い倒してみたいところですね。

おわりにその2(追記)

Persimmonを実装しているプロジェクトのGitter日本語部屋があるので興味のある型はぜひ。

gitter.im