はじめに
C#プログラマのためのF#入門、第2回目は「関数型言語としてのF#」をテーマに、関数型言語特有の機能について紹介していきたいと思います。C#においてもC# 2.0以降、関数型プログラミングの概念が、匿名メソッド、ラムダ式、LINQなどで徐々に取り入れられていますので、まったく目新しいものばかりではないかもしれませんが、関数型言語の主役とも言える「関数」について、C#との比較も交えながら紹介したいと思います。
プロジェクトの作成
前回はfsi.exeを使用して、簡単な束縛や定義について解説しましたが、今回は、VS2010でF#コンパイラも使用してみましょう。まず、VS2010から[ファイル]-[新しいプロジェクト]-[Visual F#]-[F# Application]でプロジェクトを作成してみましょう。
#light
F#のプロジェクトを作成する際に最初に考慮しなくてはならないのは、#lightシンタックスのON/OFFです。#lightシンタックスとは、ホワイトスペース(空白)によるインデントを使用した特定の書式を用いることによって、”in”、”with”、 “begin”、”end”、”;;”などの省略が可能になり、読みやすいコーディングを可能にする簡易構文モードのことです。半角4スペースがインデントの単位になります。8月15日時点で、VS2010のF#のソリューションファイルのデフォルトは簡易構文モード(#light)になっています。OFFにするには、コードの先頭に#light "off" と記述します。使用方法については以下の例を参考にしてください。
(1)複数の式を改行して記述する
#light printf "Hello, world" //改行のみでOk printf "Good, morning"
#light "off" printf "Hello, world" ;; //ダブルセミコロンにて区切る必要がある printf "Good, morning"
(2)束縛の有効範囲とインデントの使用
#light let f x = let y = 1 //インデントをそろえる必要がある let z = 2 //インデントをそろえる必要がある x * y + z //インデントをそろえる必要がある
#light "off" let f x = let y = 1 in //インデントがそろっていなくても実行可能。inが必要 let z = 2 in //インデントがそろっていなくても実行可能。inが必要 x * y + z
再帰
ここからは、いよいよ関数の説明です。最初にF#の最も特徴的とも言える関数、「再帰関数」について解説したいと思います。
再帰関数
再帰関数とは、自分自身を呼び出す関数のことです。再帰はC#でも利用されているアイデアですが、例としてnの階乗を求める再帰関数を解説します。
上記で作成したプロジェクトのソースファイルに、下記のようにタイプしてください。
#light //デフォルトで#lightモードですが、分かりやすいのであえて記述 let rec facto n = if n <= 1 then 1 else n * facto (n-1) printfn "%d" (facto 5)
再帰関数factoの定義の1文をコピーして、VS2010のボトムにある、「F# Interactive」に貼り付けてください。F# Interactive とはF#インタープリタのことで、VS2010では「F# Interactive」というラベルが付いているツールウィンドウで使用可能です。
このツールウィンドウが開いていない場合には、[Ctrl]+[Alt]+[F]、もしくは、ツールバーの[表示]-[その他のウィンドウ]-[F# Interactive]で開くことができます。F# Interactiveは開かれているプロジェクトとは別に独立して機能します。
貼り付けた部分を強調選択して、[Alt]+[Enter]を押すことで、対話的に部分的コードの確認ができます。
再帰関数factoの定義文をF# Interactiveにて送信してみると下記のようになります。
>let rec facto n = if n <= 1 then 1 else n * facto (n-1) ;;
▼
val facto : int -> int
同様にprintfの行もコピーしてF# Interactiveを実行すると詳細が確認できます。F# Interactiveでデータが戻ってくる際に送信したデータが最下行に表示され、実行内容がその上に表示されるのは若干紛らわしいですが、内容は理解できると思います。
再帰関数factoの呼び出しをF#Interactiveにて実行すると下記のようになります。
>facto 5 ;;
▼
val it : int = 120
問題ない場合には、[ビルド]-[TestApp(プロジェクト名)]-[デバッグ]-[デバッグなしで開始]を実行してください。この作業はC#でも同様ですので、解説は省きます。
上記のリスト6は、数学の書き方であらわすと、n! です。関数名factoの前にあるrecというキーワードですが、定義する関数が再帰関数の場合、関数名の前にrecキーワードを必ずつける必要があります。
例えば5の階乗を求める場合、1*2*3*4*5でもよいのですが、この場合、再帰関数を使用した方が式が簡潔で美しくなります。n!はn * (n-1)!と同じですので、(n-1)をパラメータとして、関数factoをnが1になるまで繰り返します。つまり、計算は下記のようなプロセスになります。
facto 5 = 5 * facto 4 = 5 * (4 * facto 3) = 5 * (4 * (3 * facto 2)) = 5 * (4 * (3 * (2 * facto 1))) // n <= 1 なので、facto 1に1が返される = 5 * (4 * (3 * (2 * 1))) = 5 * (4 * (3 * 2)) = 5 * (4 * 6) = 5 * 24 = 120
再帰関数の基本として、常に頭に入れておきたいポイントが2つあります。
- 再帰が終了する条件があること
- パラメーターが再帰が終了する条件にいつか必ず到着すること
一般的には、パラメーターなどのカウンターの値を減少させていき、再帰の終了する条件を「カウンターが0もしくは1になるまで」とするパターンがよく見られます。上記のリスト8もこのパターンです。
let rec 関数名 パラメーターリスト = 関数本体(function-body)
相互再帰関数
再帰関数でよく見かけられる機能で、相互再帰関数という関数があります。再帰関数の一種で、2つ以上の関数がお互いを呼び出す再帰関数のことです。
let rec 関数名1 パラメーターリスト = 関数本体1(function1-body) and 関数名2 パラメーターリスト = 関数本体 2(function2-body)
下記のリスト9はnが奇数か偶数かを判別する相互再帰関数です。パフォーマンスが下がる、デバッグしにくいなどの理由で避けたい機能としても知られていますが、個々に複数の関数を宣言するより関数を概念化しやすい、コードがすっきり見えるなどの理由で好むプログラマーもいるようです。
> let rec even n = (n = 0us) || odd (n - 1us) and odd n = (n <> 0us) && even(n - 1us) ;;
▼
val even : uint16 -> bool val odd : uint16 -> bool
> even 5us ;;
▼
val it : bool = false
> odd 5us ;;
▼
val it : bool = true
n = 0usがtrue、もしくは、odd(n - 1us) がtrueの場合、関数evenはtrueを返します。数字のあとにある"us"はその値が16ビット符号なし整数(uint16型)であることをあらわします。両方がfalseの場合にはfalseを返します。関数oddはn<>0us(パラメーターn<>符号なし0)がtrueで、かつ、even(n - 1us)がtrueの場合のみtrueを返し、それ以外の場合には、falseを返します。