特集:C#開発者のためのF#入門(前編)

F#で初めての関数型プログラミング

bleis-tift
2012/04/12
Page1 Page2 Page3

関数型プログラミングの基礎

 関数型言語でどのようにプログラムを書いていくのかを見ていこう。

不変な値

 関数型言語では極力、「状態」というものを避ける傾向にある。例えば変数が状態を持ってしまうと、その変数を参照する箇所で「この変数の今の値は何だろう?」と注意しなければ、簡単にバグを埋め込んでしまう。だが、「状態を持たずに、どうやってプログラムを書くんだ!」と思う人も多いだろう。

 状態を持たないプログラムに対する抵抗感を減らすために、.NETの文字列について考えてみよう。

 .NETでは、Stringクラス(System名前空間)はインスタンスを作るときに値は確定しており、以後、そのインスタンスの値を変更することはできない(なお、Stringインスタンスに対して文字列結合などの操作をした場合は、新しい別のStringインスタンスが返されている)。つまり、Stringクラスは状態を持っていないのだ。

 このように、実は皆さんはすでに状態を持たないプログラムの世界に少し足を突っ込んでいるのである。この少し足を突っ込んでいる割合を「状態を持たない」方に傾けるだけ、と考えると、少しは抵抗感が和らぐのではないだろうか。

 F#はこの「状態を持たない」方向に傾けたプログラムを書くためのサポートが手厚く、逆に「状態を持った」方向に傾けたプログラムを書くのが面倒になるように文法が構築されている。例えば、F#では変数は「let」キーワードを用いて次のように定義する。

