M.Hiroi's Home Page

Functional Programming

お気楽 OCaml プログラミング入門

[ PrevPage | OCaml | NextPage ]

レキシカルスコープとクロージャ

変数の有効範囲を表す用語に「スコープ (scope)」があります。この用語を使うと、厳密ではありませんが、変数の有効範囲は「レキシカルスコープ (lexical scope)」と「ダイナミックスコープ (dynamic scope)」の 2 つに分けることができます。伝統的な Lisp はダイナミックスコープですが、現在のプログラミング言語、たとえば、Scheme, Common Lisp, SML/NJ, OCaml はレキシカルスコープを採用しています。

●レキシカルスコープ

それでは、レキシカルスコープについて詳しく見てみましょう。変数 x を表示する関数 foo を定義します。

# let x = "global\n";;
val x : string = "global\n"
# let foo () = print_string x;;
val foo : unit -> unit = <fun>
# foo ();;
global
- : unit = ()

OCaml の場合、引数のない関数は unit を使って定義します。その関数を呼び出すときも、引数として unit を渡します。

関数 foo には局所変数 x を定義していないので、foo を実行した場合は大域変数の値を参照します。その結果 global が表示されます。それでは、foo1 という関数から foo を呼び出す場合を考えてみましょう。foo1 には let で局所変数 x を定義します。この場合、foo はどちらの値を表示するのでしょうか。実際に試してみましょう。

# let foo1 () = let x = "local\n" in foo ();;

... 警告メッセージ ...

val foo1 : unit -> unit = <fun>
# foo1();;
global
- : unit = ()

局所変数 x を使っていないという警告が出ますが気にしないでください。foo1 を実行すると大域変数の値 global を表示しました。このように、foo1 で定義した局所変数 x は、foo から参照することはできません。次の図を見てください。

上図では変数の有効範囲を枠で表しています。foo1 の let で定義した局所変数 x は、let の枠の中でのみ有効です。もしも、この枠で変数が見つからない場合は、ひとつ外側の枠を調べます。順番に外側の枠を調べていくと、最後には関数定義の枠に行き着きます。ここで変数(引数)が見つからない場合は大域変数を調べます。

関数 foo は関数定義の枠しかありません。そこに変数 x が定義されていないので、大域変数を調べることになるのです。このように、関数 foo から foo1 の枠と let の枠を超えて変数 x にアクセスすることはできないのです。これを「レキシカルスコープ」といいます。レキシカルには文脈上いう意味があり、変数が定義されている構造の範囲内 (枠内) でないと、その変数にアクセスすることはできません。

ところが伝統的な Lisp の場合、foo1 で定義した変数 x は呼び出された関数 foo からアクセスすることができます。これを「ダイナミックスコープ」といいます。foo1 で定義された変数 x は、foo1 の実行が終了するまで存在します。そして、foo1 から呼ばれた関数ならば、どこからでも参照することができるのです。もしも、foo1 をダイナミックスコープの処理系、たとえば Emacs Lisp で実行するならば、foo で表示される x の値は local になります。

●レキシカルスコープと局所関数

それでは、関数の中で定義された局所関数や匿名関数の場合はどうなるのでしょうか。次の例を見てください。

リスト 1 : リストの要素を n 倍する

let times_element n xs =
  List.map (fun x -> x * n) xs

匿名関数の引数は x だけなので、変数 n は大域変数を参照するように思われるかもしれません。ところが、変数 n は関数 times_element の引数 n を参照するのです。これを図に示すと、次のようになります。

ポイントは、匿名関数が times_element 内で定義されているところです。変数 n は関数の引数として定義されていて、その有効範囲は関数の終わりまでです。匿名関数はその範囲内に定義されているため、変数 n にアクセスすることができるのです。つまり、関数内で定義された匿名関数は、そのとき有効な局所変数にアクセスすることができるのです。

これは let で定義された局所的な関数も同じです。times_element は次のように書き換えることができます。

リスト 2 : リストの要素を n 倍する

let times_element n xs =
  let timesN x = x * n in
  List.map timesN xs

局所関数 timesN は times_element 内で定義されているので、timesN から times_element の引数 n を参照することができます。

