これからの記事では、このシリーズのテーマである簡潔さ、利便性、正確性、並行性、完全性を実際に示していきます。
その前に、F#で何度も出てくる重要な概念をいくつか見ていきましょう。F#は多くの点でC#のような標準的な命令型言語とは異なりますが、特に理解しておくべき大きな違いがいくつかあります:
- オブジェクト指向ではなく関数指向
- 文ではなく式
- ドメインモデルを作るための代数的型
- 制御フローのためのパターンマッチング
これらの概念は後の記事でより詳しく扱います。ここではこのシリーズの残りを理解するためのお試し版程度です。
オブジェクト指向ではなく関数指向
「関数型プログラミング」という言葉から想像できるように、F#では関数があちこちに登場します。
もちろん、関数はファーストクラスのエンティティで、他の値と同じように受け渡しができます:
let square x = x * x
// 値としての関数
let squareclone = square
let result = [1..10] |> List.map squareclone
// 他の関数をパラメータとして受け取る関数
let execFunction aFunc aParam = aFunc aParam
let result2 = execFunction square 12
でも、C#にもファーストクラス関数があるじゃないか、と思うかもしれません。では、関数型プログラミングの何がそんなに特別なんでしょう?
簡単に言えば、F#の関数指向の性質は、C#では実現が難しいか面倒なことが、F#ではとてもエレガントに表現できるほど、言語とその型システムのあらゆる部分に浸透しているのです。
これを数段落で説明するのは難しいですが、このシリーズの記事で示していく利点をいくつか挙げてみましょう:
- 合成による組み上げ。合成は、小さなシステムからより大きなシステムを作り上げる「接着剤」です。これは単なる省略可能なオプションの技術ではなく、関数型スタイルの核心部分です。ほぼすべてのコード行が合成可能な式(後述)です。合成は基本的な関数を作るのに使われ、さらにそれらの関数を使う関数を作り、というように続きます。そして合成の原則は関数だけでなく、型(後述の直積型と直和型)にも適用されます。
- 分割と再構成。問題をパーツに分割する能力は、それらのパーツをどれだけ簡単に再び組み立てられるかに依存します。命令型言語では分割不可能に見えるメソッドやクラスも、関数型設計では驚くほど小さなピースに分解できることがよくあります。これらの細かいコンポーネントは通常、(a) 他の関数をパラメータとして受け取るいくつかの非常に一般的な関数と、(b) 特定のデータ構造やアプリケーションのために一般的なケースを特殊化する他のヘルパー関数で構成されます。 一度分割されると、一般化された関数を使って、新しいコードを書くことなく多くの追加操作を非常に簡単にプログラムできるようになります。このような一般的な関数の良い例(fold関数)は、「ループから重複コードを抽出する」の記事で見ることができます。
- 良い設計。「関心の分離」、「単一責任の原則」、「実装ではなくインターフェースに対してプログラミングする」などの良い設計の原則の多くは、関数型アプローチの自然な結果として生まれます。そして一般的に、関数型コードは高レベルで宣言的になる傾向があります。
このシリーズの以降の記事では、関数がどのようにしてコードをより簡潔で便利にできるかの例を示します。さらに深く理解したい場合は、「関数型思考」のシリーズ全体があります。
文ではなく式
関数型言語には文がなく、式しかありません。つまり、すべてのコードの塊は常に値を返し、より大きな塊は、一連の文ではなく、小さな塊を合成することで作られます。
LINQやSQLを使ったことがある人なら、すでに式ベースの言語に馴染みがあるでしょう。たとえば、純粋なSQLでは、代入はできません。その代わり、より大きなクエリの中にサブクエリを持たなければなりません。
SELECT EmployeeName
FROM Employees
WHERE EmployeeID IN
(SELECT DISTINCT ManagerID FROM Employees) -- サブクエリ
F#も同じように動作します - すべての関数定義は、一連の文ではなく、単一の式です。
そしてあまり明白ではないかもしれませんが、式から作られたコードは、文を使うよりも安全でコンパクトです。 これを理解するために、C#の文ベースのコードと、同等の式ベースのコードを比較してみましょう。
まず、文ベースのコードです。文は値を返さないので、文の本体の中から代入される一時変数を使わなければなりません。
// C#の文ベースのコード
int result;
if (aBool)
{
result = 42;
}
Console.WriteLine("result={0}", result);
if-then
ブロックが文なので、 result
変数は文の外で定義しなければならないのに、文の中から代入しなければなりません。これは以下のような問題を引き起こします:
result
にはどんな初期値を設定すべき?result
変数への代入を忘れたらどうなる?- "else" の場合、
result
変数の値は何になる?
比較のために、同じコードを式指向のスタイルで書き直してみましょう:
// C#の式ベースのコード
int result = (aBool) ? 42 : 0;
Console.WriteLine("result={0}", result);
式指向のバージョンでは、これらの問題はどれも当てはまりません:
result
変数は、代入されると同時に宣言されます。式の「外側」で変数を設定する必要はなく、どんな初期値を設定すべきかを心配する必要もありません。- "else" が明示的に処理されています。分岐のどこかで代入し忘れる可能性はありません。
result
への代入を忘れることはできません。なぜなら、そうすると変数自体が存在しないことになるからです!
式指向のスタイルはF#では省略可能なオプションではなく、命令型のバックグラウンドから来た人にとっては、アプローチの変更が必要なものの一つです。
代数的型
F#の型システムは代数的型の概念に基づいています。つまり、新しい複合型は、既存の型を2つの異なる方法で組み合わせて構築されます:
- まず、一連の型からそれぞれ選ばれた値の組み合わせ。これらは「直積」型と呼ばれます。
- あるいは、一連の型の間の選択を表す、交わりを持たない和集合。これらは「直和」型と呼ばれます。
たとえば、既存の型 int
と bool
があれば、それぞれを1つずつ含む新しい直積型を作ることができます:
// 宣言
type IntAndBool = {intPart: int; boolPart: bool}
// 使用
let x = {intPart=1; boolPart=false}
あるいは、各型のどれかを選択できる新しい和集合(直和)型を作ることもできます:
// 宣言
type IntOrBool =
| IntChoice of int
| BoolChoice of bool
// 使用
let y = IntChoice 42
let z = BoolChoice true
これらの「選択」型はC#では利用できませんが、状態機械の状態(これは多くのドメインで驚くほど一般的なテーマです)など、多くの現実世界のケースをモデル化するのに非常に便利です。
そして、この方法で「直積」型と「直和」型を組み合わせることで、任意のビジネスドメインを正確にモデル化する豊富な型のセットを簡単に作ることができます。 この実際の例については、「低オーバーヘッドの型定義と型システムを使用して正しいコードを保証する」の記事を参照してください。
制御フローのためのパターンマッチング
ほとんどの命令型言語は、分岐とループのための様々な制御フロー文を提供しています:
if-then-else
(および三項演算子版のbool ? if-true : if-false
)case
またはswitch
文break
とcontinue
を伴うfor
およびforeach
ループwhile
およびuntil
ループ- そして恐ろしい
goto
まで
F#もこれらの一部をサポートしていますが、F#は最も一般的な形の条件式であるパターンマッチングもサポートしています。
if-then-else
の代わりになる典型的なマッチング式は次のようになります:
match booleanExpression with
| true -> // trueの場合の処理
| false -> // falseの場合の処理
そして、 switch
の代わりは次のようになるかもしれません:
match aDigit with
| 1 -> // 数字が1の場合の処理
| 2 -> // 数字が2の場合の処理
| _ -> // それ以外の場合の処理
最後に、ループは一般的に再帰を使って行われ、通常は次のようになります:
match aList with
| [] ->
// 空のケース
| first::rest ->
// 少なくとも1つの要素がある場合
// 最初の要素を処理し、
// リストの残りで再帰的に呼び出す
マッチ式は最初は不必要に複雑に見えるかもしれませんが、実際には優雅で強力なものだと分かるでしょう。
パターンマッチングの利点については、「網羅的なパターンマッチング」の記事を、そしてパターンマッチングを多用した実例については、「ローマ数字の例」を参照してください。
直和型とのパターンマッチング
上で、F#が「直和」型、言いかえると「選択」型をサポートしていると述べました。これは、継承の代わりに使用され、基礎となる型の異なるバリアントを扱います。パターンマッチングはこれらの型とシームレスに連携し、各選択肢に対する制御フローを作成します。
次の例では、4つの異なる形状を表す Shape
型を作成し、次に各種類の形状に対して異なる動作をする draw
関数を定義します。
これはオブジェクト指向言語のポリモーフィズムに似ていますが、関数に基づいています。
type Shape = // 代替構造の「直和」を定義
| Circle of int
| Rectangle of int * int
| Polygon of (int * int) list
| Point of (int * int)
let draw shape = // shapeパラメータを持つ「draw」関数を定義
match shape with
| Circle radius ->
printfn "この円の半径は%dです" radius
| Rectangle (height,width) ->
printfn "この長方形の高さは%d、幅は%dです" height width
| Polygon points ->
printfn "この多角形はこれらの点で構成されています: %A" points
| _ -> printfn "この形は認識できません"
let circle = Circle(10)
let rect = Rectangle(4,5)
let polygon = Polygon( [(1,1); (2,2); (3,3)])
let point = Point(2,3)
[circle; rect; polygon; point] |> List.iter draw
いくつか注目すべき点があります:
- ここでも型を指定する必要はありませんでした。コンパイラは "draw" 関数のshapeパラメータが
Shape
型であると正しく判断しました。 match..with
のロジックが、形状の内部構造に対してマッチングするだけでなく、その形状に適した値も割り当てているのがわかります。- アンダースコアは、switch文の "default" 分岐に似ていますが、F#では必須です - 可能性のあるケースは常にすべて処理しなければなりません。以下の行をコメントアウトしてみてください:
コンパイル時に何が起こるか見てみましょう!| _ -> printfn "この形は認識できません"
これらの選択型は、C#ではサブクラスやインターフェースを使ってある程度シミュレートできますが、C#の型システムにはこの種の網羅的なマッチングとエラーチェックのためのビルトインサポートはありません。