ヌルポインタ、assert、警告

ヌルポインタ

それでは、あなたが、自分のウェブサイト上である調査をすることになったとしよう。それで、あなたの読者 に、名前と年齢を質問することになった。ただひとつの問題は、なんらかの理由で、年齢を教えてくれない読者がいるということで - かたくなに、 この欄をうめるのはいやだというのだ。あわれなデータベース管理者はどうしたらいい?

前提として、年齢はintで表せるとする。この問題を解くのに、2つのやりかたが考えられる。もっとも一般的なのは(そして、もっとも間違っている)、 なんらかの"特別な"値を、年齢の情報が得られなかったというのに割り当てる、ということだ。あー、そう、データが得られなかったら age = -1 、とかね。それ以外なら、データが得られたことにする(有効な値かは別として)。この手のやりかただと、後で困ったことになる。例えば、あなたのウェブサ イトのお客さんの年齢の、平均を求めることになったとする。ここでうっかり特別な値のことを忘れてしまったら、お客さんの平均年齢は、 7.5才だってことになり、Webデザイナーに、長い単語は全て外して、そこかしこに原色を使えって言いつけることになる。

一方、正しいやりかたは、年齢の欄におさめるものが、型"int か null"をもつようにすることだ。これは、年齢をおさめるためのSQLテーブルだ:

create table users
(
  userid serial,
  name text not null,
  age int             -- may be null
);

もし年齢のデータが得られなかったら、データベースに、特別なSQLの値NULLをいれる。SQLはこれを自動的に無視して、平均を計算したりしてくれる。

プログラミング言語にもnullはあるが、他の意味で使われすぎるきらいがある。Perlでは、どんなスカラー(つまり数や文字列)でも、undef(Perlでのnull)をとれてしまう。これによって、たくさんの警告がでるが、未熟なプログラマーはしばしば無視してしまい、深刻なエラーをも見逃してしまうことがある。Javaでは、オブジェクトへの参照は、nullでもよい。なので、Javaでは、年齢にIntegerがはいっているとも、年齢が nullを参照しているとも、とれてしまう。Cのポインターも、もちろん、nullでもよいので、もしnullもとれる整数がほしければ、それをオブジェクトという箱にいれて、mallocでヒープに確保することになる。

OCamlは、nullの問題に、美しい解をもっている。簡単な多相ヴァリアント型を定義すれば(Pervasivesで)よい:

type 'a option = None | Some of 'a

"nullポインタ"は、Noneと書かれる。上の例の、年齢の型(nullもとれるint)は、int optionである。[`int list`や`int binary_tree`でやるとどうだろう]

# Some 3;;
- : int option = Some 3

int optionのリストはどうなるだろう?

# [ None; Some 3; Some 6; None ];;
- : int option list = [None; Some 3; Some 6; None]

また、int listのoptionはどうなるだろう?

# Some [1; 2; 3];;
- : int list option = Some [1; 2; 3]

assert、警告、致命的エラー、stderrへの出力

Perlの素晴らしい特徴のひとつは、プログラムのデバッグやエラー処理のためのコマンド群が豊富なこと だ。スタックトレースをプリントしたり、 throw & catch で例外をとったり、いろいろなことができる。 OCamlのデバッグコマンド群はそれほどには豊富ではない - Javaよりまし、Cと同じくらいで、Perlには及ばない。(例外について、あとでより詳しくふれる)

まずはじめに、assertは式を引数にとり、例外をthrowする。この例外をcatchしないでいると(この例外をcatchするのはあまり賢明ではない、特に初心者には)、プログラムは停止し、エラーが起きたところのソースファイルと行数を出力する。例:

# assert (Sys.os_type = "Win32");;
Exception: Assert_failure ("", 1, 0).

(これをWin32で実行したら、当然、エラーはでない)

どうもうまくいかないというとき、assert falseを呼べば、そこでプログラムを停止できる。しかし、多分使いやすいのはこっちの・・・

failwith "error message"Failure例外をthrowする。例外をcatchしないでいると、やはりプログラムは停止して、与えられたエラーメッセージをだす。failwithはしばしば、パターンマッチングでも使われる。現実的な例ではこうなる。

match Sys.os_type with
| "Unix" | "Cygwin" ->   (* code omitted *)
| "Win32" ->             (* code omitted *)
| "MacOS" ->             (* code omitted *)
| _ -> failwith "this system is not supported"

変なパターンマッチングがふたつ、この例にでてきている。いわゆる"範囲指定パターン"は、"Unix"にも"Cygwin"にもマッチさせるのに使う。_パターンは特に、"その他なんでも"にマッチさせるのに使う。

プログラムをデバッグしたいけど、私のように、デバッガはgdbじゃなきゃやだ、という人もいるだろう。それなら、警告をprintするものを関数の中にはさめばよい。そのためにはいくつかの方法がある。例えばこうだ。(注 そのコードが赤く目立つようにした。)

open Graphics
  
let () =
  open_graph " 640x480";
  for i = 12 downto 1 do
    let radius = i * 20 in
    prerr_endline ("radius is " ^ string_of_int radius);
    set_color (if i mod 2 = 0 then red else yellow);
    fill_circle 320 240 radius
  done;
  read_line ()

もしC風のprintfがよければ、OCamlのPrintfモジュールをかわりに試すとよい。

open Graphics
open Printf

let () =
  open_graph " 640x480";
  for i = 12 downto 1 do
    let radius = i * 20 in
    eprintf "radius is %d\n" radius;
    set_color (if i mod 2 = 0 then red else yellow);
    fill_circle 320 240 radius
  done;
  read_line ()