●クロージャ

Lisp などの関数型言語では、関数を生成する関数を簡単に作ることができます。このとき使われる機能が「クロージャ (closure)」です。クロージャは評価する関数と参照可能な局所変数をまとめたものです。クロージャは関数のように実行することができますが、クロージャを生成するときに参照可能な局所変数を保持するところが異なります。参照可能な局所変数の集合を「環境」と呼ぶことがあります。

OCaml の関数はカリー化できるので、関数を返す関数はとても簡単に作成することができます。また、OCaml は関数型言語なので、当然ですがクロージャも使うことができます。OCaml でクロージャを生成するには「匿名関数」を使うか、局所的な関数を定義して、その関数を返します。たとえば、「引数を n 倍する関数」を生成する関数は、匿名関数を使うと次のようになります。

# let foo n = fun x -> n * x;;
val foo : int -> int -> int = <fun>
# let foo10 = foo 10;;
val foo10 : int -> int = <fun>
# foo10 100;;
- : int = 1000
# let foo5 = foo 5;;
val foo5 = int -> int = <fun>
# foo5 11;;
- : int = 55

関数 foo は引数を n 倍する関数を生成します。関数 foo の型 int -> int -> int は int -> (int -> int) を意味しています。これは引数 int を受け取り int -> int という関数を返すことを表しています。変数 foo10 に foo 10 の返り値をセットします。すると、foo10 は引数を 10 倍する関数として使うことができます。同様に、変数 foo5 に foo 5 の返り値をセットすると、foo5 は引数を 5 倍する関数になります。

匿名関数を生成するとき、評価する関数のほかに、そのとき参照可能な局所変数、つまり「環境」もいっしょに保存されます。この場合、参照可能な局所変数は foo の引数 n です。そして、クロージャを実行するときは、保存されている局所変数を参照することができるのです。

foo 10 を実行して無名関数を生成するとき、定義されている局所変数は n で、その値は 10 ですね。この値がクロージャに保存されているので、foo10 の関数は引数を 10 倍した結果を返します。foo 5 を評価すると n の値は 5 で、それがクロージャに保存されているので、foo5 の関数は引数を 5 倍した結果を返すのです。

また、let で局所的な関数を定義して、その関数を返すとクロージャを生成することができます。let を使った例を示します。

# let foo n = let bar x = n * x in bar;;
val foo = int -> int -> int = <fun>
# let foo100 = foo 100;;
val foo100 : int -> int = <fun>
# foo100 11;;
- : int = 1100

let で関数 bar を定義して、bar を返します。すると、foo は「引数を n 倍する関数」を生成する関数になります。

もっとも、OCaml では関数をカリー化するだけで同様の関数を作ることができます。

# let foo n x = n * x;;
val foo : int -> int -> int = <fun>
# let foo10 = foo 10;;
val foo10 : int -> int = <fun>
# foo10 5;;
- : int = 50
# let foo100 = foo 100;;
val foo100 : int -> int = <fun>
# foo100 5;;
- : int = 500

このように、OCaml は関数をカリー化できるので、部分適用により目的の関数を簡単に生成することができます。

●連想リスト

クロージャを理解する場合、環境を「連想リスト (association list : a-list)」で考えるとわかりやすいと思います。ここで簡単に連想リストについて説明します。

連想リストは Lisp でよく用いられるデータ構造で、OCaml ではキーとデータの組を要素とするリストで実現することができます。データ型で記述すると ('a * 'b) list になり、'a がキーで 'b がデータに対応します。

上図の場合、文字列 "a", "b", "c", "d" がキーで、整数 1, 2, 3, 4 がデータとなります。OCaml の List モジュールには連想リストを操作する関数が用意されています。

一般に、関数を呼び出す場合、関数を評価するための環境は空リストです。最初に、引数がこの環境に追加されます。let で定義される局所変数もこの環境に追加されます。もしも、環境に該当する変数が存在しない場合は大域変数を参照します。

たとえば、foo 5 と呼び出すと環境は次のようになります。

foo 5 ==> 環境 : [(n, 5)]

