nameofについて
F# Advent Calendar 2020の5日目のエントリーです。
F#5.0で導入された nameof
は便利なんですが、いくつか制限があります。
まず、 nameof
はファーストクラス関数ではありません。
そのため、
let f = nameof
はできないx |> nameof
はできない
のような制限があります。
ひとつめは出来たら面白いですが、もし実装しようとすると関数呼び出しのたびに実体が nameof
かどうか見ないといけなくなりそうで、
おそらく実現されることはないでしょう。
ふたつめは例えばinline関数のみに限って緩和されるようなことはあるかもしれませんが、どうなんでしょうか。
他にも、昨日のエントリーで紹介した nameof
パターンにも制限があります。
nameof
は nameof symbol
と nameof<'T>
の2つの形式がありますが、 nameof
パターンで使えるのは前者のみです。
// こうは書けない let f<'a> = function | nameof<'a> -> "yes" | _ -> "no"
おまけ
nameof
はファーストクラスの関数ではないのは上で紹介した通りですが、実はシンボルではあります。
そのため、 nameof nameof
は合法で、 "nameof"
が返されます。
また、演算子もシンボルなので、 nameof (+)
のようにすれば "+"
が返されます。
面白いですね。
F# 5.0の新機能 その2
F# Advent Calendar 2020の1日目の記事であるF# 5.0の新機能で大体書かれているのですが、2つ抜けているものがあるので紹介します。
nameofパターン
nameof
の値でパターンマッチできます。
RFCにはタグを nameof
で取り出しておいて、それを元にデシリアライズする例が載っています。
// 使う型 type RecordedEvent = { EventType: string; Data: byte[] } type MyEvent = | A of AData | B of BData
// シリアライズ用関数 let serialize (e: MyEvent) : RecordedEvent = match e with | A adata -> { EventType = nameof A; Data = JsonSerializer.Serialize<AData>(adata) } | B bdata -> { EventType = nameof B; Data = JsonSerializer.Serialize<BData>(bdata) }
// デシリアライズ用関数 let deserialize (e: RecordedEvent) : MyEvent = match e.EventType with | nameof A -> A (JsonSerializer.Deserialize<AData>(e.Data)) | nameof B -> B (JsonSerializer.Deserialize<BData>(e.Data)) | t -> failwithf "Invalid EventType: %s" t
地味に便利ですね。
string関数の実装変更
string
は今まで静的に解決される型パラメーターを取るinline関数だったのですが、F#5.0からは普通の型パラメーターを取るinline関数になります。
また、今までデッドコードだったコードを有効化し、より多くの型に対するケースを追加しています。
これは例を見てもらうとびっくりするかもしれません。
// 今まではエラーだったが、F#5.0からはエラーにならない type Either<'L, 'R> = | Left of 'L | Right of 'R override this.ToString() = match this with | Left x -> string x | Right x -> string x
他にもフォーマットに "g"
を指定したのを null
に変更したりもしています。
diffを見てもらった方が分かりやすいかもしれません。
FParsecでJSONパーサーを書いてみる話
F# Advent Calendar 2017の4日目の記事です。 NGK2017B昼の部でパーサーコンビネーターについてLTしてきたので、その内容について書きます。 ただし、内容は大幅に加筆修正しています。
導入
世の中にはパースすべきものであふれています。 例えば、下記のようなものがあります。
構造を持ったものはそこら中にあります。 これらを処理するためにどうすればいいでしょうか。
一つの方法として、正規表現を使うというものがあります。 しかし、(本来の)正規表現ではネストする文法などは扱えません。 拡張機能として、ネストする文法が扱えるようになっているような処理系もあります。 しかし、そもそもそんな複雑な正規表現には近寄りたくないですよね。
では、文字列操作関数を駆使してどうにかする方法はどうでしょうか。 ごくシンプルなものならそれでもいいですが、すぐに限界が訪れます。 なんの指針もなしに書いてしまうと、機能拡張も保守も困難なひどいコードが出来上がります。
こういうものは、パーサーを使って処理するのがいいでしょう。
パーサーとは?
パーサーとは、一次元的な構造、例えば文字列などから構文木を作るものと言えます。
例えば、ログの中身から LogEntry
のリスト構造にしたり、設定ファイルの中身から Config
オブジェクトにしたり、ソースコードをASTに変換したりするものです。
では、パーサーはどのようにして作ればいいのでしょうか。 それには、おおざっぱに3つの方法があります。
- 手書き
- パーサージェネレーター
- パーサーコンビネーター
手書きによる方法
手書きによる方法では通常、LL法と呼ばれる手法により、再帰関数としてパーサーを書きます。 どのくらいの複雑さを持った文法を相手にしているのかが自明ではなくなるので、この方法に最初から手を出すのはおすすめしません。
パーサージェネレーターによる方法
文法規則のためのDSLを、パーサージェネレーターというツールを使い、その文法規則を解析するためのコードを生成する方法です。 文法規則を修正した際に、コードの再生成が必要になるため、手書きや後述のパーサーコンビネーターほど手軽ではありません。
パーサーコンビネーターによる方法
基本的なパーサーとパーサーを組み合わせるための関数から構成される、パーサーを作るためのライブラリです。 単なるライブラリなので、コードの生成などの追加のビルドプロセスを必要としません。 しかし、手書きやパーサージェネレーターに比べると処理速度の面で劣ることがあるため、頻繁に実行する必要がある場面では計測をするといいでしょう。
3つの方法をざっくりと紹介しましたが、まずは手軽なパーサーコンビネーターから始めてみるのがおすすめです*1。
パーサーコンビネーターの特徴
パーサーコンビネーターは次のような特徴を持ちます。
- パーサーが部品として再利用可能
- 各パーサー(部品)単位でテスト可能
- 表現力が高い
- パーサーを実行時に組み立てたりもできる
なにから始めればいいか
パーサーコンビネーターを使うと決めたとして、最初に取り組む題材にはどのようなものを選べばいいでしょうか。 ここでは、下記の3つの題材について考えてみます。
どの題材についても言えることですが、「すでにあるライブラリを使えばいい」という考えではいつまでたっても自分でパーサーが書けるようになりません。 パーサーが自分で書けるようになると、プログラマーとして対応できる範囲が広がり、文字列解析に対する判断力も上がります。 たとえすでにライブラリがあったとしても、練習としてそれらを自前で実装しなおすことには大きな意味があるのです。
数式
伝統的なものとして、簡単な数式があります。 四則演算からはじめて、かっこの追加や変数の追加など、発展も考えやすい題材です。 しかし、出来上がるものが地味なので、ありがたみや達成感が薄いのがつらいところです。
YAML
YAMLは設定ファイルなどで見かける形式です。 yaml.orgに仕様があります。 YAMLがパース出来るようになると、設定ファイルをオブジェクトに変換するコードが書けるようになるので、数式よりも達成感は大きいでしょう。 しかし、YAML1.0でもかなり巨大な仕様なので、最初の題材としてはあまりおすすめできません。
JSON
JSONは、設定ファイルのほかにもWebAPIのリクエストボディやレスポンスボディなど、様々な場所で使われています。 json.orgに仕様があります。 YAMLよりもはるかにコンパクトな仕様であり、さらに文法もパースしやすいように考えられたものになっています*2。 そのため、最初の題材としてはJSONがおすすめです。
もし、JSONで拍子抜けしてしまうようであれば、HOCONの機能をつけ足してみるとか、TOMLなど別の形式のパーサーを書いてみるといいでしょう。
パーサーを書く大まかな流れ
パーサーを書く流れとして、次の3つの手順を繰り返すのがいいでしょう。
- 解析したデータを格納するためのデータ構造を用意する
- パーサーを組み合わせて、パース結果を用意したデータ構造に変換する
- 作ったパーサーをテストし、通ったら1や2に戻る
JSONでの例
まずは、 [1,2,3]
というJSONをパースできるところを目指します。
このJSONを表すのに必要最小限のデータ構造を考えます。
type Json = | JNumber of float // F#のfloatは64bit | JArray of Json list // F#では配列よりもlistの方が扱いやすい
[1,2,3]
という文字列を、 JArray [JNumber 1.0; JNumber 2.0; JNumber 3.0]
に変換できたらとりあえずの目標達成ということになります。
いきなり JNumber
と JArray
の2つに対応するのは難しいので、 JNumber
を解析するパーサーだけを作ってみます。
このエントリでは、FParsecというF#用のパーサーコンビネーターライブラリを使います。
FParsecには pfloat
という、浮動小数点数形式の文字列("1.5"
とか)をパースするパーサーが用意されています*3。
このパーサーを使って浮動小数点数形式の文字列を解析すると、 float
型の結果が得られます。
let parseBy p str = // run関数はFParsecが用意している、パーサーを実行するための関数 match run p str with | Success (res, _, _) -> res | Failure (msg, _, _) -> failwithf "parse error: %s" msg "1.5" |> parseBy pfloat // => 1.5
欲しいのは、 float
ではなく JNumber
なので、結果を変換する必要があります。
そのための方法はいくつかありますが、最も手軽な方法として |>>
演算子を使ってみます。
let jnumber = pfloat |>> (fun x -> JNumber x)
|>>
は左項のパーサーの結果を右項の関数によって変換する演算子です*4。
|>>
演算子の戻り値の型もパーサーになるため、ここで作った jnumber
も先程用意した parseBy
関数に渡せます。
"1.5" |> parseBy jnumber // => JNumber 1.5
これでJSONの数値がパース出来るようになったので、次にJSONの配列に進みます。
let jarray = sepBy jnumber (pchar ',') |> between (pchar '[') (pchar ']') |>> (fun xs -> JArray xs)
新しいパーサーが3つ出てきました。
一番使われている pchar
関数は、指定された文字をパースするパーサーを返します。
sepBy
関数は、第一引数で指定されたパーサーが、第二引数で指定されたパーサーで区切られて連続するような文字列をパースするパーサーを返します。
"1,2,3" |> parseBy (sepBy jnumber (pchar ',')) // => [JNumber 1.0; JNumber 2.0; JNumber 3.0]
between
関数は、第一引数で指定されたパーサーと第二引数で指定されたパーサーの間に第三引数で指定されたパーサーが来るような文字列をパースするパーサーを返します。
コードでは第三引数はパイプライン演算子で渡しています。
jarray
全体を試してみましょう。
"[1,2,3]" |> parseBy jarray // => JArray [JNumber 1.0; JNumber 2.0; JNumber 3.0]
目標だった、 [1,2,3]
のパースができるようになりました。
このようにして小さいパーサーを組み立てて確認しながら目標に向かっていきやすいのが、パーサーコンビネーターの素晴らしい点です。
はまりがちポイント
パーサーコンビネーターでパーサーを書く際に、入門者がはまりそうなポイントを見ていきましょう。
空白の無視
パーサーの前段には、レキサー(字句解析器)を置くことがあります。 レキサーは「文字列→トークン列」への変換を行い、パーサーは文字列を直接扱うのではなく、トークン列を扱うようにするのです。 レキサーで空白やコメントを読み飛ばすようにしておけば、それらを意識せずにパーサーが書けます。
しかし、パーサーコンビネーターによってはトークン列が扱えないものがあります。 今回使っているFParsecもそのうちの一つなので、本来いレキサーがやるような仕事もパーサーでやらなければなりません。
例えば、先程の jarray
パーサーですが、空白を挟むとうまく動きません。
"[ 1, 2, 3 ]" |> parseBy jarray // => エラー
空白のスキップを下手に書くと、繰り返しや再帰と組み合わさった際に簡単に無限ループに陥ってしまいます。 そこで、空白スキップの戦略を決めておくといいでしょう。 例えば、各パーサーの「後ろの空白」を読み飛ばすようにし、最後に全体のパーサーの「前の空白」を読み飛ばすようにすれば、空白のスキップが実現できます。
let ws = spaces let jnumber = pfloat .>> ws |>> JNumber let jarray = sepBy jnumber (pchar ',' >>. ws) |> between (pchar '[' >>. ws) (pchar ']' >>. ws) |>> JArray let parseBy p str = match run (ws >>. p) str with | Success (res, _, _) -> res | Failure (msg, _, _) -> failwithf "parse error: %s" msg "[ 1, 2, 3 ]" |> parseBy jarray // => JArray [JNumber 1.0; JNumber 2.0; JNumber 3.0]
spaces
は、0文字以上の空白文字をパースするパーサーです。
これまで見てきた pfloat
や pchar
と違い、パースした文字列を結果として返さない(unit
を返す)点に注意してください。
spaces
をそのまま使ってもいいのですが、長いので ws
という別名(whitespaceの略)を与えています。
また、もしコメント機能を追加したいような場合でも、 ws
の定義を変更すれば対応できます。
このパーサーのバリエーションとして、 spaces1
というものもあり、こちらは1文字以上の空白文字をパースします。
このように、サフィックスとして1が付くようなパーサーがほかにもあります。
例えば、 jarray
の実装に使っている sepBy
のバリエーションである sepBy1
などです。
こちらも、 sepBy
は0回以上の繰り返しを表すのに対し、 sepBy1
は1回以上の繰り返しを表します。
.>>
と >>.
は、パーサーを連続して適用する演算子です。
ピリオドが付いている方を結果として使い、付いていない方はパースはしますが結果を捨てることを意味しています。
また、どちらの結果も保持する .>>.
という演算子もあり、この場合結果はタプルになります。
let hi = pchar 'h' .>>. pchar 'i' |>> (fun (h, i) -> string h + string i) // 結果はタプルとして渡される let hi2 = pchar 'h' .>> pchar 'i' // pchar 'i' の結果は捨てる |>> (fun h -> string h) // 捨てたので結果に含まれない let hi3 = pchar 'h' >>. pchar 'i' // pchar 'h' の結果は捨てる |>> (fun i -> string i) // 捨てたので結果に含まれない "hi" |> parseBy hi // => "hi" "hi" |> parseBy hi2 // => "h" "hi" |> parseBy hi3 // => "i" // 捨てるとは言っても、パースしないわけではないので、 // 後続のパーサーが失敗すると全体として失敗する "ho" |> parseBy hi2 // => エラー
jarray
の新しい定義をもう一度見てみます。
let jarray = sepBy jnumber (pchar ',' >>. ws) |> between (pchar '[' >>. ws) (pchar ']' >>. ws) |>> JArray
sepBy
や between
で .>>
ではなく >>.
を使っていますが、解析結果には含まれないため、どちらを使っても構いません。
しかし、 .>>
よりも >>.
の方が効率がいい場合があるため、どちらでもいい場合は >>.
を使うようにするといいでしょう。
ちなみに、 p |> between popen pclose
(popenとpcloseに挟まれたpをパースするパーサー)は popen >>. p .>> pclose
(popenを解析して結果を捨て、pを解析し、pcloseを解析して結果を捨てるパーサー)と同じ意味になります。
let between popen pclose p = popen >>. p .>> pclose
実際には、 >>.
と .>>
がインライン展開されたような定義になっているため、速度を気にする場面では between
を使うといいでしょう*5。
完全一致
これまで作ってきたパーサーですが、実はパース対象の文字列の後ろに余分なものがあってもパースが成功してしまいます(前方一致)。
"[1, 2, 3]]]]" |> parseBy jarray // => JArray [JNumber 1.0; JNumber 2.0; JNumber 3.0]
これでは困ることが多いので、 eof
というパーサーを最後に合成するのが普通です。
let parseBy p str = // わかりやすさのため、between ws eof p ではなく >>. と .>> を使ったが、 // 一番上のレベルのパーサーなので速度上の問題にもならないはず match run (ws >>. p .>> eof) str with | Success (res, _, _) -> res | Failure (msg, _, _) -> failwithf "parse error: %s" msg
これで、完全一致しない場合はエラーになるようになりました。
文字列のエスケープ
JSONの文字列にはエスケープがあります。 エスケープを考えなくていい場合、文字列のパーサーは次のようにすればいいでしょう。
type Json = | JNumber of float | JString of string | JArray of Json list let jstring = manyChars (noneOf ['"']) |> between (pchar '"') (pchar '"') .>> ws |>> JString
noneOf
関数は、第一引数で指定された seq<char>
に含まれる char
以外の文字にマッチするパーサーを返します。
// string は seq<char> でもあるので、 ['x'; 'y'; 'z'] の代わりに "xyz" と書いてもOK "a" |> parseBy (noneOf "xyz") // => 'a' "y" |> parseBy (noneOf "xyz") // => エラー
manyChars
関数は、 char
を返すパーサーを受け取り、その0回以上の繰り返しをパースして、文字列に連結するパーサーを返します*6。
"abc" |> parseBy (manyChars (noneOf "xyz")) // => "abc" "axc" |> parseBy (manyChars (noneOf "xyz")) // => エラー
エスケープ非対応版の文字列パーサーを再掲します。
let jstring = manyChars (noneOf ['"']) // 「"」以外の文字の繰り返しが |> between (pchar '"') (pchar '"') // 「"」に挟まれているのが文字列 .>> ws |>> JString
これをエスケープに対応させるには、エスケープシーケンスとそうでないものを分割して考えます。
まず、エスケープされていない、普通の文字とは何かを考えてみます。 これは簡単で、「エスケープの開始文字()と、文字列の終了文字(")以外の文字」です。
let nonEscapedChar = noneOf ['\\'; '"']
次に、エスケープされた文字を考えてみます(\uxxxx形式は省略します)。 エスケープされた文字は、開始文字()から始まり、エスケープシーケンスの種類を表す文字が続きます。
let escapedChar = pchar '\\' >>. anyOf @"\""/bfnrt"
anyOf
関数は noneOf
の逆で、引数で指定された seq<char>
のうちの1文字をパースするパーサーを返します。
"a" |> parseBy (anyOf "abc") // => 'a' "c" |> parseBy (anyOf "abc") // => 'c' "z" |> parseBy (anyOf "abc") // => エラー
これで、エスケープされた文字もパース出来るようになりました。
@"\\" |> parseBy escapedChar // => '\\' @"\""" |> parseBy escapedChar // => '"' @"\n" |> parseBy escapedChar // => 'n'
しかし、これだけだと \n
は改行文字ではなく n
という文字になってしまうため、結果を変換する必要があります。
変換しなければならないのは、 b
, f
, n
, r
, t
の5つです。
\
, "
, /
は変換せず、そのまま使います。
let convEsc = function | 'b' -> '\b' | 'f' -> '\f' | 'n' -> '\n' | 'r' -> '\r' | 't' -> '\t' | c -> c // '\\', '"', '/' はそのまま使う let escapedChar = pchar '\\' >>. anyOf @"\""/bfnrt" |>> convEsc
これで、エスケープされていない文字のパーサーとエスケープされた文字のパーサーが手に入りました。
文字列は、その中の1文字1文字がどちらかでパース出来るものの並びになります。
<|>
という演算子は、この「どちらか」を表すパーサーを作る演算子です。
"a" |> parseBy (pchar 'a' <|> pchar 'b') // => 'a' "b" |> parseBy (pchar 'a' <|> pchar 'b') // => 'b'
それさえわかれば、あとは簡単です。
let jstring = manyChars (nonEscapedChar <|> escapedChar) // どちらかの繰り返し |> between (pchar '"') (pchar '"') .>> ws |>> JString "\"abc\"" |> parseBy jstring // => JString "abc" "\"abc\\ndef\"" |> parseBy jstring // => JString "abc\ndef"
エスケープ非対応版を再掲しておきます。
manyChars
に渡すパーサーだけ変えたことが分かります。
let jstring = manyChars (noneOf ['"']) |> between (pchar '"') (pchar '"') .>> ws |>> JString
選択されない選択肢
文字列パーサーの実装で紹介した <|>
演算子ですが、気を付けなければならないことがあります。
JSONには浮動小数点数しかありませんが、F#には整数があるので、整数と浮動小数点数を区別するようにしてみましょう。
type Json = //| JNumber of float | JInteger of int | JFloat of float | JString of string | JArray of Json list let ws = spaces // jnumberの定義を変更 let minusSign = opt (pchar '-') |>> Option.isSome let digit1to9 = anyOf ['1'..'9'] let integer = (many1Chars2 digit1to9 digit <|> pstring "0") |>> int let jinteger = minusSign .>>. integer |>> (fun (hasMinus, x) -> JInteger (if hasMinus then -x else x)) let jfloat = tuple3 minusSign integer (pchar '.' >>. integer) |>> (fun (hasMinus, i, flac) -> let f = float i + float ("0." + string flac) JFloat (if hasMinus then -f else f)) let jnumber = (jinteger <|> jfloat) .>> ws
pfloat
よりもかなり複雑になりましたが、ほとんどjson.orgの定義を書き下しているだけです(指数表記は未対応です)。
opt
関数は、引数で指定されたパーサーが成功した場合 Some
でくるみ、失敗した場合 None
を返すパーサーを作ります。
引数で指定されたパーサーが失敗した場合でも、 opt
自体は成功するので、省略可能な構文要素を表すために使います。
JSONの数値の場合、負の符号は省略可能なので、 opt
で実現しています。
digit
は 0
から 9
までの文字をパースするパーサーです。
"-2" |> parseBy (opt (pchar '-') .>>. digit) // => (Some '-', '2') "1" |> parseBy (opt (pchar '-') .>>. digit) // => (None, '1')
many1Chars2
関数は、1回以上の繰り返しをパースするパーサーを返します。
manyChars
や many1Chars
に似ていますが、引数を2つ取り、最初の1回は1つめの引数のパーサーを、以降は2つめの引数のパーサーを使います。
複数桁の数値は、先頭に 0
を許しません(1230はOKだけど、0123はNG)。
それを簡単に実現するために、 many1Chars2
を使っています。
"1230" |> parseBy (many1Chars2 (anyOf ['1'..'9']) digit) // => "1230" "0123" |> parseBy (many1Chars2 (anyOf ['1'..'9']) digit) // => エラー
pstring
関数は、指定された文字列をパースするパーサーを返します。
many1Chars2 digit1to9 digit
だけでは "0"
がパース出来ないので、 <|>
演算子で pstring "0"
を合成することで対応しています。
tuple3
関数は、3つのパーサーを受け取ってそれらの結果を3要素タプルにするパーサーを返します。
.>>.
演算子を2回使ってもいいのですが、 .>>.
演算子を複数回使う場合、2要素タプルがネストしてしまいます。
これを避けるために、 tuple3
が使えます。tuple2
から tuple5
まで用意されています。
"1.2" |> parseBy (digit .>>. pchar '.' .>>. digit) // => (('1', '.'), '2') "1.2" |> parseBy (tuple3 digit (pchar '.') digit) // => ('1', '.', '2')
浮動小数点数は、「符号」「整数部」「小数部」の3つに分割できるため、 tuple3
を使っています。
jnumber
まわりの定義を再掲します。
let minusSign = opt (pchar '-') |>> Option.isSome let digit1to9 = anyOf ['1'..'9'] let integer = (many1Chars2 digit1to9 digit <|> pstring "0") |>> int let jinteger = minusSign .>>. integer |>> (fun (hasMinus, x) -> JInteger (if hasMinus then -x else x)) let jfloat = tuple3 minusSign integer (pchar '.' >>. integer) |>> (fun (hasMinus, i, flac) -> let f = float i + float ("0." + string flac) JFloat (if hasMinus then -f else f)) let jnumber = (jinteger <|> jfloat) .>> ws
これで動きそうに見えますが、実はこれでは動きません。
"1" |> parseBy jnumber // => JInteger 1 "2.0" |> parseBy jnumber // => エラー
エラーメッセージを見てみましょう。
parse error: Error in Ln: 1 Col: 2 2.0 ^ Expecting: decimal digit or end of input
2.0
の .
の位置で、数値か入力終了を期待していた、と出ています。
入力終了を期待していたということなので、 eof
の合成をしない場合にどういう挙動になるのか確認してみましょう。
let parseByJNumber str = match run jnumber str with // eofを合成しない | Success (res, _, _) -> res | Failure (msg, _, _) -> failwithf "parse error: %s" msg parseByJNumber "1" // => JInteger 1 parseByJNumber "2.0" // => JInteger 2
eof
を合成している parseBy jnumber
ではエラーになりましたが、eof
を合成していない parseByJNumber
では(JFloat 2.0
ではなく) JInteger 2
が返ってきました。
この結果が意味しているのは、 2.0
をパースする際、 2
までを読んで JInteger
としてしまっており、 .0
が入力に残ったままになっているということです。
これは、 jfloat
が成功する入力では必ず jinteger
が成功してしまうことが原因です。
つまり、この jnumber
の定義では jfloat
が使われることはありません。
では、項を入れ替えてみてはどうでしょうか。
let jnumber = jfloat <|> jinteger
これであれば、 jfloat
に失敗すれば jinteger
が試されるため、問題ないように見えます。
しかし、今度は "1"
のパースに失敗します。
parse error: Error in Ln: 1 Col: 2 1 ^ Note: The error occurred at the end of the input stream. Expecting: decimal digit or '.'
数値か .
を期待していたけど、入力が終了してしまった、と言っています。
これは、 <|>
演算子が左項のパーサーが失敗しても、そのパーサーが消費した入力を戻さないことが原因です。
jnumber
はまず jfloat
を試しますが、 "1"
が入力として与えられた際、整数部の入力までは成功します。
そして、小数点をパースしようとしますが .
が見つからないため jfloat
が失敗します。
この際に整数部を消費してしまったため、 jinteger
が成功することはありません。
jinteger
は数値を期待しているため、エラーがマージされ、「数値か .
が期待されている」というメッセージになります。
この問題を最も手軽に解決するためには、失敗した際に入力を戻す*7パーサー attempt
を合成します。
let jnumber = attempt jfloat <|> jinteger
attempt
は入力を巻き戻すため、 attempt
を使わずに済むなら使わない方が効率の良いパーサーになります。
<|>
演算子は、効率を重視してデフォルトの挙動が入力を巻き戻さないようになっているので、 attempt
をやみくもに使うのはやめましょう。
attempt
を使わずに今回の問題を解決する方法もあります。
例えば、 jinteger
と jfloat
を削除し、 jnumber
の定義を次のように変更します。
let jnumber = tuple3 minusSign integer (opt (pchar '.' >>. integer)) |>> (function | (hasMinus, i, None) -> JInteger (if hasMinus then -i else i) | (hasMinus, i, Some flac) -> let f = float i + float ("0." + string flac) JFloat (if hasMinus then -f else f))
この定義は、小数点数以降を opt
で省略可能にし、 |>>
による変換部分で JInteger
にするか JFloat
にするかを決めています。
これにより、 attempt
を使わずに整数と浮動小数点数を区別できるパーサーが手に入りました。
同じプレフィックス(今回の場合整数部)を持つ選択肢が3つ以上になった場合、 opt
では解決できなくなります。
そのような場合でも、今回の手法を応用すれば解決可能です。
type Json = | JInteger of int | JFloat of float | JRational of int * int // 有理数を追加 | JString of string | JArray of Json list // ..略.. let jnum = // choice [p1; p2; ...; pn] は、 p1 <|> p2 <|> ... <|> pn と同じ意味で、高速 choice [ pchar '.' >>. integer |>> (fun frac i -> JFloat (float i + float ("0." + string frac))) pchar '/' >>. integer |>> (fun d n -> JRational (n, d)) preturn JInteger ] // preturnは、常に成功し、引数に指定した結果を返すパーサーを返す関数 let jnumber = tuple3 minusSign integer jnum |>> (fun (hasMinus, i, f) -> f (if hasMinus then -i else i))
再帰文法
さて、ここまでのパーサーでは、 JArray
の要素に JInteger
か JFloat
しか許しません。
型の定義としては、 JString
でも JArray
自体でもいいようになっていますので、この対応をしましょう。
現状の jarray
の定義を確認しておきましょう。
let jarray = sepBy jnumber (pchar ',' >>. ws) |> between (pchar '[' >>. ws) (pchar ']' >>. ws) |>> JArray
sepBy
に jnumber
を渡していますね。
ここに自分自身が渡せればネストに対応できそうですが、F#ではそのようなことはできません。
let jarray = // jarrayの定義の中ではjarrayにアクセスできないのでコンパイルエラー sepBy (choice [jnumber; jstring; jarray]) (pchar ',' >>. ws) |> between (pchar '[' >>. ws) (pchar ']' >>. ws) |>> JArray
FParsecでは、このような再帰した文法を表現するために、 createParserForwardedToRef
という関数を用意してくれています。
この関数は、パーサーとそのパーサーへの参照(ref
型)のタプルを返します。
そして、パーサーへの参照に定義を再代入により設定することで、再帰した文法が表現できます。
// jarray は、 jarrayRef を見るようになっている let jarray, jarrayRef = createParserForwardedToRef () // jarrayRef は ref 型なので、再代入できる jarrayRef := // 再帰したい場合は、 !jarrayRef ではなく、 jarray を使う sepBy (choice [jnumber; jstring; jarray]) (pchar ',' >>. ws) |> between (pchar '[' >>. ws) (pchar ']' >>. ws) |>> JArray
これで、配列内に数値以外も格納できるようになりました。
JObject
に対応することを考え、 json
というパーサーを作っておきましょう。
let json, jsonRef = createParserForwardedToRef () let jarray = sepBy json (pchar ',' >>. ws) |> between (pchar '[' >>. ws) (pchar ']' >>. ws) |>> JArray jsonRef := choice [jnumber; jstring; jarray]
これで、 jobject
などを作った場合に choice
の中に入れるだけで配列の要素としてオブジェクトが使えるようになります。
"[[], 1, \"aaa\"]" |> parseBy json // => JArray [JArray []; JInteger 1; JString "aaa"]
左再帰
JSONの文法はよく考えられているため、何も考えずに実装しても問題ないようにできています。 しかし、それでは勉強になりませんので、JSONを勝手に拡張して問題を起こしてみましょう。
このような入力をパース出来るようにしたいと思います。
[ 10 - 4 + 2, 20 ]
型の定義はこうでしょうか。
type NumExpr = | NEInteger of int // 簡単のためにfloatは省略 | NEAdd of NumExpr * NumExpr | NESub of NumExpr * NumExpr type Json = | JNumExpr of NumExpr | JArray of Json list // 簡単のためstringも省略
先程のJSON(拡張)をパースした結果は、このようになればいいでしょう。
JArray [ JNumExpr (NEAdd (NESub (NEInteger 10, NEInteger 4), NEInteger 2)) JNumExpr (NEInteger 20) ]
これをパースするパーサーを何も考えずに実装すると、次のようになるでしょう。
let ws = spaces let neinteger = pint32 .>> ws |>> NEInteger let numExpr, numExprRef = createParserForwardedToRef () let neadd = (numExpr .>> pchar '+' .>> ws) .>>. neinteger |>> NEAdd let nesub = (numExpr .>> pchar '-' .>> ws) .>>. neinteger |>> NESub numExprRef := choice [attempt neadd; attempt nesub; neinteger] let jnumExpr = numExpr |>> JNumExpr let json, jsonRef = createParserForwardedToRef () let jarray = sepBy json (pchar ',' >>. ws) |> between (pchar '[' >>. ws) (pchar ']' >>. ws) |>> JArray jsonRef := choice [jnumExpr; jarray] let parseBy p str = match run (ws >>. p .>> eof) str with | Success (res, _, _) -> res | Failure (msg, _, _) -> failwithf "parse error: %s" msg
しかし、このパーサーで "[ 10 - 4 + 2, 20 ]"
をパースしようとすると StackOverflowException
が発生します。
問題は、この辺りにあります。
let neadd = (numExpr .>> pchar '+' .>> ws) .>>. neinteger |>> NEAdd let nesub = (numExpr .>> pchar '-' .>> ws) .>>. neinteger |>> NESub numExprRef := choice [attempt neadd; attempt nesub; neinteger]
まず、 numExpr
でパースしようとします。
choice
の先頭に neadd
があるので、 neadd
でパースしようとします。
neadd
の先頭に numExpr
があるので、 numExpr
でパースしようとします。
・・・戻ってきてしまいましたね。
このように、入力をなにも消費せずに再帰してしまうと、パースが先に進まずにスタックオーバーフローしてしまうのです。
このように、文法の先頭(左側)で再帰している文法を左再帰の文法といいます。
この問題を解決するために、ここでは再帰を繰り返しで表現する方法を使います。
// numExpr ::= neinteger (op neinteger)* let numExpr = neinteger .>>. (many (op .>>. neinteger)) // opの定義は後で
many
関数は、引数のパーサーの0回以上の繰り返しをパースするパーサーを返します。
このままでは、 numExpr
パーサーの結果の型は NumExpr * (opの結果の型 * NumExpr) list
になってしまいます。
これを NumExpr
にしないといけません。
複数の NumExpr
を一つの NumExpr
にする必要があるので、 List.fold
を使えばいいでしょう。
let numExpr = neinteger .>>. many (op .>>. neinteger) |>> (fun (i, xs) -> List.fold (fun crnt (op, next) -> // crnt, op, nextを使ってNEAdd, NESubを作る ...) i xs)
op
は例えばこのような定義が考えられます。
type Op = Add | Sub let op = (pchar '+' .>> ws >>% Add) <|> (pchar '-' .>> ws >>% Sub)
>>%
演算子は、左項のパーサーが成功した場合に右項の値を返すパーサーを返します。
"a" |> parseBy (pchar 'a' >>% [1; 2; 3]) // => [1; 2; 3]
先程の op
定義の場合、 List.fold
に渡す関数はこのようにすればいいでしょう。
let f crnt (op, next) = match op with | Add -> NEAdd (crnt, next) | Sub -> NESub (crnt, next)
これで、 "[ 10 - 4 + 2, 20 ]"
がパース出来るようになりました。
"[ 10 - 4 + 2, 20 ]" |> parseBy json // => JArray // [JNumExpr (NEAdd (NESub (NEInteger 10,NEInteger 4),NEInteger 2)); // JNumExpr (NEInteger 20)]
さて、これでもいいのですが、 NEAdd
に対応する Add
, NESub
に対応する Sub
を定義する必要があるのが少し面倒です。
そこで、 op
の結果の型を関数にして、 crnt
と next
を渡して NEAdd
や NESub
を返すようにしてみましょう。
let op = choice [ pchar '+' .>> ws >>% (fun a b -> NEAdd (a, b)) pchar '-' .>> ws >>% (fun a b -> NESub (a, b)) ] let numExpr = neinteger .>>. many (op .>>. neinteger) |>> (fun (i, xs) -> List.fold (fun crnt (f, next) -> f crnt next) i xs)
だいぶすっきりしました。
実は、これを一発でやってくれる chainl1
という関数がFParsecに用意されています。
let op = choice [ pchar '+' .>> ws >>% (fun a b -> NEAdd (a, b)) pchar '-' .>> ws >>% (fun a b -> NESub (a, b)) ] let numExpr = chainl1 neinteger op
便利ですね。
まとめ
最後の方はJSONに関係のない拡張をベースに説明しましたが、ここまでで数値、文字列、配列が実装できました。 残っている機能は次の通りです。
null
true
/false
JFloat
の指数表記対応\uXXXX
形式の文字対応- オブジェクト
オブジェクト以外は簡単に対応できるでしょう。 オブジェクトも、配列を参考にすればそれほど難しくないと思うので、ぜひ実装してみてください。
年末年始は、パーサーを書いて過ごしましょう!
*1:お使いの言語がジェネリクスや無名関数をサポートしていないような場合はこの限りではありません。
*2:JSONを使うだけの場合、オブジェクトのキーとして文字列しか使えないことなどが煩わしく感じた方も多いと思います。しかし、パーサーを作る側の視点に立つと、それなりの表現能力を少ない労力で手に入れられるようによく考えられた仕様であるということに気付けるようになります。コメントくらいは欲しいですが・・・
*3:p はパーサーを表すプレフィックスです。FParsecでは、標準関数などと名前がかぶるパーサーには p プレフィックスを付けて区別できるようになっています。
*4:F#では流れる方向を意識した演算子が標準でも用意されており、その文化にFParsecも合わせるような演算子の使い方をしています。
*5:>>. に入っている最適化が between には入っていません。 >>. の前段に2つのパーサがあるため、コードが複雑になる事を嫌ったのかもしれません。
*6:1回以上の繰り返しの場合は many1Chars 関数を使います。 sepBy1 や spaces1 と違い、 many のサフィックスとして1が付く点に注意してください。 many / many1 というパーサーもあり、これは list を返しますが、 manyChars / many1Chars はリストを連結して文字列を返します。
*7:厳密に言うと、引数のパーサーを実行する前に現在の状態(入力のどこまで読んだかという位置情報など)を保持しておき、引数のパーサーが失敗した場合にパーサーの状態を戻します。
F#に型クラスを入れる実装の話
この記事はF# Advent Calendar 2016の11日目のものです。ちょっと遅れてしまいました。。。
ICFP 2016(と併催されたML workshop?)で気になる内容があったので、ちょっとまとめてみました。
Classes for the Masses - ICFP 2016
ざっくり、F#に型クラスを導入してみたぜ、って内容です。
型クラスとは
JavaやC#での interface
みたいなものですが、interface
は侵入的なのに対して、型クラスは非侵入的という違いがあります。
侵入的というのは、型の定義にその interface
を実装しますよ、ということを書く必要があることを意味します。
// C# interface Eq<A> { bool Equal(A a, A b); } // intefaceは型に侵入する class SomeClass : Eq<SomeClass> { public bool Equal(A a, A b) { ... } }
それに対して型クラスは非侵入的であり、型の定義にその型クラスを実装することは書きません。
-- haskell class Eq a where (==) :: a -> a -> Bool -- 型クラスは型の定義に書かなくていい data SomeType = ... -- SomeTypeをEq型クラスのインスタンスにする(型定義と分かれている) instance Eq SomeType where x == y = ...
これの何が嬉しいかというと、ひとつは、型の定義をその型に対して可能な操作と分離できることです*1。
これによって、例えば標準ライブラリの型に対しても、後付けで型クラスのインスタンスにできるようになります。 抽象を後付けできるとでも表現すればいいでしょうか。
この型クラスをF#に導入してみた、というのが今回紹介する内容です。
F#への型クラスの実装方法
実際に動作するコードは下記のリポジトリで公開されています。
先に示した Eq
型クラスはこの実装を使うと、
// Eq型クラスの実装(interfaceとしてコンパイルされる) [<Trait>] type Eq<'a> = abstract equal: 'a -> 'a -> bool // SomeTypeをEq型クラスのインスタンスにする(structとしてコンパイルされる) // Haskellと違い、インスタンスの定義に名前(ここではEqSomeType)が必要 [<Witness>] type EqSomeType = interface Eq<SomeType> with member equal x y = ...
と書きます。
この Eq
型クラスを使うには、
let (==) a b = Eq.equal a b
のように、型クラス名.メンバー 引数リスト ...
のように書くようです。
型クラスは struct
にコンパイルされるため、デフォルト値を介して型クラスのメンバーにアクセスできます。
この関数は、下記のようにコンパイルされます。
// C#モドキ public static bool operator ==<A, EqA>(A a, A b) where EqA : struct, Main.Eq<A> => default(EqA).equal(a, b);
struct
を使うことで、追加の引数を不要にしています。
また、Eq
型クラスを要素に持つリストを Eq
型クラスのインスタンスにする(それ以外のリストは Eq
型クラスにしない)こともできます。
[<Witness>] type EqList<'a, 'EqA when 'EqA :> Eq<'a>> = interface Eq<'a list> with member equal a b = match a, b with | x::xs, y::ys -> Eq.equal a b && Eq.equal xs ys | [], [] -> true | _, _ -> false
互換性及び他の.NET言語との連携
ここまででみたように、この実装ではあくまで.NETの型でそのまま表現できる形になっています。 ランタイムに手を加える必要がないため、互換性を崩すことなく採用できるように実装されている、ということです。
また型クラスを使った関数は、型クラスに対応しない既存の.NET言語からは型パラメータを明示的に渡せば使えます(使いやすいとは言っていない)。
この方法の問題点
この方法はランタイムに手を加えないため、(例えば Monad
のような)高階型クラスがサポートできません。
ううむ、残念・・・
あ、それと、このリポジトリをcloneしてbuild.cmdを管理者権限で実行するとF#の環境がぶっ壊れた(VSでビルドできなくなった)ので、やるなら仮想環境で試してみることをお勧めします。
続・そろそろPower Assertについてひとこと言っておくか
3年前にこんな記事をあげました。
3行でまとめると、
- Power Assertはユニットテストのためにほしかったものではない
- 欲しいのは結果の差分
- 誰か作って!
というエントリでした。 そしたら id:pocketberserker が作ってくれました!
PowerAssertより強そうな名前でいい感じです。
Power Assertは時代遅れ、今はMuscle Assertだ!的な話かな?
— 裸のWPF/MVVMを書く男(マン) (@gab_km) 2016年6月1日
MuscleAssertの使い方
このライブラリは、PersimmonというF#用のテスティングフレームワークを拡張するライブラリとして作られています。 ただ、ざっくり概要をつかむだけであればどちらも知らなくても問題ありません。 このライブラリでできることはほぼ1つだけです。
open Persimmon open Persimmon.Syntax.UseTestNameByReflection open Persimmon.MuscleAssert let add x y = x + y let ``add 2 3が5を返す`` () = test { do! add 2 3 === 5 }
以上。簡単。 これを実行しても成功してしまって面白みがないので、わざと間違ってみましょう。
open Persimmon open Persimmon.Syntax.UseTestNameByReflection open Persimmon.MuscleAssert let add x y = x + x // ミス! let ``add 2 3が5を返す`` () = test { do! add 2 3 === 5 }
これをPersimmon.Consoleで実行すると、
Assertion Violated: add 2 3が5を返す 1. . left 4 right 5
こんなエラーが出てきました。 普通ですね。
では、例えばこんなJSONがあったとしましょう。
{"widget": { "debug": "on", "window": { "title": "Sample Konfabulator Widget", "name": "main_window", "width": 500, "height": 500 }, "image": { "src": "Images/Sun.png", "name": "sun1", "hOffset": 250, "vOffset": 250, "alignment": "center" }, "text": { "data": "Click Here", "size": 36, "style": "bold", "name": "text1", "hOffset": 250, "vOffset": 100, "alignment": "center", "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" } }}
これを読み込む関数を定義したとして、その関数をテストしたいですよね。
let expected = let ``JSONが読み込める`` () = test { do! read json === expected }
read
関数の実装にミスがあり、text
の vOffset
に hOffset
の値を使ってしまったとしましょう。
このテストを実行すると、下記のようなエラーメッセージが表示されます。
Assertion Violated: JSONが読み込める 1. .text.vOffset left 250 right 100
text
の vOffset
の値が左は 250
だったけど、右は 100
だった、ということが一目瞭然です。
MuscleAssert VS PowerAssert
MuscleAssertとPowerAssertの目的ははっきりと分かれています。 MuscleAssertが最初からテスティングフレームワークのアサーションを書くために特化しているのに対して、PowerAssertは(テストではなく)表明に使うことを前提にデザインされています。
表明手段
表明手段としてのPowerAssertはとても便利です。
言語内蔵の assert
は、条件式が false
の場合に何やらメッセージを出しますが、「どこで表明が false
と評価された」くらいの情報しか持っていません。
メッセージをカスタマイズすることはできますが、文字列で指定する必要があるため「どうなったか」を埋め込むのは大変です。
PowerAssertは、言語内蔵の assert
をそのままに表示されるメッセージをリッチにしてくれます。
表明として埋め込んだ式の「部分式の値」がメッセージとして表示されるため、「どの式の評価値が想定と違うのか」を調べるための情報をコーディングのコストを払わずに得られるようになるのです。
対してMuscleAssertはそもそも、Persimmon.MuscleAssertはPersimmon用のライブラリとして作られているため、Persimmonに依存しており単体で使えるものではありません。 表明に使えたとしても、MuscleAssertは式全体の評価結果の差分を出すため、ほしい情報である「どの式の評価値が想定と違うのか」を調べるための情報はそこに乗っていないでしょう。
表明手段としては、PowerAssertの圧勝です。
ユニットテスト用アサーション
しかし、MuscleAssertがやりたかったのは表明ではありません。 ユニットテストのアサーションとして使いたかったのです。
MuscleAssertが例えばJSONのようなネストした構造に対するテストに強そうだ、というのは先ほど紹介した例で分かると思います。 XMLやJSONやYAMLは当然として、そもそもクラス自体が何かを内部に持っているネスト構造をしているため、ネストした構造をそのまま比較してもわかりやすいメッセージが出力されるMuscleAssertは便利です。
対してPowerAssertはこの例には貧弱です。
let ``JSONが読み込める`` () = test { do! read json === expected }
このテストが失敗するとして、PowerAssertで表示されるのは
json
変数の中身read json
の結果expected
の中身read json === expected
がfalse
になったということ
ですかね。 どれもドバドバと大量の出力をするわりに、本当に欲しい「どこがどう違うのか?」という情報はそこから得るのは容易ではありません。 diffツールを使って外部でdiffとるとかしたことある人も多いんじゃないでしょうか?
そもそも、テストで actual
側に部分式が出てうれしいほど何かを書くことって多いのか?というのも疑問です。
このテストのように、多くのテストでは期待値との一点比較ができればいいのではないでしょうか?
ちなみに、MuscleAssertでは一度に複数の箇所の間違いを出してくれますので、小さいテストをまとめるのも容易です。
1. .image.hOffset left 500 right 250 .image.vOffset left 500 right 250 .text.vOffset left 250 right 100 .text.alignment left centre right center @@ -1,6 +1,6 @@ cent -re +er
MuscleAssertの弱点
MuscleAssertの弱点は、一点比較しかできないところです。 そのため、浮動小数点数を含むデータ構造を、浮動小数点数の一致範囲を指定して比較、ということは現状ではできません。 また、大小比較などもサポートしていません。
現状でこれらをテストしたい場合は、MuscleAssertを使わずにテストするしかありません。 今のところ、これで困ったことはありません(そういうテストが必要なドメインで仕事をしていない)。
まとめ
まとめも3行で。
- MuscleAssert便利
- テストのためのアサーションライブラリとしてはPowerAssertよりも便利
- 弱点はある。でも自分が困っていないから放置
みなさんも自分が使っている言語でMuscleAssertを実装してみてはいかがでしょう?便利ですよ。
F#でWPFやるときのTipsとか(その2)
F#でWPFやるときのTipsとか(その1)の続編です。
添付プロパティの作り方
F#で添付プロパティを作るには、添付プロパティの本体はプロパティではなくフィールドに保持する必要があるようです。
// ダメな例 type Sample private () = static member SomeValueProperty = DependencyProperty.RegisterAttached("SomeValue", typeof<string>, typeof<Sample>, FrameworkPropertyMetaData("", Sample.OnSomeValueChanged)) static member GetSomeValue(obj: DependencyObject) = obj.GetValue(Sample.SomeValueProperty) :?> string static member SetSomeValue(obj: DependencyObject, value: string) = obj.SetValue(Sample.SomeValueProperty, value) static member OnSomeValueChanged = PropertyChangedCallback(fun sender e -> // プロパティが変更されたときの処理 )
このコードはコンパイルは通りますが、この添付プロパティにXAML内でBindingしようとすると、
'Button' コレクション内で 'Binding' を使用することはできません。'Binding' は、DependencyObject の DependencyProperty でのみ設定できます。
というエラーになってしまいます。
プロパティではなくフィールドを使うとうまくいきます。
type Sample private () = // 一旦staticなフィールドに保持しておいて、 static let someValueProperty = DependencyProperty.RegisterAttached("SomeValue", typeof<string>, typeof<Sample>, FrameworkPropertyMetaData("", Sample.OnSomeValueChanged)) // プロパティの値として保持したフィールドを設定 static member SomeValueProperty = someValueProperty static member GetSomeValue(obj: DependencyObject) = obj.GetValue(Sample.SomeValueProperty) :?> string static member SetSomeValue(obj: DependencyObject, value: string) = obj.SetValue(Sample.SomeValueProperty, value) static member OnSomeValueChanged = PropertyChangedCallback(fun sender e -> // プロパティが変更されたときの処理 )
添付プロパティがF#で書けるため、WPFのかなりの部分がF#のみで完結できると思われます。 Full F#でWPFがかなり現実味を帯びてきました。 足りないのは各種ユーティリティなので、その辺の再実装が苦でない人であれば、十分選択肢に入ってくる環境はすでに整ったと言えるでしょう。
別Windowの開き方
今のところ、一番手軽に別Windowを開くには、XAML Type Providerを使うのがいいでしょう。
まずはXAMLを作る必要がありますが、F#のプロジェクトではXAMLのアイテムテンプレートがないため、「General」の「XMLファイル」を選んでファイルの拡張子をxamlに変更します。 注意点として、この方法で追加したファイルは「ビルドアクション」が「None」になっているので、「Resource」に変更しておく必要があります。
「F# Empty Windows App (WPF)」テンプレートでプロジェクトを作った場合、
type OtherView = XAML<"OtherWindow.xaml">
としてViewを表す型を作っておいて、何らかのコマンド内でこの型のオブジェクトを生成して Show
(もしくは ShowDialog
)を呼び出します。
member this.OnClick = this.Factory.CommandSync (fun () -> let view = OtherView() view.Root.Show() )
ちなみに、「F# Empty Windows App (WPF)」テンプレートで導入されるFsXaml.Wpfは古い(0.9.9)ため、パッケージを更新(現時点では2.1.0)するとビルドが通らなくなります。
ビルドを通すためには、Root
プロパティへのアクセスを消してください。
member this.OnClick = this.Factory.CommandSync (fun () -> let view = OtherView() view.Show() )
App.fsもコンパイルエラーになるので、そちらの Root
も削除しましょう。
[<STAThread>] [<EntryPoint>] let main argv = App().Run()
F#でWPFやるときのTipsとか(その1)
最近、Full F#でWPFしてるので、Tipsてきなものをまとめようと思います。 その2はTipsがたまればあるかもしれませんが、過度な期待はしないでください。
プロジェクトの作り方
基本的には、Pure F# WPF GUIアプリ開発に向けてに書いてある通りです。 細かい注意点があるので書いておきます。
.NET Frameworkの選択に関する注意点
.NET Frameworkを4.5.1や4.5.2などのような3桁のものを選ぼうとすると、プロジェクト作成に失敗します。 また、4.6を選んでも4.5として作られるので、その点にも注意しましょう。
初回以外での作成
言うまでもないことかもしれませんが一応。 一回でも「F# Empty Windows App (WPF)」を使ってプロジェクトを作った場合、それ以降は「オンライン」の方ではなく、「インストール済み」のテンプレートを選ぶことになります。
MainView.xaml.fs
「F# Empty Windows App (WPF)」というテンプレートでプロジェクトを作ると、下記の内容でMainView.xaml.fsというファイルが生成されます。
namespace ViewModels open System open System.Windows open FSharp.ViewModule open FSharp.ViewModule.Validation open FsXaml type MainView = XAML<"MainWindow.xaml", true> type MainViewModel() as self = inherit ViewModelBase()
不要な型
実際にこのテンプレートで作る際、MVVMで作る場合は MainView
は不要です。
こいつはC#でのコードビハインドで作る場合に使うものに相当するため、MVVMで行く場合は消してしまって大丈夫です。
当然、C#でのコードビハインドで作る場合は MainViewModel
の方が不要なので、消してしまいましょう。
ただし、生成されるコードは完全にMVVMでやること前提なコードになっているので、正直お勧めしません。
細かいことですが、MainViewModel
の定義の =
の後ろに1つ、その下の行に4つ、空白文字が紛れ込んでいるのも注意しましょう。
気になる人は消してしまうといいでしょう。
App.fs
MainView.xaml.fs同様にテンプレートによって作られるファイルです。
module main open System open FsXaml open System.Windows type App = XAML<"App.xaml"> [<STAThread>] [<EntryPoint>] let main argv = App().Root.Run()
モジュール名
なぜか小文字で main
というモジュールになっています。
細かい部分ですが、Main
とか App
とかに変えたほうがいいでしょう。
XAML Type Providerについて
テンプレートで作られる構成がMVVMスタイルで作ることを意識したものになっていますので、あまりXAML Type Providerの出番はないのですが、Type Providerの使い方の例として触れておきます。
コード生成の代替としてのType Provider
C#でのコードビハインドではdesigner.csファイルが自動生成されますが、F#はXAML Type Providerの力によりそういったファイルは生成されません。
その代わりに、type HogeView = XAML<"xamlファイル名", true>
のようにしてビュー用の型をXAMLから生成することになります。
テンプレートによって生成されたプロジェクトを弄って、ちょっと使って見ましょう。
不要なファイルを消す
XAML Type Providerを使う場合は次のファイルは不要です。消してしまいましょう。
MainWindow.xamlを書き換える
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:ViewModels;assembly=FsEmptyWindowsApp3" xmlns:fsxaml="http://github.com/fsprojects/FsXaml" Title="MVVM and XAML Type provider" Height="200" Width="400"> <Grid> <Label x:Name="Message"/> </Grid> </Window>
Message
という名前を持った Label
を追加しました。
App.fsを書き換える
App.fsを書き換え、先ほど追加したラベルにテキストを設定してみます。
module App open System open FsXaml open System.Windows type MainView = XAML<"MainWindow.xaml", true> [<STAThread>] [<EntryPoint>] let main argv = // XAML Type Providerによって生成された型のインスタンスを生成し、 let view = MainView() // Messageというプロパティにアクセスし、Contentに文字列を設定 view.Message.Content <- "Hello XAML Type Provider!" // Application.RunにXAMLのRoot要素であるWindowオブジェクトを渡して、 // アプリケーションを起動 let app = Application() app.Run(view.Root)
これでビルドして実行すると、「Hello XAML Type Provider!」と表示されたウィンドウが開きます。
XAML Type Providerの力によって、自動生成コードなしでビューにアクセスできました。
イベントの登録
これだけだと、全部XAMLに書けばいいじゃん、という話になってしまうので、ボタンを追加してボタンにイベントを登録してみましょう。
MainWindow.xamlの内容を変更します。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:ViewModels;assembly=FsEmptyWindowsApp3" xmlns:fsxaml="http://github.com/fsprojects/FsXaml" Title="MVVM and XAML Type provider" Height="200" Width="400"> <StackPanel> <Label x:Name="Message"/> <Button x:Name="Btn">Please Click!</Button> </StackPanel> </Window>
Grid
は面倒なので StackPanel
に変え、Label
の下に Btn
という名前でボタンを追加しました。
そして、app.Run(view.Root)
の前にイベントを追加するコードを書きます。
view.Btn.Click.Add(fun x -> view.Message.Content <- "Clicked!")
これをビルドして実行すると、先ほど作った画面にボタンが追加されたウィンドウが開きます。 そして、ボタンをクリックするとラベルの内容が変わります。
こんな感じで、画面の要素に名前を付けておけば、かなり簡単に画面が作れることが分かります。 まぁ、ちょっとした画面であればXAML Type Providerも選択肢に入れてもいいかな、という感じでしょうか。