Table of Contents
この章ではF#が備える命令型プログラミングの機能を紹介します。
関数型プログラミングのスタイルは、高い抽象度をもち簡潔な表現ができますが、そのぶん実行効率やメモリ使用量を犠牲にする場合があります。そこで、実行速度やメモリ使用量を改善したい所に対して、部分的に命令型プログラミングのスタイルを使うとそれらを改善できる場合があります。書籍「F# for Scientists」によると、一般的には以下のようなときに命令型プログラミングを併用すると効果的とのことです。
非常に大量のデータや複雑なデータを処理するとき
高い実行効率が要求されるとき
副作用を多くともなう.NET FrameworkのGUIや入出力まわりを扱うとき
命令型スタイルで書いたほうが簡潔に表現できるとき
ここまで見てきたように、F#は値に対して識別子を束縛するという考えに基づくため、一度束縛した識別子の内容は常に同じ値を指していました。しかし実はF#では、通常の命令型プログラミング言語の変数と同じように変更可能なデータを扱うこともできます。そのようなデータは可変データ(mutable data)とよばれます。それに対して、ここまでに扱ってきたような変化しないデータを不変データ(immutable data)と呼びます。
F#においてよく使われる可変データは、参照セル(reference cell)と呼ばれるものです。使い方はC言語のポインタと似ています。以下に使い方の例を示します。
> let x = ref 3;; // 参照セルを作る val x : int ref = {contents = 3;} > x;; // xは3という値を指し示すポインタ val it : int ref = {contents = 3;} > !x;; // xが指す値を取り出す val it : int = 3 > x := 5;; // xが指す値を書き換える val it : unit = () > x;; // xは5という値を指し示すポインタ val it : int ref = {contents = 5;} > !x;; // xが指す値を取り出す val it : int = 5
参照セルを定義するには、値にref
キーワードをつけます。上の例では、3という値がヒープ領域に確保され、x
はその領域を指すポインタのようなものになります。2番目の入力に対する応答は、x
が指し示す先に3という値が格納されていることを示しています。
参照セルには、2つの操作が定義されています。ひとつは値の取り出しで、もうひとつは値の書き換えです。上の例を見るとわかるように、値の取り出しを行うときは識別子の前に!
をつけ、値の書き換えを行うときは:=
をつかって値を書き変えます。
可変レコード(mutable
record)とは、ひとつ以上のフィールドに対してmutable
が指定されているレコードです。
> type person = { name : string; mutable age : int; };; // 可変レコードの型を定義 type person = {name: string; mutable age: int;} > let x = { name="taro"; age=0 };; // 可変レコードのインスタンスを作成 val x : person
可変レコードは通常のレコードと同じように扱うことができますが、それに加えてmutable
指定されたフィールドの書き換えができます。フィールドの値の書き換えを行うには、<-
演算子を使います。
> { name="taro"; age=0 };; val it : person = {name = "taro"; age = 0;} > let x = { name="taro"; age=0 };; val x : person > x.age;; // ageフィールドへアクセス val it : int = 0 > x.age <- 1;; // ageフィールドを書き換え val it : unit = () > x;; // ageフィールドが更新されてていることを確認 val it : person = {name = "taro"; age = 1;} > x.name <- "hanako";; // mutableでないフィールドは書き換えられない x.name <- "hanako";; ^^^^^^^^^^^^^^^^^^^ stdin(63,1): error FS0005: This field is not mutable
先ほど紹介した参照セルは、実は内部的には可変レコードを用いて実装されています。それを確かめるために、もう一度参照セルの例を見て見ましょう。
> let x = ref 3;; // 参照セルを作る val x : int ref > x;; val it : int ref = {contents = 3;} > !x;; // xが指す値を取り出す val it : int = 3 > x := 5;; // xが指す値を書き換える val it : unit = ()
これを見てわかるとおり、参照セルはcontents
というフィールドをもつ可変レコードであることがわかります。値を取り出す!
演算子と、値の書き換えをする:=
演算子は単なる糖衣構文にすぎず、以下のように直接contents
フィールドにアクセスすることもできます。
> let x = ref 3;; val x : int ref > x;; val it : int ref = {contents = 3;} > !x;; // 値の取り出し val it : int = 3 > x.contents;; // 値の取り出し(直接フィールドへアクセス) val it : int = 3 > x := 5;; // 値の書き換え val it : unit = () > !x;; // 書き換わったことを確認 val it : int = 5 > x.contents <- 10;; // 値の書き換え(直接フィールドへアクセス) val it : unit = () > !x;; // 書き換わったことを確認 val it : int = 10
このように、参照セルは実質的に可変レコードであるため、以降では可変レコードについて言及するときは参照セルのことも含みます。
オブジェクト指向プログラミングでは、カプセル化という概念にしたがい、クラスのフィールド(メンバ変数)を直接外部に公開することは避けます。C#では、必ずプロパティを通じて公開しますし、他の言語でも必ずをアクセス専用の関数(get~/set~)を通して公開します。これは変数へのアクセスを一箇所で集中管理することで、変更に対して柔軟にしたり、思わぬ場所からの変更を防止したりすることを目的としています。
関数型プログラミングのスタイルでは、基本的に一度束縛した値は不変であって、識別子が示す値が変わりません。ところが、ここで紹介した可変データの場合、例外的に識別子が指す値が変わる可能性があります。
そこで関数型プログラミングでは、可変データに対してオブジェクト指向でいうカプセル化の概念を適用し、外部からはアクセス用関数のみを通じてアクセスさせるようにします。
> let count_up = let counter = ref 0 // 可変データを作成 let counting_func () = // アクセサ関数(クロージャ)を作る counter := !counter + 1 !counter counting_func;; // 今作った関数値を返す val count_up : (unit -> int) > count_up();; val it : int = 1 > count_up();; val it : int = 2 > count_up();; val it : int = 3
1番目の入力ではcounting_func
という関数値を作ってcount_up
という識別子を束縛していますが、その関数値はローカルな識別子を参照しているためクロージャとなります。count_up
クロージャは、呼び出されるたびに内部のカウンタを増加し、そのカウンタ値を戻り値として返します。この実装を詳しく見てみましょう。
まずref
0
によって可変データを作成して、counter
に束縛します。次に「カウンタ値を増やしてその値を返す」という関数counting_func
の関数値(クロージャ)を作成します。このcounter
が指している可変データは、このクロージャの外部からは参照されていない、すなわちこのクロージャを通して間接的にしかアクセスできません。F#ではこのようにして、クロージャによって可変データのカプセル化するという手法がよく使用されます。
ちなみにcounting_up
の定義は、ラムダ式を使うともう少し短く記述できます。
let count_up = let counter = ref 0 // 可変データを作成 (fun () -> // アクセサ関数をラムダ式によって作成する counter := !counter + 1 !counter)
今まで説明してきた可変データは、C#でいう参照型の値に相当し、どれもヒープ領域上に確保されてガーベッジコレクタにより回収されます。しかし、可変トップレベル値(mutable top-level value)および可変ローカル値(mutable local value)というヒープ領域に確保されない可変データを作ることもできます。まずは可変トップレベル値の例を示します。
> let mutable x = 0;; // 可変トップレベル値の定義 val mutable x : int > x;; // 値の取り出し val it : int = 0 > x <- 1;; // 値の書き換え val it : unit = () > x;; // 値の取り出し val it : int = 1
可変トップレベル値は、プログラムのトップレベルの直下で定義される可変データです。ここで定義した値は、可変レコードとは異なり静的領域に確保されます。
todo: 記憶域について
次に可変ローカル値の例を示します。
let f () = let mutable x = 0
可変ローカル値は、関数の中で一時的に使われる可変データです。これはC#におけるローカル値と全く同一のものであり、この値はスタック領域に確保されます。
todo: 値の寿命について
ここで、可変トップレベル値/可変ローカル値と可変レコードとの違いについてまとめます。
まず可変データへのアクセス方法ですが、可変レコードと参照セルの場合、可変データが指す値を取り出すときは!x
やx.contents
のように何らかの修飾が必要です。単にx
と書くだけでは値そのものへアクセスできません。このように、ある参照が指している値へアクセスすることを、参照はがし(dereference)といいます。
一方、可変トップレベル値/可変ローカル値では、値を取り出すときは直接x
と書くだけで特に修飾は必要ありません。
次の大きな違いは、それらが確保される記憶域です。可変レコードはヒープ領域に確保され、可変トップレベル値と可変ローカル値はそれぞれ静的領域とスタック領域にされます。クロージャをつくるときには、この違いに注意しなければなりません。
クロージャは関数値とそれが参照している識別子の情報のセットです。クロージャは呼び出される毎にその識別子の参照先を見にいきますが、その参照先がすでに破棄されていると正常に実行できなくなってしまいます。そこでF#では、クロージャ内では可変ローカル値を使うことを禁止しています。
> let mutable top_level = 0;; // 可変トップレベル値を作成 val mutable top_level : int > let f1 = (fun () -> printfn "%d" top_level);; // 可変トップレベル値を使用したクロージャ val f1 : (unit -> unit) > let var_ref_cell = ref 0;; // 参照セルを作成 val var_ref_cell : int ref > let f2 = (fun () -> printfn "%d" !var_ref_cell);; // 参照セルを使用したクロージャ val f2 : (unit -> unit) > let f3 = let mutable local = 0 // 可変ローカル値を作成 (fun () -> printfn "%d" local);; // 可変ローカル値を使用したクロージャ(エラーになる) (fun () -> printfn "%d" local);; -------------^^^^^^^^^^^^^^^^^^^ stdin(5,14): error FS0191: The mutable variable 'local' is used in an invalid way. Mutable variables may not be captured by closures. Consider eliminating this use of mutation or using a heap-allocated mutable reference cell via 'ref' and '!'.
上の例では、f3
に可変ローカル値を使用するクロージャを束縛しようとしてエラーになっています。もし、これを許可していたらどうなるか考えてみましょう。まずlocal
にスタック上に確保した可変ローカル値を束縛します。次に、それを参照するクロージャを作成します。そしてそのクロージャを値(関数値)として返してf3
に束縛します。ところがそれと同時に、local
はスタックから破棄されます。そして、あとでf3
のクロージャを呼び出そうとしてもすでに破棄された値を参照しているので、正常に実行ができなくなってしまいます。
F#インタプリタに対して、以下に示すような一連の入力を与えました。この直後に!y
という入力を与えると、F#インタプリタからはどのような応答が返ってくるでしょうか。
> x;; val it : int ref = {contents = 0;} > !x;; val it : int = 0 > !y;; val it : int = 0 > x := 10;; val it : unit = ()
F#ではこの例のように参照セルに束縛した識別子を、さらに別の識別子で束縛することは推奨されません。それはなぜでしょうか。
(この練習問題で作ったものは次節の練習問題で利用されます)
以下のコードは、過去に渡された値の最大値、最小値、平均値、個数などの統計情報を保持するプログラムです。create_initial_state
は統計情報の初期値を生成する関数で、update_stat_immutable
は過去の統計情報stat
と今回渡される値v
を引数として受け取り、それらの情報から新たな統計情報を作って返す関数です。これらの関数は、関数型プログラミングのスタイルで書かれていますが、これらを命令型プログラミングのスタイルのコードに書き換えてください。
まず、stat_immutable
を可変レコードとして書き換えたstat_mutable
を定義してください。それに伴い、create_initial_stat_immutable
をcreate_initial_state_mutable
に書き換え、
update_stat_immutable : stat_immutable -> float ->
stat_immutable
を命令型バージョンupdate_stat_mutable :
stat_mutable -> float ->
unit
として書き換えてください。すなわち、update_stat_immutable
が新たな統計情報として毎回新たなレコードを返すのに対して、update_mutable
は新たなレコードの生成をするのではなく引数として受け取ったレコードを書き換えるようにしてください。
type stat_immutable = { Max : float Min : float Avg : float Count : float } let create_initial_stat_immutable() = { Max = Double.MinValue; Min = Double.MaxValue; Avg = 0.0; Count = 0.0; } let update_stat_immutable stat v = { Max = max stat.Max v; // 大きいほうを新たな最大値として採用 Min = min stat.Min v; // 小さいほうを新たな最小値として採用 Avg = (stat.Avg * stat.Count + v) / (stat.Count + 1.0); // 新たな平均値を計算 Count = stat.Count + 1.0 } // データ数をインクリメント
F#には、C#におけるfor
、while
、foreach
に相当する3つの命令型ループ構文が存在します。命令型ループ構文では、通常の命令型プログラミング言語のように、ループのたびにループ変数を書き換えます。しかし何度も述べているように、関数型プログラミングではそのような副作用をなるべく避ける傾向にあるため、ここで紹介する命令型ループ構文は基本的にあまり使われません。その代わりとして、F#では再帰や後で紹介する豊富なリスト処理用の高階関数を多用します。
単純forループ(simple for
loop)は以下の構文をもち、ident
をループ変数としてexpr1
からexpr2
までの整数値を、毎回1ずつ増加させながらループします。そしてループのたびにexpr
を評価しますが、expr
は最終的にunit
型となる式でなければなりません。
単純forループでは、to
の代わりにdownto
と書くとexpr1
からexpr2
までの整数値を毎回1減らしながらループします。ちなみに軽量構文では一番最後のdone
を省略して、インデントで代用することができます。
for ident
=
expr1
to expr2
do
expr
done
> for i = 1 to 5 do
printfn "%d" i;;
1
2
3
4
5
val it : unit = ()
whileループ(while
loop)は以下の構文をもち、expr_cond
がtrue
になるまでexpr
を繰り返します。単純forループと同様、expr
は最終的にunit
型となる式でなければなりません。軽量構文では一番最後のdone
を省略して、インデントで代用することができます。
while expr_cond
do
expr
done
> let i = ref 0;; val i : int ref > while (!i < 5) do i := !i + 1 printfn "%d" !i;; 1 2 3 4 5 val it : unit = ()
列挙forループ(enumerable for
loop)は以下の構文をもちます。in
のあとにはexpr_seq
かrange_expr
を書くことができます。
for pat
in
(expr_seq
|
range_expr
) do
expr
done
expr_seq
にはIEnumerable
およびIEnumerable<T>
を実装したコレクション型の値を渡すことができます。この場合、C#のforeach文と同じようにループのたびに要素が1つずつ渡されます。ちなみに、後のリスト処理のところで詳しく説明しますが、F#ではIEnumerable<T>
インターフェイスに対してseq<'a>
という別名を与えられています。F#のリストや配列などのコレクション型はseq<'a>
を実装しているため、このexpr_seq
に渡すことができます。
range_expr
にはリストや配列の説明のときに紹介した範囲式を書くことができます。範囲式は値の範囲を表現する構文で、以下の構文をもちます。
expr_begin
..
expr_end
expr_begin
..
expr_skip
..
expr_end
これらの範囲式が与えられた場合、カウンタ変数をexpr_begin
からexpr_end
まで増加させながらループをします。上の構文の場合はカウンタ変数が1ずつ増加しますが、下の構文の場合はexpr_skip
ずつ増加します。expr
は他の命令型ループ構文と同様、最終的にunit
型となる式でなければなりません。
> for x in ["hello"; "world!"] do // F#のリストは暗黙的にseq<'a>を実装する printfn "%s" x;; hello world! val it : unit = () > for i in [1..2..10] do // 1から10まで2ずつ増加しながらループをする printfn "%d" i;; 1 3 5 7 9 val it : unit = ()
単純forループと参照セルを利用して、1から100までの和を求めるコードを書いてください。
whileループと参照セルを利用して、1から100までの奇数の和を求めるコードを書いてください。
列挙forループと可変参照せるを利用して、1から100までの奇数の和を求めるコードを書いてください。
前節の練習問題の2番で作ったcreate_initial_state_mutable
関数と命令型ループ構文を使用し、ローカルマシン上の各プロセスのプライベートメモリサイズの統計情報を集計してください。プライベートメモリサイズの一覧を取得する方法は以下のコードを参考に(必要に応じて書き換えて)使ってください。
// 全プロセスのプライベートメモリ使用量の一覧(intの配列)を取得 let memsize = System.Diagnostics.Process.GetProcesses() |> Array.map (fun ps -> ps.PrivateMemorySize)