連想リストのキー n が変数名で、その値が 5 です。クロージャを生成するとき、この連想リストを保持すると考えてください。そして、クロージャを評価するときは、保存していた環境を使います。したがって、foo5 11 を評価すると、環境 [(n, 5)] に引数 x の値が追加され、[(x, 11), (n, 5)] になります。この環境で式 n * x を評価するので、5 * 11 = 55 を返すわけです。

関数の評価が終了すると、環境に追加された変数は削除されます。foo5 11 の評価で追加された変数は x なので、(x, 11) が削除され [(n, 5)] になります。このように、クロージャに保存された環境は変化しません。

ただし、Common Lisp や Scheme のように、変数の値を書き換えることができる処理系では、クロージャに保存された変数の値を変更することが可能です。興味のある方は お気楽 Common Lisp プログラミング入門 レキシカルスコープとクロージャ を読んでみてください。

●ダイナミックスコープ

ところで、伝統的な Lisp はレキシカルスコープではなくダイナミックスコープです。ここで簡単にダイナミックスコープについて説明します。もしも、OCaml がダイナミックスコープであれば、最初の例題で示した関数 foo は関数 foo1 から呼び出されると、foo1 の局所変数にアクセスすることができます。これを図に示すと次のようになります。

ダイナミックスコープの場合、局所変数はひとつの連想リストで管理されていると考えてください。この場合、キーが局所変数を表し、データがその値に対応します。局所変数にアクセスするときは、この連想リストから該当する変数を探すのです。見つからない場合は大域変数を検索します。

関数呼び出しや let などで局所変数が定義されたときに、変数とその値が連想リストの先頭へ追加されます。そして、関数や let の評価が終了したときに、連想リストから変数が削除されます。関数が呼び出されるたびに、新しい変数が連想リストに追加されますが、呼び出した側で定義した局所変数も、この連想リストの中に残っています。

たとえば、(1) のように関数 foo を呼び出した場合、関数の引数がなくて局所変数の定義もないので連想リストは空です。ところが、(2) のように関数 foo1 を呼び出した場合、局所変数 x が定義されているので、連想リストに (x, "local") がセットされます。この状態で関数 foo が呼び出されると、連想リストには foo1 で定義した (x, "local") が残っているので、foo では x の値が "local" となるのです。

このように、ダイナミックスコープでは、変数のアクセスは関数を評価する順番に左右されます。したがって、関数の中で定義されていない変数があっても、それがグローバル変数として扱われるとは限らないのです。

ダイナミックスコープで便利なところは、ほかの関数から他の関数の局所変数にアクセスできるところです。そのかわり、プログラムを見ただけでは、どの変数にアクセスしているのかわからないという欠点があります。プログラムの可読性の点でいえば、レキシカルスコープの方が優れていると思います。

●関数の合成

最後に、関数 f(x) と g(x) を合成して新しい関数 h(x) を作ることを考えてみましょう。関数 h(x) を次のように定義します。

h(x) = g( f(x) )

たとえば、f(x) = 2 * x + 1, g(y) = y * y + 3 * y とすると、h(x) は次のようになります。

h(x) = g( f(x) )
     = (2 * x + 1) * (2 * x + 1) + 3 * (2 * x + 1)
     = 4 * x * x + 4 * x + 1 + 6 * x + 3
     = 4 * x * x + 10 * x + 4

実際のプログラムは数式を展開するのではなく、f(x) の評価結果を g(x) に渡すだけなので簡単です。第 1 引数と第 2 引数に関数 f と g を受け取り、それらを第 3 引数に適用する関数 compose は次のようになります。

# let compose f g x = g (f x);;
val compose : ('a -> 'b) -> ('b -> 'c) -> 'a -> 'c = <fun>

関数 f(x) の返り値が関数 g(x) の引数になるので、関数 g(x) が受け付ける値でなければいけません。そうでないと関数を合成することはできません。この条件を関数 compose の型が表しています。関数 f の返り値が型 'b ならば、関数 g の引数も型は 'b でなければいけません。

簡単な例を示しましょう。

