open netshop

Chapter 1. 命令型プログラミング

Table of Contents

1. 可変データ
1.1. 参照セル
1.2. 可変レコード
1.3. 参照セルと可変レコードの関係
1.4. 可変データの隠蔽
1.5. 可変トップレベル値、可変ローカル値
1.6. それぞれの可変データの違い
1.7. 練習問題
2. ループ
2.1. 単純forループ
2.2. whileループ
2.3. 列挙forループ
2.4. 練習問題

この章ではF#が備える命令型プログラミングの機能を紹介します。

関数型プログラミングのスタイルは、高い抽象度をもち簡潔な表現ができますが、そのぶん実行効率やメモリ使用量を犠牲にする場合があります。そこで、実行速度やメモリ使用量を改善したい所に対して、部分的に命令型プログラミングのスタイルを使うとそれらを改善できる場合があります。書籍「F# for Scientists」によると、一般的には以下のようなときに命令型プログラミングを併用すると効果的とのことです。

1. 可変データ

ここまで見てきたように、F#は値に対して識別子を束縛するという考えに基づくため、一度束縛した識別子の内容は常に同じ値を指していました。しかし実はF#では、通常の命令型プログラミング言語の変数と同じように変更可能なデータを扱うこともできます。そのようなデータは可変データ(mutable data)とよばれます。それに対して、ここまでに扱ってきたような変化しないデータを不変データ(immutable data)と呼びます。

1.1. 参照セル

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つの操作が定義されています。ひとつは値の取り出しで、もうひとつは値の書き換えです。上の例を見るとわかるように、値の取り出しを行うときは識別子の前に!をつけ、値の書き換えを行うときは:=をつかって値を書き変えます。

1.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

1.3. 参照セルと可変レコードの関係

先ほど紹介した参照セルは、実は内部的には可変レコードを用いて実装されています。それを確かめるために、もう一度参照セルの例を見て見ましょう。

> 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

このように、参照セルは実質的に可変レコードであるため、以降では可変レコードについて言及するときは参照セルのことも含みます。

1.4. 可変データの隠蔽

オブジェクト指向プログラミングでは、カプセル化という概念にしたがい、クラスのフィールド(メンバ変数)を直接外部に公開することは避けます。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)

1.5. 可変トップレベル値、可変ローカル値

今まで説明してきた可変データは、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

可変トップレベル値は、プログラムのトップレベルの直下で定義される可変データです。ここで定義した値は、可変レコードとは異なり静的領域に確保されます。

Note

todo: 記憶域について

次に可変ローカル値の例を示します。

let f () =
  let mutable x = 0

可変ローカル値は、関数の中で一時的に使われる可変データです。これはC#におけるローカル値と全く同一のものであり、この値はスタック領域に確保されます。

Note

todo: 値の寿命について

1.6. それぞれの可変データの違い

ここで、可変トップレベル値/可変ローカル値と可変レコードとの違いについてまとめます。

まず可変データへのアクセス方法ですが、可変レコードと参照セルの場合、可変データが指す値を取り出すときは!xx.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のクロージャを呼び出そうとしてもすでに破棄された値を参照しているので、正常に実行ができなくなってしまいます。

1.7. 練習問題

  1. 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#ではこの例のように参照セルに束縛した識別子を、さらに別の識別子で束縛することは推奨されません。それはなぜでしょうか。

  2. (この練習問題で作ったものは次節の練習問題で利用されます)

    以下のコードは、過去に渡された値の最大値、最小値、平均値、個数などの統計情報を保持するプログラムです。create_initial_stateは統計情報の初期値を生成する関数で、update_stat_immutableは過去の統計情報statと今回渡される値vを引数として受け取り、それらの情報から新たな統計情報を作って返す関数です。これらの関数は、関数型プログラミングのスタイルで書かれていますが、これらを命令型プログラミングのスタイルのコードに書き換えてください。

    まず、stat_immutableを可変レコードとして書き換えたstat_mutableを定義してください。それに伴い、create_initial_stat_immutablecreate_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 } // データ数をインクリメント
    

2. ループ

F#には、C#におけるforwhileforeachに相当する3つの命令型ループ構文が存在します。命令型ループ構文では、通常の命令型プログラミング言語のように、ループのたびにループ変数を書き換えます。しかし何度も述べているように、関数型プログラミングではそのような副作用をなるべく避ける傾向にあるため、ここで紹介する命令型ループ構文は基本的にあまり使われません。その代わりとして、F#では再帰や後で紹介する豊富なリスト処理用の高階関数を多用します。

2.1. 単純forループ

単純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 = ()

2.2. whileループ

whileループ(while loop)は以下の構文をもち、expr_condtrueになるまで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 = ()

2.3. 列挙forループ

列挙forループ(enumerable for loop)は以下の構文をもちます。inのあとにはexpr_seqrange_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 = ()

2.4. 練習問題

  1. 単純forループと参照セルを利用して、1から100までの和を求めるコードを書いてください。

  2. whileループと参照セルを利用して、1から100までの奇数の和を求めるコードを書いてください。

  3. 列挙forループと可変参照せるを利用して、1から100までの奇数の和を求めるコードを書いてください。

  4. 前節の練習問題の2番で作ったcreate_initial_state_mutable関数と命令型ループ構文を使用し、ローカルマシン上の各プロセスのプライベートメモリサイズの統計情報を集計してください。プライベートメモリサイズの一覧を取得する方法は以下のコードを参考に(必要に応じて書き換えて)使ってください。

    // 全プロセスのプライベートメモリ使用量の一覧(intの配列)を取得
    let memsize = System.Diagnostics.Process.GetProcesses() |> Array.map (fun ps -> ps.PrivateMemorySize)
    

inserted by FC2 system