画面はC#業務はF#

 F#一筋の人は、画面もF#で作ってしまうのでしょうが、F#には今のところフォームデザイナーが使えないため、画面フォームはC#で作り、ボタンを押した処理だけF#で作る、と言う選択をとることも少なくないのではないでしょうか。
 ここでは、そのために必要な情報を整理しておきます。特にF#では非同期処理をすることが多く、その処理の中から画面に処理状況を表示していく方法を示していきたいと思います。

プロジェクト作成

 どちらが先でもいいのですが、C#でWindowsフォームのプロジェクトを作成します。


 そして、F#のライブラリプロジェクトを、同じソリューションに作成します。「F# Library」と「ソリューションに追加」を選択してプロジェクト名を入れればOKです。


 こんな風に二つのプロジェクトが一つのソリューションになればOKです。さらに、C#のプロジェクトからF#のプロジェクトを参照します。


 C#のプロジェクトの参照設定を右クリックし、「参照の追加」を選択します。「プロジェクト」タブにF#のプロジェクトが出ているはずなので、それを選択して「OK」します。


 これでF#で作った関数をC#から呼び出す準備はできました。

C#の作業

 まず、フォームでボタンとリストボックスを配置します。デザインも名前も適当でいいです。


 で、ボタンをダブルクリックしてボタンのイベントハンドラを作ります。このへん分からない人は読んでないと思うのですが・・・
namespace CSFormApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {

        }
    }
}
 で、ここからですよ。まず、F#から呼ばれるためのデリゲートとその実態であるメッセージハンドラを定義します。
 private delegate void MessageDelegate(string str);
 private void MsgString(string str) {
     listBox1.Items.Add(str);
 }

 これを、Form1のどこかに書いてください。デリゲートというのは、関数を値として受け渡すための型定義だと思ってください。ここではstring->void型のデリゲートを定義しています。で、その下のMsgString関数が、それに対応する実際の関数です。呼ばれたら、引数でわたってきた文字列をリストボックスに追加します。
 そして、ボタンのメッセージハンドラに、F#関数への呼び出しを記述します。
private void button1_Click(object sender, EventArgs e)
{
  FSLib.SampleFunc(
      delegate(string msg)
      {
          var deleg = new MessageDelegate(MsgString);
          var ret = this.BeginInvoke(deleg, new object[] { msg });
      },
      10);
}
 F#のライブラリに、SampleFuncという関数が定義されている前提でとりあえず書いてしまいます。
 一つ目の引数がなかなかエグいですが、これは、F#の側でAsyncワークフローを使って立ち上げた複数のスレッドから、画面のコントロールに安全に更新をかけるため必要な記述です。関数を引数にdelegeteオブジェクトを作成して、それをフォームのBeginInvokeにメソッドに渡して代理実行してもらいます。BeginInvokeの二つ目の引数は、delegateになっているMsgStringへの引数です。そしてそれ全体をさらにdelegateで包んだものを引数に渡す必要があります。
 もしF#側の関数がマルチスレッド下でこのコールバックを呼ぶようなことがなければ、ここには単純に「new MessageDelegate(MsgString)」と書いてしまってかまいません。
 二つ目の引数は、SampleFuncのが作成するスレッドの数ですが、別に何でもかまいません。
 これだけできたら、F#のプロジェクトで実際に作業をする関数を書きます。

F#の作業

 F#の側は普通に関数を書けばいいのですが、モジュール名をFSLibに変えた後、C#から渡されたデリゲートを受け取るためにデリケート型を定義しておく必要があります。
module FSLib

type msgCallback = delegate of string ->unit
 C#ではdeleateの型がstring->voidでしたが、F#ではstring->unit型になります。
 そして、関数を定義します。
let SampleFunc (msgcb:msgCallback) n =
  seq {
    for i = 1 to n do
      yield async {
              System.Threading.Thread.Sleep 100
              msgcb.Invoke (sprintf "Job %d done." i)
            }
  } |> Async.Parallel |> Async.RunSynchronously
 ここではこの内容の詳細に触れることは本題とずれてしまうので避けますが、引数で(msgcb:msgCallback)を受け取り、それに対してInvokeメソッドを呼び出し、引数の文字列を与えていることと、引数nの数だけAsyncオブジェクトが作成され、それが並列実行されていることだけに注目してください。
 これで準備はOKです。実行しましょう。


 並列実行されているため、順序がバラバラになりますが、これは想定の範囲です。
 状況によっては、ビルド時にFSharp.Coreへの参照が必要だというエラーが出る場合があります。これが出たら、C#側のプロジェクトにFSharp.Coreへの参照を追加してください。

まとめ

 C#からF#のモジュールを呼び出す場合は、ここにあげたことさえクリアしてしまえばそれ以外のことで大きく詰まってしまうことは少ないと思います。僕はここまでやるのに結構苦労しましたが、一度身に付けてしまえばF#のAsyncワークフローの恩恵を色々なところで受けられることになるでしょう。

(文責:片山 功士  2012/01/13)


今日: -
昨日: -
トータル: -
最終更新:2012年02月03日 02:00