F#でアプリケーションを書こう!今回はいつもと少し趣向を変えて、F#でのアプリケーションプログラミングについて取り上げてみたいと思います。題材として取り上げるのは「2ちゃんねる」の専用ブラウザです。いってもその開発すべてを取り上げるとさすがに退屈になりそうなので、アプリケーションのキモとなる掲示板データのアクセス部分にフォーカスして解説してみます。「2ちゃんねる」にはさまざまな内容をとりあつかう掲示板があり、これらの掲示板はその内容にあわせて便宜上カテゴリー分けされています。掲示板は特定の話題ごとにスレッドという単位で、ユーザーからの書きこみをまとめています。「2ちゃんねる」にアクセスするためのAPIについて特にオフィシャルなドキュメントは存在しないのですが、ネット上の有志の方による資料がいくつか存在します。ここでは参考資料として「専ブラ開発への道」を紹介しておきますが、このブログの記事を理解する上ではこれらのドキュメントを参照する必要は特にありません。カテゴリーごとの掲示板情報の取得さきほど掲示板はその内容にあわせて「便宜上」カテゴリー分けされていると書いたのですが「便宜上」と書いたのには訳があります。いうのもカテゴリーと掲示板の論理的な関係を厳密に取得するAPIというのは特になく、掲示板の一覧を表示するHTML文書の形でしか用意されていないのです。よって、カテゴリーと関連付けて掲示板の一覧情報を入手するには、以下のような形式で記述されているHTML文書をパースすることになります。スレッドリスト掲示板の一覧から掲示板のURLを取得することができます。このURL配下のsubjects.txtというテキストファイルに掲示板のスレッド一覧の情報が収められています。形式は以下のような形式になっており、一行がひとつのスレッドに対応する形になっています。(x)nnnnnnnnnnは10桁の整数であり、スレッドが生成された日時を1970年1月1日00時00分00秒からの経過秒数で表現しています。xは1-1000の整数であり、スレッドに対する書き込み数を表します。書き込み一覧スレッドに対する書き込みの一覧もテキストファイルとしてアクセス可能です。ボードのURLおよびスレッドのIDに基づいて以下のようなURLを合成します。掲示板URL/dat/スレッドID.dat1掲示板URL/dat/スレッドID.datダウンロードしたファイルは以下のような形式になっており、一行がひとつの書き込みに対応する形になっています。JISでエンコードされているので注意してください。ID:ユーザーID<>本文<>スレッド名称ここでも項目の区切りが'<>’になっていることにさえ気を付ければ非常にパースしやすい形式ですが、落とし穴がいくつかあります。HH:mm:ss.ffという形式ですが掲示板/スレッドによっては.ffがありません同様にIDの部分がない掲示板/スレッドもありますスレッド名称は最初の一行にのみ存在します掲示板データの読み込みこれらのカテゴリー、掲示板、スレッドおよび書き込みを以下のようなF#のデータ型として表現することにします。}掲示板にAPIを通じてアクセスするということは、HTTPでダウンロードしたテキストファイルを、これらのデータ構造に変換するということになります。テキストをダウンロードするコードは.NETのライブラリを用いると簡単に実装できるので、ここではテキストファイルを解析して上記のデータ型に変換するところの処理について詳しくみてみましょう。スレッド(書き込みのリスト)の読み込みまずは掲示板の情報そのものともいえるスレッド(書き込みのリスト)の読み込みからみていきましょう。p.Value)先ほどしめした書き込みの形式は正規表現であらわせので、それにマッチした結果に基づいてMoPostの値を返せばよいということなります。Patternとして定義してあります。スレッドリストの読み込みスレッドの一覧もほぼ同様のパターンで実装できます。正規表現の複雑さという観点ではこちらの方が簡単なので特に問題ないでしょう。p.Value)カテゴリーと掲示板の読み込み以上で見たように、スレッドおよび書き込みの情報そのものは正規表現であらわせる形のものでしたので、繰り返し文字列に対してマッチングさせていけばよいだけでした。カテゴリーおよび掲示板の情報そのものも正規表現を使えば問題なく解析できるのですが、厄介なのがカテゴリーと掲示板の階層関係の解析です。おおざっぱにいうとカテゴリーおよび掲示板の一覧を表示するHTMLは以下のような構造になっています。...カテゴリー名および掲示板にマッチする行以外は無視すると、このHTMLファイルはカテゴリー名(CategoryTitle)および掲示板(Board)のリストとしてみなすことができます。このリストを(カテゴリー、掲示板のリスト)というTupleにうまく分割するととができればMoCategoryに変換できそうです。Patternを記述しておきましょう。Patternにマッチした結果を中間表現として一時的にTokenというUnionで表現することにしましょう。すれば件のHTML文書はTokenのリストとして表現できます。]これで、このリストに対してパターンマッチングを適用し、(カテゴリー名、掲示板のリスト)という組を取り出し、それをMoCategoryに変換することができるようになりました。tokens)あと「掲示板のようにみえる行」が最初にあるので、それを読みとばすという処理を追加すると完成です。List.toSeq以上、掲示板のカテゴリー、掲示板、スレッドおよび書き込みのデータをパースする関数が定義できました。以下のようにSystem.Net.WebRequestと組み合わせれば、一応簡単なクライアントモジュールの仕上がりとなります。parseRootMenu以上の関数ほかをモジュールとして定義しているソースはこちらから入手可能です。2014設定チャームを使うWidowsストアアプリにおいて、アプリケーションの設定は設定チャームを通じて行います。実はFeedroでも設定チャームを通じたアプリケーションの設定画面の実装は行われています。試しにFeedroを起動し、Windows+[I](アルファベットのアイ)を押して設定チャームを開いてみてください。【一般】というFeedroが提供するメニュー項目が追加されています。この【一般】を選択すると【フィードを保持する日数】というコンボボックスがひとつ表示されているだけの設定画面が表示されます。この設定項目により、RSSフィードから読み取ったブログ記事をローカルに保存する期間を選択できます。この設定画面を実現するための、モデル、ビューモデルおよびビューについてそれぞれ見ていきましょう。モデル第6章で触れたようにアプリケーションの設定情報を保存するには、Windows.Storage.ApplicationDataクラスのオブジェクトのLocalSettingsプロパティを用います。付けてオブジェクトを保存、読み出しする機能を提供します。System.Collections.Generics.Dictionaryクラスと似たようなものだと考えるとわかりやすいでしょう。Runtimeがサポートする標準型の一覧についてはこちらを参考にしてください。アプリケーションのコードからApplicationDataのLocalSettingsプロパティを直接操作しても実用上は問題ありませんが、ここではSettingsHelperというユーティリティクラスを定義しておきましょう。value;}}}このようなユーティリティークラスを介して設定情報を取り扱うことで、のちのち、設定情報を端末間で同期させたい、といった要件がでてきた時に対応しやすくなります。その上で、設定値そのものをモデルStoreクラスのプロパティDaysToKeepとしてアクセスできるようにしておきましょう。value);}}....}ビューモデルビューモデルは以下のふたつのプロパティを実装しています。コンボボックスで選択された日数SelectedDaysToKeepの実装は、Store.DaysToKeepの値を読み書きしているだけです。Store();}#endregion}}ビュービューに配置されているオブジェクトは、日数を選ぶためのComboBoxおよびそのラベルであるTextBlock(CaptionDaysToKeep)のみです。/></StackPanel></StackPanel></SettingsFlyout>ComboBoxのItemsSource(選択可能なオプションの一覧)をビューモデルのDaysToKeepOptionsに、SelectedValue(選択された値)をビューモデルのSelectedDaysToKeepにバインドしておきます。SelectedDaysToKeepへのバインドは双方向になっているため、コンボボックスで日数を選択すると、その値が自動的にビューモデルのSelectedDaysToKeepプロパティに反映され、最終的にSettingsHelperを通じてアプリケーションの設定情報として保存されます。設定画面ができたので、最後にこれを実際に設定チャームと接続してみます。設定チャームとして表示されるビューはWindows.UI.ApplicationSettings.SettingsPaneクラスのインスタンスです。このビューに表示されるメニュー項目【一般】を追加します。SettingsPaneがビューの初期化を行う際、CommandsReuqestedイベントを発生させるので、そのタイミングでメニュー項目を追加します。SettingsCommand("General",loader.GetString("CaptionGeneralSettings"),showSettingsPanel));}....}“General”というメニューの識別子、メニューラベルの文字列およびメニューが選択された場合の処理(showSettingsPanel)を登録しています。メニューが選択された場合の処理そのものは非常に簡単で、先ほど定義したビューのインスタンスを生成し、Showで表示しているだけです。第10章で述べたとおり、スタート画面に表示できるアプリのタイルには4種類のサイズがあり、小サイズのタイル以外はライブタイルとして用いることができました。おさらいになりますが、以下はライブタイルを更新するコードの例です。TileNotification(document));ここではTileWide310x150Text01というテンプレートを用いて、横長のライブタイルの情報を更新しています。ここで、このライブタイルのサイズを中および大のサイズに変更してみると確認できますが、中および大サイズのタイルには、アプリのアイコンが表示されるのみです。使用したテンプレートは、横長のタイル用のものなので、横長のタイルのみ表示情報が更新された形なっているのです。ここで違うテンプレート、例えばTileSquare150x150Text01(中サイズ)やTlieSquare310x310Text01(大サイズ)を用いると、それぞれのサイズのタイルに表示されている情報を更新できます。それ以外のサイズのタイルはやはり変更されません。すべてのサイズのタイルに表示されている情報を同時に変更するにはどうすればよいのでしょうか。ここで、テンプレートの仕組みについてもう少しく詳しく見てみましょう。書籍でも触れましたが、テンプレートの実体はXmlDocument形式のオブジェクト、すなわちXMLです。TileWide310x150Text01のテンプレートを等価なXMLとして表現すると以下のようなものになります。id="1">こんにちは</text></binding></visual></tile>bindingタグおよぼその子要素に注目していください。bindingの子要素であるtextタグが実際の更新情報を表しており、bindingタグのtemplate属性がどのテンプレート(レイアウト)を用いるか指定しています。ここでピンと来た方もいるかもしれませんが、bindingは複数指定することができるのです。id="1">こんにちは</text></binding></visual></tile>この例では以下の3種類のテンプレートを用い、中、大、横長サイズのそれぞれのライブタイルに対する更新情報を指定しています。TileSquare150x150Text01TlieSquare310x310Text01TileWide310x150Text01このXMLを用いてTileUpdateManagerのUpdateを呼び出すと、ワイド以外の2種類のサイズのライブタイルにおいても表示される情報が更新されるようになります。コーディング上、少し面倒なのがこのXMLを表すXmlDocumentのオブジェクトをどのように準備するのか、という点です。Model)操作のメソッドを利用して、XmlDocumentのオブジェクトをいちから生成するのが正攻法になります。これは実際にやってみるとわかりますが、大変煩雑かつ面倒なコーディングになります。そこでここでは、System.Xml.Linqに用意されているXDocumentを用いる方法を紹介しておきます。TileNotification(document));XDocumentもXmlDocumentと同じく、XMLをオブジェクトとして操作するためのものです。XMLでの利用を想定して新たにデザインされており、XmlDocumentと比較して使い勝手がかなり改善されています。ここでは、この使いやすいXDocumentを使用してXMLのオブジェクトを構築し、それをXmlDocumentに変換するというアプローチを用いました。メモリ消費およびパフォーマンスという観点からは必ずしもベストなやり方ではないのですが、ここでは、コードの理解しやすさという観点からこの方法を紹介しています。この方法を用いたとしても以下のような条件下であれば特に問題にならないかもしれません。ライブタイルの更新頻度がそれほど頻繁でない構築するXMLのサイズがそれほど極端に大きくはない実際に使用される場合は十分にテストしてみてください。サンプルコードはgithubから入手可能です。使用が禁止されています。「ロックを取得する」とよく言いますが、これは「実行スレッドがロックを取得する」という意味です。処理の間では、「ロックを取得する」ことでは排他制御を行えないのです。使用は、そもそも期待通りの排他制御が行われないため禁止されているともいえます。オブジェクトを直接使用したとしても、排他処理はうまく意図したとおりに機能しません。スレッドが「取得する」ものだからです。これでは排他制御ができないじゃないか、という話になってしまいます。アプリのアーキテクチャを設計する上で、ロックを使わないように努力するにしても、どこかで最低限の排他制御は必ず必要になりえます。スレッドと紐づかないで排他制御をする仕組みはないものでしょうか。そこで登場するのが「セマフォ」というものです。セマフォという名前は、黎明期の鉄道における交通整理に用いられた手旗信号に由来しています。この手旗信号がどのように使われたか、鉄道マニアをのぞけばピンと来る方は少ないと思われるので、このあたりの由来はあまり気にしないほうがよいでしょう。ここでは、加減算のできる単なる「カウンター」がセマフォだと思ってください。このカウンターには以下のような性質があります。カウンターの最大値を指定できるカウンターの加算および減算を実行できるのは、ある一時点でひとつのスレッドに限られるカウンターの値が最大値の時に加算をしようとすると、カウンターの値が減算されるまで待機させられるたとえば、このカウンターの最大値が3に設定されていると仮定してください。このカウンターを4つのスレッドから同時に加算しようとした場合、何がおこるでしょうか。カウンターの加算を同時に実行できるのは、ひとつのスレッドのみです。4つのスレッドは、あるランダムな順番で順にカウンターの加算を実行していきます。最初の3つのスレッドが加算を実行した時点で、カウンターは最大値の3に到達するので、4番目のスレッドが加算をしようとすると、その時点で待機することになります。なにがしかの事象によりカウンターが減算されてはじめて、4番目のスレッドは加算を実行し、処理を継続することができるようになります。ここで、カウンターの最大値を1に設定したと考えてください。ある処理Aの前にカウンターを加算し、処理後にカウンターを減算するようにしておけば、この処理Aが同時に複数実行されることはなくなります。排他制御を実現できるわけです。実際のコードでどのように使えるのか確認しておきましょう。NameList);}Semaphore.Release();}}System.Threading.SemaphoreSlimクラスのオブジェクトがセマフォであり、以下のように用います。コンストラクターの引数でカウンターの初期値および最大値を指定カウンターの加算はWaitもしくはWaitAsyncで行うカウンターの減算はReleaseで行う上記のコードではコレクションに要素を追加すると同時にファイルに書き出していますが、この前後でセマフォを用いて排他制御を行っています。ここで、ファイルの書き出しが非同期処理である点に注意してください。セマフォのカウンターの加算を非同期に行っている点も重要です。サンプルコードはgithubから入手可能です。クラスという名前が示すように、このクラスはSemaphoreクラスの「軽量版」です。クラスを用いるとよいでしょう。スレッド間でなくプロセス間でセマフォを利用したい場合は名前付きセマフォを利用する必要があります。このような場合は、SemaphoreSlimでなくSemaphoreクラスを用いる必要がでてきます。プロセス間でのロックが必要なだけであれば、セマフォでなく名前付きミューテックスを利用すればよいでしょう。参考資料より詳しく調べてみたい方は以下の資料などを参考にしてみてください。AsyncLock日本語資料ということであれば、山本康彦(@biac)さんの資料付きデモアプリがわかりやすいです。第4章で取り上げたように、データバインディングを用いることで、ビューモデルのプロパティの値を、ビュー上のコントロールのプロパティに簡単に反映させることができます。例として、書籍のタイトルと価格を保持するBookクラスを考えてみます。value;NotifyPropertyChanged();}}}}これを以下のようなビューを用いて表示します。/></StackPanel></StackPanel></Grid></Page>ビューモデルの初期化は以下のようにMainPageのコンストラクタで行っておきましょう。2800};}}}アプリをデバッグモードで起動すると、書籍情報が以下のように表示されます。ここまでは第4章でみたとおりです。このビューをユーザーインターフェイスとして用い、ビューモデルの値を編集することを考えてみます。ビューモデルのそれぞれのプロパティがTextBoxのTexstプロパティにバインドされているのだから、テキストを編集すればTextBoxのプロパティが変更されて、それが自動的にビューモデルのプロパティに反映されるのでは?と思った方がいるかもしれません。データバインディングがそのような挙動を示すのであれば大変うれしいのですが、そのような振る舞いをするのでしょうか?実験で確かめてみましょう。ビューモデルのTitleプロパティのSetメソッドの先頭の箇所に、デバッガーでブレークポイントを設定します。デバッグ起動したのち、TextBoxに表示されている書籍の題名を編集し、TABキーを押して変更を確定させてください。ここでもし、TexstBoxのTextプロパティの変更がビューモデルに書き戻されているのであれば、ビューモデルのプロパティのSetメソッドが呼ばれるので、先ほど設定したブレークポイントでアプリケーションの実行が一時中断するはずです。中断しましたか?しませんでしたね。特別に指定しなければ、データバインディングは一方通行で作用します。このため、期待したいような振る舞いにはならないのです。ところで、「特別に指定しなければ」とあるように、実はデータバインディングを双方向で作用するように、指定することもできます。/>上記のようにMode=TwoWayの指定を追加した上で、もう一度、書籍の題名を変更してみてください。今度はTitleプロパティのSetメソッドに設定したブレークポイントで実行が中断されるはずです。TwoWayモードを指定した双方向バインディングを利用する上で注意すべきことが一点あります。書籍の価格を表示しているTextBoxについてもTwoWayを指定し、編集した金額がビューモデルのプロパティPriceに反映されるか同様に実験してみてください。デバッガーで設定したブレークポイントがヒットしていない(=変更が反映されていない)模様です。何が起こっているのでしょうか。StudioのOutputウィンドウに目を向けてください。以下のようなエラーメッセージに気がつくでしょう。'String').このエラーは、ビューモデルとビューのプロパティの型が異なるために発生しています。ここまで明示的には書いていませんでしたが、バインド元(ソース)のプロパティとバインド先(ターゲット)のプロパティの型が異なる場合、基本的になんらかの形でその変換方法を指定する必要があります。唯一の例外はターゲットの型が文字列(string)型の場合です。この場合、ソースのオブジェクトのToStringメソッドが呼び出され、文字列に変換されます。双方向バインディングを用いる場合、ターゲットからソースへの書き戻しが発生しますが、この時にも同様の原則があてはまります。上記の例ではPriceプロパティがdecimal型のため、文字列のTextプロパティの値をどのように書き戻せばよいのかを指定しない限り、エラーとなってしまいます。このような場合、双方向の変換を明示的に行うコンバーターを実装し、それをバインディング時に指定する必要があります。ここでは以下のようなコンバーターを実装してみましょう。decimal.Parse(value.ToString());}}}このコンバーターを静的リソースとして定義し、バインディング時に使用します。/>;....</Page>この状態で再度実験してみてください。今度はPriceプロパティへの書き戻しが行われていることを確認できることかと思います。サンプルコードはgithubから入手可能です。8プログラミング本格入門」の記述に関する訂正、注釈、記載できなかった技術情報などはこちらの一覧に記載しています。ページを表示させることができます。ふたつだけとあって、非常に簡単に使えるのですが、使い勝手が微妙な点もあります。ページを表示させたいというような場合を考えてください。メソッドをどこかで呼び出す必要がでてきます。ビューで表示されているページを完全に同期させようとするといろいろと面倒なことをする必要がでてきます。セットするためのプロパティをもっていれば話は簡単です。ビューモデルのプロパティをバインドするだけでよいのです。そんなプロパティさえあれさえすれば。ラップしてそのようなプロパティを提供するユーザーコントロールを書いてみました。