型とパターンマッチングを改善する
型引数(type augmentations)を使うと、型の周辺で提供される機能を効率よく扱えます(リスト1を参照)。
#light
// 図形は、四角形、円形、または正方形です。
// 正方形は特殊な四角形です。
type Shape =
| Rectangle of int * int * int * int
| Circle of int * int * int
with
static member Square(x, y, size) = Rectangle (x, y, x+size, y+size)
member self.Perimeter =
match self with
| Rectangle (x1, y1, x2, y2) ->
Float.of_int (2*(x2-x1)+2*(y2-y1))
| Circle (x, y, r) ->
Float.of_int (2*r) * System.Math.PI
end
// 絵は図形の集まりです。
type Drawing = Drawing of Shape seq
with
member self.TotalInkNeeded =
match self with
| Drawing shapes ->
shapes |> Seq.sumByFloat (fun s -> s.Perimeter)
end
通常、型定義は、その型に属する値を操作する基本的な抽象制御メカニズムと手を組みます。パターンマッチング(特定の形状のツリーや特定のサイズおよび内容のリストのような、値の構造へのマッチング)、匿名関数(ラムダ式とも呼ばれる、必要な時点で作成される名前のない関数)、集計高階関数(列挙可能なデータに適用する関数)は、このようなメカニズムです。例えば、リスト1のSeq.sumByFloatは、Seq型(すべての列挙可能な型を表す型であり、seqはこの型の標準的な省略形としてDrawingでも使用)に対する静的な集計関数であり、図形の外周を計算する匿名関数を受け取って、これを絵に含まれるすべての図形に適用し、その結果を集計して絵を描画するために必要なインクの量を求めます。
パイプと共通集計関数について
TotalInkNeededの定義になぜパイプ( |>)演算子が使われるのか不思議かもしれません。この演算子は、F#標準ライブラリに次のように定義されています。
let (|>) x f = f x
パイプは中置演算子ですから、左側のオペランドを右側のオペランドに送信(パイプ)するという形で関数呼び出しを上品に表現できます。この記法には、2つの利点があります。1つは引数をかっこで囲む必要がないことで、もう1つはコンパイラの型推論を支援できることです(これは、型がわかっている値を左側から渡すことにより、型情報が供給されるからです)。
sumByFloatは、折りたたみ(folding)の特殊なケースです。これは比較的一般的な関数型パターンであり、1つの関数と1つの初期アキュムレータを受け取ってから、その関数を初期アキュムレータを使ってコレクションの最初の要素に適用し、さらにその結果を2番目の要素に適用する、といった操作をコレクションの最後の要素にまで繰り返してアキュムレータの最終的な値を取得します。F#標準ライブラリでは、折りたたみ、マッピング(関数をコレクションのすべての要素に適用し、その結果の値を要素として持つ新しいコレクションを構築する)、反復(すべての要素に対して同じ操作を繰り返す)がすべてのジェネリックな列挙型でサポートされます。これらの集計操作と、そこから派生するより特殊な操作の多くは、関数型プログラマのツールセットで最も頻繁に利用されるツールです。
最後に、ここで作成した新しい型を対話式セッションで簡単にテストしてみましょう(リスト1のコードを使用)。例えば、次のコードは(シーケンスの内包を使用し、結果をDrawingコンストラクタにパイプすることで)20個の新しい図形を作成してから、これらの図形を描画するために必要なインクの量を計算します。
> let a = seq { for i in 1 .. 20
-> if i % 3 = 0 then
Circle (20, 20, i)
elif i % 3 = 1 then
Rectangle (20, 20, 20+i*2, 20+i)
else
Shape.Square (20, 20, i) } |> Drawing;;
val a : Drawing
> a.TotalInkNeeded;;
val it : float = 1123.840674
まとめ
この記事では、関数型プログラミングの主な概念と、これらの概念をF#でとりあえず使ってみる方法を解説しました。その過程で、基本型の定義、単純な型、関数、構造化された値、無限遅延シーケンス、集計高階関数を使う引数化されたメンバを持つ型、ラムダ式、パイプ演算子について学習しました。また、図形を表す値の列挙をシーケンスの内包を使って作成しました。この連載の次回の記事では、これらの機能をさらに詳しく学ぶと共に、F#の生産性カーブを確実に上昇させる高度な関数型構造について学習します。
