特集:C#開発者のためのF#入門(前編) F#で初めての関数型プログラミング bleis-tift2012/04/12 |
|
|
■関数型プログラミングの基礎
関数型言語でどのようにプログラムを書いていくのかを見ていこう。
●不変な値
関数型言語では極力、「状態」というものを避ける傾向にある。例えば変数が状態を持ってしまうと、その変数を参照する箇所で「この変数の今の値は何だろう?」と注意しなければ、簡単にバグを埋め込んでしまう。だが、「状態を持たずに、どうやってプログラムを書くんだ!」と思う人も多いだろう。
状態を持たないプログラムに対する抵抗感を減らすために、.NETの文字列について考えてみよう。
.NETでは、Stringクラス(System名前空間)はインスタンスを作るときに値は確定しており、以後、そのインスタンスの値を変更することはできない(※なお、Stringインスタンスに対して文字列結合などの操作をした場合は、新しい別のStringインスタンスが返されている)。つまり、Stringクラスは状態を持っていないのだ。
このように、実は皆さんはすでに状態を持たないプログラムの世界に少し足を突っ込んでいるのである。この少し足を突っ込んでいる割合を「状態を持たない」方に傾けるだけ、と考えると、少しは抵抗感が和らぐのではないだろうか。
F#はこの「状態を持たない」方向に傾けたプログラムを書くためのサポートが手厚く、逆に「状態を持った」方向に傾けたプログラムを書くのが面倒になるように文法が構築されている。例えば、F#では変数は「let」キーワードを用いて次のように定義する。
|
|
変数の定義例:変数「x」の値は「42」(F#) | |
ちなみに、行コメントはC#と同じ形式である。範囲コメントは丸括弧とアスタリスクを組み合わせる。 |
しかし、ここで定義した変数「x」の値を変更することはできない。
関数内であれば、変数「x」を(後から宣言した変数「x」で)シャドウイング(=隠ぺい)して、例えば、
|
|
変数の値を変更するコード例(F#):シャドウイング | |
正確に言うと「変数の隠ぺい」であり、「変数の値の変更」ではない(後述)。 |
とすることで、C#での、
|
|
変数の値を変更するコード例(C#):代入 |
と同じようなことはできるが、完全に同じではない。
例えばC#で、
|
|
「代入」すると「元の変数の値」を書き換えてしまうことを示すコード例(C#) |
のようにすると、どちらのコンソール出力も「32」が表示される。
それに対して、F#で、
|
|
「シャドウイング」してもスコープを抜ければ「シャドウイングが解ける」ことを示すコード例(F#) |
とすると、2番目の出力は「32」ではなく「42」と表示される。
これは各変数のスコープを考えると分かりやすい。シャドウイングは同じ名前で新しい別の変数を定義するだけなので、if式の内側で定義した変数「x」は、最後の行ではスコープから抜けており、参照できない(C#では、ローカル変数のシャドウイングはできないため、if文の中で新しい変数「x」を再度定義することはできない)。そのため、外側で定義した変数「x」表示されるのである。
ところで、上のF#コードでは、if式の終了を示すものがないことに気付いただろうか。
F#では、PythonやHaskellのように、インデントを文法に組み込んでいる。そのため、インデントは非常に重要になってくる。なお本記事では、インデントをASCII文字コードにおける空白2文字で統一している。
●値と関数
F#で「関数」を定義するためには、変数の定義と同じletキーワードを用いる。
|
|
letキーワードを用いた関数の定義例(F#) | |
「=」演算子の後で改行しているが、正しくインデントしていれば、自由に改行できる。 |
このF#コードは、C#なら、
|
|
上記の関数定義に相当するC#のコード例 |
というコードに相当する。
ここで、F#で定義したplus10関数に型を何も書いていない点に注目してほしい。
plus10関数のパラメータの型は、関数の本体(=「x + 10」というコード)から自動的に推論されるのである。
この型推論を検証するために、対話環境で、
(+);;
というF#コード(F#では演算子をカッコで囲むことで関数として扱うことができる。詳しくは後で述べる。を実行してみてほしい。すると、次のような結果になったはずだ。
|
|
「(+);;」というF#コードを対話環境で実行した結果の例 |
結果の型に「->」という矢印が現れているが、これは「左側の型を受け取って右側の型を返す関数」という意味である。例えば、
string -> int
は「string型の値を引数に取ってint型の値を返す関数」となる。
これを基に考えると、「(+)」というコードで得られるオブジェクトの型は「int型を引数に取って『int型を引数に取ってint型を返す関数』を返す関数」となる。複雑なので、今のところは「int型を2つ引数に取って、int型を返す関数」と考えておけばいい。
つまりF#では、+演算子は右辺も左辺もint型を取る、ということだ(多重定義されているため厳密には違うのだが、ここではその点は無視する)。ということは、plus10関数の本体である「x + 10」のxの型はint型であるということが分かる。ここでのxはplus10関数のパラメータ「x」なので、plus10関数のパラメータ「x」の型はint型と推論できる。そして、+演算子の戻り値の型はint型なので、「x + 10」という式の型がint型になることも分かる。plus10関数はこの式のみで構成されるので、戻り値の型はint型と推論できる。これらを合わせて考えると、plus10関数は「int型の値を引数に取ってint型の値を返す関数」と推論ができるわけである。
型は明示することもできる。そのためには、例えば次のように、コロン(:)に続けて型名を記述する。
|
|
引数や戻り値の型の指定例(F#) |
関数の戻り値の型の指定方法には注意する必要がある。
|
|
戻り値の型の指定例(F#) |
上記のコードはパラメータ「x」の型としてint型を指定しているようにも見えるが、実際は関数の戻り値の型を指定している。
定義した関数の呼び出しは、C#とは異なり引数を囲むカッコは記述しない(次のコードを参照)。
|
|
関数呼び出しの例(F#):関数呼び出しの引数にカッコは記述しない |
なお上記のコード例では、System.Console.WriteLineメソッド呼び出しの引数はカッコで囲っているが、これはスタイルの問題で、「F#の外から来ているものにはカッコを付ける」というルールを筆者が自分に課しているからだ。もちろん、このカッコも省略して記述できる。
関数にパラメータが複数ある場合、関数定義の各パラメータを空白で区切り、関数呼び出しの際にも同様に空白で各引数を区切る。
|
|
複数のパラメータがある関数の定義と呼び出しの例(F#) |
F#では関数は値として扱える。
|
|
関数を値として扱う例(F#) |
変数の定義も関数の定義も、letというキーワードで統一的に定義できるようになっている。しかしそもそも、値と関数の境界線があいまいなのである。
なお、先ほど「演算子をカッコで囲むことで関数として扱うことができる」と書いた。これはつまり、F#では「演算子も値として扱える」ということを意味している。次のコードはその例である。
|
|
演算子を関数や値として扱う例(F#) |
またF#は、ジェネリック関数も簡単に定義できる。例えば、
|
|
ジェネリック関数の定義例(F#) |
とするだけで、C#での、
|
|
ジェネリック関数の定義例(C#) |
と同じようなものが定義できる。gt関数の型を推論するときに、比較演算子がジェネリックな演算子として定義されているため、gt関数の型も比較演算子同様、ジェネリックと判断されたのだ。
これが、例えば(次のコードのように)一方の引数の型が具体的に決定できる場合は、ジェネリックにはならない。
|
|
ジェネリックな関数には推論されない例(F#) |
ほかにも例えば、
|
|
ジェネリック関数の定義例(F#):値を変更せずに返す、ジェネリックなid関数 |
というコードは、C#での、
|
|
ジェネリック関数の定義例(C#):値を変更せずに返す、ジェネリックなid関数 |
に対応する(ちなみにid関数はF#の標準ライブラリに定義されている)。
非ジェネリック関数の場合、C#やVBでもFuncデリゲートとラムダ式を使って値のように関数を扱えるが、ジェネリック関数の場合はメソッドを使わざるを得ないため、F#ほどの統一性はない。次のコードは、C#のコード例だ。
|
|
C#では、ジェネリック関数を値のようには扱えない |
●ループと高階関数
「状態を持たない」スタイルでプログラムを書くためには、C言語風のfor文は使えないことになる。なぜなら、C言語風のfor文はループ・カウンタという「今、何回目のループなのか」を保持する変数が必要になるからだ。
しかし、ループが使えないと役に立つプログラムは書けそうにない。このジレンマを解決するために、F#ではある種の高階関数を使う。
高階関数とは、関数を引数に取る関数や、関数を返す関数のことをいう。例えば、次のようなものが高階関数だ。
|
|
高階関数の定義例(F#) | |
この関数定義では、最初に「(f x)」という部分(=「f」は関数で、「x」はそのパラメータ)を実行して<結果>を得て、次に「f <結果>」という部分(=「f」は先ほどと同じ関数で、その引数として「<結果>」が渡される)が実行される。 |
このtwice関数は、「x」パラメータに渡された引数の値(以降、x値)に対して、同じく「f」パラメータに引数として渡された関数(以降、f用関数)を2回適用する関数である。この場合、f用関数は、x値と同じ型のパラメータを1つ受け取り、x値と同じ型の戻り値を返すように定義しなければならない。
実際に実行してみると分かりやすいだろう。次のコードを実行してほしい(このコードでは、f用関数は「plus10」という名前で関数定義している)。
|
|
高階関数の定義とそれを実行するコードの例(F#) |
F#ではループの代わりとして使える高階関数が数多く提供されている。LINQになじみのある読者であれば、「LINQ to Objectsを使えば、ループ用の構文がなくても問題ない」ということは分かってもらえると思う。
例として、int型のリストの全ての要素を1024倍する関数を定義してみよう。これをC#で状態を用いて手続き的に書くとすれば、次のようになるだろう。
|
|
int型のリストの全ての要素を1024倍する関数のコード例(C#) |
これがF#では、以下のようになる。
|
|
int型のリストの全ての要素を1024倍する関数のコード例(F#) |
F#では、リストの各要素を別の値に変換するための高階関数として「List.map」が用意されているので、上記のコードではそれを使った。
多くの場合、高階関数の引数として渡す関数はその場で使うだけなので、引数に直接関数を渡したい。これを実現するために、F#ではラムダ式を使う。次のコードはその例である。
|
|
高階関数の引数として渡す関数をラムダ式にした例(F#) | |
「fun x -> x * 1024」の部分がラムダ式。「->」の左辺にある「x」が定義されたパラメータで、右辺にある「x * 1024」が定義された関数内容である。 |
ラムダ式ではない最初のプログラムでは、関数「f」を定義して、それをList.map高階関数に渡していたが、ラムダ式を使ったプログラムでは、f関数の中身をその場で記述している。
なお、C#でもLINQとラムダ式を使って、次のように書くことができる。
|
|
int型のリストの全ての要素を1024倍するのに、LINQとラムダ式を使った例(C#) |
LINQは左から右へとメソッド・チェーンがつながるため、書くのも読むのも楽だが、F#でもパイプライン演算子(=「|>」演算子)を使って左から右へと書き下すことができる(次のコードを参照)。
|
|
パイプライン演算子(=「|>」演算子)を使って左から右へと書き下した場合のコード例(F#) | |
この例では、まず左から「xs」を引数として、その右の「List.map (fun x -> xs * 1024)」関数を呼び出している。 |
もちろん関数呼び出しのチェーンをつなげていくこともでき、例えば「20より大きいものをフィルタして、その値を1024倍して、先頭から10個取得する」というプログラムは、次のように書ける。
|
|
パイプライン演算子(=「|>」演算子)を使って、関数呼び出しのチェーンをつなげた場合のコード例(F#) |
次のページでは、F#でよく使われる2つのデータ構造、「リスト」と「タプル」について説明する。
INDEX | ||
特集:C#開発者のためのF#入門(前編) | ||
F#で初めての関数型プログラミング | ||
1.F#とは | ||
2.関数型プログラミングの基礎 | ||
3.リストとタプル | ||
特集:C#開発者のためのF#入門(後編) | ||
F#言語の基礎文法 | ||
1.主要な文法: if式/letキーワード/レコード | ||
2.主要な文法: 判別共用体/パターン・マッチ | ||
- 第2回 簡潔なコーディングのために (2017/7/26)
ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている - 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう - 第1回 明瞭なコーディングのために (2017/7/19)
C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える - Presentation Translator (2017/7/18)
Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
|
|