// int型の変数「x」を定義
let x = 42  (* 「x」の値は「42」*)
変数の定義例:変数「x」の値は「42」(F#)
ちなみに、行コメントはC#と同じ形式である。範囲コメントは丸括弧とアスタリスクを組み合わせる。

 しかし、ここで定義した変数「x」の値を変更することはできない。

 関数内であれば、変数「x」を(後から宣言した変数「x」で)シャドウイング(=隠ぺい)して、例えば、

// 対話環境でこのまま入力しても、
// 関数内ではないため、エラーになるので注意

let x = 42
let x = x - 10
変数の値を変更するコード例(F#):シャドウイング
正確に言うと「変数の隠ぺい」であり、「変数の値の変更」ではない(後述)。

とすることで、C#での、

var x = 42;
x = x - 10;
変数の値を変更するコード例(C#):代入

と同じようなことはできるが、完全に同じではない。

 例えばC#で、

var x = 42;
if (x == 42)
{
  x = x - 10;
  System.Console.WriteLine(x);
}
System.Console.WriteLine(x);
「代入」すると「元の変数の値」を書き換えてしまうことを示すコード例(C#)

のようにすると、どちらのコンソール出力も「32」が表示される。

 それに対して、F#で、

let x = 42
if x = 42 then
  let x = x - 10
  System.Console.WriteLine(x)
System.Console.WriteLine(x);;
「シャドウイング」してもスコープを抜ければ「シャドウイングが解ける」ことを示すコード例(F#)

とすると、2番目の出力は「32」ではなく「42」と表示される。

 これは各変数のスコープを考えると分かりやすい。シャドウイングは同じ名前で新しい別の変数を定義するだけなので、if式の内側で定義した変数「x」は、最後の行ではスコープから抜けており、参照できない(C#では、ローカル変数のシャドウイングはできないため、if文の中で新しい変数「x」を再度定義することはできない)。そのため、外側で定義した変数「x」表示されるのである。

 ところで、上のF#コードでは、if式の終了を示すものがないことに気付いただろうか。

 F#では、PythonやHaskellのように、インデントを文法に組み込んでいる。そのため、インデントは非常に重要になってくる。なお本記事では、インデントをASCII文字コードにおける空白2文字で統一している。

値と関数

 F#で「関数」を定義するためには、変数の定義と同じletキーワードを用いる。

// F#では関数名の先頭は小文字を使う
let plus10 x =
  x + 10
letキーワードを用いた関数の定義例(F#)
「=」演算子の後で改行しているが、正しくインデントしていれば、自由に改行できる。

 このF#コードは、C#なら、

public static int Plus10(int x)
{
  return x + 10;
}
上記の関数定義に相当するC#のコード例

というコードに相当する。

 ここで、F#で定義したplus10関数に型を何も書いていない点に注目してほしい。

 plus10関数のパラメータの型は、関数の本体(=「x + 10」というコード)から自動的に推論されるのである。

 この型推論を検証するために、対話環境で、

(+);;

というF#コード(F#では演算子をカッコで囲むことで関数として扱うことができる。詳しくは後で述べる。を実行してみてほしい。すると、次のような結果になったはずだ。

val it : (int -> int -> int) = <fun:it@13>
「(+);;」という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型の値を返す関数」と推論ができるわけである。

 型は明示することもできる。そのためには、例えば次のように、コロン(:)に続けて型名を記述する。

let plus10 (x: int): int = x + 10
引数や戻り値の型の指定例(F#)

 関数の戻り値の型の指定方法には注意する必要がある。

let plus10 x: int = x + 10
戻り値の型の指定例(F#)

 上記のコードはパラメータ「x」の型としてint型を指定しているようにも見えるが、実際は関数の戻り値の型を指定している。

 定義した関数の呼び出しは、C#とは異なり引数を囲むカッコは記述しない(次のコードを参照)。

let plus10 x = x + 10
let ans = plus10 20
System.Console.WriteLine(ans);; // 「30」と表示される
関数呼び出しの例(F#):関数呼び出しの引数にカッコは記述しない

 なお上記のコード例では、System.Console.WriteLineメソッド呼び出しの引数はカッコで囲っているが、これはスタイルの問題で、「F#の外から来ているものにはカッコを付ける」というルールを筆者が自分に課しているからだ。もちろん、このカッコも省略して記述できる。

 関数にパラメータが複数ある場合、関数定義の各パラメータを空白で区切り、関数呼び出しの際にも同様に空白で各引数を区切る。

let plus x y = x + y
let ans = plus 2 3
System.Console.WriteLine(ans);; // 「5」と表示される

// 引数を空白で区切るという仕様のため、
// 関数の結果を引数として直接渡したい場合は、
// 関数呼び出し全体をカッコで囲む必要がある
let ans = plus (plus 10 20) 3
System.Console.WriteLine(ans);; // 「33」と表示される
複数のパラメータがある関数の定義と呼び出しの例(F#)

 F#では関数は値として扱える。

let f x = x + 2
let g = f                       // 変数「g」を関数「f」で初期化
System.Console.WriteLine(g 10);;// 変数「g」を介して関数「f」を呼び出す
関数を値として扱う例(F#)

 変数の定義も関数の定義も、letというキーワードで統一的に定義できるようになっている。しかしそもそも、値と関数の境界線があいまいなのである。

 なお、先ほど「演算子をカッコで囲むことで関数として扱うことができる」と書いた。これはつまり、F#では「演算子も値として扱える」ということを意味している。次のコードはその例である。

let ans = (+) 2 3               // 演算子を関数のように扱う
System.Console.WriteLine(ans);; // 「5」と表示される

let plus = (+)              // 変数「plus」を演算子「+」で初期化
let ans = plus 2 3          // 変数「plus」を介して演算子「+」を使用
System.Console.WriteLine(ans);; // 「5」と表示される
演算子を関数や値として扱う例(F#)

 またF#は、ジェネリック関数も簡単に定義できる。例えば、

let gt x y = x < y
ジェネリック関数の定義例(F#)

とするだけで、C#での、

public static bool Gt<T>(T x, T y) where T : IComparable<T>
{
  return x.CompareTo(y) < 0;
}
ジェネリック関数の定義例(C#)

と同じようなものが定義できる。gt関数の型を推論するときに、比較演算子がジェネリックな演算子として定義されているため、gt関数の型も比較演算子同様、ジェネリックと判断されたのだ。

 これが、例えば(次のコードのように)一方の引数の型が具体的に決定できる場合は、ジェネリックにはならない。

// これはジェネリック関数ではなく、
// int型の値を2つ取り、bool型の値を返す関数
let gt x y =
  let i = x + 0 // ここで+演算子によってxもiもint型に決定
  i < y // 「<」演算子は同じ型どうしの比較を行うため、yもint型に決定
ジェネリックな関数には推論されない例(F#)

 ほかにも例えば、

let id x = x
ジェネリック関数の定義例(F#):値を変更せずに返す、ジェネリックなid関数

というコードは、C#での、

public static T Id<T>(T x) { return x; }
ジェネリック関数の定義例(C#):値を変更せずに返す、ジェネリックなid関数

に対応する(ちなみにid関数はF#の標準ライブラリに定義されている)。

 非ジェネリック関数の場合、C#やVBでもFuncデリゲートとラムダ式を使って値のように関数を扱えるが、ジェネリック関数の場合はメソッドを使わざるを得ないため、F#ほどの統一性はない。次のコードは、C#のコード例だ。

// 非ジェネリック関数ならC#でも値を使って定義できるが……
public static readonly Func<int, int> Plus10 = x => x + 10;

// ジェネリックには対応できない
//public static readonly Func<T, T> Id<T> …… フィールド変数をジェネリックにするのは無理!
C#では、ジェネリック関数を値のようには扱えない

ループと高階関数

 「状態を持たない」スタイルでプログラムを書くためには、C言語風のfor文は使えないことになる。なぜなら、C言語風のfor文はループ・カウンタという「今、何回目のループなのか」を保持する変数が必要になるからだ。

 しかし、ループが使えないと役に立つプログラムは書けそうにない。このジレンマを解決するために、F#ではある種の高階関数を使う。

 高階関数とは、関数を引数に取る関数や、関数を返す関数のことをいう。例えば、次のようなものが高階関数だ。

let twice f x = f (f x)
高階関数の定義例(F#)
この関数定義では、最初に「(f x)」という部分(=「f」は関数で、「x」はそのパラメータ)を実行して<結果>を得て、次に「f <結果>」という部分(=「f」は先ほどと同じ関数で、その引数として「<結果>」が渡される)が実行される。

 このtwice関数は、「x」パラメータに渡された引数の値(以降、x値)に対して、同じく「f」パラメータに引数として渡された関数(以降、f用関数)を2回適用する関数である。この場合、f用関数は、x値と同じ型のパラメータを1つ受け取り、x値と同じ型の戻り値を返すように定義しなければならない。

 実際に実行してみると分かりやすいだろう。次のコードを実行してほしい(このコードでは、f用関数は「plus10」という名前で関数定義している)。

let twice f x = f (f x)
let plus10 x = x + 10
let ans = twice plus10 3        // 「plus10 (plus10 3)」と同じ意味
System.Console.WriteLine(ans);; // 23
高階関数の定義とそれを実行するコードの例(F#)

 F#ではループの代わりとして使える高階関数が数多く提供されている。LINQになじみのある読者であれば、「LINQ to Objectsを使えば、ループ用の構文がなくても問題ない」ということは分かってもらえると思う。

 例として、int型のリストの全ての要素を1024倍する関数を定義してみよう。これをC#で状態を用いて手続き的に書くとすれば、次のようになるだろう。

public static List<int> Mul1024(List<int> xs)
{
  // 結果を格納する変数
  var res = new List<int>();
  // リストの各要素をループで回す
  foreach (var x in xs)
    res.Add(x * 1024);
  return res;
}
int型のリストの全ての要素を1024倍する関数のコード例(C#)

 これがF#では、以下のようになる。

let mul1024 xs =
  let f x = x * 1024
  List.map f xs
int型のリストの全ての要素を1024倍する関数のコード例(F#)

 F#では、リストの各要素を別の値に変換するための高階関数として「List.map」が用意されているので、上記のコードではそれを使った。

 多くの場合、高階関数の引数として渡す関数はその場で使うだけなので、引数に直接関数を渡したい。これを実現するために、F#ではラムダ式を使う。次のコードはその例である。

let mul1024 xs =
  List.map (fun x -> x * 1024) xs
高階関数の引数として渡す関数をラムダ式にした例(F#)
「fun x -> x * 1024」の部分がラムダ式。「->」の左辺にある「x」が定義されたパラメータで、右辺にある「x * 1024」が定義された関数内容である。

 ラムダ式ではない最初のプログラムでは、関数「f」を定義して、それをList.map高階関数に渡していたが、ラムダ式を使ったプログラムでは、f関数の中身をその場で記述している。

 なお、C#でもLINQとラムダ式を使って、次のように書くことができる。

public static List<int> Mul1024(List<int> xs)
{
  return xs.Select(x => x * 1024).ToList();
}
int型のリストの全ての要素を1024倍するのに、LINQとラムダ式を使った例(C#)

 LINQは左から右へとメソッド・チェーンがつながるため、書くのも読むのも楽だが、F#でもパイプライン演算子(=「|>」演算子)を使って左から右へと書き下すことができる(次のコードを参照)。

let mul1024 xs =
  xs |> List.map (fun x -> xs * 1024)
パイプライン演算子(=「|>」演算子)を使って左から右へと書き下した場合のコード例(F#)
この例では、まず左から「xs」を引数として、その右の「List.map (fun x -> xs * 1024)」関数を呼び出している。

 もちろん関数呼び出しのチェーンをつなげていくこともでき、例えば「20より大きいものをフィルタして、その値を1024倍して、先頭から10個取得する」というプログラムは、次のように書ける。

xs
|> List.filter (fun x -> x > 20) // 20より大きいものをフィルタして…
|> List.map (fun x -> x * 1024)  // 1024倍して……
|> Seq.take 10                   // 先頭から10個取得する
パイプライン演算子(=「|>」演算子)を使って、関数呼び出しのチェーンをつなげた場合のコード例(F#)

 次のページでは、F#でよく使われる2つのデータ構造、「リスト」と「タプル」について説明する。


 INDEX
  特集:C#開発者のためのF#入門(前編)
  F#で初めての関数型プログラミング
    1.F#とは
  2.関数型プログラミングの基礎
    3.リストとタプル
 
  特集:C#開発者のためのF#入門(後編)
  F#言語の基礎文法
    1.主要な文法: if式/letキーワード/レコード
    2.主要な文法: 判別共用体/パターン・マッチ


Insider.NET フォーラム 新着記事
  • 第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用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間