# let foo x = 2 * x + 1;;
val foo = int -> int = <fun>
# let bar y = y * y + 3 * y;;
val bar = int -> int = <fun>
# bar (foo 4);;
- : int = 108
# let baz = compose foo bar;;
val baz : int -> int = <fun>
# baz 4;;
- : int = 108

関数 foo と bar を定義します。foo と bar の合成は bar( foo( x ) ) と表すことができます。実際に 4 を計算すると 108 になります。この関数は compose で合成することができます。compose foo bar の返り値を変数 baz に束縛すると、baz を合成関数として使うことができます。

●問題

次の関数を定義してください。

  1. リスト xs から先頭文字が c の文字列を削除する関数 remove_string c xs
  2. リスト xs から n と割り切れる要素を削除する関数 remove_number n xs
  3. リスト xs を反転してから、リスト ys を連結する関数 rev_append xs ys
  4. リスト xs の要素に述語 pred を適用し、一つでも真を返す要素があれば真を返す関数 any pred xs
  5. リスト xs の要素に述語 pred を適用し、全てが真の場合に真を返す関数 every pred xs












●解答

リスト : 解答例

let rec remove_string c xs =
  List.filter (fun s -> s.[0] <> c) xs

let rec remove_number n xs =
  List.filter (fun x -> x mod n <> 0) xs

let rec any pred = function
    [] -> false
  | x::xs when not (pred x) -> any pred xs
  | _ -> true

let rec every pred = function
    [] -> true
  | x::xs when pred x -> every pred xs
  | _ -> false

let rec rev_append xs ys =
  match xs with
    [] -> ys
  | x::xs1 -> rev_append xs1 (x::ys)
