MSDN マガジン > Home > 発行物 > 2008 > Launch >  F# 入門: .NET Framework で関数型プログラミング手法を使用する
F# 入門
.NET Framework で関数型プログラミング手法を使用する
Ted Neward

この記事の内容 : :
  • F# のインストール
  • F# 言語の基本
  • .NET の相互運用性
  • 非同期 F#
この記事は次のテクノロジを使用しています:
..NET Framework、F#
最近 Microsoft® .NET Framework ファミリの一員となった F# を使用すると、タイプ セーフ、パフォーマンス、およびスクリプト言語同様の機能を、すべて .NET 環境内で実現できます。この関数型言語 F# は、CLR 向けの構文互換性を持つ OCaml 方言として Microsoft Research の Don Syme により考案されましたが、ラボからワークショップへと急速に移行しつつあります。
.NET ジェネリックや LINQ などのテクノロジを通して、C# や Visual Basic® などの主流言語に関数型プログラミングの概念が浸透するにつれて、.NET コミュニティにおける F# の認知度が高まっています。このため、マイクロソフトは 2007 年 11 月、F# を .NET プログラミング言語としてサポートすることを発表しました。
長年にわたって、関数型言語 (ML、Haskell など) の領域は、プロフェッショナルな開発よりもアカデミックな研究に適していると考えられてきました。これらが退屈な言語だということではありません。実際、ジェネリック、LINQ、PLINQ、フューチャなど、.NET の重要な拡張機能には、それまで無縁であった関数型プログラミングの概念が言語に応用されたケースがいくつもあるのです。むしろ、これらの言語が注目されてこなかった原因は、Windows® のプログラムを記述する開発者にほとんどかかわりがないプラットフォームを対象としていたこと、基になるプラットフォームとの統合がスムーズでなかったこと、リレーショナル データベース アクセス、XML 解析、アウトプロセス通信メカニズムなどの主要機能をサポートしていなかったことにあります。
しかし、CLR の "多言語、単一プラットフォーム" アプローチにより、必然的にこうした言語も Windows 開発の領域に進出するようになりました。当然、職業プログラマにもその存在が認知され始めました。F# もこうした言語の 1 つです。この記事では、基になるいくつかの概念と F# の利点について紹介します。その後、F# を使い始めることができるよう、インストールと単純なプログラムの記述の手順を示します。

F# を使用する理由
.NET Framework 用関数型言語の学習が強力なソフトウェア作成に役立つことをはっきり認識しているのは、ごく一部の .NET プログラマでしょう。その他の人にとって、F# を学ぶ動機はまったくの謎です。開発者にとって F# はどのように役立つのでしょうか。
過去 3 年間にマルチコア CPU が普及するにつれて、安全な並列プログラムの記述が大きな関心事となっています。関数型言語では、開発者による同時実行のサポートを支援するため、不変データ構造が推奨されます。このような構造は、スレッド セーフやアトミック アクセスについて心配することなく、スレッド間やマシン間で渡すことができます。また、関数型言語を使用すると、F# 非同期ワークフローなど、同時実行に対応したライブラリを記述しやすくなります。これらについては後で説明します。
オブジェクト指向開発に慣れきったプログラマにはそう見えないでしょうが、ある種のアプリケーションでは、多くの場合、関数型プログラムの方が記述も管理も簡単です。たとえば、XML ドキュメントを別形式のデータに変換するプログラムを記述するとします。XML ドキュメントを解析し、さまざまな if ステートメントを適用してドキュメント内の各ポイントで実行するアクションを特定する C# プログラムを記述することも可能ですが、変換を XSLT (eXtensible Stylesheet Language Transformation) プログラムとして記述するアプローチの方が優れています。当然ながら XSLT の内部には、SQL 同様、関数型の傾向が大きく見られます。
F# では、null 値の使用が強力に抑制され、不変データ構造の使用が推奨されます。これらにより、必要とされる特別なケースのコードの量が少なくなり、プログラミングのバグ発生が抑えられます。
また、プログラムを F# で記述すると、多くの場合より簡潔になります。実際、2 つの意味で "タイプ" が減少します。キーボード操作が少なくなり、変数や引数の型、戻り値の型をコンパイラに指定する必要がある場所も少なくなります。このため、管理対象のコードが大幅に減少する可能性があります。
F# のパフォーマンス プロファイルは C# と似ています。しかし、同様に簡潔な言語 (特に、動的言語とスクリプト言語) よりははるかにパフォーマンス プロファイルが優れています。また、多くの動的言語と同様、F# には、プログラム フラグメントを記述して対話的に実行することによりデータを検証できるツールが付属しています。

