nameofについて

F# Advent Calendar 2020の5日目のエントリーです。

F#5.0で導入された nameof は便利なんですが、いくつか制限があります。

まず、 nameof はファーストクラス関数ではありません。 そのため、

  • let f = nameof はできない
  • x |> nameof はできない

のような制限があります。

ひとつめは出来たら面白いですが、もし実装しようとすると関数呼び出しのたびに実体が nameof かどうか見ないといけなくなりそうで、 おそらく実現されることはないでしょう。

ふたつめは例えばinline関数のみに限って緩和されるようなことはあるかもしれませんが、どうなんでしょうか。

他にも、昨日のエントリーで紹介した nameof パターンにも制限があります。

nameofnameof symbolnameof<'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パターン

RFC FS-1085

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関数の実装変更

RFC FS-1089

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つの方法があります。

  1. 手書き
  2. パーサージェネレーター
  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. パーサーを組み合わせて、パース結果を用意したデータ構造に変換する
  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] に変換できたらとりあえずの目標達成ということになります。

いきなり JNumberJArray の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文字以上の空白文字をパースするパーサーです。 これまで見てきた pfloatpchar と違い、パースした文字列を結果として返さない(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

sepBybetween.>> ではなく >>. を使っていますが、解析結果には含まれないため、どちらを使っても構いません。 しかし、 .>> よりも >>. の方が効率がいい場合があるため、どちらでもいい場合は >>. を使うようにするといいでしょう。

ちなみに、 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 で実現しています。 digit0 から 9 までの文字をパースするパーサーです。

"-2" |> parseBy (opt (pchar '-') .>>. digit) // => (Some '-', '2')
"1"  |> parseBy (opt (pchar '-') .>>. digit) // => (None, '1')

many1Chars2 関数は、1回以上の繰り返しをパースするパーサーを返します。 manyCharsmany1Chars に似ていますが、引数を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 を使わずに今回の問題を解決する方法もあります。 例えば、 jintegerjfloat を削除し、 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 の要素に JIntegerJFloat しか許しません。 型の定義としては、 JString でも JArray 自体でもいいようになっていますので、この対応をしましょう。

現状の jarray の定義を確認しておきましょう。

let jarray =
  sepBy jnumber (pchar ',' >>. ws)
  |> between (pchar '[' >>. ws) (pchar ']' >>. ws)
  |>> JArray

sepByjnumber を渡していますね。 ここに自分自身が渡せればネストに対応できそうですが、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 の結果の型を関数にして、 crntnext を渡して NEAddNESub を返すようにしてみましょう。

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#に型クラスを導入してみたぜ、って内容です。

型クラスとは

JavaC#での 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#への型クラスの実装方法

実際に動作するコードは下記のリポジトリで公開されています。

github.com

先に示した 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でビルドできなくなった)ので、やるなら仮想環境で試してみることをお勧めします。

*1:オブジェクト指向プログラミングのよくある説明の一つに「データと操作をひとまとまりにできる」というものがありますが、それとはある意味正反対の特徴ですね。まぁ、この「データと操作をひとまとまりにできる」という説明には言いたいことがあるんですが、それは別の機会にでも

続・そろそろPower Assertについてひとこと言っておくか

3年前にこんな記事をあげました。

bleis-tift.hatenablog.com

3行でまとめると、

  • Power Assertはユニットテストのためにほしかったものではない
  • 欲しいのは結果の差分
  • 誰か作って!

というエントリでした。 そしたら id:pocketberserker が作ってくれました!

github.com

PowerAssertより強そうな名前でいい感じです。

MuscleAssertの使い方

このライブラリは、PersimmonというF#用のテスティングフレームワークを拡張するライブラリとして作られています。 ただ、ざっくり概要をつかむだけであればどちらも知らなくても問題ありません。 このライブラリでできることはほぼ1つだけです。

open Persimmon
open Persimmon.Syntax.UseTestNameByReflection
open Persimmon.MuscleAssert

let add x y = x + y

let ``add 2 35を返す`` () = test {
  do! add 2 3 === 5
}

以上。簡単。 これを実行しても成功してしまって面白みがないので、わざと間違ってみましょう。

open Persimmon
open Persimmon.Syntax.UseTestNameByReflection
open Persimmon.MuscleAssert

let add x y = x + x // ミス!

let ``add 2 35を返す`` () = 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 関数の実装にミスがあり、textvOffsethOffset の値を使ってしまったとしましょう。 このテストを実行すると、下記のようなエラーメッセージが表示されます。

 Assertion Violated: JSONが読み込める
 1. .text.vOffset
      left  250
      right 100

textvOffset の値が左は 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のようなネストした構造に対するテストに強そうだ、というのは先ほど紹介した例で分かると思います。 XMLJSONYAMLは当然として、そもそもクラス自体が何かを内部に持っているネスト構造をしているため、ネストした構造をそのまま比較してもわかりやすいメッセージが出力されるMuscleAssertは便利です。

対してPowerAssertはこの例には貧弱です。

let ``JSONが読み込める`` () = test {
  do! read json === expected
}

このテストが失敗するとして、PowerAssertで表示されるのは

  • json 変数の中身
  • read json の結果
  • expected の中身
  • read json === expectedfalse になったということ

ですかね。 どれもドバドバと大量の出力をするわりに、本当に欲しい「どこがどう違うのか?」という情報はそこから得るのは容易ではありません。 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も選択肢に入れてもいいかな、という感じでしょうか。