val remove_string : char -> string list -> string list = <fun>
val remove_number : int -> int list -> int list = <fun>
val any : ('a -> bool) -> 'a list -> bool = <fun>
val every : ('a -> bool) -> 'a list -> bool = <fun>
val rev_append : 'a list -> 'a list -> 'a list = <fun>
# remove_string 'A' ["abc"; "ABC"; "def"];;
- : string list = ["abc"; "def"]
# remove_string 'A' ["Abc"; "ABC"; "def"];;
- : string list = ["def"]
# remove_string 'A' ["abc"; "aBC"; "def"];;
- : string list = ["abc"; "aBC"; "def"]

# remove_number 2 [1; 2; 3; 4; 5; 6; 7];;
- : int list = [1; 3; 5; 7]
# remove_number 3 [1; 2; 3; 4; 5; 6; 7];;
- : int list = [1; 2; 4; 5; 7]
# remove_number 8 [1; 2; 3; 4; 5; 6; 7];;
- : int list = [1; 2; 3; 4; 5; 6; 7]

# any (fun x -> x mod 2 = 0) [1; 2; 3; 5; 7; 9];;
- : bool = true
# any (fun x -> x mod 2 = 0) [1; 3; 5; 7; 9];;
- : bool = false

# every (fun x -> x mod 2 = 0) [2; 4; 6; 8; 10];;
- : bool = true
# every (fun x -> x mod 2 = 0) [2; 4; 5; 8; 10];;
- : bool = false

# rev_append [1; 2; 3] [4; 5; 6];;
- : int list = [3; 2; 1; 4; 5; 6]
# rev_append [] [4; 5; 6];;
- : int list = [4; 5; 6]

初版 2008 年 6 月 21 日
改訂 2020 年 6 月 28 日

順列と組み合わせ

今回は簡単な例題として、「順列 (permutation)」と「組み合わせ (combination)」を取り上げます。

●順列の生成

異なる n 個の順列の総数は、n の階乗 (n!) だけあります。たとえば、3 つの整数 1, 2, 3 の順列は次に示すように 6 通りあります。

1 2 3,  1 3 2,  2 1 3,  2 3 1,  3 1 2,  3 2 1

順列を生成するプログラムは再帰定義で簡単に作ることができます。[1; 2; 3] の順列を生成する場合、最初に 1 で始まる順列を生成します。これは 1 を取り除いた数字 [2; 3] の順列を生成することで実現できます。次は 2 で始まる順列を生成します。同様に、2 を取り除いた数字 [1; 3] の順列を生成すればいいわけです。[2; 3] や [1; 3] の順列を生成する場合も同じように考えることができます。

ここでは、リストの中から n 個の要素を取り出す順列を求めます。プログラムは次のようになります。

リスト 3 : 順列の生成 (1)

(* int list の表示 *)
let print_intlist xs =
  List.iter (fun x -> print_int x; print_string " ") xs;
  print_newline ()

(* リストから要素を削除する *)
let rec remove x = function
  [] -> []
| y :: ys -> if x = y then remove x ys else y :: remove x ys

(* 順列の生成 *)
let permutation n xs f = 
  let rec perm n xs a =
    if n = 0 then f (List.rev a)
    else List.iter (fun x -> perm (n - 1) (remove x xs) (x :: a)) xs
  in
    perm n xs []

関数 permutation は高階関数で、引数 n が選ぶ要素の個数、引数 xs がリスト、引数 f が生成した順列に適用する関数です。たとえば、順列が数字のリストであれば、int list を表示する関数 print_intlist を渡すことで、生成した順列を表示することができます。実際の処理は局所関数 perm で行います。

perm の引数 n が選ぶ要素の個数、引数 xs がリストです。引数 a は累積変数で、選んだ数字を格納するリストです。n が 0 の場合、順列がひとつ完成したので List.rev で a を逆順にして関数 f に渡します。選んだ数字は a の先頭に追加していくので、逆順になることに注意してください。

n が 0 でなければ、数字を一つ選んで perm を再帰呼び出しします。数字の選択はリストの先頭から順番に行えばいいので、高階関数 List.iter を使っています。iter の定義をリスト 4 に示します

リスト 4 : 高階関数 iter の定義

let rec iter f = function
  [] -> ()
| x :: xs -> f x; iter f xs

iter はリストの要素に関数 f を適用します。その結果は捨てられることに注意してください。つまり、副作用を目的とした関数なのです。iter の返り値は unit になります。

匿名関数でリストの要素 x を受け取り、この中で prem を再帰呼び出しします。perm の第 1 引数は n - 1、第 2 引数は xs から x を削除したリスト、第 3 引数は選択した数字のリスト a に x を追加したものです。これで数字 x を選択したことになります。x の削除は関数 remove で行います。

関数 remove はリストから x を探し、見つけた x をすべて削除します。List.filter を使う場合は、次のようになります。

リスト 5 : 要素の削除

let remove x xs = List.filter (fun y -> x <> y) xs

関数 print_intlist は int list を表示します。List.iter を使って print_int で要素を表示します。print_newline は改行を出力する関数です。

それでは、実際に試してみましょう。

# permutation 3 [1; 2; 3] print_intlist;;
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
- : unit = ()

正常に動作していますね。

●順列をリストに格納する

生成した順列をリストに格納して返す場合は、List.fold_right を使うと簡単です。プログラムは次のようになります。

リスト 6 : 順列の生成 (2)

let permutation_list n xs =
  let rec perm n xs a b =
    if n = 0 then a::b
    else List.fold_right (fun x y -> perm (n-1) (remove x xs) (x::a) y) xs b
  in
    perm n xs [] []

局所関数 perm は permutation の局所関数を改造したもので、生成した順列を第 4 引数 b のリストに格納します。perm は順列を格納したリスト (第 4 引数) をそのまま返します。perm を呼び出す場合、この返り値を第 4 引数に渡すことで、生成した順列を格納していくことができます。

List.fold_right の初期値 (第 3 引数) に引数 b を渡すことで、匿名関数の第 2 引数 y に順列を格納するリストを渡します。あとは perm を再帰呼び出しすると、その返り値は次に匿名関数を呼び出すときの引数 y に渡されるので、順列を格納したリストを perm に渡していくことができます。

それでは実行結果を示します。

val permutation_list : 'a list -> 'a list list = <fun>
# permutation_list 3 [1; 2; 3];;
- : int list list = [[1; 2; 3]; [1; 3; 2]; [2; 1; 3];
 [2; 3; 1]; [3; 1; 2]; [3; 2; 1]]

正常に動作していますね。

●組み合わせの生成

次は「組み合わせ (combination)」を生成するプログラムを作ってみましょう。たとえば、リスト [1; 2; 3; 4; 5] の中から 3 個を選ぶ組み合わせは次のようになります。

1 2 3, 1 2 4, 1 2 5, 1 3 4, 1 3 5, 1 4 5
2 3 4, 2 3 5, 2 4 5, 3 4 5

最初に 1 を選択した場合、次は [2; 3; 4; 5] の中から 2 個を選べばいいですね。2 番目に 2 を選択したら、次は [3; 4; 5] の中から 1 個を選べばいいわけです。これで、[1; 2; 3], [1; 2; 4], [1; 2; 5] が生成されます。[2; 3; 4; 5] の中から 2 個選ぶとき、2 を選ばない場合があります。この場合は [3; 4; 5] の中から 2 個を選べばいいわけです。ここで 3 を選ぶと [1; 3; 4], [1; 3; 5] が生成できます。同様に、3 を除いた [4; 5] の中から 2 個をえらぶと [1; 4; 5] を生成することができます。

これで 1 を含む組み合わせを生成したので、次は 1 を含まない組み合わせ、つまり [2; 3; 4; 5] から 3 個を選ぶ組み合わせを生成すればいいわけです。けっきょく、この処理の考え方は次に示す組み合わせの公式と同じです。

\( {}_n \mathrm{C}_r = \begin{cases} 1 & if \ r = 0 \\ 1 & if \ r = n \\ {}_{n-1} \mathrm{C}_{r-1} + {}_{n-1} \mathrm{C}_r \quad & if \ r \gt 0 \end{cases} \)

これをプログラムすると次のようになります。

リスト 7 : 組み合わせの生成

let combination r xs f =
  let rec comb r xs a =
    match (r, xs) with
      (0, _) -> f (List.rev a)
    | (_, []) -> ()
    | (_, y::ys) -> comb (r - 1) ys (y::a); comb r ys a
  in
    comb r xs []
val combination : int -> 'a list -> ('a list -> unit) -> unit = <fun>

局所関数 comb は引数 xs のリストから r 個を選ぶ組み合わせを生成します。選んだ数字は引数 a に格納します。r が 0 になったら組み合わせを一つ生成できたので、a を List.rev で逆順にして関数 f を呼び出します。xs が空リストならば何もしないで unit を返します。この 2 つの条件が再帰呼び出しの停止条件になります。

最後の節は xs が空リストでない場合です。ここで comb を再帰呼び出しします。最初の呼び出しは先頭の要素を選択する場合です。先頭要素を a に追加して、リスト ys の中から r - 1 個を選びます。最後の呼び出しが先頭の要素を選ばない場合です。リスト ys の中から r 個を選びます。

簡単な実行例を示します。

# combination 3 [1; 2; 3; 4; 5] print_intlist;;
1 2 3
1 2 4
1 2 5
1 3 4
1 3 5
1 4 5
2 3 4
2 3 5
2 4 5
3 4 5
- : unit = ()

正常に動作していますね。

●組み合わせをリストに格納する

生成した組み合わせをリストに格納して返す場合も簡単です。プログラムは次のようになります。

リスト 8 : 組み合わせの生成 (2)

let combination_list r xs =
  let rec comb r xs a b =
    match (r, xs) with
      (0, _) -> (List.rev a) :: b
    | (_, []) -> b
    | (_, y::ys) -> comb (r - 1) ys (y::a) (comb r ys a b)
  in
    comb r xs [] []
val combination_list : int -> 'a list -> 'a list list = <fun>

局所関数 comb は生成した組み合わせ (引数 a) を引数 b のリストに格納し、それをそのまま返します。comb を呼び出す場合、この返り値を引数 b に渡すことで、生成した組み合わせを格納していくことができます。

具体的には、comb を再帰呼び出しするところで、1 回目の呼び出しの返り値を 2 回目の呼び出しの第 4 引数に渡します。それから、引数 xs が空リストのときは引数 b をそのまま返します。これで生成した組み合わせをリストに格納することができます。

それでは実行結果を示します。

# combination_list 3 [1; 2; 3; 4; 5];; 
- : int list list = [[1; 2; 3]; [1; 2; 4]; [1; 2; 5]; [1; 3; 4]; [1; 3; 5];
[1; 4; 5]; [2; 3; 4]; [2; 3; 5]; [2; 4; 5]; [3; 4; 5]]

正常に動作していますね。

●問題

次の関数を定義してください。

  1. リスト xs を平坦化する関数 flatten xs
  2. マッピングの結果を平坦化する関数 flatmap f xs
  3. リスト xs から要素を一つ選び、その要素と残りの要素を返す関数 select xs
  4. flatmap と select を使って順列を生成する関数 permutation_list n xs を書き直してください
  5. m 個の整数 (0, 1, 2, ..., m - 1) の「完全順列 (derangement)」を生成する高階関数 derangement fn m












●解答

リスト : 解答例

let rec flatten = function
    [] -> []
  | x::xs -> x @ flatten xs

let flatmap f xs = flatten (List.map f xs)

(* 別解 *)
let rec flatmap1 f = function
    [] -> []
  | x::xs -> f x @ flatmap1 f xs

let rec select = function
    [] -> []
  | [x] -> [(x, [])]
  | x::xs -> (x, xs) :: List.map (fun (y, ys) -> (y, x::ys)) (select xs)

let print_intlist xs =
  List.iter (fun x -> print_int x; print_string " ") xs;
  print_newline ()

let rec permutation_list n xs =
  if n = 0 then [[]]
  else flatmap
         (fun (y, ys) -> List.map (fun zs -> y::zs) (permutation_list (n - 1) ys))
         (select xs)

let rec remove x xs = List.filter (fun y -> x <> y) xs

let rec iota n m =
  if n > m then []
  else n :: iota (n + 1) m

let derangement fn m =
  let rec perm n xs a =
    if xs = [] then fn (List.rev a)
    else List.iter (fun x -> if x = n then () else perm (n + 1) (remove x xs) (x::a)) xs
  in perm 0 (iota 0 (m - 1)) []
# flatten [[1; 2; 3]; [4; 5; 6]; [7; 8; 9]];;
- : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9]