F# をインストールする
research.microsoft.com/fsharp/fsharp.aspx から無料でダウンロードできる F# で、すべてのコマンド ライン ツールに加えて、Visual Studio® 拡張パッケージもインストールされます。このパッケージは、構文の色強調表示、プロジェクトとファイルのテンプレート (スターター ガイドとして F# コードの詳細例が含まれています)、および IntelliSense® サポートを提供します。また、Visual Studio 内部で実行可能な F# 対話型シェルを使用すると、開発者は、ソース ファイル ウィンドウから式を取得して対話シェル ウィンドウに貼り付け、強化されたイミディエイト ウィンドウのような領域でコード スニペットの直接の結果を確認できます。
この記事の執筆時点において、F# は Visual Studio 内の外部ツールとして実行されています。つまり、C# や Visual Basic ほどにはシームレスでありません。また、F# には ASP.NET ページ デザイナのサポートなどもありません。(だからと言って、F# を ASP.NET で使用できないと考えるのは間違っています。単に、Visual Studio で C# や Visual Basic に対して提供されているような組み込みのドラッグ アンド ドロップ開発操作が、F# に対しては提供されていないだけです。)
こうした状況にもかかわらず、.NET 準拠の他の言語を使用できる場面には、F# の現在のリリースも使用できます。以下に、いくつかの例を示します。

"Hello, F#"
どのような言語の紹介も、おなじみの "Hello, World" プログラムから始まります。F# も例外ではありません。
printf "Hello, world!"
この拍子抜けするほど短いサンプルから、F# が明示的なエントリ ポイントを必要としない種類の言語であることがわかります (C#、Visual Basic、C++/CLI にはエントリ ポイントが必要)。この言語では、プログラムの最初の行がエントリ ポイントと見なされ、そこから実行されます。
これを実行するため、新進の F# 開発者には 2 つの選択肢があります。コンパイルするか、インタープリタ処理するかです。F# インタープリタ (fsi.exe) 内でこれを実行するのは簡単です。図 1 に示すように、単にコマンド ラインから fsi.exe を起動し、表示されたプロンプトに上記の行を入力します。
図 1 F# インタープリタ内からの 'Hello, World' の実行 (画像を拡大するには、ここをクリックします)
シェル内では、ステートメントの末尾にセミコロンを 2 つ付ける必要があります。これは対話型モードの特性であり、F# プログラムのコンパイル時には必要ありません。
このサンプルを標準の .NET 実行可能ファイルとして実行するには、通常どおり Visual Studio を起動し、新しい F# プロジェクト ([その他のプロジェクトの種類] にあります) を作成します。作成したばかりの F# プロジェクトは、file1.fs という 1 つの F# ソース ファイルから成ります。このファイルを開くと、サンプルの F# コードが多数含まれています。F# の構文がどのようなものかを知るため、内容をざっと確認してください。それが済んだら、ファイル全体を上記の "Hello, world!" コードに置き換えます。アプリケーションを実行すると、当然のことですが、コンソール アプリケーション ウィンドウに "Hello, world!" と表示されます。
コマンド ラインからは、F# インストール ディレクトリの \bin サブディレクトリにある fsc.exe ツールを使用して、コードをコンパイルできます。fsc.exe は、ほとんどのコマンド ライン コンパイラと同様に機能し、コマンド ラインのソースを取得して、実行可能ファイルを結果として生成します。ほとんどのコマンド ライン スイッチは文書化されていますが、csc.exe コンパイラや cl.exe コンパイラを使用した経験があれば、多くのスイッチを既に知っているはずです。ただし、F# が現在立ち遅れている領域の 1 つに MSBuild があります。現在のインストール (執筆時点で 1.9.3.7) では、MSBuild によるコンパイルが直接サポートされていません。
"Hello, world!" をもう少しグラフィカルにする場合、F# では、基になる CLR プラットフォーム (Windows フォーム ライブラリを含む) との完全な再現性と相互運用性の実現も簡単です。次のコードを試してください。
System.Windows.Forms.MessageBox.Show "Hello World"
F# ライブラリに加えて .NET Framework クラス ライブラリを利用できる点は、OCaml や Haskell などの関数型言語を既に使用している理数系コミュニティと、世界規模の .NET 開発者コミュニティの両方にとって、F# 言語を魅力的なものとしています。

