OCamlプログラムの構造

(Redirected from ja/the_structure_of_ocaml_programs)

OCamlプログラムの構造

さて、ここからは、俯瞰的に、実際の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つめの例のxa +. 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 ...を使う、というとき、それがトップレベル(グローバル)か、関数内か、にかかわらず、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)型推論がどれほどバグをつぶすのに役立つか がわかるはずだ。

Pervasives モジュール

モジュールで、ひとつだけ、"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に沿って;;を置いていればよい。しかし、他人のコードをたくさん読むようになると、;;を削れることも知っておかねばなるまい。特別にこれが許されるところとは:

これは、ちゃんと動くコードでありながら、;;を極限まで削ってみたものだ。

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を使っているか、何を意識したコードか、どんなふうに式にローカル、グローバルな名前をつけているか。

...何がなんだかってことにならないように、糸口をつけておこう。

1つめの例:プログラマが、標準ライブラリをふたつ開いている(;;を削っているのは、次のキーワードに openletがそれぞれきているからだ。)。それから、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 ()