この章のキーワード: インタラクティブコンパイラ,型,型システム,型安全性,
有効範囲, 環境,関数 |
|
Objective Caml処理系には2種類のコンパイラが用意されている.ひとつは gcc や
javac などのように,ソースファイルから実行のためのファイルを生成するバッ
チコンパイラ ocamlc,もうひとつは,ユーザからの入力をインタラクティブに
処理する ocaml である.このインタラクティブな処理系は,(ユーザからの)プ
ログラムの入力 → コンパイル → 実行・結果の表示,を
繰り返すもので1
,直前で実行されたプログラムの結果が次の入力時に反映される
ため,開発中のテストなど,試行錯誤を伴う過程で特に便利なものである.また,
後述するように,入力はキーボードからだけでなく,ファイルからの読み込みもでき
るので,毎回プログラムを最初から打たなければいけないなどの不便もない.
余談であるが,Lisp, Scheme など関数型言語処理系にはインタラクティブな処理系
が用意されているものが多いようだ.
この演習では,主に ocaml の方を用いて進めていく.
起動方法はEmacs で M-x run-caml (M-x はエスケープキーに続いて x をタイプする)
とする.ミニバッファ(画面の最下段)に Caml toplevel to run: というプロンプト
とともに,起動するコマンドを聞かれるが(既に ocaml になっていることを
確認し),そのまま Enter キーをタイプする.すると,以下のような
内容の新しいバッファが現われる.
Objective Caml version 3.06
#
# はインタラクティブコンパイラの入力プロンプトである.
さて,プロンプトに続いて,簡単な式を入力してみよう.
# 1 + 1;;
- : int = 2
このテキストでは,ユーザの入力を行頭に#をつけ,タイプライター体
(abc)で,コンパイラからの出力をタイプライター斜体
(abc)で示す.最後の ;; は入力終了のしるしで,プロンプ
トからここまでの部分がコンパイル・実行される.(途中に改行があってもよい.)
コンパイラの出力は,評価結果につけられた名前(ここでは式だけを入力したので,
名前をつけていないという意味である -),式および評価結果の型
(int),評価結果(2)からなっている.
複雑な式は,()で囲むことで,部分式の構造を示すことができる.また,多くの
演算には常識的な結合の強さが定義されていて,()を省略できる.
# 1 + 2 * 3;;
- : int = 7
# (1 + 2) * 3;;
- : int = 9
このバッファでは式の入力を助けるコマンドがいくつか用意されており,
例えば M-p, M-n で以前に入力した式を呼び出したりすることができる.
(表2.1にキーバインディングをまとめてある.)
Table 2.1: コンパイラバッファ内キーバインディング
C-c C-c |
入力途中で中断しプロンプトに戻る |
C-c C-d |
セッションの終了 |
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.06
# #quit;;
Process inferior-caml finished
2.1.2 |
その他: ファイルからのプログラムの読み込み・コメント |
|
ocaml 内では,コンパイラの動作を制御するための
ディレクティブと呼ばれるいくつかの命令が利用できる.たとえば,上ででてきた
#quit もディレクティブの一種である.ディレクティブは多数あるが
ここではファイルからのプログラム読み込みに関するふたつ #use, #cd
を紹介する.詳しくはマニュアル[3]を参照のこと.
#use はファイル名を引数にとって,ファイルの内容を入力としてコンパイルを
行う.
two.ml の内容
1 + 1;;
#use を使う
Objective Caml version 3.06
# #use "two.ml";;
- : int = 2
ちなみに,ディレクティブは言語の一部ではなく,
通常の式と組み合わせて使うことはできないことに注意.
# 1 + #use "two.ml";;
1 + #use "two.ml";;
^
Syntax error
#cd は,#use と同様に文字列を引数にとって,シェルの cd コマンドと
同様にカレントディレクトリを引数のものへ変更するものである.
演習のレポートは,プログラムファイルを提出することになるので,
主に別ファイルにプログラムを書いて,#use でコンパイラに読み込んで
テストをすることになる.この時,ファイル名の拡張子として
.ml を持つファイルを読み込むと,caml-mode という Objective Camlプログラムの
入力を支援するモードになり,ソースのインデントなどが
できる.コマンドは表2.2にまとめてある.
Table 2.2: caml-mode キーバインディング
TAB |
現在の行のインデント |
M-C-q および |
|
C-c C-q |
現在の行を囲むフレーズ(式として意味のあるまとまり)
のインデント |
M-C-h |
フレーズにマーク |
C-c w |
バッファに while 式を挿入 |
C-c t |
try 式を挿入 |
C-c m |
match 式を挿入 |
C-c l |
let 式を挿入 |
C-c i |
if 式を挿入 |
C-c f |
for 式を挿入 |
C-c b |
begin 式を挿入 |
M-x run-caml |
ocaml を起動.起動中には以下のコマンドが使用可能 |
M-C-x および |
|
C-c C-e |
フレーズを caml プロセスに送る |
C-c C-r |
リージョンを caml プロセスに送る |
C-c C-s |
caml プロセスのバッファを表示 |
C-c ` |
caml プロセスに送った式のコンパイルエラーを順次表示 |
コメント,日本語の扱い
ファイルにプログラムを書くときは,コメントを書くようにしたい.
プログラム中のコメントは (* と *) で囲まれた部分である.
また,コメントは入れ子になってもよいし,途中に改行をはさんでもよい.
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 などが,また,ビット演算と
して次のようなものが用意されている.
-
i land j, i lor
j, i lxor j, lnot i:
ビット毎の論理積/論理和/排他的論理和/論理的否定をとる.
- i lsl j: i の左方向への
j ビットシフト (= i * 2(j mod 32)).
- i lsr j: i の右方向へのjビッ
ト(論理)シフト.(最上位ビットには常に0がはいる.)
- i asr j: i の右方向へのjビッ
ト(算術)シフト.(最上位ビットには i の正負を保存するものがはいる.)
(浮動小数点表現の)実数の型である.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
関数の引数のまわりの () は省略可能である3.また,関数適用(float_of_int 1) は +. などの二項演算子よりも結合が強いことに注意.
これ以外にも三角関数 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'
Table 2.3: エスケープシーケンス
\\ |
バックスラッシュ(\) |
\' |
引用符 ('), ' 内でのみ有効 |
\" |
二重引用符 ("), " 内でのみ有効 |
\n |
改行 |
\r |
(行頭への)復帰 |
\t |
水平タブ |
\b |
バックスペース |
\ddd |
ddd を10進のASCIIコードとする文字 |
真偽値を示す型で,値は true (真), false (偽)の二つである.
演算として,
-
not b: b の否定を返す.
- b1 && b2 または b1 &
b2: b1, b2 の論理
積を返す.b1 の評価結果が false の場合は b2
の評価は行わない.
- b1 || b2 または b1 or
b2: b1, b2 の論理
和を返す.b1 の評価結果が true の場合は b2
の評価は行わない.
また以下の比較演算子が用意されている.どの演算子も
両辺の型が同じでなければならない.
-
e1 = e2: 式e1, e2 の値
が等しいか判定する.
- e1 <> e2: 式e1, e2 の値
が等しくない場合に真を返す.
- e1 < e2, e1 > e2,
e1 <= e2, e1 >= e2:
e1, e2 の値の大小比較を行う.
# (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)と呼ぶ4.Objective Caml も静
的に型づけされる言語である.
静的に型づけされる言語でも,C や C++ などは型システムが弱く,型エラーを
完全に防ぐことはできない.一般に静的に型づけされる言語において,型エラーを
起す操作の結果は(言語レベルで)未定義
5
なので,C などのプログラムはクラッシュして
しまう.これに対して Objective Caml は一度型チェックを通ったプログ
ラムは型エラーを起さない性質(安全性)が保証されている.静的に型づけされ安全
性が保証できる言語を強く型づけされた(strongly typed)言語というこ
とがある.
以上を,まとめると以下のようになる.動的に型づけされる言語は必然的に
全ての安全性のチェックを行えるので unsafe--dynamically typed の欄が
空いている.
|
statically typed |
dynamically typed |
unsafe |
C, C++, etc. |
--- |
safe |
Java, ML (Objective Caml, Standard ML), Haskell, etc. |
Lisp, Scheme,
Perl, PostScript, etc. |
静的型システムは,プログラムの文面だけから,つまり計算前の
複雑な式に対して型の整合性を判定しなければならず,見積もりがどうしても
保守的にならざるを得ない.例えば
if 〈 複雑な式 〉 then 1 else "foo"
は,例え 〈 複雑な式 〉が常に trueを返すような式であったとしたら,
else-節が実行されることがないので,整数が必要な文脈で使用しても
実行時には何の問題もない.しかし,型システムは条件式の値に関係なく,分
岐先の式の型が一致することを要求する.このため,型エラーを起さずに実行でき
るはずのプログラムが型チェックを通らない可能性がある.言語設計者にとっては
安全なプログラムだけを受理しつつ,できる限り多くの安全なプログラムを受理で
きるような型システムを設計するのが,頭の悩ませどころである.
一方,動的に型づけされる言語は操作が行われる度に,実行が安全に行えるかどうか
チェックをするので,チェックをまじめにやる限り安全に実行できるものの,
チェックのコストを余計に払うことになる.
Exercise 1 次の式の型と評価結果は?
-
float_of_int 3 +. 2.5
- int_of_float 0.7
- if "11" > "100" then "foo" else "bar"
- char_of_int ((int_of_char 'A') + 20)
- int_of_string "0xff"
- 5.0 ** 2.0
Exercise 2 次の式は誤った式(文法エラー,型エラー,例外を発生する)である.まず,どこが
誤りかを試さずに予想せよ.
次に,コンパイラでエラーメッセージを確認し,当初予想した理由と
エラーメッセージと違う場合,コンパイラの解釈した誤りの理由を説明せよ.
-
if true&&false then 2
- 8*-2
- int_of_string "0xfg"
- int_of_float -0.7
Exercise 3 次の式は,括弧の付き方がおかしい,もしくは型変換関数を入れ忘れたため,型
エラーが発生する,もしくは期待した結果に評価されない.各式をどう直せば,
⇒ の後に示す期待した結果に評価されるか.
-
not true && false ⇒ true
- float_of_int int_of_float 5.0 ⇒ 5.0
- sin 3.14 /. 2.0 ** 2.0 +. cos 3.14 /. 2.0 ** 2.0 ⇒ 1.0
- sqrt 3 * 3 + 4 * 4 ⇒ 5 (整数)
Exercise 4 式 b1 && b2 を,if-式と true, false,
b1, b2 のみを用いて,同じ意味になるように書き直せ.
式 b1 || b2 も同様に書き直せ.
前節で学んだのは簡単な操作を組み合わせて,複雑な計算を行う式を組み立てる方
法である.計算した結果の値には名前をつけておいてあとで参照することができる.
これを行うのが 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
を評価した値」に束縛する.変数を宣言することには,
-
名前をつけることにより,計算結果を抽象化(abstraction)するこ
とができる.また名前によりプログラムの意味を明らかにし,間違いを減らす.
- ある計算結果を何度も使う際に,結果に名前をつけることで,計算をやり直
すことなく再利用することができる.
といった意義がある.ひとつめの観点から言えば,分かりにくい変数名を
つけることは避けるべきであり,たとえ一時的にしか使わない変数でも
意味を反映した名前をつけるべきである.ふたつめに関して補足しておくと,
変数が束縛される対象は計算結果の値であって,式自体ではないことに注意.
上で,「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" に更新されたわけではなく,
同じ名前の変数宣言により前の宣言が隠されて見えなくなっただけなのである.
そもそも前の宣言と型が一致していないことに注意.
変数のひとつひとつの使用に対して,その定義は,(以前に宣言されている物で)最
も近いものが参照される.別の言い方をすると,「let宣言の有効範囲(scope)は(再宣言で隠されない限り)宣言以降,ファイル(ocaml セッション)終了
まで」といわれる.このような定義の参照の仕方を静的有効範囲(
lexical scope, static scope) と呼ぶ.
ところで,Objective Caml では変数の型はコンパイラが自動的に推論してくれるた
め,宣言する必要がない.ただ,プログラムの意味をわかりやすくするため,
デバッグのため,変数の型を明示的に示しておきたいときは,変数名の後に
``: 〈 型 〉'' として宣言することもできる.また,複数のlet-宣
言はその境目がはっきりしている(次の let が来る直前で切れる)ので間に
;; をつけずに並べることができる.
# let pi : float = 3.1415926535
# let e = 2.718281828;;
val pi : float = 3.1415926535
val e : float = 2.718281828
二つの宣言がまとめてコンパイルされて結果がまとまって出力されていることに注目.
変数の名前
変数の名前として用いることができるのは,
-
一文字目が英小文字またはアンダースコア (_) で,
- 二文字目以降は英数字(A...Z, a...z, 0...9),アンダースコアまたは
プライム (')
であるような任意の長さの文字列で,表2.4の Objective Caml の文法キーワー
ドと _ 一文字のみからなるものを除くものである.
and |
as |
assert |
asr |
begin |
class |
closed |
constraint |
do |
done |
downto |
else |
end |
exception |
external |
false |
for |
fun |
function |
functor |
if |
in |
include |
inherit |
land |
lazy |
let |
lor |
lsl |
lsr |
lxor |
match |
method |
mod |
module |
mutable |
new |
of |
open |
or |
parser |
private |
rec |
sig |
struct |
then |
to |
true |
try |
type |
val |
virtual |
when |
while |
with |
Table 2.4: Objective Caml キーワード
2.3.2 |
環境と lexical scoping |
|
ここで高レベルな式の意味を離れて,名前参照がどのように実現されているかをみ
てみる.プログラムの実行中には,実行している時点で定義されている(有効範囲に
ある)変数名とその値の組のリストがメモリ上に保存されている.このデータのこと
を環境(environment)という.特に,プログラムの一番外側における環境
をトップレベル環境(top-level environment),または大域環
境(global environment)という.
例えば,ocaml を起動したときには,sin, max_int などの名前が大域環境に
ある状態でセッションが始まる(図2.1).
変数名 |
値 |
⋮ |
⋮ |
⋮ |
⋮ |
sin |
正弦関数 |
max_int |
1073741823 |
⋮ |
⋮ |
Figure 2.1: ocaml 起動時の大域環境
let宣言を実行する際には,このトップレベル環境の最後に,新しい変数
とその値の組が追加される(図2.2) .変数の参照は,この環境を下から
順番に変数を探して行く操作に対応する.そのため,同じ名前の変数が再定義され
た場合,上のエントリに探索が到達しないために参照することができなくなる.
大域環境からエントリが削除されることはない.
そのためlet宣言の有効範囲は宣言直後からプログラム終了までなのである.
変数名 |
値 |
⋮ |
⋮ |
⋮ |
⋮ |
one |
1 |
two |
2 |
one |
"One" |
three |
"OneOneOne" |
Figure 2.2: let宣言実行後の大域環境
Exercise 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 は型を表すメタ変数である.
関数名・パラメータ名として許される名前は変数の場合と同じである.
(実は,変数名,関数名,パラメータ名を区別する必要はない.)
値の名前と同じように,関数名・パラメータ名もわかりやすいものをつけ,
関数が何を計算するのか,コメントを書く癖をつけたい.
lexical scoping について補足
関数本体中の pi は,lexical scoping によって,
関数宣言の直前に宣言されたものが参照される.そのため,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) になる.
Exercise 6 次の関数を定義せよ.実数の切り捨てを行う関数 floor を用いてよい.
-
USドル(実数)を受け取って円(整数)に換算する関数(ただし1円以下四捨五入).
(入力は小数点以下2桁で終わるときに働けばよい.)レートは 1$ = 110.35 円とす
る.
- 円(整数)を受け取って,USドル(セント以下を小数にした実数)に換算
する関数(ただし1セント以下四捨五入).レートは 1$ = 110.35 円とす
る.
- USドル(実数)を受け取って,文字列 "〈 ドル 〉 dollars are
〈 円 〉 yen." を返す関数.
- 文字を受け取って,アルファベットの小文字なら大文字に,その他の文
字はそのまま返す関数 capitalize.(例: capitalize 'h'
⇒ 'H', capitalize '1' ⇒ '1')
- 1
- この手順を read-eval-print ループと呼ばれることもある.
- 2
- ただし C の void 型はそれに属する値をもたない.
- 3
- ML の流儀としては
引数のまわりの()は(可能な場合は)省略される.ひとつには数学的な記法
により近い(sin(θ) とは余り書かないですね?)ということから
である.式が複雑でどう結合するかわかりにくい場合には,上の最後の例の
ように関数呼び出し全体を()で括る.
- 4
- コンパイルと静的な型づけの間に直接の関係はない.
Lisp は動的にチェックが行われるがコンパイラが存在するし,
インタプリタ実行する言語に静的型システムを導入することができる.
- 5
- 先に見た 0 での除算は,型エラーではないとしたが,
(Objective Camlでは)その結果が言語内の概念である例外の発生として
定義されており,しかも実行中にその発生を検知することができる.
(C では OS の助けを借りないと,0での除算の発生を検知することはできない.)
- 6
- テキストを通じて関数宣言の文法は徐々に拡大されていく.