(Redirected from ja/the_structure_of_ocaml_programs)
さて、ここからは、俯瞰的に、実際のOCamlプログラムを見ていくことにしよう。ここで述べるのは、ローカル&グローバルな定義、;;
と;
の使い分け、モジュール、入れ子関数、そして参照についてである。進むにつれ、今まで見たことがなくて意味のわからないOCamlの概念が、たくさん出てくるだろう。今はつぶさにわからなくてもよい。そのかわり、大まかな形でプログラムを見ることと、説明の主眼点をつかむことに、集中してほしい。
では、average
関数にローカルな変数を加えたものを、Cで見てみよう。(先にあげた、最初の定義と比べてみよ)
double average (double a, double b) { double sum = a + b; return sum / 2; }
同じことを、OCamlでやってみよう。
let average a b = let sum = a +. b in sum /. 2.0;;
標準の句のlet name = expression in
が、ローカルな式に名前をつけて定義するのに使われている。これでname
は、後で関数のなかで、expression
の代わりに使えるようになる。式の終わりの;;
で、コードのブロックが閉じる。inのあとでインデントをしないのに注意。これは、let ... in
はひとつの文である、と考えること。
では比べよう。Cのローカルな変数と、これらの名前をつけられたローカルな式は、ぱっと見ると一緒だが、実はちょっと違う。Cの変数sum
は、スタック上に領域が確保されている。後で関数のなかで、sum
に代入もできるし、sum
のアドレスを得たりもできる。OCamlでは、そうはいかない。OCamlでは、sum
は式a +. b
の略名にすぎない。sum
に、代入は全くできないし、その値を変えることもだめである。(本当の変数をどうやるか、はすぐに明らかになる。)
より分かりやすくするための、もうひとつの例。以下の2つのコードは、同じ値を返すわけだが(すなわち (a+b) + (a+b)2):
let f a b = (a +. b) +. (a +. b) ** 2. ;;
let f a b = let x = a +. b in x +. x ** 2. ;;
2つめのほうがたぶん速い(しかし、ほとんどのコンパイラは、この処置を"共通部分式の削除"でしてくれるはずだ)、それに、明らかに読みやすい。2つめの例のx
はa +. b
の略にすぎない。
グローバルな名前の定義は、トップレベルで行われる。さきほどのローカルな"変数"と同じく、 これらは本当の変数ではまるでなく、略名にすぎない。 この例は、実際にあったものだ(省略をしたが)。
let html = let content = read_whole_file file in GHtml.html_from_string content ;;
let menu_bold () = match bold_button#active with true -> html#set_font_style ~enable:[`BOLD] () | false -> html#set_font_style ~disable:[`BOLD] () ;;
let main () = (* code omitted *) factory#add_item "Cut" ~key:_X ~callback: html#cut ;;
この実際にあったコードで、html
は、HTML作成ウィジェット(lablgtkライブラリのオブジェクト)であり、プログラムの冒頭で、一度だけ作られている。それが最初のlet html =
文である。それから、後の関数で2、3度参照されている。
気をつけてほしいのは、上のコードのhtml
はあくまで名前で、Cでいうようなグローバルな変数と、対応していないことだ。あるいは、他の手続き型言語のそれともちがう。領域を確保しておらず、"html
ポインタ"を"格納"していないのだ。html
への代入はできないので、例えば、再代入で他のウィジェットへのポインタになったりもしない。次の節で、参照について説明する。それが本当の変数である。
let ...
を使う、というとき、それがトップレベル(グローバル)か、関数内か、にかかわらず、let束縛をする、と呼ぶことがある。
本当の変数が必要なとき、つまり代入や変更をプログラム中でやるには、どうしたらよいだろう?それには参照を使うことになる。参照は、C/C++のポインタと非常によく似ている。Javaでは、オブジェクトを格納する変数はどれも、実はオブジェクトへの参照(ポインタ)になっている。Perlでは、参照は参照で - OCamlと一緒だ。
どうやってint
への参照を作るか、OCamlではこうだ:
ref 0;;
こんな文は、くずと同じだ。参照を作ったはいいが、名前をつけていないので、ガーベジコレクタがやってきて、すぐに掃除していってしまう!(実際には、コンパイルのときに消されてしまう。)参照に名前をつけてあげよう:
let my_ref = ref 0;;
この参照は、いまは整数ゼロを格納している。何か別なものをいれてみよう(代入):
my_ref := 100;;
参照の中身がどうなったか、見てみよう:
# !my_ref;; - : int = 100
そう、:=
演算子は、参照に代入をするのに使われ、!
演算子は、参照の中身をとりだすのに使われる。 大雑把な比較を、C/C++としてみよう:
OCaml C/C++ let my_ref = ref 0;; int a = 0; int *my_ptr = &a; my_ref := 100;; *my_ptr = 100; !my_ref *my_ptr
参照にも使いどころはあるが、しかし、そう頻繁には参照は使わないと思ってよい。それよりよっぽどよく使うのは、let name = expression in
のほうで、名前を関数内のローカルな式につけることである。
Cには、入れ子関数の概念がないと言ってよい。GCCは、入れ子関数をCプログラマに提供しているが、この拡張を実際に使っているプログラムをみたことがない。それはさておき、info gccの、入れ子関数についてのページをのせておこう。
日本語訳 http://www.sra.co.jp/wingnut/gcc/gcc-j.html#Nested%20Functions
ネストした関数 とは、なにか別の関数のなかで定義された関数である。(GNU C++ ではネストした関数はサポートしていない。) ネストした関数の名前は、定義されたブロック内で有効である。以下の例では、square という名前のネストした関数を定義し、二回呼び出している。
foo (double a, double b) { double square (double z) { return z * z; } return square (a) + square (b); }
ネストした関数からは、それを含む関数の変数のうち、ネストした関数が定義されている点で見えるものは全部参照可能である。これは、レキシカル・スコーピング と呼ばれている。例えば、以下の例では、ネストした関数が offset という名前の変数を親関数から継承している。
bar (int *array, int offset, int size) { int access (int *array, int index) { return array[index + offset]; } int i; /* ... */ for (i = 0; i < size; i++) /* ... */ access (array, i) /* ... */ }
つかめただろうか。入れ子関数は、もう、大変に重宝で、OCamlではしきりに使われる。ここで入れ子関数の例を、実際のコードから見てみよう。
let read_whole_channel chan = let buf = Buffer.create 4096 in let rec loop () = let newline = input_line chan in Buffer.add_string buf newline; Buffer.add_char buf '\n'; loop () in try loop () with End_of_file -> Buffer.contents buf;;
このコードが何をしているかは、気にしなくてよい。まだこのチュートリアルで説明していないことが 、たくさん使われている。かわりによく見てほしいのは、中央の入れ子関数、loop
である。それの引数はひとつだけで、unitをとっている。loop()
の呼び出しは、関数内で、つまりread_whole_channel
内で行える。ただし、この定義は、関数の外からは参照できない。この入れ子関数は、親の関数内で定義された変数にアクセスできる。(それでloop
は、ローカルな名前buf
にアクセスしている)
この入れ子関数の形は、ローカルな名前を式につけたときと同じである:let name arguments = function-definition in
.
上の例のように、通常は、インデントを、関数定義で改行したときにやる。それから、関数がこの例のように再帰のときは、let
でなくlet rec
を使うのを忘れないように。
open
OCamlには、興味津々たるモジュール(ライブラリ。便利なコードをまとめたもの)がたくさんついている。例えば、標準ライブラリには、グラフィックを描くもの、GUI部品を操作するもの、大きな数やデータ構造をあつかうもの、POSIXシステムコールを作るもの、がある。これらのライブラリが置かれているのは、/usr/lib/ocaml/VERSION/
である(Unixなら)。これらのなかから、特に、シンプルなモジュールをひとつ選んで、紹介しよう。Graphics
モジュールだ。
Graphics
モジュールでインストールされるのは、5つのファイルである(手元のシステムでは)
/usr/lib/ocaml/3.08/graphics.a /usr/lib/ocaml/3.08/graphics.cma /usr/lib/ocaml/3.08/graphics.cmi /usr/lib/ocaml/3.08/graphics.cmxa /usr/lib/ocaml/3.08/graphics.mli
ここで、ファイルgraphics.mli
に注目してみよう。これはテキストファイルだから、目を通してみるとよい。お気づきのように、名前はgraphics.mli
で、Graphics.mli
ではない。OCamlでは、ファイル名の先頭を大文字にしたのが、モジュール名になる。慣れないうちは、これはとても間違えやすい!
Graphics
のなかの関数を使うには、やりかたが2通りある。それには、プログラムの最初で、open Graphics;;
宣言をするか、あるいは、頭に付け足しをして関数を呼ぶか、だ。それだとGraphics.open_graph.open
のようになる。open
は、ちょっと、Javaのimport
文みたいだ。もっとよく似ているのは、Perlのuse
文だ。
[Windows ユーザへ: この例を Windows でインタラクティブに実行するには、専用のトップレベルを作らないといけない。それには、コマンドラインから、ocamlmktop -o ocaml-graphics graphics.cma
とコマンドすればよい。]
すこし例を見るとわかるはずだ。(下の2つの例が描いているのは、それぞれ違う。やってみて。) ちなみに、最初の例で呼んでいるのは、open_graph
で、2つめの例では、Graphics.open_graph
だ。
(* To compile this example: ocamlc graphics.cma grtest1.ml -o grtest1 *) open Graphics;; open_graph " 640x480";; for i = 12 downto 1 do let radius = i * 20 in set_color (if (i mod 2) = 0 then red else yellow); fill_circle 320 240 radius done;; read_line ();;
(* To compile this example: ocamlc graphics.cma grtest2.ml -o grtest2 *) Random.self_init ();; Graphics.open_graph " 640x480";; let rec iterate r x_init i = if i = 1 then x_init else let x = iterate r x_init (i-1) in r *. x *. (1.0 -. x);; for x = 0 to 639 do let r = 4.0 *. (float_of_int x) /. 640.0 in for i = 0 to 39 do let x_init = Random.float 1.0 in let x_final = iterate r x_init 500 in let y = int_of_float (x_final *. 480.) in Graphics.plot x y done done;; read_line ();;
これらの例の両方のとも、まだ話していない機能をいくらか使っている。手続き型っぽいforループ、if- then-else、そして再帰だ。これらについては後で触れる。ともかくは、これらのプログラムを、よく見て、ぜひ、動かしてみてほしい。そうすれば、 (1)どう動くか (2)型推論がどれほどバグをつぶすのに役立つか がわかるはずだ。
モジュールで、ひとつだけ、"open
"する必要がないものがある。それは、Pervasives
モジュールである。(目を通すとよい。 /usr/lib/ocaml/3.08/pervasives.mli
)Pervasives
モジュールのすべてのシンボルは、自動で、OCamlプログラムにインポートされる。
こんなときはどうしたらよいだろう?Graphics
モジュールに使いたいシンボルがあるが、まるごとインポートするのは嫌で、かといってGraphics
をいちいちタイプするのも面倒だ、というとき。リネームという手がある。
module Gr = Graphics;; Gr.open_graph " 640x480";; Gr.fill_circle 320 240 240;; read_line ();;
実はこれが本当に便利なのは、インポートを、入れ子になったモジュールに対してやりたいときだ。(モジュールは、入れ子入れ子となっていることがままある。)そんなとき、入れ子になったモジュール名のフルパスを、いちいちタイプせずにすむ。
;;
と;
、使ったり、削ったり。ここで触れるのは、非常に大切なことである。;;
を使うべきとき、;
を使うべきとき、どちらも使うべきでないとき、それはいつか?これは、"コツをつかむ"までは、わかりにくい。著者も、OCamlを勉強中に、ずいぶんてこずった。
ルール1。;;
を使うべきときとは、コードのトップレベルにある文を区切るときだ。関数定義の中のときや、他の文のときは、いらない。
先ほどのgraphicsの例の、2つめのやつを見てみよう。
Random.self_init ();; Graphics.open_graph " 640x480";; let rec iterate r x_init i = if i = 1 then x_init else let x = iterate r x_init (i-1) in r *. x *. (1.0 -. x);;
2つのトップレベルの文があり、1つの関数定義(関数iterate
のやつ)がある。おのおのに;;
がついている。
ルール2。ときに、;;
を削ってもよいことがある。初心者のうちは気にしなくてよい。常にルール1に沿って;;
を置いていればよい。しかし、他人のコードをたくさん読むようになると、;;
を削れることも知っておかねばなるまい。特別にこれが許されるところとは:
let
キーワードの前open
キーワードの前type
キーワードの前これは、ちゃんと動くコードでありながら、;;
を極限まで削ってみたものだ。
open Random (* ;; *) open Graphics;; self_init ();; open_graph " 640x480" (* ;; *) let rec iterate r x_init i = if i = 1 then x_init else let x = iterate r x_init (i-1) in r *. x *. (1.0 -. x);; for x = 0 to 639 do let r = 4.0 *. (float_of_int x) /. 640.0 in for i = 0 to 39 do let x_init = Random.float 1.0 in let x_final = iterate r x_init 500 in let y = int_of_float (x_final *. 480.) in Graphics.plot x y done done;; read_line () (* ;; *)
ルール3と4は、一重の;
についてだ。こいつは、;;
とは完全に別物だ。セミコロンひとつの;
は、シークエンスポイントということになっている。言ってみれば、これは、C、C++、Java、Perlのセミコロンひとつと全く同じことである。意味は、"まずは、このポイントの前にあることをやる。前のが終わってから、後にあることをやる。"である。 ご存じなかったろう。
ルール3。let ... in
は文と思って、その後ろに一重の;を置かない。
ルール4。他のすべての文で、コードのブロック内にあるものには、一重の;
をつける。ただし一番最後のはのぞく。
上の例の、内側のforループがよい実演である。このコードでは一切、一重の;
を使っていない。
for i = 0 to 39 do let x_init = Random.float 1.0 in let x_final = iterate r x_init 500 in let y = int_of_float (x_final *. 480.) in Graphics.plot x y done
上のコードでただひとつ、;
を置いてもよいと思われるのは、Graphics. plot x y
の後ろである。しかし、これはブロックの最後の文になっているから、ルール4で、そこには置かない。
;
は演算子で、+
みたいなものだ。厳密には+
と同じではないけど、概念としては同じだ。+
は型int -> int -> int
をもち、2つのintをとり、1つのint(和)を返す。;
は型unit -> 'b -> 'b
をもち、2つの値をとり、単純に2番目のを返す。むしろ、Cの,
(コンマ)演算子みたいだ。 a ; b ; c; d
と、簡単に書ける。a + b + c + d
と書くようなものだ。
ここで、あまり語られることのない、ひとつの"悟り"がある - OCamlでは、ほとんどすべてが式だ。 if/then/else
は式だ。a ; b
は式だ。match foo with ...
は式だ。以下のコードは、完全に正しい(そして、すべて同じことをやっている):
let f x b y = if b then x+y else x+0 let f x b y = x + (if b then y else 0) let f x b y = x + (match b with true -> y | false -> 0) let f x b y = x + (let g z = function true -> z | false -> 0 in g y b) let f x b y = x + (let _ = y + 3 in (); if b then y else 0)
特に最後のには注をしておく。;
が、ふたつの文を"つなぐ"演算子として使われている。OCamlのすべての関数は、こう表現できる:
let name [parameters] = expression
OCamlの、何が式か、の定義は、Cよりもすこし広い。Cには、"文"という概念がある - しかし、Cの文はすべて、OCamlでは、実は式にすぎない(;
演算子で結合されている)
ひとつ、;
が+
と違うところは、+
は関数のように参照できることだ。例えば、sum_list
関数を定義して、intのリストの和を求められる。:
let sum_list = List.fold_left ( + ) 0
この節では、いくつか実物のコードの断片をお見せしよう。lablgtk 1.2 ライブラリから拝借する。(Lablgtk は、OCamlの、UnixネイティブGUIウィジェットを操作するライブラリである。)注意を一言:これらの断片には、まだ触れていないアイディアが、たくさん含まれている。詳細にこだわらず、コードの大まかなかたちを見てほしい。作者が、どこで;;
を使っているか、どこで;
を使っているか、どこでopen
を使っているか、何を意識したコードか、どんなふうに式にローカル、グローバルな名前をつけているか。
...何がなんだかってことにならないように、糸口をつけておこう。
?foo
と~foo
は、OCaml流の、関数のオプショナル引数と名前つき引数である。これにあたるものはC由来の言語には見あたらないが、しかし Perl、Python、Smalltalkは、この概念をもっているので、引数に名前をつけて関数を呼んだり、いくつか省略したり、つけたしをしたり、好きなようにできる。foo#bar
は、メソッド呼出しだ(bar
というメソッドを、foo
というオブジェクトで、呼んでいる)。これと同じなのが、foo->bar
とかfoo.bar
とか$foo->bar
で、C++やJavaやPerlに、それぞれある。 1つめの例:プログラマが、標準ライブラリをふたつ開いている(;;
を削っているのは、次のキーワードに open
とlet
がそれぞれきているからだ。)。それから、file_dialog
関数をつくっている。この関数の中の、2行にわたるlet sel = ... in
文で、式にsel
という名前をつけている。それからいくつかsel
のメソッドを呼んでいる。
(* First snippet *) open StdLabels open GMain let file_dialog ~title ~callback ?filename () = let sel = GWindow.file_selection ~title ~modal:true ?filename () in sel#cancel_button#connect#clicked ~callback:sel#destroy; sel#ok_button#connect#clicked ~callback:do_ok; sel#show ()
2つめの例:グローバルな名前を、トップレベルに、長々と並べただけだ。注意として、作者は;;
をすべて削っている。これはルール2による。
(* Second snippet *) let window = GWindow.window ~width:500 ~height:300 ~title:"editor" () let vbox = GPack.vbox ~packing:window#add () let menubar = GMenu.menu_bar ~packing:vbox#pack () let factory = new GMenu.factory menubar let accel_group = factory#accel_group let file_menu = factory#add_submenu "File" let edit_menu = factory#add_submenu "Edit" let hbox = GPack.hbox ~packing:vbox#add () let editor = new editor ~packing:hbox#add () let scrollbar = GRange.scrollbar `VERTICAL ~packing:hbox#pack ()
3つめの例: 作者はインポートをして、GdkKeysyms
モジュールのシンボルを、まるごと取り込んでいる。さて、見慣れないlet束縛をしている。let _ = expression
とは、"式の値を計算する(すべての副作用を伴う)、しかし結果は捨てる"ことを意味する。この場合は、"式の値を計算する"とは、Main.main ()
を実行するということで、これはGtkのメインループである。その副作用として、ウィンドウが画面にポンとでてきて、アプリケーション全体が実行される。 Main.main()
の結果は不明であり - たぶん返り値はunit
だろうが、確かめていない - アプリケーションが最後に終了するまで、帰ってこない。
この例で見てほしいのは、どのように、本質的に手続き命令の羅列であったものを、扱っているかである。これはまったく、古典的な手続き型のプログラムだ。
(* Third snippet *) open GdkKeysyms let _ = window#connect#destroy ~callback:Main.quit; let factory = new GMenu.factory file_menu ~accel_group in factory#add_item "Open..." ~key:_O ~callback:editor#open_file; factory#add_item "Save" ~key:_S ~callback:editor#save_file; factory#add_item "Save as..." ~callback:editor#save_dialog; factory#add_separator (); factory#add_item "Quit" ~key:_Q ~callback:window#destroy; let factory = new GMenu.factory edit_menu ~accel_group in factory#add_item "Copy" ~key:_C ~callback:editor#text#copy_clipboard; factory#add_item "Cut" ~key:_X ~callback:editor#text#cut_clipboard; factory#add_item "Paste" ~key:_V ~callback:editor#text#paste_clipboard; factory#add_separator (); factory#add_check_item "Word wrap" ~active:false ~callback:editor#text#set_word_wrap; factory#add_check_item "Read only" ~active:false ~callback:(fun b -> editor#text#set_editable (not b)); window#add_accel_group accel_group; editor#text#event#connect#button_press ~callback:(fun ev -> let button = GdkEvent.Button.button ev in if button = 3 then begin file_menu#popup ~button ~time:(GdkEvent.Button.time ev); true end else false); editor#text#set_vadjustment scrollbar#adjustment; window#show (); Main.main ()