はじめに
連載「C#プログラマのためのF#入門」最終回となる今回は、コンピュテーションエクスプレッション(計算式)について説明します。
コンピュテーションエクスプレッションは、F#で記述されたあるコードをどのように動作させるか独自の解釈で定義したり、他の言語に変換する機能です。F#言語構造のサブセットの意味解析を上書きするような機能と言えます。例えばwhile、return、yieldなどのF#キーワードの仕様を再定義できるのです。
関数型プログラミングのデータ制御、および副作用の管理に使用できる関数型プログラミングの機能であるモナドのF#版とも言えるでしょう。モナドと呼ばない理由は、響きが悪い(とっつきにくい)とか、副作用がF#の型システムによってトラックされない(モナドでは副作用は全てモナドにリフトされる)ようにできるところなど、小さな違いがあるからだそうです。コンピュテーションエクスプレッションはC#のLINQのquery構文とも似ています。
コンピュテーションエクスプレッションには、既にプロセスや処理が規定化されていたり自動化されて組み込まれている(ビルトイン)コンピュテーションエクスプレッションと、それとは別にユーザー定義で行う物があります。
この二つのコンピュテーションエクスプレッションの差が今の段階ではわかりにくいと思いますので、はじめにビルトインコンピュテーションエクスプレッションであるシーケンスエクスプレッションと非同期ワークフローについて説明し、その後に関係を明確にしたいと思います。
シーケンスエクスプレッション
まず、シーケンスエクスプレッションを理解する上で基本となるシーケンスについて説明します。
シーケンスとは、1つの型の複数要素からなる、コレクションです。シーケンスの個々の要素は必要に応じて計算(生成)されます(遅延評価)。そのため、すべての要素を使用するとは限らない場合、リストより効率的であることが多いです。
シーケンスはseq<'T>型で表され、ジェネリックなIEnumerable(T)を実装するすべての型は、シーケンスが期待されるすべての場所で使用できます。
seq{要素}
シーケンスを用いて、以下のようなデータをあらわすことができます。これは、1から1億までの整数を表していますが、遅延評価により実際に1億個の要素のデータ構造が作成されるのではなく、必要に応じてその数の要素が作成できるという可能性をもったシーケンスをあらわしています。
seq {1 .. 100000000}
seqであらわされるシーケンスは、シーケンスモジュールとして定義されているファンクションや集約演算子で使用可能です。例としては、Seq.map、Seq.filterなど、リスト用モジュールとして定義されているもの(list.map、 list.filterなど)と似たものも多々あります(詳細はマイクロソフトのページなどでご確認ください)。
これらのモジュールを使用してシーケンスを以下のように直接作成・操作できます。Seq.initは与えられた第一引数を最大項目数とし、第二引数のファンクションにて要素を作成し、新しいシーケンスを作成するメソッドです。
let testSeq = Seq.init 3 (fun i -> i * 2);; testSeq;;
▼
val testSeq : seq<int> > val it : seq<int> = seq [0; 2; 4]
上記のようなシーケンス用集約演算子を用いてシーケンスを操作する以外に、F#にはシーケンスエクスプレッションという構文を用いてシーケンスの値を作成したり操作する方法があります。その代表的なものが以下のforを用いたものです。与えられたパターンにシーケンスの値をひとつずつ適用させ、->(yieldと呼ぶ。do yieldでも置き換えられる)で、シーケンスの一部となるデータを生成します。
seq {for パターン in シーケンス -> エクスプレッション}
つまり、上記の構文はSeq.map(集約演算子)と同じ事を意味します。
以下の例はiに1から3の値を順にバインドし、i * iを実行して値を生成しコレクション(シーケンス)を返します。
Seq.map (fun i -> i*i) [1..3];; //集約演算子 seq { for i in 1 .. 3 do yield i * i };; //シーケンスエクスプレッション
▼
val it : seq<int> = seq [1; 4; 9] > val it : seq<int> = seq [1; 4; 9]
上記の基本構文に補助的に、以下のような句を使用することで、よりカスタマイズされたシーケンスを作成できます。
種類 | 使用方法 | 構文 |
反復 | for文を入れ子にできます | for パターン in シーケンス do エクスプレッション |
フィルター | if文による判断 | if 式 then エクスプレッション |
条件分岐 | if文による条件分岐 | if 式 then エクスプレッション else エクスプレッション |
letバインド | letによる通常バインド | let パターン = 式 in エクスプレッション |
最終生成 | 値の生成 | エクスプレッション、または、yield エクスプレッション |
シーケンスの生成 | 他のシーケンスエクスプレッションからのシーケンスをコレクションに追加 | エクスプレッション、または、yield! エクスプレッション |
以下は、これらの補助句を用いたシーケンスエクスプレッションの例です。
open System.IO let rec testFiles testdir = //※1 seq { for testfile in Directory.GetFiles(testdir) do //※2 yield (testfile) for testsubdir in Directory.GetDirectories testdir do //※3 yield! (testFiles testsubdir) };; testFiles @"C:\Users\test1";;
▼
val it : seq<string> = seq ["C:\Users\test1\111.jpg"; "C:\Users\test1\112.jpg"; "C:\Users\test1\test2\222.jpg"; "C:\Users\test1\test2\test3\333.jpg"]
yield!は一連の要素をコレクションに追加するのに使用されます。再帰シーケンスエクスプレッションにてよく使用されます。再帰シーケンスエクスプレッションとは、再帰関数同様、シーケンスエクスプレッションを通常の関数のように再帰的に使用するシーケンスエクスプレッションです。
上記リスト4では再帰シーケンスエクスプレッションtestFilesを利用して、与えられたディレクトリ配下全てのフォルダ内のファイル名をひとつのコレクションに追加しています。
再帰シーケンスエクスプレッションはrecキーワードを用いて定義します(※1)。エクスプレッション内、まず最初のfor文の部分のエクスプレッションは、引数として与えられたディレクトリにファイルがあればそれをシーケンスとしてコレクションに追加します(※2)。二つ目のfor文部分のエクスプレッションは、さらにフォルダがある場合、そのフォルダを新たな引数としてtestFilesを再帰的に呼び出します(※3)。このとき、yield!によって、戻されるシーケンスがコレクションに追加されます。