Objective Caml処理系には2種類のコンパイラが用意されている.ひとつは gcc や javac などのように,ソースファイルから実行のためのファイルを生成するバッ チコンパイラ ocamlc,もうひとつは,ユーザからの入力をインタラクティブに 処理する ocaml である.このインタラクティブな処理系は,(ユーザからの)プ ログラムの入力 → コンパイル → 実行・結果の表示,を 繰り返すもので1 ,直前で実行されたプログラムの結果が次の入力時に反映される ため,開発中のテストなど,試行錯誤を伴う過程で特に便利なものである.また, 後述するように,入力はキーボードからだけでなく,ファイルからの読み込みもでき るので,毎回プログラムを最初から打たなければいけないなどの不便もない.
余談であるが,関数型言語処理系にはインタラクティブな処理系が用意されて いるものが多いようだ.
この演習では,まず ocaml の方を用いて進めていく.
起動方法はEmacs で M-x tuareg-run-caml (M-x はエスケープキーに続いて x をタイプする) とする(後述する Tuareg mode のバッファからは C-c C-s で起動できる). ミニバッファ(画面の最下段)に Caml toplevel to run: というプロンプトと ともに,起動するコマンドを聞かれるが(既に ocaml になっていることを確 認し),そのまま Enter キーをタイプする.すると,以下のような内容の新し いバッファが現われる.
Objective Caml version 3.08.1 #
# はインタラクティブコンパイラの入力プロンプトである.
さて,プロンプトに続いて,簡単な式を入力してみよう.
# 1 + 1;; - : int = 2
このテキストでは,ユーザの入力を行頭に#をつけ,タイプライター体 (abc)で,コンパイラからの出力をタイプライター斜体 (abc)で示す.最後の ;; は入力終了のしるしで,プロンプ トからここまでの部分がコンパイル・実行される.(途中に改行があってもよい.) コンパイラの出力は,評価結果につけられた名前(ここでは式だけを入力したので, 名前をつけていないという意味である -),式および評価結果の型 (int),評価結果(2)からなっている.
複雑な式は,()で囲むことで,部分式の構造を示すことができる.また,多くの 演算には常識的な結合の強さが定義されていて,()を省略できる.
# 1 + 2 * 3;; - : int = 7 # (1 + 2) * 3;; - : int = 9
また,入力中 ;; を入力する前に Control-C を2回続けて入力することで コンパイルせず,プロンプトに戻ることができる.
このバッファでは式の入力を助けるコマンドがいくつか用意されており, 例えば M-p, M-n で以前に入力した式を呼び出したりすることができる. (表2.1にキーバインディングをまとめてある.)
C-c C-c 入力途中で中断しプロンプトに戻る C-c TAB Camlに割り込みを入れる C-c C-k Camlを終了 M-p 過去に入力した式の履歴を遡る M-n 過去に入力した式の履歴を新しい方へ辿る
さて,いくつか誤った入力例についてもみていこう.
# 2 + 3 - ;; 2 + 3 - ;; ^^ Syntax error # 5 + "abc";; 5 + "abc";; ^^^^^ This expression has type string but is here used with type int # 4 / 0;; Exception: Division_by_zero.
(端末上では下線で示されているかもしれない.) 1番目の入力は,いわゆる文法エラーである.エラーメッセージはかなりあっさりし ていて,C や Java コンパイラに比べてやや(かなり?)不親切である.2番目は,入力された式の構成自体は文法に沿っているものの,型チェック(typechecking) を通らなかったことを示す.Objective Caml では,+ の両辺は,整数に評価される式でなくてはならない.しかし, ここでは "abc" という文字列を加えようとしているためエラーとなっている.エラーメッセージは, 「下線部(エラーの発生した個所)は文字列型(string)の式であるのに, 整数型(int)が必要な個所(つまり +の右側)で使われている」ことを示している. 型(type)や型チェックは Objective Caml では非常に重要な概念で,演習を通して詳 しく学んでいくことになる.最後の例では,式は型チェックも通っているが,コン パイル後の実行中に例外(exception)—ここでは0での除算—が発生し たことを示している.例外についても詳しく学ぶが,ここではとりあえず実行時の エラーの発生だと思っていればよい.
終了はプロンプトの出ている状態で C-c C-d を,もしくは #quit;; と入力す ることで行う.
Objective Caml version 3.08.3 # #quit;; Process inferior-caml finished
ocaml 内では,コンパイラの動作を制御するための ディレクティブと呼ばれるいくつかの命令が利用できる.たとえば,上ででてきた #quit もディレクティブの一種である.ディレクティブは多数あるが ここではファイルからのプログラム読み込みに関するふたつ #use, #cd を紹介する.詳しくはマニュアル[]を参照のこと.
#use はファイル名を引数にとって,ファイルの内容を入力としてコンパイルを 行う.
1 + 1;;
Objective Caml version 3.08.3 # #use "two.ml";; - : int = 2
ちなみに,ディレクティブは言語の式ではないので, 式の一部に埋め込んで使うことはできない.
# 1 + #use "two.ml";; 1 + #use "two.ml";; ^ Syntax error
#cd は,#use と同様に文字列を引数にとって,シェルの cd コマンドと 同様にカレントディレクトリを引数のものへ変更するものである.
演習のレポートは,プログラムファイルを提出することになるので, 主に別ファイルにプログラムを書いて,#use でコンパイラに読み込んで テストをすることになる.この時,ファイル名の拡張子として .ml を持つファイルを読み込むと,Tuareg mode という Objective Camlプログラムの 入力を支援するモードになり,ソースのインデントなどが できる.コマンドは表2.2にまとめてある.
TAB 現在の行のインデント C-c C-p 前のフレーズ(意味のあるまとまり)へ移動 C-c C-n 次のフレーズへ移動 ESC C-h フレーズにマーク ESC q フレーズをインデント C-c . t try 式の挿入 C-c . m match 式の挿入 C-c . l let 式の挿入 C-c . i if 式の挿入 C-c . w while 式の挿入 C-c . f for 式の挿入 C-c . b begin 式の挿入 C-c C-s ocaml を起動.起動中には以下のコマンドが使用可能 C-c C-k ocaml を終了 C-c C-b バッファを評価(ocaml プロセスへ送信) C-c C-r 選択部分の評価 C-c C-e フレーズの評価
ファイルにプログラムを書くときは,コメントを書くようにしたい. プログラム中のコメントは (* と *) で囲まれた部分である. また,コメントは入れ子になってもよいし,途中に改行をはさんでもよい.
EUC でエンコードされている限り,コメント,文字列定数に日本語を用いるこ とができる(ようである).しかし,文字列定数に関しては,その長さなどが (バイト数を数えてしまい)正しく認識されないので,できれば使わない方が無 難であろう.
Objective Caml プログラムは式(expression)から, それが示す値(value)(例えば,式 1 + 2 の値は 3 である)を 計算することでプログラムの実行が進んで行く.式から値を 計算する過程を評価(evaluation)という. 最も簡単な式は,整数や文字列などの,基本的なデータ定数である.これらは それ自身が値である. 複雑な式は簡単な式を組み合わせることで構成する.例えば,1 + 2 という 式は二つの部分式(subexpression) 1 と 2 と + という 二項演算子から構成されている.
本格的なプログラミングに入る前に,Objective Caml で使用される基本的なデータ(整数,実 数,文字列など)とそれに対する演算(加減乗除,文字列の結合など)を,データの型 ごとに説明し,複雑な式を構成する方法をみていく.
メタ変数について テキスト 中,Objective Caml 式を表記する際に, i + jのように,斜体英小文字とタイプライタ体を 混ぜて表記することがある.+ 記号が Objective Caml の式の一部の文字である ことに対して,i, j は(このテキスト 中では) 任意の整数式を表すためのテキスト上での表記である.すると,例えば Objective Caml 式 1 + 2 は i を 1, j を 2と考えた場合の例と考え ることになる.このようなプログラムの世界の外での表記のための変数を メタ変数(metavariable)と呼ぶ.プログラムに用いられる変数 (プログラム変数)と混同しないように気をつけたい.特に,プログラム変数 のためのメタ変数を用いる場合には注意が必要である.例えば,テキスト中 で x, y などをプログラム変数のためのメタ変数として使用するが, x + 1と書いたときには,a + 1, pi + 1, hoge + 1, x + 1 な ど,a, pi, hoge, x という具体的な変数を使った任意の式を表わし ている.
unitは,() (unit value と呼ぶ) という値をただひとつの 要素として含むような型である.
# ();; - : unit = ()
この値に対して行える演算はなく,役に立たないものに思えるかもしれない. 典型的な使用法にはふたつある.ひとつは,返り値に意味がないような,例えば ファイルに書き込みを行なうだけの式は,unit型を持つ. その意味で,C などにおける void 型と似ている2. また,もうひとつは,(意味のある)引数の要らない手続きは unit型の引数を取る関数として表される.
いわゆる整数,…, −2, −1, 0, 1, 2, … の型である.算術演算として 四則演算 +, -, *, /, 剰余を求める mod などが,また,ビット演算と して次のようなものが用意されている.
(浮動小数点表現の)実数の型である.3.1415 などの小数点表現と 31.415e-1 などの10を基底とする指数表現 (= 31.415 � 10−1) が 使用できる.また,小数点の前の0は省略できない.
先述の四則演算記号は浮動小数点に対して用いることはできない.その代りに 小数点をつけた +., -., *., /. を使う.また,逆に整数を 「そのまま」実数とみなし,+. などを使うこともできない. 整数/実数間の変換には int_of_float, float_of_int という関数が 用意されている.(つまり,C 言語などのように暗黙の型変換は存在しない.)
# 2.1 +. 5.9;; - : float = 8. # 1 +. 3.4;; 1 +. 3.4;; ^ This expression has type int but is here used with type float # float_of_int(1) +. 3.4;; - : float = 4.4 # float_of_int 1 +. 3.4;; - : float = 4.4 # 1 + (int_of_float 3.4);; - : int = 4
関数の引数のまわりの () は省略可能である.省略可能,というより,そも そも関数適用の文法と括弧は関係がなく,括弧はあくまで (1+2)*3 で使う ように演算の結合の強さに逆らって部分式をまとめるためのものと考えた方が よい.上の4番目の例でわかるように,関数適用 (float_of_int 1) は +. などの二項演算子よりも結合が強い.そのため,引数が 1+2 のような複雑 な式である場合には, f 1 + 2 は (f 1)+2 の事なので,引数に括弧をつ け f(1+2) と書く必要がある.
実数演算に関しては,三角関数 sin, cos, tan, 平方根 sqrt なども予め用意されている.詳しくはマニュアルを参照のこと.
ASCII 文字の型で,定数として引用符 ' で囲まれた文字,もしくは表 2.3 のエスケープシーケンス(\" を除く), また,int型との変換関数 char_of_int, int_of_char が用意されている.
# '\120';; - : char = 'x' # int_of_char 'Z';; - : int = 90
文字列の型で,定数として二重引用符 " で囲まれた文字列が使われる. 文字列中の文字には, \' を除く,表2.3 のエスケープシーケンス が使用できる. また,C とは異なり,\000 は文字列の終端を表さない.
s1 ^ s2 で二つの文字列s1, s2を結合した文字列に評価される.また, s.[i] で s から i 番目の文字を 取り出すことができる.
# "Hello," ^ " World!";; - : string = "Hello, World!" # ("Hello," ^ " World!").[10];; - : char = 'l'
\\ バックスラッシュ(\) \' 引用符 ('), ' 内でのみ有効 \" 二重引用符 ("), " 内でのみ有効 \n 改行 \r (行頭への)復帰 \t 水平タブ \b バックスペース \ddd ddd を10進のASCIIコードとする文字
真偽値を示す型で,値は true (真), false (偽)の二つである. 演算として,
また以下の比較演算子が用意されている.どの演算子も 両辺の型が同じでなければならない.
# (not (1 < 2)) || (() = ());; - : bool = true # 3.2 > 5.1;; - : bool = false # 'a' >= 'Z';; - : bool = true # 2 < 4.1;; 2 < 4.1;; ^^^ This expression has type float but is here used with type int
また,if-式: if b then e1 else e2 で条件分岐を行うことができる.b が true に評価されたときは e1 の値, false であれば e2 の値が式全体の値になる.
# (if 3 + 4 > 6 then "foo" else "bar") ^ "baz";; - : string = "foobaz"
分岐後に評価される式 e1, e2 の型は一致している必 要がある.また,if-式の else-節は省略可能であるが,その場合は else () が隠れていると見なされる.(すなわち,その場合 then-節には unit型の式が来なければならない.)
Objective Caml には型(type)の概念があり,これから学んでいくように言語の大 きな特徴のひとつをなしている.型は,最も単純には,上でみた 1 は int型に 属するといった,プログラム中で使われるデータの分類である. この分類は, true に加算を行うなどの,型エラー(type error)と呼ばれる,ある 種の「意味のない」操作が行われるのを防ぐのに用いられる.型システム(type system)という用語は,プログラムから型チェック(typecheck)に より,型エラーの発生を防ぎ,安全にプログラムを実行するための仕組みで,言語 ごとに大きく異なっている.そもそも型エラーがなんであるか,ということも言語 によって違ってくるものであることに注意.例えば,多くの言語では0での除算は 型エラーとは見なされないことが多い.
Lisp, Perl, Postscript などの言語では,文法に即したプログラムはそのまま実行 を始めることができる.そのかわり実行時に,何かの操作が行われる度に,それが 型エラーを起すかどうかをチェックする.このような言語を,しばしば 動的に型づけされる言語(dynamically typed language) と呼ぶ.
これに対して,C, C++, Java などの言語は,コンパイラがプログラム実行前に型 チェックを行い,それを通ったもののみがコンパイルされる.このようなプログラ ム実行前に型チェックを行うものを,静的に型づけされる言語(statically typed language)と呼ぶ3.Objective Caml も静 的に型づけされる言語である.
静的に型づけされる言語でも,C や C++ などは型システムが弱く,型エラーを 完全に防ぐことはできない.一般に静的に型づけされる言語において,型エラーを 起す操作の結果は(言語レベルで)未定義 4 なので,C などのプログラムはクラッシュして しまう.これに対して Objective Caml は一度型チェックを通ったプログ ラムは型エラーを起さない性質(型安全性)が保証されている.静的に型づけされ安全 性が保証できる言語を強く型づけされた(strongly typed)言語というこ とがある.
以上を,まとめると以下のようになる.動的に型づけされる言語は必然的に 全ての安全性のチェックを行えるので unsafe–dynamically typed の欄が 空いている.
statically typed | dynamically typed | |
unsafe | C, C++, etc. | — |
safe | Java, ML, Haskell, etc. | Lisp, Scheme, Perl, PostScript, etc. |
静的型システムは,プログラムの文面だけから,つまり計算前の 複雑な式に対して型の整合性を判定しなければならず,見積もりがどうしても 保守的にならざるを得ない.例えば
if ⟨ 複雑な式 ⟩ then 1 else "foo"
は,⟨ 複雑な式 ⟩が常に trueを返すような式であったとしたら, else-節が実行されることがないので,整数が必要な文脈で使用しても 実行時には何の問題もない.しかし,型システムは条件式の値に関係なく,分 岐先の式の型が一致することを要求する.このため,型エラーを起さずに実行でき るはずのプログラムが型チェックを通らない可能性がある.静的型付言語の設 計者にとっては安全なプログラムだけを受理しつつ,できる限り多くの安全な プログラムを受理できるような型システムを設計するのが,頭の悩ませどころ である.
一方,動的に型づけされる言語は操作が行われる度に,実行が安全に行えるかどうか チェックをするので,チェックをまじめにやる限り安全に実行できるものの, チェックのコストを余計に払うことになる.
前節で学んだのは簡単な操作を組み合わせて,複雑な計算を行う式を組み立てる方 法である.計算した結果の値には名前をつけておいてあとで参照することができる. これを行うのが let 宣言である.
まずは,let 宣言の例をみてみる.
# let pi = 3.1415926535;; val pi : float = 3.1415926535
これは pi という名前の変数を宣言し,その変数を3.1415926535 という実数に 束縛している.コンパイラの出力として,値の名前が宣言されたことを示す val,変数名 pi,その変数が束縛され た値(もちろん3.1415926535),とその型が得られる.この値は,以降で pi と いう名前で参照することができ,pi と書くことと,3.1415926535 と書くこと は同じことを意味する.
# pi;; - : float = 3.1415926535 # let area_circle2 = 2.0 *. 2.0 *. pi;; val area_circle2 : float = 12.566370614
一般的には
let x = e;;
という形で,宣言された変数 x を「式 e を評価した値」に束縛する.変数を宣言することには,
といった意義がある.ひとつめの観点から言えば,分かりにくい変数名を つけることは避けるべきであり,たとえ一時的にしか使わない変数でも 意味を反映した名前をつけるべきである.ふたつめに関して補足しておくと, 変数が束縛される対象は計算結果の値であって,式自体ではないことに注意. 上で,「pi と書くことと,3.1415926535 と書くことは同じことを 意味する」といったのは式自体が値になっているからである.ただ,再度計算 することの無駄を除けば,(ディスプレイ出力などの副作用がない限り) 式とその値は計算結果に影響をおよぼさない.
Objective Caml における変数宣言は値に名前をつけるもので,C, C++ のように, メモリ領域に名前をつけるものではなく,代入文のようなもので「中身を更新する」 ことはできない.ただし同じ名前の変数を再宣言することはできる.
# let one = 1;; val one : int = 1 # let two = one + one;; val two : int = 2 # let one = "One";; val one : string = "One" # let three = one ^ one ^ one;; val three : string = "OneOneOne"
この場合,one の値は 1 から "One" に更新されたわけではなく, 同じ名前の変数宣言により前の宣言が隠されて見えなくなっただけなのである. (そもそも代入だと考えると,前の宣言と右辺の型が一致していないので変である.)
宣言された変数は常に有効範囲(scope)といって,その変数を使って よいプログラム文面上の範囲が割当てられる.例えば,let宣言の有効範囲(scope)は宣言以降,ファイル(ocamlセッション)終了までである. 変数のひとつひとつの使用(宣言と区別して変数参照(variable reference)とも言う)に対して,(以前に宣言されている物で)最も近いもの が対応する.この対応関係は,実行時ではなくプログラムが書かかれた(もしくは コンパイルされた)時点で決定されるので,「この言語での変数宣言は 静的有効範囲(static scope, lexical scope)を持つ」と言われる.
ところで,Objective Caml では変数の型はコンパイラが自動的に推論してくれるた め,宣言する必要がない.ただ,プログラムの意味をわかりやすくするため, デバッグのため,変数の型を明示的に示しておきたいときは,変数名の後に “: ⟨ 型 ⟩” として宣言することもできる.また,複数のlet-宣 言はその境目がはっきりしている(次の let が来る直前で切れる)ので間に ;; をつけずに並べることができる.
# let pi : float = 3.1415926535 let e = 2.718281828;; val pi : float = 3.1415926535 val e : float = 2.718281828
二つの宣言がまとめてコンパイルされて結果がまとまって出力されていることに注目.
変数の名前として用いることができるのは,
であるような任意の長さの文字列で,表2.4の Objective Caml の文法キーワー ドと _ 一文字のみからなるものを除くものである.
ここで高レベルな式の意味を離れて,名前参照がどのように実現されているかをみ てみる.プログラムの実行中には,実行している時点で定義されている(有効範囲に ある)変数名とその値の組のリストがメモリ上に保存されている.このデータのこと を環境(environment)という.特に,プログラムの一番外側における環境 をトップレベル環境(top-level environment),または大域環 境(global environment)という.
例えば,ocaml を起動したときには,sin, max_int などの名前が大域環境に ある状態でセッションが始まる(図2.1).
let宣言を実行する際には,このトップレベル環境の最後に,新しい変数 とその値の組が追加される(図2.2) .変数の参照は,この環境を下から 順番に変数を探して行く操作に対応する5.そのため,同じ名前の変数が再定義され た場合,上のエントリに探索が到達しないために参照することができなくなる. 大域環境からエントリが削除されることはない. そのためlet宣言の有効範囲は宣言直後からプログラム終了までなのである.
a_2'������____������Cat������_'_'_������ 7eleven������'ab2_�����_ |
多くのプログラミング言語では,計算手順に名前をつけて抽象化することができる. Objective Caml ではこれを関数(function)と呼ぶ.
上で定義した変数 pi を使って,円の面積を求めることを考える.円の面積 自体はもちろん,
⟨半径⟩ *. ⟨半径⟩ *. pi
という式で求まるわけだが,違った半径に対して「同じような式」を 何度も入力するのは,間違いのもとであり,また,その式が何を意味するのかが わかりにくくなる.そこで,「似たような計算の手順」に名前をつけ, 出現個所によって違う部分(実際の半径)は,パラメータ化(parameterization) することを考える.この「パラメータ化された計算手順」が関 数である.
Objective Caml では円の面積を求める関数は次のように定義することができる.
# let circle_area r = (* area of circle with radius r *) r *. r *. pi;; val circle_area : float -> float = <fun>
関数宣言にも let 宣言を使用する.入力の circle_area が宣言された関 数の名前である.関数名の後の r がパラメータであり,定義内で通常の変 数と同じように使用することができる.= よりあとの式 r *. r *. pi が 関数の本体(body)と呼ばれる部分で計算手順であるところの式を書 くところである.(;; はいつものようにコンパイラに入力終了を知らせるも のである.)コンパイラからの応答は,宣言された名前 circle_area,その 型 float->float,その値 <fun> と並んでいる.型の中の -> は, ⟨ パラメータの型 ⟩->⟨ 結果の型 ⟩ という形で,その circle_area が関数であることを意味しており,ここでは実数をとって実数 を返すことを表している.-> のようにより単純な型から型を構成する記号 を型構築子(type constructor)と呼ぶ.基本型も0個の型から型を 作る型構築子と考えられる.<fun> は,なんらかの関数であることを示して いる.今まで見てきた整数などとは異なり,ディスプレイに表示できる表現 (“3” など)を持たない.
宣言された関数は,組み込みの int_of_float などと同じように呼び出すことが できる.
# circle_area 2.0;; - : float = 12.566370614
関数呼び出し(関数適用(function application)ともいう)は, 最も素朴な見方では,関数本体中のパラメータ r を引数 2.0 に 置き換えた式,2.0 *. 2.0 *. pi を評価し,その値が,呼び出し式 全体の値となる.
Objective Caml では,値の束縛と同様,宣言される関数のパラメータおよび結果の 型を明示的に宣言する必要がない.これは,コンパイラが型推論(type inference)を行って,上の例のように型情報を補ってくれるためである. 簡単な型推論の仕組みについては,後程見ていくことにする.それでも明示的 に型を宣言したい場合には,値の束縛と同様,型情報を補うことができる.
# let circle_area(r : float) : float = (* area of circle with radius r *) r *. r *. pi;; val circle_area : float -> float = <fun>
結果の型は = の前に記述する.また, パラメータを囲む () が必要になる. (型宣言をしない場合でも () をつけることができるが,呼び出しの時の省略を行 うのと同様な理由で () は省略することが多い.)
ここでの,関数宣言の文法をまとめると,
let f ⟨ parameter ⟩ [: t] = e ただし ⟨ parameter ⟩ ::= x | (x: t)
となる6. [ ] 部分はオプションである.f は関数名を表すメタ変数, t は型を表すメタ変数である. 関数名・パラメータ名として許される名前は変数の場合と同じである. (実は,変数名,関数名,パラメータ名を区別する必要はない.) 値の名前と同じように,関数名・パラメータ名もわかりやすいものをつけ, 関数が何を計算するのか,コメントを書く癖をつけたい.
静的有効範囲について補足 関数本体中の pi は,静的有効範囲によって,関数宣言の時点で宣言さ れているものが参照される.そのため,circle_area のあとでpi を再宣 言しても,circle_area の定義には影響がない.# let pi = 1.0;; val pi : float = 1. # circle_area 2.0;; - : float = 12.566370614これに対して,関数を呼び出した時点の pi の値を見る dynamic scoping という 方式を採用している言語(例えば Emacs Lisp)もある.dynamic scoping の下では, 上の結果は,4.0 (つまり 2.0 *. 2.0 *. 1.0) になる.