# List.map (fun x -> [x;x;x]) [1; 2; 3; 4];;
- : int list list = [[1; 1; 1]; [2; 2; 2]; [3; 3; 3]; [4; 4; 4]]
# flatmap (fun x -> [x;x;x]) [1;2;3;4];;
- : int list = [1; 1; 1; 2; 2; 2; 3; 3; 3; 4; 4; 4]

# select [1; 2; 3; 4];;
- : (int * int list) list =
[(1, [2; 3; 4]); (2, [1; 3; 4]); (3, [1; 2; 4]); (4, [1; 2; 3])]

# permutation_list 4 [1; 2; 3; 4];;
- : int list list =
[[1; 2; 3; 4]; [1; 2; 4; 3]; [1; 3; 2; 4]; [1; 3; 4; 2]; [1; 4; 2; 3];
 [1; 4; 3; 2]; [2; 1; 3; 4]; [2; 1; 4; 3]; [2; 3; 1; 4]; [2; 3; 4; 1];
 [2; 4; 1; 3]; [2; 4; 3; 1]; [3; 1; 2; 4]; [3; 1; 4; 2]; [3; 2; 1; 4];
 [3; 2; 4; 1]; [3; 4; 1; 2]; [3; 4; 2; 1]; [4; 1; 2; 3]; [4; 1; 3; 2];
 [4; 2; 1; 3]; [4; 2; 3; 1]; [4; 3; 1; 2]; [4; 3; 2; 1]]

# derangement print_intlist 3;;
1 2 0
2 0 1
- : unit = ()
# derangement print_intlist 4;;
1 0 3 2
1 2 3 0
1 3 0 2
2 0 3 1
2 3 0 1
2 3 1 0
3 0 1 2
3 2 0 1
3 2 1 0
- : unit = ()

初版 2008 年 6 月 21 日
改訂 2020 年 6 月 28 日

Copyright (C) 2008-2020 Makoto Hiroi
All rights reserved.

[ PrevPage | OCaml | NextPage ]