let 式
お決まりの "Hello, world!" ほど簡単ではない F# コードを見てみましょう。次のコードについて考えます。
let results = [ for i in 0 .. 100 -> (i, i*i) ]
printfn "results = %A" results
この F# 構文で興味深い要素の 1 つが let 式です。これは、この言語の中で最も重要な式です。形式的に言うと、let は識別子に値を割り当てます。Visual Basic や C# の開発者はこれを "変数を定義する" と解釈しがちです。しかし、その考え方は間違っています。実際のところ、F# の識別子には 2 つの原則があります。まず、いったん定義された識別子は変化することがありません (F# のこの特性によって可変状態が抑制されるため、プログラマは同時実行に適したプログラムを作成しやすくなります)。第 2 に、識別子は、C# や Visual Basic の場合と同様のプリミティブ型やオブジェクト型だけでなく、LINQ の場合と似た関数型にすることもできます。
また、識別子の持つ型がどこにも明示的に定義されていないことに注目してください。たとえば、results 識別子はどこにも定義されておらず、続く式の右辺から推定されます。これは型の推定と呼ばれ、コンパイラがコードを解析し決定した戻り値を自動的に使用する機能を表します (var キーワードを使用した C# の新しい推定型の式に似ています)。
let 式で扱うのは必ずしもデータだけではありません。F# の一流の概念である関数を定義するためにも使用できます。たとえば次のコードでは、2 つのパラメータ a と b を受け取る add 関数を定義しています。
let add a b =
    a + b
この実装の動作はだいたいご想像のとおりです。a と b を加算し、その結果を呼び出し元に対し暗黙的に返します。つまり F# においては、厳密に言うとすべての関数が値を返します。ただし、返される値が 1 つの値ではなく、unit (ユニット) という特別な名前を持つ場合もあります。この点は、特に .NET Framework クラス ライブラリと交差する場合、F# コードに興味深い影響を及ぼしますが、今のところ C# と Visual Basic の開発者は、unit を void と同じようなものと考えておいてください。
関数に渡されたパラメータを無視する必要があるケースもあります。その場合 F# では、単に、パラメータ自体のプレースホルダとしてアンダースコアを使用します。
let return10 _ =
    add 5 5

// 12 is effectively ignored, and ten is set to the resulting
// value of add 5 5
let ten = return10 12

printf "ten = %d\n" ten
多くの関数型言語と同様、F# でもカリー化が可能です。これにより、関数の適用を部分的にのみ定義し、カリー化を呼び出して残りのパラメータの指定を行うことができます。
let add5 a =
    add a 5
これは、異なるパラメータ セットを受け取って別のメソッドを呼び出す、次のようなオーバーロード メソッドを作成することと似ています。
public class Adders {
    public static int add(int a, int b) { return a + b; }
    public static int add5(int a) { return add(a, 5); }
}
ただし、わずかな違いがあります。F# バージョンでは、型が明示的に定義されていません。この場合、コンパイラが型の推定を実行して、add5 のパラメータの型に整数リテラル 5 との加算を行うための互換性があるかどうかを判断し、そのままコンパイルするか、エラーのフラグを付けます。実際、F# 言語では多くの場合、暗黙的に型がパラメータ化されます (つまり、ジェネリックを使用します)。
Visual Studio で上記の ten の定義にマウス ポインタを合わせると、型が次のように宣言されていることがわかります。
val ten : ('a -> int)
F# においてこれは、ten が値である、つまり任意の型のパラメータを 1 つ受け取って int 型の結果を生成する関数であることを意味します。tick 構文は C# における <T> 構文とほぼ同等です。したがって、ten を最も近い C# 関数で表すとすれば、本当は型を無視したいが C# の規則のために無視できない、型がパラメータ化されたメソッドに対するデリゲート インスタンスとなります。
delegate int Transformer<T>(T ignored);

public class App
{
  public static int return10(object ignored) { return 5 + 5; }

  static void Main()
  {
    Transformer<object> ten = return10;
    System.Console.WriteLine("ten = {0}", return10(0));
  }
}

for キーワード
では、最初の例の for キーワードを見てみましょう。
#light

let results = [ for i in 0 .. 100 -> (i, i*i) ]
printfn "results = %A" results
コードの冒頭にある #light 構文に注目してください。OCaml プログラマ以外の F# 学習者に譲歩して、OCaml 言語の構文要件が一部緩和され、コード ブロックを明確にするため空白が使用されています。これは必須ではないものの、C# や Visual Basic から移行する平均的開発者にとって構文の解析が容易になるため、F# の例や投稿されるコードでよく使われ、F# プログラミングにおける事実上の標準となっています (実際、F# の将来のバージョンでは、#light が既定の構文とされる可能性があります)。
この一見無害な for ループが、実はなかなか複雑です。正式な言い方では生成リストですが、要するに、結果としてリストが作成されるコード ブロックなのです。
リストは、関数型言語によく見られるプリミティブ構造体であり、多くの点で配列に似ています。ただし、リストは位置ベースのアクセス (C# における従来の a[i] 構文など) に対応していません。リストは関数型プログラミングのさまざまな場所で使用され、ほとんどの場合、.NET Framework の List<T> の機能が拡張された F# バージョンと見なすことができます。
リストは常に何らかの型を持ち、この例の識別子 results は組 (tuple) のリストです。具体的には、F# によって (int * int) 型として識別される tuple 型です。組のリストという概念は、SQL の SELECT ステートメントから返される列のペアに相当すると考えればわかりやすいでしょう。したがって、この例で実質的に作成されるのは、長さが 100 アイテムの、整数ペアのリストです。
一般に、関数型言語では、コード自体を使用できる場所ならばどこでも関数の定義を使用できます。そこで、前の例を拡張する場合は、次のように記述できます。
let compute2 x = (x, x*x)
let compute3 x = (x, x*x, x*x*x)
let results2 = [ for i in 0 .. 100 -> compute2 i ]
let results3 = [ for i in 0 .. 100 -> compute3 i ]
リスト (または配列、あるいはその他の反復処理可能な構造体) のループ処理は、関数型言語においてよく使用されるタスクであるため、基本的なメソッド呼び出し List.iter として一般化されています。これは単に、リストの各要素で関数を呼び出します。他にも類似のライブラリ関数があり、便利な機能を提供しています。たとえば List.map は、引数として受け取った関数をリストの各要素に適用し、プロセス内で新しいリストを返します。

パイプライン
さて、F# の構成要素をもう 1 つ見てみましょう。パイプライン演算子は、関数の結果を受け取って、コマンド シェル (Windows PowerShell® など) のパイプと同様に、後続の関数への入力として使用します。図 2 に示す F# のコードについて考えます。このコードでは System.Net 名前空間を使用して、HTTP サーバーへの接続、対応する HTML の読み取り、および結果の解析を行います。
/// Get the contents of the URL via a web request 
let http(url: string) = 
  let req = System.Net.WebRequest.Create(url) 
  let resp = req.GetResponse() 
  let stream = resp.GetResponseStream() 
  let reader = new System.IO.StreamReader(stream) 
  let html = reader.ReadToEnd() 
  resp.Close() 
  html
let getWords s = String.split [ ' '; '\n'; '\t'; '<'; '>'; '=' ] s 
let getStats site = 
  let url = "http://" + site 
  let html = http url 
  let words = html |> getWords 
  let hrefs = html |> getWords |> List.filter (fun s -> s = "href") 
  (site,html.Length, words.Length, hrefs.Length)

getStats の定義に words 識別子があります。これは URL から返された html 値を受け取り、その値に対して getWords 関数を適用します。この定義を次のように記述することもできます。
let words = getWords html
この 2 つは同じです。しかし、hrefs 識別子は任意の数のアプリケーションをつなぎ合わせることができる点で、パイプライン演算子の力を発揮します。ここでは、生成された words のリストを受け取り、List.filter 関数にパイプ処理しています。List.filter 関数が受け取る無名関数は、href という語を検索して、式が true の場合にその語を返します。最終的に、getStats 呼び出しの結果が別の組、この場合は (string * int *int * int) となります。これを C# のコードで記述するとしたら、15 行にはとても収まりきれません。
図 2 の例では、F# と .NET Framework の優れた互換性も発揮されています。この点は次の例も同様です。
open System.Collections.Generic
let capitals = Dictionary<string, string>()
capitals.["Great Britain"] <- "London"
capitals.["France"] <- "Paris"
capitals.ContainsKey("France")
ここでは実際、Dictionary<K,V> 型が使用されているに過ぎませんが、F# でジェネリックを指定する方法 (C# と同様に山かっこを使用)、F# でインデクサを使用する方法 (これも C# と同様に角かっこの構文を使用)、および .NET メソッドを実行する方法 (C# と同様に "ドット" およびかっこを使用) が示されています。この中で唯一新しい点は可変値の代入方法で、左矢印の演算子を使用します。これが必要なのは、ほとんどの関数型言語と同様、F# でも等号演算子の使用が比較用に予約されているためです。x = y が、y を x に代入することを意味せず、x と y が同じ値であることを意味する数学的概念と同じです (本物の数学者たちは、現実世界か仮想空間かを問わず x = x + 1 が成り立つという考えを軽蔑し嘲笑してきました)。

F# ではオブジェクトも使用可能
当然ながら、F# を学ぶすべての .NET 開発者がすぐに関数型の概念を受け入れるわけではありません。実際、C# や Visual Basic から F# に移行する開発者のほとんどは、F# 言語を破壊せずに昔の習慣に戻ることができるかどうかを知りたがります。そして、それはある程度までは可能です。
例として、図 3 の冒頭にある 2 次元ベクタのクラス定義について考えます。ここには興味深い点が 2 つ見られます。まず、明示的なコンストラクタ本体が存在しないことに注目してください。最初の行のパラメータが、Vector2D のインスタンスを作成するためのパラメータを表しており、実質的にコンストラクタとして機能します。length 識別子は、dx 識別子および dy 識別子と共に、Vector2D 型の内部のプライベート要素となります。これに対して member キーワードは、標準の .NET プロパティ アクセスを経由して Vector2D の外部で使用できるメンバを示しています。つまり、この F# コードの宣言は、図 3 の下部に示したもの (Reflector による報告) と同等です。
                                                            VECTOR2D IN F#
type Vector2D(dx:float,dy:float) = 
    let length = sqrt(dx*dx + dy*dy)
    member obj.Length = length
    member obj.DX = dx
    member obj.DY = dy
    member obj.Move(dx2,dy2) = Vector2D(dx+dx2,dy+dy2)

VECTOR2D IN C# (REFLECTOR>
[Serializable, CompilationMapping(SourceLevelConstruct.ObjectType)]
public class Vector2D
{
  // Fields
  internal double _dx@48;
  internal double _dy@48;
  internal double _length@49;

  // Methods
  public Vector2D(double dx, double dy)
  {
    Hello.Vector2D @this = this;
    @this._dx@48 = dx;
    @this._dy@48 = dy;
    double d = (@this._dx@48 * @this._dx@48) + 
               (@this._dy@48 * @this._dy@48);
    @this._length@49 = Math.Sqrt(d);
  }

  public Hello.Vector2D Move(double dx2, double dy2)
  {
    return new Hello.Vector2D(this._dx@48 + dx2, this._dy@48 + dy2);
  }

  // Properties
  public double DX
  {
    get
    {
      return this._dx@48;
    }
  }

  public double DY
  {
    get
    {
      return this._dy@48;
    }
  }

  public double Length
  {
    get
    {
      return this._length@49;
    }
  }
}
既に説明したとおり、ほとんどの関数型言語と同様に F# でも不変値と不変状態が推奨されます。このことは図 3 のコードからも明らかです。ご覧のとおり、すべてのプロパティが読み取り専用です。また、Move メンバは既存の Vector2D を変更せず、現在の Vector2D から新しい Vector2D を作成し、変更値を適用してから返します。
もう 1 つ注目すべき点は、F# バージョンが完全にスレッド セーフであるだけでなく、従来の C# または Visual Basic のコードから完全にアクセス可能であることです。この点を利用して、F# を使い始める簡単な方法があります。ビジネス オブジェクトなどの、スレッド セーフかつ不変にすべき型を定義するために使用するのです。確かに、従来の可変処理 (プロパティの設定など) のセットを提供する型を F# で作成することも可能ですが、そのためには mutable キーワードを使用する必要があり、手間もかかります。同時実行への考慮が最優先課題となりつつある昨今、まさにこの "既定で不変、必要に応じて可変" という方式が望まれています。
F# における型の作成も興味深いのですが、従来の C# または Visual Basic のコードでできることにも F# を使用できます。たとえば図 4 に示すような、単純な Windows フォーム アプリケーションの作成や、ユーザーからの入力の収集です。
#light

open System
open System.IO
open System.Windows.Forms
open Printf

let form = new Form(Text="My First F# Form", Visible=true)

let menu = form.Menu <- new MainMenu()
let mnuFile = form.Menu.MenuItems.Add("&File")
let filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*"

let mnuiOpen =
  new MenuItem("&Open...",
    new EventHandler(fun _ _ ->
      let dialog = 
        new OpenFileDialog(InitialDirectory="c:\\",
          Filter=filter;
          FilterIndex=2, 
          RestoreDirectory=true) 
        if dialog.ShowDialog() = DialogResult.OK then
          match dialog.OpenFile() with
          | null -> printf "Could not read the file...\n"
          | s ->
            let r = new StreamReader(s) 
            printf "First line is: %s!\n" (r.ReadLine());
            s.Close();
    ),
    Shortcut.CtrlO)

mnuFile.MenuItems.Add(mnuiOpen)

[<STAThread>]
do Application.Run(form)

Windows フォームを使用した開発経験があれば、その実行内容をすぐに理解できるでしょう。単純なフォームが作成され、プロパティが設定され、イベント ハンドラが満たされると、右上隅の閉じるボタンをユーザーがクリックするまでアプリケーションが実行されます。.NET アプリケーションに関しては標準的な内容ですから、F# 構文についてのみ説明しましょう。
open ステートメントは C# の using ステートメントと同様の機能を持ち、形式的な修飾子がなくても使用できる .NET 名前空間を開きます。printf は F# 独自の名前空間で、技術的には同じ名前の OCaml モジュールの移植です。F# には、.NET Framework クラス ライブラリに対する完全な再現性があるだけでなく、OCaml ライブラリがほぼ忠実に移植されています。このため、OCaml 言語のプログラマにとっても .NET Framework が使いやすく感じられます (ちなみに、printf はアセンブリ FSharp.Core.dll 内部にあります)。もちろん System.Console.WriteLine を使用してもかまいません。単に美的センスの問題です。
form 識別子の作成では、F# の名前付きパラメータを利用しています。これは、オブジェクトをインスタンス化した後、一連のプロパティ設定呼び出しを行ってプロパティに値を設定することと同じです。dialog 識別子に対しても同じ操作を行い、その下に数行を作成します。
mnuiOpen 識別子の定義には興味深い構成要素が含まれています。.NET Framework 2.0 の匿名デリゲートや .NET Framework 3.5 のラムダ式を使用した経験のある開発者には、それほど目新しくないかもしれません。Open MenuItem に関連付けられた EventHandler の作成時に、次の構文で定義された無名関数が出てきます。
fun _ _ -> ...
匿名デリゲートの場合と同様、メニュー項目の選択時に起動する関数が作成されます。ただし、この構文にはいくらか注意が必要です。
MenuItem 定義の EventHandler 部分の定義にある無名関数は、渡された 2 つのパラメータを無視します。これらはちょうど、標準の EventHandler デリゲート型における、送信元とイベント引数に対応します。この関数では、次のように、新しい OpenFileDialog を表示し、[OK] がクリックされたときに結果を調べます。
if dialog.ShowDialog() = DialogResult.OK then
    match dialog.OpenFile() with
    | null -> printf "Could not read the file...\n"
    | s ->
        let r = new StreamReader(s) in
        printf "First line is: %s!\n" (r.ReadLine());
        s.Close();
結果の調査には、関数型言語の強力なパターン マッチング機能を使用します。表面上は C# の switch/case に似ていますが、パターン マッチングは名前のとおりの働きをします。値をさまざまなパターン (必ずしも定数値である必要はありません) と比較し、一致するコード ブロックを実行します。たとえば、この例の match ブロックでは、OpenFile の結果が 2 つの値と比較されます。つまり null と s です。null は、ファイルを開くことができなかったことを意味します。s には null 以外の任意の値が代入された後、StreamReader に対するコンストラクタとして使用され、指定のテキスト ファイルを開いて最初の行を読み取ります。
パターン マッチングはほとんどの関数型言語が備えている一大機能です。これについて少し説明しましょう。通常は、次のように弁別子付き共用体 (discriminated union) 型と組み合わせて使用します。この型は、C# や Visual Basic の列挙型にどことなく似ています。
// Declaration of the 'Expr' type
type Expr = 
  | Binary   of string * Expr * Expr
  | Variable of string 
  | Constant of int

// Create a value 'v' representing 'x + 10'
let v = Binary("+", Variable "x", Constant 10)
これは、ドメイン固有の言語の中核的表現を作成するため、関数型言語でよく使われます。開発者がそれらの表現を使用すると、より複雑で強力な構文を記述できます。たとえば、この構文を拡張して完全な計算言語を作成することは、すぐに思いつきます。Expr 型に新しい要素を追加することにより、さらに拡張することも容易にできます。ここで注意が必要なのは、* 文字を使用した構文が乗算を意味しないことです。関数型言語では、これは複数部分から成る型を表す標準的な方法です。
実際、インタープリタやコンパイラなどの言語指向プログラミング ツールの記述には、関数型言語がよく使われます。その場合、Expr 型が最終的には言語式の型の完全なセットとなります。F# には、これをさらに簡素化するツールとして、fslex および fsyacc の 2 つが含まれています。これらは、従来の言語による入力 (lex ファイルおよび yacc ファイル) を受け取って F# コードにコンパイルするように設計されており、ファイルの操作を容易にします。関心のある読者は、F# のインストーラをダウンロードして試してください。特に、標準の F# ディストリビューションに含まれている例 Parsing の基本構造を基に作業を開始することをお勧めします。
パターン マッチングには、弁別子付き共用体の他にも利点があります。第 2 の利点は、図 5 に示すとおり式の実行です。eval の定義内の rec は、eval を定義本体の中で再帰的に呼び出すよう F# コンパイラに伝えるために必要です。これを記述しないと、F# は、eval という名前の入れ子になったローカル関数が存在するものと見なします。ここでは、関数 getVarValue を使用して、変数に対して定義済みの値を返しています。現実のアプリケーションの getVarValue では、通常、変数作成時の指定に従って、返す値を Dictionary で調べます。
let getVarValue v =
    match v with
    | "x" -> 25
    | "y" -> 12
    | _ -> 0

let rec eval x =
    match x with
    | Binary(op, l, r) ->
        let (lv, rv) = (eval l, eval r) in
        if   (op = "+") then lv + rv 
        elif (op = "-") then lv - rv 
        else failwith "E_UNSUPPORTED"
    | Variable(var) -> 
        getVarValue var
    | Constant(n) ->
        n

do printf "Results = %d\n" (eval v)

eval が呼び出されて値 v を受け取ると、Binary 値であることが判明します。これは 1 つ目のサブ式に一致するため、直前に調べた Binary 値の左側と右側の部分の評価結果に、値 (lv, rv) をバインドします。名前がない値 (lv, rv) は組であり、基本的に、複数の部分から成る単一の値です。この点で、リレーショナル セットや C 構造体とは異なります。
eval l が最初に呼び出されたとき、Binary インスタンスの l は Variable 型です。したがって、eval に対する再帰呼び出しは、パターン マッチング ブロックのその分岐に一致します。続いて getVarValue が呼び出されると、ハードコードされた 25 が返されます。これは最終的に値 lv にバインドされます。同じシーケンスが、値 10 を含む Constant である r に対しても実行され、これは rv にバインドされます。ブロックの残りの if/else-if/else ブロックが実行されますが、C#、Visual Basic、または C++ に精通した開発者にとって、この部分は簡単に理解できます。
ここでもやはり重要なのは、すべての式が値を返す点です。パターン マッチング ブロックの中でも同様です。この例の戻り値は整数値で、演算の値、変数から取得した値、定数自体のいずれかです。特にこの点が、オブジェクト指向プログラミングや命令型プログラミングに慣れ親しんだ開発者がつまづくところなのです。C#、Visual Basic、および C++ においては戻り値が省略可能で、指定されていても無視できるためです。F# などの関数型言語で戻り値を無視するには、明示的なコーディング手法が必要となります。そのような場合、プログラマは ignore という関数に結果を入力することができます。この関数の機能は名前のとおりです。

非同期 F#
F# 構文を紹介するにあたり、これまでの方法は次のいずれかでした。かなり単純な関数型構文を使用するか、そうでなければ、従来の .NET 準拠のオブジェクト指向言語 (C#、Visual Basic、または C++/CLI) の風変わりで簡潔なバリエーションとして紹介してきたのです。これでは、企業に F# を導入してもらえるような説得力の持ちようがありません。
しかし、図 6 を見てください。これは先ほどの 2 つのどちらにも該当しないはずです。2 か所に出てくる ”!” という文字と、async 修飾子を除けば、このコードは比較的簡単に見えます。ソース グラフィック イメージを読み込んでデータを抽出し、そのデータをスタンドアロン関数に渡して操作 (回転、傾斜など) を実行し、そのデータを出力ファイルに書き込みます。
let TransformImage pixels i =
  // Some kind of graphic manipulation of images

let ProcessImage(i) =
  async { use  inStream  = File.OpenRead(sprintf "source%d.jpg" i)
          let! pixels    = inStream.ReadAsync(1024*1024)
          let  pixels'   = TransformImage(pixels,i)
          use  outStream = File.OpenWrite(sprintf "result%d.jpg" i)
          do!  outStream.WriteAsync(pixels')
          do   Console.WriteLine "done!"  }

let ProcessImages() =
  Async.Run (Async.Parallel 
               [ for i in 1 .. numImages -> ProcessImage(i) ])

ここで説明が必要なのは、async 修飾子です。この修飾子を使用することにより、コードが、F# において非同期ワークフローと呼ばれるものに変わります (Windows Workflow Foundation とは関係がありません)。これは、読み込み、処理、保存の各ステップが、.NET スレッド プール内の並列スレッドで実行されることを意味します。
わかりやすくするため、図 7 のコードについて考えましょう。このシーケンスでは、非同期ワークフローが単純かつ理解しやすい方法で使用されています。煩雑な説明は省きますが、evals は実行を待機している関数の配列です。Async.Parallel 呼び出しにより、各関数がスレッド プールで実行のキューに登録されます。実行時に、evals 内の関数が実際は別の awr 内の関数のスレッド上にあることが明らかになります (ただし、.NET システム スレッド プールの特性により、evals 関数の一部または全部が同一スレッド上で実行される場合があります)。
#light
open System.Threading

let printWithThread str =
    printfn "[ThreadId = %d] %s" Thread.CurrentThread.ManagedThreadId str
    
let evals =
    let z = 4.0
    [ async { do printWithThread "Computing z*z\n"
              return z * z };
      async { do printWithThread "Computing sin(z)\n"
              return (sin z) };
      async { do printWithThread "Computing log(z)\n"
              return (log z) } ]

let awr =
    async { let! vs = Async.Parallel evals
            do printWithThread "Computing v1+v2+v3\n"
            return (Array.fold_left (fun a b -> a + b) 0.0 vs) }

let R = Async.Run awr
printf "Result = %f\n" R

これらが .NET スレッド プールから実行される事実も、基になるランタイムとの相互運用性において F# 言語がいかに優れているかを示しています。関数型言語において従来は特別な実装 (スレッド化など) 向けに予約されていた領域でも .NET Framework クラス ライブラリに依存しているため、F# の開発者が C# のライブラリを利用するように、C# のプログラマも F# のライブラリやモジュールを利用できるのです。将来的には、非同期タスクなどの F# 機能で、Parallel Extensions ライブラリの Task Processing Library などの新しい .NET Framework ライブラリを活用できるようになります。

F# に慣れる
念のため申し上げると、F# 言語について言うべきことは 1 つの記事に収まりきれません。実際、構文も考え方 ("関数型" 対 "命令型") もまったく異なるのですから、C# や Visual Basic に慣れ親しんだ平均的なオブジェクト指向開発者にとって、F# の習得にしばらく時間がかかるのも当然です。さいわい、F# は .NET のエコシステム内において完全に相互運用可能であるため、既存の知識とツールを大いに活用して、F# をコーディングの武器に加えることができるのです。
F# 開発者はすべての foundation ライブラリにアクセスできます。また、F# は命令型開発やオブジェクト開発の一部の側面をサポートしているため、F# の構文と、Windows Presentation Foundation、Windows Communication Foundation、または Windows Workflow Foundation の詳細とを同時に (しかもコンパイル サイクルに中断されることなく) 学ぶ手段として、F# の対話型モードの使用を検討することも可能です。
既に述べたとおり、開発者は F# で記述したビジネス オブジェクトをアプリケーション コードの他の部分で使用できます。F# の type 構文で作成されるクラスが、C# または Visual Basic のクラスとほぼ同じであるため、NHibernate などの永続化ライブラリで、F# の型も問題なく永続化できます。このことは、既存のビジネス アプリケーションに F# をシームレスに投入するために役立ちます。
単に F# を学ぶだけでも、C# や Visual Basic の将来のバージョンの多くの機能を理解しやすくなります。ジェネリック、反復子 (C# の yield キーワード)、LINQ を初めとする、そうした考え方や概念の多くが、関数型にルーツを持ち、F# チームによる研究から生まれたものだからです。人々の見方にかかわらず、関数型プログラミングは確かに存在し、これからも存在し続けます。

Ted Neward は、大規模エンタープライズ システムを専門とするフリーのコンサルタントです。複数の著書および共著書があり、Microsoft MVP アーキテクト、BEA の技術ディレクター、INETA の講演者、Pluralsight のインストラクタでもあります。Ted の連絡先は、ted@tedneward.com です。また、blogs.tedneward.com にブログを公開しています。

Page view tracker