Objective Caml

The original edition of this book (ISBN 2-84177-121-0) was published in France by O’REILLY & Associates under the title Dveloppement d’applications av...

0 downloads 186 Views 3MB Size
Developing Applications With

Objective Caml

和訳スナップショット: October 3, 2004

Emmanuel Chailloux Pascal Manoury Bruno Pagano

Developing Applications With

Objective Caml Translated by Francisco Albacete • Mark Andrew • Martin Anlauf • Christopher Browne • David Casperson • Gang Chen • Harry Chomsky • Ruchira Datta • Seth Delackner • Patrick Doane • Andreas Eder • Manuel Fahndrich • Joshua Guttman • Theo Honohan • Xavier Leroy • Markus Mottl • Alan Schmitt • Paul Steckler • Perdita Stevens • Fran¸cois Thomasset 和訳(五十音順) 五十嵐 淳・Jacques Garrigue・住井 英二郎・関口 龍郎・ 玉田 嘉紀・富沢 伸行・橋本 政朋・原 耕司・ 坂内 英夫・古瀬 淳・細谷 晴夫・脇田 建 ´ Editions O’REILLY 18 rue S´eguier 75006 Paris FRANCE [email protected]

Cambridge • Cologne • Farnham • Paris • P´ekin • Sebastopol • Taipei • Tokyo

The original edition of this book (ISBN 2-84177-121-0) was published in France by O’REILLY & Associates under the title Dveloppement d’applications avec Objective Caml. Historique : •

Version 19990324???????????

c O’REILLY & Associates, 2000 °

Cover concept by Ellie Volckhausen.

´ Edition : Xavier Cazin.

Les programmes figurant dans ce livre ont pour but d’illustrer les sujets trait´es. Il n’est donn´e aucune garantie quant `a leur fonctionnement une fois compil´es, assembl´es ou interpr´et´es dans le cadre d’une utilisation professionnelle ou commerciale.

´ c Editions ° O’Reilly, Paris, 2000 ISBN

Toute repr´esentation ou reproduction, int´egrale ou partielle, faite sans le consentement de l’auteur, de ses ayants droit, ou ayants cause, est illicite (loi du 11 mars 1957, alin´ea 1er de l’article 40). Cette repr´esentation ou reproduction, par quelque proc´ed´e que ce soit, constituerait une contrefa¸con sanctionn´ee par les articles 425 et suivants du Code p´enal. La loi du 11 mars 1957 autorise uniquement, aux termes des alin´eas 2 et 3 de l’article 41, les copies ou reproductions strictement r´eserv´ees ` a l’usage priv´e du copiste et non destin´ees ` a une utilisation collective d’une part et, d’autre part, les analyses et les courtes citations dans un but d’exemple et d’illustration.

前書き Objective Caml の教科書を書きたいという欲求は、著者らが Objective Caml 言語を通 してプログラミングとは何かを教えていた間に得た教育体験から沸いてきたものです。 Pierre et Marie Curie 大学には、様々な分野を専門にする学生や、社会人教育を受けて いる技術者たちがいましたが、彼らの積極性と批判力のおかげで、この本での言語の説 明の仕方は大幅な改良をすることができました。特に、この本で出てくる例のいくつか は、彼らのプロジェクトから直接思いついたものです。 Caml 言語の開発は、15 年もの間ずっと続けられてきました。この開発は、初めは Formel プロジェクトとして、続いて Cristal プロジェクトとして、INRIA にて、Denis Diderot ´ 大学とEcole Normale Sup´eieure の共同で行われてきました。これらのチームにいた研 究者たちのたゆまぬ努力(開発もさながら、理論的な下支えの構築も)は、長年にわたっ て最高品質の言語を産み出してきました。彼らは、プログラミング言語の分野の止まら ざる進歩についていくと同時に、新しいプログラミングパラダイムを理論的枠組みに取 り入れていくことのできる人たちだったのです。彼らの言語は、広く普及するに値する でしょう。私たちは、この著作を通じて、それに貢献できればと思っています。 数々の同士たちの協力がなければ、この本は今あるような形にはならなかったでしょう。 彼らは、初期の草稿を嫌がらず何度も読んでくれました。執筆の間、常に彼らのコメント のおかげで説明方法を改善することができたのです。私たちが特に感謝を捧げたい方々 は、Mar´ıa-Virginia Aponte、Sylvain Baro、Christian Codognet、H´el`ene Cottier、Guy Cousineau、Pierre Cr´egut、Titou Durand、Christophe Gonzales、Michelle Morcrette、 Christian Queinnec、Attila Raksany、Didier R´emy の各氏です。 この本の HTML 版は、hevea と VideoC というツールがなければ日の目を見なかったで しょう。それぞれの作者 Luc Maranget 氏と Christian Queinnec 氏は、私たちの質問や ツールへの変更希望に対して常に短時間で返事をしてくれました。深く感謝します。

Contents v

前書き

Table of contents

vii

はじめに

xxi

1:

How to obtain Objective Caml

Description of the CD-ROM . . . . . . . . . . . . Downloading . . . . . . . . . . . . . . . . . . . . Installation . . . . . . . . . . . . . . . . . . . . . Installation under Windows . . . . . . . Installation under Linux . . . . . . . . . Installation under MacOS . . . . . . . . . Installation from source under Unix . . . Installation of the HTML documentation Testing the installation . . . . . . . . . . . . . .

I

2:

Language Core

関数型プログラミング

. . . . . . . . .

1 . . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

1 2 2 2 4 4 5 5 5

7

11

Objective Caml の核となる関数型の部分 . . . . . . . . . . . . . . . . . . . . . . . 12 プリミティブな値、関数、および型 . . . . . . . . . . . . . . . . . . . . . 12 条件制御構造 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

viii

Table of Contents

値の宣言 . . . . . . . . . . 関数式、関数 . . . . . . . . 多相性と型制約 . . . . . . 例 . . . . . . . . . . . . . . 型宣言とパターンマッチング . . . パターンマッチング . . . . 型宣言 . . . . . . . . . . . レコード . . . . . . . . . . 和型 . . . . . . . . . . . . . 再帰型 . . . . . . . . . . . パラメータ化された型 . . . 宣言のスコープ . . . . . . 関数型 . . . . . . . . . . . 例:木の表現 . . . . . . . . 関数以外の再帰的値 . . . . 型付け、定義域、例外 . . . . . . . 部分関数と例外 . . . . . . 例外の定義 . . . . . . . . . 例外の発生 . . . . . . . . . 例外処理 . . . . . . . . . . 多相性と関数の返り値 . . . . . . . 電卓 . . . . . . . . . . . . . . . . . 練習問題 . . . . . . . . . . . . . . . 二つのリストをマージする 字句木 . . . . . . . . . . . Graph traversal . . . . . . Summary . . . . . . . . . . . . . . To learn more . . . . . . . . . . . .

3:

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . .

65

手続き型プログラミング

変更可能データ構造 . . . . . . . . . . ベクトル . . . . . . . . . . . . 文字列 . . . . . . . . . . . . . レコードの変更可能フィールド 参照 . . . . . . . . . . . . . . . 多相性と書き換え可能な値 . . 入出力 . . . . . . . . . . . . . . . . . . チャネル . . . . . . . . . . . . 読み書き . . . . . . . . . . . . 例:数当てゲーム . . . . . . . 制御構造 . . . . . . . . . . . . . . . . . 連接 . . . . . . . . . . . . . . . 繰り返し . . . . . . . . . . . . 例:スタックの実装 . . . . . . 例:行列計算 . . . . . . . . . . 引数の評価順序 . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

19 21 28 31 33 33 41 42 44 47 47 48 49 50 52 53 53 54 55 56 57 59 62 62 62 63 64 64

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

66 66 70 71 72 72 74 75 75 76 77 77 79 80 82 84

Table of Contents メモリつき電卓 . . . . . . . . 練習問題 . . . . . . . . . . . . 二重リンクリスト . . 連立一次方程式を解く まとめ . . . . . . . . . . . . . もっと知りたい方へ . . . . .

4:

ix . . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

Functional and Imperative Styles

Comparison between Functional and Imperative The Functional Side . . . . . . . . . . . . The Imperative Side . . . . . . . . . . . . Recursive or Iterative . . . . . . . . . . . Which Style to Choose? . . . . . . . . . . . . . . Sequence or Composition of Functions . . Shared or Copy Values . . . . . . . . . . How to Choose your Style . . . . . . . . Mixing Styles . . . . . . . . . . . . . . . . . . . . Closures and Side Effects . . . . . . . . . Physical Modifications and Exceptions . Modifiable Functional Data Structures . Lazy Modifiable Data Structures . . . . . Streams of Data . . . . . . . . . . . . . . . . . . Construction . . . . . . . . . . . . . . . . Destruction and Matching of Streams . . Exercises . . . . . . . . . . . . . . . . . . . . . . Binary Trees . . . . . . . . . . . . . . . . Spelling Corrector . . . . . . . . . . . . . Set of Prime Numbers . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . To Learn More . . . . . . . . . . . . . . . . . . .

5:

. . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

89 . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

The Graphics Interface

Using the Graphics Module . . . . . . . . . . . . . . . Basic notions . . . . . . . . . . . . . . . . . . . . . . . Graphical display . . . . . . . . . . . . . . . . . . . . . Reference point and graphical context . . . . . Colors . . . . . . . . . . . . . . . . . . . . . . . Drawing and filling . . . . . . . . . . . . . . . Text . . . . . . . . . . . . . . . . . . . . . . . . Bitmaps . . . . . . . . . . . . . . . . . . . . . Example: drawing of boxes with relief patterns Animation . . . . . . . . . . . . . . . . . . . . . . . . . Events . . . . . . . . . . . . . . . . . . . . . . . . . . . Types and functions for events . . . . . . . . . Program skeleton . . . . . . . . . . . . . . . .

84 87 87 87 88 88

. . . . . . . . . . . . . . . . . . . . . .

90 91 91 93 94 95 97 99 101 101 103 103 105 108 108 110 114 114 114 115 115 115

117 . . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

118 118 119 119 120 121 123 125 126 130 132 132 133

x

Table of Contents

Example: telecran A Graphical Calculator . Exercises . . . . . . . . . Polar coordinates Bitmap editor . . Earth worm . . . Summary . . . . . . . . . To learn more . . . . . . .

6:

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

7:

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

Applications . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

Development Tools

134 136 141 141 142 143 144 144

. . . . . . . . . . . . . . . . . . . . .

148 148 150 151 153 156 157 159 159 160 162 163 165 170 174 176 176 177 182 188 192

193

Compilation and Portability

Steps of Compilation . . . . . . . . . . . . . . The Objective Caml Compilers . . . . Description of the Bytecode Compiler Compilation . . . . . . . . . . . . . . . . . . . Command Names . . . . . . . . . . . Compilation Unit . . . . . . . . . . . Naming Rules for File Extensions . . The Bytecode Compiler . . . . . . . . Native Compiler . . . . . . . . . . . .

. . . . . . . .

147

Database queries . . . . . . . . . . . . . . . . . . . Data format . . . . . . . . . . . . . . . . . Reading a database from a file . . . . . . . General principles for database processing Selection criteria . . . . . . . . . . . . . . . Processing and computation . . . . . . . . An example . . . . . . . . . . . . . . . . . Further work . . . . . . . . . . . . . . . . . BASIC interpreter . . . . . . . . . . . . . . . . . . Abstract syntax . . . . . . . . . . . . . . . Program pretty printing . . . . . . . . . . Lexing . . . . . . . . . . . . . . . . . . . . Parsing . . . . . . . . . . . . . . . . . . . . Evaluation . . . . . . . . . . . . . . . . . . Finishing touches . . . . . . . . . . . . . . Further work . . . . . . . . . . . . . . . . . Minesweeper . . . . . . . . . . . . . . . . . . . . . The abstract mine field . . . . . . . . . . . Displaying the Minesweeper game . . . . . Interaction with the player . . . . . . . . . Exercises . . . . . . . . . . . . . . . . . . .

II

. . . . . . . .

. . . . . . . . .

197 . . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

198 198 199 201 201 201 202 202 204

Table of Contents

xi

Toplevel Loop . . . . . . . . . . . . . . . . . . . . Construction of a New Interactive System . . . . Standalone Executables . . . . . . . . . . . . . . . . . . . Portability and Efficiency . . . . . . . . . . . . . . . . . . Standalone Files and Portability . . . . . . . . . . Efficiency of Execution . . . . . . . . . . . . . . . Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . Creation of a Toplevel and Standalone Executable Comparison of Performance . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . To Learn More . . . . . . . . . . . . . . . . . . . . . . . .

8:

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

Libraries

Categorization and Use of the Libraries Preloaded Library . . . . . . . . . . . . Standard Library . . . . . . . . . . . . . Utilities . . . . . . . . . . . . . . Linear Data Structures . . . . . Input-output . . . . . . . . . . . Persistence . . . . . . . . . . . . Interface with the System . . . . Other Libraries in the Distribution . . . Exact Math . . . . . . . . . . . Dynamic Loading of Code . . . Exercises . . . . . . . . . . . . . . . . . Resolution of Linear Systems . . Search for Prime Numbers . . . Displaying Bitmaps . . . . . . . Summary . . . . . . . . . . . . . . . . . To Learn More . . . . . . . . . . . . . .

9:

. . . . . . . . . . .

. . . . . . . . . . .

205 206 207 208 208 208 209 209 209 210 210

213 . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

214 215 215 216 217 223 228 234 239 240 242 245 245 245 246 246 246

249

ガーベージコレクション

プログラムの使用するメモリ . . . . . . . . . . . メモリの割り当てと解放 . . . . . . . . . . . . . . 明示的な割り当て . . . . . . . . . . . . . 明示的な解放 . . . . . . . . . . . . . . . . 暗黙の解放 . . . . . . . . . . . . . . . . . ガーベージコレクション . . . . . . . . . . . . . . 参照カウント . . . . . . . . . . . . . . . . スイープアルゴリズム . . . . . . . . . . . マーク・アンド・スイープ . . . . . . . . ストップ・アンド・コピー . . . . . . . . 他のガーベージコレクションアルゴリズム Objective Caml でのメモリ管理 . . . . . . . . . . Gc モジュール . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

250 251 251 252 253 253 254 255 256 258 261 263 265

xii

Table of Contents

Weak モジュール . . . . . . . . . . . . . . . . . 練習問題 . . . . . . . . . . . . . . . . . . . . . . ヒープの変化の追跡 . . . . . . . . . . . メモリ使用量とプログラミングスタイル まとめ . . . . . . . . . . . . . . . . . . . . . . . もっと知りたい人へ . . . . . . . . . . . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

10 : プログラム解析ツール 依存性解析 . . . . . . . . . デバッグツール . . . . . . . Trace(トレース) . デバッグ . . . . . . 実行の制御 . . . . . プロファイリング . . . . . . コンパイル方法 . . プログラムの実行 . 結果の出力 . . . . . 練習問題 . . . . . . . . . . . 関数適用のトレース 性能解析 . . . . . . まとめ . . . . . . . . . . . . もっと知りたい人へ . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

267 270 270 271 271 272

273 . . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

11 : 字句解析と構文解析のためのツール 語彙 . . . . . . . . . . . . . . . . . . Genlex モジュール . . . . . 文字の流れの制御 . . . . . . 正規表現 . . . . . . . . . . . Str ライブラリ . . . . . . . ocamllex ツール . . . . . . . 構文 . . . . . . . . . . . . . . . . . . 文法 . . . . . . . . . . . . . . 生成と認識 . . . . . . . . . . トップダウン解析 . . . . . . ボトムアップ解析 . . . . . . ocamlyacc ツール . . . . . . 文脈依存文法 . . . . . . . . . Basic 再考 . . . . . . . . . . . . . . . basic parser.mly ファイル basic lexer.mll ファイル . コンパイルとリンク . . . . . 練習問題 . . . . . . . . . . . . . . . . コメントの除去 . . . . . . . 評価器 . . . . . . . . . . . . まとめ . . . . . . . . . . . . . . . . .

. . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . .

274 275 275 280 281 282 283 283 285 287 287 287 287 288

289 . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . .

290 290 291 292 294 295 297 297 298 299 302 305 308 309 310 312 313 314 314 314 315

Table of Contents

xiii

もっと知りたい人へ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315

12 : Interoperability with C

317

Communication between C and Objective Caml . . . . . . . . . . External declarations . . . . . . . . . . . . . . . . . . . . Declaration of the C functions . . . . . . . . . . . . . . . Linking with C . . . . . . . . . . . . . . . . . . . . . . . . Mixing input-output in C and in Objective Caml . . . . Exploring Objective Caml values from C . . . . . . . . . . . . . . Classification of Objective Caml representations . . . . . Accessing immediate values . . . . . . . . . . . . . . . . . Representation of structured values . . . . . . . . . . . . Creating and modifying Objective Caml values from C . . . . . . Modifying Objective Caml values . . . . . . . . . . . . . Allocating new blocks . . . . . . . . . . . . . . . . . . . . Storing C data in the Objective Caml heap . . . . . . . . Garbage collection and C parameters and local variables Calling an Objective Caml closure from C . . . . . . . . Exception handling in C and in Objective Caml . . . . . . . . . Raising a predefined exception . . . . . . . . . . . . . . . Raising a user-defined exception . . . . . . . . . . . . . . Catching an exception . . . . . . . . . . . . . . . . . . . . Main program in C . . . . . . . . . . . . . . . . . . . . . . . . . . Linking Objective Caml code with C . . . . . . . . . . . Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Polymorphic Printing Function . . . . . . . . . . . . . . . Matrix Product . . . . . . . . . . . . . . . . . . . . . . . Counting Words: Main Program in C . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . To Learn More . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . .

13 : Applications グラフィカルインターフェースの構築 . . . . . . . . . . . . グラフィックスコンテキスト、イベント、オプション コンポーネントとコンテナ . . . . . . . . . . . . . . イベントハンドリング . . . . . . . . . . . . . . . . . コンポーネントの定義 . . . . . . . . . . . . . . . . . 拡張コンポーネント . . . . . . . . . . . . . . . . . . Awi ライブラリのセットアップ . . . . . . . . . . . . Example: A フラン-ユーロ変換器 . . . . . . . . . . もっと知りたい方は . . . . . . . . . . . . . . . . . . 最小コスト経路を見つける . . . . . . . . . . . . . . . . . . . グラフの表現 . . . . . . . . . . . . . . . . . . . . . . ダイクストラ法 . . . . . . . . . . . . . . . . . . . . キャッシュの紹介 . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . .

319 320 320 322 325 325 326 327 328 336 337 337 338 342 343 345 345 345 346 348 348 348 348 349 349 350 350

353 . . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

. . . . . . . . . . . . .

353 354 358 362 367 378 380 381 384 384 385 389 393

xiv

Table of Contents グラフィカルインターフェース . . . . . . . . . . . . . . . . . . . . . . . . 395 スタンドアロンアプリケーションの作成 . . . . . . . . . . . . . . . . . . . 401 最後に . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 404

III

Application Structure

405

14 : モジュールを使ったプログラミング コンパイル単位としてのモジュール . . . . . . インターフェースと実装 . . . . . . . . インターフェースと実装を関連づける 分割コンパイル . . . . . . . . . . . . モジュール言語 . . . . . . . . . . . . . . . . . ふたつのスタックモジュール . . . . . モジュールと情報隠蔽 . . . . . . . . . モジュール間の型共有 . . . . . . . . . 単純なモジュールを拡張する . . . . . パラメータつきモジュール . . . . . . . . . . . ファンクタとコード再利用 . . . . . . モジュールの局所定義 . . . . . . . . . より大きな例: 銀行口座の管理 . . . . . . . . . プログラムの構成 . . . . . . . . . . . モジュールパラメータのシグネチャ . 口座管理用パラメータつきモジュール パラメータの実装 . . . . . . . . . . . 練習問題 . . . . . . . . . . . . . . . . . . . . . 連想リスト . . . . . . . . . . . . . . . パラメータつきベクトル . . . . . . . . 字句解析木 . . . . . . . . . . . . . . . まとめ . . . . . . . . . . . . . . . . . . . . . . もっと学びたい人のために . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

409 . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

15 : オブジェクト指向プログラミング クラス、オブジェクト、メソッド . . . . . . . . . . . オブジェクト指向についての用語 . . . . . . . クラス宣言 . . . . . . . . . . . . . . . . . . . インスタンス生成 . . . . . . . . . . . . . . . メッセージ送信 . . . . . . . . . . . . . . . . クラスの間の関係 . . . . . . . . . . . . . . . . . . . . 集約 . . . . . . . . . . . . . . . . . . . . . . . 継承関係 . . . . . . . . . . . . . . . . . . . . その他のオブジェクト指向の機能 . . . . . . . . . . . 特別な参照 self と super . . . . . . . . . . 遅延束縛 . . . . . . . . . . . . . . . . . . . . オブジェクトの内部表現とメッセージの発送

. . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

410 410 412 413 414 415 418 420 422 423 424 427 428 428 428 430 432 436 436 436 437 437 437

439 . . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

440 440 441 444 444 445 445 447 449 449 450 451

Table of Contents

xv

初期化 . . . . . . . . . . . . . . . プライベートメソッド . . . . . . . 型と総称性 . . . . . . . . . . . . . . . . . 抽象クラスと抽象メソッド . . . . クラス、型、オブジェクト . . . . 多重継承 . . . . . . . . . . . . . . 型パラメータを持つクラス . . . . 部分型と包含的多相性 . . . . . . . . . . . Example . . . . . . . . . . . . . . 部分型関係は継承ではない . . . . 包含的多相性 . . . . . . . . . . . . オブジェクトの等価性 . . . . . . . 関数型スタイル . . . . . . . . . . . . . . . オブジェクト拡張機能の他の部分について インターフェース . . . . . . . . . クラス内での局所宣言 . . . . . . . 練習問題 . . . . . . . . . . . . . . . . . . . オブジェクトによるスタック . . . 遅延束縛 . . . . . . . . . . . . . . 抽象クラスと式の評価器 . . . . . . ライフゲームとオブジェクト . . . まとめ . . . . . . . . . . . . . . . . . . . . もっと知りたい人へ . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . .

16 : アプリケーションの構成モデルの比較 モジュールとオブジェクトの比較 . . . . . . モジュールをクラスに変換する . . . モジュールにおける継承の実現 . . . それぞれのモデルの限界 . . . . . . . コンポーネントの拡張 . . . . . . . . . . . . 関数型モデルの場合 . . . . . . . . . オブジェクトモデルの場合 . . . . . データとメソッドの拡張 . . . . . . . 混在した構成 . . . . . . . . . . . . . . . . . 練習問題 . . . . . . . . . . . . . . . . . . . . データ構造へのクラスとモジュール 抽象型 . . . . . . . . . . . . . . . . まとめ . . . . . . . . . . . . . . . . . . . . . さらに学びたい人のために . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

452 453 454 455 457 461 464 470 470 471 473 474 475 478 479 480 482 482 483 484 485 485 485

487 . . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

17 : アプリケーション 二人ゲーム . . . . . . . . . . . . . . . 二人プレーヤー用ゲームの問題 ミニマックス αβ . . . . . . . . ゲームプログラムの構造 . . . .

. . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . .

488 491 493 494 496 497 497 499 501 502 502 502 503 503

505 . . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

505 506 506 514

xvi

Table of Contents

Connect Four . . . . . . . Stonehenge . . . . . . . . . ファンシー・ロボット . . . . . . . 「抽象」ロボット . . . . . 純粋仮想世界 . . . . . . . . 文字ロボット . . . . . . . . テキストの仮想世界 . . . . グラフィカルなロボット . . グラフィカルな世界 . . . . さらに学びたい人のために

IV

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

. . . . . . . . . .

519 531 554 555 557 558 560 562 565 567

569

並行分散プログラミング

18 : 通信とプロセス Unix モジュール . . . . . . . . . . . . エラー処理 . . . . . . . . . . . システムコールの互換性 . . . . ファイル記述子 . . . . . . . . . . . . . ファイル操作 . . . . . . . . . . ファイル入出力 . . . . . . . . プロセス . . . . . . . . . . . . . . . . . プログラムの実行 . . . . . . . プロセス生成 . . . . . . . . . . 複製によるプロセス生成 . . . . 実行の順序と継続時間 . . . . . プロセスの死、プロセスの葬儀 プロセス間通信 . . . . . . . . . . . . . 通信パイプ . . . . . . . . . . . 通信チャネル . . . . . . . . . . Unix のシグナル . . . . . . . . 練習問題 . . . . . . . . . . . . . . . . . 語数計算: wc . . . . . . . . . . パイプを使ったスペルチェック 計算状態の対話的な取得 . . . . まとめ . . . . . . . . . . . . . . . . . . もっと知りたい人へ . . . . . . . . . .

. . . . . . . . . .

575 . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

19 : 並行プログラミング 並行プロセス . . . . . . . . . . . . . . . . . . . . スレッドを使ったプログラムのコンパイル Thread モジュール . . . . . . . . . . . . プロセスの同期 . . . . . . . . . . . . . . . . . . . クリティカルセクションと相互排除 . . . Mutex モジュール . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . .

576 577 577 577 579 580 582 583 584 586 588 589 591 591 592 594 598 599 599 599 600 600

601 . . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

602 603 604 606 606 606

Table of Contents 待機と同期 . . . . . . . . . . . . . . . . Condition モジュール . . . . . . . . . 同期通信 . . . . . . . . . . . . . . . . . . . . . . 通信イベントを使った同期 . . . . . . . 転送される値 . . . . . . . . . . . . . . . Event モジュール . . . . . . . . . . . . 例題: 郵便局 . . . . . . . . . . . . . . . . . . . . 資源と主体の実装 . . . . . . . . . . . . 客と職員 . . . . . . . . . . . . . . . . . システム全体 . . . . . . . . . . . . . . . 練習問題 . . . . . . . . . . . . . . . . . . . . . . 食事にありつく哲学者 . . . . . . . . . . 郵便局の拡張 . . . . . . . . . . . . . . . 生産者消費者のオブジェクトバージョン まとめ . . . . . . . . . . . . . . . . . . . . . . . もっと知りたい人へ . . . . . . . . . . . . . . .

xvii . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

20 : 分散プログラミング インターネット(The Internet) . . . . . . . . . . . . . . . . . . . . . . Unix モジュールと IP アドレッシング . . . . . . . . . . . . . . . ソケット . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 記述と生成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . アドレスと接続 . . . . . . . . . . . . . . . . . . . . . . . . . . . クライアント‐サーバ . . . . . . . . . . . . . . . . . . . . . . . . . . . . クライアント‐サーバの動作モデル . . . . . . . . . . . . . . . . クライアント‐サーバプログラミング . . . . . . . . . . . . . . . サーバのコード . . . . . . . . . . . . . . . . . . . . . . . . . . . telnet を利用したテスト . . . . . . . . . . . . . . . . . . . . . . クライアントコード . . . . . . . . . . . . . . . . . . . . . . . . . ライト級プロセスを用いたクライアント‐サーバプログラミング 多段クライアント‐サーバプログラミング . . . . . . . . . . . . . クライアント‐サーバプログラムに関するコメント . . . . . . . . 通信プロトコル . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . テクストプロトコル . . . . . . . . . . . . . . . . . . . . . . . . . 確認応答と時間制限のあるプロトコル . . . . . . . . . . . . . . . 内部表現のままの値の転送 . . . . . . . . . . . . . . . . . . . . . 異言語間相互運用 . . . . . . . . . . . . . . . . . . . . . . . . . . 演習問題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . サービス: 時計 . . . . . . . . . . . . . . . . . . . . . . . . . . . ネットワーク コーヒー自動販売機 . . . . . . . . . . . . . . . . . まとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . さらに進んだ話題 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

21 : アプリケーション

. . . . . . . . . . . . . . . .

611 611 615 615 615 616 618 618 620 622 622 622 622 623 624 624

625 . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . .

626 628 629 630 631 632 633 633 634 637 637 641 644 644 645 646 648 649 649 650 650 650 651 651

653

Client-server Toolbox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 653

xviii

Table of Contents

Protocols . . . . . . . . . . . . . . . . . . . . . . . . . . Communication . . . . . . . . . . . . . . . . . . . . . . Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . To Learn More . . . . . . . . . . . . . . . . . . . . . . . The Robots of Dawn . . . . . . . . . . . . . . . . . . . . . . . . World-Server . . . . . . . . . . . . . . . . . . . . . . . . Observer-client . . . . . . . . . . . . . . . . . . . . . . . Robot-Client . . . . . . . . . . . . . . . . . . . . . . . . To Learn More . . . . . . . . . . . . . . . . . . . . . . . HTTP Servlets . . . . . . . . . . . . . . . . . . . . . . . . . . . HTTP and CGI Formats . . . . . . . . . . . . . . . . . HTML Servlet Interface . . . . . . . . . . . . . . . . . . Dynamic Pages for Managing the Association Database Analysis of Requests and Response . . . . . . . . . . . Main Entry Point and Application . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

22 : Objective Caml でのアプリケーション開発 評価項目 . . . . . . . . . . . . . . . . . . . . . . 言語 . . . . . . . . . . . . . . . . . . . . ライブラリとツール . . . . . . . . . . . 文書 . . . . . . . . . . . . . . . . . . . . 他の開発ツール . . . . . . . . . . . . . . . . . . 編集ツール . . . . . . . . . . . . . . . . 文法拡張 . . . . . . . . . . . . . . . . . 他言語インターフェース . . . . . . . . . グラフィックインターフェース . . . . . 並列分散プログラミング . . . . . . . . . Objective Caml で開発されたアプリケーション 類似の関数型言語 . . . . . . . . . . . . . . . . . ML 系列 . . . . . . . . . . . . . . . . . Scheme . . . . . . . . . . . . . . . . . . 遅延評価の機能を持つ言語 . . . . . . . 通信記述言語 . . . . . . . . . . . . . . . オブジェクト指向言語 — Java との比較 . . . . 主な特徴 . . . . . . . . . . . . . . . . . Objective Caml との違い . . . . . . . . Objective Caml 開発の将来 . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

654 654 655 657 658 658 659 663 664 666 666 667 672 675 677 677

681 . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

682 682 683 684 684 684 685 685 685 686 687 688 688 689 689 692 692 693 693 694

おわりに

697

V

699

A:

Appendices

Cyclic Types

701

Table of Contents

xix

Cyclic types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 701 Option -rectypes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 703

B:

Objective Caml 3.04

Language Extensions . . . . . Labels . . . . . . . . Optional arguments . Labels and objects . . Polymorphic variants LablTk Library . . . . . . . . OCamlBrowser . . . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

707 . . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

707 708 710 712 713 716 716

参考文献

719

概念索引

723

言語要素索引

729

和訳担当 前書き・はじめに: 細谷, 1 章: (未定), 2 章: 住井, 3 章: 細谷, 4 章: 脇田, 5 章: 古瀬, 6 章: (未定), 7 章: 玉田, 8 章: 坂内, 9–11 章: 関口, 12 章: Garrigue, 13 章: 原, 14 章: 五十嵐, 15 章: 関口, 16–17 章: 富沢, 18–19 章: 関口, 20 章: 橋本, 21–22 章: 関口, 参 考文献: 富沢

はじめに Objective Caml はプログラミング言語の一つです。どうしてまた新しい言語が要るの か?と聞く人もいるかも知れません。実際、今すでに数多くの言語が存在していますし、 新しいものもどんどん現れています。まずこれらの言語の違いはさておき、それぞれの 理念と起源は、共通した動機からきています。それは「抽象化したい」ということです。 計算機の抽象化 まずプログラミング言語では、計算機の「機械」的な部分を無視できる ようになります。マイクロプロセッサやオペレーティングシステムさえも、プログ ラムがその上を走っているのにもかかわらず、忘れ去ることができます。 操作的モデルの抽象化 大概の言語は、関数という概念を何らかの形で持っていますが、 それは数学から借りてきた概念であって、電子工学からではありません。一般に言 語は、純粋に計算機的な視点ではなく、数学的モデルで考えさせてくれます。こう して言語は表現力を得るのです。 エラーの抽象化 これは、実行の安全性を保証しようという話です。プログラムは、エ ラーが発生したときに突如停止したり不整合を起こしたりするべきではありませ ん。安全性の保証を得るひとつの方法は、プログラムに強い静的型付けを施し、適 切な場所で例外機構を使用するということです。 部品の抽象化(i)プログラミング言語では、一つのアプリケーションを様々なおおよそ 独立自立したソフトウェア部品に分解できるようになります。モジュールを使うと、 複雑な一つのアプリケーション全体を高度な方法で構造化することができます。 部品の抽象化(ii)プログラムの単位というものがあるおかげで、今度はそれを、開発 時に想定されていた場所以外で再利用できる可能性が出てきました。オブジェクト 指向言語は、速攻プロトタイピングを可能にする再利用性への新しいアプローチ です。

Objective Caml は、最近現れた言語であり、プログラミング言語の歴史の中では Lisp の 遠い子孫と位置づけられます。また、従兄にあたる言語からの教訓を生かしたり、その

xxii

はじめに

ほかの言語からも主要概念を取り込んだりしてきました。この言語は、INRIA1 で開発さ れ、長期に渡る ML 言語族の概念構築の経験の上に成り立っています。Objective Caml は、記号および数値アルゴリズムを記述するための汎用言語です。オブジェクト指向で もあり、またパラメータつきモジュールシステムも備えています。並列・分散アプリケー ションの開発のための機能もサポートしています。静的型チェック、例外機構、ガベージ コレクションのおかげで優れた実行時安全性を提供しています。高速で、しかもポータ ブルです。開発環境も充実しています。 しかしながら、Objective Caml は「一般人」向けに宣伝される機会がありませんでした。 これは、この本の著者たちに課せられた任務です。この著作には3つの目的があります。

1.

Objective Caml 言語、およびそのライブラリ、開発環境を詳しく解説すること。

2.

Objective Caml で使えるプログラミングスタイルの裏には、どういう概念が潜ん でいるのかを明らかにすること。

3.

多数の例を通して、Objective Caml がいかに多種多様なアプリケーションの開発 言語として役に立つかということを例示すること。

著者の最終目標は、どのようにプログラミングスタイルを選び、どのようにプログラム を構築するかということへの知見を提供することです。プログラムを組むときには、与 えられた問題と整合性を持つように組むべきで、そうすることによってそのプログラム が維持可能になり、その部品が再利用可能になるのです。

言語の特徴 Objective Caml は関数型言語です。 この言語では、関数を値として扱うことができ ます。これらはさらに他の関数への引数として、もしくは関数の結果として返すことが できます Objective Caml は静的型付きです。 実引数と仮引数の型の整合性は、プログラムの コンパイル時に検査されます。その後、プログラムの実行時にはそういう検査は不要に なるので、効率が向上します。さらに、静的型判定によって、打ち間違いや思慮不足か ら生じるエラーのほとんどを除去でき、実行時の安全性に貢献します。 Objective Caml はパラメータ的多相性を備えています。 もしある関数が、引数の データ構造全体をなめることがない場合、その引数の型は完全に決定されていなくても よくなります。このとき、この引数は多相的であるといいます。この機能によって、い ろいろなデータ構造のために利用できる汎用的なコードを開発することができるように なります。このコードは、受け取るデータ構造の表現を完全に知っている必要がありま せん。型付けアルゴリズムが、表現を識別する任務を担います。

1. Institut National de Recherche en Informatique et Automatique (National Institute for Research in Automation and Information Technology).

はじめに

xxiii

Objective Caml は型推論を備えています。 プログラマは、プログラムに型情報を全 く与えなくて構いません。この言語は、現れる式や宣言の型として最も一般的なものを、 コードから自動的に導き出してくれます。この推論は、検査と同時に、プログラムコン パイル時に行われます。 Objective Caml は例外機構を備えています。 この機能のおかげで、プログラムは通 常実行をある場所で中断して、他の場所で再開するということが可能になります。この 機構は、例外的な状況の制御に使えることはもちろん、一つのプログラミングスタイル として利用することもできます。 Objective Caml は手続き型の機能も備えています。 入出力、値の物理的更新、繰り 返し制御構造が、関数型プログラミング機能の助けを借りずに使えます。これら2つの スタイルを混ぜてもよく、それによって非常に柔軟な開発ができ、また新しい種類のデー タ構造を定義することもできます。 Objective Caml はスレッドを走らせることができます。 有メモリの管理、スレッド間通信が組み込まれています。

スレッドの生成、同期、共

Objective Caml は Internet で通信できます。 異なるマシン間の通信チャネルを開 くために必要な関数が組み込まれており、クライアント=サーバアプリケーションの開 発が可能です。 Objective Caml にはライブラリがたくさん揃っています。 古典的なデータ構造、入 出力、システム資源とのインタフェース、字句・構文解析、大きな数値を使った計算、永 続的な値など。 Objective Caml は開発環境を提供します。 応答型トップレベル、実行トレース、依 存関係の計算、プロファイルなど。 Objective Caml は C 言語とインタフェースをとることができます。 これは、C の 関数を Objective Caml プログラムから呼んだり、またその逆によって行います。これに よって数多くの C のライブラリへアクセスすることが可能になります。 Objective Caml には3つの実行モードがあります。 トップレベルによるインタラク ティブモード、仮想機械によって解釈実行されるバイトコードへのコンパイル、ネイティ ブ機械コードへのコンパイルです。これによってプログラマは、開発の柔軟性、異なる アーキテクチャ間でのオブジェクトコードの可搬性、特定のアーキテクチャでの実行効 率、という3つの選択肢を得ることになります。

xxiv

はじめに

プログラムの構成 重要なアプリケーションの開発では、プログラマもしくは開発チームは、プログラムを どう整理するか、どう構成するかという問題を考えることが要求されます。Objective Caml では、利点と機能の違う2つのモデルが用意されております。

パラメータつきモジュールモデル データと手続きは、ひとつの入れ物の中にまとめる ことができます。その入れ物は、コードそのものと、インタフェースという2つの側面 を持っています。モジュール間の通信はインタフェースを通して行われます。型の定義 は、隠れてもよい、つまりモジュールインタフェースに現れなくても構いません。この ような抽象的なデータ型を使うと、モジュールの内部実装を変更しながら、使用側のモ ジュールには影響を与えないということが簡単にできます。さらに、モジュールは別の モジュールをパラメータとして受け取るようにできるので、再利用性が向上できます。

オブジェクトモデル 手続きとデータは、クラスという箱の中にまとめることができま す。オブジェクトとは、クラスの一つのインスタンスです。オブジェクト間の通信は、 「メッセージ渡し」を通して実現されます。受信オブジェクトは、どの手続きがメッセー ジに対応するかということを実行時に決定します(遅延束縛)。このように、オブジェク ト指向はデータ駆動的です。プログラムの構成はクラス間の関係から成ります。とくに、 継承を使うと、あるクラスを定義するのに、他のクラスを拡張することによって行うこ とができます。このオブジェクトモデルでは、具体クラスも、抽象クラスも、パラメー タつきクラスも使えます。さらに、クラス間のサブタイプ関係を定義することによって、 包含的多相性をも導入することができます。 モデルとしてこのように選択肢が2つあるおかげで、アプリケーションの論理的整理を 非常に柔軟に行うことができ、またその維持と進化も容易にします。これら2つのモデ ルの間には双対性があります。モジュールの型にはデータフィールドを追加することは できません(データの拡張不能性)が、データに作用する手続きを新しく追加すること はできます(手続きの拡張可能性)。オブジェクトモデルの場合は、クラスのサブクラス を追加することはできます(データの拡張可能性)が、新しい手続きを先祖のクラスに 見えるように追加することはできません(手続きの拡張不能性)。それでも、これら2つ を組合わせることによって、データと手続きの拡張性への新たな可能性が生まれます。

実行時安全性と効率 Objective Caml は、優れた安全性を、効率の犠牲なしに提供してくれます。理論的な言 葉では、静的型付けとは実行時の型エラーが起き得ないことの保証です。コンパイラに とっては、静的情報は動的な型チェックで効率が損なわれないようにするために有用で す。この利点はオブジェクト指向の機能についても言えることです。さらに、組み込みの ガベージコレクションのおかげで処理系の安全性がさらに向上します。Objective Caml は抜群に高速です。例外機構は、プログラムがゼロによる除算や配列の範囲外アクセス をしたときに、不整合な状態に陥らないようにするためのものです。

はじめに

xxv

この本の構成 本著書は、4つのメインの部から成り、その前後には章が2つ、それから付録が2つ、 参考文献、言語構成要素とプログラミング概念の索引がついています。

1 章: この章では、Objective Caml 言語バージョン 2.04 を現行の主なシステム(Windows、Unix、MacOS)の上にインストールする方法を説明します。 第1部:言語コア 第1部では、Objective Caml 言語の基本要素をすべて網羅して解説 します。まず 2 章で、本言語の関数型のコアからスタートします。3 章は、前章の 続きとして手続き型の機能を説明します。4 章では、純粋な関数型と手続き型を比 較し、両者を使ったスタイルについてもお話しします。5 章では、グラフィックス ライブラリを紹介します。6 章では、3つのアプリケーションを書いてみることに します。簡単なデータベース管理、ミニ Basic インタープリタ、そして有名なマイ ンスイーパという一人用ゲームです。 第2部:開発ツール 第2部では、アプリケーション開発のためのいろいろなツールにつ いて解説します。7 章では異なるコンパイルモードを比較します。それは、インタ ラクティブトップレベル、コマンドラインからのバイトコードコンパイル、ネイ ティブコードへのコンパイルです。8 章では、言語のディストリビューションに提 供されている標準ライブラリを紹介します。 9 章は、ガベージコレクションとは 何か、特に Objective Caml で使われているものについて詳しく述べます。10 章 では、デバッグやプロファイルを行うためのツールを紹介します。11 章は、字句・ 構文解析ツールについてやります。12 章では、Objective Caml プログラムの中で C 言語とのインタフェースを取る方法を説明します。13 章では、あるライブラリ とアプリケーションを構築してみます。そのライブラリとは、GUI 構築をするた めのツールで、アプリケーションの方は、グラフの最小費用経路の探索をするもの で、前述のライブラリを使った GUI を持っています。 第3部:アプリケーションの整理 第3部では、2通りあるプログラム整理方法を解説し ます。つまりモジュールによるものと、オブジェクトによるものです。14 章では、 単純なモジュール、およびパラメータつきのモジュールを説明します。15 章では、 Objective Caml のオブジェクト指向機能についてやります。16 章では、これら2 つの整理方を比較し、2つを混在させることがプログラムの拡張性を向上させる のに役に立つことを示します。17 章では、歯応えのあるアプリケーションを2つ 書いてみることにします。1つめは二人用ゲームで、異なる2つのゲームのために パラメータつきモジュールをいくつも駆使します。もう1つは、ロボット世界のシ ミュレーションで、オブジェクト間通信を実演します。 第4部:並行・分散プログラミング 第4部では、プロセス(軽量プロセス、および通常 のプロセス)の間の通信と、Internet 上の通信について詳しく説明しながら、並 行・分散プログラミングというものを紹介します。18 章では、言語とシステムラ イブラリがどう直結するのか、特にプロセスと通信の概念を説明します。19 章で は、Objective Caml のスレッドを紹介しながら、並行プログラミングにおいて決 定性がなくなることについて触れます。20 章では、分散メモリモデルにおけるソ ケットを通じたプロセス間通信について議論します。21 章では、まずクライアン ト=サーバアプリケーションを書くためのツールボックスを紹介します。そしてこ

xxvi

はじめに れを使って、前部のロボットの例をクライアント=サーバモデルに拡張します。最 後に、すでに出てきたプログラムを HTTP サーバの形に変身させます。

22 章: この最後の章では、Objective Caml によるアプリケーション開発のよさを値踏 みし、ML 言語族で書かれたアプリケーションの中で最も有名なものを紹介します。 付録 1つ目の付録では、オブジェクトの型付けで使われている循環型について説明しま す。2つ目の付録では、新しいバージョン 3.00 で加えられた言語の変更について 述べます。これらの変更は、Objective Caml のこの後のバージョン(3.xx)でも 採り入れられています。 各章は、導入、章の構成、いくつかの節からなる本文、練習問題、まとめ、に続いて「さ らに知りたい方へ」と題した節が最後に来ます。その最後の節では、その章のテーマに 関連した参考文献を挙げます。

1 How to obtain Objective Caml The various programs used in this work are “free” software 1 . They can be found either on the CD-ROM accompanying this work, or by downloading them from the Internet. This is the case for Objective Caml, developed at Inria.

Description of the CD-ROM The CD-ROM is provided as a hierarchy of files. At the root can be found the file index.html which presents the CD-ROM, as well as the five subdirectories below: •

book: root of the HTML version of the book along with the solutions to the exercises;



apps: applications described in the book;



exercises: independent solutions to the proposed exercises;



distrib: set of distributions provided by Inria, as described in the next section;



tools: set of tools for development in Objective Caml;



docs: online documentation of the distribution and the tools.

To read the CD-ROM, start by opening the file index.html in the root using your browser of choice. To access directly the hypertext version of the book, open the file book/index.html. This file hierarchy, updated in accordance with readers’ remarks, can be found posted on the editor’s site: リンク: http://www.oreilly.fr

1. “Free software” is not to be confused with “freeware”. “Freeware” is software which costs nothing, whereas “free software” is software whose source is also freely available. In the present case, all the programs used cost nothing and their source is available.

2

Chapter 1 : How to obtain Objective Caml

Downloading Objective Caml can be downloaded via web browser at the following address: リンク: http://caml.inria.fr/ocaml/distrib.html

There one can find binary distributions for Linux (Intel and PPC), for Windows (NT, 95, 98) and for MacOS (7, 8), as well as documentation, in English, in different formats (PDF, PostScript and HTML). The source code for the three systems is available for download as well. Once the desired distribution is copied to one’s machine, it’s time to install it. This procedure varies according to the operating system used.

Installation Installing Objective Caml requires about 10MB of free space on one’s hard disk drive. The software can easily be uninstalled without corrupting the system.

Installation under Windows The file containing the binary distribution is called: ocaml-2.04-win.zip, indicating the version number (here 2.04) and the operating system. 警告

Objective Caml only works under recent versions of Windows : Windows 95, 98 and NT. Don’t try to install it under Windows 3.x or OS2/Warp.

1.

The file is in compressed (.zip) format; the first thing to do is decompress it. Use your favorite decompression software for this. You obtain in this way a file hierarchy whose root is named ocaml. You can place this directory at any location on your hard disk. It is denoted by in what follows.

2.

This directory includes: • two subdirectories: bin for binaries and lib for libraries; • two “text” files: License.txt and Changes.txt containing the license to use the software and the changes relative to previous versions; • an application: OCamlWin corresponding to the main application; • a configuration file: Ocamlwin.ini which will need to be modified (see the following point); • two files of version notes: the first, Readme.gen, for this version and the second, Readme.win, for the version under Windows.

3.

If you have chosen a directory other than c:\ocaml as the root of your file hierarchy, then it is necessary to indicate this in the configuration file. Edit it with Wordpad and change the line defining CmdLine which is of the form: CmdLine=ocamlrun c:\ocaml\bin\ocaml.exe -I c:\ocaml\lib to

Installation

3

CmdLine=ocamlrun \bin\ocaml.exe -I \lib You have to replace the names of the search paths for binaries and libraries with the name of the Objective Caml root directory. If we have chosen C:\Lang\ocaml as the root directory (), the modification becomes: CmdLine=ocamlrun C:\Lang\ocaml\bin\ocaml.exe -I C:\Lang\ocaml\lib

4.

Copy the file OCamlWin.ini to the main system directory, that is, C:\windows or C:\win95 or C:\winnt according to the installation of your system.

Now it’s time to test the OCamlWin application by double-clicking on it. You’ll get the window in figure 1.1.

図 1.1: Objective Caml window under Windows.

The configuration of command-line executables, launched from a DOS window, is done by modifying the PATH variable and the Objective Caml library search path variable (CAMLLIB), as follows: PATH=%PATH%;\bin set CAMLLIB=\lib where is replaced by the path where Objective Caml is installed.

4

Chapter 1 : How to obtain Objective Caml

These two commands can be included in the autoexec.bat file which every good DOS has. To test the command-line executables, type the command ocaml in a DOS window. This executes the file: /bin/ocaml.exe corresponding to the Objective Caml. text mode toplevel. To exit from this command, type #quit;;. To install Objective Caml from source under Windows is not so easy, because it requires the use of commercial software, in particular the Microsoft C compiler. Refer to the file Readme.win of the binary distribution to get the details.

Installation under Linux The Linux installation also has an easy-to-install binary distribution in the form of an rpm. package. Installation from source is described in section 1. The file to download is: ocaml-2.04-2.i386.rpm which will be used as follows with root privileges: rpm -i ocaml-2.04-2.i386.rpm which installs the executables in the /usr/bin directory and the libraries in the /usr/lib/ocaml directory. To test the installation, type: ocamlc -v which prints the version of Objective Caml installed on the machine. ocamlc -v The Objective Caml compiler, version 2.04 Standard library directory: /usr/lib/ocaml You can also execute the command ocaml which prints the header of the interactive toplevel. Objective Caml version 2.04 # The # character is the prompt in the interactive toplevel. This interactive toplevel can be exited by the #quit;; directive, or by typing CTRL-D. The two semi-colons indicate the end of an Objective Caml phrase.

Installation under MacOS The MacOS distribution is also in the form of a self-extracting binary. The file to download is: ocaml-2.04-mac.sea.bin which is compressed. Use your favorite software

Testing the installation

5

to decompress it. Then all you have to do to install it is launch the self-extracting archive and follow the instructions printed in the dialog box to choose the location of the distribution. For the MacOS X server distribution, follow the installation from source under Unix.

Installation from source under Unix Objective Caml can be installed on systems in the Unix family from the source distribution. Indeed it will be necessary to compile the Objective Caml system. To do this, one must either have a C compiler on one’s Unix, machine, which is generally the case, or download one such as gcc which works on most Unix. systems. The Objective Caml distribution file containing the source is: ocaml-2.04.tar.gz. The file INSTALL describes, in a very clear way, the various stages of configuring, making, and then installing the binaries.

Installation of the HTML documentation Objective Caml’s English documentation is present also in the form of a hierarchy of HTML files which can be found in the docs directory of the CD-ROM. This documentation is a reference manual. It is not easy reading for the beginner. Nevertheless it is quite useful as a description of the language, its tools, and its libraries. It will soon become indispensable for anyone who hopes to write a program of more than ten lines.

Testing the installation Once installation of the Objective Caml development environment is done, it is necessary to test it, mainly to verify the search paths for executables and libraries. The simplest way is to launch the interactive toplevel of the system and write the first little program that follows: String.concat "/" ["a"; "path"; "here"] ;; This expression concatenates several character strings, inserting the “/” character between each word. The notation String.concat indicates use of the function concat from the String. If the library search path is not correct, the system will print an error. It will be noted that the system indicates that the computation returns a character string and prints the result. The documentation of this function String.concat can be found in the online reference manual by following the links “The standard library” then “Module String: string operations”. To exit the interactive toplevel, the user must type the directive “#quit ;;”.

Part I

Language Core

7

9 The first part of this book is a complete introduction to the core of the Objective Caml language, in particular the expression evaluation mechanism, static typing and the data memory model. An expression is the description of a computation. Evaluation of an expression returns a value at the end of the computation. The execution of an Objective Caml program corresponds to the computation of an expression. Functions, program execution control structures, even conditions or loops, are themselves also expressions. Static typing guarantees that the computation of an expression cannot cause a run-time type error. In fact, application of a function to some arguments (or actual parameters) isn’t accepted unless they all have types compatible with the formal parameters indicated in the definition of the function. Furthermore, the Objective Caml language has type infererence: the compiler automatically determines the most general type of an expression. Finally a minimal knowledge of the representation of data is indispensable to the programmer in order to master the effects of physical modifications to the data.

Outline Chapter 2 contains a complete presentation of the purely functional part of the language and the constraints due to static typing. The notion of expression evaluation is illustrated there at length. The following control structures are detailed: conditional, function application and pattern matching. The differences between the type and the domain of a function are discussed in order to introduce the exception mechanism. This feature of the language goes beyond the functional context and allows management of computational breakdowns. Chapter 3 exhibits the imperative style. The constructions there are closer to classic languages. Associative control structures such as sequence and iteration are presented there, as well as mutable data structures. The interaction between physical modifications and sharing of data is then detailed. Type inference is described there in the context of these new constructions. Chapter 4 compares the two preceding styles and especially presents different mixed styles. This mixture supports in particular the construction of lazy data structures, including mutable ones. Chapter 5 demonstrates the use of the Graphics library included in the language distribution. The basic notions of graphics programming are exhibited there and immediately put into practice. There’s even something about GUI construction thanks to the minimal event control provided by this library. These first four chapters are illustrated by a complete example, the implementation of a calculator, which evolves from chapter to chapter. Chapter 6 presents three complete applications: a little database, a mini-BASIC interpreter and the game Minesweeper. The first two examples are constructed mainly in a functional style, while the third is done in an imperative style.

10

The rudiments of syntax Before beginning we indicate the first elements of the syntax of the language. A program is a sequence of phrases in the language. A phrase is a complete, directly executable syntactic element (an expression, a declaration). A phrase is terminated with a double semi-colon (; ;). There are three different types of declarations which are each marked with a different keyword: value declaration exception declaration type declaration

: : :

let exception type

All the examples given in this part are to be input into the interactive toplevel of the language. Here’s a first (little) Objective Caml program, to be entered into the toplevel, whose prompt is the pound character (#), in which a function fact computing the factorial of a natural number, and its application to a natural number 8, are defined. # let rec fact n = if n < 2 then 1 else n * fact(n-1) ; ; val fact : int -> int = # fact 8 ; ; - : int = 40320

This program consists of two phrases. The first is the declaration of a function value and the second is an expression. One sees that the toplevel prints out three pieces of information which are: the name being declared, or a dash (-) in the case of an expression; the inferred type; and the return value. In the case of a function value, the system prints . The following example demonstrates the manipulation of functions as values in the language. There we first of all define the function succ which calculates the successor of an integer, then the function compose which composes two functions. The latter will be applied to fact and succ. # let succ x = x+1 ; ; val succ : int -> int = # let compose f g x = f(g x) ; ; val compose : (’a -> ’b) -> (’c -> ’a) -> ’c -> ’b = # compose fact succ 8 ; ; - : int = 362880

This last call carries out the computation fact(succ 8) and returns the expected result. Let us note that the functions fact and succ are passed as parameters to compose in the same way as the natural number 8.

2 関数型プログラミング 最初の関数型言語 Lisp は 1950 年代末に登場しました。これは、最初の代表的な手続き 型言語である Fortran と同時期です。この二つの言語は今でも存在しますが、どちらも 大幅に進化しており、Fortran の場合は数値計算に、Lisp の場合は記号処理に広く使わ れています。関数型プログラミングが興味を惹くのは、プログラムを書いたり、プログ ラムが操作する値を指定するのが非常に簡単であるという点です。プログラムは関数で あり、引数に適用され、計算した値を(その計算が停止すれば)プログラムの出力とし て返します。これにより、プログラムを組み合わせることが簡単になります。関数の合 成により、あるプログラムの出力が別のプログラムの入力になるからです。 関数型プログラミングは、三つの構成要素を持つ単純な計算モデルに基づいています。 その構成要素とは、変数、関数定義、そして関数を引数に適用することです。このモデ ルは λ 計算と呼ばれており、アロンゾ・チャーチによって 1932 年、すなわち最初のコ ンピュータ以前に提案されました。λ 計算は計算可能性の概念に一般的・理論的なモデ ルを与えるために考えられました。λ 計算では、すべての関数を値として扱うことがで き、他の関数の引数として使ったり、他の関数を呼び出した結果として返すことができ ます。λ 計算の理論では、およそ計算可能(すなわちプログラム可能)なものは、この 形式論の中で表すことができるとされています。ただし、λ 計算の構文は実用的なプロ グラミング言語として用いるには小さすぎるので、より使いやすくするために、プリミ ティブな値(整数や文字列など)、それらに対する演算、制御構造、関数や値に名前をつ けることを可能にする宣言、また特に再帰関数等々が追加されてきました。 関数型言語の分類はいくつかありますが、ここでは我々にとってもっとも顕著と思われ る二つの特徴に基づいて区別することにします。



副作用がない(純粋)か、副作用がある(純粋でない)か。純粋な関数型言語とは、 状態の変化がない言語のことです。そのような言語では、すべてが単なる計算であ り、それが実行される方法は重要ではありません。Lisp や ML のような純粋でな い関数型言語は、状態の変化といった手続き型言語の特徴を取り入れ、Fortran の ような言語に近いスタイルでアルゴリズムを書くことを許しています。そのような 言語では、式を評価する順序が重要です。

12 •

Chapter 2 : 関数型プログラミング 動的に型付けされるか、静的に型付けされるか。型付けは、関数に渡される引数が 実際にその関数の仮引数の型を持っているかどうか、検証することを可能にしま す。この検証はプログラムの実行中に行うことができます。その場合、この検証を 動的な型付けと呼びます。もし型エラーが起こったら、プログラムは整合性のとれ た状態で停止します。これは Lisp に当てはまります。また、この検証は実行前、つ まりコンパイル時に行うこともできます。そのような先に行われる検証のことを静 的な型付けと呼びます。静的な型付けは一度しか行われないので、プログラムの実 行を遅くすることがありません。これは ML や、その方言である Objective Caml に当てはまります。正しく型付けされたプログラム、すなわち型検証器によって受 理されたプログラムだけがコンパイルでき、実行されます。

本章の概要 本章では、言語 Objective Caml の関数型部分の基本的要素、すなわち構文的要素、型 の文法および例外機構について説明します。その後で、例として初めての完全なプログ ラムを開発します。 第一節では、プリミティブな値やそれらを操作する関数をはじめ、言語の核となる部分 を説明してから、構造を持つ値や関数値に進みます。また、基本的な制御構造や、ロー カルおよびグローバルな値宣言を導入します。第二節では、構造を持った値を構成する ための型定義と、そのような構造値にアクセスするパターンマッチングについて扱いま す。第三節では、関数の定義域と、推論される型とを比較することにより、例外機構に ついて説明します。第四節では、単純なアプリケーションである電卓を通じて、これら の概念をまとめて示します。

Objective Caml の核となる関数型の部分 あらゆる関数型言語と同様に、Objective Caml は式を中心とする言語であり、主に関数 を作ったり適用することによりプログラミングが行われます。一つの式を評価した結果 は言語における値であり、プログラムの実行は、そのプログラムを構成するすべての式 を評価することにあたります。

プリミティブな値、関数、および型 Objective Caml では、整数、浮動小数、文字、文字列、および論理値があらかじめ定義 されています。

数値 Objective Caml には二種類の数があります。型 int の整数1 と型 float の浮動小数です。 Objective Caml における倍精度浮動小数の表現は、IEEE 754 規格2 に従っています。整 1. 32 ビットのマシンでは [−230 , 230 − 1] の区間、64 ビットのマシンでは [−262 , 262 − 1] の区間 2. 浮動小数 m × 10n を、53 ビットの仮数部 m と、区間 [−1022, 1023] の指数部 n により表す

Objective Caml の核となる関数型の部分

13

数と浮動小数に対する演算を図 2.1 に示します。整数演算の結果が型 int の定義されて いる区間外となるときは、エラーが起こるのではなく、そのシステムにおける整数の区 間内の結果となることに注意してください。いいかえれば、すべての整数演算は、その 区間の境界を法とする剰余の演算となります。 整数 + * / mod

足し算 引き算および符号反転 かけ算 整数の割り算 整数の割り算の余り

# 1 ;; - : int = 1 # 1 + 2 ;; - : int = 3 # 9 / 2 ;; - : int = 4 # 11 mod 3 ; ; - : int = 2 (* 整数の表現の限界 *) # 2147483650 ; ; - : int = 2

浮動小数 +. 足し算 -. 引き算および符号反転 *. かけ算 /. 割り算 ** べき乗 # 2.0 ; ; - : float = 2 # 1.1 +. 2.2 ; ; - : float = 3.3 # 9.1 /. 2.2 ; ; - : float = 4.13636363636 # 1. /. 0. ; ; - : float = inf (* limits of the representation (* of floating-point numbers (* 浮動小数の表現の限界 *) # 222222222222.11111 ; ; - : float = 222222222222

*) *)

図 2.1: 数値に対する演算

整数と浮動小数の違い float と int のように異なる型を持つ値を直接比較することは できません。しかし、一方を他方に変換する関数(float of int と int of float)が あります。 # 2 = 2.0 ; ; Characters 5-8: 2 = 2.0 ;; ^^^ This expression has type float but is here used with type int # 3.0 = float of int 3 ; ; - : bool = true

同様に、浮動小数に対する演算は整数に対する演算と異なります。 # 3 + 2 ;; - : int = 5

14

Chapter 2 : 関数型プログラミング

# 3.0 +. 2.0 ; ; - : float = 5 # 3.0 + 2.0 ; ; Characters 0-3: 3.0 + 2.0 ;; ^^^ This expression has type float but is here used with type int # sin 3.14159 ; ; - : float = 2.65358979335e-06

0 による除算のような、結果が定義されていない計算は例外を発生し(53 ページを参照)、 計算が中断します3 。浮動小数には、無限の値(Inf と表示)や結果が定義されていない 計算(NaN4 と表示)の表現があります。浮動小数に対する主な関数を図 2.2 に示します。 浮動小数関数 ceil 切り上げ floor 切り下げ sqrt 平方根 exp 指数関数 log 自然対数 log10 底が 10 の log

三角関数 cos 余弦 sin 正弦 tan 正接 acos 逆余弦 asin 逆正弦 atan 逆正接

# ceil 3.4 ; ; - : float = 4 # floor 3.4 ; ; - : float = 3 # ceil (-.3.4) ; ; - : float = -3 # floor (-.3.4) ; ; - : float = -4

# sin 1.57078 ; ; - : float = 0.999999999867 # sin (asin 0.707) ; ; - : float = 0.707 # acos 0.0 ; ; - : float = 1.57079632679 # asin 3.14 ; ; - : float = nan

図 2.2: 浮動小数関数

文字と文字列 文字は型 char を持ち、0 以上 255 以下の整数に対応します。初めの 128 文字は ASCII エ ンコーディングに従います。関数 char of int および int of char により、整数と文字 の間の変換が可能です。 文字列は型 string を持ち、決まった長さ(224 − 6 未満)の文 字の列です。文字列を連結する演算子は^です。関数 int of string、string of int、 string of float および float of string は、数値と文字列との間の様々な変換を行い ます。 3. 訳注:整数の場合 4. Not a Number

Objective Caml の核となる関数型の部分

15

# ’B’ ; ; - : char = ’B’ # int of char ’B’ ; ; - : int = 66 # "is a string" ; ; - : string = "is a string" # (string of int 1987) ^ " is the year Caml was created" ; ; - : string = "1987 is the year Caml was created"

たとえ文字列の中身が数字であっても、明示的な変換を行わない限り、数値に対する演 算を使うことはできません。 # "1999" + 1 ; ; Characters 1-7: "1999" + 1 ;; ^^^^^^ This expression has type string but is here used with type int # (int of string "1999") + 1 ; ; - : int = 2000

String モジュールには、文字列に対する多数の関数が集められています(217 ページ 参照)。

論理値 論理値は型 bool を持ち、true と false という二つの値からなる集合に属します。論理 値に対するプリミティブ演算を図 2.3 に示します。歴史的理由により、“and” と “or” の 演算子にはそれぞれ二つの形があります。

not && ||

否定 逐次的 and 逐次的 or

& or

&&と同義 ||と同義

図 2.3: 論理値に対する演算子 # true ; ; - : bool = true # not true ; ; - : bool = false # true && false ; ; - : bool = false

演算子&&および||(ないし同義の演算子)は、まず左側の引数を評価し、その結果によっ ては右側の引数を評価します。これらの演算子は条件文によって書き直すこともできま す(18 ページ参照)。 等値演算子と比較演算子を図 2.4 に示します。これらの演算子は多相的です。つまり、整 数の比較にも文字列の比較にも使うことができます。唯一の制約として、二つの引数は

16

Chapter 2 : 関数型プログラミング = == <> !=

< > <= >=

構造等値 物理等値 =の否定 ==の否定

より小さい より大きい 以下 以上

図 2.4: 等値演算子および比較演算子 同じ型でなければなりません(28 ページ参照)。 # 1<=118 && (1=2 || not(1=2)) ; ; - : bool = true # 1.0 <= 118.0 && (1.0 = 2.0 || not (1.0 = 2.0)) ; ; - : bool = true # "one" < "two" ; ; - : bool = true # 0 < ’0’ ; ; Characters 4-7: 0 < ’0’ ;; ^^^ This expression has type char but is here used with type int

構造等値演算子は二つの値の構造を巡回してそれらの等しさを調べます。これに対して 物理等値演算子は、二つの値がメモリの上で同じ場所におかれているかどうかを調べま す。どちらの等値演算子も単純な値(論理値、文字、整数、および定数コンストラクタ) については同じ結果を返します(44 ページ参照). 警告

浮動小数と文字列は構造を持つ値として扱われます。

ユニット unit 型は、唯一の元 () を持つ集合を表します。 # () ; ; - : unit = ()

この値は手続き的プログラム(第 3 章 65 ページ参照)において、副作用を持つ関数の ためによく使われます。Objective Caml には手続きの概念が存在しませんが、C 言語の void 型と同様に、値 () を結果とする関数により手続きを模倣します。

デカルト積、組 型が異なるかもしれない値を集めて、組にすることができます。組を作る値は、コンマ によって区切ります。型コンストラクタ*は組を表します。型 int * string は、第 1 要 素が整数(型 int)で、第 2 要素が文字列(型 string)であるような組の型です。 # ( 12 , "October" ) ; ; - : int * string = (12, "October")

曖昧さがないときは、より単純に

Objective Caml の核となる関数型の部分

17

# 12 , "October" ; ; - : int * string = (12, "October")

と書くこともできます。関数 fst および snd により、二つ組の第 1 要素と第 2 要素にア クセスすることができます。 # fst ( 12 , "October" ) ; ; - : int = 12 # snd ( 12 , "October" ) ; ; - : string = "October"

この二つの関数はどのような型の値の二つ組でも受け付け、等値演算子と同様に多相的 です。 # fst; ; - : ’a * ’b -> ’a = # fst ( "October", 12 ) ; ; - : string = "October"

型 int * char * string は第 1 要素が型 int を持ち、第 2 要素が型 char を持ち、第 3 要素が型 string を持つような 3 つ組の型です。この型を持つ値は # ( 65 , ’B’ , "ascii" ) ; ; - : int * char * string = (65, ’B’, "ascii")

のように書きます。 警告

関数 fst および snd を二つ組以外の組に適用すると、型 エラーになります。

# snd ( 65 , ’B’ , "ascii" ) ; ; Characters 7-25: snd ( 65 , ’B’ , "ascii" ) ;; ^^^^^^^^^^^^^^^^^^ This expression has type int * char * string but is here used with type ’a * ’b

実際に、二つ組の型と三つ組の型には違いがあります。型 int * int * int は、(int * int) * int や int * (int * int) のような型とは異なるのです。三つ組やその他 の組にアクセスする関数は、言語の核のライブラリでは定義されていません。そのよう な関数は、必要であれば、パターンマッチングを利用して定義することができます(33 ページ参照)。

リスト 同じ型の値を集めて、リストにすることができます。リストは、空であるか、あるいは 同じ型の要素から成ります。 # [] ; ; - : ’a list = [] # [ 1 ; 2 ; 3 ] ;; - : int list = [1; 2; 3] # [ 1 ; "two" ; 3 ] ; ; Characters 6-11: [ 1 ; "two" ; 3 ] ;; ^^^^^

18

Chapter 2 : 関数型プログラミング

This expression has type string but is here used with type int

リストの先頭に要素を追加する関数は、中置演算子 :: です。これは Lisp の cons と同様 です。 # 1 :: 2 :: 3 :: [] ; ; - : int list = [1; 2; 3]

リストの連結も中置演算子@です。 # [ 1 ] @ - : int list # [ 1 ; 2 ] - : int list

[ 2 ; 3 ] = [1; 2; @ [ 3 ] = [1; 2;

;; 3] ;; 3]

その他のリスト操作関数は、List ライブラリに定義されています。このライブラリの関 数 hd と tl は、リストの先頭と末尾を(もしそれらの値があれば)与えます。List モ ジュールに属していることをシステムに対して示すために、これらの関数は List.hd お よび List.tl と表されます5 。 # List.hd [ 1 ; 2 ; 3 ] ; ; - : int = 1 # List.hd [] ; ; Exception: Failure "hd".

最後の例は、空リストの最初の要素を取得するように要求しており、実際には問題があ ります。システムが例外を発生するのは、まさにこのためです(53 ページ参照)。

条件制御構造 いかなるプログラミング言語でも必要不可欠な制御構造の一つとして、条件の関数とし て計算を誘導する、条件文(あるいは分岐)といわれる構造があります。 構文 :

if expr1 then expr2 else expr3

式 expr1 は型 bool を持ちます。式 expr2 と expr3 の型は何でも構いませんが、同じでな ければいけません。 # if 3=4 then 0 else 4 ; ; - : int = 4 # if 3=4 then "0" else "4" ; ; - : string = "4" # if 3=4 then 0 else "4"; ; Characters 20-23: if 3=4 then 0 else "4";; ^^^ 5. List モジュールについては 217 ページで説明します。

Objective Caml の核となる関数型の部分

19

This expression has type string but is here used with type int

条件構文はそれ自体も式であり、評価すると値を返します。 # (if 3=5 then 8 else 10) + 5 ; ; - : int = 15

注意

else 節は省略することができますが、その場合は暗黙に else () とおきか えられます。したがって、式 expr2 の型は unit でなければいけません(77 ページ参照)。

値の宣言 宣言は、名前を値に束縛します。宣言には、グローバルな宣言とローカルな宣言の二種 類があります。前者の場合、宣言された名前は後に続くすべての式で使えます。後者の 場合、宣言された名前は一つの式でしか使えません。複数の名前と値の束縛を同時に宣 言することも同様に可能です。

グローバルな宣言 構文 :

let name = expr ;;

グローバルな宣言は、名前 name と式 expr の値との束縛を定義します。その束縛は、後 に続くすべての式で用いることができます。 # let yr = "1999" ; ; val yr : string = "1999" # let x = int of string(yr) ; ; val x : int = 1999 # x ;; - : int = 1999 # x + 1 ;; - : int = 2000 # let new yr = string of int (x + 1) ; ; val new_yr : string = "2000"

グローバルな同時宣言

構文 :

let name1 = expr1 and name2 = expr2 .. . and namen = exprn ;;

20

Chapter 2 : 関数型プログラミング

同時宣言は、異なるシンボルを同じレベルで宣言します。それらのシンボルは、すべて の宣言が終わるまで使うことができません。 # let x = 1 and y = 2 ; ; val x : int = 1 val y : int = 2 # x + y ;; - : int = 3 # let z = 3 and t = z + 2 ; ; Characters 18-19: let z = 3 and t = z + 2 ;; ^ Unbound value z

いくつかのグローバルな宣言を一つの節にまとめることもできます。この場合、それら の宣言の型や値は、二重の “;;” で節が終わるまで表示されません。同時宣言と異なり、 これらの宣言は逐次に評価されます。 # let x = 2 let y = x + 3 ; ; val x : int = 2 val y : int = 5

グローバルな宣言は、同じ名前の新しい宣言により隠すこともできます(26 ページ参照)。

ローカルな宣言 構文 :

let name = expr1 in expr2 ;;

このローカルな宣言は、名前 name を expr1 の値に束縛します。その名前は expr2 を評 価する間のみ使えます。 # let xl = 3 in xl * xl ; ; - : int = 9

この xl を値 3 に束縛するローカルな宣言は、xl * xl を評価するときのみ有効です。 # xl ; ; Characters 1-3: xl ;; ^^ Unbound value xl

ローカルな宣言は、同じ名前に対する以前の宣言をすべて隠しますが、スコープを抜け ると前の値に戻ります。 # let x = 2 ; ; val x : int = 2 # let x = 3 in x * x ; ; - : int = 9 # x * x ;; - : int = 4

ローカルな宣言は式であり、他の式を構成するために利用することができます。 # (let x = 3 in x * x) + 1 ; ;

Objective Caml の核となる関数型の部分

21

- : int = 10

ローカルな宣言も同時に行うことができます。

構文 :

let and .. .

name1 = expr1 name2 = expr2

and in

namen = exprn expr ;;

# let a = 3.0 and b = 4.0 in sqrt (a*.a +. b*.b) ; ; - : float = 5 # b ;; Characters 0-1: b ;; ^ Unbound value b

関数式、関数 関数式は引数と本体からなります。仮引数は変数名であり、本体は式です。この引数は 抽象的であると言います。この理由から、関数式は抽象とも呼びます。 構文 :

function p –> expr

したがって、引数を二乗する関数は # function x → x*x ; ; - : int -> int =

と書きます。この関数の型は、Objective Caml のシステムが推論します。関数型 int -> int は、型 int の引数を期待し、型 int の値を返す関数を示します。 関数を引数に適用するには、関数の後に引数を書きます。 # (function x → x * x) 5 ; ; - : int = 25

適用を評価するということは、仮引数 x を引数の値(すなわち実引数)、ここでは 5 で 置き換え、関数の本体、ここでは x * x を評価することになります。 関数式を構成するとき、expr はいかなる式でも構いません。特に、expr 自体が関数式 でも構いません。 # function x → (function y → 3*x + y) ; ; - : int -> int -> int =

22

Chapter 2 : 関数型プログラミング

括弧はなくても構いません。より単純に、次のように書くこともできます。 # function x → function y → 3*x + y ; ; - : int -> int -> int =

この式の型は、二つの整数を期待し、一つの整数を返す関数の型として普通に読むこと もできます。しかし Objective Caml のような関数型言語の文脈では、より正確に、一つ の整数を期待し、型 int -> int の関数値を返す関数の型として扱います。 # (function x → function y → 3*x + y) 5 ; ; - : int -> int =

もちろん、この関数式を普通に二つの引数に適用することもできます。 # (function x → function y → 3*x + y) 4 5 ; ; - : int = 17

と書きます。f a b と書くときは左側に暗黙の括弧がつくので、この式は (f a) b と等 価です。

(function x → function y → 3*x + y) 4 5 なる適用について詳しく調べましょう。この式の値を計算するには、

(function x → function y → 3*x + y) 4 の値を計算する必要がありますが、これは 3*x + y において x を 4 で置き換えて得ら れる

function y → 3*4 + y に等価な関数式です。この値(関数)を 5 に適用すると、最終的な値 3*4+5 = 17 が得 られます。 # (function x → function y → 3*x + y) 4 5 ; ; - : int = 17

関数のアリティ 関数の引数の数は、その関数のアリティと呼ばれます。数学に由来する記法では、f (4, 5) のように、関数の名前の後で括弧の中に関数の引数を書くことになっています。先に見た ように、Objective Caml では f 4 5 のように書くほうが普通です。もちろん、Objective Caml でも (4, 5) に適用できるような関数を書くことは可能です。 # function (x,y) → 3*x + y ; ; - : int * int -> int =

しかし、型が示すように、この式は二つではなく一つの引数、すなわち整数のペアを期 待しています。一つのペアを期待している関数に二つの引数を渡そうとしたり、二つの 引数を期待している関数に一つのペアを渡そうとすると、型エラーになります。 # (function (x,y) → 3*x + y) 4 5 ; ;

Objective Caml の核となる関数型の部分

23

Characters 2-27: (function (x,y) -> 3*x + y) 4 5 ;; ^^^^^^^^^^^^^^^^^^^^^^^^^ This function is applied to too many arguments # (function x → function y → 3*x + y) (4, 5) ; ; Characters 39-43: (function x -> function y -> 3*x + y) (4, 5) ;; ^^^^ This expression has type int * int but is here used with type int

別の構文 複数の引数を持つ関数式を、もっと簡潔に書く方法もあります。これは Caml 言語の以 前のバージョンの遺物で、次のような形をしています。 構文 :

fun p1 . . . pn –> expr

これにより、function キーワードや矢印の繰り返しを省くことができます。これは次の 言い換えと同じです。

function p1 –> . . . –> function pn –> expr # fun x y → 3*x + y ; ; - : int -> int -> int = # (fun x y → 3*x + y) 4 5 ; ; - : int = 17

この形は、特に Objective Caml と一緒に配布されているライブラリの中で、今でもよ く出てくることがあります。

クロージャ Objective Caml は関数式を他の式と同様に扱うことができ、関数式の値を計算すること ができます。そのような計算によって返される値は関数式であり、クロージャと呼ばれ ます。Objective Caml のすべての式は、その式より前の宣言に由来する名前と値の束縛 からなる環境の元で評価されます。クロージャは仮引数の名前、関数の本体、および式 の環境の 3 つ組として表すことができます。関数式の本体は、仮引数の他に、先に宣言 されたすべての変数を使うことができるので、そのような環境を保存する必要がありま す。それらの変数は、その関数式において「自由」であるといいます。自由変数の値は、 関数式が適用されるときに必要となります。 # let m = 3 ; ; val m : int = 3 # function x → x + m ; ; - : int -> int = # (function x → x + m) 5 ; ; - : int = 8

24

Chapter 2 : 関数型プログラミング

クロージャを引数に適用して新しいクロージャが返されるとき、新しいクロージャは将来 の適用で必要なすべての束縛を環境に保持します。この概念については、変数のスコー プに関する節で詳細に説明します(26 ページ参照)。クロージャのメモリ表現について は、後で 4 章(101 ページ)および 12 章(334 ページ)において扱います。 今まで扱ってきた関数式は匿名でした。関数式に名前をつけられると便利です。

関数値の宣言 関数値は、言語の他の値と同じように、let 構文で宣言します。 # let succ = function x → x + 1 ; ; val succ : int -> int = # succ 420 ; ; - : int = 421 # let g = function x → function y → 2*x + 3*y ; ; val g : int -> int -> int = # g 1 2; ; - : int = 8

記述を簡単にするために、次のような書き方が許されています。 構文 :

let name p1 . . . pn = expr

これは次の形と同じです。

let name = function p1 –> . . . –> function pn –> expr 次の succ と g の宣言は、先の宣言と等価です。 # let succ x = x + 1 ; ; val succ : int -> int = # let g x y = 2*x + 3*y ; ; val g : int -> int -> int =

次の例では、Objective Caml の完全に関数的な特徴が発揮されています。この例では、 g を一つの整数に適用することにより関数 h1 が得られています。このような場合を部分 適用と言います。 # let h1 = g 1 ; ; val h1 : int -> int = # h1 2 ; ; - : int = 8

g において第 2 引数 y の値を固定し、関数 h2 を定義することもできます。 # let h2 = function x → g x 2 ; ; val h2 : int -> int =

Objective Caml の核となる関数型の部分

25

# h2 1 ; ; - : int = 8

インフィックス関数の宣言 ある種の二引数関数は、インフィックス(中置)形式で適用できます。整数の加算がそ うです。+を 3 と 5 に適用するには、3 + 5 と書きます。記号+を普通の関数値として使 用するには、そのインフィックス記号を括弧でかこむ構文により指示する必要がありま す。その構文は次の通りです。 構文 :

( op )

次の例は、( + ) を使って関数 succ を定義します。 # ( + ) ;; - : int -> int -> int = # let succ = ( + ) 1 ; ; val succ : int -> int = # succ 3 ; ; - : int = 4

新しい演算子を定義することも可能です。整数のペアを加算する演算子++を定義します。 # let ( ++ ) c1 c2 = (fst c1)+(fst c2), (snd c1)+(snd c2) ; ; val ( ++ ) : int * int -> int * int -> int * int = # let c = (2,3) ; ; val c : int * int = (2, 3) # c ++ c ; ; - : int * int = (4, 6)

このような定義の可能な演算子には、重要な制限があります。*、+、@といった記号のみ を含み、文字や数字を含んではいけないという制限です。元からインフィックスとして 定義されているある種の関数は、この規則の例外です。それらを列挙すると、次の通り です:or mod land lor lxor lsl lsr asr。

高階関数 関数値(クロージャ)は結果として返すことができます。同様に、引数として関数に渡 すこともできます。関数値を引数としてとったり結果として返したりする関数を高階関 数と呼びます。 # let h = function f → function y → (f y) + y ; ; val h : (int -> int) -> int -> int =

26

Chapter 2 : 関数型プログラミング

注意 適用は左側に暗黙の括弧がつきますが、関数型は右側に暗黙の括弧がつき ます。したがって、関数 h の型は (int -> int) -> int -> int すなわち (int -> int) -> (int -> int) と書くことができます。 高階関数を使うと、リストをエレガントに扱うことができます。たとえば関数 List.map は、リストのすべての要素に一つの関数を適用し、結果をリストにして返します。 # List.map ; ; - : (’a -> ’b) -> ’a list -> ’b list = # let square x = string of int (x*x) ; ; val square : int -> string = # List.map square [1; 2; 3; 4] ; ; - : string list = ["1"; "4"; "9"; "16"]

他の例として、関数 List.for all は、リストのすべての要素が与えられた基準を満た すかどうか調査します。 # List.for all ; ; - : (’a -> bool) -> ’a list -> bool = # List.for all (function n → n<>0) [-3; -2; -1; 1; 2; 3] ; ; - : bool = true # List.for all (function n → n<>0) [-3; -2; 0; 1; 2; 3] ; ; - : bool = false

変数のスコープ 式を評価するためには、その式に現れるすべての変数が定義されていなければなりませ ん。これは特に宣言 let p = e における式 e に当てはまります。しかし、式 e の中で変 数 p はまだ知られていないので、前の宣言で与えられた値を参照する場合しか使えませ ん。 # let p = p ^ "-suffix" ; ; Characters 9-10: let p = p ^ "-suffix" ;; ^ Unbound value p # let p = "prefix" ; ; val p : string = "prefix" # let p = p ^ "-suffix" ; ; val p : string = "prefix-suffix"

Objective Caml では、変数は静的に束縛されます。クロージャを適用するときに使用さ れる環境は、そのクロージャを宣言した時点のもの(静的スコープ)であり、適用する 時点のもの(動的スコープ)ではありません。

Objective Caml の核となる関数型の部分

27

# let p = 10 ; ; val p : int = 10 # let k x = (x, p, x+p) ; ; val k : int -> int * int * int = # k p ;; - : int * int * int = (10, 10, 20) # let p = 1000 ; ; val p : int = 1000 # k p ;; - : int * int * int = (1000, 10, 1010)

関数 k は自由変数 p を含んでいます。p はグローバル環境で定義されているので、k の 定義は合法です。クロージャk の環境における、名前 p と値 10 の束縛は静的です。すな わち、それ以降の p の定義には依存しません。

再帰的定義 定義において自分自身の識別子を使用する変数宣言は再帰的であるといいます。この機 能は主に関数のため、特に漸化式による定義を模倣するために使用します。このような 定義を let 宣言がサポートしないことは先ほど見ました。再帰的関数を宣言するには、 専用の構文要素を使用します。 構文 :

let rec name = expr ;;

同様に、関数の引数を指定して関数値を定義する構文機能を使用することもできます。 構文 :

let rec name p1 . . . pn = expr ;;

たとえば、0 から引数までの非負整数の総和を計算する関数 sigma はこのようになりま す。 # let rec sigma x = if x = 0 then 0 else x + sigma (x-1) ; ; val sigma : int -> int = # sigma 10 ; ; - : int = 55

なお、この関数は引数が真に負だと停止しません。 一般に再帰的な値は関数です。コンパイラは、値が関数でない再帰的宣言を拒否します。 # let rec x = x + 1 ; ; Characters 13-18: let rec x = x + 1 ;; ^^^^^ This kind of expression is not allowed as right-hand side of ‘let rec’

ただし、ある種の場合にはこのような宣言が許されることもあります(52 ページ参照)。

let rec 宣言は and 構文と組み合わせて、同時に宣言を行うこともできます。この場合、 同じレベルで定義したすべての関数は、互いの本体で使うことができます。これを用い ると特に相互再帰的な関数の宣言が可能です。 # let rec even n = (n<>1) && ((n=0) or (odd (n-1)))

28

Chapter 2 : 関数型プログラミング

and odd n = (n<>0) && ((n=1) or (even (n-1))) val even : int -> bool = val odd : int -> bool = # even 4 ; ; - : bool = true # odd 5 ; ; - : bool = true

;;

同様に、ローカルな宣言も再帰的に行うことができます。次に示す新しい sigma の定義 は、引数の正当性をテストしてから、ローカル関数 sigma rec の定義する総和を計算し ます。 # let sigma x = let rec sigma rec x = if x = 0 then 0 else x + sigma rec (x-1) in if (x<0) then "error: negative argument" else "sigma = " ^ (string of int (sigma rec x)) ; ; val sigma : int -> string =

注意 引数が負であろうとなかろうと同じ型の値を返す必要があるので、結果は 文字列の形で与えざるを得ません。実際、引数が負の場合、sigma はどん な値を返すべきでしょうか。この問題に適切に対処する方法については後 で述べます(53 ページ参照)。

多相性と型制約 ある種の関数は異なる型を持つ引数に対して同じコードを実行します。たとえば、二つ の値からペアを作る際に、システムが知っている一つ一つの型について、別々の関数が 必要となることはありません6 。同様に、ペアの第一要素をアクセスする関数も、その第 一要素の型によって区別する必要はありません。 # let make pair a b = (a,b) ; ; val make_pair : ’a -> ’b -> ’a * ’b = # let p = make pair "paper" 451 ; ; val p : string * int = ("paper", 451) # let a = make pair ’B’ 65 ; ; val a : char * int = (’B’, 65) # fst p ; ; - : string = "paper" # fst a ; ; - : char = ’B’

返り値や引数の型を特定しない関数は、多相的であるといいます。Objective Caml のコ ンパイラに含まれる型推論器は、一つ一つの式についてもっとも一般的な型を求めます。 6. これは幸いなことです。というのは、型の数はマシンの記憶容量によってしか制限されないからです。

Objective Caml の核となる関数型の部分

29

この場合、Objective Caml は変数(ここでは’a と’b)を用いて、そのような一般的な型 を示します。これらの変数は、関数を適用するときに引数の型によって具体化されます。

Objective Caml の多相関数を用いれば、静的型付けによる実行安全性を維持しつつ、す べての型について使える一般的なコードを書くことができる、という利点が得られま す。実際、make pair は多相的ですが、(make pair ’B’ 65) によって作られる値は、 (make pair "paper" 451) とは異なる、適切に特定された型を持ちます。さらに、型の 検証はコンパイルの際に行われるので、コードの一般性がプログラムの効率性を損なう ことはありません。

多相的な関数や値の例 以下に、「パラメータ化された型を持つ関数」をパラメータとする多相関数の例を挙げ ます。

app 関数は、関数を引数に適用します。 # let app = function f → function x → f x ; ; val app : (’a -> ’b) -> ’a -> ’b =

よって、以前に定義した関数 odd に適用することが可能です。 # app odd 2; ; - : bool = false

恒等関数 (id ) はパラメータをとり、そのまま返します。 # let id x = x ; ; val id : ’a -> ’a = # app id 1 ; ; - : int = 1

compose 関数は二つの関数と一つの値をとり、関数を合成して値に適用します。 # let compose f g x = f (g x) ; ; val compose : (’a -> ’b) -> (’c -> ’a) -> ’c -> ’b = # let add1 x = x+1 and mul5 x = x*5 in compose mul5 add1 9 ; ; - : int = 50

g の結果は f の引数と同じ型でなければならないことがわかります。 関数以外の値も多相的になることがあります。たとえば、空リストの場合がそうです。 # let l = [] ; ; val l : ’a list = []

型推論は関数適用に由来する制約の解消によって行われるのであり、実行時に得られる 値によって行われるわけではありません。次の例はそれを実際に証明しています。 # let t = List.tl [2] ; ; val t : int list = []

List.tl の型は’a list -> ’a list なので、この関数を整数のリストに適用すると、

30

Chapter 2 : 関数型プログラミング

整数のリストを返します。実行して得られるのが空リストであるという事実があっても、 型はまったく変わりません。

Objective Caml は、引数の形を使わないすべての関数に対して、パラメータ化された型 を与えます。このような多相性をパラメータ的多相性といいます7 。

型制約 Caml の型推論器はもっとも一般的な型を与えるので、式の型を指定することが便利な いし必要な場合もあります。 型制約の構文形式は次の通りです。 構文 :

( expr : t )

型推論器は、このような制約に出会うと、式の型を構成する際に考慮します。型制約を 使うことにより、次のようなことができます。



関数のパラメータの型を見やすくする。



意図した文脈以外で関数を使えないようにする。



式の型を指定する。これは特に変更可能な値に対して有用です(66 ページ参照)。

次の例は、そのような型制約の使用法を示しています。 # let add (x:int) (y:int) = x + y ; ; val add : int -> int -> int = # let make pair int (x:int) (y:int) = x,y; ; val make_pair_int : int -> int -> int * int = # let compose fn int (f : int → int) (g : int → int) (x:int) = compose f g x; ; val compose_fn_int : (int -> int) -> (int -> int) -> int -> int = # let nil = ( [] : string list); ; val nil : string list = [] # ’H’ :: nil; ; Characters 5-8: ’H’::nil;; ^^^ This expression has type string list but is here used with type char list

このようにして多相性を制限することにより、システムが推論する型を制約し、式の型 をよりよく制御することができます。次の例が示すように、型変数を含む場合も含めて、 定義されているいかなる型も使用可能です。 # let llnil = ( [] : ’a list list) ; ; val llnil : ’a list list = [] # [1;2;3]:: llnil ; ;

7. いくつかのあらかじめ定義された関数はこの規則にしたがいません。特に、構造等値関数(=)は多相的(型 が’a -> ’a -> bool)ですが、引数が等値かどうかテストするために、その構造を探索します。

Objective Caml の核となる関数型の部分

31

- : int list list = [[1; 2; 3]]

記号 llnil は任意の型のリストのリストになります。 ここでは制約の話をしているのであって、Objective Caml の型推論を明示的な型付けで 置き換えているわけではありません。特に、推論が許す範囲を越えて型を一般化するこ とはできません。 # let add general (x:’a) (y:’b) = add x y ; ; val add_general : int -> int -> int =

型制約はモジュールインターフェース (第 14 章参照) およびクラス定義 (第 15 章参照) で も使用されます。

例 この節では、やや複雑な関数の例をいくつか挙げます。これらの関数のほとんどは、元 から Objective Caml で定義されています。「教育」のために、それらを再定義します。 ここでは、再帰関数の最終的な場合わけを条件文によって実装します。したがって、Lisp に近いプログラミングスタイルとなります。より ML 風の定義の方法については、場合 わけによって関数を定義する別の方法を説明する際に述べます(33 ページ参照)。

リストの長さ まず、リストが空かどうかをテストする関数 null から始めましょう。 # let null l = (l = [] ) ; ; val null : ’a list -> bool =

次に、リストの長さ(すなわち要素の数)を計算する関数 size を定義します。 # let rec size l = if null l then 0 else 1 + (size (List.tl l)) ; ; val size : ’a list -> int = # size [] ; ; - : int = 0 # size [1;2;18;22] ; ; - : int = 4

関数 size は引数が空かどうかをテストします。もし空ならば 0 を返し、空でなければ リストの末尾の長さを計算し、得られた値に 1 をプラスして返します。

合成の繰り返し 式 iterate n f は f を n 回繰り返した値を計算します。 # let rec iterate n f = if n = 0 then (function x → x) else compose f (iterate (n-1) f) ; ; val iterate : int -> (’a -> ’a) -> ’a -> ’a =

32

Chapter 2 : 関数型プログラミング

関数 iterate は n が 0 かどうかテストして、もしそうならば恒等関数を返し、そうでな ければ f を n-1 回繰り返した関数を f と合成します。

iterate を用いると、べき乗を乗算の繰り返しとして定義することができます。 # let rec power i n = let i times = ( * ) i in iterate n i times 1 ; ; val power : int -> int -> int = # power 2 8 ; ; - : int = 256

power 関数は関数式 i times を n 回繰り返してから、その結果を 1 に適用します。そう すれば確かに整数の n 乗が求まります。

乗算表 引数として渡された整数の乗算表を計算する関数 multab を書くことにします。 まず、関数 apply fun list を定義し、f list が関数のリストならば、apply fun list x f list は f list の各要素を x に適用した結果のリストとなるようにします。 # let rec apply fun list x f list = if null f list then [] else ((List.hd f list) x) :: (apply fun list x (List.tl f list)) ; ; val apply_fun_list : ’a -> (’a -> ’b) list -> ’b list = # apply fun list 1 [( + ) 1;( + ) 2;( + ) 3] ; ; - : int list = [2; 3; 4]

関数 mk mult fun list は、0 から n まで変化する i について、引数を i 倍する関数の リストを返します。 # let mk mult fun list n = let rec mmfl aux p = if p = n then [ ( * ) n ] else (( * ) p) :: (mmfl aux (p+1)) in (mmfl aux 1) ; ; val mk_mult_fun_list : int -> (int -> int) list =

7 の乗算表は次のように得られます。 # let multab n = apply fun list n (mk mult fun list 10) ; ; val multab : int -> int list = # multab 7 ; ; - : int list = [7; 14; 21; 28; 35; 42; 49; 56; 63; 70]

リストに対する繰り返し 関数呼び出し fold left f a [e1; e2; ... ; en] は f ... (f (f a e1) e2) ... en を返します。したがって、n 回の適用が行われます。

型宣言とパターンマッチング

33

# let rec fold left f a l = if null l then a else fold left f ( f a (List.hd l)) (List.tl l) ; ; val fold_left : (’a -> ’b -> ’a) -> ’a -> ’b list -> ’a =

関数 fold left を使うと、整数のリストの要素の和を計算する関数を簡潔に定義するこ とができます。 # let sum list = fold left (+) 0 ; ; val sum_list : int list -> int = # sum list [2;4;7] ; ; - : int = 13

あるいは、文字列のリストの要素の連結を計算することもできます。 # let concat list = fold left (^) "" ; ; val concat_list : string list -> string = # concat list ["Hello "; "world" ; "!"] ; ; - : string = "Hello world!"

型宣言とパターンマッチング Objective Caml で元から定義されている型を使えば、組やリストからデータを作ること はできますが、ある種のデータ構造を記述するためには、新しい型を定義する必要があ ります。Objective Caml では、型宣言は再帰的であり、型’a list と同様の感覚で、型 変数によりパラメータ化できます。型推論は新しい宣言を考慮して、式の型を提示しま す。新しい型の値を作るには、型を定義するときに記述したコンストラクタを使います。 ML 系の言語の特徴として、パターンマッチングがあります。それにより、複雑なデー タ構造の要素を簡単にアクセスすることができます。関数定義は往々にして一つの引数 に対するパターンマッチングに相当し、場合わけによる関数の定義を可能にします。 我々はまず、元から定義されている型に対するパターンマッチングを示し、それから構 造型を宣言したり、そのような型の値を構成したり、それらの要素をパターンマッチン グでアクセスする様々な方法について説明します。

パターンマッチング パターンは厳密には Objective Caml の式ではありません。プリミティブ型の定数 (int, bool, char, . . . )、変数、コンストラクタ、ワイルドカードパターンと呼ばれる記号 など の要素を(文法および型の観点から)正しく並べたものです。それ以外の記号もパター ンを書くのに使います。我々は必要な範囲で、それらを紹介します。 パターンマッチングは値に対して適用され、値の形を認識して、それに応じて計算を導 くために使われます。このために、それぞれのパターンに対して、計算する式を関連づ けます。

34

構文 :

Chapter 2 : 関数型プログラミング

match expr with | p1 –> expr1 .. . | pn –> exprn

式 expr は異なるパターン p1 , . . . , pn に対して逐次的にマッチされます。もしそれらの パターンの一つ(たとえば pi )が expr の値に合致すれば、対応する計算分岐 (expri ) が 評価されます。これらの異なるパターン pi は同じ型を持ちます。異なる式 expri につい ても同様です。最初のパターンの前にある縦棒は省くこともできます。

例 パターンマッチングを用いて、論理的含意を実装する型 (bool * bool) –> bool の関 数 imply を定義する方法を、二通り示します。2 つ組にマッチするパターンは ( , ) と いう形をしています。 最初の方法では、真理値表と同様に、すべての可能性を列挙します。 # let imply v = match v with (true,true) → true | (true,false) → false | (false,true) → true | (false,false) → true; ; val imply : bool * bool -> bool =

変数を使って数個のケースをまとめれば、より簡潔な定義が得られます。 # let imply v = match v with (true,x) → x | (false,x) → true; ; val imply : bool * bool -> bool =

これら二つの imply は同じ関数を計算します。すなわち、同じ入力に対しては同じ値を 返します。

線形パターン パターンは必ず線形でなければいけません。すなわち、どの変数も、マッチされるパター ンの中に高々一回しか現われることはできません8 。したがって、 # let equal c = match c with (x,x) → true | (x,y) → false; ; Characters 35-36: (x,x) -> true ^ This variable is bound several times in this matching 8. 訳注:バージョン 3.01 からサポートされた、OR パターンの中の変数を除く

型宣言とパターンマッチング

35

のような書き方をしたいと思ったかもしれませんが、これはエラーになります。もしそ のような書き方ができたとすると、コンパイラは等値検査のやり方を知らなければなり ません。けれども、そうすると直ちに多くの問題が発生します。もし値の間の物理等値 を用いたら、あまりにも弱いシステムを得ることになります。たとえば、リスト [1; 2] が二回出現しても、それらが等値であることを認識できません。かといって、もし構造 等値を使うことにしたら、循環構造を永久に探索する危険をおかすことになります。た とえば再帰関数は循環構造ですが、関数以外の再帰的な(すなわち循環的な)値を構成 することも可能です(52 ページ参照)。

ワイルドカードパターン 記号 はありとあらゆる値にマッチします。これはワイルドカードパターンと呼ばれます。 ワイルドカードパターンは、複雑な型にマッチするように使うことができます。たとえ ば関数 imply の定義をさらに簡単にするために使います。 # let imply v = match v with (true,false) → false | _ → true; ; val imply : bool * bool -> bool =

パターンマッチングによる定義は、マッチされる値として可能なすべての場合を処理し なければなりません。さもないと、コンパイラは警告メッセージを表示します。 # let is zero n = match n with 0 → true ; ; Characters 17-40: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: 1 let is_zero n = match n with 0 -> true ;; ^^^^^^^^^^^^^^^^^^^^^^^ val is_zero : int -> bool =

実際、もし実引数が 0 と異なっていたら、この関数は何の値を返せばよいのかわかりま せん。そこで、ワイルドカードパターンを使用して場合わけを完全にすることができま す。 # let is zero n = match n with 0 → true | _ → false ; ; val is_zero : int -> bool =

もし実行時にどのパターンも選択されなければ、例外が起こります。したがって、次の ように書くこともできます。 # let f x = match x with 1 → 3 ; ; Characters 11-30: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: 0

36

Chapter 2 : 関数型プログラミング

let f x = match x with 1 -> 3 ;; ^^^^^^^^^^^^^^^^^^^ val f : int -> int = # f 1 ;; - : int = 3 # f 4 ;; Exception: Match_failure ("", 11, 30).

この Match Failure 例外は f 4 の呼び出しによって起こり、もし処理されなければ、進 行中の計算を停止します(53 ページ参照)。

パターンの組み合わせ 複数のパターンを組み合わせることにより、元のパターンのいずれかにしたがって値に マッチする、新しいパターンを得ることができます。その構文形式は次の通りです。 構文 :

p1 | . . . | pn

これはパターン p1 , . . . , pn を組み合わせて、新しいパターンを作ります。唯一の強い制 約として、これらのパターンにおいては、すべての名前づけが禁止されています9 。した がって、これらのパターンの一つ一つは、定数値かワイルドカードパターンしか含むこ とができません。次の例は、ある文字が母音かどうか確かめる方法を示しています。 # let is a vowel c = match c with ’a’ | ’e’ | ’i’ | ’o’ | ’u’ | ’y’ → true | _ → false ; ; val is_a_vowel : char -> bool = # is a vowel ’i’ ; ; - : bool = true # is a vowel ’j’ ; ; - : bool = false

パラメータのパターンマッチング 場合わけによる関数の定義は、パターンマッチングの本質的な使い方の一つです。そのよ うな定義を簡単に書くために、構文 function は引数のパターンマッチを許しています。

function 構文 :

| |

p1 –> expr1 p2 –> expr2 .. .

|

pn –> exprn

一つ目のパターンの前にある縦棒は、ここでも省略可能です。実際、我々はまるでジョル ダン氏のように10 、関数を定義するたびにパターンマッチングを使用します。事実、構 9. 訳注:バージョン 3.01 以降では可能 10. 原訳注:モリエールの劇 Le Bourgeois Gentilhomme(町人貴族)において、登場人物のジョルダン氏 は生まれて以来、ずっと散文詩調で話していたことに気づいて驚きます。この劇は

型宣言とパターンマッチング

37

文 function x –> expression は、一個の変数に縮退した単一のパターンを使用する、 パターンマッチングによる定義です。この仕様は、 # let f = function (x,y) → 2*x + 3*y + 4 ; ; val f : int * int -> int =

のように単純なパターンで使用することもできます。 実際、構文

function p1 –> expr1 | . . . | pn –> exprn は

function expr –> match expr with p1 –> expr1 | . . . | pn –> exprn と等価です。24 ページで触れた宣言の等価性を用いて、 # let f (x,y) = 2*x + 3*y + 4 ; ; val f : int * int -> int =

と書くこともできます。しかし、マッチされる値がコンストラクタを一つしか持たない 型でないと、こういう自然な書き方はできません。さもないと、パターンマッチングが 完全ではなくなってしまいます。 # let is zero 0 = true ; ; Characters 13-21: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: 1 let is_zero 0 = true ;; ^^^^^^^^ val is_zero : int -> bool =

マッチされる値の名前づけ パターンマッチングの際、パターンの一部や全体に名前をつけると便利なことがありま す。次の構文形式は、名前をパターンに束縛するキーワード as を導入します。 構文 :

( p as name )

これは、全体性を保ちつつ値を分解する必要があるときに便利です。次の例において、 関数 min rat は有理数の組を受け取り、小さいほうの有理数を与えます。それぞれの有 理数は分子と分母の組により表されています。 # let min rat pr = match pr with リンク: http://www.site-moliere.com/pieces/bourgeoi.htm で読むことができます。また、 リンク: http://moliere-in-english.com/bourgeois.html には、その部分を含め、英訳の抜粋があります。

38

Chapter 2 : 関数型プログラミング

((_,0),p2) → p2 | (p1,(_,0)) → p1 | (((n1,d1) as r1), ((n2,d2) as r2)) → if (n1 * d2) < (n2 * d1) then r1 else r2; ; val min_rat : (int * int) * (int * int) -> int * int =

二つの有理数を比較するには、分解して分子と分母(n1, n2, d1 および d2)に名前をつ ける必要がありますが、元の組(r1 または r2)も返さなければなりません。as 構文は、 このように単一の値の一部に名前をつけることを可能とします。これにより、結果とし て返す有理数を再構築する必要がなくなります。

ガードつきパターンマッチング ガードつきパターンマッチングは、パターンがマッチされた直後に条件式を評価するこ とに相当します。もし条件式が true を返せば、パターンに関連づけられた式が評価さ れ、さもなければ次のパターンへパターンマッチングが続きます。

構文 :

match expr with .. . | pi when condi –> expri .. .

次の例は二つの条件を用いて、二つの有理数の等しさをテストします。 # let eq rat cr = match cr with ((_,0),(_,0)) → true | ((_,0),_) → false | (_,(_,0)) → false | ((n1,1), (n2,1)) when n1 = n2 → true | ((n1,d1), (n2,d2)) when ((n1 * d2) = (n2 * d1)) → true | _ → false; ; val eq_rat : (int * int) * (int * int) -> bool =

もし第 4 のパターンがマッチしても、ガードが失敗したら、マッチングは第 5 のパター ンに続きます。 注意 パターンマッチングが完全かどうか Objective Caml が行う検証は、ガード の条件式が偽かもしれないと仮定します。結果的に、そのようなパターン は考慮されません。ガードが充足されるかどうか実行前に知ることは不可 能なためです。 次の例では、パターンマッチングが完全かどうか、検出することができません。 # let f = function x when x = x → true; ; Characters 10-40: Warning: Bad style, all clauses in this pattern-matching are guarded. let f = function x when x = x -> true;; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ val f : ’a -> bool =

型宣言とパターンマッチング

39

文字の区間に対するパターンマッチング 文字に対するパターンマッチングを行う際に、文字区間に対応するすべてのパターンの 組み合わせを作るのは面倒です。実際、ある文字が英字かどうか検査するのにさえ、少 なくとも 26 個のパターンを書いて組み合わせないといけません。そこで、文字について Objective Caml では 構文 :

’c1 ’ .. ’cn ’

という形のパターンを書くことができます。これは’c1 ’ | ’c2 ’ | ... | ’cn ’ という 組み合わせと等価です。 たとえばパターン’0’ .. ’9’ はパターン’0’ | ’1’ | ’2’ | ’3’ | ’4’ | ’5’ | ’6’ | ’7’ | ’8’ | ’9’ に対応します。最初の形のほうが読みやすく、かつ書きやすくなっ ています。 警告

この仕様は言語拡張の一部であり、将来のバージョンで は変更されるかもしれません。

パターンの組み合わせと区間を用いて、いくつかの基準にしたがって文字を分類する関 数を定義します。 # let char discriminate c = match c with ’a’ | ’e’ | ’i’ | ’o’ | ’u’ | ’y’ | ’A’ | ’E’ | ’I’ | ’O’ | ’U’ | ’Y’ → "Vowel" | ’a’..’z’ | ’A’..’Z’ → "Consonant" | ’0’..’9’ → "Digit" | _ → "Other" ; ; val char_discriminate : char -> string =

パターンのグループの順番に意味があることに注意してください。実際、二番目のパ ターンの集まりは一番目を含みますが、一番目のチェックが済むまで二番目は調べられ ません。

リストに対するパターンマッチング すでに見たように、リストは



空であるか(そのようなリストは [] という形をしています)、



第一要素(先頭)と部分リスト(末尾)からなります。このとき、リストは h::t という形をしています。

このような二つのリストの書き方は、パターンとして用いることができます。これによ り、リストに対するパターンマッチングを行うことができます。 # let rec size x = match x with [] → 0 | _::tail x → 1 + (size tail x) ; ; val size : ’a list -> int =

40 # size - : int # size - : int

Chapter 2 : 関数型プログラミング [] ; ; = 0 [7;9;2;6]; ; = 4

したがって、たとえばリストに対する繰り返し等の以前に記述した例(31 ページ参照) を、パターンマッチングを用いて書き直すことができます。 # let rec fold left f a = function [] → a | head :: tail → fold left f (f a head) tail ; ; val fold_left : (’a -> ’b -> ’a) -> ’a -> ’b list -> ’a = # fold left (+) 0 [8;4;10]; ; - : int = 22

パターンマッチングによる値の宣言 値の宣言は、実はパターンマッチングを用いています。宣言 let x = 18 は値 18 をパ ターン x にマッチさせます。宣言の左辺には、どのようなパターンでも書くことができ ます。パターンの中の変数は、マッチする値に束縛されます。 # let (a,b,c) = (1, true, ’A’); ; val a : int = 1 val b : bool = true val c : char = ’A’ # let (d,c) = 8, 3 in d + c; ; - : int = 11

このようなパターン変数のスコープは、通常のローカル宣言の静的スコープです。ここ では、c が’A’ に束縛されたままとなります。 # a + (int of char c); ; - : int = 66

他のパターンマッチングと同じように、値宣言もパターンマッチングとして完全ではな いことがあります。 # let [x;y;z] = [1;2;3]; ; Characters 5-12: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: [] let [x;y;z] = [1;2;3];; ^^^^^^^ val x : int = 1 val y : int = 2 val z : int = 3 # let [x;y;z] = [1;2;3;4]; ; Characters 4-11: Warning: this pattern-matching is not exhaustive.

41

型宣言とパターンマッチング Here is an example of a value that is not matched: [] let [x;y;z] = [1;2;3;4];; ^^^^^^^ Exception: Match_failure ("", 4, 11).

コンストラクタやワイルドカード、パターンの組み合わせを含めて、どのようなパター ンも使えます。 # let head :: 2 :: _ = [1; 2; 3] ; ; Characters 5-19: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: [] let head :: 2 :: _ = [1; 2; 3] ;; ^^^^^^^^^^^^^^ val head : int = 1 # let _ = 3. +. 0.14 in "PI" ; ; - : string = "PI"

この最後の例は、関数的世界の範囲ではほとんど意味がありません。計算された値 3.14 に名前が与えられず、失われるからです。

型宣言 Objective Caml の語句表現における、もう一つの可能な構成要素としては、型宣言があ ります。型宣言は、プログラムで使用する独自のデータ構造に対応する新しい型の定義 を可能にします。型には大きくわけて二つの種類があります。組やレコードのための積 型と、ユニオンのための和型です。 型宣言には、キーワード type を用います。 構文 :

type name = typedef ;;

変数宣言とは対照的に、型宣言はデフォルトで再帰的です。したがって、型宣言を結合 することにより相互再帰的な型の定義が可能です。

構文 :

type and

name1 name2 .. .

= =

typedef1 typedef2

and

namen

=

typedefn ;;

型宣言は、型変数によってパラメータ化できます。型変数名は常にアポストロフィ(文 字')で始まります。 構文 :

type 'a name = typedef ;;

42

Chapter 2 : 関数型プログラミング

複数の型引数があるときは、型の名前の前で組として宣言します。 構文 :

type ('a1 . . . 'an ) name = typedef ;;

宣言の左辺で定義された型引数しか、右辺にあらわれることはできません。 注意 Objective Caml の型表示器は、与えられた型の名前をつけかえます。一番 目は’a、二番目は’b、…というように名づけられます。 すでに存在する型から新しい型を定義することは、いつでも可能です。 構文 :

type name = type expression

これは一般的すぎる思われる型を制限するのに便利です。 # type ’param paired with integer = int * ’param ; ; type ’a paired_with_integer = int * ’a # type specific pair = float paired with integer ; ; type specific_pair = float paired_with_integer

型制約がなくても、推論によりもっとも一般的な型が生成されます。 # let x = (3, 3.14) ; ; val x : int * float = (3, 3.14)

しかし、型制約を使えば、望みの名前が表示されます。 # let (x:specific pair) = (3, 3.14) ; ; val x : specific_pair = (3, 3.14)

レコード レコードは、Pascal の record や C の struct のように、各々のフィールドに名前のつい た組です。レコードは常に、新しい型の宣言に対応します。レコード型は、自身の名前 と、それぞれのフィールドの名前および型によって定義されます。 構文 :

type name = { name1 : t1 ; . . . ; namen : tn } ;;

複素数を表現する型は # type complex = { re:float; im:float } ; ; type complex = { re : float; im : float; }

のように定義できます。 レコード型の値を作るには、それぞれのフィールドに(任意の順番で)値を与えます。 構文 :

{ namei1 = expri1 ; . . . ; namein = exprin } ;;

たとえば、実数部分 2. と虚数部分 3. を持つ複素数を作ります。 # let c = {re=2.;im=3.} ; ;

型宣言とパターンマッチング

43

val c : complex = {re = 2; im = 3} # c = {im=3.;re=2.} ; ; - : bool = true

一部のフィールドがない場合は、次のようなエラーが出ます。 # let d = { im=4. } ; ; Characters 9-18: let d = { im=4. } ;; ^^^^^^^^^ Some record field labels are undefined: re

フィールドをアクセスするには二つの方法があります。ドット記法と、一部のフィール ドに対するパターンマッチングです。 ドット記法の文法は普通です。 構文 :

expr.name

式 expr はフィールド name を含むレコード型でなければいけません。 レコードに対するパターンマッチングを用いると、複数のフィールドに束縛された値を 取り出すことができます。 構文 :

{ namei = pi ; . . . ; namej = pj }

ここでのパターンは=記号の右側にきます(pi , . . . , pj )。このようなパターンにおいて は、レコードのすべてのフィールドが出現する必要はありません。 関数 add complex はドット記法を用いてフィールドにアクセスするのに対し、関数 mult complex はパターンマッチングを用いてフィールドにアクセスします。 # let add complex c1 c2 = {re=c1.re+.c2.re; im=c1.im+.c2.im}; ; val add_complex : complex -> complex -> complex = # add complex c c ; ; - : complex = {re = 4; im = 6} # let mult complex c1 c2 = match (c1,c2) with ({re=x1;im=y1},{re=x2;im=y2}) → {re=x1*.x2-.y1*.y2;im=x1*.y2+.x2*.y1} ; ; val mult_complex : complex -> complex -> complex = # mult complex c c ; ; - : complex = {re = -5; im = 12}

組に対するレコードの利点は、少なくとも二つあります。



フィールド名が説明的かつ識別的な情報を与えてくれる。特に、パターンマッチン グが簡単になります。



名前により、どのようなレコードでも任意のフィールドに同一の方法でアクセスが 可能となる。もはやフィールドの順番は関係がなく、名前のみが重要です。

44

Chapter 2 : 関数型プログラミング

次の例は、組に比べてレコードのフィールドにアクセスするのが楽であることを示して います。 # let a = (1,2,3) ; ; val a : int * int * int = (1, 2, 3) # let f tr = match tr with x,_,_ → x ; ; val f : ’a * ’b * ’c -> ’a = # f a ;; - : int = 1 # type triplet = {x1:int; x2:int; x3:int} ; ; type triplet = { x1 : int; x2 : int; x3 : int; } # let b = {x1=1; x2=2; x3=3} ; ; val b : triplet = {x1 = 1; x2 = 2; x3 = 3} # let g tr = tr.x1 ; ; val g : triplet -> int = # g b ;; - : int = 1

パターンマッチングでは、マッチされるレコードのすべてのフィールドを指示する必要 はありません。よって推論される型は最後のフィールドの型となります11 。 # let h tr = match tr with {x1=x} → x; ; val h : triplet -> int = # h b; ; - : int = 1

あるレコードと、いくつかのフィールドを除いて等しいレコードを作る構文があります。 これは、多くのフィールドを含むレコードに対し、しばしば役に立ちます。 構文 :

{ name with namei = expri ; . . . ; namej =exprj }

# let c = {b with x1=0} ; ; val c : triplet = {x1 = 0; x2 = 2; x3 = 3}

b の値がコピーされて、フィールド x1 だけが違う値を持つ新しいレコードが作られます。 警告

この仕様は言語拡張の一部であり、将来のバージョンで は変更されるかもしれません。

和型 デカルト積に相当する組やレコードとは対照的に、和型の宣言は集合のユニオンに相当 します。異なる型(たとえば整数や文字列)が一つの型としてまとめられます。そのよ うな和において、異なる要素はコンストラクタによって区別します。コンストラクタは、 一方ではその名が示す通り、その型の値を構成することを可能にし、他方ではパターン

11. 訳注:“The inferred type is then that of the last field.” 英文の意味が不明。仏文の確認が必要。

型宣言とパターンマッチング

45

マッチングにより、値の構成要素にアクセスすることを可能にします。コンストラクタ を引数に適用すると、値はこのような新しい型に属することになります。 和型を宣言するには、コンストラクタと、その引数の型を与えます。

type 構文 :

name = . . . | Namei . . . | Namej of tj . . . | Namek of tk * ...* tl . . . ;;

コンストラクタの名前は特別な識別子です。 警告

コンストラクタの名前は、常に大文字で始まります。

定数コンストラクタ 引数を期待しないコンストラクタは、定数コンストラクタと呼ばれます。定数コンスト ラクタは、定数のように、言語の値として以降で直接使用することができます。 # type coin = Heads | Tails; ; type coin = Heads | Tails # Tails; ; - : coin = Tails

bool 型はこのような方法で定義することが可能です。

引数つきコンストラクタ コンストラクタは引数をとることができます。キーワード of は、コンストラクタの引数 の型を示します。これにより、異なる型の対象を一つの型にまとめ、それぞれを特定の コンストラクタにより導入することが可能です。 データ型を定義する古典的な例として、一つのゲーム(ここではタロット12 )における トランプのカードを表現してみます。型 suit と card は次のように定義されます。 # type suit = Spades | Hearts | Diamonds | Clubs ; ; # type card = King of suit | Queen of suit | Knight of suit | Knave of suit | Minor card of suit * int | Trump of int | Joker ; ;

12. 原訳注:フランスのタロットのルールは、たとえば リンク: http://www.pagat.com/tarot/frtarot.html にあります。

46

Chapter 2 : 関数型プログラミング

型 card の値は、コンストラクタを適切な型の値に適用することによって生成されます。 # King Spades ; ; - : card = King Spades # Minor card(Hearts, 10) ; ; - : card = Minor_card (Hearts, 10) # Trump 21 ; ; - : card = Trump 21

また、たとえば引数として渡されたマークの、すべてのカードのリストを構成する関数 all cards は、このようになります。 # let rec interval a b = if a = b then [b] else a :: (interval (a+1) b) ; ; val interval : int -> int -> int list = # let all cards s = let face cards = [ Knave s; Knight s; Queen s; King s ] and other cards = List.map (function n → Minor card(s,n)) (interval 1 10) in face cards @ other cards ; ; val all_cards : suit -> card list = # all cards Hearts ; ; - : card list = [Knave Hearts; Knight Hearts; Queen Hearts; King Hearts; Minor_card (Hearts, 1); Minor_card (Hearts, 2); Minor_card (Hearts, 3); Minor_card (Hearts, ...); ...]

和型の値を扱うには、パターンマッチングを使います。次の例では、型 suit および型 card の値を文字列(型 string)に変換する関数を構築しています。 # let string of suit = function Spades → "spades" | Diamonds → "diamonds" | Hearts → "hearts" | Clubs → "clubs" ; ; val string_of_suit : suit -> string = # let string of card = function King c → "king of " ^ (string of suit c) | Queen c → "queen of " ^ (string of suit c) | Knave c → "knave of " ^ (string of suit c) | Knight c → "knight of " ^ (string of suit c) | Minor card (c, n) → (string of int n) ^ " of "^(string of suit c) | Trump n → (string of int n) ^ " of trumps" | Joker → "joker" ; ; val string_of_card : card -> string =

パターンを一列に並べることで、これらの関数が読みやすくなります。 コンストラクタ Minor card は二つの引数を持つコンストラクタとして扱われます。そ のような値に対してパターンマッチングを行うには、その二つの構成要素を指定する必 要があります。 # let is minor card c = match c with

型宣言とパターンマッチング

47

Minor card v → true | _ → false; ; Characters 41-53: Minor_card v -> true ^^^^^^^^^^^^ The constructor Minor_card expects 2 argument(s), but is here applied to 1 argument(s)

コンストラクタの一つ一つの構成要素を指定しないですませるためには、対応する組型 を括弧で囲んで、一つの引数をとるようにコンストラクタを宣言します。次の二つのコ ンストラクタは、パターンマッチングのしかたが異なります。 # type t = C of int * bool | D of (int * bool) ; ; # let access v = match v with C (i, b) → i,b | D x → x; ; val access : t -> int * bool =

再帰型 再帰的な型の定義は、通常のデータ構造(リスト、ヒープ、木、グラフなど)を記述す るために、どのような算法言語でも必要不可欠です。そのため、値の宣言(let)とは対 照的に、Objective Caml における型の宣言はデフォルトで再帰的です。

Objective Caml の元から定義されているリストの型は、一つだけ引数をとります。リス ト構造に二つの異なる型、たとえば整数(int)と文字(char)に属する値を格納した いと思うかもしれません。その場合は、以下のように定義します。 # type int or char list = Nil | Int cons of int * int or char list | Char cons of char * int or char list ; ; # let l1 = Char cons ( ’=’, Int cons(5, Nil) ) in Int cons ( 2, Char cons ( ’+’, Int cons(3, l1) ) ) ; ; - : int_or_char_list = Int_cons (2, Char_cons (’+’, Int_cons (3, Char_cons (’=’, Int_cons (...)))))

パラメータ化された型 同様に、ユーザはパラメータをもつ型を定義することができます。これにより、二つの 異なる型の値を含むリストの例を一般化することができます。 # type (’a, ’b) list2 =

48

Chapter 2 : 関数型プログラミング

| |

Nil Acons of ’a * (’a, ’b) list2 Bcons of ’b * (’a, ’b) list2 ; ;

# Acons(2, Bcons(’+’, Acons(3, Bcons(’=’, Acons(5, Nil))))) ; ; - : (int, char) list2 = Acons (2, Bcons (’+’, Acons (3, Bcons (’=’, Acons (...)))))

当然、引数’a と’b を同じ型で具体化することもできます。 # Acons(1, Bcons(2, Acons(3, Bcons(4, Nil)))) ; ; - : (int, int) list2 = Acons (1, Bcons (2, Acons (3, Bcons (4, Nil))))

このように型 list2 を使えば、先の例のように、偶数と奇数を区別することができます。 その方法で偶数の部分リストを抽出し、通常のリストを構築します。 # let rec extract odd = function Nil → [] | Acons(_, x) → extract odd x | Bcons(n, x) → n :: (extract odd x) ; ; val extract_odd : (’a, ’b) list2 -> ’b list =

この関数の定義は、リスト構造に格納された値の性質について、何ら手がかりを与えま せん。型がパラメータ化されているのは、そのためです。

宣言のスコープ コンストラクタの名前はグローバル宣言と同じスコープ規則にしたがいます。すなわ ち、再定義は以前の定義を隠します。それでも、その隠された型の値はまだ存在します。 Objective Caml の対話的トップレベルは出力のとき、そのような二つの型を区別しませ ん。そのために、ある種の不明確なエラーメッセージが出ます。 下の一つ目の例では、型 int or char の定数コンストラクタ Nil が、型 (’a, ’b) list2 のコンストラクタ宣言によって隠されています。 # Int cons(0, Nil) ; ; Characters 13-16: Int_cons(0, Nil) ;; ^^^ This expression has type (’a, ’b) list2 but is here used with type int_or_char_list

次の例では、少なくともはじめて見たときにはかなり不可解なエラーメッセージが発生 します。下の小さなプログラムを考えましょう。 # type t1 = Empty | Full; ; type t1 = Empty | Full # let empty t1 x = match x with Empty → true | Full → false ; ; val empty_t1 : t1 -> bool = # empty t1 Empty; ;

型宣言とパターンマッチング

49

- : bool = true

そして、型 t1 を再宣言します。 # type t1 = {u : int; v : int} ; ; type t1 = { u : int; v : int; } # let y = { u=2; v=3 } ; ; val y : t1 = {u = 2; v = 3}

ここで、関数 empty t1 を新しい型 t1 の値に適用すると、次のエラーメッセージを得ま す。 # empty t1 y; ; Characters 10-11: empty_t1 y;; ^ This expression has type t1 but is here used with type t1

一つ目の t1 は一番目に定義した型を表し、二つ目の t1 は二番目に定義した型に対応し ます。

関数型 コンストラクタの引数の型は任意です。特に、関数型を含んでもまったく構いません。次 の型は、最後以外の要素がすべて関数値であるようなリストを構成します。 # type ’a listf = Val of ’a | Fun of (’a → ’a) * ’a listf ; ; type ’a listf = Val of ’a | Fun of (’a -> ’a) * ’a listf

関数値は言語によって扱うことのできる値なので、型 listf の値 # let eight div = (/) 8 ; ; val eight_div : int -> int = # let gl = Fun (succ, (Fun (eight div, Val 4))) ; ; val gl : int listf = Fun (, Fun (, Val 4))

や、そのような値にパターンマッチする関数 # let rec compute = function Val v → v | Fun(f, x) → f (compute x) ; ; val compute : ’a listf -> ’a = # compute gl; ; - : int = 3

を構成することができます。

50

Chapter 2 : 関数型プログラミング

例:木の表現 木構造はプログラミングにおいて頻繁に出現します。再帰型を利用すれば、そのような構 造を簡単に定義したり操作することができます。本節では、木構造の例を二つ挙げます。 二分木 頂点が一つの型の値によってラベルづけされている二分木構造を、以下の宣言 により定義します。 # type ’a bin tree = Empty | Node of ’a bin tree * ’a * ’a bin tree ; ;

この構造を用いて、二分探索木を使った小さな整列プログラムを定義します。二分探索 木は、左の枝にあるすべての値が根の値より小さく、右の枝にあるすべての値は根の値 より大きいという性質があります。図 2.5 に、整数上のそのような構造の例を挙げます。 空の頂点(コンストラクタ Empty)は小さな四角により表現され、その他の頂点(コン ストラクタ Node)は、格納している値が記された円で表現されています。

5

2

1

0

8

3

4

7

9

6

図 2.5: 二分探索木 以下の関数により、二分探索木を順番に探索して、整列リストを抽出します。 # let rec list of tree = function Empty → [] | Node(lb, r, rb) → (list of tree lb) @ (r :: (list of tree rb)) ; ; val list_of_tree : ’a bin_tree -> ’a list =

リストから二分探査木を得るために、挿入関数を定義します。 # let rec insert x = function

型宣言とパターンマッチング

51

Empty → Node(Empty, x, Empty) | Node(lb, r, rb) → if x < r then Node(insert x lb, r, rb) else Node(lb, r, insert x rb) ; ; val insert : ’a -> ’a bin_tree -> ’a bin_tree =

関数 insert を繰り返すことにより、リストを木に変換する関数が得られます。 # let rec tree of list = function [] → Empty | h :: t → insert h (tree of list t) ; ; val tree_of_list : ’a list -> ’a bin_tree =

すると、整列関数は単純に関数 tree of list と list of tree の合成になります。 # let sort x = list of tree (tree of list x) ; ; val sort : ’a list -> ’a list = # sort [5; 8; 2; 7; 1; 0; 3; 6; 9; 4] ; ; - : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9]

一般平面木 この部分では、List モジュールであらかじめ定義されている以下の関数を 使用します(217 ページ参照)。



List.map:リストのすべての要素に関数を適用し、その結果のリストを返します。



List.fold left:32 ページで定義した関数 fold left と等価です。



List.exists:リストのすべての要素に、論理値関数を適用します。もしそれらの 適用が一つでも true を与えたら、結果は true になり、そうでなければ、この関 数は false を返します。

一般平面木とは、枝の数があらかじめ決まっていない木のことです。個々の頂点に枝の リストが関連づけられており、その長さは様々です。 # type ’a tree = Empty | Node of ’a * ’a tree list ; ; 空の木は値 Empty により表現されます。葉とは、Node(x,[]) あるいは縮退した Node(x, [Empty;Empty; ..]) という形をした、枝のない頂点のことです。すると、これらの木 を操作する関数、たとえばある要素が木に属するかどうか判定したり、木の高さを計算 する関数を書くことは、比較的容易です。 要素 e が木に属するかどうか検査するには、次のアルゴリズムを用います:もし木が空 ならば、e はその木に属しません。そうでなければ、e が根のラベルに等しいか、あるい は枝の一つに属するときのみ、e は木に属します。 # let rec belongs e = function Empty → false | Node(v, bs) → (e=v) or (List.exists (belongs e) bs) ; ; val belongs : ’a -> ’a tree -> bool =

52

Chapter 2 : 関数型プログラミング

木の高さを計算するには、次の定義を用います:空の木は高さが 0 です。さもなくば、木 の高さは、もっとも高い部分木の高さプラス 1 に等しくなります。 # let rec height = let max list l = List.fold left max 0 l in function Empty → 0 | Node (_, bs) → 1 + (max list (List.map height bs)) ; ; val height : ’a tree -> int =

関数以外の再帰的値 関数値以外の再帰的宣言により、循環データ構造を定義することができます。 以下の宣言は、一要素の循環リストを構築します。 # let rec l = 1 :: l ; ; val l : int list = [1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; ...]

そのようなリストに再帰関数を適用すると、メモリがオーバーフローするまでループす る危険があります。 # size l ; ; Stack overflow during evaluation (looping recursion?).

構造等値が依然として使用可能なのは、最初に物理等値が確認される場合のみです。 # l=l ; ; - : bool = true

要するに、もし新しいリストを定義したら、たとえ等しくても、構造等値検査を使って はいけません。さもないと、プログラムが永久にループします。ですから、次の例を評 価しようと試みることはお勧めできません。

let rec l2 = 1::l2 in l=l2 ;; 一方、物理等値は依然としていつでも判定可能です。 # let rec l2 = 1 :: l2 in l==l2 ; ; - : bool = false

述語==は即値の等しさか、構造オブジェクトの共有(値のアドレスの等しさ)を検査し ます。これを用いて、リストを探索するとき、すでに調べた部分リストを再探索しない ように検査してみましょう。まずはじめに、物理等値によってリストの要素の存在を検 証する関数 memq を定義します。これは構造等値を検査する関数 mem とは対照的な関数 です。これら二つの関数は、モジュール List に属します。 # let rec memq a l = match l with

型付け、定義域、例外

53

[] → false | b :: l → (a==b) or (memq a l) ; ; val memq : ’a -> ’a list -> bool =

すでに調べたリストのリストを覚えておき、一つのリストが二度あらわれたら止まるよ うに、サイズを計算する関数を再定義します。 # let special size l = let rec size aux previous l = match l with [] → 0 _::l1 → if memq l previous then 0 else 1 + (size aux (l :: previous) l1) in size aux [] l ; ; val special_size : ’a list -> int = # special size [1;2;3;4] ; ; - : int = 4 # special size l ; ; - : int = 1 # let rec l1 = 1 :: 2 :: l2 and l2 = 1 :: 2 :: l1 in special size l1 ; ; - : int = 4 |

型付け、定義域、例外 関数に対して推論される型は、その関数の定義域の上位集合に対応します。関数が型 int の引数をとるからといって、引数として渡されるすべての整数について、値を計算する 方法を知っているとは限りません。この問題は一般に、Objective Caml の例外機構によ り処理されます。例外が発生すると計算への割り込みが起こりますが、この割り込みは プログラムで傍受して処理することができます。そうするためには、プログラムの実行 において、例外が発生する式を計算する前に、例外ハンドラを登録しておかなくてはな りません。

部分関数と例外 関数の定義域は、その関数が計算を行う値の集合に相当します。多くの数学的関数は部 分的です。たとえば割り算や自然対数が挙げられます。この問題は、もっと複雑なデー タ構造を操作する関数でも発生します。実際、空リストの第一要素を計算した結果は何 になるというのでしょうか。同様に、factorial 関数を負の整数について評価したら、 無限に再帰してしまいます。 プログラムの実行中には、たとえばゼロによる割り算の試みのように、例外的な状況が 発生することがあります。数をゼロで割ろうとしたら、良くともプログラムが止まって しまうでしょうし、悪ければマシンの状態が不整合になるでしょう。プログラミング言 語の安全性は、このような特別な場合に、そういう状況が発生しないという保証に由来 します。例外は、そのようなケースに対応する方法の一つです。

54

Chapter 2 : 関数型プログラミング

1 を 0 で割ると、特定の例外が発生します。 # 1/0; ; Exception: Division_by_zero.

Exception: Division_by_zero というメッセージは、Division by zero 例外が発生 したことと共に、その例外が処理されなかったことを示しています。この例外は、言語 の核で宣言されているものの一つです。 パターンマッチングが完全でない、すなわち与えられた式のすべての場合にマッチせ ず、関数の型が定義域に一致しないことがよくあります。このような誤りを防ぐために、 Objective Caml はそのような場合にメッセージを表示します。 # let head l = match l with h :: t → h ; ; Characters 14-36: Warning: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: [] let head l = match l with h::t -> h ;; ^^^^^^^^^^^^^^^^^^^^^^ val head : ’a list -> ’a =

もしそれでもプログラマが不完全な定義を直さず、誤って部分関数を呼んだ場合は、Objective Caml は例外機構を用います。 # head [] ; ; Exception: Match_failure ("", 14, 36).

最後に、元から定義されているもう一つの例外 Failure は前に出てきました。この例外 は、型 string の引数をとります。関数 failwith を使用してこの例外を発生させること が可能です。これを使用して、独自に head を定義することができます。 # let head = function [] → failwith "Empty list" | h :: t → h; ; val head : ’a list -> ’a = # head [] ; ; Exception: Failure "Empty list".

例外の定義 Objective Caml では、例外は元から定義されている型 exn に属します。この型は拡張可 能な和型であるという点で非常に特別です。この型の値の集合は、新しいコンストラク タを宣言することにより、拡張することができます13 。この機能を利用して、ユーザは

13. 原訳注:Objective Caml 3.00 の新しい「多相バリアント」の機能により、今では他にも一種の和型を 拡張することが可能です。

型付け、定義域、例外

55

型 exn に新しいコンストラクタを追加することにより、独自の例外を定義することがで きます。 例外宣言の文法は次の通りです: 構文 :

exception Name ;;

または 構文 :

exception Name of t ;;

例外宣言の例をいくつか示します: # exception MY EXN; ; exception MY_EXN # MY EXN; ; - : exn = MY_EXN # exception Depth of int; ; exception Depth of int # Depth 4; ; - : exn = Depth 4

このように、例外は言語の値として一人前に扱われます。 警告

例外の名前はコンストラクタです。したがって必ず大文 字で始まらなければいけません。

# exception lowercase ; ; Characters 11-20: exception lowercase ;; ^^^^^^^^^ Syntax error

警告

例外は単相的です。例外の引数の宣言に型変数はありま せん。

# exception Value of ’a ; ; Characters 20-22: exception Value of ’a ;; ^^ Unbound type parameter ’a

もし多相的な例外があったら、任意の返り値型を持つ関数を定義することができてしま います。これについては、57 ページでより詳しく述べます。

例外の発生 関数 raise は言語のプリミティブ関数です。これは例外を引数にとり、完全に多相的な 返り値型を持ちます。 # raise ; ;

56

Chapter 2 : 関数型プログラミング

- : exn -> ’a = # raise MY EXN; ; Exception: MY_EXN. # 1+(raise MY EXN); ; Exception: MY_EXN. # raise (Depth 4); ; Exception: Depth 4.

関数 raise は Objective Caml で記述することができないので、あらかじめ定義されて いなければなりません。

例外処理 そもそも例外を発生させる目的は、例外を処理して、発生した例外の値により計算の系 列をコントロールすることにあります。したがって、式を評価する順序は、どの例外が 発生するかを決定する場合に重要となります。我々は純粋に関数的な文脈から抜け出て、 次章で議論するように(84 ページ参照)、引数を評価する順序が計算の結果を変えうる 世界へ入っていきます。 次の構文は式の値を計算しますが、計算中に発生した例外の処理を可能とします。

構文 :

try expr with | p1 –> expr1 .. . | pn –> exprn

もし expr の評価が例外を発生しなければ、結果は expr を評価した結果になります。さ もなくば、発生した例外の値がパターンマッチされ、最初にマッチしたパターンに対応 する式の値が返されます。もしどのパターンも発生した例外の値に対応しなければ、そ の例外はプログラムの実行中に入った一つ外側の try-with に伝搬します。したがって、 例外のパターンマッチングは常に完全であると考えられます。暗黙に、最後のパターン は | e -> raise e となります。もしマッチする例外ハンドラがプログラムの中で見つ からなければ、システム自身が例外を傍受する役目をし、エラーメッセージを表示して プログラムを終了します。 例外(すなわち型 exn の値)を計算することと、例外を発生して計算を中断することを 混同しないでください。例外は他の値と同じように、関数の結果として返すことができ ます。 # let return x = Failure x ; ; val return : string -> exn = # return "test" ; ; - : exn = Failure "test" # let my raise x = raise (Failure x) ; ; val my_raise : string -> ’a = # my raise "test" ; ;

多相性と関数の返り値

57

Exception: Failure "test".

my raise を適用しても値を返さないのに対し、return を適用すると型 exn の値が返る ことに気をつけます。

例外を利用した計算 例外的な値を扱うという使い方の他に、例外は特定のプログラミングスタイルをサポー トし、最適化の源となることもできます。次の例は、整数のリストのすべての要素の積 を求めます。例外を利用して、値 0 に遭遇したらリストの探索を中断し、0 を返します。 # exception Found zero ; ; exception Found_zero # let rec mult rec l = match l with [] → 1 | 0 :: _ → raise Found zero | n :: x → n * (mult rec x) ; ; val mult_rec : int list -> int = # let mult list l = try mult rec l with Found zero → 0 ; ; val mult_list : int list -> int = # mult list [1;2;3;0;5;6] ; ; - : int = 0

待機しているすべての計算、すなわち一つ一つの再帰呼び出しに続く n による掛け算は、 放棄されます。raise に遭遇すると、with で指定されたパターンマッチングから計算が 再開します。

多相性と関数の返り値 Objective Caml のパラメータ的多相性を利用すれば、返り値型がまったく定まっていな い関数を定義することができます。たとえば # let id x = x ; ; val id : ’a -> ’a =

です。 しかし、この返り値型は引数の型に依存します。したがって、関数 id が引数に適用され るとき、型推論機構には型変数’a を具体化する方法がわかります。よって、各々の特定 の使用については、id の型が定まります。 もし仮にそうでなかったとしたら、実行安全性の保証を得るために強い静的な型付けを 用いる意味はもはやありません。実際、もし仮に’a -> ’b のようなまったく不定の型が あったら、どんな型の変換もできてしまい、必然的に実行時エラーとなります。異なる 型の値の物理的表現は、同じではないからです。

58

Chapter 2 : 関数型プログラミング

見かけ上の矛盾 しかし、Objective Caml 言語においては、引数の型に現れない型変数を返り値型が含む ような関数を定義することが可能です。そのような例についていくつか考え、なぜそう いう可能性が、強い静的な型付けに矛盾しないのか見ていきます。 最初の例は次の通りです: # let f x = [] ; ; val f : ’a -> ’b list =

この関数を使えば、何からでも多相的な値を作ることができます。 # f () ; ; - : ’_a list = [] # f "anything at all" ; ; - : ’_a list = []

しかし、得られる値がまったく不定なわけではなく、リストになっています。よって、こ の値は単にどこでも使えるわけではありません。 また、恐るべき型’a -> ’b を持つ三つの例を、次に示します。 rec f1 x = f1 x ; ;

# let val f1 # let val f2 # let val f3

: ’a f2 x : ’a f3 x : ’a

-> ’b = = failwith "anything at all" ; ; -> ’b = = List.hd [] ; ; -> ’b =

これらの関数は、実行安全性に関する限り、実際には危険ではありません。というのは、 これらを用いて値を作ることはできないからです。最初の関数は永久にループしますし、 後の二つは例外を発生して計算を中断します。 同様に、新しい例外コンストラクタが、変数を含む型の引数を持つことを禁じているの は、型’a -> ’b の関数を定義できないようにするためです。 実際、もし仮に型’a -> exn の多相的な例外 Poly exn を宣言することができたとした ら、次のような関数を書くことができてしまいます。 let f = function 0 → raise (Poly exn false) | n → n+1 ; ;

すると、関数 f は型 int -> int、例外 Poly exn は型’a -> exn ですから、 let g n = try f n with Poly exn x → x+1 ; ; と定義することができます。この関数は正しく型付けされてしまい (Poly exn の引数 は任意なので)、よって (g 0) を評価したら、整数を論理値に足そうと試みることになっ てしまいます!

電卓

59

電卓 Objective Caml でプログラムを作る方法を理解するためには、何かを開発してみる必要 があります。ここでは例として電卓を選ぶことにします。ただし、整数の四則演算だけ 実行できる、もっとも簡単なモデルとします。 はじめに、電卓のキーを表現する型 key を定義します。この電卓には 15 個のキーがあり ます。それぞれの演算について 1 個、それぞれの数字について 1 個、そして=キーです。 # type key = Plus | Minus | Times | Div | Equals | Digit of int ; ; 数字のキーは整数引数をとる単一のコンストラクタ Digit にまとめられていることに注 意します。本当は、型 key には実際のキーを表現しない値もあります。たとえば、(Digit 32) は型 key の値としてはありえますが、電卓のいかなるキーも表現しません。 そこで、引数が電卓のキーに対応するかどうか確かめる関数 valid を書きます。この関 数の型は key -> bool です。すなわち、型 key の値を引数としてとり、型 bool の値を 返します。 最初のステップとして、整数が 0 から 9 の間に含まれるか確かめる関数を定義します。 この関数を is digit という名前で宣言します。 # let is digit = function x → (x>=0) && (x<=9) ; ; val is_digit : int -> bool =

そして、型 key の引数についてのパターンマッチングにより、関数 valid を定義します。 let valid ky = match ky with Digit n → is digit n | _ → true ; ; val valid : key -> bool = 最初のパターンは、valid の引数が Digit コンストラクタにより作られた値の場合に適 用されます。この場合は、Digit の引数が関数 is digit により検査されます。二番目の パターンはそれ以外のすべての型 key の値に適用されます。型付けのおかげで、マッチ される値は必ず型 key をもつことを思い出してください。 #

電卓の機構を書き始める前に、装置のキーが有効になったときの反応を形式的な観点か ら記述できるようなモデルを指定します。電卓は、最後にした計算、最後に有効となっ たキー、最後に有効となった演算、画面に表示された数、の 4 つのレジスタを持つと考 えます。これら 4 つのレジスタの集合を、電卓の状態と呼ぶことにします。この状態は、 キーパッドのキーが押されるたびに変化します。この変化を遷移と呼びます。このよう な機構を支配するのはオートマトンの理論です。我々のプログラムでは、以下のレコー ド型により状態を表現します。 # type state = { lcd : int; (* 最後に行われた計算 *) lka : key; (* 最後に有効となったキー *) loa : key; (* 最後に有効となった演算 *) vpr : int (* 表示されている値 *)

60

Chapter 2 : 関数型プログラミング } ;;

図 2.6 に遷移の列の例を挙げます。

−→ −→ −→ −→ −→ −→ −→

state (0, =, =, 0) (0, 3, =, 3) (3, +, +, 3) (3, 2, +, 2) (3, 1, +, 21) (24, ∗, ∗, 24) (24, 2, ∗, 2) (48, =, =, 48)

key 3 + 2 1 × 2 =

図 2.6: Transitions for 3 + 21 ∗ 2 = .

以下においては、演算子を含んだ型 key の値と二つの整数をとり、キーに対応する演算 を整数に適用した結果を返す関数 evaluate が必要になります。この関数は、型 key の 最後の引数についてのパターンマッチングにより定義されます。 # let evaluate x y ky = match ky with Plus → x + y | Minus → x - y | Times → x * y | Div → x / y | Equals → y | Digit _ → failwith "evaluate : no op"; ; val evaluate : int -> int -> key -> int =

そして、すべての場合を列挙することにより、遷移関数を定義します。現在の状態が 4 つ組 (a, b, ⊕, d) であると仮定します。



数字 x のキーが押されたら、二つの場合が考えられます。 – 最後に押されたキーも数字だった。したがって、これは電卓のユーザが入力 している最中の値です。よって、表示されている値に数字 x を付け加えなけ ればなりません。つまり、値を d × 10 + x で置き換えます。新しい状態は

(a, (Digit x), ⊕, d × 10 + x) –

となります。 最後に押されたキーが数字ではなかった。したがって、これは新たに入力さ れる数字の始まりです。新しい状態は

(a, (Digit x), ⊕, x) となります。

61

電卓



演算子 ⊗ のキーが押されたら、それにより演算の第二オペランドの入力が完了す るので、電卓は演算を実行しなければいけません。最後の演算(ここでは ⊕)が 保存されているのは、このためです。新しい状態は

(a ⊕ d, ⊗, ⊗, a ⊕ d) となります。 関数 transition を書くためには、上述の定義の言葉を Objective Caml の言葉に翻訳 すれば十分です。場合わけによる定義は、引数として渡されるキーについてのパターン マッチングによる定義となります。数字の場合は、それ自身が二つの場合から成り、ロー カルな関数 digit transition によって、最後に有効となったキーについてのパターン マッチングにより扱われます。 # let transition st ky = let digit transition n = function Digit _ → { st with lka=ky; vpr=st.vpr*10+n } | _ → { st with lka=ky; vpr=n } in match ky with Digit p → digit transition p st.lka | _ → let res = evaluate st.lcd st.vpr st.loa in { lcd=res; lka=ky; loa=ky; vpr=res } ; ; val transition : state -> key -> state =

この関数は state と key をとり、新しい state を計算します。 こうして、このプログラムを先の例でテストすることができます。 # let initial state = { lcd=0; lka=Equals; loa=Equals; vpr=0 } ; ; val initial_state : state = {lcd = 0; lka = Equals; loa = Equals; vpr = 0} # let state2 = transition initial state (Digit 3) ; ; val state2 : state = {lcd = 0; lka = Digit 3; loa = Equals; vpr = 3} # let state3 = transition state2 Plus ; ; val state3 : state = {lcd = 3; lka = Plus; loa = Plus; vpr = 3} # let state4 = transition state3 (Digit 2) ; ; val state4 : state = {lcd = 3; lka = Digit 2; loa = Plus; vpr = 2} # let state5 = transition state4 (Digit 1) ; ; val state5 : state = {lcd = 3; lka = Digit 1; loa = Plus; vpr = 21} # let state6 = transition state5 Times ; ; val state6 : state = {lcd = 24; lka = Times; loa = Times; vpr = 24} # let state7 = transition state6 (Digit 2) ; ; val state7 : state = {lcd = 24; lka = Digit 2; loa = Times; vpr = 2} # let state8 = transition state7 Equals ; ; val state8 : state = {lcd = 48; lka = Equals; loa = Equals; vpr = 48}

このような実行は、引数として渡されたキーのリストに対応する遷移の列を適用する関 数を使って、より簡潔に書くことができます。

62

Chapter 2 : 関数型プログラミング

# let transition list st ls = List.fold left transition st ls ; ; val transition_list : state -> key list -> state = # let example = [ Digit 3; Plus; Digit 2; Digit 1; Times; Digit 2; Equals ] in transition list initial state example ; ; - : state = {lcd = 48; lka = Equals; loa = Equals; vpr = 48}

練習問題 二つのリストをマージする 1.

昇順にソートされた二つの整数リストを入力として受け取り、それらの要素からな る新しいソートされたリストを返す関数 merge i を書いてください。

2.

比較関数と、その順序によってソートされた二つのリストを受け取り、同じ順序で マージされたリストを返す一般的な関数 merge を書いてください。比較関数は型 ’a → ’a → bool になります。

3.

その関数を降順にソートされた二つの整数リストと、降順にソートされた二つの 文字列リストにそれぞれ適用してください。

4.

もしリストの一つが要求された降順になっていなかったら、 何が起こるでしょうか?

5.

次の 3 つのフィールドからなる新しい list 型を書いてください:従来のリスト、 順序関数、およびリストがその順序になっているかどうかを示す論理値。

6.

その型のリストに新しい要素を加える関数 insert を書いてください。

7.

リストの要素を挿入ソートする関数 sort を書いてください。

8.

このリストについて新たに関数 merge を書いてください。

字句木 字句木(ないしトライ)は辞書を表現するのに用いられます。 # type lex node = Letter of char * bool * lex tree and lex tree = lex node list; ; # type word = string; ; lex node のブール値が true だと、単語の終わりを表します。このような構造において、 単語の列 “fa, false, far, fare, fried, frieze” は次のように格納されます。

63

練習問題

F R

A* L S

R*

I E

E*

E*

D*

Z E*

アスタリスク (*) は単語の終わりを表します。

1.

ある単語が型 lex tree の辞書に属するかどうかテストする関数 exists を書いて ください。

2.

単語と辞書を受け取って、その単語を加えた新しい辞書を返す関数 insert を書い てください。もしすでにその単語が辞書にあったら、挿入する必要はありません。

3.

単語のリストを受け取って、対応する辞書を構築する関数 construct を書いてく ださい。

4.

単語のリストと辞書を受け取って、辞書に属さない単語のリストを返す関数 verify を書いてください。

5.

辞書と長さを受け取って、その長さの単語の集合を14 返す関数 select を書いてく ださい。

Graph traversal 各頂点の後続頂点からなる隣接リストにより有効グラフを表現する型’a graph を定義 します。 # type ’a graph = ( ’a * ’a list) list ; ;

1.

グラフに頂点を挿入し、新しいグラフを返す関数 insert vtx を書いてください。

2.

グラフに辺を追加する関数 insert edge を書いてください。ただし、辺の両端の 頂点はすでにグラフに含まれているものとします。

3.

与えられた頂点から直接つながっている、すべての頂点を返す関数 has edges to を書いてください。

4.

与えられた頂点へ直接つながっている、すべての頂点のリストを返す関数 has edges from を書いてください。

14. 訳注:リストとして

64

Chapter 2 : 関数型プログラミング

Summary この章では、Objective Caml 言語の本質的特徴である、関数型プログラミングとパラメ タ的多相性について解説しました。言語の関数的な核の部分の式の構文と、解説した型 の構文を用いて、はじめてのプログラムをいくつか開発することができました。さらに、 関数の型と定義域との深い違いについて注意しました。例外機構を導入することでこの 問題を解消することができ、どのように計算が展開するか指定する新しいプログラミン グスタイルが早くも示されました。

To learn more 関数型言語の計算モデルは、アロンゾ・チャーチにより 1932 年に提唱された λ 計算で す。チャーチの目的は λ 定義可能性により実効的計算可能性の概念を定義することにあ りました。後に、このように導入された概念はチューリング(チューリング・マシン)や ゲーデルとエルブラン(再帰関数)の意味での計算可能性と等価であることがわかりま した。この一致から、特定の定式化によらない、普遍的な計算可能性の概念が存在する と考えられます。これがチャーチの提題です。この計算体系には、抽象と適用という二 個の構成要素しかありません。データ構造(整数、ブール値、組、…)は λ 式として表 されます。 関数型言語はこのモデルを実装し、より効率のよいデータ構造で拡張します。その最初 の代表は Lisp です。効率のために、最初の関数型言語たちはメモリの物理的変更を実装 したので、即時 (immediate) すなわち厳格 (strict) な評価戦略が必要でした。この戦略 では、関数の引数は関数へ渡されるまえに評価されます。純粋な関数型言語について遅 延評価(lazy あるいは call-by-need)の戦略が実装されたのは、Miranda、Haskell、あ るいは LML といった他の言語であり、実は後のことです。 型推論をともなう静的型つけは、80 年代の初頭に ML 一族により促進されました。ウェ ブぺージ リンク: http://www.pps.jussieu.fr/˜cousinea/Caml/caml history.html では、ML の歴史について概観しています。ML の計算モデルは、λ 計算の部分集合であ る型つき λ 計算で、プログラムを実行している途中で型エラーが発生しないことを保証 しています。しかし、 「まったく正当な」プログラムが ML の型システムにより拒否され ることもあります。そのようなケースは稀で、そういったプログラムは必ず型システム に合うように書き換えることができます。 もっともよく使われている関数型言語は、純粋でない関数型言語の代表である Lisp と ML の二つです。プログラミングに対する関数型のアプローチについて深く知るには、 [ASS96] と [CM98] の本が、それぞれ Scheme(Lisp の方言)と Caml-Light を用いた一 般的なプログラミングの道程を提示しています。

3 手続き型プログラミング 関数型プログラミングでは、関数に引数を渡してやることによって、何かの値を計算し ます。そこでどのように演算が実行されるのかということは気にしません。それに対し て、手続き型プログラミングは計算機の表現により近くなります。そこにはメモリ状態 というものがあり、プログラムの命令 の実行によって変更されていきます。手続き型の プログラムは、命令の列からなります。それぞれの命令は、実行するとメモリ状態を変 えることができます。入出力命令も、メモリや、ビデオメモリ、ファイルの変更である と考えることにします。 こういうプログラミングスタイルは、アセンブリプログラミングの発想からそのまま出 てきたものです。また、初期の汎用言語の数々にも見ることができます(Fortran、C、 Pascal など)。Objective Caml では、次の言語構成要素がこのモデルの当てはまります。



変更可能データ構造(配列や更新可能フィールドを持つレコードなど)



入出力演算



制御構造(ループや例外など)

アルゴリズムによっては、このプログラミングスタイルの方が簡単に書けます。例えば、 行列の積の計算がそうです。たしかに、ベクトルの代わりにリストを使って、純粋な関 数型に書き直すこともできますが、これは手続き型で書いたものと比べて自然でもなく、 効率的でもありません。 手続き的な要素を関数型言語に取り込んだ動機は、あるアルゴリズムを書こうしたとき に、手続き的スタイルが適切であるというときにはそれができるようにするということ です。一方、純粋な関数型と比べ、欠点が主に2つあります。



言語の型システムが複雑になり、純粋関数型なら正しいように思えるプログラムも 拒否されてしまうことになる。



メモリ表現と計算順序を常に把握しておく必要が出る。

それでも、プログラムの書き方のガイドラインをいくつか用意しておけば、プログラミ ングスタイルの選択肢が複数あるということが、アルゴリズムを書く上で非常に大きな

66

Chapter 3 : 手続き型プログラミング

柔軟性となります。それは、どんなプログラミング言語にとっても最大の目標です。そ れはともかくとしても、使用しているアルゴリズムに近い形で書かれたプログラムは、 より単純でしょうし、正しく動く可能性が大きいでしょう(少なくとも、間違えてもす ばやく訂正できるでしょう)。 このような理由から、Objective Caml 言語では、値を物理的に書き換えることのできる データ構造が幾種類か、プログラムの実行を制御する構造、手続き的な入出力ライブラ リが提供されています。

本章の流れ この章では、前章に続いて、Objective Caml 言語の基本要素を紹介してきますが、今度 は手続き的な構文に焦点を当てます。本章は5節からなります。第1節が最も重要です。 ここで、いろいろな変更可能データ構造とそのメモリ表現を解説します。第2節では、 この言語の基本的な入出力機能を簡単に説明します。第3節では、新しく登場する繰り 返し制御構造についてお話しします。第4節では、手続き的な機能がプログラムの実行 にどのような影響をおよぼすのか、特に、関数の引数の評価順序について論じます。最 後の節では、前章の電卓の例に戻り、これをメモリつき計算機へと変身させていきます。

変更可能データ構造 要素の物理的変更が可能なデータ構造は、ベクトル、文字列、変更可能フィールドのあ るレコード、参照です。 すでに見たように、Objective Caml の変数は、ある値に束縛されると、変数が生きてい る間ずっとこの値を保持し続けます。この束縛を変更するためには、変数の再定義が必 要です。この場合、再定義された変数は、前と「同じ」変数であるとはあまりいえませ ん。むしろ、同じ名前を持つ新しい変数が、前の変数を隠したのです。前の変数は、もは やアクセス不能でありながらも、同じ状態であり続けます。変更可能な値を使うと、新 たな変数を再定義しなくても、変数に割り当てられている値を書き換えることができる ようになります。変数の値には、書き込むためにも読み込むためにもアクセスできます。

ベクトル ベクトル、もしくは一次元配列は、同じ型の要素を決まった数だけ持つようなデータ構 造です。 ベクトルのひとつの書き方は、直接要素値を、[| と|] の間に、リスト同様セ ミコロンで区切って、書き並べるやりかたです。 # let v = [| 3.14; 6.28; 9.42 |] ; ; val v : float array = [|3.14; 6.28; 9.42|]

生成関数 Array.create に、ベクトルの要素数と初期値を渡して、新しいベクトルを作 ることもできます。 # let v = Array.create 3 3.14; ; val v : float array = [|3.14; 3.14; 3.14|]

変更可能データ構造

67

ある要素を参照したり書き換えたりするには、その要素のインデックスを与えてやります。 構文 :

expr1 . ( expr2 )

構文 :

expr1 . ( expr2 ) <- expr3

expr1 は、ベクトル(array 型)で、その要素の型は expr3 でなくてはなりません。式 expr2 はもちろん int 型でなくてはなりません。上記の書き換えを行う式は unit 型を 取ります。ベクトルの最初の要素のインデックスは 0 で、最後の要素のインデックスは ベクトルの長さから 1 を引いた数です。インデックスの式は、必ず括弧で囲う必要があ ります。 # v.(1) ; ; - : float = 3.14 # v.(0) <- 100.0 ; ; - : unit = () # v ;; - : float array = [|100; 3.14; 3.14|]

配列のインデックス範囲外のインデックスを使って要素参照を行うと、例外が発生しま す。 # v.(-1) +. 4.0; ; Exception: Invalid_argument "Array.get".

このチェックはプログラムの実行時に行われます。だから実行速度を落とすこともあり ます。それでもこれは必要不可欠なのです。そうしないと、ベクトルが割り当てられた 記憶領域の外に書き込みが行われ、深刻な実行時エラーが起きるかも知れないからです。 配列操作をするための関数群は、標準ライブラリの Array モジュールの一部をなしてい ます。これらは 8 章(217 ページ)で解説します。このあと出てくる例では、Array モ ジュールの中にある次の3つの関数を用います。



create(与えられたサイズの配列を生成し、与えられた初期値で埋める)



length(ベクトルの長さを返す)



append(2つのベクトルを連結する)

ベクトル要素の値共有 ベクトルが生成されるとき、その要素に入るものは生成時に渡された値です。というこ とは、もしこの値が構造を持つ値である場合、その値は共有されることになります。例 えば、Array モジュールの create 関数を使って、ベクトルのベクトルとして表現され る行列を1つ作ってみましょう。 # let v = Array.create 3 0; ; val v : int array = [|0; 0; 0|] # let m = Array.create 3 v; ; val m : int array array = [|[|0; 0; 0|]; [|0; 0; 0|]; [|0; 0; 0|]|]

68

Chapter 3 : 手続き型プログラミング

m

v

0

0

0

図 3.1: Memory representation of a vector sharing its elements.

今ベクトル v のひとつのフィールドを書き換えたとします。このベクトルは m の生成に 使われたのですから、自動的にすべての行列の「行」をいっぺんに書き換えたことにな ります(図 3.1 と 3.2 を参照)。 # v.(0) <- 1; ; - : unit = () # m; ; - : int array array = [|[|1; 0; 0|]; [|1; 0; 0|]; [|1; 0; 0|]|]

m

v

1

0

0

図 3.2: Modification of shared elements of a vector.

複製が起こるのは、ベクトルの初期値(Array.create の第2引数)が原子的値である ときで、共有が起こるのは、この値が構造を持つ値であるときです。 値の中で、サイズが Objective Caml の値の基準サイズ、つまりメモリワードサイズを 越えないものは、原子的値と呼ばれます。整数、文字、真偽値、定数コンストラクタが それにあたります。他の値、つまり構造を持つ値は、あるメモリ領域へのポインタとし て表されます。この違いは 9 章に詳しく説明されます(249 ページ)。例外は浮動小数の ベクトルです。浮動小数は構造を持つ値ですが、浮動小数ベクトルの生成では初期値は コピーされます。これは最適化のためです。12 章で、C 言語とのインタフェースについ て解説するとき(317 ページ)に、この例外について説明します。

変更可能データ構造

69

長方形でない行列 行列、すなわちベクトルのベクトルは、長方形である必要はありません。実際、ベクト ルであるひとつの要素を、異なる長さのベクトルで置き換えてもいっこうに構わないの です。これは行列のサイズを小さく抑えるのに使い出があります。次に定義する値 t は、 パスカルの三角形の係数を表す三角行列です。 # let t = [| [|1|]; [|1; 1|]; [|1; 2; 1|]; [|1; 3; 3; 1|]; [|1; 4; 6; 4; 1|]; [|1; 5; 10; 10; 5; 1|] |] ; ; val t : int array array = [|[|1|]; [|1; 1|]; [|1; 2; 1|]; [|1; 3; 3; 1|]; [|1; 4; 6; 4; ...|]; ...|] # t.(3) ; ; - : int array = [|1; 3; 3; 1|]

この例では、ベクトル t の i 番要素は、サイズ i + 1 の整数ベクトルです。こういう行 列を操作するためには、各要素ベクトルのサイズを計算する必要があります。

ベクトルのコピー ベクトルをコピー、もしくはベクトルを2つ連結して得られる結果は新規のベクトルで す。元のベクトルを書き換えても、複製の方は書き換えられることはありません。ただ し、例によって値の共有があったりしなければの話です。 # let v2 = Array.copy v ; ; val v2 : int array = [|1; 0; 0|] # let m2 = Array.copy m ; ; val m2 : int array array = [|[|1; 0; 0|]; [|1; 0; 0|]; [|1; 0; 0|]|] # v.(1)<- 352; ; - : unit = () # v2; ; - : int array = [|1; 0; 0|] # m2 ; ; - : int array array = [|[|1; 352; 0|]; [|1; 352; 0|]; [|1; 352; 0|]|]

この例では、m をコピーしても v へのポインタがコピーされるだけだということに注意 しましょう。v の要素がひとつ書き換えられると、m2 もいっしょに書き換えられます。 連結によって作られる新しいベクトルのサイズは、元の2つのベクトルのサイズの和に なります。 # let mm = Array.append m m ; ; val mm : int array array = [|[|1; 352; 0|]; [|1; 352; 0|]; [|1; 352; 0|]; [|1; 352; 0|]; [|1; 352; ...|]; ...|] # Array.length mm ; ; - : int = 6

70

Chapter 3 : 手続き型プログラミング

# m.(0) <- Array.create 3 0 ; ; - : unit = () # m ;; - : int array array = [|[|0; 0; 0|]; [|1; 352; 0|]; [|1; 352; 0|]|] # mm ; ; - : int array array = [|[|1; 352; 0|]; [|1; 352; 0|]; [|1; 352; 0|]; [|1; 352; 0|]; [|1; 352; ...|]; ...|]

v の書き換えを行うと m と mm にも影響を及ぼします。v が m と mm に共有されている値 からです。 # v.(1) <- 18 ; ; - : unit = () # mm; ; - : int array array = [|[|1; 18; 0|]; [|1; 18; 0|]; [|1; 18; 0|]; [|1; 18; 0|]; [|1; 18; ...|]; ...|]

文字列 文字列は、文字のベクトルの特殊な場合とも考えることができます。それでも、メモリ の使用を効率化するために1 、文字列の型は特別扱いになっています。要素のアクセスに は特別な構文を用います。 構文 :

expr1 . [expr2 ]

文字列の要素は物理的に書き換えることができます。 構文 :

expr1 . [expr2 ] <- expr3

# let s = "hello"; ; val s : string = "hello" # s.[2]; ; - : char = ’l’ # s.[2]<-’Z’; ; - : unit = () # s; ; - : string = "heZlo"

1. 32 ビットのワードに1文字1バイトとして4文字詰め込まれています。

変更可能データ構造

71

レコードの変更可能フィールド レコードのフィールドは、変更可能と宣言することができます。これをやるためにする べきことは、そのレコードの型宣言のときに、mutable というキーワードを使って明示 することだけです。 構文 :

type name = { . . . ; mutable namei : t ; . . . }

次の小さな例では、平面上の点を表現するレコードの型を定義します。 # type point = { mutable xc : float; mutable yc : float } ; ; type point = { mutable xc : float; mutable yc : float; } # let p = { xc = 1.0; yc = 0.0 } ; ; val p : point = {xc = 1; yc = 0}

そして、mutable と宣言されたフィールドの値は、次のような構文を用いて書き換える ことができます。 構文 :

expr1 . name <- expr2

式 expr1 は name フィールドを持つレコード型でなければなりません。書き換え演算の 帰り値は unit 型の値です。 # p.xc <- 3.0 ; ; - : unit = () # p ;; - : point = {xc = 3; yc = 0}

与えられた点を、その座標を書き換えて移動させる関数を書いてみましょう。副作用を 伴う演算を順々に並べるのには、パターンマッチ付の局所宣言を用います。 # let moveto p dx dy = let () = p.xc <- p.xc +. dx in p.yc <- p.yc +. dy ; ; val moveto : point -> float -> float -> unit = # moveto p 1.1 2.2 ; ; - : unit = () # p ;; - : point = {xc = 4.1; yc = 2.2}

ひとつのレコードの定義で、変更可能なフィールドとそうでないフィールドを混ぜるこ ともできます。mutable と指定されたフィールドだけが変更可能になります。 # type t = { c1 : int; mutable c2 : int } ; ; type t = { c1 : int; mutable c2 : int; } # let r = { c1 = 0; c2 = 0 } ; ; val r : t = {c1 = 0; c2 = 0} # r.c1 <- 1 ; ; Characters 0-9: r.c1 <- 1 ;;

72

Chapter 3 : 手続き型プログラミング

^^^^^^^^^ The record field label c1 is not mutable # r.c2 <- 1 ; ; - : unit = () # r ;; - : t = {c1 = 0; c2 = 1}

80 ページに、変更可能フィールドと配列を用いてスタック構造を実装するという例を紹 介します。

参照 Objective Caml は、多相型 ref を提供しています。これは、任意の値へのポインタを表 す型と見ることができます。Objective Caml の用語では、値への参照と呼びます。参照 値は変更可能です。ref 型は変更可能フィールドをひとつ持つようなレコードとして定 義されています。 type ’a ref = {mutable contents:’a} この型は省略記法として提供されています。参照先の値へと到達するには、プレフィッ クス関数 (!) を用います。参照の中身を書き換えるには、インフィックス関数 (:=) を用 います。 # let x = ref 3 ; ; val x : int ref = {contents = 3} # x ;; - : int ref = {contents = 3} # !x ; ; - : int = 3 # x := 4 ; ; - : unit = () # !x ; ; - : int = 4 # x := !x+1 ; ; - : unit = () # !x ; ; - : int = 5

多相性と書き換え可能な値 ref 型はパラメータつきの型です。だから、どんな型の値に対しても参照を作ることが できるのです。しかしながら、参照される値の型には、ある制限を課す必要があります。 多相型の値への参照を作っても良いようにするには、すこし警戒しなければなりません。

変更可能データ構造

73

今、何の制限も課されていないとしましょう。すると、こういう宣言を書く人が出てく るでしょう。

let x = ref [] ;; この変数 x は’a list ref 型を持つことになります。そして、この値が、Objective Caml の強い静的型付けと矛盾を来たすようなやり方で書き換えられてしまうかも知れません。 x := 1 :: !x ; ; x := true :: !x ; ;

このように、ひとつの同じ変数が、あるときには int list 型をとり、そのあとには bool list 型をとるなどということが起こり得ます。 こういう状況を防ぐために、Objective Caml の型推論機構では、弱い型変数 という新 しいカテゴリーの型変数を用います。文法的には、この型変数はアンダースコアを前に つけて普通の型変数と区別されます。 # let x = ref [] ; ; val x : ’_a list ref = {contents = []}

型変数’ a は型引数ではなく、具体化されるのを待っている未知の型なのです。x の宣言 のあと、それがはじめて使われるときに、’ a の取る値が定まり、それ以降は変わりま せん。(’ a に依存する型も同様に決定されます。) # x := 0::!x ; ; - : unit = () # x ;; - : int list ref = {contents = [0]}

これ以降は、変数 x はずっと int list ref 型をとります。 実は、未知のものを含む型は単相型です。その未知のものが何かが決まっていないのに も関わらずです。未知のものを多相型に具体化することはできません。 # let x = ref [] ; ; val x : ’_a list ref = {contents = []} # x := (function y → () )::!x ; ; - : unit = () # x ;; - : (’_a -> unit) list ref = {contents = []}

この例では、ある未知の型を別の型に具体化しました。その別の型は、その前の時点で は多相的であった型(’a -> unit)でしたが、それにもかかわらず、具体化された型は 新たな未知の型を含む単相型のままであり続けます。 この多相型に対する制限は、参照だけでなく、変更可能な部分を含むどんな値にも適用 されます。つまり、ベクトル、mutable 宣言付のフィールドを少なくともひとつ含むレ コード、などです。ここでの型引数は、変更可能な部分と関係ないものでも、すべて弱 い型変数となります。 # type (’a,’b) t = { ch1 :’a list ; mutable ch2 : ’b list } ; ; type (’a, ’b) t = { ch1 : ’a list; mutable ch2 : ’b list; } # let x = { ch1 = [] ; ch2 = [] } ; ;

74

Chapter 3 : 手続き型プログラミング

val x : (’_a, ’_b) t = {ch1 = []; ch2 = []}

警告

関数適用の型付けが次のように変更されるために、純粋 に関数的なプログラムにも影響が出ます。

同様に、多相関数を多相的な値に適用した場合、弱い型変数が出てきます。というのは、 この関数が物理的変更が可能な値を作るかもしれない、という可能性を除外できないか らです。別の言い方をすると、関数適用の結果は必ず単相型です。 # (function x → x) [] ; ; - : ’_a list = []

部分適用でも、同様な結果が得られます。 # let f a b = a ; ; val f : ’a -> ’b -> ’a = # let g = f 1 ; ; val g : ’_a -> int =

もう一度多相型が欲しければ、f の第2引数を抽象化して、そしてそれを再び適用して あげる必要があります。 # let h x = f 1 x ; ; val h : ’a -> int =

事実上、h を定義している式は、関数式 function x → f 1 x です。これを評価すると関数クロージャが生成されますが、これは副作用を起こ す危険はありません。それは関数の本体が評価されないからです。 一般に、「非拡張性」の式といわれるもの、つまり副作用を起こす危険が全くないと分 かっている式と、そうでない「拡張性」の式は、区別されます。Objective Caml の型シ ステムでは、式を構文の形でもって分類します。



「非拡張性」の式は主として、変数、変更不能な値のコンストラクタ、関数抽象を 含みます。



「拡張性」の式は主として、関数適用、変更可能な値のコンストラクタを含みま す。他にも、条件分岐、パターンマッチなどの制御構造もあります。

入出力 入出力関数は、何か値(たいがいは unit 型)を計算することはするのですが、その計算 中には入出力装置の状態変化が起ります。それはキーボードバッファの状態変化、画面出 力、ファイルへの書き込み、読み込みポインタの移動だったりします。初めから定義され ている型に、in channel と out channel があります。それぞれ入力チャネルと出力チャ ネルを表します。ファイルの最後が来ると、End of file 例外が発生します。Unix 流の 標準入力、標準出力、標準エラー出力に対応するものは、3つの定数 stdin、stdout、 と stderr です。

75

入出力

チャネル Objective Caml 標準ライブラリにある入出力関数が操作する対象は、通信チャネル、す なわち in channel 型、もしくは out channel 型の値です。チャネルは、最初から定義 されている標準の3つ以外は、以下の関数のどちらかを使って作ります。 # open in; ; - : string -> in_channel = # open out; ; - : string -> out_channel =

open in は、指定ファイルを、もし存在したら2 開きます。存在しなければ Sys error 例 外を発生させます。open out は、指定ファイルを、もし存在しなければ作成します。存 在すれば、元の内容を一旦破棄した上で開きます。 # let val ic # let val oc

ic = open in "koala"; ; : in_channel = oc = open out "koala"; ; : out_channel =

ファイルを閉じるための関数は、以下の通りです。 # close in ; ; - : in_channel -> unit = # close out ; ; - : out_channel -> unit =

読み書き 読み書きのための最も一般的な関数は以下のものです。 # input line ; ; - : in_channel -> string = # input ; ; - : in_channel -> string -> int -> int -> int = # output ; ; - : out_channel -> string -> int -> int -> unit =



input line ic は、入力チャネル ic から、最初の改行かファイル末尾までに出て くる、すべての文字を読み込み、それらを文字列で返します。 (改行は含みません。)



input ic s p l は、入力チャネル ic から、l 文字を読み込み、それを文字列 s の p 番目から後に保存します。実際に読み込んだ文字数が帰り値になります。



output oc s p l は、出力チャネル oc へ、文字列 s の p 番目から l 文字を書き 出します。

標準入出力から読み書きするには次の関数を使います。 # read line ; ; - : unit -> string = 2. 適切な読み込み許可があった場合。

76

Chapter 3 : 手続き型プログラミング

# print string ; ; - : string -> unit = # print newline ; ; - : unit -> unit =

文字以外に、単純な型をもつ値で、そのまま読み込んだり書き出したりできるものもあ ります。それらは、文字リストに変換できるような型の値です。 局所宣言と評価順序 let x = e1 in e2 という形の式を用いて、印字の羅列のような ことができます。一般論として x が e2 で使われる局所変数であるということを念頭に 置くと、まず e1 の評価が行われ、それから e2 の番が来るということが分かるでしょう。 今、これら2つの式として、手続き的で、() を返し、副作用も起こすような式を使えば、 この評価順序がピッタリ合います。一続きの局所宣言を入れ子状に書けば、印字の羅列 が実現できます。ここで、e1 の返す値が unit 型の () であるということが分かっている ので、局所宣言は、 () に対してパターンマッチを行うようにできます。 # let () = print string "and one," in let () = print string " and two," in let () = print string " and three" in print string " zero"; ; and one, and two, and three zero- : unit = ()

例:数当てゲーム この例では「数当てゲーム」をやります。まずあらかじめ、ある数値が決められます。 ユーザはそれを推測しなければなりません。プログラムは、毎回ユーザが数値を提示す ると、その数があらかじめ決められた数値よりも大きいか小さいかを教えてくれます。 #

let rec hilo n = let () = print string "type a number: " in let i = read int () in if i = n then let () = print string "BRAVO" in let () = print newline () in print newline () else let () = if i < n then let () = print string "Higher" in print newline () else let () = print string "Lower"

77

制御構造 in print newline () in hilo n ; ; val hilo : int -> unit =

実行例です。

# hilo type a Lower type a Higher type a BRAVO

64;; number: 88 number: 44 number: 64

- : unit = ()

制御構造 入出力操作や値の変更操作は、副作用を起こします。これらを使うとき、ここで新しく 登場する制御構造を取り入れて手続き的なプログラミングスタイルをすると楽になりま す。この節では、連接と繰り返しの構造を紹介します。

18 ページで条件分岐は見ましたが、それを省略した形 if then も手続き型の世界で良く 出てきます。例えば、こういう風に書きます。 # let n = ref 1 ; ; val n : int ref = {contents = 1} # if !n > 0 then n := !n - 1 ; ; - : unit = ()

連接 最も典型的な手続き型の構造は、連接です。連接は、式をセミコロンで区切って並べた もので、それらの式の評価は左から右へと行われます。 構文 :

expr1 ; . . . ; exprn

複数の式を連接したもの自体も式です。その連接式の値となるのは、連接の最後におか れた式(この場合は exprn )の値です。最終的な値に反映されないものの、最後以外の 式も含めてすべての式が評価されます。副作用も実行されます。 # print string "2 = "; 1+1 ; ; 2 = - : int = 2

副作用を用いると、普通の手続き型言語に見られる構文を再現することができます。 # let x = ref 1 ; ;

78

Chapter 3 : 手続き型プログラミング

val x : int ref = {contents = 1} # x:=!x+1 ; x:=!x*4 ; !x ; ; - : int = 8

セミコロンの前に出てくる式の値は破棄されるため、Objective Caml は、そういう式が unit 型でない場合には警告を発します。 # print int 1; 2 ; 3 ; ; Characters 14-15: Warning: this expression should have type unit. print_int 1; 2 ; 3 ;; ^ 1- : int = 3

このメッセージが出ないようにするには、ignore 関数を使います。 # print int 1; ignore 2; 3 ; ; 1- : int = 3

値が関数型の場合、Objective Caml は関数に渡すべき引数を忘れたのではないかと疑っ て、違うメッセージを出します。 # let g x y = x := y ; ; val g : ’a ref -> ’a -> unit = # let a = ref 10; ; val a : int ref = {contents = 10} # let u = 1 in g a ; g a u ; ; Characters 13-16: Warning: this function application is partial, maybe some arguments are missing. let u = 1 in g a ; g a u ;; ^^^ - : unit = () # let u = !a in ignore (g a) ; g a u ; ; Characters 22-25: Warning: this function application is partial, maybe some arguments are missing. let u = !a in ignore (g a) ; g a u ;; ^^^ - : unit = ()

一般に、連接は、その範囲を明確にするために括弧でくくった方が良いとされています。 括弧の構文は2通りあります。 構文 :

( expr )

構文 :

begin expr end

79

制御構造

これでようやく 76 ページでやった数当てゲームのプログラムをもっと自然な形に書き直 すことができます。 # let rec hilo n = print string "type a number: "; let i = read int () in if i = n then print string "BRAVO\n\n" else begin if i < n then print string "Higher\n" else print string "Lower\n" hilo n end ; ; val hilo : int -> unit =

;

繰り返し 繰り返し制御構造も、関数型の世界の外からきたものです。ループを続けるかどうか、 もしくは抜けるかどうかを判断する条件式というものは、その値を変化させるようなメ モリの物理的書き換えがない限り、意味をなしません。Objective Caml には2種類の繰 り返し制御構造があります。ひとつは範囲限定の for ループ、もうひとつは範囲非限定 の while ループです。ループ構造自身、式になります。ですから、値を返します。それ は unit 型の定数 () です。

for ループは、1ずつ上がっていく (to) ものと、1 ずつ下がっていく(downto)ものが あります。 構文 :

for name = expr1 to expr2 do expr3 done for name = expr1 downto expr2 do expr3 done

expr1 と expr2 は int 型です。もし expr3 が unit 型でないと、コンパイラは警告を発し ます。 # for i=1 to 10 do print int i; print string " " done; print newline () ; ; 1 2 3 4 5 6 7 8 9 10 - : unit = () # for i=10 downto 1 do print int i; print string " " done; print newline () ; ; 10 9 8 7 6 5 4 3 2 1 - : unit = ()

範囲非限定の while ループの文法は次の通りです。 構文 :

while expr1 do expr2 done

expr1 式は bool 型でなければなりません。for ループ同様、expr2 が unit 型でないと、 警告がでます。 # let r = ref 1 in while !r < 11 do print int !r ;

80

Chapter 3 : 手続き型プログラミング

print string " " ; r := !r+1 done ; ; 1 2 3 4 5 6 7 8 9 10 - : unit = ()

ループが、前に出てきたものと同様に、unit 型の値 () を計算する式である、というこ とを理解しておくのは大事なことです。 # let f () = print string "-- end\n" ; ; val f : unit -> unit = # f (for i=1 to 10 do print int i; print string " " done) ; ; 1 2 3 4 5 6 7 8 9 10 -- end - : unit = ()

整数 1 から 10 までが表示されたあとに、文字列"-- end\n"が出てくる、ということに 注意してください。これから、引数(今の場合は for ループ)は関数に渡される前に評 価されるということが分かるでしょう。 手続き型プログラミングでは、ループの本体(expr2 )は何か値を計算するわけではなく、 副作用によって前進していきます。Objective Caml では、連接と同様に、ループの本体 が unit 型でないと、警告がでます。 # let s = [5; 4; 3; 2; 1; 0] ; ; val s : int list = [5; 4; 3; 2; 1; 0] # for i=0 to 5 do List.tl s done ; ; Characters 17-26: Warning: this expression should have type unit. for i=0 to 5 do List.tl s done ;; ^^^^^^^^^ - : unit = ()

例:スタックの実装 データ構造’a stack をレコードとして実装することにします。このレコードには、要素 を保持する配列と、この配列中の一番初めの空きの位置が入っています。対応する型は このようになります。 # type ’a stack = { mutable ind:int; size:int; mutable elts : ’a array } ; ; size フィールドには、スタックのサイズの上限が入っています。 スタックに対する操作には、スタックの初期化を行う init stack、スタックへ要素を プッシュする push、スタックのトップを返し、それをポップする pop があります。 # let init stack n = {ind=0; size=n; elts =[||]} ; ; val init_stack : int -> ’a stack =

この関数は、空でない配列を作るように書くことはできません。そうしたければ、その配 列の初期値をこの関数が取るようにしなければならないでしょう。だから、elts フィー ルドは空配列で初期化するようにしています。 例外を2つ宣言しておきます。ひとつは空スタックからのポップをしようとするのを防 ぐため、もうひとつは満杯のスタックへプッシュしようとするのを防ぐためです。これ

制御構造

81

らは pop および push 関数で使われます。 # exception Stack empty ; ; # exception Stack full ; ; # let pop p = if p.ind = 0 then raise Stack empty else (p.ind <- p.ind - 1; p.elts.(p.ind)) ; ; val pop : ’a stack -> ’a = # let push e p = if p.elts = [||] then (p.elts <- Array.create p.size e; p.ind <- 1) else if p.ind >= p.size then raise Stack full else (p.elts.(p.ind) <- e; p.ind <- p.ind + 1) ; ; val push : ’a -> ’a stack -> unit =

このデータ構造の簡単な使用例です。 # let p = init stack 4 ; ; val p : ’_a stack = {ind = 0; size = 4; elts = [||]} # push 1 p ; ; - : unit = () # for i = 2 to 5 do push i p done ; ; Exception: Stack_full. # p ;; - : int stack = {ind = 4; size = 4; elts = [|1; 2; 3; 4|]} # pop p ; ; - : int = 4 # pop p ; ; - : int = 3

もし、スタックへ要素をプッシュしようとしたときに Stack full 例外が発生するのを やめさせたければ、配列の拡張をするという手があります。このとき、size フィールド も変えなればなりません。 # type ’a stack = {mutable ind:int ; mutable size:int ; mutable elts : ’a array} ; ; # let init stack n = {ind=0; size=max n 1; elts = [||]} ; ; # let n push e p = if p.elts = [||] then begin p.elts <- Array.create p.size e; p.ind <- 1 end else if p.ind >= p.size then begin let nt = 2 * p.size in

82

Chapter 3 : 手続き型プログラミング

let nv = Array.create nt e in for j=0 to p.size-1 do nv.(j) <- p.elts.(j) done ; p.elts <- nv; p.size <- nt; p.ind <- p.ind + 1 end else begin p.elts.(p.ind) <- e ; p.ind <- p.ind + 1 end ; ; val n_push : ’a -> ’a stack -> unit =

上限なしに拡張できるようになりましたが、それでもなお、こういうデータ構造の扱い には注意が必要ということには変わりありません。この例では、最初のスタックが必要 に応じて伸びていきます。 # let p = init stack 4 ; ; val p : ’_a stack = {ind = 0; size = 4; elts = [||]} # for i = 1 to 5 do n push i p done ; ; - : unit = () # p ;; - : int stack = {ind = 5; size = 8; elts = [|1; 2; 3; 4; 5; 5; 5; 5|]} # p.stack ; ; Characters 0-7: p.stack ;; ^^^^^^^ Unbound record field label stack

pop の方も、不要になったメモリを回収するためにスタックを縮めるようにすれば有用 かも知れません。

例:行列計算 この例では、浮動小数の2次元配列として行列の型を定義し、そういう行列に対する演 算をいくつか書くことにします。単相型 mat は、行列の次元と要素を保持するレコード です。関数 create mat、access mat、mod mat は、それぞれ、行列の作成、要素の参 照、要素の変更を行う関数です。 # type mat = { n:int; m:int; t: float array array }; ; type mat = { n : int; m : int; t : float array array; } # let create mat n m = { n=n; m=m; t = Array.create matrix n m 0.0 } ; ; val create_mat : int -> int -> mat = # let access mat m i j = m.t.(i).(j) ; ; val access_mat : mat -> int -> int -> float = # let mod mat m i j e = m.t.(i).(j) <- e ; ; val mod_mat : mat -> int -> int -> float -> unit =

制御構造 # let a = create mat 3 3 ; ; val a : mat = {n = 3; m = 3; t = [|[|0; 0; 0|]; [|0; 0; 0|]; [|0; 0; 0|]|]} # mod mat a 1 1 2.0; mod mat a 1 2 1.0; mod mat a 2 1 1.0 ; ; - : unit = () # a ;; - : mat = {n = 3; m = 3; t = [|[|0; 0; 0|]; [|0; 2; 1|]; [|0; 1; 0|]|]}

行列 a と b の和を取ると、cij = aij + bij を満たす行列 c となります。 # let add mat p q = if p.n = q.n && p.m = q.m then let r = create mat p.n p.m in for i = 0 to p.n-1 do for j = 0 to p.m-1 do mod mat r i j (p.t.(i).(j) +. q.t.(i).(j)) done done ; r else failwith "add_mat : dimensions incompatible"; ; val add_mat : mat -> mat -> mat = # add mat a a ; ; - : mat = {n = 3; m = 3; t = [|[|0; 0; 0|]; [|0; 4; 2|]; [|0; 2; 0|]|]}

Pk=m 行列 a と b の積を取ると、cij = k=1 a aik .bkj を満たす行列 c となります。 # let mul mat p q = if p.m = q.n then let r = create mat p.n q.m in for i = 0 to p.n-1 do for j = 0 to q.m-1 do let c = ref 0.0 in for k = 0 to p.m-1 do c := !c +. (p.t.(i).(k) *. q.t.(k).(j)) done; mod mat r i j !c done done; r else failwith "mul_mat : dimensions incompatible" ; ; val mul_mat : mat -> mat -> mat = # mul mat a a; ; - : mat = {n = 3; m = 3; t = [|[|0; 0; 0|]; [|0; 5; 2|]; [|0; 2; 1|]|]}

83

84

Chapter 3 : 手続き型プログラミング

引数の評価順序 純粋な関数型言語では、引数の評価順序が問題になることはありません。メモリ状態が 更新されたり計算が中断されたりするすることがないので、ある引数の計算が別の引数 にも影響したりすることがないわけです。一方、Objective Caml では、物理的に変更可 能な値とか、例外とかがありますので、評価順序を考慮にいれないと危険です。例えば、 次の結果は、Intel マシン上の Linux 用の Objective Caml バージョン 2.04 に特有のもの です。 # let new print string s = print string s; String.length s ; ; val new_print_string : string -> int = # (+) (new print string "Hello ") (new print string "World!") ; ; World!Hello - : int = 12

これは2つの文字列を印字する例ですが、2番目の文字列が1番目のよりも先に出力さ れています。 例外についても同じことが起ります。 # try (failwith "function") (failwith "argument") with Failure s → s; ; - : string = "argument"

もし引数の評価順序を指定したければ、関数呼び出しの前に局所宣言を使って強制的に 順序付けをしなければなりません。上の例だとこういう風に書き換えることができます。 # let e1 = (new print string "Hello ") in let e2 = (new print string "World!") in (+) e1 e2 ; ; Hello World!- : int = 12

Objective Caml では引数の評価順序は規定されていません。たまたま、現在の Objective Caml の実装では左から右へ引数を評価しています。それでもやはり、この実装に依存し たプログラミングをしたりすると、将来のバージョンでこの実装方法が変更されたりし たときに、実は危険であったなんていうことがあるかも知れません。 あとで戻ってお話ししますが、言語デザインに関して果てしなく続いている論争があり ます。ある言語機能について、それは意図的に規定なしとしておくべきか?その場合、 プログラマにはその機能は使わないでくれと頼むことになる。ただし、そのプログラマ は、コンパイラの実装によってプログラムの結果が違ったりするということに苦しむこ とになる。それとも、すべては規定しておくべきか?その場合、プログラマは言語機能 すべてを使ってもよいことになる。ただし、コンパイラの実装は複雑なるかも知れない し、使えない最適化もでてくるかも知れない。

メモリつき電卓 今から、前章ででてきた電卓の例をもう一度やります。ただし今回は、ユーザインター フェースを付け加え、より電卓的な使い方ができるようにします。新しい電卓プログラ

メモリつき電卓

85

ムは、ひとつのループからなっていて、ユーザが、演算を入力し、その結果を画面上に 見るということを繰り返します。また、毎回キー入力の度にいちいち遷移関数を適用し なくてもよくなります。 4つのキーを新たに追加します。画面をゼロにクリアする C、結果を記憶する M、記憶 したものをよみがえらせる m、電卓をスイッチオフする OFF です。これらに対応して 次の型を定義します。 # type key = Plus | Minus | Times | Div | Equals | Digit of int | Store | Recall | Clear | Off ; ;

キーボードに打ち込まれた文字を key 型に変換する関数を定義する必要があります。打 ち込まれた文字が電卓のどのキーにも相当しない場合には、Invalid key 例外で対処し ます。文字をアスキーコードに変換するには、Char モジュールの code 関数を使います。 # exception Invalid key ; ; exception Invalid_key # let translation c = match c with ’+’ → Plus | ’-’ → Minus | ’*’ → Times | ’/’ → Div | ’=’ → Equals | ’C’ | ’c’ → Clear | ’M’ → Store | ’m’ → Recall | ’o’ | ’O’ → Off | ’0’..’9’ as c → Digit ((Char.code c) - (Char.code ’0’)) | _ → raise Invalid key ; ; val translation : char -> key =

手続き型のスタイルでは、遷移関数は、新しい状態を計算するのではなく、電卓の状態 を更新します。そのため、state 型を再定義して、フィールドを変更可能にしてやる必 要があります。最後に、OFF キーが押されたときに対応するため、Key off 例外を定義 しておきます。 # type state = { mutable lcd : int; (* last computation done *) mutable lka : bool; (* last key activated *) mutable loa : key; (* last operator activated *) mutable vpr : int; (* value printed *) mutable mem : int (* memory of calculator *) }; ;

# exception Key off ; ; exception Key_off

86

Chapter 3 : 手続き型プログラミング

# let transition s key Clear → s.vpr | Digit n → s.vpr s.lka | Store → s.lka s.mem | Recall → s.lka s.vpr | Off → raise Key | _ → let lcd =

= match key with <- 0 <- ( if s.lka then s.vpr*10+n else n ); <- true <- false ; <- s.vpr <- false ; <- s.mem off match s.loa with Plus → s.lcd + s.vpr | Minus → s.lcd - s.vpr | Times → s.lcd * s.vpr | Div → s.lcd / s.vpr | Equals → s.vpr | _ → failwith "transition: impossible match"

in s.lcd <- lcd ; s.lka <- false ; s.loa <- key ; s.vpr <- s.lcd; ; val transition : state -> key -> unit =

電卓を開始させる go 関数を定義します。帰り値は () ですが、これはプログラムの実行 によって引き起こされた環境への作用(開始/終了/状態の更新)だけが今の関心事だ からです。引数も定数 () ですが、これはこの電卓が自立して(つまり自分で初期状態を 設定する)、外界と呼応しながら(つまりキーボード入力を引数としながら計算が進む) 動作するからです。状態遷移は無限ループ(while true do)の中で行われます。抜け出 すために、Key off 例外を使います。 # let go () = let state = { lcd=0; lka=false; loa=Equals; vpr=0; mem=0 } in try while true do try let input = translation (input char stdin) in transition state input ; print newline () ; print string "result: " ; print int state.vpr ; print newline () with Invalid key → () (* no effect *) done with Key off → () ; ;

練習問題

87

val go : unit -> unit =

ここで、初期状態は、パラメータとして渡すか、go 関数の中で局所的に宣言しなければな らない、ということに注意しましょう。これは、この関数を適用する度に状態は初期化し てやる必要があるからです。もし前章の関数型プログラムでやったように initial state を使うようにすると、電卓はいったん停止したあと、再開したときに、前と同じ状態か ら始まることになります。同じプログラムで電卓を2つ使うなどということも難しくな るでしょう。

練習問題 二重リンクリスト 関数型プログラミングは、リストのようなサイクルのないデータ構造を扱いやすいよう にできています。一方、サイクルのある構造を実装するのは非常に大変です。ここでは、 二重リンクリストを定義しましょう。これは、各要素が、前と後の要素を保持している ようなリストのことです。

1.

変更可能フィールドを少なくとも1つもっているレコードを使って、二重リンクリ ストを表すパラメータつき型を定義してください。

2.

二重リンクリストへ要素を追加する add 関数、要素を削除する remove 関数を書い てください。

連立一次方程式を解く 行列の代数に関する練習問題です。与えられた連立方程式をガウスの除去法(もしくは ピボット法)によって解きます。解くべき連立方程式は A X = Y と書くことにします。 ただし、A は n 次元正方行列、Y は n 次元定数ベクトル、X は同次元の未知のベクト ルです。 この手法では、まず方程式 A X = Y をある等価な方程式 C X = Z に変形します。ただ し、C は上方三角行列です。それから、C を対角化して解を求めます。

1.

vect 型、mat 型、syst 型を定義してください。

2.

ベクトルを操作する補助関数を書いてください。具体的には、方程式の画面表示、 2つのベクトルの加算、ベクトルのスカラーによる乗算です。

3.

行列を操作する補助関数を書いてください。具体的には、2つの行列の乗算、行列 とベクトルの乗算です。

4.

方程式を操作する補助関数を書いてください。具体的には、方程式のある行を、あ るピボット(Aij ) で割って、2つの行を入れ替えるという関数です。

5.

方程式を対角化する関数を書いてください。これから、連立一次方程式の解を求め る関数を書いてください。

88 6.

Chapter 3 : 手続き型プログラミング 次の方程式について、あなたの書いた関数をテストしてください。       10 7 8 7 x1 32        7 5 6 5   x2   23  AX =  ∗ = =Y  8 6 10 9   x3   33  7 5 9 10 x4 31

   AX =      AX =   7.

10 7 8 7

7 5 6 5

8 6 10 9

7 5 9 10

      ∗  

10 7 8.1 7.2 7.08 5.04 6 5 8 5.98 9.89 9 6.99 4.99 9 9.98

x1 x2 x3 x4





    =  

      ∗  

x1 x2 x3 x4



32.1 22.9 33.1 30.9 

    =  

   =Y 

32 23 33 31

   =Y 

あなたの得た結果についてどう思いますか?

まとめ この章では、手続き型プログラミングで主流の機能(変更可能な値、入出力、繰り返し 制御構造)を関数型言語へと統合するということについてお話ししました。文字列、配 列、レコードなどの変更可能な値だけが、物理的に書き換えをすることができます。そ のほかの値は、いったん生成されると、書き換えることはできません。このようにして、 関数的な部分として読み込み専用の値が、手続き的な部分として読み書き可能な値があ ることになるのです。 ひとつ留意しておくべきことは、もしこの言語の手続き的な機能を使わない場合、この 関数的コアへの機能拡張は、関数的な部分に影響がありません。ただし、手続き的な機 能を導入する上で回避しなければならなかった型付け上の問題がありましたが、それは 除いての話です。

もっと知りたい方へ 手続き型プログラミングは、Fortran、C、Pascal などの初期の計算機言語の頃から最も 広く使われ続けてきたプログラミングスタイルです。そういう理由で、数々のアルゴリ ズムはこのスタイルで記述されてきました。多くは、なんらかの疑似 Pascal が使われて きました。これらは関数的スタイルでも実装できますが、配列を使うので手続き的スタ イルを使った方がやりやすいのです。古典的なアルゴリズムの教科書(例えば [AHU83] や [Sed88])に出ているデータ構造とアルゴリズムは、この適切なスタイルでそのまま もってくることができます。また、この2つのスタイルを一つの言語の中にいっしょに すると、2つを合わせた新しいプログラミングモデルを定義することができるという長 所が生まれます。これがまさにつぎの章でお話しすることです。

4 Functional and Imperative Styles Functional and imperative programming languages are primarily distinguished by the control over program execution and the data memory management. •

A functional program computes an expression. This computation results in a value. The order in which the operations needed for this computation occur does not matter, nor does the physical representation of the data manipulated, because the result is the same anyway. In this setting, deallocation of memory is managed implicitly by the language itself: it relies on an automatic garbage collector or GC; see chapter 9.



An imperative program is a sequence of instructions modifying a memory state. Each execution step is enforced by rigid control structures that indicate the next instruction to be executed. Imperative programs manipulate pointers or references to values more often than the values themselves. Hence, the memory space needed to store values must be allocated and reclaimed explicitly, which sometimes leads to errors in accessing memory. Nevertheless, nothing prevents use of a GC.

Imperative languages provide greater control over execution and the memory representation of data. Being closer to the actual machine, the code can be more efficient, but loses in execution safety. Functional programming, offering a higher level of abstraction, achieves a better level of execution safety: Typing (dynamic or static) may be stricter in this case, thus avoiding operations on incoherent values. Automatic storage reclamation, in exchange for giving up efficiency, ensures the current existence of the values being manipulated. Historically, the two programming paradigms have been seen as belonging to different universes: symbolic applications being suitable for the former, and numerical applications being suitable for the latter. But certain things have changed, especially techniques for compiling functional programming languages, and the efficiency of GCs. From another side, execution safety has become an important, sometimes the predominant criterion in the quality of an application. Also familiar is the “selling point” of

90

Chapter 4 : Functional and Imperative Styles

the Java language, according to which efficiency need not preempt assurance, especially if efficiency remains reasonably good. And this idea is spreading among software producers. Objective Caml belongs to this class. It combines the two programming paradigms, thus enlarging its domain of application by allowing algorithms to be written in either style. It retains, nevertheless, a good degree of execution safety because of its static typing, its GC, and its exception mechanism. Exceptions are a first explicit execution control structure; they make it possible to break out of a computation or restart it. This trait is at the boundary of the two models, because although it does not replace the result of a computation, it can modify the order of execution. Introducing physically mutable data can alter the behavior of the purely functional part of the language. For instance, the order in which the arguments to a function are evaluated can be determined, if that evaluation causes side effects. For this reason, such languages are called “impure functional languages.” One loses in level of abstraction, because the programmer must take account of the memory model, as well as the order of events in running the program. This is not always negative, especially for the efficiency of the code. On the other hand, the imperative aspects change the type system of the language: some functional programs, correctly typed in theory, are no longer in fact correctly typed because of the introduction of references. However, such programs can easily be rewritten.

Plan of the Chapter This chapter provides a comparison between the functional and imperative models in the Objective Caml language, at the level both of control structure and of the memory representation of values. The mixture of these two styles allows new data structures to be created. The first section studies this comparison by example. The second section discusses the ingredients in the choice between composition of functions and sequencing of instructions, and in the choice between sharing and copying values. The third section brings out the interest of mixing these two styles to create mutable functional data, thus permitting data to be constructed without being completely evaluated. The fourth section describes streams, potentially infinite sequences of data, and their integration into the language via pattern-matching.

Comparison between Functional and Imperative Character strings (of Objective Caml type string) and linked lists (of Objective Caml type ’a list) will serve as examples to illustrate the differences between “functional” and “imperative.”

Comparison between Functional and Imperative

91

The Functional Side The function map (see page 26) is a classic ingredient in functional languages. In a purely functional style, it is written: # let rec map f l = match l with [] → [] | h :: q → (f h) :: (map f q) ; ; val map : (’a -> ’b) -> ’a list -> ’b list =

It recursively constructs a list by applying f to the elements of the list given as argument, independently specifying its head (f h) and its tail (map f q). In particular, the program does not stipulate which of the two will be computed first. Moreover, the physical representation of lists need not be known to the programmer to write such a function. In particular, problems of allocating and sharing data are managed implicitly by the system and not by the programmer. An example illustrating this follows: # let example = [ "one" ; "two" ; "three" ] ; ; val example : string list = ["one"; "two"; "three"] # let result = map (function x → x) example ; ; val result : string list = ["one"; "two"; "three"]

The lists example and result contain equal values: # example = result ; ; - : bool = true

These two values have exactly the same structure even though their representation in memory is different, as one learns by using the test for physical equality: # example == result ; ; - : bool = false # (List.tl example) == (List.tl result) ; ; - : bool = false

The Imperative Side Let us continue the previous example, and modify a string in the list result. # (List.hd result).[1] <- ’s’ ; ; - : unit = () # result ; ; - : string list = ["ose"; "two"; "three"] # example ; ; - : string list = ["ose"; "two"; "three"]

Evidently, this operation has modified the list example. Hence, it is necessary to know the physical structure of the two lists being manipulated, as soon as we use imperative aspects of the language.

92

Chapter 4 : Functional and Imperative Styles

Let us now observe how the order of evaluating the arguments of a function can amount to a trap in an imperative program. We define a mutable list structure with primitive functions for creation, modification, and access: # type ’a ilist = { mutable c : ’a list } ; ; type ’a ilist = { mutable c : ’a list; } # let icreate () = { c = [] } let iempty l = (l.c = [] ) let icons x y = y.c <- x :: y.c ; y let ihd x = List.hd x.c let itl x = x.c <- List.tl x.c ; x ; ; val icreate : unit -> ’a ilist = val iempty : ’a ilist -> bool = val icons : ’a -> ’a ilist -> ’a ilist = val ihd : ’a ilist -> ’a = val itl : ’a ilist -> ’a ilist = # let rec imap f l = if iempty l then icreate () else icons (f (ihd l)) (imap f (itl l)) ; ; val imap : (’a -> ’b) -> ’a ilist -> ’b ilist =

Despite having reproduced the general form of the map of the previous paragraph, with imap we get a distinctly different result: # let example = icons "one" (icons "two" (icons "three" (icreate () ))) ; ; val example : string ilist = {c = ["one"; "two"; "three"]} # imap (function x → x) example ; ; Exception: Failure "hd".

What has happened? Just that the evaluation of (itl l) has taken place before the evaluation of (ihd l), so that on the last iteration of imap, the list referenced by l became the empty list before we examined its head. The list example is henceforth definitely empty even though we have not obtained any result: # example ; ; - : string ilist = {c = []}

The flaw in the function imap arises from a mixing of the genres that has not been controlled carefully enough. The choice of order of evaluation has been left to the system. We can reformulate the function imap, making explicit the order of evaluation, by using the syntactic construction let .. in .. # let rec imap f l = if iempty l then icreate () else let h = ihd l in icons (f h) (imap f (itl l)) ; ; val imap : (’a -> ’b) -> ’a ilist -> ’b ilist = # let example = icons "one" (icons "two" (icons "three" (icreate () ))) ; ; val example : string ilist = {c = ["one"; "two"; "three"]} # imap (function x → x) example ; ;

Comparison between Functional and Imperative

93

- : string ilist = {c = ["one"; "two"; "three"]}

However, the original list has still been lost: # example ; ; - : string ilist = {c = []}

Another way to make the order of evaluation explicit is to use the sequencing operator and a looping structure. # let imap f l = let l res = icreate () in while not (iempty l) do ignore (icons (f (ihd l)) l res) ; ignore (itl l) done ; { l res with c = List.rev l res.c } ; ; val imap : (’a -> ’b) -> ’a ilist -> ’b ilist = # let example = icons "one" (icons "two" (icons "three" (icreate () ))) ; ; val example : string ilist = {c = ["one"; "two"; "three"]} # imap (function x → x) example ; ; - : string ilist = {c = ["one"; "two"; "three"]}

The presence of ignore emphasizes the fact that it is not the result of the functions that counts here, but their side effects on their argument. In addition, we had to put the elements of the result back in the right order (using the function List.rev).

Recursive or Iterative People often mistakenly associate recursive with functional and iterative with imperative. A purely functional program cannot be iterative because the value of the condition of a loop never varies. By contrast, an imperative program may be recursive: the original version of the function imap is an example. Calling a function conserves the values of its arguments during its computation. If it calls another function, the latter conserves its own arguments in addition. These values are conserved on the execution stack. When the call returns, these values are popped from the stack. The memory space available for the stack being bounded, it is possible to encounter the limit when using a recursive function with calls too deeply nested. In this case, Objective Caml raises the exception Stack overflow. # let rec succ n = if n = 0 then 1 else 1 + succ (n-1) ; ; val succ : int -> int = # succ 100000 ; ; Stack overflow during evaluation (looping recursion?).

94

Chapter 4 : Functional and Imperative Styles

In the iterative version succ iter, the stack space needed for a call does not depend on its argument. # let succ iter n = let i = ref 0 in for j=0 to n do incr i done ; !i ; ; val succ_iter : int -> int = # succ iter 100000 ; ; - : int = 100001

The following recursive version has a priori the same depth of calls, yet it executes successfully with the same argument. # let succ tr n = let rec succ aux n accu = if n = 0 then accu else succ aux (n-1) (accu+1) in succ aux 1 n ; ; val succ_tr : int -> int = # succ tr 100000 ; ; - : int = 100001

This function has a special form of recursive call, called tail recursion, in which the result of this call will be the result of the function without further computation. It is therefore unnecessary to have stored the values of the arguments to the function while computing the recursive call. When Objective Caml can observe that a call is tail recursive, it frees the arguments on the stack before making the recursive call. This optimization allows recursive functions that do not increase the size of the stack. Many languages detect tail recursive calls, but it is indispensable in a functional language, where naturally many tail recursive calls are used.

Which Style to Choose? This is no matter of religion or esthetics; a priori neither style is prettier or holier than the other. On the contrary, one style may be more adequate than the other depending on the problem to be solved. The first rule to apply is the rule of simplicity. Whether the algorithm to use implemented is written in a book, or whether its seed is in the mind of the programmer, the algorithm is itself described in a certain style. It is natural to use the same style when implementing it. The second criterion of choice is the efficiency of the program. One may say that an imperative program (if well written) is more efficient that its functional analogue, but in very many cases the difference is not enough to justify complicating the code to

Which Style to Choose?

95

adopt an imperative style where the functional style would be natural. The function map in the previous section is a good example of a problem naturally expressed in the functional style, which gains nothing from being written in the imperative style.

Sequence or Composition of Functions We have seen that as soon as a program causes side effects, it is necessary to determine precisely the order of evaluation for the elements of the program. This can be done in both styles: functional: using the fact that Objective Caml is a strict language, which means that the argument is evaluated before applying the function. The expression (f (g x)) is computed by first evaluating (g x), and then passing the result as argument to f. With more complex expressions, we can name an intermediate result with the let in construction, but the idea remains the same: let aux=(g x) in (f aux). imperative: using sequences or other control structures (loops). In this case, the result is not the value returned by a function, but its side effects on memory: aux:=(g x) ; (f !aux). Let us examine this choice of style on an example. The quick sort algorithm, applied to a vector, is described recursively as follows: 1.

Choose a pivot: This is the index of an element of the vector;

2.

Permute around the pivot: Permute the elements of the vector so elements less than the value at the pivot have indices less than the pivot, and vice versa;

3.

sort the subvectors obtained on each side of the pivot, using the same algorithm: The subvector preceding the pivot and the subvector following the pivot.

The choice of algorithm, namely to modify a vector so that its elements are sorted, incites us to use an imperative style at least to manipulate the data. First, we define a function to permute two elements of a vector: # let permute element vec n p = let aux = vec.(n) in vec.(n) <- vec.(p) ; vec.(p) <- aux val permute_element : ’a array -> int -> int -> unit =

;;

The choice of a good pivot determines the efficiency of the algorithm, but we will use the simplest possible choice here: return the index of the first element of the (sub)vector. # let choose pivot vec start finish = start ; ; val choose_pivot : ’a -> ’b -> ’c -> ’b =

Let us write the algorithm that we would like to use to permute the elements of the vector around the pivot.

96

Chapter 4 : Functional and Imperative Styles

1.

Place the pivot at the beginning of the vector to be permuted;

2.

Initialize i to the index of the second element of the vector;

3.

Initialize j to the index of the last element of the vector;

4.

If the element at index j is greater than the pivot, permute it with the element at index i and increment i; otherwise, decrement j;

5.

While i < j, repeat the previous operation;

6.

At this stage, every element with index < i (or equivalently, j) is less than the pivot, and all others are greater; if the element with index i is less than the pivot, permute it with the pivot; otherwise, permute its predecessor with the pivot.

In implementing this algorithm, it is natural to adopt imperative control structures. # let permute pivot vec start finish ind pivot = permute element vec start ind pivot ; let i = ref (start+1) and j = ref finish and pivot = vec.(start) in while !i < !j do if vec.(!j) >= pivot then decr j else begin permute element vec !i !j ; incr i end done ; if vec.(!i) > pivot then decr i ; permute element vec start !i ; !i ;; val permute_pivot : ’a array -> int -> int -> int -> int =

In addition to its effects on the vector, this function returns the index of the pivot as its result. All that remains is to put together the different stages and add the recursion on the sub-vectors. # let rec quick vec start finish = if start < finish then let pivot = choose pivot vec start finish in let place pivot = permute pivot vec start finish pivot in quick (quick vec start (place pivot-1)) (place pivot+1) finish else vec ; ; val quick : ’a array -> int -> int -> ’a array =

We have used the two styles here. The chosen pivot serves as argument to the permutation around this pivot, and the index of the pivot after the permutation is an argument to the recursive call. By contrast, the vector obtained after the permutation is not returned by the permute pivot function; instead, this result is produced by side

Which Style to Choose?

97

effect. However, the quick function returns a vector, and the sorting of sub-vectors is obtained by composition of recursive calls. The main function is: # let quicksort vec = quick vec 0 ((Array.length vec)-1) ; ; val quicksort : ’a array -> ’a array =

It is a polymorphic function because the order relation < on vector elements is itself polymorphic. # let t1 = [|4;8;1;12;7;3;1;9|] ; ; val t1 : int array = [|4; 8; 1; 12; 7; 3; 1; 9|] # quicksort t1 ; ; - : int array = [|1; 1; 3; 4; 7; 8; 9; 12|] # t1 ; ; - : int array = [|1; 1; 3; 4; 7; 8; 9; 12|] # let t2 = [|"the"; "little"; "cat"; "is"; "dead"|] ; ; val t2 : string array = [|"the"; "little"; "cat"; "is"; "dead"|] # quicksort t2 ; ; - : string array = [|"cat"; "dead"; "is"; "little"; "the"|] # t2 ; ; - : string array = [|"cat"; "dead"; "is"; "little"; "the"|]

Shared or Copy Values When the values that we manipulate are not mutable, it does not matter whether they are shared or not. # let id x = x ; ; val id : ’a -> ’a = # let a = [ 1; 2; 3 ] ; ; val a : int list = [1; 2; 3] # let b = id a ; ; val b : int list = [1; 2; 3]

Whether b is a copy of the list a or the very same list makes no difference, because these are intangible values anyway. But if we put modifiable values in place of integers, we need to know whether modifying one value causes a change in the other. The implementation of polymorphism in Objective Caml causes immediate values to be copied, and structured values to be shared. Even though arguments are always passed by value, only the pointer to a structured value is copied. This is the case even in the function id: # let a = [| 1 ; 2 ; 3 |] ; ; val a : int array = [|1; 2; 3|] # let b = id a ; ; val b : int array = [|1; 2; 3|] # a.(1) <- 4 ; ; - : unit = () # a ;;

98

Chapter 4 : Functional and Imperative Styles

- : int array = [|1; 4; 3|] # b ;; - : int array = [|1; 4; 3|]

We have here a genuine programming choice to decide which is the most efficient way to represent a data structure. On one hand, using mutable values allows manipulations in place, which means without allocation, but requires us to make copies sometimes when immutable data would have allowed sharing. We illustrate this here with two ways to implement lists. # type ’a list immutable = LInil | LIcons of ’a * ’a list immutable ; ; # type ’a list mutable = LMnil | LMcons of ’a * ’a list mutable ref ; ;

The immutable lists are strictly equivalent to lists built into Objective Caml, while the mutable lists are closer to the style of C, in which a cell is a value together with a reference to the following cell. With immutable lists, there is only one way to write concatenation, and it requires duplicating the structure of the first list; by contrast, the second list may be shared with the result. # let rec concat l1 l2 = match l1 with LInil → l2 | LIcons (a,l11) → LIcons(a, (concat l11 l2)) ; ; val concat : ’a list_immutable -> ’a list_immutable -> ’a list_immutable = # let li1 = LIcons(1, LIcons(2, LInil)) and li2 = LIcons(3, LIcons(4, LInil)) ; ; val li1 : int list_immutable = LIcons (1, LIcons (2, LInil)) val li2 : int list_immutable = LIcons (3, LIcons (4, LInil)) # let li3 = concat li1 li2 ; ; val li3 : int list_immutable = LIcons (1, LIcons (2, LIcons (3, LIcons (4, LInil)))) # li1==li3 ; ; - : bool = false # let tlLI l = match l with LInil → failwith "Liste vide" | LIcons(_,x) → x ; ; val tlLI : ’a list_immutable -> ’a list_immutable = # tlLI(tlLI(li3)) == li2 ; ; - : bool = true

From these examples, we see that the first cells of li1 and li3 are distinct, while the second half of li3 is exactly li2. With mutable lists, we have a choice between modifying arguments (function concat share) and creating a new value (function concat copy). # let rec concat copy l1 l2 = match l1 with LMnil → l2 | LMcons (x,l11) → LMcons(x, ref (concat copy !l11 l2)) ; ;

Which Style to Choose?

99

val concat_copy : ’a list_mutable -> ’a list_mutable -> ’a list_mutable =

This first solution, concat copy, gives a result similar to the previous function, concat. A second solution shares its arguments with its result fully: # let concat share l1 l2 = match l1 with LMnil → l2 | _ → let rec set last = function LMnil → failwith "concat_share : impossible case!!" | LMcons(_,l) → if !l=LMnil then l:=l2 else set last !l in set last l1 ; l1 ; ; val concat_share : ’a list_mutable -> ’a list_mutable -> ’a list_mutable =

Concatenation with sharing does not require any allocation, and therefore does not use the constructor LMcons. Instead, it suffices to cause the last cell of the first list to point to the second list. However, this version of concatenation has the potential weakness that it alters arguments passed to it. # let lm1 = LMcons(1, ref (LMcons(2, ref LMnil))) and lm2 = LMcons(3, ref (LMcons(4, ref LMnil))) ; ; val lm1 : int list_mutable = LMcons (1, {contents = LMcons (2, {contents = LMnil})}) val lm2 : int list_mutable = LMcons (3, {contents = LMcons (4, {contents = LMnil})}) # let lm3 = concat share lm1 lm2 ; ; val lm3 : int list_mutable = LMcons (1, {contents = LMcons (2, {contents = LMcons (...)})})

We do indeed obtain the expected result for lm3. However, the value bound to lm1 has been modified. # lm1 ; ; - : int list_mutable = LMcons (1, {contents = LMcons (2, {contents = LMcons (...)})})

This may therefore have consequences on the rest of the program.

How to Choose your Style In a purely functional program, side effects are forbidden, and this excludes mutable data structures, exceptions, and input/output. We prefer, though, a less restrictive definition of the functional style, saying that functions that do not modify their global environment may be used in a functional style. Such a function may manipulate mutable values locally, and may therefore be written in an imperative style, but must not modify global variables, nor its arguments. We permit them to raise exceptions in addition. Viewed from outside, these functions may be considered “black boxes.” Their behavior matches a function written in a purely functional style, apart from being able of breaking control flow by raising an exception. In the same spirit, a mutable value which can no longer be modified after initialization may be used in a functional style.

100

Chapter 4 : Functional and Imperative Styles

On the other hand, a program written in an imperative style still benefits from the advantages provided by Objective Caml: static type safety, automatic memory management, the exception mechanism, parametric polymorphism, and type inference. The choice between the imperative and functional styles depends on the application to be developed. We may nevertheless suggest some guidelines based on the character of the application, and the criteria considered important in the development process. •

choice of data structures: The choice whether to use mutable data structures follows from the style of programming adopted. Indeed, the functional style is essentially incompatible with modifying mutable values. By contrast, constructing and traversing objects are the same whatever their status. This touches the same issue as “modification in place vs copying” on page 97; we return to it again in discussing criteria of efficiency.



required data structures: If a program must modify mutable data structures, then the imperative style is the only one possible. If, on the other hand, you just have to traverse values, then adopting the functional style guarantees the integrity of the data. Using recursive data structures requires the use of functions that are themselves recursive. Recursive functions may be defined using either of the two styles, but it is often easier to understand the creation of a value following a recursive definition, which corresponds to a functional approach, than to repeat the recursive processing on this element. The functional style allows us to define generic iterators over the structure of data, which factors out the work of development and makes it faster.



criteria of efficiency: Modification in place is far more efficient than creating a value. When code efficiency is the preponderant criterion, it will usually tip the balance in favor of the imperative style. We note however that the need to avoid sharing values may turn out to be a very hard task, and in the end costlier than copying the values to begin with. Being purely functional has a cost. Partial application and using functions passed as arguments from other functions has an execution cost greater than total application of a function whose declaration is visible. Using this eminently functional feature must thus be avoided in those portions of a program where efficiency is crucial.



development criteria: the higher level of abstraction of functional programs permits them to be written more quickly, leading to code that is more compact and contains fewer errors than the equivalent imperative code, which is generally more verbose. The functional style is better suited to the constraints imposed by developing substantial applications. Since each function is not dependent upon its evaluation context, functional can be easily divided into small units that can be examined separately; as a consequence, the code is easier to read. Programs written using the functional style are more easily reusable because of its better modularity, and because functions may be passed as arguments to other functions.

Mixing Styles

101

These remarks show that it is often a good idea to mix the two programming styles within the same application. The functional programming style is faster to develop and confers a simpler organization to an application. However, portions whose execution time is critical repay being developed in a more efficient imperative style.

Mixing Styles As we have mentioned, a language offering both functional and imperative characteristics allows the programmer to choose the more appropriate style for each part of the implementation of an algorithm. One can indeed use both aspects in the same function. This is what we will now illustrate.

Closures and Side Effects The convention, when a function causes a side effect, is to treat it as a procedure and to return the value (), of type unit. Nevertheless, in some cases, it can be useful to cause the side effect within a function that returns a useful value. We have already used this mixture of the styles in the function permute pivot of quicksort. The next example is a symbol generator that creates a new symbol each time that it is called. It simply uses a counter that is incremented at every call. # let c = ref 0; ; val c : int ref = {contents = 0} # let reset symb = function () → c:=0 ; ; val reset_symb : unit -> unit = # let new symb = function s → c:=!c+1 ; s^(string of int !c) ; ; val new_symb : string -> string = # new symb "VAR" ; ; - : string = "VAR1" # new symb "VAR" ; ; - : string = "VAR2" # reset symb () ; ; - : unit = () # new symb "WAR" ; ; - : string = "WAR1" # new symb "WAR" ; ; - : string = "WAR2"

The reference c may be hidden from the rest of the program by writing: # let (reset s , new s) = let c = ref 0 in let f1 () = c := 0 and f2 s = c := !c+1 ; s^(string of int !c) in (f1,f2) ; ;

102

Chapter 4 : Functional and Imperative Styles

val reset_s : unit -> unit = val new_s : string -> string =

This declaration creates a pair of functions that share the variable c, which is local to this declaration. Using these two functions produces the same behavior as the previous definitions. # new s "VAR"; ; - : string = "VAR1" # new s "VAR"; ; - : string = "VAR2" # reset s () ; ; - : unit = () # new s "WAR"; ; - : string = "WAR1" # new s "WAR"; ; - : string = "WAR2"

This example permits us to illustrate the way that closures are represented. A closure may be considered as a pair containing the code (that is, the function part) as one component and the local envoronment containing the values of the free variables of the function. Figure 4.1 shows the memory representation of the closures reset s and new s. reset_s c

fun () -> c:=0 environment

code

{contents=0} newt_s fun s -> ...

c environment

code

図 4.1: Memory representation of closures.

These two closures share the same environment, containing the value of c. When either one modifies the reference c, it modifies the contents of an area of memory that is shared with the other closure.

Mixing Styles

103

Physical Modifications and Exceptions Exceptions make it possible to escape from situations in which the computation cannot proceed. In this case, an exception handler allows the calculation to continue, knowing that one branch has failed. The problem with side effects comes from the state of the modifiable data when the exception was raised. One cannot be sure of this state if there have been physical modifications in the branch of the calculation that has failed. Let us define the increment function (++) analogous to the operator in C: # let (++) x = x:=!x+1; x; ; val ( ++ ) : int ref -> int ref =

The following example shows a little computation where division by zero occurs together with # let x = ref 2; ; val x : int ref = {contents = 2} (* 1 *) # !((++) x) * (1/0) ; ; Exception: Division_by_zero. # x; ; - : int ref = {contents = 2} (* 2 *) # (1/0) * !((++) x) ; ; Exception: Division_by_zero. # x; ; - : int ref = {contents = 3}

The variable x is not modified during the computation of the expression in (∗1∗), while it is modified in the computation of (∗2∗). Unless one saves the initial values, the form try .. with .. must not have a with .. part that depends on modifiable variables implicated in the expression that raised the exception.

Modifiable Functional Data Structures In functional programming a program (in particular, a function expression) may also serve as a data object that may be manipulated, and one way to see this is to write association lists in the form of function expressions. In fact, one may view association lists of type (’a * ’b) list as partial functions taking a key chosen from the set ’a and returning a value in the set of associated values ’b. Each association list is then a function of type ’a -> ’b. The empty list is the everywhere undefined function, which one simulates by raising an exception: # let nil assoc = function x → raise Not found ; ; val nil_assoc : ’a -> ’b =

We next write the function add assoc which adds an element to a list, meaning that it extends the function for a new entry:

104

Chapter 4 : Functional and Imperative Styles

# let add assoc (k,v) l = function x → if x = k then v else l x ; ; val add_assoc : ’a * ’b -> (’a -> ’b) -> ’a -> ’b = # let l = add assoc (’1’, 1) (add assoc (’2’, 2) nil assoc) ; ; val l : char -> int = # l ’2’ ; ; - : int = 2 # l ’x’ ; ; Exception: Not_found.

We may now re-write the function mem assoc: # let mem assoc k l = try (l k) ; true with Not found → false ; ; val mem_assoc : ’a -> (’a -> ’b) -> bool = # mem assoc ’2’ l ; ; - : bool = true # mem assoc ’x’ l ; ; - : bool = false

By contrast, writing a function to remove an element from a list is not trivial, because one no longer has access to the values captured by the closures. To accomplish the same purpose we mask the former value by raising the exception Not found. # let rem assoc k l = function x → if x=k then raise Not found else l x ; ; val rem_assoc : ’a -> (’a -> ’b) -> ’a -> ’b = # let l = rem assoc ’2’ l ; ; val l : char -> int = # l ’2’ ; ; Exception: Not_found.

Clearly, one may also create references and work by side effect on such values. However, one must take some care. # let add assoc again (k,v) l = l := (function x → if x=k then v else !l x) ; ; val add_assoc_again : ’a * ’b -> (’a -> ’b) ref -> unit =

The resulting value for l is a function that points at itself and therefore loops. This annoying side effect is due to the fact that the dereferencing !l is within the scope of the closure function x →. The value of !l is not evaluated during compilation, but at run-time. At that time, l points to the value that has already been modified by add assoc. We must therefore correct our definition using the closure created by our original definition of add assoc: # let add assoc again (k, v) l = l := add assoc (k, v) !l ; ; val add_assoc_again : ’a * ’b -> (’a -> ’b) ref -> unit = # let l = ref nil assoc ; ; val l : (’_a -> ’_b) ref = {contents = } # add assoc again (’1’,1) l ; ; - : unit = ()

Mixing Styles

105

# add assoc again (’2’,2) l ; ; - : unit = () # !l ’1’ ; ; - : int = 1 # !l ’x’ ; ; Exception: Not_found.

Lazy Modifiable Data Structures Combining imperative characteristics with a functional language produces good tools for implementing computer languages. In this subsection, we will illustrate this idea by implementing data structures with deferred evaluation. A data structure of this kind is not completely evaluated. Its evaluation progresses according to the use made of it. Deferred evaluation, which is often used in purely functional languages, is simulated using function values, possibly modifiable. There are at least two purposes for manipulating incompletely evaluated data structures: first, so as to calculate only what is effectively needed in the computation; and second, to be able to work with potentially infinite data structures. We define the type vm, whose members contain either an already calculated value (constructor Imm) or else a value to be calculated (constructor Deferred): # type ’a v = Imm of ’a | Deferred of (unit → ’a); ; # type ’a vm = {mutable c : ’a v }; ;

A computation is deferred by encapsulating it in a closure. The evaluation function for deferred values must return the value if it has already been calculated, and otherwise, if the value is not already calculated, it must evaluate it and then store the result. # let eval e = match e.c with Imm a → a | Deferred f → let u = f () in e.c <- Imm u ; u ; ; val eval : ’a vm -> ’a =

The operations of deferring evaluation and activating it are also called freezing and thawing a value. We could also write the conditional control structure in the form of a function: # let if deferred c e1 e2 = if eval c then eval e1 else eval e2; ; val if_deferred : bool vm -> ’a vm -> ’a vm -> ’a =

106

Chapter 4 : Functional and Imperative Styles

Here is how to use it in a recursive function such as factorial: # let rec facr n = if deferred {c=Deferred(fun () → n = 0)} {c=Deferred(fun () → 1)} {c=Deferred(fun () → n*(facr(n-1)))}; ; val facr : int -> int = # facr 5; ; - : int = 120

The classic form of if can not be written in the form of a function. In fact, if we define a function if function this way: # let if function c e1 e2 = if c then e1 else e2; ; val if_function : bool -> ’a -> ’a -> ’a =

then the three arguments of if function are evaluated at the time they are passed to the function. So the function fact loops, because the recursive call fact(n-1) is always evaluated, even when n has the value 0. # let rec fact n = if function (n=0) 1 (n*fact(n-1)) ; ; val fact : int -> int = # fact 5 ; ; Stack overflow during evaluation (looping recursion?).

Module Lazy The implementation difficulty for frozen values is due to the conflict between the eager evaluation strategy of Objective Caml and the need to leave expressions unevaluated. Our attempt to redefine the conditional illustrated this. More generally, it is impossible to write a function that freezes a value in producing an object of type vm: # let freeze e = { c = Deferred (fun () → e) }; ; val freeze : ’a -> ’a vm =

When this function is applied to arguments, the Objective Caml evaluation strategy evaluates the expression e passed as argument before constructing the closure fun () → e. The next example shows this: # freeze (print string "trace"; print newline () ; 4*5); ; trace - : int vm = {c = Deferred }

This is why the following syntactic form was introduced. 構文 : 警告

lazy expr This form is a language extension that may evolve in future versions.

Mixing Styles

107

When the keyword lazy is applied to an expression, it constructs a value of a type declared in the module Lazy: # let x = lazy (print string "Hello"; 3*4) ; ; val x : int lazy_t =

The expression (print string "Hello") has not been evaluated, because no message has been printed. The function force of module Lazy allows one to force evaluation: # Lazy.force x ; ; Hello- : int = 12

Now the value x has altered: # x ;; - : int lazy_t = lazy 12

It has become the value of the expression that had been frozen, namely 12. For another call to the function force, it’s enough to return the value already calculated: # Lazy.force x ; ; - : int = 12

The string "Hello" is no longer prefixed.

“Infinite” Data Structures The second reason to defer evaluation is to be able to construct potentially infinite data structures such as the set of natural numbers. Because it might take a long time to construct them all, the idea here is to compute only the first one and to know how to pass to the next element. We define a generic data structure ’a enum which will allow us to enumerate the elements of a set. # type ’a enum = { mutable i : ’a; f :’a → ’a } ; ; type ’a enum = { mutable i : ’a; f : ’a -> ’a; } # let next e = let x = e.i in e.i <- (e.f e.i) ; x ; ; val next : ’a enum -> ’a =

Now we can get the set of natural numbers by instantiating the fields of this structure: # let nat = { i=0; f=fun x → x + 1 }; ; val nat : int enum = {i = 0; f = } # next nat; ; - : int = 0 # next nat; ; - : int = 1 # next nat; ; - : int = 2

Another example gives the elements of the Fibonnacci sequence, which has the defini-

108

Chapter 4 : Functional and Imperative Styles

tion:

   u0 = 1 u1 = 1   un+2 = un + un+1

The function to compute the successor must take account of the current value, (un−1 ), but also of the preceding one (un−2 ). For this, we use the state c in the following closure: # let fib = let fx = let c = ref 0 in fun v → let r = !c + v in c:=v ; r in { i=1 ; f=fx } ; ; val fib : int enum = {i = 1; f = } # for i=0 to 10 do print int (next fib); print string " " done ; ; 1 1 2 3 5 8 13 21 34 55 89 - : unit = ()

Streams of Data Streams are (potentially infinite) sequences containing elements of the same kind. The evaluation of a part of a stream is done on demand, whenever it is needed by the current computation. A stream is therefore a lazy data structure. The stream type is an abstract data type; one does not need to know how it is implemented. We manipulate objects of this type using constructor functions and destructor (or selector) functions. For the convenience of the user, Objective Caml has simple syntactic constructs to construct streams and to access their elements. 警告

Streams are an extension of the language, not part of the stable core of Objective Caml.

Construction The syntactic sugar to construct streams is inspired by that for lists and arrays. The empty stream is written: # [< >] ; ; Characters 1-3: [< >] ;; ^^ Syntax error

One may construct a stream by enumerating its elements, preceding each one with an with a single quote (character ’): # [< ’0; ’2; ’4 >] ; ; Characters 1-3: [< ’0; ’2; ’4 >] ;; ^^

Streams of Data

109

Syntax error

Expressions not preceded by an apostrophe are considered to be sub-streams: # [< ’0; [< ’1; ’2; ’3 >]; ’4 >] ; ; Characters 1-3: [< ’0; [< ’1; ’2; ’3 >]; ’4 >] ;; ^^ Syntax error # let s1 = [< ’1; ’2; ’3 >] in [< s1; ’4 >] ; ; Characters 9-11: let s1 = [< ’1; ’2; ’3 >] in [< s1; ’4 >] ;; ^^ Syntax error # let concat stream a b = [< a ; b >] ; ; Characters 24-26: let concat_stream a b = [< a ; b >] ;; ^^ Syntax error # concat stream [< ’"if"; ’"c";’"then";’"1" >] [< ’"else";’"2" >] ; ; Characters 14-16: concat_stream [< ’"if"; ’"c";’"then";’"1" >] [< ’"else";’"2" >] ;; ^^ Syntax error

The Stream module also provides other construction functions. For instance, the functions of channel and of string return a stream containing a sequence of characters, received from an input stream or a string. # Stream.of channel ; ; - : in_channel -> char Stream.t = # Stream.of string ; ; - : string -> char Stream.t =

The deferred computation of streams makes it possible to manipulate infinite data structures in a way similar to the type ’a enum defined on page 107. We define the stream of natural numbers by its first element and a function calculating the stream of elements to follow: # let rec nat stream n = [< ’n ; nat stream (n+1) >] ; ; Characters 24-26: let rec nat_stream n = [< ’n ; nat_stream (n+1) >] ;; ^^ Syntax error # let nat = nat stream 0 ; ; Characters 10-20: let nat = nat_stream 0 ;; ^^^^^^^^^^

110

Chapter 4 : Functional and Imperative Styles

Unbound value nat_stream

Destruction and Matching of Streams The primitive next permits us to evaluate, retrieve, and remove the first element of a stream, all at once: # for i=0 to 10 do print int (Stream.next nat) ; print string " " done ; ; Characters 44-47: print_int (Stream.next nat) ; ^^^ This expression has type int enum but is here used with type ’a Stream.t # Stream.next nat ; ; Characters 12-15: Stream.next nat ;; ^^^ This expression has type int enum but is here used with type ’a Stream.t

When the stream is exhausted, an exception is raised. # Stream.next [< >] ; ; Characters 13-15: Stream.next [< >] ;; ^^ Syntax error

To manipulate streams, Objective Caml offers a special-purpose matching construct called destructive matching. The value matched is calculated and removed from the stream. There is no notion of exhaustive match for streams, and, since the data type is lazy and potentially infinite, one may match less than the whole stream. The syntax for matching is: 構文 :

match expr with parser [< 'p1 . . . >] -> expr1 | . . .

The function next could be written: # let next s = match s with parser [< ’x >] → x ; ; Characters 34-36: let next s = match s with parser [< ’x >] -> x ;; ^^ Syntax error # next nat; ; - : int = 3

Note that the enumeration of natural numbers picks up where we left it previously. As with function abstraction, there is a syntactic form matching a function parameter of type Stream.t.

Streams of Data 構文 :

111

parser p -¿ . . .

The function next can thus be rewritten: # let next = parser [<’x>] → x ; ; Characters 19-21: let next = parser [<’x>] -> x ;; ^^ Syntax error # next nat ; ; - : int = 4

It is possible to match the empty stream, but take care: the stream pattern [<>] matches every stream. In fact, a stream s is always equal to the stream [< [<>]; s >]. For this reason, one must reverse the usual order of matching: # let rec it stream f s = match s with parser [< ’x ; ss >] → f x ; it stream f ss | [<>] → () ; ; Characters 49-51: [< ’x ; ss >] -> f x ; it_stream f ss ^^ Syntax error # let print int1 n = print int n ; print string" " ; ; val print_int1 : int -> unit = # it stream print int1 [<’1; ’2; ’3>] ; ; Characters 21-23: it_stream print_int1 [<’1; ’2; ’3>] ;; ^^ Syntax error

Since matching is destructive, one can equivalently write: # let rec it stream f s = match s with parser [< ’x >] → f x ; it stream f s | [<>] → () ; ; Characters 49-51: [< ’x >] -> f x ; it_stream f s ^^ Syntax error # it stream print int1 [<’1; ’2; ’3>] ; ; Characters 21-23: it_stream print_int1 [<’1; ’2; ’3>] ;; ^^ Syntax error

Although streams are lazy, they want to be helpful, and never refuse to furnish a first element; when it has been supplied once it is lost. This has consequences for matching.

112

Chapter 4 : Functional and Imperative Styles

The following function is an attempt (destined to fail) to display pairs from a stream of integers, except possibly for the last element. # let print int2 n1 n2 = print string "(" ; print int n1 ; print string "," ; print int n2 ; print string ")" ; ; val print_int2 : int -> int -> unit = # let rec print stream s = match s with parser [< ’x; ’y >] → print int2 x y; print stream s | [< ’z >] → print int1 z; print stream s | [<>] → print newline () ; ; Characters 49-51: [< ’x; ’y >] -> print_int2 x y; print_stream s ^^ Syntax error # print stream [<’1; ’2; ’3>]; ; Characters 13-15: print_stream [<’1; ’2; ’3>];; ^^ Syntax error

The first two two members of the stream were displayed properly, but during the evaluation of the recursive call (print stream [<3>]), the first pattern found a value for x, which was thereby consumed. There remained nothing more for y. This was what caused the error. In fact, the second pattern is useless, because if the stream is not empty, then first pattern always begins evaluation. To obtain the desired result, we must sequentialize the matching: # let rec print stream s = match s with parser [< ’x >] → (match s with parser [< ’y >] → print int2 x y; print stream s | [<>] → print int1 x; print stream s) | [<>] → print newline () ; ; Characters 50-52: [< ’x >] ^^ Syntax error # print stream [<’1; ’2; ’3>]; ; Characters 13-15: print_stream [<’1; ’2; ’3>];; ^^ Syntax error

Streams of Data

113

If matching fails on the first element of a pattern however, then we again have the familiar behavior of matching: # let rec print stream s = match s with parser [< ’1; ’y >] → print int2 1 y; print stream s | [< ’z >] → print int1 z; print stream s | [<>] → print newline () ; ; Characters 50-52: [< ’1; ’y >] -> print_int2 1 y; print_stream s ^^ Syntax error # print stream [<’1; ’2; ’3>] ; ; Characters 13-15: print_stream [<’1; ’2; ’3>] ;; ^^ Syntax error

The Limits of Matching Because it is destructive, matching streams differs from matching on sum types. We will now illustrate how radically different it can be. We can quite naturally write a function to compute the sum of the elements of a stream: # let rec sum s = match s with parser [< ’n; ss >] → n+(sum ss) | [<>] → 0 ; ; Characters 41-43: [< ’n; ss >] -> n+(sum ss) ^^ Syntax error # sum [<’1; ’2; ’3; ’4>] ; ; Characters 4-6: sum [<’1; ’2; ’3; ’4>] ;; ^^ Syntax error

However, we can just as easily consume the stream from the inside, naming the partial result: # let rec sum s = match s with parser [< ’n; r = sum >] → n+r | [<>] → 0 ; ; Characters 41-43: [< ’n; r = sum >] -> n+r ^^ Syntax error

114

Chapter 4 : Functional and Imperative Styles

# sum [<’1; ’2; ’3; ’4>] ; ; Characters 4-6: sum [<’1; ’2; ’3; ’4>] ;; ^^ Syntax error

We will examine some other important uses of streams in chapter 11, which is devoted to lexical and syntactic analysis. In particular, we will see how consuming a stream from the inside may be profitably used.

Exercises Binary Trees We represent binary trees in the form of vectors. If a tree a has height h, then the length of the vector will be 2(h+1) − 1. If a node has position i, then the left subtree of this node lies in the interval of indices [i + 1 , i + 1 + 2h ], and its right subtree lies in the interval [i + 1 + 2h + 1 , 2(h+1) − 1]. This representation is useful when the tree is almost completely filled. The type ’a of labels for nodes in the tree is assumed to contain a special value indicating that the node does not exist. Thus, we represent labeled trees by the by vectors of type ’a array. 1.

Write a function , taking as input a binary tree of type ’a bin tree (defined on page 50) and an array (which one assumes to be large enough). The function stores the labels contained in the tree in the array, located according to the discipline described above.

2.

Write a function to create a leaf (tree of height 0).

3.

Write a function to construct a new tree from a label and two other trees.

4.

Write a conversion function from the type ’a bin tree to an array.

5.

Define an infix traversal function for these trees.

6.

Use it to display the tree.

7.

What can you say about prefix traversal of these trees?

Spelling Corrector The exercise uses the lexical tree , from the exercise of chapter 2, page 62, to build a spelling corrector. 1.

Construct a dictionary from a file in ASCII in which each line contains one word. For this, one will write a function which takes a file name as argument and returns the corresponding dictionary.

Summary

115

2.

Write a function words that takes a character string and constructs the list of words in this string. The word separators are space, tab, apostrophe, and quotation marks.

3.

Write a function verify that takes a dictionary and a list of words, and returns the list of words that do not occur in the dictionary.

4.

Write a function occurrences that takes a list of words and returns a list of pairs associating each word with the number of its occurrences.

5.

Write a function spellcheck that takes a dictionary and the name of a file containing the text to analyze. It should return the list of incorrect words, together with their number of occurrences.

Set of Prime Numbers We would like now to construct the infinite set of prime numbers (without calculating it completely) using lazy data structures. 1.

Define the predicate divisible which takes an integer and an initial list of prime numbers, and determines whether the number is divisible by one of the integers on the list.

2.

Given an initial list of prime numbers, write the function next that returns the smallest number not on the list.

3.

Define the value setprime representing the set of prime numbers, in the style of the type ’a enum on page 107. It will be useful for this set to retain the integers already found to be prime.

Summary This chapter has compared the functional and imperative programming styles. They differ mainly in the control of execution (implicit in functional and explicit in imperative programming), and in the representation in memory of data (sharing or explicitly copied in the imperative case, irrelevant in the functional case). The implementation of algorithms must take account of these differences. The choice between the two styles leads in fact to mixing them. This mixture allows us to clarify the representation of closures, to optimize crucial parts of applications, and to create mutable functional data. Physical modification of values in the environment of a closure permits us to better understand what a functional value is. The mixture of the two styles gives powerful implementation tools. We used them to construct potentially infinite values.

To Learn More The principal consequences of adding imperative traits to a functional language are: •

To determine the evaluation strategy (strict evaluation);

116

Chapter 4 : Functional and Imperative Styles



to add implementation constraints, especially for the GC (see Chapter 9);



For statically typed languages, to make their type system more complex;



To offer different styles of programming in the same language, permitting us to program in the style appropriate to the algorithm at hand, or possibly in a mixed style.

This last point is important in Objective Caml where we need the same parametric polymorphism for functions written in either style. For this, certain purely functional programs are no longer typable after the addition. Wright’s article ([Wri95]) explains the difficulties of polymorphism in languages with imperative aspects. Objective Caml adopts the solution that he advocates. The classification of different kinds of polymorphism in the presence of physical modification is described well in the thesis of Emmanuel Engel ([Eng98]). These consequences make the job of programming a bit harder, and learning the language a bit more difficult. But because the language is richer for this reason and above all offers the choice of style, the game is worth the candle. For example, strict evaluation is the rule, but it is possible to implement basic mechanisms for lazy evaluation, thanks to the mixture of the two styles. Most purely functional languages use a lazy evaluation style. Among languages close to ML, we would mention Miranda, LazyML, and Haskell. The first two are used at universities for teaching and research. By contrast, there are significant applications written in Haskell. The absence of controllable side effects necessitates an additional abstraction for input/output called monads. One can read works on Haskell (such as [Tho99]) to learn more about this subject. Streams are a good example of the mixture of functional and imperative styles. Their use in lexical and syntactic analysis is described in Chapter 11.

5 The Graphics Interface This chapter presents the Graphics library, which is included in the distribution of the Objective Caml-language. This library is designed in such a way that it works identically under the main graphical interfaces of the most commonly used operating systems: Windows, MacOS, Unix with X-Windows. Graphics permits the realization of drawings which may contain text and images, and it handles basic events like mouse clicks or pressed keys. The model of programming graphics applied is the “painter’s model:” the last touch of color erases the preceding one. This is an imperative model where the graphics window is a table of points which is physically modified by each graphics primitive. The interactions with the mouse and the keyboard are a model of event-driven programming: the primary function of the program is an infinite loop waiting for user interaction. An event starts execution of a special handler, which then returns to the main loop to wait for the next event. Although the Graphics library is very simple, it is sufficient for introducing basic concepts of graphical interfaces, and it also contains basic elements for developing graphical interfaces that are rich and easy to use by the programmer.

Chapter overview The first section explains how to make use of this library on different systems. The second section introduces the basic notions of graphics programming: reference point, plotting, filling, colors, bitmaps. The third section illustrates these concepts by describing and implementing functions for creating and drawing “boxes.” The fourth section demonstrates the animation of graphical objects and their interaction with the background of the screen or other animated objects. The fifth section presents event-driven programming, in other terms the skeleton of all graphical interfaces. Finally, the last

118

Chapter 5 : The Graphics Interface

section uses the library Graphics to construct a graphical interface for a calculator (see page 84).

Using the Graphics Module Utilization of the library Graphics differs depending on the system and the compilation mode used. We will not cover applications other than usable under the interactive toplevel of Objective Caml. Under the Windows and MacOS systems the interactive working environment already preloads this library. To make it available under Unix, it is necessary to create a new toplevel. This depends on the location of the X11 library. If this library is placed in one of the usual search paths for C language libraries, the command line is the following: ocamlmktop -custom -o mytoplevel graphics.cma -cclib -lX11 It generates a new executablemytoplevel into which the library Graphics is integrated. Starting the executable works as follows: ./mytoplevel If, however, as under Linux, the library X11 is placed in another directory, this has to be indicated to the command ocamlmktop: ocamlmktop -custom -o mytoplevel graphics.cma -cclib \ -L/usr/X11/lib -cclib -lX11 In this example, the file libX11.a is searched in the directory /usr/X11/lib. A complete description of the command ocamlmktop can be found in chapter 7.

Basic notions Graphics programming is tightly bound to the technological evolution of hardware, in particular to that of screens and graphics cards. In order to render images in sufficient quality, it is necessary that the drawing be refreshed (redrawn) at regular and short intervals, somewhat like in a cinema. There are basically two techniques for drawing on the screen: the first makes use of a list of visible segments where only the useful part of the drawing is drawn, the second displays all points of the screen (bitmap screen). It is the last technique which is used on ordinary computers. Bitmap screens can be seen as rectangles of accessible, in other terms, displayable points. These points are called pixels, a word derived from picture element. They are the basic elements for constructing images. The height and width of the main bitmap

Graphical display

119

is the resolution of the screen. The size of this bitmap therefore depends on the size of each pixel. In monochrome (black/white) displays, a pixel can be encoded in one bit. For screens that allow gray scales or for color displays, the size of a pixel depends on the number of different colors and shades that a pixel may take. In a bitmap of 320x640 pixels with 256 colors per pixel, it is therefore necessary to encode a pixel in 8 bits, which requires video memory of: 480 ∗ 640 bytes = 307200 bytes ' 300KB. This resolution is still used by certain MS-DOS programs. The basic operations on bitmaps which one can find in the Graphics library are: •

coloration of pixels,



drawing of pixels,



drawing of forms: rectangles, ellipses,



filling of closed forms: rectangles, ellipses, polygons,



displaying text: as bitmap or as vector,



manipulation or displacement of parts of the image.

All these operations take place at a reference point, the one of the bitmap. A certain number of characteristics of these graphical operations like the width of strokes, the joints of lines, the choice of the character font, the style and the motive of filling define what we call a graphical context. A graphical operation always happens in a particular graphical context, and its result depends on it. The graphical context of the Graphics library does not contain anything except for the current point, the current color, the current font and the size of the image.

Graphical display The elements of the graphical display are: the reference point and the graphical context, the colors, the drawings, the filling pattern of closed forms, the texts and the bitmaps.

Reference point and graphical context The Graphics library manages a unique main window. The coordinates of the reference point of the window range from point (0, 0) at the bottom left to the upper right corner of the window. The main functions on this window are: •

open graph, of type string -> unit, which opens a window;



close graph, of type unit -> unit, which closes it;



clear graph, of type unit -> unit, which clears it.

The dimensions of the graphical window are given by the functions size x and size y. The string argument of the function open graph depends on the window system of the machine on which the program is executed and is therefore not platform independent. The empty string, however, opens a window with default settings. It is possible to

120

Chapter 5 : The Graphics Interface

specify the size of the window: under X-Windows, " 200x300" yields a window which is 200 pixels wide and 300 pixels high. Beware, the space at the beginning of the string " 200x300" is required! The graphical context contains a certain number of readable and/or modifiable parameters: the current point: current point : unit -> int * int moveto : int -> int -> unit the current color: set color : color -> unit the width of lines: set line width : int -> unit the current character font: set font : string -> unit the size of characters: set text size : int -> unit

Colors Colors are represented by three bytes: each stands for the intensity value of a main color in the RGB-model (red, green, blue), ranging from a minimum of 0 to a maximum of 255. The function rgb (of type int -> int -> int -> color) allows the generation of a new color from these three components. If the three components are identical, the resulting color is a gray which is more or less intense depending on the intensity value. Black corresponds to the minimum intensity of each component (0 0 0) and white is the maximum (255 255 255). Certain colors are predefined: black, white, red, green, blue, yellow, cyan and magenta. The variables foreground and background correspond to the color of the fore- and the background respectively. Clearing the screen is equivalent to filling the screen with the background color. A color (a value of type color) is in fact an integer which can be manipulated to, for example, decompose the color into its three components (from rgb) or to apply a function to it that inverts it (inv color). (* color == R * 256 * 256 + G * 256 + B *) # let from rgb (c : Graphics.color) = let r = c / 65536 and g = c / 256 mod 256 and b = c mod 256 in (r,g,b); ; val from_rgb : Graphics.color -> int * int * int = # let inv color (c : Graphics.color) = let (r,g,b) = from rgb c in Graphics.rgb (255-r) (255-g) (255-b); ; val inv_color : Graphics.color -> Graphics.color =

The function point color, of type int -> int -> color, returns the color of a point when given its coordinates.

Graphical display

121

Drawing and filling A drawing function draws a line on the screen. The line is of the current width and color. A filling function fills a closed form with the current color. The various line- and filling functions are presented in figure 5.1. drawing plot lineto

draw arc draw ellipse draw circle

filling

fill fill fill fill fill

rect poly arc ellipse circle

int -> int -> ( int * int -> int -> int -> int -> int -> int ->

int -> int int -> int int -> int int) array int -> int int -> int int -> int

-> -> -> -> -> -> ->

type unit unit unit unit unit unit unit

図 5.1: Drawing- and filling functions.

Beware, the function lineto changes the position of the current point to make drawing of vertices more convenient. Drawing polygons To give an example, we add drawing primitives which are not predefined. A polygon is described by a table of its vertices. # let draw rect x0 y0 w h = let (a,b) = Graphics.current point () and x1 = x0+w and y1 = y0+h in Graphics.moveto x0 y0; Graphics.lineto x0 y1; Graphics.lineto x1 y1; Graphics.lineto x1 y0; Graphics.lineto x0 y0; Graphics.moveto a b; ; val draw_rect : int -> int -> int -> int -> unit = # let draw poly r = let (a,b) = Graphics.current point () in let (x0,y0) = r.(0) in Graphics.moveto x0 y0; for i = 1 to (Array.length r)-1 do let (x,y) = r.(i) in Graphics.lineto x y done; Graphics.lineto x0 y0; Graphics.moveto a b; ; val draw_poly : (int * int) array -> unit =

122

Chapter 5 : The Graphics Interface

Please note that these functions take the same arguments as the predefined ones for filling forms. Like the other functions for drawing forms, they do not change the current point. Illustrations in the painter’s model This example generates an illustration of a token ring network (figure 5.2). Each machine is represented by a small circle. We place the set of machines on a big circle and draw a line between the connected machines. The current position of the token in the network is indicated by a small black disk. The function net points generates the coordinates of the machines in the network. The resulting data is stored in a table. # let pi = 3.1415927; ; val pi : float = 3.1415927 # let net points (x,y) l n = let a = 2. *. pi /. (float n) in let rec aux (xa,ya) i = if i > n then [] else let na = (float i) *. a in let x1 = xa + (int of float ( cos(na) *. l)) and y1 = ya + (int of float ( sin(na) *. l)) in let np = (x1,y1) in np :: (aux np (i+1)) in Array.of list (aux (x,y) 1); ; val net_points : int * int -> float -> int -> (int * int) array =

The function draw net displays the connections, the machines and the token. # let draw net (x,y) l n sc st = let r = net points (x,y) l n in draw poly r; let draw machine (x,y) = Graphics.set color Graphics.background; Graphics.fill circle x y sc; Graphics.set color Graphics.foreground; Graphics.draw circle x y sc in Array.iter draw machine r; Graphics.fill circle x y st; ; val draw_net : int * int -> float -> int -> int -> int -> unit =

The following function call corresponds to the left drawing in figure 5.2. # draw net (140,20) 60.0 10 10 3; ; Exception: Graphics.Graphic_failure "graphic screen not opened". # save screen "IMAGES/tokenring.caa"; ;

Graphical display

123

- : unit = ()

We note that the order of drawing objects is important. We first plot the connections

図 5.2: Tokenring network.

then the nodes. The drawing of network nodes erases some part of the connecting lines. Therefore, there is no need to calculate the point of intersection between the connection segments and the circles of the vertices. The right illustration of figure 5.2 inverts the order in which the objects are displayed. We see that the segments appear inside of the circles representing the nodes.

Text The functions for displaying texts are rather simple. The two functions draw char (of type char -> unit) and draw string (of type string -> unit) display a character and a character string respectively at the current point. After displaying, the latter is modified. These functions do not change the current font and its current size. 注意 The displaying of strings may differ depending on the graphical interface.

The function text size takes a string as input and returns a pair of integers that correspond to the dimensions of this string when it is displayed in the current font and size. Displaying strings vertically This example describes the function draw string v, which displays a character string vertically at the current point. It is used in figure 5.3. Each letter is displayed separately by changing the vertical coordinate. # let draw string v

s =

124

Chapter 5 : The Graphics Interface

let (xi,yi) = Graphics.current point () and l = String.length s and (_,h) = Graphics.text size s in Graphics.draw char s.[0]; for i=1 to l-1 do let (_,b) = Graphics.current point () in Graphics.moveto xi (b-h); Graphics.draw char s.[i] done; let (a,_) = Graphics.current point () in Graphics.moveto a yi; ; val draw_string_v : string -> unit =

This function modifies the current point. After displaying, the point is placed at the initial position offset by the width of one character. The following program permits displaying a legend around the axes (figure 5.3) # Graphics.moveto 0 150; Graphics.lineto 300 150; Graphics.moveto 2 130; Graphics.draw string "abscissa"; Graphics.moveto 150 0; Graphics.lineto 150 300; Graphics.moveto 135 280; draw string v "ordinate"; ; Exception: Graphics.Graphic_failure "graphic screen not opened".

図 5.3: Legend around axes.

If we wish to realize vertical displaying of text, it is necessary to account for the fact that the current point is modified by the function draw string v. To do this, we define the function draw text v, which accepts the spacing between columns and a list of words as parameters.

Graphical display

125

# let draw text v n l = let f s = let (a,b) = Graphics.current point () in draw string v s; Graphics.moveto (a+n) b in List.iter f l; ; val draw_text_v : int -> string list -> unit =

If we need further text transformations like, for example, rotation, we will have to take the bitmap of each letter and perform the rotation on this set of pixels.

Bitmaps A bitmap may be represented by either a color matrix (color array array) or a value of abstract type 1 image, which is declared in library Graphics. The names and types of the functions for manipulating bitmaps are given in figure 5.4. function make image dump image draw image get image blit image create image

type color array array -> image image -> color array array image -> int -> int -> unit int -> int -> int -> int -> image image -> int -> int -> unit int -> int -> image

図 5.4: Functions for manipulating bitmaps.

The functions make image and dump image are conversion functions between types image and color array array. The function draw image displays a bitmap starting at the coordinates of its bottom left corner. The other way round, one can capture a rectangular part of the screen to create an image using the function get image and by indicating the bottom left corner and the upper right one of the area to be captured. The function blit image modifies its first parameter (of type image) and captures the region of the screen where the lower left corner is given by the point passed as parameter. The size of the captured region is the one of the image argument. The function create image allows initializing images by specifying their size to use them with blit image. The predefined color transp can be used to create transparent points in an image. This makes it possible to display an image within a rectangular area only; the transparent points do not modify the initial screen.

1. Abstract types hide the internal representation of their values. The declaration of such types will be presented in chapter 14.

126

Chapter 5 : The Graphics Interface

Polarization of Jussieu This example inverts the color of points of a bitmap. To do this, we use the function for color inversion presented on page 120, applying it to each pixel of a bitmap. # let inv image i = let inv vec = Array.map (fun c → inv color c) in let inv mat = Array.map inv vec in let inverted matrix = inv mat (Graphics.dump image i) in Graphics.make image inverted matrix; ; val inv_image : Graphics.image -> Graphics.image =

Given the bitmap jussieu, which is displayed in the left half of figure 5.5, we use the function inv image and obtain a new “solarized” bitmap, which is displayed in the right half of the same figure. # let f jussieu2 () = inv image jussieu1; ; Characters 32-40: let f_jussieu2 () = inv_image jussieu1;; ^^^^^^^^ Unbound value jussieu1

図 5.5: Inversion of Jussieu.

Example: drawing of boxes with relief patterns In this example we will define a few utility functions for drawing boxes that carry relief patterns. A box is a generic object that is useful in many cases. It is inscribed in a rectangle which is characterized by a point of origin, a height and a width.

Graphical display

127 color 1

To give an impression of a box with a relief pattern, it is sufficient to surround it with two trapezoids in a light color and two others in a somewhat darker shade.

color 2 color 3

Inverting the colors, one can give the impression that the boxes are on top or at the bottom.

Implementation We add the border width, the display mode (top, bottom, flat) and the colors of its edges and of its bottom. This information is collected in a record. # type relief = Top | Bot | Flat; ; # type box config = { x:int; y:int; w:int; h:int; bw:int; mutable r:relief; b1 col : Graphics.color; b2 col : Graphics.color; b col : Graphics.color}; ;

Only field r can be modified. We use the function draw rect defined at page 121, which draws a rectangle. For convenience, we define a function for drawing the outline of a box. # let draw box outline bcf col = Graphics.set color col; draw rect bcf.x bcf.y bcf.w bcf.h; ; val draw_box_outline : box_config -> Graphics.color -> unit =

The function of displaying a box consists of three parts: drawing the first edge, drawing the second edge and drawing the interior of the box. # let draw box bcf = let x1 = bcf.x and y1 = bcf.y in let x2 = x1+bcf.w and y2 = y1+bcf.h in let ix1 = x1+bcf.bw and ix2 = x2-bcf.bw and iy1 = y1+bcf.bw and iy2 = y2-bcf.bw in let border1 g = Graphics.set color g; Graphics.fill poly [| (x1,y1);(ix1,iy1);(ix2,iy1);(ix2,iy2);(x2,y2);(x2,y1) |]

128

Chapter 5 : The Graphics Interface

in let border2 g = Graphics.set color g; Graphics.fill poly [| (x1,y1);(ix1,iy1);(ix1,iy2);(ix2,iy2);(x2,y2);(x1,y2) |] in Graphics.set color bcf.b col; ( match bcf.r with Top → Graphics.fill rect ix1 iy1 (ix2-ix1) (iy2-iy1); border1 bcf.b1 col; border2 bcf.b2 col | Bot → Graphics.fill rect ix1 iy1 (ix2-ix1) (iy2-iy1); border1 bcf.b2 col; border2 bcf.b1 col | Flat → Graphics.fill rect x1 y1 bcf.w bcf.h ); draw box outline bcf Graphics.black; ; val draw_box : box_config -> unit =

The outline of boxes is highlighted in black. Erasing a box fills the area it covers with the background color. # let erase box bcf = Graphics.set color bcf.b col; Graphics.fill rect (bcf.x+bcf.bw) (bcf.y+bcf.bw) (bcf.w-(2*bcf.bw)) (bcf.h-(2*bcf.bw)); ; val erase_box : box_config -> unit =

Finally, we define a function for displaying a character string at the left, right or in the middle of the box. We use the type position to describe the placement of the string. # type position = Left | Center | Right; ; type position = Left | Center | Right # let draw string in box pos str bcf col = let (w, h) = Graphics.text size str in let ty = bcf.y + (bcf.h-h)/2 in ( match pos with Center → Graphics.moveto (bcf.x + (bcf.w-w)/2) ty | Right → let tx = bcf.x + bcf.w - w - bcf.bw - 1 in Graphics.moveto tx ty | Left → let tx = bcf.x + bcf.bw + 1 in Graphics.moveto tx ty Graphics.set color col; Graphics.draw string str; ; val draw_string_in_box : position -> string -> box_config -> Graphics.color -> unit =

);

Graphical display

129

Example: drawing of a game We illustrate the use of boxes by displaying the position of a game of type “tic-tac-toe” as shown in figure 5.6. To simplify the creation of boxes, we predefine colors. # let set gray x = (Graphics.rgb x x x); ; val set_gray : int -> Graphics.color = # let gray1= set gray 100 and gray2= set gray 170 and gray3= set gray 240; ; val gray1 : Graphics.color = 6579300 val gray2 : Graphics.color = 11184810 val gray3 : Graphics.color = 15790320

We define a function for creating a grid of boxes of same size. # let rec create grid nb col n sep b = if n < 0 then [] else let px = n mod nb col and py = n / nb col in let nx = b.x +sep + px*(b.w+sep) and ny = b.y +sep + py*(b.h+sep) in let b1 = {b with x=nx; y=ny} in b1 :: (create grid nb col (n-1) sep b); ; val create_grid : int -> int -> int -> box_config -> box_config list =

And we create the vector of boxes: # let vb = let b =

{x=0; y=0; w=20;h=20; bw=2; b1 col=gray1; b2 col=gray3; b col=gray2; r=Top} in Array.of list (create grid 5 24 2 b); ; val vb : box_config array = [|{x = 90; y = 90; w = 20; h = 20; bw = 2; r = Top; b1_col = 6579300; b2_col = 15790320; b_col = 11184810}; {x = 68; y = 90; w = 20; h = 20; bw = 2; r = Top; b1_col = 6579300; b2_col = 15790320; b_col = ...}; ...|]

Figure 5.6 corresponds to the following function calls: # Array.iter draw box vb; draw string in box Center "X" vb.(5) Graphics.black; draw string in box Center "X" vb.(8) Graphics.black; draw string in box Center "O" vb.(12) Graphics.yellow; draw string in box Center "O" vb.(11) Graphics.yellow; ; Exception: Graphics.Graphic_failure "graphic screen not opened".

130

Chapter 5 : The Graphics Interface

図 5.6: Displaying of boxes with text.

Animation The animation of graphics on a screen reuses techniques of animated drawings. The major part of a drawing does not change, only the animated part must modify the color of its constituent pixels. One of the immediate problems we meet is the speed of animation. It can vary depending on the computational complexity and on the execution speed of the processor. Therefore, to be portable, an application containing animated graphics must take into account the speed of the processor. To get smooth rendering, it is advisable to display the animated object at the new position, followed by the erasure of the old one and taking special care with the intersection of the old and new regions. Moving an object We simplify the problem of moving an object by choosing objects of a simple shape, namely rectangles. The remaining difficulty is knowing how to redisplay the background of the screen once the object has been moved. We try to make a rectangle move around in a closed space. The object moves at a certain speed in directions X and Y. When it encounters a border of the graphical window, it bounces back depending on the angle of impact. We assume a situation without overlapping of the new and old positions of the object. The function calc pv computes the new position and the new velocity from an old position (x,y), the size of the object (sx,sy) and from the old speed (dx,dy), taking into account the borders of the window. # let calc pv (x,y) (sx,sy) (dx,dy) = let nx1 = x+dx and ny1 = y + dy and nx2 = x+sx+dx and ny2 = y+sy+dy and ndx = ref dx and ndy = ref dy in ( if (nx1 < 0) || (nx2 >= Graphics.size x () ) then ndx := -dx );

Animation

131

( if (ny1 < 0) || (ny2 >= Graphics.size y () ) then ndy := -dy ); ((x+ !ndx, y+ !ndy), (!ndx, !ndy)); ; val calc_pv : int * int -> int * int -> int * int -> (int * int) * (int * int) =

The function move rect moves the rectangle given by pos and size n times, the trajectory being indicated by its speed and by taking into account the borders of the space. The trace of movement which one can see in figure 5.7 is obtained by inversion of the corresponding bitmap of the displaced rectangle. # let move rect pos size speed n = let (x, y) = pos and (sx,sy) = size in let mem = ref (Graphics.get image x y sx sy) in let rec move aux x y speed n = if n = 0 then Graphics.moveto x y else let ((nx,ny),n speed) = calc pv (x,y) (sx,sy) speed and old mem = !mem in mem := Graphics.get image nx ny sx sy; Graphics.set color Graphics.blue; Graphics.fill rect nx ny sx sy; Graphics.draw image (inv image old mem) x y; move aux nx ny n speed (n-1) in move aux x y speed n; ; val move_rect : int * int -> int * int -> int * int -> int -> unit =

The following code corresponds to the drawings in figure 5.7. The first is obtained on a uniformly red background, the second by moving the rectangle across the image of Jussieu. # let anim rect () = Graphics.moveto 105 120; Graphics.set color Graphics.white; Graphics.draw string "Start"; move rect (140,120) (8,8) (8,4) 150; let (x,y) = Graphics.current point () in Graphics.moveto (x+13) y; Graphics.set color Graphics.white; Graphics.draw string "End"; ; val anim_rect : unit -> unit = # anim rect () ; ; Exception: Graphics.Graphic_failure "graphic screen not opened".

The problem was simplified, because there was no intersection between two successive positions of the moved object. If this is not the case, it is necessary to write a function that computes this intersection, which can be more or less complicated depending on

132

Chapter 5 : The Graphics Interface

図 5.7: Moving an object.

the form of the object. In the case of a square, the intersection of two squares yields a rectangle. This intersection has to be removed.

Events The handling of events produced in the graphical window allows interaction between the user and the program. Graphics supports the treating of events like keystrokes, mouse clicks and movements of the mouse. The programming style therefore changes the organization of the program. It becomes an infinite loop waiting for events. After handling each newly triggered event, the program returns to the infinite loop except for events that indicate program termination.

Types and functions for events The main function for waiting for events is wait next event of type event list -> status. The different events are given by the sum type event. type event = Button down | Button up | Key pressed | Mouse motion | Poll; ;

The four main values correspond to pressing and to releasing a mouse button, to movement of the mouse and to keystrokes. Waiting for an event is a blocking operation except if the constructor Poll is passed in the event list. This function returns a value of type status: type status = { mouse x : int; mouse y : int;

Events

133

button : bool; keypressed : bool; key : char}; ;

This is a record containing the position of the mouse, a Boolean which indicates whether a mouse button is being pressed, another Boolean for the keyboard and a character which corresponds to the pressed key. The following functions exploit the data contained in the event record: •

mouse pos: unit -> int * int: returns the position of the mouse with respect to the window. If the mouse is placed elsewhere, the coordinates are outside the borders of the window.



button down: unit -> bool: indicates pressing of a mouse button.



read key: unit -> char: fetches a character typed on the keyboard; this operation blocks.



key pressed: unit -> bool: indicates whether a key is being pressed on the keyboard; this operation does not block.

The handling of events supported by Graphics is indeed minimal for developing interactive interfaces. Nevertheless, the code is portable across various graphical systems like Windows, MacOS or X-Windows. This is the reason why this library does not take into account different mouse buttons. In fact, the Mac does not even possess more than one. Other events, such as exposing a window or changing its size are not accessible and are left to the control of the library.

Program skeleton All programs implementing a graphical user interface make use of a potentially infinite loop waiting for user interaction. As soon as an action arrives, the program executes the job associated with this action. The following function possesses five parameters of functionals. The first two serve for starting and closing the application. The next two arguments handle keyboard and mouse events. The last one permits handling of exceptions that escape out of the different functions of the application. We assume that the events associated with terminating the application raise the exception End. # exception End; ; exception End # let skel f init f end f key f mouse f except = f init () ; try while true do try let s = Graphics.wait next event [Graphics.Button down; Graphics.Key pressed] in if s.Graphics.keypressed then f key s.Graphics.key else if s.Graphics.button then f mouse s.Graphics.mouse x s.Graphics.mouse y

134

Chapter 5 : The Graphics Interface with |

with

End → raise End e → f except e

done

End → f end () ; ; val skel : (unit -> ’a) -> (unit -> unit) -> (char -> unit) -> (int -> int -> unit) -> (exn -> unit) -> unit =

Here, we use the skeleton to implement a mini-editor. Touching a key displays the typed character. A mouse click changes the current point. The character ’&’ exits the program. The only difficulty in this program is line breaking. We assume as simplification that the height of characters does not exceed twelve pixels. # let next line () = let (x,y) = Graphics.current point () in if y>12 then Graphics.moveto 0 (y-12) else Graphics.moveto 0 y; ; val next_line : unit -> unit = # let handle char c = match c with ’&’ → raise End | ’\n’ → next line () | ’\r’ → next line () | _ → Graphics.draw char c; ; val handle_char : char -> unit = # let go () = skel (fun () → Graphics.clear graph () ; Graphics.moveto 0 (Graphics.size y () -12) ) (fun () → Graphics.clear graph () ) handle char (fun x y → Graphics.moveto x y) (fun e → () ); ; val go : unit -> unit =

This program does not handle deletion of characters by pressing the key DEL.

Example: telecran Telecran is a little drawing game for training coordination of movements. A point appears on a slate. This point can be moved in directions X and Y by using two control buttons for these axes without ever releasing the pencil. We try to simulate this behavior to illustrate the interaction between a program and a user. To do this we reuse the previously described skeleton. We will use certain keys of the keyboard to indicate movement along the axes.

Events

135

We first define the type state, which is a record describing the size of the slate in terms of the number of positions in X and Y, the current position of the point and the scaling factor for visualization, the color of the trace, the background color and the color of the current point. # type state = {maxx:int; maxy:int; mutable x : int; mutable y :int; scale:int; bc : Graphics.color; fc: Graphics.color; pc : Graphics.color}; ;

The function draw point displays a point given its coordinates, the scaling factor and its color. # let draw point x y s c = Graphics.set color c; Graphics.fill rect (s*x) (s*y) s s; ; val draw_point : int -> int -> int -> Graphics.color -> unit =

All these functions for initialization, handling of user interaction and exiting the program receive a parameter corresponding to the state. The first four functions are defined as follows: # let t init s () = Graphics.open graph (" " ^ (string of int (s.scale*s.maxx)) ^ "x" ^ (string of int (s.scale*s.maxy))); Graphics.set color s.bc; Graphics.fill rect 0 0 (s.scale*s.maxx+1) (s.scale*s.maxy+1); draw point s.x s.y s.scale s.pc; ; val t_init : state -> unit -> unit = # let t end s () = Graphics.close graph () ; print string "Good bye..."; print newline () ; ; val t_end : ’a -> unit -> unit = # let t mouse s x y = () ; ; val t_mouse : ’a -> ’b -> ’c -> unit = # let t except s ex = () ; ; val t_except : ’a -> ’b -> unit =

The function t init opens the graphical window and displays the current point, t end closes this window and displays a message, t mouse and t except do not do anything. The program handles neither mouse events nor exceptions which may accidentally arise during program execution. The important function is the one for handling the keyboard t key: # let t key s c = draw point s.x s.y s.scale s.fc; (match c with ’8’ → if s.y < s.maxy then s.y <- s.y + 1; | ’2’ → if s.y > 0 then s.y <- s.y - 1

136

Chapter 5 : The Graphics Interface

| ’4’ → if s.x > 0 then s.x <- s.x - 1 | ’6’ → if s.x < s.maxx then s.x <- s.x + 1 | ’c’ → Graphics.set color s.bc; Graphics.fill rect 0 0 (s.scale*s.maxx+1) (s.scale*s.maxy+1); Graphics.clear graph () | ’e’ → raise End | _ → () ); draw point s.x s.y s.scale s.pc; ; val t_key : state -> char -> unit =

It displays the current point in the color of the trace. Depending on the character passed, it modifies, if possible, the coordinates of the current point (characters: ’2’, ’4’, ’6’, ’8’), clears the screen (character: ’c’) or raises the exception End (character: ’e’), then it displays the new current point. Other characters are ignored. The choice of characters for moving the cursor comes from the layout of the numeric keyboard: the chosen keys correspond to the indicated digits and to the direction arrows. It is therefore useful to activate the numeric keyboard for the ergonomics of the program. We finally define a state and apply the skeleton function in the following way: # let stel = {maxx=120; maxy=120; x=60; y=60; scale=4; bc=Graphics.rgb 130 130 130; fc=Graphics.black; pc=Graphics.red}; ; val stel : state = {maxx = 120; maxy = 120; x = 60; y = 60; scale = 4; bc = 8553090; fc = 0; pc = 16711680} # let slate () = skel (t init stel) (t end stel) (t key stel) (t mouse stel) (t except stel); ; val slate : unit -> unit =

Calling function slate displays the graphical window, then it waits for user interaction on the keyboard. Figure 5.8 shows a drawing created with this program.

A Graphical Calculator Let’s consider the calculator example as described in the preceding chapter on imperative programming (see page 84). We will give it a graphical interface to make it more usable as a desktop calculator. The graphical interface materializes the set of keys (digits and functions) and an area for displaying results. Keys can be activated using the graphical interface (and the mouse) or by typing on the keyboard. Figure 5.9 shows the interface we are about to construct.

A Graphical Calculator

137

図 5.8: Telecran.

図 5.9: Graphical calculator.

We reuse the functions for drawing boxes as described on page 126. We define the following type: # type calc state = { s : state; k : (box config * key * string ) list; v : box config } ; ;

It contains the state of the calculator, the list of boxes corresponding to the keys and the visualization box. We plan to construct a calculator that is easily modifiable. Therefore, we parameterize the construction of the interface with an association list: # let descr calc =

138

Chapter 5 : The Graphics Interface [

(Digit 0,"0"); (Digit 3,"3"); (Digit 6,"6"); (Digit 9,"9"); (Off,"AC"); ] ;;

(Digit 1,"1"); (Digit 4,"4"); (Digit 7,"7"); (Recall,"RCL"); (Store, "STO");

(Digit 2,"2"); (Digit 5,"5"); (Digit 8,"8"); (Div, "/"); (Clear,"CE/C")

(Equals, "="); (Plus, "+"); (Minus, "-"); (Times, "*");

Generation of key boxes At the beginning of this description we construct a list of key boxes. The function gen boxes takes as parameters the description (descr), the number of the column (n), the separation between boxes (wsep), the separation between the text and the borders of the box (wsepint) and the size of the board (wbord). This function returns the list of key boxes as well as the visualization box. To calculate these placements, we define the auxiliary functions max xy for calculating the maximal size of a list of complete pairs and max lbox for calculating the maximal positions of a list of boxes. # let gen xy vals comp o = List.fold left (fun a (x,y) → comp (fst a) x,comp (snd a) y) o vals ; ; val gen_xy : (’a * ’a) list -> (’b -> ’a -> ’b) -> ’b * ’b -> ’b * ’b = # let max xy vals = gen xy vals max (min int,min int); ; val max_xy : (int * int) list -> int * int = # let max boxl l = let bmax (mx,my) b = max mx b.x, max my b.y in List.fold left bmax (min int,min int) l ; ; val max_boxl : box_config list -> int * int =

Here is the principal function gen boxes for creating the interface. # let gen boxes descr n wsep wsepint wbord = let l l = List.length descr in let nb lig = if l l mod n = 0 then l l / n else l l / n + 1 in let ls = List.map (fun (x,y) → Graphics.text size y) descr in let sx,sy = max xy ls in let sx,sy= sx+wsepint ,sy+wsepint in let r = ref [] in for i=0 to l l-1 do let px = i mod n and py = i / n in let b = { x = wsep * (px+1) + (sx+2*wbord) * px ; y = wsep * (py+1) + (sy+2*wbord) * py ; w = sx; h = sy ; bw = wbord; r=Top; b1 col = gray1; b2 col = gray3; b col =gray2} in r:= b::!r done; let mpx,mpy = max boxl !r in let upx,upy = mpx+sx+wbord+wsep,mpy+sy+wbord+wsep in 0" in let (wa,ha) = Graphics.text size "

A Graphical Calculator

139

let v = { x=(upx-(wa+wsepint +wbord))/2 ; y= upy+ wsep; w=wa+wsepint; h = ha +wsepint; bw = wbord *2; r=Flat ; b1 col = gray1; b2 col = gray3; b col =Graphics.black} in upx,(upy+wsep+ha+wsepint+wsep+2*wbord),v, List.map2 (fun b (x,y) → b,x,y ) (List.rev !r) descr; ; val gen_boxes : (’a * string) list -> int -> int -> int -> int -> int * int * box_config * (box_config * ’a * string) list =

Interaction Since we would also like to reuse the skeleton proposed on page 133 for interaction, we define the functions for keyboard and mouse control, which are integrated in this skeleton. The function for controlling the keyboard is very simple. It passes the translation of a character value of type key to the function transition of the calculator and then displays the text associated with the calculator state. # let f key cs c = transition cs.s (translation c); erase box cs.v; draw string in box Right (string of int cs.s.vpr) cs.v Graphics.white ; ; val f_key : calc_state -> char -> unit =

The control of the mouse is a bit more complex. It requires verification that the position of the mouse click is actually in one of the key boxes. For this we first define the auxiliary function mem, which verifies membership of a position within a rectangle. # let mem (x,y) (x0,y0,w,h) = (x >= x0) && (x< x0+w) && (y>=y0) && ( y int * int * int * int -> bool = # let f mouse cs x y = try let b,t,s = List.find (fun (b,_,_) → mem (x,y) (b.x+b.bw,b.y+b.bw,b.w,b.h)) cs.k in transition cs.s t; erase box cs.v; draw string in box Right (string of int cs.s.vpr ) cs.v Graphics.white with Not found → () ; ; val f_mouse : calc_state -> int -> int -> unit =

140

Chapter 5 : The Graphics Interface

The function f mouse looks whether the position of the mouse during the click is reallydwell within one of the boxes corresponding to a key. If it is, it passes the corresponding key to the transition function and displays the result, otherwise it will not do anything. The function f exc handles the exceptions which can arise during program execution. # let f exc cs ex = match ex with Division by zero → transition cs.s Clear; erase box cs.v; draw string in box Right "Div 0" cs.v (Graphics.red) | Invalid key → () | Key off → raise End | _ → raise ex; ; val f_exc : calc_state -> exn -> unit =

In the case of a division by zero, it restarts in the initial state of the calculator and displays an error message on its screen. Invalid keys are simply ignored. Finally, the exception Key off raises the exception End to terminate the loop of the skeleton. Initialization and termination The initialization of the calculator requires calculation of the window size. The following function creates the graphical information of the boxes from a key/text association and returns the size of the principal window. # let create e k = Graphics.close graph () ; Graphics.open graph " 10x10"; let mx,my,v,lb = gen boxes k 4 4 5 2 in let s = {lcd=0; lka = false; loa = Equals; vpr = 0; mem = 0} in mx,my,{s=s; k=lb;v=v}; ; val create_e : (key * string) list -> int * int * calc_state =

The initialization function makes use of the result of the preceding function. # let f init mx my cs () = Graphics.close graph () ; Graphics.open graph (" "^(string of int mx)^"x"^(string of int my)); Graphics.set color gray2; Graphics.fill rect 0 0 (mx+1) (my+1); List.iter (fun (b,_,_) → draw box b) cs.k; List.iter (fun (b,_,s) → draw string in box Center s b Graphics.black) cs.k ; draw box cs.v; erase box cs.v; draw string in box Right "hello" cs.v (Graphics.white); ; val f_init : int -> int -> calc_state -> unit -> unit =

Exercises

141

Finally the termination function closes the graphical window. # let f end e () = Graphics.close graph () ; ; val f_end : ’a -> unit -> unit =

The function go is parameterized by a description and starts the interactive loop. # let go descr = let mx,my,e = create e descr in skel (f init mx my e) (f end e) (f key e) (f mouse e) (f exc e); ; val go : (key * string) list -> unit =

The call to go descr calc corresponds to the figure 5.9.

Exercises Polar coordinates Coordinates as used in the library Graphics are Cartesian. There a line segment is represented by its starting point (x0,y0) and its end point (x1,y1). It can be useful to use polar coordinates instead. Here a line segment is described by its point of origin (x0,y0), a length (radius) (r) and an angle (a). The relation between Cartesian and Polar coordinates is defined by the following equations: ( x1 = x0 + r ∗ cos(a) y1 = y0 + r ∗ sin(a) The following type defines the polar coordinates of a line segment: # type seg pol = {x:float; y:float; r:float; a:float}; ; type seg_pol = { x : float; y : float; r : float; a : float; }

1.

Write the function to cart that converts polar coordinates to Cartesian ones.

2.

Write the function draw seg which displays a line segment defined by polar coordinates in the reference point of Graphics.

3.

One of the motivations behind polar coordinates is to be able to easily apply transformations to line segments. A translation only modifies the point of origin, a rotation only affects the angle field and modifying the scale only changes the length field. Generally, one can represent a transformation as a triple of floats: the first represents the translation (we do not consider the case of translating the second point of the line segment here), the second the rotation and the third the scaling factor. Define the function app trans which takes a line segment in polar coordinates and a triple of transformations and returns the new segment.

4.

One can construct recursive drawings by iterating transformations. Write the function draw r which takes as arguments a line segment s, a number of itera-

142

Chapter 5 : The Graphics Interface tions n, a list of transformations and displays all the segments resulting from the transformations on s iterated up to n.

5.

Verify that the following program does produce the images in figure 5.10. let pi = 3.1415927 ; ; let s = {x=100.; y= 0.; a= pi /. 2.; r = 100.} ; ; draw r s 6 [ (-.pi/.2.),0.6,1.; (pi/.2.), 0.6,1.0] ; ; Graphics.clear graph () ; ; draw r s 6 [(-.pi /. 6.), 0.6, 0.766; (-.pi /. 4.), 0.55, 0.333; (pi /. 3.), 0.4, 0.5 ] ; ;

図 5.10: Recursive drawings.

Bitmap editor We will attempt to write a small bitmap editor (similar to the command bitmap in X-window). For this we represent a bitmap by its dimensions (width and height), the pixel size and a two-dimensional table of booleans. 1.

Define a type bitmap state describing the information necessary for containing the values of the pixels, the size of the bitmap and the colors of displayed and erased points.

2.

Write a function for creating bitmaps (create bitmap) and for displaying bitmaps (draw bitmap) .

3.

Write the functions read bitmap and write bitmap which respectively read and write in a file passed as parameter following the ASCII format of X-window. If the file does not exist, the function for reading creates a new bitmap using the function create bitmap. A displayed pixel is represented by the character #, the absence of a pixel by the character -. Each line of characters represents a line of the bitmap. One can test the program using the functions atobm and bmtoa of

Exercises

143

X-window, which convert between this ASCII format and the format of bitmaps created by the command bitmap. Here is an example. ###################-------------#######---------###### ###################---------------###-------------##-###-----###-----###---------------###-------------#--##------###------##----------------###-----------##--#-------###-------#-----------------###---------##---#-------###-------#-----------------###--------##------------###--------------------------###-------#-------------###-------###############-----###----##--------------###-------###---------###------###--##---------------###-------###----------##-------###-#----------------###-------###-----------#-------#####----------------###-------###-----------#--------###-----------------###-------###--------------------####----------------###-------###--------------------####----------------###-------###------#-----------##---###--------------###-------###------#----------##----###--------------###-------##########----------#------###-------------###-------##########---------##-------###------------###-------###------#--------##--------###------------###-------###------#-------##----------###-----------###-------###--------------#------------###--------#######-----###-----------#######--------####### ------------------###--------------------------------------------------###-----------#--------------------------------------###-----------#--------------------------------------###----------##--------------------------------------###---------###--------------------------------------###############---------------------

4.

We reuse the skeleton for interactive loops on page 133 to construct the graphical interface of the editor. The human-computer interface is very simple. The bitmap is permanently displayed in the graphical window. A mouse click in one of the slots of the bitmap inverts its color. This change is reflected on the screen. Pressing the key ’S’ saves the bitmap in a file. The key ’Q’ terminates the program. • Write a function start of type bitmap state -> unit -> unit which opens a graphical window and displays the bitmap passed as parameter. • Write a function stop that closes the graphical window and exits the program. • Write a function mouse of type bitmap state -> int -> int -> unit which modifies the pixel state corresponding to the mouse click and displays the change. • Write a function key of type string -> bitmap state -> char -> unit which takes as arguments the name of a file, a bitmap and the char of the pressed key and executes the associated actions: saving to a file for the key ’S’ and raising of the exception End for the key ’Q’.

5.

Write a function go which takes the name of a file as parameter, loads the bitmap, displays it and starts the interactive loop.

Earth worm The earth worm is a small, longish organism of a certain size which grows over time while eating objects in a world. The earth worm moves constantly in one direction. The only actions allowing a player to control it are changes in direction. The earth worm vanishes if it touches a border of the world or if it passes over a part of its body. It is most often represented by a vector of coordinates with two principal indices: its head and its tail. A move will therefore be computed from the new coordinates of its head, will display it and erase the tail. A growth step only modifies its head without affecting the tail of the earth worm.

144

Chapter 5 : The Graphics Interface

1.

Write the Objective Caml type or types for representing an earth worm and the world where it evolves. One can represent an earth worm by a queue of its coordinates.

2.

Write a function for initialization and displaying an earth worm in a world.

3.

Modify the function skel of the skeleton of the program which causes an action at each execution of the interactive loop, parameterized by a function. The treatment of keyboard events must not block.

4.

Write a function run which advances the earth worm in the game. This function raises the exception Victory (if the worm reaches a certain size) and Loss if it hits a full slot or a border of the world.

5.

Write a function for keyboard interaction which modifies the direction of the earth worm.

6.

Write the other utility functions for handling interaction and pass them to the new skeleton of the program.

7.

Write the initiating function which starts the application.

Summary This chapter has presented the basic notions of graphics programming and event-driven programming using the Graphics library in the distribution of Objective Caml. After having explained the basic graphical elements (colors, drawing, filling, text and bitmaps) we have approached the problem of animating them. The mechanism of handling events in Graphics was then described in a way that allowed the introduction of a general method of handling user interaction. This was accomplished by taking a game as model for event-driven programming. To improve user interactions and to provide interactive graphical components to the programmer, we have developed a new library called Awi, which facilitates the construction of graphical interfaces. This library was used for writing the interface to the imperative calculator.

To learn more Although graphics programming is naturally event-driven, the associated style of programming being imperative, it is not only possible but also often useful to introduce more functional operators to manipulate graphical objects. A good example comes from the use of the MLgraph library, リンク: http://www.pps.jussieu.fr/˜cousinea/MLgraph/mlgraph.html

which implements the graphical model of PostScript and proposes functional operators to manipulate images. It is described in [CC92, CS94] and used later in [CM98] for the optimized placement of trees to construct drawings in the style of Escher. One interesting characteristic of the Graphics library is that it is portable to the graphical interfaces of Windows, MacOS and Unix. The notion of virtual bitmaps can

To learn more

145

be found in several languages like Le Lisp and more recently in Java. Unfortunately, the Graphics library in Objective Caml does not possess interactive components for the construction of interfaces. One of the applications described in part II of this book contains the first bricks of the Awi library. It is inspired by the Abstract Windowing Toolkit of the first versions of Java. One can perceive that it is relatively easy to extend the functionality of this library thanks to the existence of functional values in the language. Therefore chapter 16 compares the adaptation of object oriented programming and functional and modular programming for the construction of graphical interfaces. The example of Awi is functional and imperative, but it is also possible to only use the functional style. This is typically the case for purely functional languages. We cite the systems Fran and Fudget developed in Haskell and derivatives. The system Fran permits construction of interactive animations in 2D and 3D, which means with events between animated objects and the user. リンク: http://www.research.microsoft.com/˜conal/fran/

The Fudget library is intended for the construction of graphical interfaces. リンク: http://www.cs.chalmers.se/ComputingScience/Research/Functional/Fudgets/

One of the difficulties when one wants to program a graphical interface for ones application is to know which of the numerous existing libraries to choose. It is not sufficient to determine the language and the system to fix the choice of the tool. For Objective Caml there exist several more or less complete ones: •

the encapsulation of libX, for X-Windows;



the librt library, also for X-Windows;



ocamltk, an adaptation of Tcl/Tk, portable;



mlgtk, an adaptation of Gtk, portable.

We find the links to these developments in the “Caml Hump”: リンク: http://caml.inria.fr/humps/index.html

Finally, we have only discussed programming in 2D. The tendency is to add one dimension. Functional languages must also respond to this necessity, perhaps in the model of VRML or the Java 3D-extension. In purely functional languages the system Fran offers interesting possibilities of interaction between sprites. More closely to Objective Caml one can use the VRcaML library or the development environment SCOL. The VRcaML library was developed in the manner of MLgraph and integrates a part of the graphical model of VRML in Objective Caml. リンク: http://www.pps.jussieu.fr/˜emmanuel/Public/enseignement/VRcaML

One can therefore construct animated scenes in 3D. The result is a VRML-file that can be directly visualized.

146

Chapter 5 : The Graphics Interface

Still in the line of Caml, the language SCOL is a functional communication language with important libraries for 2D and 3D manipulations, which is intended as environment for people with little knowledge in computer science. リンク: http://www.cryo-networks.com

The interest in the language SCOL and its development environment is to be able to create distributed applications, e.g. client-server, thus facilitating the creation of Internet sites. We present distributed programming in Objective Caml in chapter 20.

6 Applications The reason to prefer one programming language over another lies in the ease of developing and maintaining robust applications. Therefore, we conclude the first part of this book, which dealt with a general presentation of the Objective Caml language, by demonstrating its use in a number of applications. The first application implements a few functions which are used to write database queries. We emphasize the use of list manipulations and the functional programming style. The user has access to a set of functions with which it is easy to write and run queries using the Objective Caml language directly. This application shows the programmer how he can easily provide the user with most of the query tools that the user should need. The second application is an interpreter for a tiny BASIC1 . This kind of imperative language fueled the success of the first microcomputers. Twenty years later, they seem to be very easy to design. Although BASIC is an imperative language, the implementation of the interpreter uses the functional features of Objective Caml, especially for the evaluation of commands. Nevertheless, the lexer and parser for the language use a mutable structure. The third application is a one-player game, Minesweeper, which is fairly well-known since it is bundled with the standard installation of Windows systems. The goal of the game is to uncover a bunch of hidden mines by repeatedly uncovering a square, which then indicates the number of mines around itself. The implementation uses the imperative features of the language, since the data structure used is a two-dimensional array which is modified after each turn of the game. This application uses the Graphics module to draw the game board and to interact with the player. However, the automatic uncovering of some squares will be written in a more functional style. This latter application uses functions from the Graphics module described in chapter

1. which means “Beginner’s All purpose Symbolic Instruction Code”.

148

Chapter 6 : Applications

5 (see page 117) as well as some functions from the Random and Sys modules (see chapter 8, pages 216 and 234).

Database queries The implementation of a database, its interface, and its query language is a project far too ambitious for the scope of this book and for the Objective Caml knowledge of the reader at this point. However, restricting the problem and using the functional programming style at its best allows us to create an interesting tool for query processing. For instance, we show how to use iterators as well as partial application to formulate and execute queries. We also show the use of a data type encapsulating functional values. For this application, we use as an example a database on the members of an association. It is presumed to be stored in the file association.dat.

Data format Most database programs use a “proprietary” format to store the data they manipulate. However, it is usually possible to store the data as some text that has the following structure: •

the database is a list of cards separated by carriage-returns;



each card is a list of fields separated by some given character, ’:’ in our case;



a field is a string which contains no carriage-return nor the character ’:’;



the first card is the list of the names associated with the fields, separated by the character ’|’.

The association data file starts with: Num|Lastname|Firstname|Address|Tel|Email|Pref|Date|Amount 0:Chailloux:Emmanuel:Universit´ e P6:0144274427:[email protected]:email:25.12.1998:100.00 1:Manoury:Pascal:Laboratoire PPS::[email protected]:mail:03.03.1997:150.00 2:Pagano:Bruno:Cristal:0139633963::mail:25.12.1998:150.00 3:Baro:Sylvain::0144274427:[email protected]:email:01.03.1999:50.00

The meaning of the fields is the following: •

Num is the member number;



Lastname, Firstname, Address, Tel, and Email are obvious;



Pref indicates the means by which the member wishes to be contacted: by mail (mail), by email (email), or by phone (tel);



Date and Amount are the date and the amount of the last membership fee received, respectively.

Database queries

149

We need to decide what represention the program should use internally for a database. We could use either a list of cards or an array of cards. On the one hand, a list has the nice property of being easily modified: adding and removing a card are simple operations. On the other hand, an array allows constant access time to any card. Since our goal is to work on all the cards and not on some of them, each query accesses all the cards. Thus a list is a good choice. The same issue arises concerning the cards themselves: should they be lists or arrays of strings? This time an array is a good choice, since the format of a card is fixed for the whole database. It not possible to add a new field. Since a query might access only a few fields, it is important for this access to be fast. The most natural solution for a card would be to use an array indexed by the names of the fields. Since such a type is not available in Objective Caml, we can use an array (indexed by integers) and a function associating a field name with the array index corresponding to the field. # type data card = string array ; ; # type data base = { card index : string → int ; data : data card list } ; ;

Access to the field named n of a card dc of the database db is implemented by the function: # let field db n (dc : data card) = dc.(db.card index n) ; ; val field : data_base -> string -> data_card -> string =

The type of dc has been set to data card to constrain the function field to only accept string arrays and not arrays of other types. Here is a small example: # let base ex = { data = [ [|"Chailloux"; "Emmanuel"|] ; [|"Manoury"; "Pascal"|] ] card index = function "Lastname"→0 | "Firstname"→1 | _->raise Not found } ; ; val base_ex : data_base = {card_index = ; data = [[|"Chailloux"; "Emmanuel"|]; [|"Manoury"; "Pascal"|]]} # List.map (field base ex "Lastname") base ex.data ; ; - : string list = ["Chailloux"; "Manoury"]

;

The expression field base ex "Lastname" evaluates to a function which takes a card and returns the value of its "Lastname" field. The library function List.map applies the function to each card of the database base ex, and returns the list of the results: a list of the "Lastname" fields of the database. This example shows how we wish to use the functional style in our program. Here, the partial application of field allows us to define an access function for a given field, which we can use on any number of cards. This also shows us that the implementation of the field function is not very efficient, since although we are always accessing the same field, its index is computed for each access. The following implementation is better:

150

Chapter 6 : Applications

# let field base name = let i = base.card index name in fun (card : data card) → card.(i) ; ; val field : data_base -> string -> data_card -> string =

Here, after applying the function to two arguments, the index of the field is computed and is used for any subsequent application.

Reading a database from a file As seen from Objective Caml, a file containing a database is just a list of lines. The first work that needs to be done is to read each line as a string, split it into smaller parts according to the separating character, and then extract the corresponding data as well as the field indexing function.

Tools for processing a line We need a function split that splits a string at every occurrence of some separating character. This function uses the function suffix which returns the suffix of a string s after some position i. To do this, we use three predefined functions: •

String.length returns the length of a string;



String.sub returns the substring of s starting at position i and of length l;



String.index from computes the position of the first occurrence of character c in the string s, starting at position n.

# let suffix s i = try String.sub s i ((String.length s)-i) with Invalid argument("String.sub") → "" ; ; val suffix : string -> int -> string = # let split c s = let rec split from n = try let p = String.index from s n c in (String.sub s n (p-n)) :: (split from (p+1)) with Not found → [ suffix s n ] in if s="" then [] else split from 0 ; ; val split : char -> string -> string list =

The only remarkable characteristic in this implementation is the use of exceptions, specifically the exception Not found. Computing the data base structure There is no difficulty in creating an array of strings from a list of strings, since this is what the of list function in the Array module does. It might seem more complicated to compute the index function from a list of field names, but the List module provides all the needed tools.

Database queries

151

Starting from a list of strings, we need to code a function that associates each string with an index corresponding to its position in the list. # let mk index list names = let rec make enum a b = if a > b then [] else a :: (make enum (a+1) b) in let list index = (make enum 0 ((List.length list names) - 1)) in let assoc index name = List.combine list names list index in function name → List.assoc name assoc index name ; ; val mk_index : ’a list -> ’a -> int =

To create the association function between field names and indexes, we combine the list of indexes and the list of names to obtain a list of associations of the type string * int list. To look up the index associated with a name, we use the function assoc from the List library. The function mk index returns a function that takes a name and calls assoc on this name and the previously built association list. It is now possible to create a function that reads a file of the given format. # let read base filename = let channel = open in filename in let split line = split ’:’ in let list names = split ’|’ (input line channel) in let rec read file () = try let data = Array.of list (split line (input line channel )) in data :: (read file () ) with End of file → close in channel ; [] in { card index = mk index list names ; data = read file () } ; ; val read_base : string -> data_base =

The auxiliary function read file reads records from the file, and works recursively on the input channel. The base case of the recursion corresponds to the end of the file, signaled by the End of file exception. In this case, the empty list is returned after closing the channel. The association’s file can now be loaded: # let base ex = read base "association.dat" ; ; val base_ex : data_base = {card_index = ; data = [[|"0"; "Chailloux"; "Emmanuel"; "Universit\233 P6"; "0144274427"; "[email protected]"; "email"; "25.12.1998"; "100.00"|]; [|"1"; "Manoury"; "Pascal"; "Laboratoire PPS"; ...|]; ...]}

General principles for database processing The effectiveness and difficulty of processing the data in a database is proportional to the power and complexity of the query language. Since we want to use Objective Caml as query language, there is no limit a priori on the requests we can express! However,

152

Chapter 6 : Applications

we also want to provide some simple tools to manipulate cards and their data. This desire for simplicity requires us to limit the power of the Objective Caml language, through the use of general goals and principles for database processing. The goal of database processing is to obtain a state of the database. Building such a state may be decomposed into three steps: 1.

selecting, according to some given criterion, a set of cards;

2.

processing each of the selected cards;

3.

processing all the data collected on the cards.

Figure 6.1 illustrates this decomposition. Selection of cards to process

Processing a card

Processing the results

図 6.1: Processing a request.

According to this decomposition, we need three functions of the following types: 1.

(data card -> bool) -> data card list -> data card list

2.

(data card -> ’a) -> data card list -> ’a list

3.

(’a -> ’b -> ’b) -> ’a list -> ’b -> ’b

Objective Caml provides us with three higher-order function, also known as iterators, introduced page 219, that satisfy our specification: # List.find all ; ; - : (’a -> bool) -> ’a list -> ’a list = # List.map ; ; - : (’a -> ’b) -> ’a list -> ’b list = # List.fold right ; ; - : (’a -> ’b -> ’b) -> ’a list -> ’b -> ’b =

We will be able to use them to implement the three steps of building a state by choosing the functions they take as an argument.

Database queries

153

For some special requests, we will also use: # List.iter ; ; - : (’a -> unit) -> ’a list -> unit =

Indeed, if the required processing consists only of displaying some data, there is nothing to compute. In the next paragraphs, we are going to see how to define functions expressing simple selection criteria, as well as simple queries. We conclude this section with a short example using these functions according to the principles stated above.

Selection criteria Concretely, the boolean function corresponding to the selection criterion of a card is a boolean combination of properties of some or all of the fields of the card. Each field of a card, even though it is a string, can contain some information of another type: a float, a date, etc.

Selection criteria on a field Selecting on some field is usually done using a function of the type data base -> ’a -> string -> data card -> bool. The ’a type parameter corresponds to the type of the information contained in the field. The string argument corresponds to the name of the field. String fields We define two simple tests on strings: equality with another string, and non-emptiness. # let eq sfield db s n dc = (s = (field db n dc)) ; ; val eq_sfield : data_base -> string -> string -> data_card -> bool = # let nonempty sfield db n dc = ("" <> (field db n dc)) ; ; val nonempty_sfield : data_base -> string -> data_card -> bool =

Float fields To implement tests on data of type float, it is enough to translate the string representation of a decimal number into its float value. Here are some examples obtained from a generic function tst ffield: # let tst ffield r db val tst_ffield : (’a -> float -> ’b) # let eq ffield = tst # let lt ffield = tst # let le ffield = tst (* etc. *)

v n dc = r v (float of string (field db n dc)) ; ; -> data_base -> ’a -> string -> data_card -> ’b = ffield (=) ; ; ffield (<) ; ; ffield (<=) ; ;

These three functions have type: data base -> float -> string -> data card -> bool.

154

Chapter 6 : Applications

Dates This kind of information is a little more complex to deal with, as it depends on the representation format of dates, and requires that we define date comparison. We decide to represent dates in a card as a string with format dd.mm.yyyy. In order to be able to define additional comparisons, we also allow the replacement of the day, month or year part with the underscore character (’_’). Dates are compared according to the lexicographic order of lists of integers of the form [year; month; day]. To express queries such as: “is before July 1998”, we use the date pattern: " .07.1998". Comparing a date with a pattern is accomplished with the function tst dfield which analyses the pattern to create the ad hoc comparison function. To define this generic test function on dates, we need a few auxiliary functions. We first code two conversion functions from dates (ints of string) and date patterns (ints of dpat) to lists of ints. The character ’_’ of a pattern will be replaced by the integer 0: # let split date = split ’.’ ; ; val split_date : string -> string list = # let ints of string d = try match split date d with [d;m;y] → [int of string y; int of string m; int of string d] | _ → failwith "Bad date format" with Failure("int_of_string") → failwith "Bad date format" ; ; val ints_of_string : string -> int list = # let ints of dpat d = let int of stringpat = function "_" → 0 | s → int of string s in try match split date d with [d;m;y] → [ int of stringpat y; int of stringpat m; int of stringpat d ] | _ → failwith "Bad date format" with Failure("int_of_string") → failwith "Bad date pattern" ; ; val ints_of_dpat : string -> int list =

Given a relation r on integers, we now code the test function. It simply consists of implementing the lexicographic order, taking into account the particular case of 0: # let rec app dtst r d1 d2 = match d1, d2 with [] , [] → false | (0 :: d1) , (_::d2) → app dtst r d1 d2 | (n1 :: d1) , (n2 :: d2) → (r n1 n2) || ((n1 = n2) && (app dtst r d1 d2)) | _, _ → failwith "Bad date pattern or format" ; ; val app_dtst : (int -> int -> bool) -> int list -> int list -> bool =

We finally define the generic function tst dfield which takes as arguments a relation r, a database db, a pattern dp, a field name nm, and a card dc. This function checks that the pattern and the field from the card satisfy the relation. # let tst dfield r db dp nm dc =

Database queries

155

r (ints of dpat dp) (ints of string (field db nm dc)) ; ; val tst_dfield : (int list -> int list -> ’a) -> data_base -> string -> string -> data_card -> ’a =

We now apply it to three relations. # let eq dfield = tst dfield (=) ; ; # let le dfield = tst dfield (<=) ; ; # let ge dfield = tst dfield (>=) ; ;

These three functions have type: data base -> string -> string -> data card -> bool.

Composing criteria The tests we have defined above all take as first arguments a database, a value, and the name of a field. When we write a query, the value of these three arguments are known. For instance, when we work on the database base ex, the test “is before July 1998” is written # ge dfield base ex "_.07.1998" "Date" ; ; - : data_card -> bool =

Thus, we can consider a test as a function of type data card -> bool. We want to obtain boolean combinations of the results of such functions applied to a given card. To this end, we implement the iterator: # let fold funs b c fs dc = List.fold right (fun f → fun r → c (f dc) r) fs b ; ; val fold_funs : ’a -> (’b -> ’a -> ’a) -> (’c -> ’b) list -> ’c -> ’a =

Where b is the base value, the function c is the boolean operator, fs is the list of test functions on a field, and dc is a card. We can obtain the conjunction and the disjunction of a list of tests with: # let and fold fs = fold funs true (&) fs ; ; val and_fold : (’a -> bool) list -> ’a -> bool = # let or fold fs = fold funs false (or) fs ; ; val or_fold : (’a -> bool) list -> ’a -> bool =

We easily define the negation of a test: # let not fun f dc = not (f dc) ; ; val not_fun : (’a -> bool) -> ’a -> bool =

For instance, we can use these combinators to define a selection function for cards whose date field is included in a given range: # let date interval db d1 d2 = and fold [(le dfield db d1 "Date"); (ge dfield db d2 "Date")] ; ; val date_interval : data_base -> string -> string -> data_card -> bool =

156

Chapter 6 : Applications



Processing and computation It is difficult to guess how a card might be processed, or the data that would result from that processing. Nevertheless, we can consider two common cases: numerical computation and data formatting for printing. Let’s take an example for each of these two cases.

Data formatting In order to print, we wish to create a string containing the name of a member of the association, followed by some information. We start with a function that reverses the splitting of a line using a given separating character: # let format list c = let s = String.make 1 c in List.fold left (fun x y → if x="" then y else x^s^y) "" ; ; val format_list : char -> string list -> string =

In order to build the list of fields we are interested in, we code the function extract that returns the fields associated with a given list of names in a given card: # let extract db ns dc = List.map (fun n → field db n dc) ns ; ; val extract : data_base -> string list -> data_card -> string list =

We can now write the line formatting function: # let format line db ns dc = (String.uppercase (field db "Lastname" dc)) ^" "^(field db "Firstname" dc) ^"\t"^(format list ’\t’ (extract db ns dc)) ^"\n" ; ; val format_line : data_base -> string list -> data_card -> string =

The argument ns is the list of requested fields. In the resulting string, fields are separated by a tab (’\t’) and the string is terminated with a newline character. We display the list of last and first names of all members with: # List.iter print string (List.map (format line base ex [] ) base ex.data) ; ; CHAILLOUX Emmanuel MANOURY Pascal PAGANO Bruno BARO Sylvain - : unit = ()

Database queries

157

Numerical computation We want to compute the total amount of received fees for a given set of cards. This is easily done by composing the extraction and conversion of the correct field with the addition. To get nicer code, we define an infix composition operator: # let (++) f g x = g (f x) ; ; val ( ++ ) : (’a -> ’b) -> (’b -> ’c) -> ’a -> ’c =

We use this operator in the following definition: # let total db dcs = List.fold right ((field db "Amount") ++ float of string ++ (+.)) dcs 0.0 ; ; val total : data_base -> data_card list -> float =

We can now apply it to the whole database: # total base ex base ex.data ; ; - : float = 450

An example To conclude, here is a small example of an application that uses the principles described in the paragraphs above. We expect two kinds of queries on our database: •

a query returning two lists, the elements of the first containing the name of a member followed by his mail address, the elements of the other containing the name of the member followed by his email address, according to his preferences.



another query returning the state of received fees for a given period of time. This state is composed of the list of last and first names, dates and amounts of the fees as well as the total amount of the received fees.

List of addresses To create these lists, we first select the relevant cards according to the field "Pref", then we use the formatting function format line: # let mail addresses db = let dcs = List.find all (eq sfield db "mail" "Pref") db.data in List.map (format line db ["Mail"]) dcs ; ; val mail_addresses : data_base -> string list = # let email addresses db = let dcs = List.find all (eq sfield db "email" "Pref") db.data in List.map (format line db ["Email"]) dcs ; ; val email_addresses : data_base -> string list =

158

Chapter 6 : Applications

State of received fees Computing the state of the received fees uses the same technique: selection then processing. In this case however the processing part is twofold: line formatting followed by the computation of the total amount. # let fees state db d1 d2 = let dcs = List.find all (date interval db d1 d2) db.data in let ls = List.map (format line db ["Date";"Amount"]) dcs in let t = total db dcs in ls, t ; ; val fees_state : data_base -> string -> string -> string list * float =

The result of this query is a tuple containing a list of strings with member information, and the total amount of received fees.

Main program The main program is essentially an interactive loop that displays the result of queries asked by the user through a menu. We use here an imperative style, except for the display of the results which uses an iterator. # let main () = let db = read base "association.dat" in let finished = ref false in while not !finished do print string" 1: List of mail addresses\n"; print string" 2: List of email addresses\n"; print string" 3: Received fees\n"; print string" 0: Exit\n"; print string"Your choice: "; match read int () with 0 → finished := true | 1 → (List.iter print string (mail addresses db)) | 2 → (List.iter print string (email addresses db)) | 3 → (let d1 = print string"Start date: "; read line () in let d2 = print string"End date: "; read line () in let ls, t = fees state db d1 d2 in List.iter print string ls; print string"Total: "; print float t; print newline () ) | _ → () done; print string"bye\n" ; ; val main : unit -> unit =

This example will be extended in chapter 21 with an interface using a web browser.

BASIC interpreter

159

Further work A natural extension of this example would consist of adding type information to every field of the database. This information would be used to define generic comparison operators with type data base -> ’a -> string -> data card -> bool where the name of the field (the third argument) would trigger the correct conversion and test functions.

BASIC interpreter The application described in this section is a program interpreter for Basic. Thus, it is a program that can run other programs written in Basic. Of course, we will only deal with a restricted language, which contains the following commands: • • • • • •

PRINT expression Prints the result of the evaluation of the expression. INPUT variable Prints a prompt (?), reads an integer typed in by the user, and assigns its value to the variable. LET variable = expression Assigns the result of the evaluation of expression to the variable. GOTO line number Continues execution at the given line. IF condition THEN line number Continues execution at the given line if the condition is true. REM any string One-line comment.

Every line of a Basic program is labelled with a line number, and contains only one command. For instance, a program that computes and then prints the factorial of an integer given by the user is written: 5 10 20 30 35 40 50 60 70 75 80

REM inputting the argument PRINT " factorial of:" INPUT A LET B = 1 REM beginning of the loop IF A <= 1 THEN 80 LET B = B * A LET A = A - 1 GOTO 40 REM prints the result PRINT B

We also wish to write a small text editor, working as a toplevel interactive loop. It should be able to add new lines, display a program, execute it, and display the result.

160

Chapter 6 : Applications

Execution of the program is started with the RUN command. Here is an example of the evaluation of this program: > RUN factorial of: ? 5 120 The interpreter is implemented in several distinct parts: Description of the abstract syntax : describes the definition of data types to represent Basic programs, as well as their components (lines, commands, expressions, etc.). Program pretty printing : consists of transforming the internal representation of Basic programs to strings, in order to display them. Lexing and parsing : accomplish the inverse transformation, that is, transform a string into the internal representation of a Basic program (the abstract syntax). Evaluation : is the heart of the interpreter. It controls and runs the program. As we will see, functional languages, such as Objective Caml, are particularly well adapted for this kind of problem. Toplevel interactive loop : glues together all the previous parts.

Abstract syntax Figure 6.2 introduces the concrete syntax, as a BNF grammar, of the Basic we will implement. This kind of description for language syntaxes is described in chapter 11, page 297. We can see that the way expressions are defined does not ensure that a well formed expression can be evaluated. For instance, 1+"hello" is an expression, and yet it is not possible to evaluate it. This deliberate choice lets us simplify both the abstract syntax and the parsing of the Basic language. The price to pay for this choice is that a syntactically correct Basic program may generate a runtime error because of a type mismatch. Defining Objective Caml data types for this abstract syntax is easy, we simply translate the concrete syntax into a sum type: # type unr op = UMINUS | NOT ; ; # type bin op = PLUS | MINUS | MULT | DIV | MOD | EQUAL | LESS | LESSEQ | GREAT | GREATEQ | DIFF | AND | OR ; ; # type expression = ExpInt of int | ExpVar of string | ExpStr of string

BASIC interpreter

161

Unary Op

::=



|

Binary Op

::= | |

+ = &

|

::= | | | | |

integer variable "string" Unary Op Expression Expression Binary Op ( Expression )

Command

::= | | | | |

REM string GOTO integer LET variable = Expression PRINT Expression INPUT variable IF Expression THEN integer

Line

::=

integer Command

Program

::= |

Line Line Program

Phrase

::=

Line | RUN | LIST | END

Expression

!

| |

− | ∗ | / | % < | > | <= | 0 0 |

図 6.2: BASIC Grammar. | ExpUnr of unr op * expression | ExpBin of expression * bin op * expression # type command = Rem of string | Goto of int | Print of expression | Input of string | If of expression * int | Let of string * expression ; ; # type line = { num : int ; cmd : command } ; ; # type program = line list ; ;

;;

>=

|

Expression

<>

162

Chapter 6 : Applications

We also define the abstract syntax for the commands for the small program editor: # type phrase =

Line of line | List | Run | PEnd

;;

It is convenient to allow the programmer to skip some parentheses in arithmetic expressions. For instance, the expression 1 + 3 ∗ 4 is usually interpreted as 1 + (3 ∗ 4). To this end, we associate an integer with each operator of the language: # let priority uop = function NOT → 1 | UMINUS → 7 let priority binop = function MULT | DIV → 6 | PLUS | MINUS → 5 | MOD → 4 | EQUAL | LESS | LESSEQ | GREAT | GREATEQ | DIFF → 3 | AND | OR → 2 ; ; val priority_uop : unr_op -> int = val priority_binop : bin_op -> int =

These integers indicate the priority of the operators. They will be used to print and parse programs.

Program pretty printing To print a program, one needs to be able to convert abstract syntax program lines into strings. Converting operators is easy: # let pp binop = function PLUS → "+" | MULT → "*" | MOD → "%" | MINUS → "-" | DIV → "/" | EQUAL → " = " | LESS → " < " | LESSEQ → " <= " | GREAT → " > " | GREATEQ → " >= " | DIFF → " <> " | AND → " & " | OR → " | " let pp unrop = function UMINUS → "-" | NOT → "!" ; ; val pp_binop : bin_op -> string = val pp_unrop : unr_op -> string =

Expression printing needs to take into account operator priority to print as few parentheses as possible. For instance, parentheses are put around a subexpression at the right of an operator only if the subexpression’s main operator has a lower priority that the main operator of the whole expression. Also, arithmetic operators are left-associative, thus the expression 1 − 2 − 3 is interpreted as (1 − 2) − 3. To deal with this, we use two auxiliary functions ppl and ppr to print left and right subtrees, respectively. These functions take two arguments: the tree to print and the priority of the enclosing operator, which is used to decide if parentheses are necessary. Left and right subtrees are distinguished to deal with associativity. If the current operator priority is the same than the enclosing operator priority, left trees do not need parentheses whereas right ones may require them, as in 1 − (2 − 3) or 1 − (2 + 3). The initial tree is taken as a left subtree with minimal priority (0). The expression pretty printing function pp expression is:

BASIC interpreter

163

# let parenthesis x = "(" ^ x ^ ")"; ; val parenthesis : string -> string = # let pp expression = let rec ppl pr = function ExpInt n → (string of int n) | ExpVar v → v | ExpStr s → "\"" ^ s ^ "\"" | ExpUnr (op,e) → let res = (pp unrop op)^(ppl (priority uop op) e) in if pr=0 then res else parenthesis res | ExpBin (e1,op,e2) → let pr2 = priority binop op in let res = (ppl pr2 e1)^(pp binop op)^(ppr pr2 e2) (* parenthesis if priority is not greater *) in if pr2 >= pr then res else parenthesis res and ppr pr exp = match exp with (* right subtrees only differ for binary operators *) ExpBin (e1,op,e2) → let pr2 = priority binop op in let res = (ppl pr2 e1)^(pp binop op)^(ppr pr2 e2) in if pr2 > pr then res else parenthesis res | _ → ppl pr exp in ppl 0 ; ; val pp_expression : expression -> string =

Command pretty printing uses the expression pretty printing function. Printing a line consists of printing the line number before the command. # let pp command = function Rem s → "REM " ^ s | Goto n → "GOTO " ^ (string of int n) | Print e → "PRINT " ^ (pp expression e) | Input v → "INPUT " ^ v | If (e,n) → "IF "^(pp expression e)^" THEN "^(string of int n) | Let (v,e) → "LET " ^ v ^ " = " ^ (pp expression e) ; ; val pp_command : command -> string = # let pp line l = (string of int l.num) ^ " " ^ (pp command l.cmd) ; ; val pp_line : line -> string =

Lexing Lexing and parsing do the inverse transformation of printing, going from a string to a syntax tree. Lexing splits the text of a command line into independent lexical units called lexemes, with Objective Caml type: # type lexeme = Lint of int | Lident of string

164

Chapter 6 : Applications | Lsymbol of string | Lstring of string | Lend ; ;

A particular lexeme denotes the end of an expression: Lend. It is not present in the text of the expression, but is created by the lexing function (see the lexer function, page 165). The string being lexed is kept in a record that contains a mutable field indicating the position after which lexing has not been done yet. Since the size of the string is used several times and does not change, it is also stored in the record: # type string lexer = {string:string; mutable current:int; size:int } ; ;

This representation lets us define the lexing of a string as the application of a function to a value of type string lexer returning a value of type lexeme. Modifying the current position in the string is done as a side effect. # let init lex s = { string=s; current=0 ; size=String.length s } ; ; val init_lex : string -> string_lexer = # let forward cl = cl.current <- cl.current+1 ; ; val forward : string_lexer -> unit = # let forward n cl n = cl.current <- cl.current+n ; ; val forward_n : string_lexer -> int -> unit = # let extract pred cl = let st = cl.string and pos = cl.current in let rec ext n = if n bool) -> string_lexer -> string =

The following functions extract a lexeme from the string and modify the current position. The two functions extract int and extract ident extract an integer and an identifier, respectively. # let extract int = let is int = function ’0’..’9’ → true | _ → false in function cl → int of string (extract is int cl) let extract ident = let is alpha num = function ’a’..’z’ | ’A’..’Z’ | ’0’ .. ’9’ | ’_’ → true | _ → false in extract is alpha num ; ; val extract_int : string_lexer -> int = val extract_ident : string_lexer -> string =

The lexer function uses the two previous functions to extract a lexeme. # exception LexerError ; ; exception LexerError

BASIC interpreter

165

# let rec lexer cl = let lexer char c = match c with ’ ’ | ’\t’ → forward cl ; lexer cl | ’a’..’z’ | ’A’..’Z’ → Lident (extract ident cl) | ’0’..’9’ → Lint (extract int cl) | ’"’ → forward cl ; let res = Lstring (extract ((<>) ’"’) cl) in forward cl ; res | ’+’ | ’-’ | ’*’ | ’/’ | ’%’ | ’&’ | ’|’ | ’!’ | ’=’ | ’(’ | ’)’ → forward cl; Lsymbol (String.make 1 c) | ’<’ | ’>’ → forward cl; if cl.current >= cl.size then Lsymbol (String.make 1 c) else let cs = cl.string.[cl.current] in ( match (c,cs) with (’<’,’=’) → forward cl; Lsymbol "<=" | (’>’,’=’) → forward cl; Lsymbol ">=" | (’<’,’>’) → forward cl; Lsymbol "<>" | _ → Lsymbol (String.make 1 c) ) | _ → raise LexerError in if cl.current >= cl.size then Lend else lexer char cl.string.[cl.current] ; ; val lexer : string_lexer -> lexeme =

The lexer function is very simple: it matches the current character of a string and, based on its value, extracts the corresponding lexeme and modifies the current position to the start of the next lexeme. The code is simple because, for all characters except two, the current character defines which lexeme to extract. In the more complicated cases of ’<’, we need to look at the next character, which might be a ’=’ or a ’>’, producing two different lexemes. The same problem arises with ’>’.

Parsing The only difficulty in parsing our language comes from expressions. Indeed, knowing the beginning of an expression is not enough to know its structure. For instance, having parsed the beginning of an expression as being 1 + 2 + 3, the resulting syntax tree for this part depends on the rest of the expression: its structure is different when it is followed by +4 or ∗4 (see figure 6.3). However, since the tree structure for 1 + 2 is the same in both cases, it can be built. As the position of +3 in the structure is not fully known, it is temporarily stored. To build the abstract syntax tree, we use a pushdown automaton similar to the one built by yacc (see page 305). Lexemes are read one by one and put on a stack until

166

Chapter 6 : Applications

+ +

+

4 *

+

+

3

3

1

1

2

4

2

図 6.3: Basic: abstract syntax tree examples.

there is enough information to build the expression. They are then removed from the stack and replaced by the expression. This latter operation is called reduction. The stack elements have type: # type exp Texp | Tbin | Tunr | Tlp

elem = of expression of bin op of unr op

(* (* (* (*

expression binary operator unary operator left parenthesis

*) *) *) *) ; ;

Right parentheses are not stored on the stack as only left parentheses matter for reduction. Figure 6.4 illustrates the way the stack is used to parse the expression (1 + 2 ∗ 3) + 4. The character above the arrow is the current character of the string. We define an exception for syntax errors. # exception ParseError ; ;

The first step consists of transforming symbols into operators: # let unr symb = function "!" → NOT | "-" → UMINUS | _ → raise ParseError let bin symb = function "+" → PLUS | "-" → MINUS | "*" → MULT | "/" → DIV | "%" → MOD | "=" → EQUAL | "<" → LESS | "<=" → LESSEQ | ">" → GREAT | ">=" → GREATEQ | "<>" → DIFF | "&" → AND | "|" → OR | _ → raise ParseError let tsymb s = try Tbin (bin symb s) with ParseError → Tunr (unr symb s) ; ;

BASIC interpreter

167

3 (

1

(

+

2

*

3

*

2

2

2

+

+

+

+

1

1

1

1

1

(

(

(

(

(

*

(1+2*3)

*

4

4

*

*

(1+2*3)

(1+2*3)

)

(1+2*3)

end

(1+2*3)*4

図 6.4: Basic: abstract syntax tree construction example. val unr_symb : string -> unr_op = val bin_symb : string -> bin_op = val tsymb : string -> exp_elem =

The reduce function implements stack reduction. There are two cases to consider, whether the stack starts with: •

an expression followed by a unary operator,



an expression followed by a binary operator and an expression.

Moreover, reduce takes an argument indicating the minimal priority that an operator should have to trigger reduction. To avoid this reduction condition, it suffices to give the minimal value, zero, as the priority. # let reduce pr = function (Texp e) :: (Tunr op) :: st when (priority uop op) >= pr → (Texp (ExpUnr (op,e))) :: st | (Texp e1) :: (Tbin op) :: (Texp e2) :: st when (priority binop op) >= pr → (Texp (ExpBin (e2,op,e1))) :: st | _ → raise ParseError ; ; val reduce : int -> exp_elem list -> exp_elem list =

168

Chapter 6 : Applications

Notice that expression elements are stacked as they are read. Thus it is necessary to swap them when they are arguments of a binary operator. The main function of our parser is stack or reduce that, according to the lexeme given in argument, puts it on the stack or triggers a reduction. # let rec stack or reduce lex stack = match lex , stack with Lint n , _ → (Texp (ExpInt n)) :: stack | Lident v , _ → (Texp (ExpVar v)) :: stack | Lstring s , _ → (Texp (ExpStr s)) :: stack | Lsymbol "(" , _ → Tlp :: stack | Lsymbol ")" , (Texp e) :: Tlp :: st → (Texp e) :: st | Lsymbol ")" , _ → stack or reduce lex (reduce 0 stack) | Lsymbol s , _ → let symbol = if s<>"-" then tsymb s (* remove the ambiguity of the ‘‘-’’ symbol *) (* according to the last exp element put on the stack *) else match stack with (Texp _)::_ → Tbin MINUS | _ → Tunr UMINUS in ( match symbol with Tunr op → (Tunr op) :: stack | Tbin op → ( try stack or reduce lex (reduce (priority binop op) stack ) with ParseError → (Tbin op) :: stack ) | _ → raise ParseError ) | _ , _ → raise ParseError ; ; val stack_or_reduce : lexeme -> exp_elem list -> exp_elem list =

Once all lexemes are defined and stacked, the function reduce all builds the abstract syntax tree with the elements remaining in the stack. If the expression being parsed is well formed, only one element should remain in the stack, containing the tree for this expression. # let rec reduce all = function | [] → raise ParseError | [Texp x] → x | st → reduce all (reduce 0 st) ; ; val reduce_all : exp_elem list -> expression =

The parse exp function is the main expression parsing function. It reads a string, extracts its lexemes and passes them to the stack or reduce function. Parsing stops when the current lexeme satisfies a predicate that is given as an argument. # let parse exp stop cl = let p = ref 0 in let rec parse one stack

=

BASIC interpreter

169

let l = ( p:=cl.current ; lexer cl) in if not (stop l) then parse one (stack or reduce l stack) else ( cl.current <- !p ; reduce all stack ) in parse one [] ; ; val parse_exp : (lexeme -> bool) -> string_lexer -> expression =

Notice that the lexeme that made the parsing stop is not used to build the expression. It is thus necessary to modify the current position to its beginning (variable p) to parse it later. We can now parse a command line: # let parse cmd cl = match lexer cl with Lident s → ( match s with "REM" → Rem (extract (fun _ → true) cl) | "GOTO" → Goto (match lexer cl with Lint p → p | _ → raise ParseError) | "INPUT" → Input (match lexer cl with Lident v → v | _ → raise ParseError) | "PRINT" → Print (parse exp ((=) Lend) cl) | "LET" → let l2 = lexer cl and l3 = lexer cl in ( match l2 ,l3 with (Lident v,Lsymbol "=") → Let (v,parse exp ((=) Lend) cl) | _ → raise ParseError ) | "IF" → let test = parse exp ((=) (Lident "THEN")) cl in ( match ignore (lexer cl) ; lexer cl with Lint n → If (test,n) | _ → raise ParseError ) | _ → raise ParseError ) | _ → raise ParseError ; ; val parse_cmd : string_lexer -> command =

Finally, we implement the function to parse commands typed by the user: # let parse str = let cl = init lex str in match lexer cl with Lint n → Line { num=n ; cmd=parse cmd cl } | Lident "LIST" → List | Lident "RUN" → Run | Lident "END" → PEnd | _ → raise ParseError ; ; val parse : string -> phrase =

170

Chapter 6 : Applications

Evaluation A Basic program is a list of lines. Execution starts at the first line. Interpreting a program line consists of executing the task corresponding to its command. There are three different kinds of commands: input-output (PRINT and INPUT), variable declaration or modification (LET), and flow control (GOTO and IF. . . THEN). Inputoutput commands interact with the user and use the corresponding Objective Caml functions. Variable declaration and modification commands need to know how to compute the value of an arithmetic expression and the memory location to store the result. Expression evaluation returns an integer, a boolean, or a string. Their type is value. # type value = Vint of int | Vstr of string | Vbool of bool

;;

Variable declaration should allocate some memory to store the associated value. Similarly, variable modification requires the modification of the associated value. Thus, evaluation of a Basic program uses an environment that stores the association between a variable name and its value. It is represented by an association list of tuples (name,value): # type environment = (string * value) list ; ;

The variable name is used to access its value. Variable modification modifies the association. Flow control commands, conditional or unconditional, specify the number of the next line to execute. By default, it is the next line. To do this, it is necessary to remember the number of the current line. The list of commands representing the program being edited under the toplevel is not an efficient data structure for running the program. Indeed, it is then necessary to look at the whole list of lines to find the line indicated by a flow control command (If and goto). Replacing the list of lines with an array of commands allows direct access to the command following a flow control command, using the array index instead of the line number in the flow control command. This solution requires some preprocessing called assembly before executing a RUN command. For reasons that will be detailed shortly, a program after assembly is not represented as an array of commands but as an array of lines: # type code = line array ; ;

As in the calculator example of previous chapters, the interpreter uses a state that is modified for each command evaluation. At each step, we need to remember the whole program, the next line to interpret and the values of the variables. The program being interpreted is not exactly the one that was entered in the toplevel: instead of a list of commands, it is an array of commands. Thus the state of a program during execution is: # type state exec = { line:int ; xprog:code ; xenv:environment } ; ;

BASIC interpreter

171

Two different reasons may lead to an error during the evaluation of a line: an error while computing an expression, or branching to an absent line. They must be dealt with so that the interpreter exits nicely, printing an error message. We define an exception as well as a function to raise it, indicating the line where the error occurred. # exception RunError of int let runerr n = raise (RunError n) ; ; exception RunError of int val runerr : int -> ’a =

Assembly Assembling a program that is a list of numbered lines (type program) consists of transforming this list into an array and modifying the flow control commands. This last modification only needs an association table between line numbers and array indexes. This is easily provided by storing lines (with their line numbers), instead of commands, in the array: to find the association between a line number and the index in the array, we look the line number up in the array and return the corresponding index. If no line is found with this number, the index returned is -1. # exception Result lookup index of int ; ; exception Result_lookup_index of int # let lookup index tprog num line = try for i=0 to (Array.length tprog)-1 do let num i = tprog.(i).num in if num i=num line then raise (Result lookup index i) else if num i>num line then raise (Result lookup index (-1)) done ; (-1 ) with Result lookup index i → i ; ; val lookup_index : line array -> int -> int = # let assemble prog = let tprog = Array.of list prog in for i=0 to (Array.length tprog)-1 do match tprog.(i).cmd with Goto n → let index = lookup index tprog n in tprog.(i) <- { tprog.(i) with cmd = Goto index } | If(c,n) → let index = lookup index tprog n in tprog.(i) <- { tprog.(i) with cmd = If (c,index) } | _ → () done ; tprog ; ; val assemble : line list -> line array =

Expression evaluation The evaluation function does a depth-first traversal on the abstract syntax tree, and executes the operations indicated at each node.

172

Chapter 6 : Applications

The RunError exception is raised in case of type inconsistency, division by zero, or an undeclared variable. # let rec eval exp n envt expr = match expr with ExpInt p → Vint p | ExpVar v → ( try List.assoc v envt with Not found → runerr n ) | ExpUnr (UMINUS,e) → ( match eval exp n envt e with Vint p → Vint (-p) | _ → runerr n ) | ExpUnr (NOT,e) → ( match eval exp n envt e with Vbool p → Vbool (not p) | _ → runerr n ) | ExpStr s → Vstr s | ExpBin (e1,op,e2) → match eval exp n envt e1 , op , eval exp n envt e2 with Vint v1 , PLUS , Vint v2 → Vint (v1 + v2) | Vint v1 , MINUS , Vint v2 → Vint (v1 - v2) | Vint v1 , MULT , Vint v2 → Vint (v1 * v2) | Vint v1 , DIV , Vint v2 when v2<>0 → Vint (v1 / v2) | Vint v1 , MOD , Vint v2 when v2<>0 → Vint (v1 mod v2) | | | | | |

Vint Vint Vint Vint Vint Vint

v1 v1 v1 v1 v1 v1

, , , , , ,

EQUAL DIFF LESS GREAT LESSEQ GREATEQ

, , , , , ,

Vint Vint Vint Vint Vint Vint

v2 v2 v2 v2 v2 v2

→ → → → → →

Vbool Vbool Vbool Vbool Vbool Vbool

(v1 (v1 (v1 (v1 (v1 (v1

= v2) <> v2) < v2) > v2) <= v2) >= v2)

| Vbool v1 , AND , Vbool v2 → Vbool (v1 && v2) | Vbool v1 , OR , Vbool v2 → Vbool (v1 || v2) | Vstr v1 , PLUS , Vstr v2 → Vstr (v1 ^ v2) | _ , _ , _ → runerr n ; ; val eval_exp : int -> (string * value) list -> expression -> value =

Command evaluation To evaluate a command, we need a few additional functions. We add an association to an environment by removing a previous association for the same variable name if there is one: # let rec add v e env = match env with [] → [v,e] | (w,f) :: l → if w=v then (v,e) :: l else (w,f) :: (add v e l) ; ; val add : ’a -> ’b -> (’a * ’b) list -> (’a * ’b) list =

BASIC interpreter

173

A function that prints the value of an integer or string is useful for evaluation of the PRINT command. # let print value v = match v with Vint n → print int n | Vbool true → print string "true" | Vbool false → print string "false" | Vstr s → print string s ; ; val print_value : value -> unit =

The execution of a command corresponds to a transition from one state to another. More precisely, the environment is modified if the command is an assignment. Furthermore, the next line to execute is always modified. As a convention, if the next line to execute does not exist, we set its value to -1 # let next line state = let n = state.line+1 in if n < Array.length state.xprog then n else -1 ; ; val next_line : state_exec -> int = # let eval cmd state = match state.xprog.(state.line).cmd with Rem _ → { state with line = next line state } | Print e → print value (eval exp state.line state.xenv e) ; print newline () ; { state with line = next line state } | Let(v,e) → let ev = eval exp state.line state.xenv e in { state with line = next line state ; xenv = add v ev state.xenv } | Goto n → { state with line = n } | Input v → let x = try read int () with Failure "int_of_string" → 0 in { state with line = next line state; xenv = add v (Vint x) state.xenv } | If (t,n) → match eval exp state.line state.xenv t with Vbool true → { state with line = n } | Vbool false → { state with line = next line state } | _ → runerr state.line ; ; val eval_cmd : state_exec -> state_exec =

On each call of the transition function eval cmd, we look up the current line, run it, then set the number of the next line to run as the current line. If the last line of the program is reached, the current line is given the value -1. This will tell us when to stop. Program evaluation We recursively apply the transition function until we reach a state where the current line number is -1. # let rec run state =

174

Chapter 6 : Applications

if state.line = -1 then state else run (eval cmd state) ; ; val run : state_exec -> state_exec =

Finishing touches The only thing left to do is to write a small editor and to plug together all the functions we wrote in the previous sections. The insert function adds a new line in the program at the requested place. # let rec insert line p = match p with [] → [line] | l :: prog → if l.num < line.num then l :: (insert line prog) else if l.num=line.num then line :: prog else line :: l :: prog ; ; val insert : line -> line list -> line list =

The print prog function prints the source code of a program. # let print prog prog = let print line x = print string (pp line x) ; print newline () in print newline () ; List.iter print line prog ; print newline () ; ; val print_prog : line list -> unit =

The one command function processes the insertion of a line or the execution of a command. It modifies the state of the toplevel loop, which consists of a program and an environment. This state, represented by the loop state type, is different from the evaluation state. # type loop state = { prog:program; env:environment } ; ; # exception End ; ; # let one command state = print string "> " ; flush stdout ; try match parse (input line stdin) with Line l → { state with prog = insert l state.prog } | List → (print prog state.prog ; state ) | Run → let tprog = assemble state.prog in let xstate = run { line = 0; xprog = tprog; xenv = state.env } in {state with env = xstate.xenv } | PEnd → raise End with

BASIC interpreter

175

LexerError → print string "Illegal character\n"; state | ParseError → print string "syntax error\n"; state | RunError n → print string "runtime error at line "; print int n ; print string "\n"; state ; ; val one_command : loop_state -> loop_state =

The main function is the go function, which starts the toplevel loop of our Basic. # let go () = try print string "Mini-BASIC version 0.1\n\n"; let rec loop state = loop (one command state) loop { prog = [] ; env = [] } with End → print string "See you later...\n"; ; val go : unit -> unit =

in

The loop is implemented by the local function loop. It stops when the End exception is raised by the one command function.

Example: C+/CWe return to the example of the C+/C- game described in chapter 3, page 76. Here is the Basic program corresponding to that Objective Caml program: 10 PRINT "Give the hidden number: " 20 INPUT N 30 PRINT "Give a number: " 40 INPUT R 50 IF R = N THEN 110 60 IF R < N THEN 90 70 PRINT "C-" 80 GOTO 30 90 PRINT "C+" 100 GOTO 30 110 PRINT "CONGRATULATIONS" And here is a sample run of this program. > RUN Give the hidden number: 64 Give a number: 88 C-

176

Chapter 6 : Applications

Give a number: 44 C+ Give a number: 64 CONGRATULATIONS

Further work The Basic we implemented is minimalist. If you want to go further, the following exercises hint at some possible extensions. 1.

Floating-point numbers: as is, our language only deals with integers, strings and booleans. Add floats, as well as the corresponding arithmetic operations in the language grammar. We need to modify not only parsing, but also evaluation, taking into account the implicit conversions between integers and floats.

2.

Arrays: Add to the syntax the command DIM var[x] that declares an array var of size x, and the expression var[i] that references the ith element of the array var.

3.

Toplevel directives: Add the toplevel directives SAVE "file name" and LOAD "file name" that save a Basic program to the hard disk, and load a Basic program from the hard disk, respectively.

4.

Sub-program: Add sub-programs. The GOSUB line number command calls a subprogram by branching to the given line number while storing the line from where the call is made. The RETURN command resumes execution at the line following the last GOSUB call executed, if there is one, or exits the program otherwise. Adding sub-programs requires evaluation to manage not only the environement but also a stack containing the return addresses of the current GOSUB calls. The GOSUB command adds the possibility of defining recursive sub-programs.

Minesweeper Let us briefly recall the object of this game: to explore a mine field without stepping on one. A mine field is a two dimensional array (a matrix) where some cells contain hidden mines while others are empty. At the beginning of the game, all the cells are closed and the player must open them one after another. The player wins when he opens all the cells that are empty. Every turn, the player may open a cell or flag it as containing a mine. If he opens a cell that contains a mine, it blows up and the player loses. If the cell is empty, its appearance is modified and the number of mines in the 8 neighbor cells is displayed (thus at most 8). If the player decides to flag a cell, he cannot open it until he removes the flag. We split the implementation of the game into three parts.

Minesweeper

177

図 6.5: Screenshot.

1.

The abstract game, including the internal representation of the mine field as well as the functions manipulating this representation.

2.

The graphical part of the game, including the function for displaying cells.

3.

The interaction between the program and the player.

The abstract mine field This part deals with the mine field as an abstraction only, and does not address its display. Configuration A mine field is defined by its dimensions and the number of mines it contains. We group these three pieces of data in a record and define a default configuration: 10 × 10 cells and 15 mines. # type config = { nbcols : int ; nbrows : int ; nbmines : int }; ;

178

Chapter 6 : Applications

# let default config = { nbcols=10; nbrows=10; nbmines=15 } ; ;

The mine field It is natural to represent the mine field as a two dimensional array. However, it is still necessary to specify what the cells are, and what information their encoding should provide. The state of a cell should answer the following questions: •

is there a mine in this cell?



is this cell opened (has it been seen)?



is this cell flagged?



how many mines are there in neighbor cells?

The last item is not mandatory, as it is possible to compute it when it is needed. However, it is simpler to do this computation once at the beginning of the game. We represent a cell with a record that contains these four pieces of data. # type cell mutable mutable mutable mutable } ;;

= { mined : bool ; seen : bool ; flag : bool ; nbm : int

The two dimensional array is an array of arrays of cells: # type board = cell array array

;;

An iterator In the rest of the program, we often need to iterate a function over all the cells of the mine field. To do it generically, we define the operator iter cells that applies the function f, given as an argument, to each cell of the board defined by the configuration cf. # let iter cells cf f = for i=0 to cf.nbcols-1 do for j=0 to cf.nbrows-1 do f (i,j) done done ; ; val iter_cells : config -> (int * int -> ’a) -> unit =

This is a good example of a mix between functional and imperative programming styles, as we use a higher order function (a function taking another function as an argument) to iterate a function that operates through side effects (as it returns no value). Initialization We randomly choose which cells are mines. If c and r are respectively the number of columns and rows of the mine field, and m the number of mines, we need to generate m different numbers between 1 and c × r. We suppose that m ≤ c × r to define the algorithm, but the program using it will need to check this condition. The straightforward algorithm consists of starting with an empty list, picking a random number and putting it in the list if it is not there already, and repeating this until the

Minesweeper

179

list contains m numbers. We use the following functions from the Random and Sys modules: •

Random.int: int -> int, picks a number between 0 and n−1 (n is the argument) according to a random number generator;



Random.init: int -> unit, initializes the random number generator;



Sys.time: unit -> float, returns the number of milliseconds of processor time the program used since it started. This function will be used to initialize the random number generator with a different seed for each game.

The modules containing these functions are described in more details in chapter 8, pages 216 and 234. The random mine placement function receives the number of cells (cr) and the number of mines to place (m), and returns a list of linear positions for the m mines. # let random list mines cr m = let cell list = ref [] in while (List.length !cell list) < m do let n = Random.int cr in if not (List.mem n !cell list) then cell list := n :: !cell list done ; !cell list ; ; val random_list_mines : int -> int -> int list =

With such an implementation, there is no upper bound on the number of steps the function takes to terminate. If the random number generator is reliable, we can only insure that the probability it does not terminate is zero. However, all experimental uses of this function have never failed to terminate. Thus, even though it is not guaranteed that it will terminate, we will use it to generate the list of mined cells. We need to initialize the random number generator so that each run of the game does not use the same mine field. We use the processor time since the beginning of the program execution to initialize the random number generator. # let generate seed () = let t = Sys.time () in let n = int of float (t*.1000.0) in Random.init(n mod 100000) ; ; val generate_seed : unit -> unit =

In practice, a given program very often takes the same execution time, which results in a similar result for generate seed for each run. We ought to use the Unix.time function (see chapter 18). We very often need to know the neighbors of a given cell, during the initialization of the mine field as well as during the game. Thus we write a neighbors function. This function must take into account the side and corner cells that have fewer neighbors than the middle ones (function valid). # let valid cf (i,j) = i>=0 && i=0 && j int * int -> bool =

;;

180

Chapter 6 : Applications

# let neighbors cf (x,y) = let ngb = [x-1,y-1; x-1,y; x-1,y+1; x,y-1; x,y+1; x+1,y-1; x+1,y; x+1,y+1] in List.filter (valid cf) ngb ; ; val neighbors : config -> int * int -> (int * int) list =

The initialize board function creates the initial mine field. It proceeds in four steps: 1.

generation of the list of mined cells;

2.

creation of a two dimensional array containing different cells;

3.

setting of mined cells in the board;

4.

computation of the number of mines in neighbor cells for each cell that is not mined.

The function initialize board uses a few local functions that we briefly describe.

cell init : creates an initial cell value; copy cell init : puts a copy of the initial cell value in a cell of the board; set mined : puts a mine in a cell; count mined adj : computes the number of mines in the neighbors of a given cell; set count : updates the number of mines in the neighbors of a cell if it is not mined.

# let initialize board cf = let cell init () = { mined=false; seen=false; flag=false; nbm=0 } in let copy cell init b (i,j) = b.(i).(j) <- cell init () in let set mined b n = b.(n / cf.nbrows).(n mod cf.nbrows).mined <- true in let count mined adj b (i,j) = let x = ref 0 in let inc if mined (i,j) = if b.(i).(j).mined then incr x in List.iter inc if mined (neighbors cf (i,j)) ; !x in let set count b (i,j) = if not b.(i).(j).mined then b.(i).(j).nbm <- count mined adj b (i,j) in let list mined = random list mines (cf.nbcols*cf.nbrows) cf.nbmines in let board = Array.make matrix cf.nbcols cf.nbrows (cell init () ) in iter cells cf (copy cell init board) ; List.iter (set mined board) list mined ; iter cells cf (set count board) ; board ; ; val initialize_board : config -> cell array array =

Minesweeper

181

Opening a cell During a game, when the player opens a cell whose neighbors are empty (none contains a mine), he knows that he can open the neighboring cells without risk, and he can keep opening cells as long as he opens cells without any mined neighbor. In order to relieve the player of this boring process (as it is not challenging at all), our Minesweeper opens all these cells itself. To this end, we write the function cells to see that returns a list of all the cells to open when a given cell is opened. The algorithm needed is simple to state: if the opened cell has some neighbors that contain a mine, then the list of cells to see consists only of the opened cell; otherwise, the list of cells to see consists of the neighbors of the opened cell, as well as the lists of cells to see of these neighbors. The difficulty is in writing a program that does not loop, as every cell is a neighbor of any of its neighbors. We thus need to avoid processing the same cell twice. To remember which cells were processed, we use the array of booleans visited. Its size is the same as the mine field. The value true for a cell of this array denotes that it was already visited. We recurse only on cells that were not visited. We use the auxiliary function relevant that computes two sublists from the list of neighbors of a cell. Each one of these lists only contains cells that do not contain a mine, that are not opened, that are not flagged by the player, and that were not visited. The first sublist is the list of neighboring cells who have at least one neighbor containing a mine; the second sublist is the list of neighboring cells whose neighbors are all empty. As these lists are computed, all these cells are marked as visited. Notice that flagged cells are not processed, as a flag is meant to prevent opening a cell. The local function cells to see rec implements the recursive search loop. It takes as an argument the list of cells to visit, updates it, and returns the list of cells to open. This function is called with the list consisting only of the cell being opened, after it is marked as visited. # let cells to see bd cf (i,j) = let visited = Array.make matrix cf.nbcols cf.nbrows false in let rec relevant = function [] → ([],[]) | ((x,y) as c) :: t → let cell=bd.(x).(y) in if cell.mined || cell.flag || cell.seen || visited.(x).(y) then relevant t else let (l1,l2) = relevant t in visited.(x).(y) <- true ; if cell.nbm=0 then (l1,c :: l2) else (c :: l1,l2) in let rec cells to see rec = function [] → [] | ((x,y) as c) :: t → if bd.(x).(y).nbm<>0 then c :: (cells to see rec t) else let (l1,l2) = relevant (neighbors cf c) in (c :: l1) @ (cells to see rec (l2 @ t)) in visited.(i).(j) <- true ;

182

Chapter 6 : Applications

cells to see rec [(i,j)] ; ; val cells_to_see : cell array array -> config -> int * int -> (int * int) list =

At first sight, the argument of cells to see rec may grow between two consecutive calls, although the recursion is based on this argument. It is legitimate to wonder if this function always terminates. The way the visited array is used guarantees that a visited cell cannot be in the result of the relevant function. Also, all the cells to visit come from the result of the relevant function. As the relevant function marks as visited all the cells it returns, it returns each cell at most once, thus a cell may be added to the list of cells to visit at most once. The number of cells being finite, we deduce that the function terminates. Except for graphics, we are done with our Minesweeper. Let us take a look at the programming style we have used. Mutable structures (arrays and mutable record fields) make us use an imperative style of loops and assignments. However, to deal with auxiliary issues, we use lists that are processed by functions written in a functional style. Actually, the programming style is a consequence of the data structure that it manipulates. The function cells to see is a good example: it processes lists, and it is natural to write it in a functional style. Nevertheless, we use an array to remember the cells that were already processed, and we update this array imperatively. We could use a purely functional style by using a list of visited cells instead of an array, and check if a cell is in the list to see if it was visited. However, the cost of such a choice is important (looking up an element in a list is linear in the size of the list, whereas accessing an array element takes constant time) and it does not make the program simpler.

Displaying the Minesweeper game This page dow, page

part depends on the data structures representing the state of the game (see 177). It consists of displaying the different components of the Minesweeper winas shown in figure 6.6. To this end, we use the box drawing functions seen on 126.

The following parameters characterize the components of the graphical window. # # # # # #

let let let let let let

b0 w1 w2 w4 w3 w5

= = = = = =

3 ;; 15 ; ; w1 ; ; 20 + 2*b0 ; ; w4*default config.nbcols + 2*b0 ; ; 40 + 2*b0 ; ;

# # # # # #

let let let let let let

h1 h2 h3 h4 h5 h6

= = = = = =

w1 ; ; 30 ; ; w5+20 + 2*b0 ; ; h2 ; ; 20 + 2*b0 ; ; w5 + 2*b0 ; ;

We use them to extend the basic configuration of our Minesweeper board (value of type config). Below, we define a record type window config. The cf field contains the basic configuration. We associate a box with every component of the display: main window

Minesweeper

183 l3

h4

h6

h3

l5 h2

l1

l2 l4

h5

h1

図 6.6: The main window of Minesweeper.

(field main box), mine field (field field box), dialog window (field dialog box) with two sub-boxes (fields d1 box and d2 box), flagging button (field flag box) and current cell (field current box). # type window config = { cf : config ; main box : box config ; field box : box config ; dialog box : box config ; d1 box : box config ; d2 box : box config ; flag box : box config ; mutable current box : box config ; cell : int*int → (int*int) ; coor : int*int → (int*int) } ;;

Moreover, a record of type window config contains two functions:

184

Chapter 6 : Applications



cell: takes the coordinates of a cell and returns the coordinates of the corresponding box;



coor: takes the coordinates of a pixel of the window and returns the coordinates of the corresponding cell.

Configuration We now define a function that builds a graphical configuration (of type window config) according to a basic configuration (of type config) and the parameters above. The values of the parameters of some components depend on the value of the parameters of other components. For instance, the global box width depends on the mine field width, which, in turn, depends on the number of columns. To avoid computing the same value several times, we incrementally create the components. This initialization phase of a graphical configuration is always a little tedious when there is no adequate primitive or tool available. # let make box x y w h bw r = { x=x; y=y; w=w; h=h; bw=bw; r=r; b1 col=gray1; b2 col=gray3; b col=gray2 } ; ; val make_box : int -> int -> int -> int -> int -> relief -> box_config = # let make wcf cf = let wcols = b0 + cf.nbcols*w4 + b0 and hrows = b0 + cf.nbrows*h5 + b0 in let main box = let gw = (b0 + w1 + wcols + w2 + b0) and gh = (b0 + h1 + hrows + h2 + h3 + h4 + b0) in make box 0 0 gw gh b0 Top and field box = make box w1 h1 wcols hrows b0 Bot in let dialog box = make box ((main box.w - w3) / 2) (b0+h1+hrows+h2) w3 h3 b0 Bot in let d1 box = make box (dialog box.x + b0) (b0 + h1 + hrows + h2) ((w3-w5)/2-(2*b0)) (h3-(2*b0)) 5 Flat in let flag box = make box (d1 box.x + d1 box.w) (d1 box.y + (h3-h6) / 2) w5 h6 b0 Top in let d2 box = make box (flag box.x + flag box.w) d1 box.y d1 box.w d1 box.h 5 Flat in let current box = make box 0 0 w4 h5 b0 Top in { cf = cf; main box = main box; field box=field box; dialog box=dialog box; d1 box=d1 box; flag box=flag box; d2 box=d2 box; current box = current box; cell = (fun (i,j) → ( w1+b0+w4*i , h1+b0+h5*j)) ; coor = (fun (x,y) → ( (x-w1)/w4 , (y-h1)/h5 )) } ; ; val make_wcf : config -> window_config =

Minesweeper

185

Cell display We now need to write the functions to display the cells in their different states. A cell may be open or closed and may contain some information. We always display (the box corresponding with) the current cell in the game configuration (field cc bcf). We thus write two functions modifying the configuration of the current cell; one closing it, the other opening it. # let close ccell wcf i j = let x,y = wcf.cell (i,j) in wcf.current box <- {wcf.current box with x=x; y=y; r=Top} ; ; val close_ccell : window_config -> int -> int -> unit = # let open ccell wcf i j = let x,y = wcf.cell (i,j) in wcf.current box <- {wcf.current box with x=x; y=y; r=Flat} ; ; val open_ccell : window_config -> int -> int -> unit =

Depending on the game phase, we may need to display some information on the cells. We write, for each case, a specialized function. •

Display of a closed cell: # let draw closed cc wcf i j = close ccell wcf i j; draw box wcf.current box ; ; val draw_closed_cc : window_config -> int -> int -> unit =



Display of an opened cell with its number of neighbor mines: # let draw num cc wcf i j n = open ccell wcf i j ; draw box wcf.current box ; if n<>0 then draw string in box Center (string of int n) wcf.current box Graphics.white ; ; val draw_num_cc : window_config -> int -> int -> int -> unit =



Display of a cell containing a mine: # let draw mine cc wcf i j = open ccell wcf i j ; let cc = wcf.current box in draw box wcf.current box ; Graphics.set color Graphics.black ; Graphics.fill circle (cc.x+cc.w/2) (cc.y+cc.h/2) (cc.h/3) ; ; val draw_mine_cc : window_config -> int -> int -> unit =



Display of a flagged cell containing a mine: # let draw flag cc wcf i j = close ccell wcf i j ; draw box wcf.current box ;

186

Chapter 6 : Applications draw string in box Center "!" wcf.current box Graphics.blue ; ; val draw_flag_cc : window_config -> int -> int -> unit =



Display of a wrongly flagged cell: # let draw cross cc wcf i j = let x,y = wcf.cell (i,j) and w,h = wcf.current box.w, wcf.current let a=x+w/4 and b=x+3*w/4 and c=y+h/4 and d=y+3*h/4 in Graphics.set color Graphics.red ; Graphics.set line width 3 ; Graphics.moveto a d ; Graphics.lineto Graphics.moveto a c ; Graphics.lineto Graphics.set line width 1 ; ; val draw_cross_cc : window_config -> int ->

box.h in

b c ; b d ; int -> unit =

During the game, the choice of the display function to use is done by: # let draw cell wcf bd i j = let cell = bd.(i).(j) in match (cell.flag, cell.seen , cell.mined ) with (true,_,_) → draw flag cc wcf i j | (_,false,_) → draw closed cc wcf i j | (_,_,true) → draw mine cc wcf i j | _ → draw num cc wcf i j cell.nbm ; ; val draw_cell : window_config -> cell array array -> int -> int -> unit =

A specialized function displays all the cells at the end of the game. It is slightly different from the previous one as all the cells are taken as opened. Moreover, a red cross indicates the empty cells where the player wrongly put a flag. # let draw cell end wcf bd i j = let cell = bd.(i).(j) in match (cell.flag, cell.mined ) with (true,true) → draw flag cc wcf i j | (true,false) → draw num cc wcf i j cell.nbm; draw cross cc wcf i j | (false,true) → draw mine cc wcf i j | (false,false) → draw num cc wcf i j cell.nbm ; ; val draw_cell_end : window_config -> cell array array -> int -> int -> unit =

Display of the other components The state of the flagging mode is indicated by a box that is either at the bottom or on top and that contain either the word ON or OFF: # let draw flag switch wcf on =

Minesweeper

187

if on then wcf.flag box.r <- Bot else wcf.flag box.r <- Top ; draw box wcf.flag box ; if on then draw string in box Center "ON" wcf.flag box Graphics.red else draw string in box Center "OFF" wcf.flag box Graphics.blue ; ; val draw_flag_switch : window_config -> bool -> unit =

We display the purpose of the flagging button above it: # let draw flag title wcf = let m = "Flagging" in let w,h = Graphics.text size m in let x = (wcf.main box.w-w)/2 and y0 = wcf.dialog box.y+wcf.dialog box.h in let y = y0+(wcf.main box.h-(y0+h))/2 in Graphics.moveto x y ; Graphics.draw string m ; ; val draw_flag_title : window_config -> unit =

During the game, the number of empty cells left to be opened and the number of cells to flag are displayed in the dialog box, to the left and right of the flagging mode button. # let print score wcf nbcto nbfc = erase box wcf.d1 box ; draw string in box Center (string of int nbcto) wcf.d1 box Graphics.blue ; erase box wcf.d2 box ; draw string in box Center (string of int (wcf.cf.nbmines-nbfc)) wcf.d2 box ( if nbfc>wcf.cf.nbmines then Graphics.red else Graphics.blue ) ; ; val print_score : window_config -> int -> int -> unit =

To draw the initial mine field, we need to draw (number of rows) × (number of columns) times the same closed cell. It is always the same drawing, but it may take a long time, as it is necessary to draw a rectangle as well as four trapezoids. To speed up this initialization, we draw only one cell, take the bitmap corresponding to this drawing, and paste this bitmap into every cell. # let draw field initial wcf = draw closed cc wcf 0 0 ; let cc = wcf.current box in let bitmap = draw box cc ; Graphics.get image cc.x cc.y cc.w cc.h let draw bitmap (i,j) = let x,y=wcf.cell (i,j) in Graphics.draw image bitmap x y in iter cells wcf.cf draw bitmap ; ; val draw_field_initial : window_config -> unit =

in

At the end of the game, we open the whole mine field while putting a red cross on cells wrongly flagged:

188

Chapter 6 : Applications

# let draw field end wcf bd = iter cells wcf.cf (fun (i,j) → draw cell end wcf bd i j) ; ; val draw_field_end : window_config -> cell array array -> unit =

Finally, the main display function called at the beginning of the game opens the graphical context and displays the initial state of all the components. # let open wcf wcf = Graphics.open graph ( " " ^ (string of int wcf.main box.w) ^ "x" ^ (string of int wcf.main box.h) ) ; draw box wcf.main box ; draw box wcf.dialog box ; draw flag switch wcf false ; draw box wcf.field box ; draw field initial wcf ; draw flag title wcf ; print score wcf ((wcf.cf.nbrows*wcf.cf.nbcols)-wcf.cf.nbmines) 0 ; ; val open_wcf : window_config -> unit =

Notice that all the display primitives are parameterized by a graphical configuration of type window config. This makes them independent of the layout of the components of our Minesweeper. If we wish to modify the layout, the code still works without any modification, only the configuration needs to be updated.

Interaction with the player We now list what the player may do: •

he may click on the mode box to change mode (opening or flagging),



he may click on a cell to open it or flag it,



he may hit the ’q’ key to quit the game.

Recall that a Graphic event (Graphics.event) must be associated with a record (Graphics.status) that contains the current information on the mouse and keyboard when the event occurs. An interaction with the mouse may happen on the mode button, or on a cell of the mine field. Every other mouse event must be ignored. In order to differentiate these mouse events, we create the type: # type clickon = Out | Cell of (int*int) | SelectBox

;;

Also, pressing the mouse button and releasing it are two different events. For a click to be valid, we require that both events occur on the same component (the flagging mode button or a cell of the mine field). # let locate click wcf st1 st2 = let clickon of st = let x = st.Graphics.mouse x and y = st.Graphics.mouse y in if x>=wcf.flag box.x && x<=wcf.flag box.x+wcf.flag box.w &&

Minesweeper

189

y>=wcf.flag box.y && y<=wcf.flag box.y+wcf.flag box.h then SelectBox else let (x2,y2) = wcf.coor (x,y) in if x2>=0 && x2=0 && y2
in let r1=clickon of st1 and r2=clickon of st2 in if r1=r2 then r1 else Out ; ; val locate_click : window_config -> Graphics.status -> Graphics.status -> clickon =

The heart of the program is the event waiting and processing loop defined in the function loop. It is similar to the function skel described page 133, but specifies the mouse events more precisely. The loop ends when: •

the player presses the q or Q key, meaning that he wants to end the game;



the player opens a cell containing a mine, then he loses;



the player has opened all the cell that are empty, then he wins the game.

We gather in a record of type minesw cf the information useful for the interface: # type minesw cf = { wcf : window config; bd : cell array array; mutable nb flagged cells : int; mutable nb hidden cells : int; mutable flag switch on : bool } ; ;

The meaning of the fields is: •

wcf: the graphical configuration;



bd: the board;



flag switch on: a boolean indicating whether flagging mode or opening mode is on;



nb flagged cells: the number of flagged cells;



nb hidden cells: the number of empty cells left to open;

The main loop is implemented this way: # let loop d f init f key f mouse f end = f init () ; try while true do let st = Graphics.wait next event [Graphics.Button down;Graphics.Key pressed] in if st.Graphics.keypressed then f key st.Graphics.key else let st2 = Graphics.wait next event [Graphics.Button up] in f mouse (locate click d.wcf st st2) done with End → f end () ; ;

190

Chapter 6 : Applications

val loop : minesw_cf -> (unit -> ’a) -> (char -> ’b) -> (clickon -> ’b) -> (unit -> unit) -> unit =

The initialization function, cleanup function and keyboard event processing function are very simple. # let d init d () = open wcf d.wcf let d end () = Graphics.close graph () let d key c = if c=’q’ || c=’Q’ then raise End; ; val d_init : minesw_cf -> unit -> unit = val d_end : unit -> unit = val d_key : char -> unit =

However, the mouse event processing function requires the use of some auxiliary functions: •

flag cell: when clicking on a cell with flagging mode on.



ending: when ending the game. The whole mine field is revealed, we display a message indicating whether the game was won or lost, and we wait for a mouse or keyboard event to quit the application.



reveal: when clicking on a cell with opening mode on (i.e. flagging mode off).

# let flag cell d i j = if d.bd.(i).(j).flag then ( d.nb flagged cells <- d.nb flagged cells -1; d.bd.(i).(j).flag <- false ) else ( d.nb flagged cells <- d.nb flagged cells +1; d.bd.(i).(j).flag <- true ); draw cell d.wcf d.bd i j; print score d.wcf d.nb hidden cells d.nb flagged cells; ; val flag_cell : minesw_cf -> int -> int -> unit = # let ending d str = draw field end d.wcf d.bd; erase box d.wcf.flag box; draw string in box Center str d.wcf.flag box Graphics.black; ignore(Graphics.wait next event [Graphics.Button down;Graphics.Key pressed]); raise End; ; val ending : minesw_cf -> string -> ’a = # let reveal d i j = let reveal cell (i,j) = d.bd.(i).(j).seen <- true;

Minesweeper

191

draw cell d.wcf d.bd i j; d.nb hidden cells <- d.nb hidden cells -1

in List.iter reveal cell (cells to see d.bd d.wcf.cf (i,j)); print score d.wcf d.nb hidden cells d.nb flagged cells; if d.nb hidden cells = 0 then ending d "WON"; ; val reveal : minesw_cf -> int -> int -> unit =

The mouse event processing function matches a value of type clickon. # let d mouse d click = match click with Cell (i,j) → if d.bd.(i).(j).seen then () else if d.flag switch on then flag cell d i j else if d.bd.(i).(j).flag then () else if d.bd.(i).(j).mined then ending d "LOST" else reveal d i j | SelectBox → d.flag switch on <- not d.flag switch on; draw flag switch d.wcf d.flag switch on | Out → () ; ; val d_mouse : minesw_cf -> clickon -> unit =

To create a game configuration, three parameters are needed: the number of columns, the number of rows, and the number of mines. # let create minesw nb c nb r nb m = let nbc = max default config.nbcols nb c and nbr = max default config.nbrows nb r in let nbm = min (nbc*nbr) (max 1 nb m) in let cf = { nbcols=nbc ; nbrows=nbr ; nbmines=nbm } in generate seed () ; let wcf = make wcf cf in { wcf = wcf ; bd = initialize board wcf.cf; nb flagged cells = 0; nb hidden cells = cf.nbrows*cf.nbcols-cf.nbmines; flag switch on = false } ; ; val create_minesw : int -> int -> int -> minesw_cf =

The launch function creates a configuration according to the numbers of columns, rows, and mines, before calling the main event processing loop. # let go nbc nbr nbm = let d = create minesw nbc nbr nbm in loop d (d init d) d key (d mouse d) (d end); ; val go : int -> int -> int -> unit =

192

Chapter 6 : Applications

The function call go 10 10 10 builds and starts a game of the same size as the one depicted in figure 6.5.

Exercises This program can be built as a standalone executable program. Chapter 7 explains how to do this. Once it is done, it is useful to be able to specify the size of the game on the command line. Chapter 8 describes how to get command line arguments in an Objective Caml program, and applies it to our minesweeper (see page 236). Another possible extension is to have the machine play to discover the mines. To do this, one needs to be able to find the safe moves and play them first, then compute the probabilities of presence of a mine and open the cell with the smallest probability.

Part II

Development Tools

193

195 We describe the set of elements of the environment included in the language distribution. There one finds different compilers, numerous libraries, program analysis tools, lexical and syntactic analysis tools, and an interface with the C language. Objective Caml is a compiled language offering two types of code generation: 1.

bytecode to be executed by a virtual machine;

2.

native code to be executed directly by a microprocessor.

The Objective Caml toplevel uses bytecode to execute the phrases submitted to it. It constitutes the primary development aid, offering the possibility of rapid typing, compilation and testing of function definitions. Moreover, it offers a trace mechanism visualizing parameter values and return values of functions. The other usual development tools are supplied by the distribution as well: file dependency computation, debugging and profiling. The debugger allows one to execute programs step-by-step, use breakpoints and inspect values. The profiling tool gives measurements of the number of calls or the amount of time spent in a particular function or a particular part of the code. These two tools are only available for Unix platforms. The richness of a language derives from its core but also from the libraries, sets of reusable programs, which come with it. Objective Caml is no exception to the rule. We have already portrayed to a large extent the graphical library that comes with the distribution. There are many others which we will describe. Libraries bring new functionality to the language, but they are not without drawbacks. In particular, they can present some difficulty vis-a-vis the type discipline. However rich a language’s set of libraries may be, it will always be necessary that it be able to communicate with another language. The Objective Caml distribution includes an interface with the C language allowing Objective Caml to call C functions or be called by them. The difficulty of understanding and implementing this interface lies in the fact that the memory models of Objective Caml and C are different. The essential reason for this difference is that an Objective Caml program includes a garbage collection mechanism. C as well as Objective Caml allow dynamic memory allocation, and thus fine control over space according to the needs of a program. This only makes sense if unused space can be reclaimed for other use during the course of execution. Garbage collection frees the programmer from responsibility for managing deallocation, a frequent source of execution errors. This feature constitutes one of the safety elements of the Objective Caml language. However, this mechanism has an impact on the representation of data. Also, knowledge of the guiding principles of memory management is indispensable in order to use communication between the Objective Caml world and the C world correctly.

196 Chapter 7 presents the basic elements of the Objective Caml system: virtual machine, compilers, and execution library. It describes the language’s different compilation modes and compares their portability and efficiency. Chapter 8 gives a bird’s-eye view of the set of predefined types, functions, and exceptions that come with the system distribution. It does not do away with the need to read the reference manual ([LRVD99]) which describes these libraries very well. On the contrary it focuses on the new functionalities supplied by some of them. In particular we may mention output formatting, persistence of values and interfacing with the operating system. Chapter 9 presents different garbage collection methods in order to then describe the mechanism used by Objective Caml. Chapter 10 presents debugging tools for Objective Caml programs. Although still somewhat frustrating in some respects, these tools quite often allow one to understand why a program does not work. Chapter 11 describes the language’s different approaches to lexical and syntactic analysis problems: a regular expression library, the ocamlex and ocamlyacc tools, but also the use of streams. Chapter 12 describes the interface with the C language. It is no longer possible for a language to be completely isolated from other languages. This interface lets an Objective Caml program call a C function, while passing it values from the Objective Caml world, and vice-versa. The main difficulty with this interface stems from the memory model. For this reason it is recommended that you read the 9 chapter beforehand. Chapter 13 covers two applications: an improved graphics library based on a hierarchical model of graphical components inspired by the JAVA AWT2 ; and a classic program to find least-cost paths in a graph using our new graphical interface as well as a cache memory mechanism.

2. Abstract Windowing Toolkit

7 Compilation and Portability The transformation from human readable source code to an executable requires a number of steps. Together these steps constitute the process of compilation. The compilation process produces an abstract syntax tree (for an example, see page 159) and a sequence of instructions for a cpu or virtual machine. In Objective Caml, the product of compilation is linked with the Objective Caml runtime library. The library is provided with the compiler distribution and is adapted to different host environments (operating system and CPU). The runtime library contains primitive functions such as operations over numbers, the interface to the operating system, and memory management. Objective Caml has two compilers. The first compiler produces bytecode for the Objective Caml virtual machine. The second compiler generates instructions for a number of “real” processors, such as the Intel, Motorola, SPARC, HP-PA, Power-PC and Alpha CPUs. The Objective Caml bytecode compiler produces compact portable code, while the native-code compiler generates high performance architecture dependent code. The Objective Caml toplevel system, which appeared in the first part of this book, uses the bytecode compiler; each user input is compiled and executed in the symbolic environment defined by the current interactive session.

Chapter Overview This chapter presents the different ways to compile an Objective CAML program and compares their portability and efficiency. The first section explains the different steps of Objective Caml compilation. The second section describes the different types of compilation and the syntax for the production of executables. The third section shows how to construct standalone executables - programs which are independent of an installation of the Objective Caml system. Finally the fourth section compares the different types of compilation with respect to portability and efficiency of execution.

198

Chapter 7 : Compilation and Portability

Steps of Compilation An executable file is obtained by translating and linking as described in figure 7.1. Source program preprocessing

↓ Source program

compiling

↓ Assembly program

assembling

↓ Machine instructions ↓ Executable code

linking

図 7.1: Steps in the production of an executable.

To start off, preprocessing replaces certain pieces of text by other text according to a system of macros. Next, compilation translates the source program into assembly instructions, which are then converted to machine instructions. Finally, the linking process establishes a connection to the operating system for primitives. This includes adding the runtime library, which mainly consists of memory management routines.

The Objective Caml Compilers The code generation phases of the Objective Caml compiler are detailed in figure 7.2. The internal representation of the code generated by the compiler is called an intermediate language (IL). The lexical analysis stage transforms a sequence of characters to a sequence of lexical elements. These lexical entities correspond principally to integers, floating point numbers, characters, strings of characters and identifiers. The message Illegal character might be generated by this analysis. The parsing stage constructs a syntax tree and verifies that the sequence of lexical elements is correct with respect to the grammar of the language. The message Syntax error indicates that the phrase analyzed does not follow the grammar of the language. The semantic analysis stage traverses the syntax tree, checking another aspect of program correctness. The analysis consists principally of type inference, which if successful, produces the most general type of an expression or declaration. Type error messages may occur during this phase. This stage also detects whether any members of a sequence are not of type unit. Other warnings may result, including pattern matching analy-

Steps of Compilation

199 Sequence of characters

lexical analysis

↓ Sequence of lexical elements

parsing

↓ Syntax tree

semantic analysis

↓ Annotated syntax tree

generation of intermediate code

↓ Sequence of IL

optimization of intermediate code

↓ Sequence of IL

generation of pseudo code

↓ Assembly program

図 7.2: Compilation stages.

sis (e.g pattern matching is not exhaustive, part of pattern matching will not be used). Generation and the optimization of intermediate code does not produce errors or warning messages. The final step in the compilation process is the generation of a program binary. Details differ from compiler to compiler.

Description of the Bytecode Compiler The Objective Caml virtual machine is called Zinc (“Zinc Is Not Caml”). Originally created by Xavier Leroy, Zinc is described in ([Ler90]). Zinc’s name was chosen to indicate its difference from the first implementation of Caml on the virtual machine CAM (Categorical Abstract Machine, see [CCM87]). Figure 7.3 depicts the bytecode compiler. The first part of this figure shows the Zinc machine interpreter, linked to the runtime library. The second part corresponds to the Objective Caml bytecode compiler which produces instructions for the Zinc machine. The third part contains the set of libraries that come with the compiler. They will be described in Chapter 8. Standard compiler graphical notation is used for describing the components in figure 7.3. A simple box represents a file written in the language indicated in the box. A double box represents the interpretation of a language by a program written in another language. A triple box indicates that a source language is compiled to a machine language by using a compiler written in a third language. Figure 7.4 gives the legend of each box. The legend of figure 7.3 is as follows:

200

Chapter 7 : Compilation and Portability

BC

Zinc

BC .o

C

C

BC µ

.o

ocamlrun

µ

runtime library

C

C

.o

.o

µ

O’CAML compiler v1 -> v2

OC-v2

BC

OC-v2 BC

OC-v1 OC-v1

BC BC

ocamlc

BC

BC µ

library of modules

OC-v2

BC

OC-v2

BC

BC .cmo

BC µ

図 7.3: Virtual machine.

program written in source language (SL)

SL

SL

interpreter of source language SL written in the implementation language IL

IL

SL

TL

compilation of source language SL towards target language TL written in the implementation language IL

IL

図 7.4: Graphical notation for interpreters and compilers.



BC : Zinc bytecode;



C : C code;



.o : object code



µ : micro-processor;

Compilation •

201

OC (v1 or v2) : Objective Caml code.

注意 The majority of the Objective Caml compiler is written in Objective Caml. The second part of figure 7.3 shows how to pass from version v1 of a compiler to version v2.

Compilation The distribution of a language depends on the processor and the operating system. For each architecture, a distribution of Objective Caml contains the toplevel system, the bytecode compiler, and in most cases a native compiler.

Command Names The figure 7.5 shows the command names of the different compilers in the various Objective Caml distributions. The first four commands are available for all distributions. ocaml ocamlrun ocamlc ocamlopt

toplevel loop bytecode interpreter bytecode batch compiler native code batch compiler

ocamlc.opt ocamlopt.opt

optimized bytecode batch compiler optimized native code batch compiler

ocamlmktop

new toplevel constructor

図 7.5: Commands for compiling.

The optimized compilers are themselves compiled with the Objective Caml native compiler. They compile faster but are otherwise identical to their unoptimized counterparts.

Compilation Unit A compilation unit corresponds to the smallest piece of an Objective Caml program that can be compiled. For the interactive system, the unit of compilation corresponds to a phrase of the language. For the batch compiler, the unit of compilation is two files: the source file, and the interface file. The interface file is optional - if it does not exist, then all global declarations in the source file will be visible to other compilation units. The construction of interface files is described in the chapter on module programming (第 14 章参照). The two file types (source and interface) are differentiated by separate file extensions.

202

Chapter 7 : Compilation and Portability

Naming Rules for File Extensions Figure 7.6 presents the extensions of different files used for Objective CAML and C programs. extension .ml .mli .cmo .cma .cmi .cmx .cmxa .c .o .a

meaning source file interface file object file (bytecode) library object file (bytecode) compiled interface file object file (native) library object file (native) C source file C object file (native) C library object file (native)

図 7.6: File extensions.

The files example.ml and example.mli form a compilation unit. The compiled interface file (example.cmi) is used for both the bytecode and native code compiler. The C language related files are used when integrating C code with Objective Caml code. (第 12 章参照).

The Bytecode Compiler The general form of the batch compiler commands are: command options file name For example: ocamlc -c example.ml The command-line options for both the native and bytecode compilers follow typical Unix conventions. Each option is prefixed by the character -. File extensions are interpreted in the manner described by figure 7.6. In the above example, the file example.ml is considered an Objective Caml source file and is compiled. The compiler will produce the files example.cmo and example.cmi. The option -c informs the compiler to generate individual object files, which may be linked at a later time. Without this option, the compiler will produce an executable file named a.out. The table in figure 7.7 describes the principal options of the bytecode compiler. The table in figure 7.8 indicates other possible options.

Compilation

203

-a -c -o name of executable -linkall -i -pp command -unsafe -v -w list -impl file -intf file -I directory

Principal options construct a runtime library compile without linking specify the name of the executable link with all libraries used display all compiled global declarations uses command as preprocessor turn off index checking display the version of the compiler choose among the list the level of warning message (see fig. 7.9) indicate that file is a Caml source (.ml) indicate that file is a Caml interface (.mli) add directory in the list of directories

図 7.7: Principal options of the bytecode compiler.

light process linking standalone executable runtime C interface

Other options -thread (第 19 章参照, page 601) -g, -noassert (第 10 章参照, page 273) -custom, -cclib, -ccopt, -cc (see page 207) -make-runtime , -use-runtime -output-obj (第 12 章参照, page 317)

図 7.8: Other options for the bytecode compiler.

To display the list of bytecode compiler options, use the option -help. The different levels of warning message are described in figure 7.9. A message level is a switch (enable/disable) represented by a letter. An upper case letter activates the level and a lower case letter disables it. Principal levels A/a F/f P/p U/u X/x for hidden object

enable/disable all messages partial application in a sequence for incomplete pattern matching for missing cases in pattern matching enable/disable all other messages M/m and V/v (see chapter 15)

図 7.9: Description of compilation warnings.

204

Chapter 7 : Compilation and Portability

By default, the highest level (A) is chosen by the compiler. Example usage of the bytecode compiler is given in figure 7.10.

図 7.10: Session with the bytecode compiler.

Native Compiler The native compiler has behavior similar to the bytecode compiler, but produces different types of files. The compilation options are generally the same as those described in figures 7.7 and 7.8. It is necessary to take out the options related to runtime in figure 7.8. Options specific to the native compiler are given in figure 7.11. The different warning levels are same. -compact -S -inline level

optimize the produced code for space keeps the assembly code in a file set the aggressiveness of inlining

図 7.11: Options specific to the native compiler.

Inlining is an elaborated version of macro-expansion in the preprocessing stage. For functions whose arguments are fixed, inlining replaces each function call with the body of the function called. Several different calls produce several copies of the function body. Inlining avoids the overhead that comes with function call setup and return, at the expense of object code size. Principal inlining levels are: •

0 : The expansion will be done only when it will not increase the size of the object code.



1 : This is the default value; it accepts a light increase on code size.



n > 1 : Raise the tolerance for growth in the code. Higher values result in more inlining.

Compilation

205

Toplevel Loop The toplevel loop provides only two command line options. •

-I directory: adds the indicated directory to the list of search paths for compiled source files.



-unsafe: instructs the compiler not to do bounds checking on array and string accesses.

The toplevel loop provides several directives which can be used to interactively modify its behavior. They are described in figure 7.12. All these directives begin with the character # and are terminated by ;;. #quit ;; #directory directory ;; #cd directory ;; #load object file ;; #use source file ;; #print depth depth ;; #print length width ;; #install printer function ;; #remove printer function ;; #trace function ;; #untrace function ;; #untrace all ;;

quit from the toplevel interaction add the directory to the search path change the working directory load an object file (.cmo) compile and load a source file modify the depth of printing modify the length of printing specify a printing function remove a printing function trace the arguments of the function stop tracing the function stop all tracing

図 7.12: Toplevel loop directives.

The directives dealing with directories respect the conventions of the operating system used. The loading directives do not have exactly the same behavior. The directive #use reads the source file as if it was typed directly in the toplevel loop. The directive #load loads the file with the extension .cmo. In the later case, the global declarations of this file are not directly accessible. If the file example.ml contains the global declaration f, then once the bytecode is loaded (#load "example.cmo";;), it is assumed that the value of f could be accessed by Example.f, where the first letter of the file is capitalized. This notation comes from the module system of Objective Caml (see chapter 14, page 409). The directives for the depth and width of printing are used to control the display of values. This is useful when it is necessary to display the contents of a value in detail. The directives for printer redefinition are used to install or remove a user defined printing function for values of a specified type. In order to integrate these printer functions

206

Chapter 7 : Compilation and Portability

into the default printing procedure, it is necessary to use the Format library(第 8 章参 照) for the definition. The directives for tracing arguments and results of functions are particularly useful for debugging programs. They will be discussed in the chapter on program analysis (第 10 章参照). Figure 7.13 shows a session in the toplevel loop.

図 7.13: Session with the toplevel loop.

Construction of a New Interactive System The command ocamlmktop can be used to construct a new toplevel executable which has specific library modules loaded by default. For example, ocamlmktop is often used for pulling native object code libraries (typically written in C) into a new toplevel. ocamlmktop options are a subset of those used by the bytecode compiler (ocamlc): -cclib libname, -ccopt option, -custom, -I directory -o executable name The chapter on graphics programming (第 5 章参照, page 117) uses this command for constructing a toplevel system containing the Graphics library in the following manner: ocamlmktop -custom -o mytoplevel graphics.cma -cclib \ -I/usr/X11/lib -cclib -lX11 This command constructs an executable with the name mytoplevel, containing the bytecode library graphics.cma. This standalone executable (-custom, see the following section) will be linked to the library X11 (libX11.a) which in turn will be looked up in the path /usr/X11/lib.

Standalone Executables

207

Standalone Executables A standalone executable is a program that does not depend an Objective Caml installation to run. This facilitates the distribution of binary applications and robustness against runtime library changes across Objective Caml versions. The Objective Caml native compiler produces standalone executables by default. But without the -custom option, the bytecode compiler produces an executable which requires the bytecode interpreter ocamlrun. Imagine the file example.ml is as follows: let f x = x + 1;; print_int (f 18);; print_newline();; Then the following command produces the (approximately 8k) file example.exe: ocamlc -o example.exe example.ml This file can be executed by the Objective Caml bytecode interpreter: $ ocamlrun example.exe 19 The interpreter executes the Zinc machine instructions contained in the file example.exe. Under Unix, the first line of the file example.exe contains the location of the interpreter, for example: #!/usr/local/bin/ocamlrun This means the file can be executed directly (without using ocamlrun. Like a shellscript, executing the file in turn runs the program specified on the first line, which is then used to interpret the remainder of the file. If ocamlrun can’t be found, execution will fail and the error message Command not found will be displayed. The same compilation with the option -custom produces a standalone executable with name exauto.exe: ocamlc -custom -o exauto.exe example.ml This time the file is about 85K, as it contains the Zinc interpreter as well as the program bytecode. This file can be executed directly or copied to another machine (using the same CPU/Operating System) for execution.

208

Chapter 7 : Compilation and Portability

Portability and Efficiency One reason to compile to an abstract machine is to produce an executable independent of the architecture of the real machine where it runs. A native compiler will produce more efficient code, but the binary can only be executed on the architecture it was compiled for.

Standalone Files and Portability To produce a standalone executable, the bytecode compiler links the bytecode object file example.cmo with the runtime library, the bytecode interpreter and some C code. It is assumed that there is a C compiler on the host system. The inclusion of machine code means that stand-alone bytecode executables are not portable to other systems or other architectures. This is not the case for the non-standalone version. Since the Zinc machine is not included, the only things generated are the platform independent bytecode instructions. Bytecode programs will run on any platform that has the interpreter. Ocamlrun is part of the default Objective Caml distribution for Sparc running Solaris, Intel running Windows, etc. It is always preferable to use the same version of interpreter and compiler. The portability of bytecode object files makes it possible to directly distribute Objective Caml libraries in bytecode form.

Efficiency of Execution The bytecode compiler produces a sequence of instructions for the Zinc machine, which at the moment of the execution, will be interpreted by ocamlrun. Interpretation has a moderately negative linear effect on speed of execution. It is possible to view Zinc’s bytecode interpretation as a big pattern matching machine (matching match ... with) where each instruction is a trigger and the computation branch modifies the stack and the counter (address of the next instruction). Without testing all parts of the language, the following small example which computes Fibonacci numbers shows the difference in execution time between the bytecode compiler and the native compiler. Let the program fib.ml as follows: let rec fib n = if n < 2 then 1 else (fib (n-1)) + (fib(n-2));; and the following program main.ml as follows: for i = 1 to 10 do

Exercises

209

print_int (Fib.fib 30); print_newline() done;; Their compilation is as follows: $ ocamlc -o fib.exe fib.ml main.ml $ ocamlopt -o fibopt.exe fib.ml main.ml These commands produce two executables: fib.exe and fibopt.exe. Using the Unix command time in Pentium 350 under Linux, we get the following data: fib.exe (bytecode) 7s

fibopt.exe (native) 1s

This corresponds to a factor 7 between the two versions of the same program. This program does not test all characteristics of the language. The difference depends heavily on the type of application, and is typically much smaller.

Exercises Creation of a Toplevel and Standalone Executable Consider again the Basic interpreter. Modify it to make a new toplevel. 1.

Split the Basic application into 4 files, each with the extension .ml. The files will be organized like this: abstract syntax (syntax.ml), printing (pprint.ml), parsing (alexsynt.ml) and evaluation of instructions (eval.ml). The head of each file should contain the open statements to load the modules required for compilation.

2.

Compile all files separately.

3.

Add a file mainbasic.ml which contains only the statement for calling the main function.

4.

Create a new toplevel with the name topbasic, which starts the Basic interpreter.

5.

Create a standalone executable which runs the Basic interpreter.

Comparison of Performance Try to compare the performance of code produced by the bytecode compiler and by the native compiler. For this purpose, write an application for sorting lists and arrays.

210

Chapter 7 : Compilation and Portability

1.

Write a polymorphic function for sorting lists. The order relation should be passed as an argument to the sort function. The sort algorithm can be selected by the reader. For example: bubble sort, or quick sort. Write this function as sort.ml.

2.

Create the main function in the file trilist.ml, which uses the previous function and applies it to a list of integers by sorting it in increasing order, then in decreasing order.

3.

Create two standalone executables - one with the bytecode compiler, and another with the native compiler. Measure the execution time of these two programs. Choose lists of sufficient size to get a good idea of the time differences.

4.

Rewrite the sort program for arrays. Continue using an order function as argument. Perform the test on arrays filled in the same manner as for the lists.

5.

What can we say about the results of these tests?

Summary This chapter has shown the different ways to compile an Objective Caml program. The bytecode compiler is favorable for portable code, allowing for the system independent distribution of programs and libraries. This property is lost in the case of standalone bytecode executables. The native compiler trades producing efficient architecture dependent code for a loss of portability.

To Learn More The techniques to compile for abstract machines were used in the first generation of SmallTalk, then in the functional languages LISP and ML. The argument that the use of abstract machines will hinder performance has put a shadow on this technique for a long time. Now, the JAVA language has shown that the opposite is true. An abstract machine provides several advantages. The first is to facilitate the porting of a compiler to different architectures. The part of the compiler related to portability has been well defined (the abstract machine interpreter and part of runtime library). Another benefit of this technique is portable code. It is possible to compile an application on one architecture and execute it on another. Finally, this technique simplifies compiler construction by adding specific instructions for the type of language to compile. In the case of functional languages, the abstract machines make it easy to create the closures (packing environment and code together) by adding the notion of execution environment to the abstract machine. To compensate for the loss in efficiency caused by the use of the bytecode interpreter, one can expand the set of abstract machine instructions to include those of a real machine at runtime. This type of expansion has been found in the implementation of Lisp (llm3) and JAVA (JIT). The performance increases, but does not reach the level of a native C compiler.

To Learn More

211

One difficulty of functional language compilation comes from closures. They contain both the executable code and execution environment (see page 23). The choice of implementation for the environment and the access of values in the environment has a significant influence on the performance of the code produced. An important function of the environment consists of obtaining access to values in constant time; the variables are viewed as indexes in an array containing their values. This requires the preprocessing of functional expressions. An example can be found in L. Cardelli’s book - Functional Abstract Machine. Zinc uses this technique. Another crucial optimization is to avoid the construction of useless closures. Although all functions in ML can be viewed as functions with only one argument, it is necessary to not create intermediate closures in the case of application on several arguments. For example, when the function add is applied with two integers, it is not useful to create the first closure corresponding to the function of applying add to the first argument. It is necessary to note that the creation of a closure would allocate certain memory space for the environment and would require the recovery of that memory space in the future (第 9 章参照). Automatic memory recovery is the second major performance concern, along with environment. Finally, bootstrapping allows us to write the majority of a compiler with the same language which it is going to compile. For this reason, like the chicken and the egg, it is necessary to define the minimal part of the language which can be expanded later. In fact, this property is hardly appreciable for classifying the languages and their implementations. This property is also used as a measure of the capability of a language to be used in the implementation of a compiler. A compiler is a large program, and bootstrapping is a good test of it’s correctness and performance. The following are links to the references: リンク: http://caml.inria.fr/camlstone.txt

At that time, Caml was compiled over fifty machines, these were antecedent versions of Objective Caml. We can get an idea of how the present Objective Caml has been improved since then.

8 Libraries Every language comes with collections of programs that are reusable by the programmer, called libraries. The quality and diversity of these programs are often some of the criteria one uses to assess the ease of use of a language. You could separate libraries into two categories: those that offer types and functions that are often useful but could be written in the language, and those that offer functionality that cannot be defined in the language. The first group saves the programmer the effort of redefining utilities such as stacks, lists, etc. The second group extends the possible uses of the language by incorporating new functionality into it. The Objective Caml language distribution comes with many precompiled libraries. For the curious reader, the uncompiled version of these libraries comes packaged with the source code distribution for the language. In Objective Caml, all the libraries are organized into modules that are also compilation units. Each one contains declarations of globals and types, exceptions and values that can be used in programs. In this chapter we are not interested in how to create new modules; we just want to use the existing ones. Chapter 14 will revisit the concepts of the module and the compilation unit while describing the module language of Objective Caml, including parameterized modules. Regarding the creation of libraries that incorporate code that is not written in Objective Caml, chapter 12 will describe how to integrate Objective Caml programs with code written in C. The Objective Caml distribution contains a preloaded library (the Pervasives module), a collection of basic modules called the standard library, and many other libraries adding functionality to the language. Some of the libraries are briefly shown in this chapter while others are described in later chapters.

214

Chapter 8 : Libraries

Chapter Outline This chapter describes the collection of libraries in the Objective Caml distribution. Some have been used in previous chapters, such as the Graphics library (see chapter 5), or the Array library. The first section shows the organization of the various libraries. The second section finishes describing the preloaded Pervasives module. The third section classifies the set of modules found in the standard library. The fourth section examines the high precision math libraries and the libraries for dynamically loading code.

Categorization and Use of the Libraries The libraries in the Objective Caml distribution fall into three categories. The first contains preloaded global declarations. The second is called the standard library and is subdivided into four parts: •

data structures;



input/output



system interface;



lexical and syntactic analysis.

Finally there are the libraries in the third group that generally extend the language, such as the Graphics library (see chapter 5). In this last group you will find libraries dealing with the following areas: regular expressions (Str), arbitrary-precision math (Num), Unix system calls (Unix), lightweight processes (Threads) and dynamic loading of bytecode (Dynlink). The I/O and the system interface portions of the standard library are compatible with different operating systems such as Unix, Windows and MacOS. This is not always the case with the libraries in the third group (those that extend the language). There are also many independently written libraries that are not part of the Objective Caml distribution. Usage and naming To use modules or libraries in a program, one has to use dot notation to specify the module name and the object to access. For example if one wants to use a function f in a library called Name, one qualifies it as Name.f. To avoid having to prefix everything with the name of the library, it is possible to open the library and use f directly. 構文 :

open Name

From then on, all the global declarations of the library Name will be considered as if they belonged to the global environment. If two declarations have the same name in two distinct open libraries, then only the last declaration is visible. To be able to call the first, it would be necessary to use the point notation.

Preloaded Library

215

Preloaded Library The Pervasives library is always preloaded so that it will be available at the toplevel (interactive) loop or for inline compilation. It is always linked and is the initial environment of the language. It contains the declarations of: •

type: basic types (int, char, string, float, bool, unit, exn, ’a array, ’a list) and the types ’a option (see page 223) and (’a, ’b, ’c) format (see page 267).



exceptions: A number of exceptions are raisable by the execution library. Some of the more common ones are the following: – Failure of string that is raised by the function failwith applied to a string. – Invalid argument of string that indicates that an argument cannot be handled by the function having raised the exception. The function invalid arg applied to a string starts this exception. – Sys error of string, for the input/output, typically in attempting to open a nonexistent file for reading. – End of file for detecting the end of a file. – Division by zero for zero divide errors between integers. As well as internal exceptions like: – Out of memory and Stack overflow for going beyond the memory of the heap or the stack. It should be noted that a program cannot recover from the Out of memory exception. In effect, when it is raised it is too late to allocate new memory space to continue functioning. Handling the Stack Overflow exception differs depending on whether the program was compiled in byte code or native code. In the latter case, it is not possible to recover.



functions: there are roughly 140, half of which correspond to the C functions of the execution library. There you may find mathematical and comparison operators, functions on integer and floating-point numbers, functions on character strings, on references and input-output. It should be noted that a certain number of these declarations are in fact synonyms for declarations defined in other modules. They are nevertheless declared here for historical and implementation reasons.

Standard Library The standard library contains a group of stable modules. These are operating system independent. There are currently 29 modules in the standard library containing 400 functions, 30 types of which half are abstract, 8 exceptions, 10 sub-modules, and 3 parameterized modules. Clearly we will not describe all of the declarations in all of these modules. Indeed, the reference manual [LRVD99] already does that quite well. Only those modules presenting a new concept or a real difficulty in use will be detailed.

216

Chapter 8 : Libraries

The standard library can be divided into four distinct parts: •

linear data structures (15 modules), some of which have already appeared in the first part;



input-output (4 modules), for the formatting of output, the persistence and creation of cryptographic keys;



parsing and lexical analysis (4 modules). They are described in chapter 11 (page 289);



system interface that permit communication and examination of parameters passed to a command, directory navigation and file access.

To these four groups we add a fifth containing some utilities for handling or creating structures such as functions for text processing or generating pseudo-random numbers, etc.

Utilities The modules that we have named ”utilities” concern: •

characters: the Char module primarily contains conversion functions;



object cloning: OO will be presented in chapter 15 (page 439), on object oriented programming



lazy evaluation: Lazy is first presented on page 105;



random number generator: Random will be described below.

Generation of Random Numbers The Random module is a pseudo-random number generator. It establishes a random number generation function starting with a number or a list of numbers called a seed. In order to ensure that the function does not always return the same list of numbers, the programmer must give it a different seed each time the generator is initialized. From this seed the function generates a succession of seemingly random numbers. Nevertheless, an initialization with the same seed will create the same list. To correctly initialize the generator, you need to find some outside resource, like the date represented in milliseconds, or the length of time since the start of the program. The functions of the module: •

initialization: init of type int -> unit and full init of type int array -> unit initialize the generator. The second function takes an array of seeds.



generate random numbers: bits of type unit -> int returns a positive integer, int of type int -> int returns a positive integer ranging from 0 to a limit given as a parameter, and float returns a float between 0. and a limit given as a parameter.

Standard Library

217

Linear Data Structures The modules for linear data structures are: •

simple modules: Array, String, List, Sort, Stack, Queue, Buffer, Hashtbl (that is also parameterized) and Weak;



parameterized modules: Hashtbl (of HashedType parameters), Map and Set (of OrderedType parameters).

The parameterized modules are built from the other modules, thus making them more generic. The construction of parameterized modules will be presented in chapter 14, page 423.

Simple Linear Data Structures The name of the module describes the type of data structures manipulated by the module. If the type is abstract, that is to say, if the representation is hidden, the current convention is to name it t inside the module. These modules establish the following structures: •

module Array: vectors;



module List: lists;



module String: character strings;



module Hashtbl: hash tables (abstract type);



module Buffer: extensible character strings (abstract type);



module Stack: stacks (abstract type);



module Queue: queues or FIFO (abstract type);



module Weak: vector of weak pointers (abstract type).

Let us mention one last module that implements linear data structures: •

module Sort: sorting on lists and vectors, merging of lists.

Family of common functions Each of these modules (with the exception of Sort), has functions for defining structures, creating/accessing elements (such as handler functions), and converting to other types. Only the List module is not physically modifiable. We will not give a complete description of all these functions. Instead, we will focus on families of functions that one finds in these modules. Then we will detail the List and Array modules that are the most commonly used structures in functional and imperative programming. One finds more or less the following functionality in all these modules: •

a length function that takes the value of a type and calculates an integer corresponding to its length;



a clear function that empties the linear structure, if it is modifiable;

218

Chapter 8 : Libraries



a function to add an element, add in general, but sometimes named differently according to common practice, (for example, push for stacks);



a function to access the n-th element, often called get;



a function to remove an element (often the first) remove or take.

In the same way, in several modules the names of functions for traversal and processing are the same: •

map: applies a function on all the elements of the structure and returns a new structure containing the results of these calls;



iter: like map, but drops successive results, and returns ().

For the structures with indexed elements we have: •

fill: replaces (modifies in place) a part of the structure with a value;



blit: copies a part of one structure into another structure of the same type;



sub: copies a part of one structure into a newly created structure.

Modules List and Array We describe the functions of the two libraries while placing an emphasis on the similarities and the particularities of each one. For the functions common to both modules, t designates either the ’a list or ’a array type. When a function belongs to one module, we will use the dot notation. Common or analogous functionality The first of them is the calculation of length. List.length

:

’a t -> int

Two functions permitting the concatenation of two structures or all the structures of a list. List.append List.concat

: :

’a t -> ’a t -> ’a t ’a t list -> ’a t

Both modules have a function to access an element designated by its position in the structure. List.nth Array.get

: :

’a list -> int -> ’a ’a array -> int -> ’a

The function to access an element at index i of a vector t, which is frequently used, has a syntactic shorthand: t.(i). Two functions allow you to apply an operation to all the elements of a structure.

Standard Library

219 iter map

: :

(’a -> unit) -> ’a t -> unit (’a -> ’b) -> ’a t -> ’b t

You can use iter to print the contents of a list or a vector. # let print content iter print item xs = iter (fun x → print string"("; print item x; print string")") xs; print newline () ; ; val print_content : ((’a -> unit) -> ’b -> ’c) -> (’a -> ’d) -> ’b -> unit = # print content List.iter print int [1;2;3;4;5] ; ; (1)(2)(3)(4)(5) - : unit = () # print content Array.iter print int [|1;2;3;4;5|] ; ; (1)(2)(3)(4)(5) - : unit = ()

The map function builds a new structure containing the result of the application. For example, with vectors whose contents are modifiable: # let a = [|1;2;3;4|] ; ; val a : int array = [|1; 2; # let b = Array.map succ a val b : int array = [|2; 3; # a, b; ; - : int array * int array =

3; 4|] ;; 4; 5|] ([|1; 2; 3; 4|], [|2; 3; 4; 5|])

Two iterators can be used to compose successive applications of a function on all elements of a structure. fold left fold right

: :

(’a -> ’b -> ’a) -> ’a -> ’b t -> ’a (’a -> ’b -> ’b) -> ’a t -> ’b -> ’b

You have to give these iterators a base case that supplies a default value when the structure is empty.

fold left f r [v1; v2; ...; vn] fold right f [v1; v2; ...; vn] r

= =

f ... ( f (f r v1) v2 ) ... vn f v1 ( f v2 ... (f vn r) ... )

These functions allow you to easily transform binary operations into n-ary operations. When the operation is commutative and associative, left and right iteration are indistinguishable: # List.fold left (+) 0 [1;2;3;4] ; ; - : int = 10 # List.fold right (+) [1;2;3;4] 0 ; ;

220

Chapter 8 : Libraries

- : int = 10 # List.fold left List.append [0] [[1];[2];[3];[4]] ; ; - : int list = [0; 1; 2; 3; 4] # List.fold right List.append [[1];[2];[3];[4]] [0] ; ; - : int list = [1; 2; 3; 4; 0]

Notice that, for binary concatenation, an empty list is a neutral element to the left and to the right. We find thus, in this specific case, the equivalence of the two expressions: # List.fold left List.append [] [[1];[2];[3];[4]] ; ; - : int list = [1; 2; 3; 4] # List.fold right List.append [[1];[2];[3];[4]] [] ; ; - : int list = [1; 2; 3; 4]

We have, in fact, found the List.concat function. Operations specific to lists. It is useful to have the following list functions that are provided by the List module: List.hd

:

List.tl

:

List.rev

:

List.mem

:

List.flatten

:

List.rev append

:

’a list -> ’a first element of the list ’a list -> ’a the list, without its first element ’a list -> ’a list reversal of a list ’a -> ’a list -> bool membership test ’a list list -> ’a list flattens a list of lists ’a list -> ’a list -> ’a list is the same as append (rev l1) l2

The first two functions are partial. They are not defined on the empty list and raise a Failure exception. There is a variant of mem: memq that uses physical equality. # let c = (1,2) ; ; val c : int * int = (1, 2) # let l = [c] ; ; val l : (int * int) list = [(1, 2)] # List.memq (1,2) l ; ; - : bool = false # List.memq c l ; ; - : bool = true

The List module provides two iterators that generalize boolean conjunction and disjunction (and / or): List.for all and List.exists that are defined by iteration:

Standard Library

221

# let for all f xs = List.fold right (fun x → fun b → (f x) & b) xs true ; ; val for_all : (’a -> bool) -> ’a list -> bool = # let exists f xs = List.fold right (fun x → fun b → (f x) or b) xs false ; ; val exists : (’a -> bool) -> ’a list -> bool =

There are variants of the iterators in the List module that take two lists as arguments and traverse them in parallel (iter2, map2, etc.). If they are not the same size, the Invalid argument exception is raised. The elements of a list can be searched using the criteria provided by the following boolean functions: List.find List.find all

: :

(’a -> bool) -> ’a list -> ’a (’a -> bool) -> ’a list -> ’a list

The find all function has an alias: filter. A variant of the general search function is the partitioning of a list: List.partition

:

(’a -> bool) -> ’a list -> ’a list * ’a list

The List module has two often necessary utility functions permitting the division and creation of lists of pairs: List.split List.combine

: :

(’a * ’b) list -> ’a list * ’b list ’a list -> ’b list -> (’a * ’b) list

Finally, a structure combining lists and pairs is often used: association lists. They are useful to store values associated to keys. These are lists of pairs such that the first entry is a key and the second is the information associated to the key. One has these data structures to deal with pairs: List.assoc

:

List.mem assoc

:

List.remove assoc

:

’a -> (’a * ’b) list -> ’b extract the information associated to a key ’a -> (’a * ’b) list -> bool test the existence of a key ’a -> (’a * ’b) list -> (’a * ’b) list deletion of an element corresponding to a key

Each of these functions has a variant using physical equality instead of structural equality: List.assq, List.mem assq and List.remove assq. Handlers specific to Vectors. The vectors that imperative programmers often use are physically modifiable structures. The Array module furnishes a function to change the value of an element:

222

Chapter 8 : Libraries Array.set

:

’a array -> int -> ’a -> unit

Like get, the set function has a syntactic shortcut: t.(i) <- a. There are three vector allocation functions: Array.create

:

Array.make

:

Array.init

:

int -> ’a -> ’a array creates a vector of a given size whose elements are all initialized with the same value int -> ’a -> ’a array alias for create int -> (int -> ’a) -> ’a array creates a vector of a given size whose elements are each initialized with the result of the application of a function to the element’s index

Since they are frequently used, the Array module has two functions for the creation of matrices (vectors of vectors): Array.create matrix Array.make matrix

: :

int -> int -> ’a -> ’a array array int -> int -> ’a -> ’a array array

The set function is generalized as a function modifying the values on an interval described by a starting index and a length: Array.fill

:

’a array -> int -> int -> ’a -> unit

One can copy a whole vector or extract a sub-vector (described by a starting index and a length) to obtain a new structure: Array.copy Array.sub

: :

’a array -> ’a array ’a array -> int -> int -> ’a array

The copy or extraction can also be done towards another vector: Array.blit

:

’a array -> int -> ’a array -> int -> int -> unit

The first argument is the index into the first vector, the second is the index into the second vector and the third is the number of values copied. The three functions blit, sub and fill raise the Invalid argument exception. The privileged use of indices in the vector manipulation functions leads to the definition of two specific iterators:

Standard Library

223

Array.iteri Array.mapi

: :

(int -> ’a -> unit) -> ’a array -> unit (int -> ’a -> ’b) -> ’a array -> ’b array

They apply a function whose first argument is the index of the affected element. let f i a = (string of int i) ^ ":" ^ (string of int a) in Array.mapi f [| 4; 3; 2; 1; 0 |] ; ; - : string array = [|"0:4"; "1:3"; "2:2"; "3:1"; "4:0"|]

#

Although the Array module does not have a function to modify the contents of all the elements in a vector, this effect can be easily obtained using iteri: # let iter and set f t = Array.iteri (fun i → fun x → t.(i) <- f x) t ; ; val iter_and_set : (’a -> ’a) -> ’a array -> unit = # let v = [|0;1;2;3;4|] ; ; val v : int array = [|0; 1; 2; 3; 4|] # iter and set succ v ; ; - : unit = () # v ;; - : int array = [|1; 2; 3; 4; 5|]

Finally, the Array module provides two list conversion functions: Array.of list Array.to list

: :

’a list -> ’a array ’a array -> ’a list

Input-output The standard library has four input-output modules: •

module Printf: for the formatting of output;



Format: pretty-printing facility to format text within “pretty-printing boxes”. The pretty-printer breaks lines at specified break hints, and indents lines according to the box structure.



module Marshal: implements a mechanism for persistent values;



module Digest: for creating unique keys.

The description of the Marshal module will be given later in the chapter when we begin to discuss persistent data structures (see page 228).

Module Printf The Printf module formats text using the rules of the printf function in the C language library. The display format is represented as a character string that will be

224

Chapter 8 : Libraries

decoded according to the conventions of printf in C, that is to say, by specializing the % character. This character followed by a letter indicates the type of the argument at this position. The following format "(x=%d, y=%d)" indicates that it should put two integers in place of the %d in the output string. Specification of formats. A format defines the parameters for a printed string. Those, of basic types: int, float, char and string, will be converted to strings and will replace their occurrence in the printed string. The values 77 and 43 provided to the format "(x=%d, y=%d)" will generate the complete printed string "(x=77, y=43)". The principal letters indicating the type of conversion to carry out are given in figure 8.1. Type

Letter

Result

integer

d or i u x X c s f e or E g or G

signed decimal unsigned decimal unsigned hexadecimal, lower case form same, with upper case letters character string decimal scientific notation same

b a or t

true or false functional parameter of type (out channel -> ’a -> unit) -> ’a -> unit or out channel -> unit

character string float

boolean special

図 8.1: Conversion conventions.

The format also allows one to specify the justification of the conversion, which allows for the alignment of the printed values. One can indicate the size in conversion characters. For this one places between the % character and the type of conversion an integer number as in %10d that indicates a conversion to be padded on the right to ten characters. If the size of the result of the conversion exceeds this limit, the limit will be discarded. A negative number indicates left justification. For conversions of floating point numbers, it is helpful to be able to specify the printed precision. One places a decimal point followed by a number to indicate the number of characters after the decimal point as in %.5f that indicates five characters to the right of the decimal point. There are two specific format letters: a and t that indicate a functional argument. Typically, a print function defined by the user. This is specific to Objective Caml.

Standard Library

225

Functions in the module in figure 8.2. fprintf printf eprintf sprintf bprintf

: : : : :

The types of the five functions in this module are given

out channel -> (’a, out channel, unit) format -> ’a (’a, out channel, unit) format -> ’a (’a, out channel, unit) format -> ’a (’a, unit, string) format -> ’a Buffer.t -> (’a, Buffer.t, string) format -> ’a 図 8.2: Printf formatting functions.

The fprintf function takes a channel, a format and arguments of types described in the format. The printf and eprintf functions are specializations on standard output and standard error. Finally, sprintf and bprintf do not print the result of the conversion, but instead return the corresponding string. Here are some simple examples of the utilization of formats. # Printf.printf "(x=%d, y=%d)" 34 78 ; ; (x=34, y=78)- : unit = () # Printf.printf "name = %s, age = %d" "Patricia" 18 ; ; name = Patricia, age = 18- : unit = () # let s = Printf.sprintf "%10.5f\n%10.5f\n" (-.12.24) (2.30000008) ; ; val s : string = " -12.24000\n 2.30000\n" # print string s ; ; -12.24000 2.30000 - : unit = ()

The following example builds a print function from a matrix of floats using a given format. # let print mat m = Printf.printf "\n" ; for i=0 to (Array.length m)-1 do for j=0 to (Array.length m.(0))-1 do Printf.printf "%10.3f" m.(i).(j) done ; Printf.printf "\n" done ; ; val print_mat : float array array -> unit = # print mat (Array.create 4 [| 1.2; -.44.22; 35.2 |]) ; ; 1.200 -44.220 1.200 -44.220 1.200 -44.220 1.200 -44.220 - : unit = ()

35.200 35.200 35.200 35.200

226

Chapter 8 : Libraries

Note on the format type. The description of a format adopts the syntax of character strings, but it is not a value of type string. The decoding of a format, according to the preceding conventions, builds a value of type format where the ’a parameter is instantiated either with unit if the format does not mention a parameter, or by a functional type corresponding to a function able to receive as many arguments as are mentioned and returning a value of type unit. One can illustrate this process by partially applying the printf function to a format: # let p3 = Printf.printf "begin\n%d is val1\n%s is val2\n%f is val3\n" ; ; begin val p3 : int -> string -> float -> unit =

One obtains thus a function that takes three arguments. Note that the word begin had already been printed. Another format would have given another type of function: # let p2 = Printf.printf "begin\n%f is val1\n%s is val2\n"; ; begin val p2 : float -> string -> unit =

In providing arguments one by one to p3, one progressively obtains the output. # let p31 = p3 45 ; ; 45 is val1 val p31 : string -> float -> unit = # let p32 = p31 "hello" ; ; hello is val2 val p32 : float -> unit = # let p33 = p32 3.14 ; ; 3.140000 is val3 val p33 : unit = () # p33 ; ; - : unit = ()

From the last obtained value, nothing is printed: it is the value () of type unit. One cannot build a format using values of type string: # let f d = Printf.printf (d^d); ; Characters 27-30: Printf.printf (d^d);; ^^^ This expression has type string but is here used with type (’a, out_channel, unit) format

The compiler cannot know the value of the string passed as an argument. It thus cannot know the type that instantiates the ’a parameter of type format.

Standard Library

227

On the other hand, strings are physically modifiable values, it would thus be possible to replace, for example, the %d part with another letter, thus dynamically changing the print format. This conflicts with the static generation of the conversion function.

Digest Module A hash function converts a character string of unspecified size into a character string of fixed length, most often smaller. Hashing functions return a fingerprint (digest) of their entry. Such functions are used for the construction of hash tables, as in the Hashtbl module, permitting one to rapidly test if an element is a member of such a table by directly accessing the fingerprint. For example the function f mod n, that generates the modulo n sum of the ASCII codes of the characters in a string, is a hashing function. If one creates an n by n table to arrange the strings, from the fingerprint one obtains direct access. Nevertheless two strings can return the same fingerprint. In the case of collisions, one adds to the hash table an extension to store these elements. If there are too many collisions, then access to the hash table is not very effective. If the fingerprint has a length of n bits, then the probability of collision between two different strings is 1/2n . A non-reversible hash function has a very weak probability of collision. It is thus difficult, given a fingerprint, to construct a string with this fingerprint. The preceding function f mod n is not, based on the evidence, such a function. One way hash functions permit the authentification of a string, that it is for some text sent over the Internet, a file, etc. The Digest module uses the MD5 algorithm, short for Message Digest 5. It returns a 128 bit fingerprint. Although the algorithm is public, it is impossible (today) to carry out a reconstruction from a fingerprint. This module defines the Digest.t type as an abbreviation of the string type. The figure 8.3 details the main functions of this module. string

:

file

:

string returns string returns

-> the -> the

t fingerprint of a string t fingerprint of a file

図 8.3: Functions of the Digest module.

We use the string function in the following example on a small string and on a large one built from the first. The fingerprint is always of fixed length. # let s = "The small cat is not dead..."; ; val s : string = "The small cat is not dead..." # Digest.string s; ; - : Digest.t = "\167h{Ic\223)
228

Chapter 8 : Libraries

for i=1 to 100 do r:= s^ !r done; String.escaped (Digest.string !r); ; - : string = "-\\012\\218\\210i\\196 \\137\\000M\\004\\015\\001\\251\\1404"

The creation of a fingerprint for a program allows one to guarantee the contents and thus avoids the use of a bad version. For example, when code is dynamically loaded (see page 242), a fingerprint is used to select the binary file to load. # Digest.file "basic.ml" ; ; - : Digest.t = "‘\189A]<\209\131)\193\023\166\161\227\017\024-"

Persistence Persistence is the conservation of a value outside the running execution of a program. This is the case when one writes a value in a file. This value is thus accessible to any program that has access to the file. Writing and reading persistent values requires the definition of a format for representing the coding of data. In effect, one must know how to go from a complex structure stored in memory, such as a binary tree, to a linear structure, a list of bytes, stored in a file. This is why the coding of persistent values is called linearization 1 .

Realization and Difficulties of Linearization The implementation of a mechanism for the linearization of data structures requires choices and presents difficulties that we describe below. •

read-write of data structures. Since memory can always be viewed as a vector of words, one value can always correspond to the memory that it occupies, leaving us to preserve the useful part by then compacting the value.



share or copy. Must the linearization of a data structure conserve sharing? Typically a binary tree having two identical children (in the sense of physical equality) can indicate, for the second child, that it has already saved the first. This characteristic influences the size of the saved value and the time taken to do it. On the other hand, in the presence of physically modifiable values, this could change the behavior of this value after a recovery depending on whether or not sharing was conserved.



circular structures. In the case of a circular value, linearization without sharing is likely to loop. It will be necessary to conserve sharing.



functional values. Functional values, or closures, are composed of an environment part and a code part. The code part corresponds to the entry point (address) of the code to execute. What must thus be done with code? It is possible to uniquely store this address, but thus only the same program will find the correct

1. JAVA uses the term serialization

Standard Library

229

meaning of this address. It is also possible to save the list of machine instructions of this function, but that would require having a mechanism to dynamically load code. •

guaranteeing the type when reloading. This is the main difficulty of this mechanism. Static typing guarantees that typed values will not generate type errors at execution time. But this is not true except for values belonging to the program during the course of execution. What type can one give to a value outside the program, that was not seen by the type verifier? Just to verify that the re-read value has the monomorphic type generated by the compiler, the type would have to be transmitted at the moment the value was saved, then the type would have to be checked when the value was loaded. Additionally, a mechanism to manage the versions of types would be needed to be safe in case a type is redeclared in a program.

Marshal Module The linearization mechanism in the Marshal module allows you to choose to keep or discard the sharing of values. It also allows for the use of closures, but in this case, only the pointer to the code is saved. This module is mainly comprised of functions for linearization via a channel or a string, and functions for recovery via a channel or a string. The linearization functions are parameterizable. The following type declares two possible options: type external_flag = No_sharing | Closures;; The No sharing constant constructor indicates that the sharing of values is not to be preserved, though the default is to keep sharing. The Closures constructor allows the use of closures while conserving its pointer to the code. Its absence will raise an exception if one tries to store a functional value. 警告

The Closures constructor is inoperative in interactive mode. It can only be used in command line mode.

The reading and writing functions in this module are gathered in figure 8.4. to channel to string to buffer from channel from string

: : : : :

out channel -> ’a -> extern flag list -> unit ’a -> extern flag list -> string string -> int -> int -> ’a -> extern flag list -> unit in channel -> ’a string -> int -> ’a 図 8.4: Functions of the Marshal module.

230

Chapter 8 : Libraries

The to channel function takes an output channel, a value, and a list of options and writes the value to the channel. The to string function produces a string corresponding to the linearized value, whereas to buffer accomplishes the same task by modifying part of a string passed as an argument. The from channel function reads a linearized value from a channel and returns it. The from string variant takes as input a string and the position of the first character to read in the string. Several linearized values can be stored in the same file or in the same string. For a file, they can be read sequentially. For a string, one must specify the right offset from the beginning of the string to decode the desired value. # let s = Marshal.to string [1;2;3;4] [] in String.sub s 0 10; ; - : string = "\132\149\166\190\000\000\000\t\000\000"

警告

Using this module one loses the safety of static typing (see infra, page 234).

Loading a persistent object creates a value of indeterminate type: # let x = Marshal.from string (Marshal.to string [1; 2; 3; 4] [] ) 0; ; val x : ’_a =

This indetermination is denoted in Objective Caml by the weakly typed variable ’ a. You should specify the expected type: # let l = let s = (Marshal.to string [1; 2; 3; 4] [] ) in (Marshal.from string s 0 : int list) ; ; val l : int list = [1; 2; 3; 4]

We return to this topic on page 234. 注意 The output value function of the preloaded library corresponds to calling to channel with an empty list of options. The input value function in the Pervasives module directly calls the from channel function. These functions were kept for compatibility with old programs.

Example: Backup Screens We want to save the bitmap, represented as a matrix of colors, of the whole screen. The save screen function recovers the bitmap, converts it to a table of colors and saves it in a file whose name is passed as a parameter. # let save screen name = let i = Graphics.get image 0 0 (Graphics.size x () ) (Graphics.size y () ) let j = Graphics.dump image i in let oc = open out name in output value oc j; close out oc; ; val save_screen : string -> unit =

in

Standard Library

231

The load screen function does the reverse operation. It opens the file whose name is passed as a parameter, restores the value stored inside, converts this color matrix into a bitmap, then displays the bitmap. # let load screen name = let ic = open in name in let image = ((input value ic) : Graphics.color array array) in close in ic; Graphics.close graph () ; Graphics.open graph (" "^(string of int(Array.length image.(0))) ^"x"^(string of int(Array.length image))); let image2 = Graphics.make image image in Graphics.draw image image2 0 0; image2 ; ; val load_screen : string -> Graphics.image =

警告

Abstract typed values cannot be made persistent.

It is for this reason that the preceding example does not use the abstract Graphics.image type, but instead uses the concrete color array array type. The abstraction of types is presented in chapter 14.

Sharing The loss of sharing in a data structure can make the structure completely lose its intended behavior. Let us revisit the example of the symbol generator from page 101. For whatever reason, we want to save the functional values new s and reset s, and thereafter use the current value of their common counter. We thus write the following program: # let reset s,new s = let c = ref 0 in ( function () → c := 0 ) , ( function s → c:=!c+1; s^(string of int !c) ) ; ; # let save = Marshal.to string (new s,reset s) [Marshal.Closures;Marshal.No sharing] ; ; # let (new s1,reset s1) = (Marshal.from string save 0 : ((string → string ) * (unit → unit))) ; ; # (* 1 *) Printf.printf "new_s : \%s\n" (new s "X"); Printf.printf "new_s : \%s\n" (new s "X"); (* 2 *) Printf.printf "new_s1 : \%s\n" (new s1 "X"); (* 3 *) reset s1 () ;

232

Chapter 8 : Libraries

Printf.printf "new_s1 (after reset_s1) : \%s\n" (new s1 "X") ; ; Characters 44-46: Warning: Illegal backslash escape in string Printf.printf "new_s : \%s\n" (new_s "X"); ^^ Characters 88-90: Warning: Illegal backslash escape in string Printf.printf "new_s : \%s\n" (new_s "X"); ^^ Characters 140-142: Warning: Illegal backslash escape in string Printf.printf "new_s1 : \%s\n" (new_s1 "X"); ^^ Characters 224-226: Warning: Illegal backslash escape in string Printf.printf "new_s1 (after reset_s1) : \%s\n" (new_s1 "X") ;; ^^ Characters 148-154: Printf.printf "new_s1 : \%s\n" (new_s1 "X"); ^^^^^^ Unbound value new_s1

The first two outputs in (* 1 *) comply with our intent. The output obtained in (* 2 *) after re-reading the closures also appears correct (after X2 comes X3). But, in fact, the sharing of the c counter between the re-read functions new s1 and reset s1 is lost, as the output of X4 attests that one of them set the counter to zero. Each closure has a copy of the counter and the call to reset s1 does not reset the new s1 counter to zero. Thus we should not have used the No sharing option during the linearization. It is generally necessary to conserve sharing. Nevertheless in certain cases where execution speed is important, the absence of sharing speeds up the process of saving. The following example demonstrates a function that copies a matrix. In this case it might be preferable to break the sharing: # let copy mat f (m : float array array) = let s = Marshal.to string m [Marshal.No sharing] in (Marshal.from string s 0 : float array array); ; val copy_mat_f : float array array -> float array array =

One can also use it to create a matrix without sharing: # let create mat f n m v = let m = Array.create n (Array.create m v) in copy mat f m; ; val create_mat_f : int -> int -> float -> float array array = # let a = create mat f 3 4 3.14; ; val a : float array array = [|[|3.14; 3.14; 3.14; 3.14|]; [|3.14; 3.14; 3.14; 3.14|];

Standard Library

233

[|3.14; 3.14; 3.14; 3.14|]|] # a.(1).(2) <- 6.28; ; - : unit = () # a; ; - : float array array = [|[|3.14; 3.14; 3.14; 3.14|]; [|3.14; 3.14; 6.28; 3.14|]; [|3.14; 3.14; 3.14; 3.14|]|]

Which is a more common behavior than that of Array.create, and resembles that of Array.create matrix.

Size of Values It may be useful to know the size of a persistent value. If sharing is conserved, this size also reflects the amount of memory occupied by a value. Although the encoding sometimes optimizes the size of atomic values2 , knowing the size of their respective encodings permits us to compare different implementations of a data structure. In addition, for programs that will never stop themselves, like embedded systems or even network servers; watching the size of data structures can help detect memory leaks. The Marshal module has two functions to calculate the size of a constant. They are described in figure 8.5. The total size of a persistent value is the same as the size of its header size data size total size

: : :

int string -> int -> int string -> int -> int

図 8.5: Size functions of Marshal.

data structures plus the size of its header. Below is a small example of the use of MD5 encoding to compare two representations of binary trees: # let size x = Marshal.data size (Marshal.to string x [] ) 0; ; val size : ’a -> int = # type ’a bintree1 = Empty1 | Node1 of ’a * ’a bintree1 * ’a bintree1 ; ; type ’a bintree1 = Empty1 | Node1 of ’a * ’a bintree1 * ’a bintree1 # let s1 = Node1(2, Node1(1, Node1(0, Empty1, Empty1), Empty1), Node1(3, Empty1, Empty1)) ; ; val s1 : int bintree1 = Node1 (2, Node1 (1, Node1 (0, Empty1, Empty1), Empty1), Node1 (3, Empty1, Empty1)) # type ’a bintree2 = 2. Arrays of characters, for example.

234

Chapter 8 : Libraries

Empty2 | Leaf2 of ’a | Node2 of ’a * ’a bintree2 * ’a bintree2 ; ; type ’a bintree2 = Empty2 | Leaf2 of ’a | Node2 of ’a * ’a bintree2 * ’a bintree2 # let s2 = Node2(2, Node2(1, Leaf2 0, Empty2), Leaf2 3) ; ; val s2 : int bintree2 = Node2 (2, Node2 (1, Leaf2 0, Empty2), Leaf2 3) # let s1, s2 = size s1, size s2 ; ; val s1 : int = 13 val s2 : int = 9

The values given by the size function reflect well the intuition that one might have of the size of s1 and s2.

Typing Problem The real problem with persistent values is that it is possible to break the type system of Objective Caml. The creation functions return a monomorphic type (unit or string). On the other hand unmarshalling functions return a polymorphic type ’a. From the point of view of types, you can do anything with a persistent value. Here is the usage that can be done with it (see chapter 2, page 57): create a function magic copy of type ’a -> ’b. # let magic copy a = let s = Marshal.to string a [Marshal.Closures] in Marshal.from string s 0; ; val magic_copy : ’a -> ’b =

The use of such a function causes a brutal halt in the execution of the program. # (magic_copy 3 : float) +. 3.1;; Segmentation fault In interactive mode (under Linux), we even leave the toplevel (interactive) loop with a system error signal corresponding to a memory violation.

Interface with the System The standard library has six system interface modules: •

module Sys: for communication between the operating system and the program;



module Arg: to analyze parameters passed to the program from the command line;



module Filename: operations on file names



module Printexc: for the interception and printing of exceptions;

Standard Library

235



module Gc: to control the mechanism that automatically deallocates memory, described in chapter 9;



module Callback: to call Objective Caml functions from C, described in chapter 12.

The first four modules are described below.

Module Sys This module provides quite useful functions for communication with the operating system, such as handling the signals received by a program. The values in figure 8.6 contain information about the system. OS type

:

interactive

:

word size

:

max string length

:

max array length

:

time

:

string type of system bool ref true if executing at the toplevel string size of a word (32 or 64 bits) int maximum size of a string int maximum size of a vector unit -> float gives the time in seconds since the start of the program

図 8.6: Information about the system.

Communication between the program and the system can go through the command line, the value of an environmental variable, or through running another program. These functions are described in figure 8.7. argv

:

getenv

:

command

:

string array contains the vector of parameters string -> string retrieves the value of a variable string -> int executes the command passed as an argument

図 8.7: Communication with the system.

The functions of the figure 8.8 allow us to navigate in the file hierarchy.

236

Chapter 8 : Libraries file exists

:

remove

:

rename

:

chdir

:

getcwd

:

string -> bool returns true if the file exists string -> unit destroys a file string -> string -> unit renames a file string -> unit change the current directory unit -> string returns the name of the current directory 図 8.8: File manipulation.

Finally, the management of signals will be described in the chapter on system programming (第 18 章参照). Here is a small program that revisits the example of saving a graphics window as an array of colors. The main function verifies that it is not started from the interactive loop, then reads from the command line the names of files to display, then tests if they exist, then displays them (with the load screen function). We wait for a key to be pressed between displaying two images. # let main () = if not (!Sys.interactive) then for i = 0 to Array.length(Sys.argv) -1 do let name = Sys.argv.(i) in if Sys.file exists name then begin ignore(load screen name); ignore(Graphics.read key) end done; ; Characters 235-252: Warning: this function application is partial, maybe some arguments are missing. ignore(Graphics.read_key) ^^^^^^^^^^^^^^^^^ val main : unit -> unit =

Module Arg The Arg module defines a small syntax for command line arguments. With this module, you can parse arguments and associate actions with them. The various elements of the command line are separated by one or more spaces. They are the values stored in the

Standard Library

237

Sys.argv array. In the syntax provided by Arg, certain elements are distinguished by starting with the minus character (-). These are called command line keywords or switches. One can associate a specific action with a keyword or take as an argument a value of type string, int or float. The value of these arguments is initialized with the value found on the command line just after the keyword. In this case one can call a function that converts character strings into the expected type. The other elements on the command line are called anonymous arguments. One associates an action with them that takes their value as an argument. An undefined option causes the display of some short documentation on the command line. The documentation’s contents are defined by the user. The actions associated with keywords are encapsulated in the type: type spec = | Unit of (unit → unit) | Set of bool ref | Clear of bool ref | String of (string → unit) | Int of (int → unit) | Float of (float → unit) | Rest of (string → unit)

(* Call the function with unit argument*) (* Set the reference to true*) (* Set the reference to false*) (* Call the function with a string argument *) (* Call the function with an int argument *) (* Call the function with a float argument *) (* Stop interpreting keywords and call the function with each remaining argument*)

The command line parsing function is: # Arg.parse ; ; - : (string * Arg.spec * string) list -> (string -> unit) -> string -> unit =

Its first argument is a list of triples of the form (key, spec, doc) such that: •

key is a character string corresponding to the keyword. It starts with the reserved character ’ ’.



spec is a value of type spec specifying the action associated with key.



doc is a character string describing the option key. It is displayed upon a syntax error.

The second argument is the function to process the anonymous command line arguments. The last argument is a character string displayed at the beginning of the command line documentation. The Arg module also includes: •

Bad: an exception taking as its argument a character string. It can be used by the processing functions.

238

Chapter 8 : Libraries



usage: of type (string * Arg.spec * string) list -> string -> unit, this function displays the command line documentation. One preferably provides it with the same arguments as those of parse.



current: of type int ref that contains a reference to the current value of the index in the Sys.argv array. One can therefore modify this value if necessary.

By way of an example, we show a function read args that initializes the configuration of the Minesweeper game seen in chapter 6, page 176. The possible options will be -col, -lin and -min. They will be followed by an integer indicating, respectively: the number of columns, the number of lines and the number of mines desired. These values must not be less than the default values, respectively 10, 10 and 15. The processing functions are: # let set nbcols cf n = cf := {!cf with nbcols = n} ; ; # let set nbrows cf n = cf := {!cf with nbrows = n} ; ; # let set nbmines cf n = cf := {!cf with nbmines = n} ; ;

All three are of type config ref -> int -> unit. The command line parsing function can be written: # let read args () = let cf = ref default config in let speclist = [("-col", Arg.Int (set nbcols cf), "number of columns (>=10)"); ("-lin", Arg.Int (set nbrows cf), "number of lines (>=10)"); ("-min", Arg.Int (set nbmines cf), "number of mines (>=15)")] in let usage msg = "usage : minesweep [-col n] [-lin n] [-min n]" in Arg.parse speclist (fun s → () ) usage msg; !cf ; ; val read_args : unit -> config =

This function calculates a configuration that will be passed as arguments to open wcf, the function that opens the main window when the game is started. Each option is, as its name indicates, optional. If it does not appear on the command line, the corresponding parameter keeps its default value. The order of the options is unimportant.

Module Filename The Filename module has operating system independant functions to manipulate the names of files. In practice, the file and directory naming conventions differ greatly between Windows, Unix and MacOS.

Other Libraries in the Distribution

239

Module Printexc This very short module (three functions described in figure 8.9) provides a general exception handler. This is particularly useful for programs executed in command mode3 to be sure not to allow an exception to escape that would stop the program. catch

:

print

:

to string

:

(’a -> ’b) -> ’a -> ’b general exception handler (’a -> ’b) -> ’a -> ’b print and re-raise the exception exn -> string convert an exception to a string

図 8.9: Handling exceptions.

The catch function applies its first argument to its second. This launches the main function of the program. If an exception arrives at the level of catch, that is to say that if it is not handled inside the program, then catch will print its name and exit the program. The print function has the same behavior as catch but re-raises the exception after printing it. Finally the to string function converts an exception into a character string. It is used by the two preceding functions. If we look again at the main function for displaying bitmaps, we might thus write an encapsulating function go in the following manner: # let go () = Printexc.catch main () ; ; val go : unit -> unit =

This permits the normal termination of the program by printing the value of the uncaptured exception.

Other Libraries in the Distribution The other libraries provided with the Objective Caml language distribution relate to the following extensions: •

graphics, with the portable Graphics module that was described in chapter 5;



exact math, containing many modules, and allowing the use of exact calculations on integers and rational numbers. Numbers are represented using Objective Caml integers whenever possible;

3. The interactive mode has a general exception handler that prints a message signaling that an exception was not handled.

240

Chapter 8 : Libraries



regular expression filtering, allowing easier string and text manipulations. The Str module will be described in chapter 11;



Unix system calls, with the Unix module allowing one to make unix system calls from Objective Caml. A large part of this library is nevertheless compatible with Windows. This bibliography will be used in chapters 18 and 20;



light-weight processes, comprising many modules that will largely be described and used in chapter 19;



access to NDBD databases, works only in Unix and will not be described;



dynamic loading of bytecode, implemented by the Dynlink module.

We will describe the big integer and dynamic loading libraries by using them.

Exact Math The big numbers library provides exact math functions using integers and rational numbers. Values of type int and float have two limitations: calculations on integers are done modulo the greatest positive integer, which can cause unperceived overflow errors; the results of floating point calculations are rounded, which by propagation can lead to errors. The library presented here mitigates these defects. This library is written partly in C. For this reason, you have to build an interactive loop that includes this code using the command: ocamlmktop -custom -o top nums.cma -cclib -lnums The library contains many modules. The two most important ones are Num for all the operations and Arith status for controlling calculation options. The general type num is a variant type gathering three basic types: type num = Int of int | Big int of big int | Ratio of ratio

The types big int and ratio are abstract. The operations on values of type num are followed by the symbol /. For example the addition of two num variables is written +/ and will be of type num -> num -> num. It will be the same for comparisons. Here is the first example that calculates the factorial: # let rec fact num n = if Num.(<=/) n (Num.Int 0) then (Num.Int 1) else Num.( */ ) n (fact num ( Num.(-/) n (Num.Int 1))); ; val fact_num : Num.num -> Num.num = # let r = fact num (Num.Int 100); ; val r : Num.num = Num.Big_int # let n = Num.string of num r in (String.sub n 0 50) ^ "..." ; ; - : string = "93326215443944152681699238856266700490715968264381..."

Other Libraries in the Distribution

241

Opening the Num module makes the code of fact num easier to read: # open Num ; ; # let rec fact num n = if n <=/ (Int 0) then (Int 1) else n */ (fact num ( n -/ (Int 1))) ; ; val fact_num : Num.num -> Num.num =

Calculations using rational numbers are also exact. If we want to calculate the number e by following the following definition: µ ¶m 1 e = limm→∞ 1 + m We should write a function that calculates this limit up to a certain m. # let calc e m = let a = Num.(+/) (Num.Int 1) ( Num.(//) (Num.Int 1) m) in Num.( **/ ) a m; ; val calc_e : Num.num -> Num.num = # let r = calc e (Num.Int 100); ; val r : Num.num = Ratio # let n = Num.string of num r in (String.sub n 0 50) ^ "..." ; ; - : string = "27048138294215260932671947108075308336779383827810..."

The Arith status module allows us to control some calculations such as the normalization of rational numbers, approximation for printing, and processing null denominators. The arith status function prints the state of these indicators. # Arith status.arith status () ; ; Normalization during computation --> OFF (returned by get_normalize_ratio ()) (modifiable with set_normalize_ratio ) Normalization when printing --> ON (returned by get_normalize_ratio_when_printing ()) (modifiable with set_normalize_ratio_when_printing ) Floating point approximation when printing rational numbers --> OFF (returned by get_approx_printing ()) (modifiable with set_approx_printing ) Error when a rational denominator is null --> ON (returned by get_error_when_null_denominator ()) (modifiable with set_error_when_null_denominator ) - : unit = ()

242

Chapter 8 : Libraries

They can be modified according to the needs of a calculation. For example, if we want to print an approximate value for a rational number, we can obtain, for the preceding calculation: # Arith status.set approx printing true; ; - : unit = () # Num.string of num (calc e (Num.Int 100)); ; - : string = "0.270481382942e1"

Calculations with big numbers take longer than those with integers and the values occupy more memory. Nevertheless, this library tries to use the most economical representations whenever possible. In any event, the ability to avoid the propagation of rounding errors and to do calculations on big numbers justifies the loss of efficiency.

Dynamic Loading of Code The Dynlink module offers the ability to dynamically load programs in the form of bytecode. The dynamic loading of code provides the following advantages: •

reduces the size of a program’s code. If certain modules are not used, they are not loaded.



allows the choice at execution time of which module to load. According to certain conditions at execution time you choose to load one module rather than another.



allows the modification of the behavior of a module during execution. Here again, under some conditions the program can load a new module and hide the old code.

The interactive loop of Objective Caml already uses such a mechanism. It is convenient to let the programmer have access to it as well. During the loading of an object file (with the .cmo extension), the various expressions are evaluated. The main program, that initiated the dynamic loading of the code does not have access to the names of declarations. Therefore it is up to the dynamically loaded module to update a table of functions used by the main program. 警告

The dynamic loading of code only works for object files in bytecode.

Description of the Module For dynamic loading of a bytecode file f.cmo, we need to know the access path to the file and the names of the modules that it uses. By default, dynamically loaded bytecode files do not have access to the paths and modules of the libraries in the distribution. Thus we have to add the path and the name of the required modules to the dynamic loading of the module. Many errors can occur during a request to load a module. Not only must the file exist with the right interface in one of the paths, but the bytecode must also be correct

Other Libraries in the Distribution init

:

add interfaces

:

loadfile

:

clear avalaible units

:

add avalaible units

:

allow unsafe modules

:

loadfile private

:

243

unit -> unit initialize dynamic loading string list -> string list -> unit add the names of modules and paths for loading string -> unit load a bytecode file unit -> unit empty the names of loadable modules and paths (string * Digest.t) list -> unit add the name of a module and a checksum† for loading without needing the interface file bool -> unit allow the loading of files containing external declarations string -> unit the loaded module is not accessible to modules loaded later



The checksum of an interface .cmi can be obtained from the extract crc command found in the catalog of libraries in the distribution.

図 8.10: Functions of the Dynlink module.

and loadable. These errors are gathered in the type error used as an argument to the Error exception and to the error function of type error -> string that allows the conversion of an error into a clear description.

Example To write a small program that allows us to illustrate dynamic loading of bytecode, we provide three modules: •

F that contains the definition of a reference to a function f;



Mod1 and Mod2 that modify in different ways the function referenced by F.f.

The F module is defined in the file f.ml: let g () = print string "I am the ’f’ function by default\n" ; flush stdout let f = ref g ; ;

;;

The Mod1 module is defined in the file mod1.ml: print string "The ’Mod1’ module modifies the value of ’F.f’\n" ; flush stdout ; ;

244

Chapter 8 : Libraries

let g () = print string "I am the ’f’ function of module ’Mod1’\n" ; flush stdout ; ; F.f := g ; ;

The Mod2 module is defined in the file mod2.ml: print string "The ’Mod2’ module modifies the value of ’F.f’\n" ; flush stdout ; ; let g () = print string "I am the ’f’ function of module ’Mod2’\n" ; flush stdout ; ; F.f := g ; ;

Finally we define in the file main.ml, a main program that calls the original function referenced by F.f, loads the Mod1 module, calls F.f again, then loads the Mod2 module and calls the F.f function one last time: let main () = try Dynlink.init () ; Dynlink.add interfaces [ "Pervasives"; "F" ; "Mod1" ; "Mod2" ] [ Sys.getcwd () ; "/usr/local/lib/ocaml/" ] ; !(F.f) () ; Dynlink.loadfile "mod1.cmo" ; !(F.f) () ; Dynlink.loadfile "mod2.cmo" ; !(F.f) () with Dynlink.Error e → print endline (Dynlink.error message e) ; exit 1 ; ; main () ; ;

The main program must, in addition to initializing the dynamic loading, declare by a call to Dynlink.add interfaces the interface used. We compile all of these modules: $ $ $ $

ocamlc ocamlc ocamlc ocamlc

-c -o -c -c

f.ml main dynlink.cma f.cmo main.ml f.cmo mod1.ml f.cmo mod2.ml

If we execute program main, we obtain: $ main I am the ’f’ function by default The ’Mod1’ module modifies the value of ’F.f’ I am the ’f’ function of module ’Mod1’ The ’Mod2’ module modifies the value of ’F.f’ I am the ’f’ function of module ’Mod2’

Exercises

245

Upon the dynamic loading of a module, its code is executed. This is demonstrated in our example, with the outputs beginning with The ’Mod.... The possible side effects that it contains are therefore reflected at the level of the program that caused the code to be loaded. This is why the different calls to F.f call different functions. The Dynlink library offers the basic mechanism for dynamically loading bytecode. The programmer still has to manage tables such that the loading will really be effective.

Exercises Resolution of Linear Systems This exercise revisits the resolution of linear systems presented as an exercise in the chapter on imperative programming (第 3 章参照). 1.

By using the Printf module, write a function print system that aligns the columns of the system.

2.

Test this function on the examples given on page 87.

Search for Prime Numbers The Sieve of Eratosthenes is an easily programmed algorithm that searches for prime numbers in a range of integers, given that the lower limit is a prime number. The method is: 1.

Enumerate, in a list, all the values on the range.

2.

Remove from the list all the values that are multiples of the first element.

3.

Remove this first element from the list, and keep it as a prime.

4.

Restart at step 2 as long as the list is not empty.

Here are the steps to create a program that implements this algorithm: 1.

Write a function range that builds a range of integers represented in the form of a list.

2.

Write a function eras that calculates the prime numbers on a range of integers starting with 2, according to the algorithm of the Sieve of Eratosthenes. Write a function era go that takes an integer and returns a list of all the prime numbers smaller than this integer.

3.

We want to write an executable primes that one will launch by typing the command primes n, where n is an integer. This executable will print the prime numbers smaller than n. For this we must use the Sys module and check whether a parameter was passed.

246

Chapter 8 : Libraries

Displaying Bitmaps Bitmaps saved as color array array are bulky. Since 24 bits of color are rarely used, it is possible to encode a bitmap in less space. For this we will analyze the number of colors in a bitmap. If the number is small (for example less than 256) we can encode each pixel in 1 byte, representing the number of the color in the table of colors of this bitmap. 1.

Write a function analyze colors exploring a value of type color array array and that returns a list of all the colors found in this image.

2.

From this list, construct a palette. We will take a vector of colors. The index in the table will correspond to the order of the color, and the contents are the color itself. Write the function find index that returns the index of a value stored in the array.

3.

From this table, write a conversion function, encode, that goes from a color array array to a string. Each pixel is thus represented by a character.

4.

Define a type image tdc comprising a table that matches colors to a vector of strings, allowing the encoding of a bitmap (or color array) using a smaller method.

5.

Write the function to image tdc to convert a color array array to this type.

6.

Write the function save image tdc to save the values to a file.

7.

Compare the size of the file obtained with the saved version of an equivalent palette.

8.

Write the function from image tdc to do the reverse conversion.

9.

Use it to display an image saved in a file. The file will be in the form of a value of type bitmap tdc.

Summary This chapter gave an overview of the different Objective Caml libraries presented as a set of simple modules (or compilation units). The modules for output formatting (Printf), persistant values (Marshal), the system interface (Sys) and the handling of exceptions (module Printexc) were detailed. The modules concerning parsing, memory management, system and network programming and light-weight processes will be presented in the following chapters.

To Learn More The overview of the libraries in the distribution of the language showed the richness of the basic environment. For the Printf module nothing is worth more than reading a work on the C language, such as [HS94]. In [FW00] a solution is proposed for the typing of intput-output of values (module Marshal). The MD5 algorithm of the Digest module is described on the web page of its designer:

To Learn More

247

リンク: http://theory.lcs.mit.edu/˜rivest/homepage.html

In the same way you may find many articles on exact arithmetic used by the num library on the web page of Val´erie M´enissier-Morain : リンク: http://www-calfor.lip6.fr/˜vmm/

There are also other libraries than those in the distribution, developed by the community of Objective Caml programmers. Objective Caml. The majority of them are listed on the “Camel’s hump” site: リンク: http://caml.inria.fr/humps/index.html

Some of them will be presented and discussed in the chapter on applications development (第 22 章参照). To know the exact contents of the various modules, don’t hesitate to read the description of the libraries in the reference manual [LRVD99] or consult the online version in HTML format (第 1 章参照). To enter into the details of the implementations of these libraries, nothing is better than reading the source code, available in the distribution of the language (第 1 章参照). Chapter 14 presents the language of Objective Caml modules. This allows you to build simple modules seen as independent compilation units, which will be similar to the modules presented in this chapter.

9 ガーベージコレクション マイクロプロセッサ上のプログラムの実行モデルは命令型プログラミング言語の実行モ デルと良く対応しています。命令型言語ではプログラムを、マシンのメモリ状態を変更 する命令の列として解釈します。メモリには主にプログラムによって設定された値が保 存されています。しかし他のリソースと同様にメモリは有限です。プログラムは OS に よって提供される以上のメモリを使うことはできません。このため将来の計算で必要と しない値が保存されているメモリ領域を再利用する必要が出てきます。このようなメモ リ管理はプログラムの実行と効率に大きな影響を持っています。 何らかの目的のためにメモリの一部を予約することを割り当て allocation と呼びます。 プログラムをロードする時に行われる静的な割り当てとプログラムの実行中に行われる 動的な割り当ては違います。静的に割り当てられるメモリはプログラムの実行中に解放 されませんが、動的に割り当てられる領域はプログラムの実行中に解放され、再利用さ れます。 メモリ管理をプログラマに任せることには 2 つの大きな問題があります。



まだ使っている値が含まれているメモリブロックを間違って解放してしまうとその 値をアクセスしようとした時に不具合を生じることがあります。このような解放さ れた領域内の値を指すポインタを揺れ動くポインタ dangling pointer と呼びます。



プログラムからメモリブロックのアドレスが失われてしまうとそのメモリブロック をプログラムの実行が終るまで解放できなくなってしまいます。このような状況を メモリリークと呼びます。

プログラマによる明示的なメモリ管理ではこの二つの問題を起こさないように注意を払わ なければなりません。この作業はプログラムが複雑なデータ構造を扱う時にはかなり困難 です。特にメモリブロックを共有しているデータ構造があるとより難しくなるでしょう。 自動メモリ管理機構は、プログラマからこの困難な作業を解放するために数多くのプロ グラミング言語に導入されてきました。自動メモリ管理の基本的な考え方は、動的に割 り当てられた値の中でプログラムに取って必要な値とはプログラムの実行状態から直接 または間接的に参照できる値である、というものです。言い替えるとプログラムから参

250

Chapter 9 : ガーベージコレクション

照できなくなってしまった値は解放し、再利用しても構わないということです。メモリ を実際に解放するタイミングは値に到達できなくなった瞬間でもいいですし、後でメモ リが必要になった時点でも構いません。

Objective Caml では自動メモリ管理のためにガーベージコレクション(GC) と呼ばれる 機構を備えています。メモリはデータ構造が作られる時 (コンストラクタが呼ばれる時) に確保され、暗黙のうちに解放されます。ガーベージコレクションは舞台裏で自動的に 機能しているのでほとんどのプログラムでは明示的にガーベージコレクションを扱う必 要はありません。しかしメモリを頻繁に利用するプログラムではガーベージコレクショ ンの動作が実行効率に大きな影響を与える場合があります。このような場合にはプログ ラマが GC のパラメータを制御し、明示的にコレクタを呼び出せるようにした方が有益 です。また Objective Caml から他の言語を呼び出す時 (第 12 章参照) にはガーベージコ レクタが値の表現に対して課している制限を良く理解しなければなりません。

この章のあらまし この章では動的なメモリ割り当て戦略とガーベージコレクションのアルゴリズムについ て解説します。Objective Caml で採用されているガーベージコレクションはこの章で示 されたアルゴリズムを組み合わせたものになっています。最初の節ではメモリの種類と 性質について背景知識を説明しています。第二節ではメモリ割り当てについて扱い、明 示的な解放と暗黙の解放の比較を行います。第三節では代表的な GC のアルゴリズムを 紹介します。第四節では Objective Caml のアルゴリズムについて詳細に説明します。第 五節ではヒープ領域を制御する Gc モジュールの使い方について紹介します。第六節では キャッシュを実装するのに使われる弱いポインタ weak pointer の使い方について紹介し ます。

プログラムの使用するメモリ 機械語コードのプログラムとはメモリ上の値を操作する命令列です。メモリとは一般的 に以下の要素から成り立っています。



プロセッサレジスタ



スタック



データセグメント (静的に割り当てられる領域)



ヒープ領域 (動的に割り当てられる領域)

スタックと動的に割り当てられる領域のみがプログラムの実行中に大きさを変更できま す。メモリの種類によって制御の仕方と機能は変化します。メモリの種類毎に行える制 御はプログラム言語と OS に依存して変わります。プログラム命令列は通常静的なメモ リに置かれますが、動的リンク (242 ページ参照) では動的なメモリを利用します。

251

メモリの割り当てと解放

メモリの割り当てと解放 C, Pascal, Lisp, ML, SmallTalk, C++, Java, ADA 等のほとんどのプログラミング言語 では動的なメモリの割り当ての機能を持っています。

明示的な割り当て メモリの割り当てには次の 2 種類の方式があります。



メモリの内容について考慮せず、一定のサイズの領域を予約する方式。



メモリの内容を初期化して、一定のサイズの領域を予約する方式。

最初の方式は Pascal の new 関数や C 言語の malloc 関数によって採用されている方式で す。これらの関数は、内容を読みだしたり更新できるメモリ領域を指すポインタ (アドレ ス) を返します。2 番目の方式は Objective Caml や Lisp 等の言語またはオブジェクト指 向言語で値を構築する操作に対応しています。オブジェクト指向言語の new 関数は値の 初期化に必要なパラメータを引数として受け取り、値が設定されたクラスのインスタン スを返り値として返します。関数型言語では構造的な値 (組、リスト、レコード、配列、 関数クロージャ) が定義される地点で値が構築されます。

Objective Caml での値の構築の仕方について具体的に調べてみましょう。図 9.1 にメモ リ上での値の表現例を示します。

l

u

’c’

’a’

’z’

’m’

’z’

rp 図 9.1: メモリ上の値の表現 # let u = let l = [’c’; ’a’; ’m’] in List.tl l ; ; val u : char list = [’a’; ’m’] # let v = let r = ( [’z’] , u ) in match r with p → (fst p) @ (snd p) ; ;

v

252

Chapter 9 : ガーベージコレクション

val v : char list = [’z’; ’a’; ’m’]

リストの要素は 2 ワードの組によって表現されています。最初の要素は文字であり、2 番目の要素はリストの中の次の要素へのポインタです。実際の実行時の表現は少し違っ ていますが、その違いは C とのインターフェースについて扱う章 (317 ページ参照) で説 明します。 変数 l の定義により個々の要素 [’c’;’a’;’m’] のためのセル (コンストラクタ ::) がメモ リ上に確保されます。変数 u は変数 l の指すセルの末尾のセルに対応しています。すな わち変数 l と変数 u の指す対象は一部共有されています。これは関数 List.tl の引数と 返り値が持つ性質のためです。 ただし最初の文の評価の後にアクセスできるのは変数 u だけです。 二番目の文では要素が 1 つだけのリストが構築され、次にそのリストと変数 u が指すリ ストの組が構築され、変数 r に束縛されます。この組はパターンマッチの結果変数 p に 束縛されます。次に変数 p の最初の要素と二番目の要素がリストとして結合され、結果 として [’z’;’a’;’m’] というリストを作り出し、大域変数 v に結びつけられます。ここ で注意して欲しいのは関数 snd の結果 (リスト [’a’;’m’]) は引数の変数 p と共有されて いますが、関数 fst の結果 (文字’z’) はコピーされているということです。 この例ではメモリ割り当てはすべて明示的でした。言い替えるとプログラマによって構 文的に指定されたものでした。 注意 図中には明示されていませんが、割り当てられたメモリ領域には後で解放 できるようにサイズ情報が書き込まれています。

明示的な解放 明示的にメモリ領域を解放する言語では、メモリ領域のアドレスを受け取ってその領域 を解放する演算子 (C 言語の free や Pascal の dispose) があります。メモリ割り当ての 時に書き込まれた情報を利用してプログラムは指定された領域を解放し、後に再利用で きるようにします。 動的な割り当ては一般にリストや木などの構造が変化するデータを操作する時に利用さ れています。そのような複雑なデータ構造を含む領域を一挙に解放することはできませ ん。複雑なデータ構造のすべての要素を辿る関数が必要になります。ここではそのよう な関数をデストラクタと呼びます。 デストラクタを正しく定義するのはそれほど難しくありませんが、極めて慎重に呼び出 す必要があります。データ構造を含むメモリを実際に解放するにはデータ構造を辿りな がら言語が持つ解放のための関数を呼び出さなければなりません。メモリを解放する責 任をプログラマに任せれば、プログラマはメモリ解放の動作を把握することができます が、メモリの解放の仕方を間違った場合にプログラムの実行に対して深刻な影響が出る 危険性を生じます。明示的なメモリ解放の持つ主な危険性とは以下のようなものです。



揺れ動くポインタ: あるメモリ領域を指すポインタが存在しているにも関わらず、 そのメモリ領域を解放してしまった時にそのポインタのことを揺れ動くポインタ

ガーベージコレクション

253

と呼びます。そのメモリ領域が後に再利用されるとその領域に保存された値の一貫 性が失われる危険性があります。



アクセスできないメモリ領域 (メモリリーク): メモリ領域は割り当てられたままで あるが、その領域を指すポインタが失われてしまった状態。このような場合、その 領域を解放する手段がなくなってしまいます。明らかにメモリの無駄になります。

明示的なメモリの解放の問題点は、プログラム中で使われる値の生存期間を正確に知る ことが難しいという問題に起因しています。

暗黙の解放 暗黙のメモリの解放の機能を持つ言語にはメモリを解放する演算子がありません。つま りプログラマは割り当てられた領域を解放できません。その代わりに言語に自動的にメ モリを再利用する機能があり、値が参照されなくなってしまった時や新しいメモリを割 り当てることができなくなった時 (ヒープがいっぱいになった時) に自動的に作動します。 自動的にメモリを再利用するアルゴリズムは大域的なデストラクタとも考えることがで きます。この性質のため特定のデータ構造のためのデストラクタよりも設計と実装が困 難です。しかし、一度正確に実装してしまえばメモリ管理の安全性が飛躍的に高まりま す。とりわけ揺れ動くポインタの危険性は消滅します。 その上、自動メモリ管理機構はヒープに対して良い性質を与えます。



稠密性: 不用なメモリを再利用した後は値は一つの連続したメモリ領域に保存され ています。したがってメモリのフラグメンテーションの問題はありません。また ヒープに残されている最大の連続領域を割り当てることができます。



局所性: 同じ値を構成する要素はメモリアドレスという意味で近い場所に置かれて います。したがって同じメモリページに含まれている可能性が高く、キャッシュの 効果を高めます。

ガーベージコレクタを設計する際にはいくつかの基準と制限を考慮に入れなければなり ません。



メモリ利用率: 未使用のメモリ領域を何%にすべきか?



フラグメンテーション: 連続領域として未使用の領域全体を割り当てることができ るか?



割り当てと回収の速度



値の表現の自由度

実際の言語では安全性の要請が最も重要です。ガーベージコレクタの設計ではこれらの 矛盾する要求の中で折衷案を探さなければなりません。

ガーベージコレクション メモリを自動的に再利用するアルゴリズムには大きく分けて 2 つあります。

254

Chapter 9 : ガーベージコレクション



参照カウント: 個々の割り当て領域は自分を指している参照がいくつあるのか知っ ています。参照数が 0 になった時、すなわち誰からも参照されなくなった時にその 領域を解放します。



スイープアルゴリズム: ルートと呼ばれる参照の集合から辿ることのできるすべて の値を、ちょうど有向グラフのすべてのノードを辿るやり方と同じように辿る方式。

スイープアルゴリズムの方がプログラミング言語では良く使われています。参照カウン ト方式のガーベージコレクタではメモリ領域を回収しない時でも参照カウントを更新す るコストがかかります。また相互参照するメモリ領域をうまく回収できないことが知ら れています。

参照カウント 個々の割り当て領域 (オブジェクト) はカウンタを持っています。このカウンタは自分を 指すポインタの数を表しています。オブジェクトを指す参照が増えるごとにカウンタの 値に 1 を加えます。オブジェクトを指す参照が消滅するとカウンタの値から 1 を引きま す。カウンタの値が 0 になった時にそのオブジェクトは回収されます。 この方式の利点は、オブジェクトが使われなくなった瞬間に解放できることです。カウ ンタを増減するためのオーバヘッドがかかること以外にもこの方式には欠点があります。 相互参照するオブジェクトを扱うのが難しいという点です。以下の例では循環する値 l を一時的に作ります。l は文字を含むリストですが、最後の要素は最初の文字’c’ を含 むセルを指しています。明らかにこの値は循環しています (図 9.2 参照)。 # let rec l = ’c’ :: ’a’ :: ’m’ :: l in List.hd l ; ; - : char = ’c’

この文を評価した後、変数 l は無効になりますがリストに含まれるすべての要素のカウ

l

’c’

’a’

’m’

図 9.2: 循環リストのメモリ表現 ンタは 1 に等しくなります (最初の要素は最後の要素から指されているため)。この循環 リストへはもはやアクセスできなくなりますが、カウンタが 0 でないので回収もできま せん。参照カウントを使ったメモリ回収を行い、循環する値を作り出せる言語— 例えば Python — ではスイープアルゴリズムを併用する必要があります。

255

ガーベージコレクション

スイープアルゴリズム スイープアルゴリズムではヒープ上の値のグラフ構造を探索します。グラフ上の探索は ルートと呼ばれる参照の集合から開始されます。ルートとはヒープの外部のデータ構造 であり、多くの場合スタックを含んでいます。図 9.1 の例ではトップレベルから参照で きる変数 u と変数 v をルートであると仮定します。ルートから参照できるオブジェクト が保存しなければならないオブジェクトです。図 9.3 では太線で表されています。

l

u

’c’

’a’

’m’

’z’

’z’

r p

v

図 9.3: ガーベージコレクション後のメモリの再利用 グラフを辿るためにはヒープ上で整数等の即値とポインタを区別しなければなりません。 オブジェクトに含まれている値が整数であれば、その値をアドレスであると思って辿っ てはいけません。多くの関数型言語ではこの区別は個々のセルの数ビットを使って表現 します。このビットをタグビットと呼びます。Objective Caml で整数が 31 ビットなの はこのためです。値の表現法については第 12 章 (327 ページ) でも議論しています。ポイ ンタと即値を区別する別の方法についてこの章の 262 ページで紹介します。 良く使われている代表的なアルゴリズムはマーク・アンド・スイープ とストップ・アン ド・コピー です。マーク・アンド・スイープはヒープ領域の未使用のオブジェクトのリ スト (フリーリスト) を作ります。ストップ・アンド・コピーは生存しているオブジェク トを新しいメモリ領域にコピーしていきます。 ヒープは値を保存できる箱の列と見なすことができます。図 9.1 の例でのヒープの状態 は図 9.4 のように表現できます。 スイープアルゴリズムを比較するのに以下の基準を使います。



効率性: 計算量はヒープの大きさに依存するべきか、生存しているオブジェクトの 数に依存するべきか?



回収率: 未使用のメモリをすべて回収するべきか?



稠密性: 未使用のメモリを連続領域にするべきか?

256

Chapter 9 : ガーベージコレクション

HEAP

u v roots

’c’

’a’

’m’

’z’

’z’

図 9.4: ヒープの状態



局所性: 構造を持った値の中のセルはメモリ上で近いアドレスに置くべきか?



補助メモリの必要性: 補助的なメモリが必要か?



再配置: 値が置かれるアドレスを変えても良いか?

局所性があれば構造を持った値は同じメモリページに置かれます。稠密性があればフラ グメンテーションは起こらず、計算に充分な量以上のメモリを使わずに済みます。効率 性、回収率、補助メモリの必要性はアルゴリズムの時間空間計算量と密接な関係を持っ ています。

マーク・アンド・スイープ マーク・アンド・スイープの基本的なアイデアはヒープ上のすべての未使用のオブジェ クトをリストにして (フリーリスト) 管理するというものです。新しいメモリを割り当て る時にはフリーリストから適切なサイズの領域を割り当てます。もしフリーリストが空 だったり、あるいは充分なサイズの領域がなかった場合にはマーク・アンド・スイープ が行われます。 マーク・アンド・スイープは二段階のアルゴリズムです。

1.

マークフェーズ: ルートから到達できるメモリ領域を使用中と見倣してマークを付 ける。

2.

スイープフェーズ: ヒープをアドレス順に調べ、マークの付いていないメモリ領域 をフリーリストに加える。

マーク・アンド・スイープによるメモリ管理はヒープ上のセルを 4 色 (白、灰色1 、黒、 斜線) に塗り分けて考えると分かりやすくなります。灰色はマークフェーズでのみ使いま す。斜線のセルはスイープフェーズで作られます。白のセルは割り当て時に作られます。

The meaning of the gray and black used by marking is as follows:

1. オンラインバージョンでは灰色は青みがかって見えます。

257

ガーベージコレクション



灰色: 自分から辿れるセルをまだマークしていない。



黒: 自分から辿れるセルはすべてマークしてある。

すべてのセルを辿ったかどうか確かめるためにはすべての灰色のセルを何らかの方法で 覚えておかなくてはなりません。マークフェーズが終った段階ですべてのセルは白か黒 のどちらかに塗り分けられています。黒いセルはルートから直接的または間接的に参照 できるセルです。図 9.5 にマークの途中の状態を示します。ルートの一つである u はす でに辿ってしまっていて、次に v を辿ろうとしている状態です。 HEAP black

u v roots

grey ’c’

’a’

’m’

’z’

white

’z’

free list

図 9.5: マークフェーズ フリーリストが形成されるのはスイープフェーズです。スイープフェーズでは次のよう な規則にしたがって色を書き換えます。



生存しているセルである黒を白に書き換えます。



白いセルを斜線に書き換え、フリーリストに加えます。

図 9.6 に色を書き換え、フリーリストを作っている途中の状態を示します。 handled part of the heap HEAP

u v roots

            ’c’

’a’

’m’

            ’z’

’z’

free list

図 9.6: スイープフェーズ マーク・アンド・スイープには以下のような特徴があります。



black grey white hatched

258

Chapter 9 : ガーベージコレクション



スイープフェーズの計算量はヒープ全体のサイズに依存しています。



未使用のメモリはすべて回収されます。



未使用のメモリは必ずしも連続領域になりません。



データの局所性を保証しません。



値の置かれるアドレスを変更しません。

マークフェーズは一般に再帰関数を使って実装されます。これは実行スタックのスペー スを事実上利用していることになります。実行スタックを無制限に使わないようにマー ク・アンド・スイープ のアルゴリズムを記述することは可能ですが、再帰関数を使った 場合と比べて効率的に書きにくいことが分かっています。 マーク・アンド・スイープでは値の大きさを知る必要があります。値の大きさは値の中 に埋め込むことも可能ですし、ヒープ領域を分割してサイズごとに別のページに保存す るようにして、値の大きさをアドレスから計算できるようにすることも可能です。最初 期の Lisp に対して設計されたマーク・アンド・スイープのアルゴリズムは今でも広く 使われています。Objective Caml のガーベージコレクタの一部でもマーク・アンド・ス イープのアルゴリズムを使っています。

ストップ・アンド・コピー このガーベージコレクタでは値を詰めてコピーするための二次メモリ領域を使用します。 ヒープは同じ大きさの二つの領域に分割されます。使用中の領域を起領域 from-space、 コピーされる領域を至領域 to-space と呼びます。 HEAP

u v roots

’c’

’a’

’m’

’z’

’z’

from-space

to-space

already handled

free box

図 9.7: ストップ・アンド・コピーの開始直前の状態 アルゴリズムは以下のように進みます。まず起領域の中で、ルートから辿れる値を至領 域にコピーします。値の置かれるアドレスは、その値を指すポインタを正しく書き換え ることができるように (多くの場合、古い値の領域に) 保存されます。

259

ガーベージコレクション HEAP

u v roots

’c’

  

’m’

’z’

’z’

to-space

’a’

already handled

from-space

free box

図 9.8: 起領域から至領域への値のコピー 新しく至領域に書き込まれた値もルートとして扱います。まだ処理されていないルート がある限り、値のコピーは続きます。 HEAP

u v roots

’c’

’a’

           

’z’

’z’

from-space

to-space

’m’

already handled

free box

図 9.9: 新しいルート 共有されている値があった場合、言い替えると既にコピーした値を指すポインタがあっ た場合には新しいアドレスを指すように書き換えます。

260

Chapter 9 : ガーベージコレクション HEAP

u v roots

’c’

’a’

           

’m’

’z’

   

from-space

to-space

’z’

already handled

free box

図 9.10: 値の共有 ガーベージコレクションが終了するとすべてのルートの値は更新され、新しい領域のア ドレスを参照しています。最後に次のガーベージコレクションに備えて二つの領域の役 割を入れ換えます。 HEAP

u v roots

’c’

’a’

                   

’m’

’z’

    

to-space

from-space

’z’

free box

図 9.11: 領域の役割の交換 このガーベージコレクタには次のような特徴があります。



計算量は生存している値の大きさに依存します。



メモリの半分しか利用できません。

ガーベージコレクション

261



未使用のメモリは一つの連続領域となります。



(幅優先探索を行えば) 強い局所性があります。



スタックのような不定長の領域を必要としません。(ただしメモリを 2 分割したこ とは除外して考えます。)



再帰的なアルゴリズムではありません。



値の置かれるアドレスを変更します。

他のガーベージコレクションアルゴリズム 他にも数多くのガーベージコレクションのアルゴリズムが利用されています。多くのも のは今紹介した 2 種類のアルゴリズムを拡張したものです。数式処理システムでの巨大 配列の操作など、特定のアプリケーションに対して特化されたアルゴリズムやコンパイ ラ技術と統合させたものなどがあります。多世代ガーベージコレクションは値の寿命に 応じた最適化を考えた方式です。保守的ガーベージコレクションは即値とポインタの区 別が難しい場合 (例えば C 言語へ変換することによってコンパイルする言語) に使われ ています。また漸進的ガーベージコレクションはガーベージコレクションが起動された 時の待ち時間を減らすことができます。

多世代ガーベージコレクション 関数型言語は頻繁にメモリの確保を行う傾向があります。しかもほとんどの値の生存期 間は非常に短いことが分かっています2 。一方、数回のガーベージコレクションで回収さ れなかった値はその後非常に長い期間生存する傾向があります。メモリを再利用をする 時にマーク・アンド・スイープのようにヒープ全体を辿らずに済ませるために、長い生 存期間を持ちガーベージコレクションで回収される可能性の少ない値は辿らないように する方式が考えられます。オブジェクトに作成時間やガーベージコレクションを経た回 数などの情報を書き込んでおき、値の年齢に応じて別のアルゴリズムを使うことができ ます。



若いオブジェクトに対しては高速で、若い世代のみを辿るガーベージコレクション を行います。



古いオブジェクトに対するガーベージコレクションは少ない頻度で行い、(少ない と予測される) 使わなくなったオブジェクトを回収するアルゴリズムを使います。

年を取れば取るほどガーベージコレクションで回収されにくくなります。この方式で困 難なのは、若いオブジェクトが保存されているメモリ領域のみを辿るようにする方法で す。純関数型言語、すなわち代入を持たない言語では若いオブジェクトは古いオブジェ クトへの参照を持っていますが、古いオブジェクトは若いオブジェクトへの参照を持っ ていません。それは、古いオブジェクトは若いオブジェクトが作られる前に作られてい るからです。したがって古いオブジェクトを探索せずに、すべての若いオブジェクトを 探索できます。このような理由から、多世代ガーベージコレクションは純関数型言語に 適しています。ただし遅延評価を行う言語では、構造の中身よりも構造の評価を先に行

2. ほとんどの値は最初のガーベージコレクションで回収されてしまいます。

262

Chapter 9 : ガーベージコレクション

う時があるため古いオブジェクトが若いオブジェクトを参照する場合があり得ます。一 方、代入のある関数型言語では古いオブジェクトから若いオブジェクトを指す参照を簡 単に作ることができます。そのため古いオブジェクトから指されている若いオブジェク トのリストを常に管理する必要があります。Objective Caml で採用しているアルゴリズ ムはこの後の節で解説します。

保守的ガーベージコレクション ここまで出て来たガーベージコレクションの手法ではポインタと即値を区別することが 可能であるという前提でアルゴリズムが作られていました。ところが、パラメトリック な多相性を持った関数型言語では値が単一な表現 (一般にメモリの 1 ワード) で表されて います。3 。これは、多相的な関数を型に依存しない一般的なコードで表すために必要な 制限です。 しかし単一な表現を使ってしまうと整数とポインタの区別を付けるのが難しくなります。 このような場合は保守的なガーベージコレクタを使えば、整数のような即値をポインタ として誤ってマークせずに済みます。保守的なガーベージコレクタではタグビットを使 わずにメモリの 1 ワードすべてを値の表現に使えます。実際は整数を表している値をポ インタとして誤って辿らないようにするために以下のような観察に基づいた、即値とポ インタを区別するアルゴリズムを使います。



ヒープ領域の開始アドレスと終了アドレスの範囲外の値は即値と判断します。



すべての割り当てられたオブジェクトはワード境界に置かれています。ワード境界 に置かれていない値は即値と判断します。

したがってヒープ上の値はヒープ内の有効なアドレスであれば、例えその値が実際は整 数であったとしても、ポインタと判断されその指す領域は回収されません。メモリペー ジごとに同じサイズのオブジェクトを割り当てるようにすれば、即値が有効なアドレス と判断される場合を減らすことができます。未使用領域をすべて回収することはもちろ ん保証できません。これが保守的なガーベージコレクションの第一の欠点です。しかし、 回収されるのは未使用な領域であることは確かです。 一般的に保守的なガーベージコレクションでは値を再配置しません。保守的なガーベー ジコレクタは即値をポインタと判断することがあるため値の移動は危険な場合がありま す。それでもルートを作成するアルゴリズムを工夫することで、(明らかにポインタであ る) ルートに関しては再配置を行えます。 曖昧なルートに対するガーベージコレクションの技術は関数型言語を、ポータブルなア センブラとしての C 言語にコンパイルした場合にしばしば必要になります。保守的な ガーベージコレクションでは C 言語のポインタと即値の表現をそのまま使うことができ ます。

3. Objective Caml では一つだけ例外があり、浮動小数点数の配列では別の表現を使います。第 12 章 333 ページ参照。

Objective Caml でのメモリ管理

263

漸進的ガーベージコレクション ガーベージコレクションに対する批判として良くあるものの一つに、ガーベージコレク ションがユーザがはっきり知覚できる時間、プログラムの実行を一時的に停止させてしま う、という批判があります。予測できないタイミングで実行の停止と再開が頻繁に起こ るのは、リアルタイムゲームのような高速なインタラクションを行うアプリケーション のユーザにとって受け入れられるものではありません。またガーベージコレクションの 停止時間は数秒に達することもあり、制限時間内にイベントを処理しなければならない アプリケーションにとって深刻な問題を引き起こします。例えば乗物などで使われてい る物理デバイスを制御する組み込みシステムなどでは数秒の遅延は受け入れられません。 制限時間内に反応しなければならないリアルタイムシステムでは多くの場合ガーベージ コレクションを使いません。 漸進的ガーベージコレクションではメモリの割り当てができる限り任意の時点でガーベー ジコレクションを中断し、再開できます。これによりリアルタイムゲームであってもガー ベージコレクションを利用できます。厳格なリアルタイムシステムであってもガーベー ジコレクションを使うモジュールと使わないモジュールを分けることでリアルタイム性 を保ったままガーベージコレクションを利用可能です。 マーク・アンド・スイープを漸進的にするにはどのように変更すればいいか考えてみま しょう。本質的には以下の 2 点について考える必要があります。

1.

マークフェーズの途中ですべてのオブジェクトにマークすることを保証するには どうすればよいか?

2.

マークフェーズまたはスイープフェーズの途中で新しいメモリを割り当てるには どうすればよいか?

マークフェーズの途中でガーベージコレクションが中断された場合、中断されている間 に新しく割り当てられた領域はその後のスイープフェーズに誤って回収されてしまうか もしれません。これを防ぐためには中断されている間に割り当てられた領域を黒または 灰色で色付けする方法があります。 スイープフェーズの途中でガーベージコレクションが中断された場合は通常通り色の書 き換えを再開できます。ただし中断されている間に割り当てられる領域はスイープが中 断された時点でフリーリストにある領域から選ばれます。スイープはアドレス順に進行 しますが、フリーリストにある領域はスイープが中断した地点よりも前の領域に含まれ ており、次のガーベージコレクションまでスイープの対象になりません。 図 9.12 はスイープフェーズの途中で新しいメモリの割り当てを行った状態を示していま す。新しいルートである w は以下のプログラムによって作られます。 # let w = ’f’ :: v; ; val w : char list = [’f’; ’z’; ’a’; ’m’]

Objective Caml でのメモリ管理 Objective Caml のガーベージコレクタはこれまで説明した技法を組み合わせたもので す。Objective Caml のガーベージコレクションは 2 世代からなり、若い世代 (マイナー

264

Chapter 9 : ガーベージコレクション handled part of the heap HEAP

u v w roots

’f’

’a’

’m’

   ’z’

’z’



black grey white hatched

free list 図 9.12: スイープフェーズの途中での割り当て ガーベージコレクション) では主にストップ・アンド・コピーを使い、古い世代 (メジャー ガーベージコレクション) では漸進的なマーク・アンド・スイープを使います。 一度のマイナーガーベージコレクションを生き残った若いオブジェクトは古い世代に移 されます。古い世代を対象にストップ・アンド・コピーを使い、ガーベージコレクショ ンの後には from-space 全体が解放されます。 多世代ガーベージコレクションについて説明した時に、純粋でない関数型言語での難し さについて注意を行いました。古い世代の値が新しい世代を参照する可能性があります。 例えば次のような場合です。 # let older = ref [1] ; ; val older : int list ref = {contents = [1]} (* ... *) # let newer = [2;5;8] in older := newer ; ; - : unit = ()

ただしコメント (* ... *) によって省略された部分で長い計算があり、その間に older は古い世代に移されていると仮定します。マイナーガーベージコレクションでは古い世 代から指された若い世代の値を解放してしまわないように配慮しなければなりません。 このため古い世代から参照されている若い世代の値の表を作成し、マイナーガーベージ コレクションのルートの一部に加えます。実際にこの表のサイズが増大することはほと んどありません。マイナーガーベージコレクションの直後にこの表は空になります。 古い世代に対するマーク・アンド・スイープは漸進的と言いましたが、それはマイナー ガーベージコレクションを行う間にメジャーガーベージコレクションを少しだけ実行さ せるという意味です。メジャーガーベージコレクションのアルゴリズムは 261 ページに 説明されている漸進的なマーク・アンド・スイープを基にしています。漸進的アルゴリ ズムの利点とは、マイナーガーベージコレクションの間にマークフェーズを少しずつ進

Gc モジュール

265

めることによってメジャーガーベージコレクションの待ち時間を減らすことにあります。 正式なメジャーガーベージコレクションでは、まだマークしていない領域をマークし、再 回収を行います。マーク・アンド・スイープでは古い世代にひどいフラグメンテーション を生じる場合があり、その時はメジャーガーベージコレクションの最後にコンパクショ ンを行います。 まとめると、アルゴリズムは以下のようになります。

1.

マイナーガーベージコレクション: 若い世代に対してストップ・アンド・コピーを 行います。生き残ったオブジェクトは加齢され、古い世代に移されます。古い世代 に対してマーク・アンド・スイープを少しだけ進めます。 古い世代に空き領域がなく、生き残ったオブジェクトをコピーできないときはス テップ 2 に進みます。

2.

メジャーガーベージコレクションを最後まで進めます。 これでも空き領域が足らなかった時はステップ 3 に進みます。

3.

漸進的アルゴリズムによってマークされたオブジェクトが解放されていないか調 べるため、もう一度メジャーガーベージコレクションを行います。 これでも空き領域が足らなかった時はステップ 4 に進みます。

4.

連続した空き領域の大きさを最大にするために古い世代に対してコンパクション を行います。もしこれでも空き領域が足らなかった時は諦めてプログラムの実行を 停止させます。

GC モジュールを使うとガーベージコレクタの様々なフェーズを起動させることができ ます。 Objective Caml のメモリ管理機構の中でまだ説明されていないことが一つだけありま す。ヒープ全体のサイズは起動時に決定されるのではなく、プログラムの実行状況に応 じて増減します。

Gc モジュール Gc モジュールを使うとヒープに関する統計情報を入手でき、またガーベージコレクショ ンの各フェーズを起動させることなどの制御も行えます。このモジュールでは stat と control の二つのレコード型が定義されています。control のフィールドは変更可能で すが、stat はある瞬間のヒープの状態を表しており、フィールドは変更できません。 stat レコードのフィールドは主にカウンタです。 •

ガーベージコレクションの回数を表すもの: minor collections, major collections, compactions



プログラムの開始時から割り当てられた総ワード数、コピーされた総ワード数: minor words, promoted words, major words.

control レコードのフィールドは以下のようになっています。 •

minor heap size: 若い世代の大きさ



major heap increment: 古い世代の領域を拡張する時の最小の大きさ

266

Chapter 9 : ガーベージコレクション



space overhead: メジャーガーベージコレクションを行わないことによって未使 用であるにも関わらず解放されていないメモリの割合。(デフォルト値は 80) この 値からメジャーガーベージコレクションを行う頻度を決定します。



max overhead: 無駄な (未使用であるが解放されていない) 領域の大きさがこの値 を越えた時にコンパクションを行います。この値を 0 にするとメジャーガーベージ コレクションの後に必ずコンパクションを行います。この値を 1000000 にすると コンパクションを行わなくなります。



verbose: ガーベージコレクタの動作を表示させる方法を指定します。

図 9.13 に stat と control を扱う関数をいくつか示します。

stat print stat get set

unit → stat out channel → unit unit → control control → unit

図 9.13: ヒープを制御し、統計情報を得る関数 以下の 4 つの関数はすべて型が unit -> unit であり、Objective Caml のガーベージ コレクタの一つまたは複数のフェーズを実行させます。それらは minor (フェーズ 1)、 major (フェーズ 1 と 2)、full major (フェーズ 1 から 3)、compact (フェーズ 1 から 4) の 4 つの関数です。

例 Gc.stat を呼び出した結果は以下のようになります。 # Gc.stat () ; ; - : Gc.stat = {Gc.minor_words = 1027347; Gc.promoted_words = 131663; Gc.major_words = 262302; Gc.minor_collections = 33; Gc.major_collections = 3; Gc.heap_words = 253952; Gc.heap_chunks = 4; Gc.live_words = 188700; Gc.live_blocks = 43317; Gc.free_words = 65201; Gc.free_blocks = 427; Gc.largest_free = 25852; Gc.fragments = 51; Gc.compactions = 0; Gc.top_heap_words = 253952}

結果にはマイナーガーベージコレクション、メジャーガーベージコレクション、コンパ クションの実行回数や、それぞれのメモリ領域で処理されたワード数等が含まれていま す。compact 関数を呼び出し、ガーベージコレクタの各フェーズを実行させてみましょ う。 # Gc.compact () ; ; - : unit = () # Gc.stat () ; ; - : Gc.stat = {Gc.minor_words = 1043434; Gc.promoted_words = 131663;

Weak モジュール

267

Gc.major_words = 264281; Gc.minor_collections = 33; Gc.major_collections = 5; Gc.heap_words = 253952; Gc.heap_chunks = 4; Gc.live_words = 158783; Gc.live_blocks = 35367; Gc.free_words = 95169; Gc.free_blocks = 2; Gc.largest_free = 63488; Gc.fragments = 0; Gc.compactions = 1; Gc.top_heap_words = 253952}

GC.minor collections フィールドと compactions フィールドの値がそれぞれ一つず つ増えています。一方 Gc.major collections フィールドの値は 2 つ増えています。 GC.control 型のレコードのすべてのフィールドは変更可能ですが、言語のランタイムに 反映させるためには Gc.set 関数を呼び出さなければなりません。この関数は control 型の値を受け取りガーベージコレクタの動作を変更します。 例えば verbose フィールドは 9 種類の指示の組合せの 0 から 1023 までの値を受け取り ます。ここでは 31 を代入してみます。 # let c = Gc.get () ; ; val c : Gc.control = {Gc.minor_heap_size = 32768; Gc.major_heap_increment = 63488; Gc.space_overhead = 80; Gc.verbose = 0; Gc.max_overhead = 500; Gc.stack_limit = 262144} # c.Gc.verbose <- 31; ; - : unit = () # Gc.set c; ; - : unit = () # Gc.compact () ; ; <>Starting new major GC cycle Starting new major GC cycle Compacting heap... done. - : unit = ()

ガーベージコレクタの各フェーズの実行が表示されるようになりました。

Weak モジュール 弱いポインタ weak pointer とはガーベージコレクタによっていつでも回収される可能性 のある領域を指しているポインタです。ポインタの指す値が不意に消滅してしまうかも しれない機能があるのは不思議と感じるかもしれません。しかしこのような弱いポイン タは値の一時的な保管機能を実装するのに必須のものです。特に、保存しなければなら ない要素よりもメモリが少ない時に極めて有用であることが分かります。良く引き合い に出される例はメモリキャッシュです。キャッシュ上の値はある時消滅してしまうかもし れませんが、値がある限り高速にアクセスできます。

Objective Caml では弱いポインタを直接扱うことはできず、弱いポインタの配列を使い ます。Weak モジュールは抽象型 ’a Weak.t を定義しています。’a 型の弱いポインタの 配列は’a option array 型を持ちます。’a option 型の定義は以下のようになります。

268

Chapter 9 : ガーベージコレクション

type ’a option = None | Some of ’a;; 図 9.14 にこのモジュールの主な関数が定義されています。 関数 create set get check

型 int -> ’a t ’a t -> int -> ’a option -> unit ’a t -> int -> ’a option ’a t -> int -> bool

図 9.14: Weak モジュールの主要な関数

create 関数はすべての要素が None で初期化された弱いポインタの配列を生成します。 set 関数は’a option 型の値を配列の指定されたインデックスの要素に設定します。get 関数は配列の指定されたインデックスの要素を返します。読み出された値を保持してい る限り、ガーベージコレクションによって回収されません。値が実際に存在しているか どうか確かめるには check 関数を使うか、または get 関数の返り値をパターンマッチし て調べます。 順序構造を扱うための標準的な関数も用意されています。配列のサイズを知るには length 関数を使います。配列のすべてまたは一部を埋めるには fill 関数や blit 関数を使い ます。

例: 画像イメージのキャッシュ 画像処理のアプリケーションでは一度に複数の巨大な画像を扱うことは稀ではありませ ん。ユーザが処理の対象をある画像から別の画像へと移すと、以前の画像をファイルに 保存し、新しい画像を読み込みます。ファイルに保存された画像はその名前だけが管理さ れています。ディスクアクセスをできるだけ減らすと同時にメモリ使用量も抑えるため に良くアクセスされる画像をメモリ上に置く画像キャッシュを作ってみましょう。キャッ シュの内容は必要に応じていつでも消去されます。この機能を弱いポインタの表を使っ て実装します。キャッシュの内容の破棄の部分はガーベージコレクタに任せます。画像 をロードする時はまず同じ画像があるかどうかキャッシュを探し、もしあればその内容 を返し、なければファイルから読み込みます。 キャッシュの表は以下のように定義します。 # type table of images = { size : int; mutable ind : int; mutable name : string; mutable current : Graphics.color array array; cache : ( string * Graphics.color array array) Weak.t } ; ; size フィールドは表の大きさを表しています。ind フィールドは現在の画像を格納して いるインデックスを指しています。name フィールドは現在の画像の名前を表しています。 current フィールドは現在の画像を保持しています。cache フィールドは画像を指す弱 いポインタの配列を保持しています。この配列の要素は画像の名前と画像本体の組です。

Weak モジュール

269

init table 関数は最初の画像で表を初期化します。 # let open image filename = let ic = open in filename in let i = ((input value ic) : Graphics.color array array) in ( close in ic ; i ) ; ; val open_image : string -> Graphics.color array array = # let init table n filename = let i = open image filename in let c = Weak.create n in Weak.set c 0 (Some (filename,i)) ; { size=n; ind=0; name = filename; current = i; cache = c } ; ; val init_table : int -> string -> table_of_images =

新しい画像を読み込む前にまず現在の画像を保存します。新しい画像がもしキャッシュ にあれば、ファイルから読み込まずキャッシュの内容を返します。 # exception Found of int * Graphics.color array array ; ; # let search table filename table = try for i=0 to table.size-1 do if i<>table.ind then match Weak.get table.cache i with Some (n,img) when n=filename → raise (Found (i,img)) | _ → () done ; None with Found (i,img) → Some (i,img) ; ;

# let load table filename table = if table.name = filename then () (* the image is the current image *) else match search table filename table with Some (i,img) → (* the image found becomes the current image *) table.current <- img ; table.name <- filename ; table.ind <- i | None → (* the image isn’t in the cache, need to load it *) (* find an empty spot in the cache *) let i = ref 0 in while (!i
270

Chapter 9 : ガーベージコレクション

table.ind <- !i ; table.name <- filename ; Weak.set table.cache table.ind (Some (filename,table.current)) ; ; val load_table : string -> table_of_images -> unit =

load table 関数はまず要求された画像が現在の画像かどうか調べます。違う時はキャッ シュにその画像があるかどうか調べます。キャッシュにない時はディスクから画像を読 み込みます。どちらの場合でも要求された画像を現在の画像とします。 このプログラムの動作を確認するためにキャッシュの内容を表示させる関数を使います。 # let print table table = for i = 0 to table.size-1 do match Weak.get table.cache ((i+table.ind) mod table.size) with None → print string "[] " | Some (n,_) → print string n ; print string " " done ; ; val print_table : table_of_images -> unit =

画像を一枚読み込み、キャッシュの内容を表示させてみましょう。Then we test the following program: # let t = init table 10 "IMAGES/animfond.caa" ; ; val t : table_of_images = {size = 10; ind = 0; name = "IMAGES/animfond.caa"; current = [|[|7371936; 7371936; 7371936; 7371936; 7371936; 7371936; 7371936; 7371936; 7371936; 7371936; 7371936; 7371936; 7373984; 7373984; ...|]; ...|]; cache = ...} # load table "IMAGES/anim.caa" t ; ; - : unit = () # print table t ; ; IMAGES/anim.caa [] [] [] [] [] [] [] [] [] - : unit = ()

このキャッシュのアイデアは様々なアプリケーションに応用することができます。

練習問題 ヒープの変化の追跡 ヒープの変化を追うためにヒープの情報を以下のようなフォーマットのレコード型とし て保持する関数を書いてみましょう。 # type tr gc = {state : Gc.stat; time : float; number : int}; ;

ここでの時間はプログラムが開始されてからの時間をミリ秒で表した数です。number

まとめ

271

フィールドはこのレコードに対して与えられる通し番号です。時間を得るには Sys.time 関数 (234 ページ参照) を使います。

1.

このようなレコードを生成する trace gc 関数を書きましょう。

2.

この関数が tr gc 型のレコードをファイルに永続的な形式で書き込めるように書 き換えなさい。 新しい関数は書き出すための出力チャネルを引数として受け取り ます。レコードを保存するには 228 ページで記述されている Marshal モジュール を使います。

3.

tr gc 型のレコードが保存されたファイルを受け取り、時間の経過にそってメジャー ガーベージコレクションとマイナーガーベージコレクションの回数を表示する単 体のプログラムを書きなさい。

4.

トレースファイルをインタープリタを使って作成し、内容を表示させてみましょう。

メモリ使用量とプログラミングスタイル この演習ではメモリ使用量に対するプログラミングスタイルの影響について比較します。 このために第 8 章 245 ページで扱った素数に関する演習を再考してみましょう。この演 習ではエラトステネスのふるいを末尾再帰で書いたものと末尾再帰を使わずに書いたも のを比べます。

1.

指定された区間に含まれるすべての素数を末尾再帰的に計算する関数 erart (こ の名前は変更した方がよいでしょう) を書きましょう。次に与えられた整数よりも 小さい素数を出力する関数を書きましょう。

2.

以上の関数を利用してプログラム era2main (この名前は変更した方がいいでしょ う) を作りなさい。このプログラムはファイル名と数のリストを受け取り、指定さ れた一つの数ごとにそれよりも小さな素数のリストを計算します。計算の途中での ガーベージコレクションのトレースを指定されたファイルに保存します。トレース を取るには前の演習の結果を利用します。

3.

以上のプログラムをコンパイルして単体のコマンドを作りなさい。 そのコマンド を以下の引数で実行し、トレースを表示させてみましょう。 % erart trace_rt 3000 4000 5000 6000

4.

同じことを末尾再帰を使わない関数に対して行いなさい。

5.

両者のトレースを比べてみましょう。

まとめ この章では、Objective Caml のガーベージコレクタについて解説するという趣旨から自 動メモリ再回収機構の代表的なアルゴリズムを紹介しました。Objective Caml のガー ベージコレクタは 2 世代を持つ漸進的アルゴリズムを採用しています。古い世代にはマー ク・アンド・スイープを行い、若い世代にはストップ・アンド・コピーを行います。ガー ベージコレクタと密接に結び付いた 2 つのモジュールを使って、ヒープの振る舞いを制 御することができます。Gc モジュールは、特定のアプリケーションに対してガーベージ

272

Chapter 9 : ガーベージコレクション

コレクタを最適化するためにガーベージコレクタに関する統計情報を提供し、ガーベー ジコレクタのアルゴリズムのパラメータを制御できます。Weak モジュールは要素が再回 収される可能性のある配列を提供しています。このモジュールはメモリキャッシュを実 装するのに重宝します。

もっと知りたい人へ メモリ管理の技法はプログラミング言語 Lisp の最初の実装から既に 40 年以上に渡って 研究されてきました。このため膨大な量の文献があります。 ガーベージコレクションに関する包括的な資料は Jones の本 [Jon98] です。Paul Wilson のチュートリアル [Wil92] は多くの資料を参照しており、この分野への優れた入門書と なっています。以下のウェブページもメモリ管理の現状について優れた資料を提供して います。 リンク: ftp://ftp.netcom.com/pub/hb/hbaker/home.html このページは逐次的なガーベージコレクタについて分かりやすく紹介しています。 リンク: http://www.cs.ukc.ac.uk/people/staff/rej/gc.html このページは Jones の本 [Jon98] について解説しています。検索可能な巨大な文献リス トも備えています。 リンク: http://www.cs.colorado.edu/˜zorn/DSA.html このページはガーベージコレクションをデバグするための様々なツールを紹介しています。 リンク: http://reality.sgi.com/boehm mti/ このページは C 言語のための保守的なガーベージコレクタのソースコードを提供してい ます。このガーベージコレクタは C 言語の標準のメモリ割り当て関数 malloc を自前の 関数 GC malloc で置き換えます。明示的なメモリ領域の解放を行う関数 free は何もし ない関数と置き換えられます。 リンク: http://www.harlequin.com/mm/reference/links.html このページにはメモリ管理に関するリンクがあります。 第 12 章で C 言語と Objective Caml のインターフェースについて説明する時に再びメ モリ管理の話題について扱います。

10 プログラム解析ツール プログラム解析ツールはプログラマに対してコンパイラやリンカが提供する情報を補う 様々な有益な情報を提供できます。いくつかのツールは静的解析を行いモジュール間の 相互依存関係や捕捉されない例外などを調査します。他のツールは実行時の解析を行い、 関数の呼び出された回数や引数の値、関数の実行時間などの情報を提供します。デバッ ガのような対話的なツールもあります。デバッガを使うと特定の地点にブレークポイン トを設定でき、その地点での変数の値を調査したり、引数を変えて繰り返し実行させた りできます。 これらのツールは Objective Caml 配布パッケージに含まれています。いくつかのツール は他の言語の標準的なツールと比べて変わった動作をする場合がありますが、この原因 には静的な型システムに起因するものが多くあります。静的な型システムのおかげで型 チェックなしに安全で効率の良いコードを生成できますが、実行時には値の型情報が失 われている場合があります。このため多相的な関数の引数を表示できない等の不都合が あります。

この章のあらまし この短い章では Objective Caml 配布パッケージに含まれているプログラム解析ツール について紹介します。まず最初に一つのアプリケーションプログラムに含まれるファイ ルの相互依存関係を調べる ocamldep コマンドについて解析します。 次にプログラムの実行トレースを取るツールと Unix 環境で動作するデバッガについて 扱います。 最後に最適化を行う時に有益な関数の実行回数や実行時間などの情報を解析するプロファ イラについて触れます。

274

Chapter 10 : プログラム解析ツール

依存性解析 大規模な Objective Caml のアプリケーションは通常多くのプログラムファイルとイン ターフェースファイルによって構成されています。それらのファイルの間の依存性解析 には二つの目的があります。一つはモジュール間の相互依存関係について理解をするこ と。もう一つはあるファイルを変更したときに再コンパイルする必要のあるファイルを 知ることです。

ocamldep コマンドは.ml ファイルと .mli ファイルを引数として受け取り、Makefile1 フォーマットの依存性情報を出力します。 他のモジュールへの依存は具体的にはドット記法 (M1.f) や他のモジュールを開くこと (open M1) によって生じます。 以下のような内容を含んだ二つのファイルがあるとします。

dp.ml : let print vect v = for i = 0 to Array.length v do Printf.printf "%f " v.(i) done; print newline () ; ;

d1.ml : let init n e = let v = Array.create 4 3.14 in Dp.print vect v; v; ;

これらのファイル名を ocamldep コマンドに与えると以下のような依存性についての結 果を出力します。 $ ocamldep dp.ml d1.ml array.ml array.mli printf.ml printf.mli dp.cmo: array.cmi printf.cmi dp.cmx: array.cmx printf.cmx d1.cmo: array.cmi dp.cmo d1.cmx: array.cmx dp.cmx array.cmo: array.cmi array.cmx: array.cmi printf.cmo: printf.cmi printf.cmx: printf.cmi

バイトコードとネイティブコードの両方に対して依存性情報を出力します。依存性情報 は行ごとに一つの項目を表しており、コロン (:) の右側にあるものは左側の項目に依存 1. Makefile は make コマンドによって使われているファイルで、プログラムを変更したときに再コンパイル する手順が記録されています。

275

デバッグツール

していることを表しています。dp.cmo ファイルは array.cmi ファイルと printf.cmi ファイルに依存しています。暗黙の依存関係として拡張子が .cmi のファイルは同じ名 前で拡張子が.mli のファイルに依存しています。同様に拡張子が.cmo または .cmx の ファイルは.ml ファイルに依存しています。 ディストリビューションに含まれるオブジェクトファイルは依存性情報の中に表示され ません。実際もし array.ml ファイルと printf.ml ファイルが現在のディレクトリにな ければ ocamldep はシステムライブラリからモジュールの情報を取得し、以下のような 結果を出力することになるでしょう。

$ ocamldep dp.ml d1.ml d1.cmo: dp.cmo d1.cmx: dp.cmx ocamldep コマンドがファイルを検索するディレクトリを追加するには-I オプションを 使います。

デバッグツール デバッグのためのツールには二つあります。一つ目のツールはインタープリタで利用で きるトレースの機能です。もう一つのツールはいわゆるデバッガです。ブレークポイン トでプログラムを停止させたり、引数を変えてプログラムを再実行させたりできます。 この二番目のツールは fork システムコール (586 ページ参照) を利用してプロセスを複 製するので Unix 環境でのみ利用できます。

Trace(トレース) ある関数のトレースとはプログラムを実行中にその関数が呼ばれた時の引数と帰り値の 値の記録のことです。 トレースコマンドはトップレベルのディレクティブです。トレースコマンドを使ってあ る関数のトレースを行うようにしたり、また止めるように指定できます。トレースコマ ンドには以下の 3 種類があります。

#trace 関数名 #untrace 関数名 #untrace all

指定された関数のトレースを開始する 指定された関数のトレースを終了する すべてのトレースを終了する

簡単な関数を使ってトレースの実演をしてみましょう。 # let f x = x + 1; ; val f : int -> int = # f 4; ; - : int = 5

276

Chapter 10 : プログラム解析ツール

関数 f のトレースを開始して、引数と帰り値の値が表示されるようにします。 # #trace f; ; f is now traced. # f 4; ; f <-- 4 f --> 5 - : int = 5

関数 f に引数 4 が適用され、帰り値が返されたことが表示されています。関数の引数は 左向きの矢印で表され、帰り値は右向きの矢印で表されています。

2 つ以上の引数を持つ関数 複数の引数を持つ関数(部分適用が起こる関数)も同様にトレースできます。すべての 引数は表示されますが、何回目の適用であるか示すために*記号を関数に付加していま す。4 つの引数 (a, b, q, r) を受け取る関数 verif div を考えます。この関数は整数の割 算が正しいかどうか検算 a = bq + r を行います。 # let verif div a b q r = a = b*q + r; ; val verif_div : int -> int -> int -> int -> bool = # verif div 11 5 2 1; ; - : bool = true

4 つの値を適用した結果は以下のようになります。 # #trace verif div; ; verif_div is now traced. # verif div 11 5 2 1; ; verif_div <-- 11 verif_div --> verif_div* <-- 5 verif_div* --> verif_div** <-- 2 verif_div** --> verif_div*** <-- 1 verif_div*** --> true - : bool = true

再帰関数 再帰関数の実行トレースには、実行効率についての価値ある情報が含まれています。再 帰実行の終了条件に問題があればトレースからそのヒントが得られるでしょう。 関数 belongs to は与えられた整数のリストに指定された整数が含まれているかどうか 判定する再帰的な関数です。この関数の定義は以下のようになります。 # let rec belongs to (e : int) l = match l with [] → false

277

デバッグツール | t :: q → (e = t) || belongs to e q ; ; val belongs_to : int -> int list -> bool = # belongs to 4 [3;5;7] ; ; - : bool = false # belongs to 4 [1; 2; 3; 4; 5; 6; 7; 8] ; ; - : bool = true

関数に値を適用した式 belongs to 4 [3;5;7] の実行結果を見ると、関数呼び出しと関 数からの復帰がそれぞれ 4 回ずつ起こっています。 # #trace belongs to ; ; belongs_to is now traced. # belongs to 4 [3;5;7] ; ; belongs_to <-- 4 belongs_to --> belongs_to* <-- [3; 5; 7] belongs_to <-- 4 belongs_to --> belongs_to* <-- [5; 7] belongs_to <-- 4 belongs_to --> belongs_to* <-- [7] belongs_to <-- 4 belongs_to --> belongs_to* <-- [] belongs_to* --> false belongs_to* --> false belongs_to* --> false belongs_to* --> false - : bool = false

関数 belongs to が呼ばれるごとに引数の 4 と検索されるリストが渡されてます。リス トが空になった時にこの関数は false を帰り値として返しています。この値は再帰的に 呼び出している関数に渡されていっています。 以下の例では値が見付かった場合を示しています。 # belongs to 4 [1; 2; 3; 4; 5; belongs_to <-- 4 belongs_to --> belongs_to* <-- [1; 2; 3; 4; belongs_to <-- 4 belongs_to --> belongs_to* <-- [2; 3; 4; 5; belongs_to <-- 4 belongs_to --> belongs_to* <-- [3; 4; 5; 6; belongs_to <-- 4

6; 7; 8] ; ;

5; 6; 7; 8]

6; 7; 8]

7; 8]

278

Chapter 10 : プログラム解析ツール

belongs_to --> belongs_to* <-- [4; 5; 6; 7; 8] belongs_to* --> true belongs_to* --> true belongs_to* --> true belongs_to* --> true - : bool = true

リストの先頭に 4 が来ると、この関数はすぐに true を返しています。この値は再帰的 に呼び出している関数に渡されていっています。 論理和 || の両辺の順序が変わると、関数 belongs to の呼び出しの結果は同じですが 常にリストをすべて検索するようになります。 # let rec belongs to (e : int) = function [] → false | t :: q → belongs to e q || (e = t) ; ; val belongs_to : int -> int list -> bool = # #trace belongs to ; ; belongs_to is now traced. # belongs to 3 [3;5;7] ; ; belongs_to <-- 3 belongs_to --> belongs_to* <-- [3; 5; 7] belongs_to <-- 3 belongs_to --> belongs_to* <-- [5; 7] belongs_to <-- 3 belongs_to --> belongs_to* <-- [7] belongs_to <-- 3 belongs_to --> belongs_to* <-- [] belongs_to* --> false belongs_to* --> false belongs_to* --> false belongs_to* --> true - : bool = true

リストの先頭に 3 が来ていても、すべてのリストが検索されています。このように実行 トレースの情報から再帰関数の効率について考えることができます。

多相関数 パラメータ化された型に対応する引数の値は実行トレースでは表示されません。例とし て関数 belongs to を明示的な型宣言なしに書き直してみましょう。 # let rec belongs to e l = match l with [] → false | t :: q → (e = t) || belongs to e q ; ; val belongs_to : ’a -> ’a list -> bool =

関数 belongs to は多相的になりました。実行トレース中では引数の値に代わって (poly)

デバッグツール

279

とだけ表示するようになりました。 # #trace belongs to ; ; belongs_to is now traced. # belongs to 3 [2;3;4] ; ; belongs_to <-- belongs_to --> belongs_to* <-- [; ; ] belongs_to <-- belongs_to --> belongs_to* <-- [; ] belongs_to* --> true belongs_to* --> true - : bool = true

Objective Caml インタープリタのトップレベルでは単相的な値しか表示できません。トッ プレベルは大域的な宣言の (推論された) 型しか保存していません。このためトップレ ベルは関数 belongs to の型を ’a -> ’a list -> bool であるとしか認識していませ ん。式 belongs to 3 [2;3;4] をコンパイルした後は 3 と [2;3;4] の型は忘れられて しまいます。これは Objective Caml が静的な型システムを持っているためであり、こ のために実行トレース中に多相的な値を表示することができないのです。 Objective Caml で総称的な print 関数 (’a -> unit) を定義できないのもまさにこの 理由からです。

局所関数 局所関数に対しても上で述べたのと全く同じ理由によりトレースを表示することができ ません。トップレベルでは大域的な宣言の型のみを保持しています。以下のようなプロ グラミングスタイルに人気があることは分かっています。 # let belongs to e l = let rec bel aux l = match l with [] → false | t :: q → (e = t) || (bel aux q) in bel aux l; ; val belongs_to : ’a -> ’a list -> bool = 大域的関数は局所関数を呼び出すだけであり、本質的な仕事は局所関数の中で行われて います。

トレースについての註 実行トレースとは実質的にプラットフォームに依存しないデバッグツールとなっていま す。トレースの弱点とは局所関数と多相関数の値を表示できないことです。この弱点は、 特にプログラミング言語は学習していく過程で大きな障害となるかもしれません。

280

Chapter 10 : プログラム解析ツール

デバッグ ocamldebug はいわゆるデバッガです。命令のステップ実行、ブレークポイントの挿入、 環境の中の値の表示、変更などを行うことができます。 プログラムのステップ実行と言った場合には「ステップとは何か」という定義が前もっ て必要になります。命令型言語の場合は簡単です。1 ステップとは 1 命令 (または 1 操作) に対応しています。しかし関数型言語ではこの定義ではうまくいきません。関数型言語 ではイベントを単位として考えます。イベントとは関数適用、パターンマッチ、条件判 定、ループなどを指しています。 警告

このツールは Unix 上でのみ利用できます。

デバッグモードでのコンパイル -g オプションを付けてコンパイルすると、デバッグのために必要な命令を含んだ.cmo ファイルを生成します。プログラムのデバッグを行うには必ずこのオプションを付けてプ ログラムのコンパイルを行う必要があります。実行ファイルが生成されたら ocamldebug を使ってプログラムを起動します。 ocamldebug [options ] executable [arguments ] 階乗を計算する以下のプログラムを fact.ml というファイルに保存します。 let fact n = let rec fact aux p q n = if n = 0 then p else fact aux (p+q) p (n-1) in fact aux 1 1 n; ;

メインプログラムを main.ml に保存します。このプログラムは非常に長い再帰呼び出し を作り出すことになります。 let x = ref 4; ; let go () = x := -1; Fact.fact !x; ; go () ; ;

-g オプションを付けて二つのファイルをコンパイルします。 $ ocamlc -g -i -o fact.exe fact.ml main.ml val fact : int -> int val x : int ref val go : unit -> int

デバッグツール

281

デバッガの起動 デバッグモードでプログラムがコンパイルされたらデバッガを使って実行ファイルを起 動します。

$ ocamldebug fact.exe Objective Caml Debugger version 3.00 (ocd)

実行の制御 実行の制御はプログラムイベントを通して行います。n 個前 (または n 個後) のプログラ ムイベントまで実行を戻す (または進める) ことができます。ブレークポイントについて も同様です。ブレークポイントは関数またはプログラムイベントに設定できます。対応 するソースコードはファイルの行数とカラム数によって示されます。 以下の例ではブレークポイントは Main モジュールの 4 行目に設定されています。

(ocd) step 0 Loading program... done. Time : 0 Beginning of program. (ocd) break @ Main 4 Breakpoint 1 at 5028 : file Main, line 4 column 3 モジュールの初期化はプログラムの実行の前に行われます。4 行目に設定されたブレー クポイントが 5028 番目の命令になっているのは初期化があるためです。 プログラムイベントまたはブレークポイントを指定してプログラムの実行を進めたり、あ るいは巻き戻したりすることができます。run によって次のブレークポイントまで実行を 進めます。reverse によって一つ前のブレークポイントまで実行を巻き戻します。step によって次のプログラムイベントまたは n 個先のプログラムイベントまで実行を進めま す。next は step と同じですが、関数呼び出し全体を 1 イベントと数えます。backstep と previous はそれぞれ step と next と同じことを逆方向に行います。finish は現在 の関数から戻るまで実行を進めます。start は現在の関数が呼び出される直前まで実行 を戻します。 例の中で次のブレークポイントまで実行を進め、その後プログラムイベントを 3 回進め ます。 (ocd) run Time : 6 - pc : 4964 - module Main Breakpoint : 1 4 <|b|>Fact.fact !x;; (ocd) step Time : 7 - pc : 4860 - module Fact

282

Chapter 10 : プログラム解析ツール

2 <|b|>let rec fact_aux p q n = (ocd) step Time : 8 - pc : 4876 - module Fact 6 <|b|>fact_aux 1 1 n;; (ocd) step Time : 9 - pc : 4788 - module Fact 3 <|b|>if n = 0 then p

値の表示 ブレークポイントでスタック上 (activation record) の変数の値を表示させることができ ます。print と display によって変数の値を表示させることができます。display は 階層的なデータ構造のもっとも浅い部分しか表示しません。 変数 n の値を表示させ、それから 3 ステップ実行を戻して、変数 x の値を表示させてみ ましょう。 (ocd) print n n : int = -1 (ocd) backstep 3 Time : 6 - pc : 4964 - module Main Breakpoint : 1 4 <|b|>Fact.fact !x;; (ocd) print x x : int ref = {contents=-1}

これらの表示コマンドではレコードのフィールドや配列の要素への参照を書くことがで きます。 (ocd) print x.contents 1 : int = -1

実行スタック 関数呼び出しの状態は実行スタックによって表現されています。backtrace(または短縮 形の bt) コマンドによって関数呼び出しの状況を表示させることができます。up と down コマンドによってそれぞれ次、または直前のスタックフレームを選択することができま す。frame コマンドによって現在選択されているスタックフレームの内容を表示させる ことができます。

プロファイリング このツールはプログラムの実行について、特定の関数の実行回数などの制御構造 (条件 文、パターンマッチ、ループ) に関する様々な統計情報を入手できます。結果はファイル に保存されます。この情報を利用すればアルゴリズムの間違いや、最適化に関するボト ルネックなどを発見することができるでしょう。

283

プロファイリング

プロファイリングを行うためには、プロファイリングのための専用の命令を含んだコー ドを生成する必要があります。そのためには特別なオプションを付けてプログラムをコ ンパイルしなければなりません。機械語でのプロファイリングでは個々の関数の実行時 間に関する情報も取得できます。 アプリケーションプログラムのプロファイリングは次のような手順で行います。

1.

プロファイリングオプションを付けてアプリケーションをコンパイルする。

2.

プログラムを実行する。

3.

プロファイリングの結果を出力する。

コンパイル方法 プロファイリングのためのコードを生成するには次のように指定します。



ocamlcp -p バイトコードコンパイラの場合。



ocamlopt -p 機械語コードコンパイラの場合。

これらのツールは通常のコンパイラによって生成されるファイル (第 7 章参照) と全く同 じ種類のファイルを生成します。サブオプションには図 10.1 に挙げたものがあります。

f i l m t a

関数呼び出し if による分岐 while と for によるループ match による分岐 try による分岐 以上のすべて

図 10.1: プロファイリングツールのオプション これらのオプションは情報を取得する対象となる制御構造を指定しています。デフォル トでは fm オプションが付いていると解釈されています。

プログラムの実行 バイトコードコンパイラの場合 プロファイリングオプションを付けてコンパイルされたプログラムは終了した時に、プロ ファイリングに必要な情報の入っている ocamlprof.dump というファイルを生成します。 整数のリストの乗算の例を使って説明します。以下のプログラムが f1.ml というファイ ルに保存されているとします。 let rec interval a b = if b < a then []

284

Chapter 10 : プログラム解析ツール

else a :: (interval (a+1) b); ; exception Found zero ; ; let mult list l = let rec mult rec l = match l with [] → 1 | 0::_ → raise Found zero | n :: x → n * (mult rec x) in try mult rec l with Found zero → 0 ;;

同様に以下のプログラムが f2.ml というファイルに保存されているとします。 let l1 = F1.interval 1 30; ; let l2 = F1.interval 31 60; ; let l3 = l1 @ (0 :: l2); ; print int (F1.mult list l1); ; print newline () ; ; print int (F1.mult list l3); ; print newline () ; ;

これらのファイルをプロファイリングモードでコンパイルすると次のようになります。

ocamlcp -i -p a -c f1.ml val profile_f1_ : int array val interval : int -> int -> int list exception Found_zero val mult_list : int list -> int -p オプションを付けるとコンパイラは新しい関数 (profile f1 ) を追加します。この関 数はプロファイリングのためのカウンタの初期化を行います。f2.ml ファイルのコンパ イルでも同様です。 ocamlcp -i -p a -o f2.exe f1.cmo f2.ml val profile_f2_ : int array val l1 : int list val l2 : int list val l3 : int list

機械語コードコンパイラの場合 機械語コードコンパイラでコンパイルすると以下のような結果になります。The native code compilation gives the following result:

プロファイリング

285

$ ocamlopt -i -p -c f1.ml val interval : int -> int -> int list exception Found_zero val mult_list : int list -> int $ ocamlopt -i -p -o f2nat.exe f1.cmx f2.ml ここではサブオプションのない -p オプションを使っています。f2nat.exe の実行が終 了すれば gmon.out というファイルを生成します。このファイルは Unix の標準的なツー ル (286 ページ参照) で使えるフォーマットになっています。

結果の出力 バイトコードと機械語コードで生成されるファイルが異なっているので、結果を出力さ せる方法も異なっています。バイトコードの場合は制御構造を通過した回数がプログラ ム中にコメントとして挿入されます。一方、機械語コードの場合は関数ごとの呼び出さ れた回数と実行時間のグラフが出力されます。

バイトコードコンパイラの場合 結果を見るためには ocamlprof コマンドを使います。このコマンドはプログラムのソー スファイルを引数として取り、camlprof.dump ファイルの内容を調べ、その情報をコメ ントとして付加したソースファイルを出力します。 整数のリストの乗算の例では以下のような結果になります。 ocamlprof f1.ml let rec interval a b = (* 62 *) if b < a then (* 2 *) [] else (* 60 *) a::(interval (a+1) b);; exception Found_zero ;; let mult_list l = (* 2 *) let rec mult_rec l = (* 62 *) match l with [] -> (* 1 *) 1 | 0::_ -> (* 1 *) raise Found_zero | n::x -> (* 60 *) n * (mult_rec x) in try mult_rec l with Found_zero -> (* 1 *) 0 ;;

解析結果には F2 モジュールの結果も反映されています。mult list が呼び出されたの は 2 回であり、補助関数 mult rec は 62 回呼び出されています。パターンマッチの部分 では一般の場合が 60 回、[] パターンは 1 度だけ実行されています。リストの先頭が 0 であったのは正確に 1 度だけであり、これは try 文の catch 節が 1 度実行されている ことと一致しています。

286

Chapter 10 : プログラム解析ツール

ocamlprof コマンドには 2 つのオプションがあります。-f オプションではプロファイリ ングの情報を含んだファイルを指定します。-F オプションは挿入する数字の前に指定さ れた文字列を追加するように指示します。

機械語コードコンパイラの場合 リストの要素を掛け合わせるプログラムで、それぞれの関数の実行時間を知る方法につ いて考えます。まず以下のプログラムを f3.ml というファイルに書き込みます。 let l1 = F1.interval 1 30; ; let l2 = F1.interval 31 60; ; let l3 = l1 @ (0 :: l2); ; for i=0 to 100000 do F1.mult list l1; F1.mult list l3 done; ; print int (F1.mult list l1); ; print newline () ; ; print int (F1.mult list l3); ; print newline () ; ;

これはループを 100000 回繰り返すことを除いて f2.ml と同じです。 プログラムを実行すると gmon.out というファイルが生成されます。このファイルは gprof という Unix のツールによって読むことができます。gprof を使うと以下のよう に個々の関数の実行時間に関する情報が出力されます。この出力は非常に長くなるので、 ここでは関数の実行時間についての情報を含んでいる最初のページだけを示します。 $ gprof f3nat.exe Flat profile: Each sample counts as 0.01 seconds. % cumulative self self time seconds seconds calls us/call 92.31 0.36 0.36 200004 1.80 7.69 0.39 0.03 200004 0.15 0.00 0.39 0.00 2690 0.00 0.00 0.39 0.00 302 0.00 0.00 0.39 0.00 188 0.00 0.00 0.39 0.00 174 0.00 0.00 0.39 0.00 173 0.00 0.00 0.39 0.00 173 0.00 0.00 0.39 0.00 34 0.00 0.00 0.39 0.00 30 0.00 0.00 0.39 0.00 30 0.00 ...

total us/call 1.80 1.95 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

name F1_mult_rec_45 F1_mult_list_43 oldify darken gc_message aligned_malloc alloc_shr fl_allocate caml_alloc3 caml_call_gc garbage_collection

練習問題

287

このプログラムでは実行時間のほとんどが F1 mult rec 45 という関数の実行によって 占められています。この関数は f1.ml の中で定義された F1.mult rec という関数に対 応しています。リストには他にも呼び出された関数を見ることができますが、このほと んどはメモリ管理のためのランタイムライブラリに含まれているものです (第 9 章参照)。

練習問題 関数適用のトレース この練習問題では関数適用の時点での引数の評価状況について調べてみましょう。

1.

関数 List.fold left に以下の引数を与えて評価した時のトレース結果を調べな さい。 List.fold left (-) 1 [2; 3; 4; 5]; ;

どのようなトレースが得られましたか。

2.

関数 List.fold left と同じ働きをする関数 fold left int を定義しなさい。た だしこの関数の型は (int → int → int) → int → int list → int とし ます。この関数のトレースを取り、 なぜ元の関数とトレースが変わるのか答えな さい。

性能解析 第 9 章 (249 ページ) の練習問題の続きを行います。その練習問題では素数を計算する二 つのプログラム (末尾再帰を使ったものと使わなかったもの) のメモリの仕様状況を比較 しましたが、ここではプロファイリングツールを使って関数の実行時間の比較を行いま す。この練習問題ではインライン展開 (第 7 章参照) の重要性について学びます。

1.

erart と eranrt の二つのプログラムをプロファイリングオプション付きでコン パイルしなさい。 コンパイルするにはバイトコードコンパイラと機械語コードコ ンパイラの両方で行いなさい。

2.

引数に 3000 4000 5000 6000 を指定してプログラムを実行しなさい。

3.

結果を ocamlprof と gprof 使って表示させ、 分析しなさい。

まとめ この章では Objective Caml パッケージに含まれているプログラミング支援ツールを紹 介しました。 最初のツールはファイル同士の依存関係を解析します。この情報は Makefile に供給さ れ、分割コンパイルを容易に実現します。 他のツールはプログラムの実行に関する情報を提供します。インタープリタはプログラ ムの実行のトレースを提供しますが本文中で既に示されているように、値を表示する時

288

Chapter 10 : プログラム解析ツール

に多相性が障害となります。しかし単相的な引数や再帰的な関数のトレースについては 全く問題ありません。 残りはデバッガとプロファイラで Unix 環境で伝統的に使われているツールです。デバッ ガではプログラムをステップ実行させることができます。プロファイラは性能に関する 情報を得ることができます。どちらも Unix 環境でのみ利用できます。

もっと知りたい人へ ocamldep コマンドの出力結果は ocamldot でグラフィカルに表示させることができま す。ocamldot についての詳細は以下のページを御覧ください。 リンク: http://www.cis.upenn.edu/˜tjim/ocamldot/index.html

ocamldot は別のツールである dot を利用しています。このツールは以下のページから ダウンロードできます。 リンク: http://www.research.att.com/sw/tools/graphviz/

Objective Caml でソフトウェア開発を行うときのプロジェクト管理について Makefile のテンプレートが用意されています。 リンク: http://caml.inria.fr/FAQ/Makefile ocaml-eng.html リンク: http://www.ai.univie.ac.at/˜markus/ocaml sources これらは ocamldep の出力を利用しています。 文献 [HF+ 96] では 20 種類の関数型言語 (ML を含む) の性能評価を行っています。ベン チマークは巨大なデータ構造の数値計算です。

11 字句解析と構文解析のた めのツール 字句解析と構文解析のためのツールの整備は情報科学の重要な研究分野であり続けてい ます。この活動の一つとして字句・構文解析器の自動生成を行う lex と yacc が生み出 されました。この章では、それらのツールに由来する camllex と camlyacc について解 説を行います。これら二つのツールは字句解析器と構文解析器を実装する事実上標準的 なやり方ですが、他にも行単位の処理などを行う正規表現ライブラリ str があります。 もっともこれは強力な解析を必要としないで済むアプリケーションに向いています。 このようなツールは現代的なプログラミング言語の分野でとりわけ重宝されていますが、 他のアプリケーションにも適用することができます。たとえばデータベースシステムで は問い合わせを構文解析する場合がありますし、スプレッドシートでは数式を評価した 結果を利用してセルの内容を定義することがあります。そこまでいかなくてもシステム コンフィグレーションファイルのように、プレーンテクストにデータを保存しておくこ とは良く行われています。このように限定された場合ではありますが、字句・構文解析 を必要とする処理を行っています。 これらすべての例において字句・構文解析が解決しなければならないのは単純な文字の 列を、語やレコードまたはプログラムの抽象構文木などの豊かな構造を持ったデータへ と変換する問題です。 どんな言語でも語彙(字句の集合)と字句をどのように組み合わせ方を記述した文法を 持っています。コンピュータのプログラムが言語を正しく処理するためには正確な字句・ 構文規則に従わなければなりません。コンピュータは自然言語の曖昧性を解消できる細 やかな意味上の理解ができません。この限界があるためにコンピュータ言語は一般に、 例外なく明確に規定された規則に従う必要があります。そのため、このような言語の字 句・文法の持つ構造は形式的な手法によって定義されています。この章では形式的な定 義法について簡単に述べ、その後でその使い方について説明を行います。

290

Chapter 11 : 字句解析と構文解析のためのツール

この章のあらまし この章では Objective Caml の配付パッケージに含まれている字句・構文解析のためのツー ルについて紹介します。構文解析用のツールは通常、字句解析用のツールと一緒に使うこ とを想定しています。第一節では Genlex モジュールによって提供されている、字句解析 のためのツールについて紹介します。Next we give details about the definition of sets of lexical units by introducing the formalism of regular expressions. We illustrate their behavior within module Str and the ocamllex tool. In section two we define grammars and give details about sentence production rules for a language to introduce two types of parsing: bottom-up and top-down. They are further illustrated by using Stream and the ocamlyacc tool. These examples use context-free grammars. We then show how to carry out contextual analysis with Streams. In the third section we go back to the example of a BASIC interpreter from page 159, using ocamllex and ocamlyacc to implement the lexical analysis and parsing functions.

語彙 字句解析は文字列処理の第一歩です。字句解析は文字の列を分割して語 lexemes の列 へと変換します。

Genlex モジュール このモジュールは、字句解析を行う基本的な機能を提供しています。文字の列を解析す るためのいくつかの語彙があらかじめ定義されています。それには次のようなものがあ ります。 # type token = Kwd of string | Ident of string | Int of int | Float of float | String of string | Char of char ; ;

したがってこのモジュールを使うと文字の列の中から整数(Int 構築子)を認識すること ができ、その値を取り出すことができます(Int 構築子の引数)。また慣習的な記法であ る(")に囲まれた文字列や(’)に囲まれた文字を取り出すこともできます。浮動小数 点数は 0.01 のような通常の記法でも、1E-2 のような指数仮数表現でも認識できます。

Ident 構築子は識別子のカテゴリーを示しています。識別子とは例えばプログラミング 言語の変数名や関数名のことです。識別子は文字や数字、下線( )、アクセント記号(’) などを含んでいます。識別子は数字で始まってはいけません。このモジュールでは演算 子(+、*、>、=など)もまた識別子であると考えています。最後に Kwd 構築子は特別な 識別子や文字を含んでいるキーワードのカテゴリーを示しています。

語彙

291

引数によって制御できるのはキーワードだけです。次の関数はキーワードのリストを受 け取って字句解析器(lexer)を生成します。 # Genlex.make lexer ; ; - : string list -> char Stream.t -> Genlex.token Stream.t =

make lexer にキーワードのリストを適用した結果は、文字列を受け取り、語(token 型) の列を返す関数になります。 したがってこれを利用すると BASIC インタープリタを簡単に作ることができます。ま ずキーワードを宣言します。 # let keywords = [ "REM"; "GOTO"; "LET"; "PRINT"; "INPUT"; "IF"; "THEN"; "-"; "!"; "+"; "-"; "*"; "/"; "%"; "="; "<"; ">"; "<="; ">="; "<>"; "&"; "|" ] ; ;

この定義をした後に字句解析器を次のように定義します。 # let line lexer l = Genlex.make lexer keywords (Stream.of string l) ; ; val line_lexer : string -> Genlex.token Stream.t = # line lexer "LET x = x + y * 3" ; ; - : Genlex.token Stream.t =

line lexer 関数は文字の流れを受け取り、語 lexeme の列を返します。

文字の流れの制御 文字の流れを直接扱って字句解析を行うこともできます。 以下の例では数式の字句解析器を扱います。関数 lexer は文字の流れを受け取り、lexeme Stream.t 型の語の列を返します1 。空白文字、タブ、改行コードは除去されます。問題 を簡単にするために変数と負の数は扱わないことにします。 # let rec spaces s = match s with parser [<’’ ’ ; rest >] → spaces rest | [<’’\t’ ; rest >] → spaces rest | [<’’\n’ ; rest >] → spaces rest 1. lexeme 型は 163 ページで定義されます。

292

Chapter 11 : 字句解析と構文解析のためのツール

| [<>] → () ; ; Characters 46-48: [<’’ ’ ; rest >] -> spaces rest ^^ Syntax error # let rec lexer s = spaces s; match s with parser [< ’’(’ >] → [< ’Lsymbol "(" ; lexer s >] | [< ’’)’ >] → [< ’Lsymbol ")" ; lexer s >] | [< ’’+’ >] → [< ’Lsymbol "+" ; lexer s >] | [< ’’-’ >] → [< ’Lsymbol "-" ; lexer s >] | [< ’’*’ >] → [< ’Lsymbol "*" ; lexer s >] | [< ’’/’ >] → [< ’Lsymbol "/" ; lexer s >] | [< ’’0’..’9’ as c; i,v = lexint (Char.code c - Char.code(’0’)) >] → [<’Lint i ; lexer v>] and lexint r s = match s with parser [< ’’0’..’9’ as c >] → let u = (Char.code c) - (Char.code ’0’) in lexint (10*r + u) s | [<>] → r,s ;; Characters 56-58: [< ’’(’ >] -> [< ’Lsymbol "(" ; lexer s >] ^^ Syntax error

関数 lexint は文字の流れの中にある整数定数に対する字句解析を行います。この関数 は lexer 関数が入力の中に数字を見付けたときに呼び出されます。lexint 関数はその ときに数字の列を消費して対応する整数値を求めます。

正規表現 2

字句解析での基本的な単位についての問題をちょっとばかり抽象化して理論的な観点か ら考えてみましょう。 我々の観点では字句解析の基本的な単位は「語」になります。語とは文字の集合「アル ファベット」の中の特定の文字を並べて結合させたものです。ここではアルファベットと してアスキー文字集合の部分集合を考えています。理論的には 0 文字からなる「空語3 」

2. アカデミックの世界の用語では「正則」表現とするのが正しいのであるが、我々はプログラマの伝統に従 い「正規」表現と呼ぶことにする。 3. 慣習にしたがって空語はギリシャ文字 ² によって表現されます。

293

語彙

を考えることもできます。アルファベットの要素から字句解析の基本的な単位(lexeme) を構成する方法について理論的な研究が長い間行われ、その成果の一つとして「正規表 現」として知られている単純明解な形式が考案されました。

定義 正規表現は語の集合を定義します。例えば、正しい識別子の集合を定義すること ができます。正規表現はいくつかの集合理論的操作によって定義されます。M と N を 語の集合とすると、

1.

M と N の和集合を M | N と書きます。

2.

M の補集合を ^M と書きます。補集合とは M に含まれないすべての語の集合 です。

3.

M と N の連接を M N と書きます。M と N の連接とは M に含まれる語と N に含まれる語を並べて結合したすべての語の集合です。

4.

M に含まれる語を有限個並べて出来るすべての語の集合を M + と書きます。

5.

記述上の慣習として語の集合 M に空語を加えた集合を M ? と書きます。

一つ一つの文字はその一文字だけを要素として含む集合を表します。そのため a | b | c という式は a と b と c の三語を含む集合を表します。特にこのような集合を定義する 場合は [abc] というより簡潔な表現も使います。我々の考えているアルファベットには 順序があります(アスキーコードによって指定された順序)ので文字を範囲で指定する ことができます。例えば数字の集合を [0-9] と書きます。また演算子の優先順位を変更 するために括弧を使います。 もし演算子のために使われている文字を文字として使いたい場合にはエスケープ文字 \ をその使いたい文字の直前に置きます。例えば (\*)* はアスタリスク文字の繰り返しを 表しています。 例 アルファベットとして数字(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)、プラス記号(+)、 マイナス記号(-)、ドット記号(.)と英文字 E を考えます。これを利用して数を表す 語の集合 num を定義します。まず整数を表す集合 integers を [0-9]+ と定義しましょ う。符号のない数を表す集合 unum を次のように定義します。

integers?(.integers)?(E(\+|-)?integers)? 符号付き数はしたがって次のような定義になります。

unum | -unum or with −?unum 認識 正規表現はそれ自体とても役に立つ形式ですが、実際にはある文字列が、正規表 現によって指定された語の集合の要素であるかどうか判定するプログラムを実装したい 場合があります。そのためには集合の形式的な定義から、表現を処理して認識を行うプ ログラムを生成する必要があります。正規表現の場合は、この変換を自動的に行うこと ができます。このような技術は以下に続く節で紹介を行う ocamllex ツールと、Str ラ イブラリの中の Genlex モジュールが提供しています。

294

Chapter 11 : 字句解析と構文解析のためのツール

Str ライブラリ このモジュールには正規表現を表す抽象データ型 regexp と、おおよそ上で説明した構 文に従う正規表現を表す文字列を受け取り、その抽象的な表現を返す関数 regexp が含 まれています。 このモジュールはまた正規表現を利用して文字列を操作する多くの関数を含んでいます。 Str ライブラリでの正規表現の構文は図 11.1 のようになっています。

. * + ? [..]

^ $ | (..) i \

\n 以外の任意の一文字 直前の表現の零回以上の繰り返し 直前の表現の一回以上の繰り返し 直前の表現であるかまたは空語 文字の集合(例 [abc]) 区間(例 [0-9]) 補集合(例 [^A-Z]) 行頭(補集合を表す ^ と間違えないように) 行末 選択 式のグループ化(以下で説明しますが、グループ化した式は番号で参 照できます。) 整数定数。i 番目のグループとマッチした文字列を指しています。 エスケープ文字(正規表現のために予約されている文字をマッチング で使いたい場合) 図 11.1: 正規表現

例 なんらかのテキストファイルに含まれている英米流の日付表示をフランス流に変換 する関数を書いてみましょう。そのテキストファイルは行ごとに何らかのデータが書か れており、英米流の日付表示ではドットによって項目が区切られていると仮定します。 ファイルから読まれた一行を表す文字列を引数として受け取り、日付を分離してフラン ス流の表示に変換する関数を定義してみましょう。 # let french date of d = match d with [mm; dd; yy] → dd^"/"^mm^"/"^yy | _ → failwith "Bad date format" ; ; val french_date_of : string list -> string = # let english date format = Str.regexp "[0-9]+\.[0-9]+\.[0-9]+" ; ; Characters 45-47: Warning: Illegal backslash escape in string Characters 53-55: Warning: Illegal backslash escape in string

語彙

295

let english_date_format = Str.regexp "[0-9]+\.[0-9]+\.[0-9]+" ;; ^^ let english_date_format = Str.regexp "[0-9]+\.[0-9]+\.[0-9]+" ;; ^^ val english_date_format : Str.regexp = # let trans date l = try let i=Str.search forward english date format l 0 in let d1 = Str.matched string l in let d2 = french date of (Str.split (Str.regexp "\.") d1) in Str.global replace english date format d2 l with Not found → l ; ; Characters 165-167: Warning: Illegal backslash escape in string let d2 = french_date_of (Str.split (Str.regexp "\.") d1) in ^^ val trans_date : string -> string = # trans date "..............06.13.99............" ; ; - : string = "..............13/06/99............"

ocamllex ツール ocamllex ツールとは、C 言語のための字句解析器生成器である lex ツールをモデル として Objective Caml のために作られたものです。このツールは正規表現として認識 される字句要素を記述したファイルを入力として受け取り、その字句要素を解析する Objective Caml のソースプログラムを出力として生成します。プログラマは字句要素の 記述に意味動作 semantic action を付け加えることができます。生成されたプログラ ムは Lexing モジュールで定義されている lexbuf 抽象型を利用しています。プログラ マはこのモジュールを使って、バッファの処理を制御することもできます。 字句解析の記述ファイルの拡張子は通常 .mll となっています。lex file.mll という記 述ファイルから Objective Caml のプログラムを生成するには次のように打ち込みます。

ocamllex lex_file.mll この結果、lex file.ml というファイルが生成されます。このファイルは、記述ファイル に指定されている字句解析を行うプログラムを含んでいます。この後に Objective Caml アプリケーションの他のモジュールと一緒にこのファイルをコンパイルすることができ ます。字句解析ルールごとに同じ名前を持った関数が定義されています。その関数は字 句解析のバッファ(Lexing.lexbuf 型)を引数として受け取り、意味動作で定義された 値を返します。したがって、同じルールの意味動作はすべて同じ型の値を返さなければ なりません。

ocamllex の記述ファイルは一般に次のようなフォーマットになっています。

296

Chapter 11 : 字句解析と構文解析のためのツール

{ ヘッダー

}

let ident = regexp ... rule ruleset1 = parse regexp { 意味動作 } | ... | regexp { 意味動作 } and ruleset2 = parse ... and ... { 後処理

} ヘッダーと後処理の部分はなくてもかまいません。それらの部分では字句解析の時に必 要になる Objective Caml の型、関数などを定義するために使います。後処理の部分で は、中間部分から生成される字句解析のための関数を使うことができます。ルール定義 の直前にある宣言リストは正規表現に名前を付けるのに使います。その名前はルールの 中で参照できます。 例 BASIC の題材をまた考えてみましょう。今回は、返される語の型を改良しましょ う。前回(163 ページ)と同様に lexeme 型の値を返す lexer 関数を定義します。ただ し、今回は Lexing.lexbuf 型のバッファを引数に取ります。 {

let string chars s = String.sub s 1 ((String.length s)-2) ; ; }

let op ar = [’-’ ’+’ ’*’ ’%’ ’/’] let op bool = [’!’ ’&’ ’|’] let rel = [’=’ ’<’ ’>’] rule lexer = parse [’ ’] { lexer lexbuf } | op ar { Lsymbol (Lexing.lexeme lexbuf) } | op bool { Lsymbol (Lexing.lexeme lexbuf) } | "<=" | ">=" | "<>"

{ Lsymbol (Lexing.lexeme lexbuf) } { Lsymbol (Lexing.lexeme lexbuf) } { Lsymbol (Lexing.lexeme lexbuf) }

297

構文 | rel

{ Lsymbol (Lexing.lexeme lexbuf) }

| | | | | |

{ { { { { {

"REM" "LET" "PRINT" "INPUT" "IF" "THEN"

Lsymbol Lsymbol Lsymbol Lsymbol Lsymbol Lsymbol

| ’-’? [’0’-’9’]+ | [’A’-’z’]+ | ’"’ [^ ’"’]* ’"’

(Lexing.lexeme (Lexing.lexeme (Lexing.lexeme (Lexing.lexeme (Lexing.lexeme (Lexing.lexeme

lexbuf) lexbuf) lexbuf) lexbuf) lexbuf) lexbuf)

} } } } } }

{ Lint (int of string (Lexing.lexeme lexbuf)) } { Lident (Lexing.lexeme lexbuf) } { Lstring (string chars (Lexing.lexeme lexbuf)) }

ocamllex によってこのファイルを変換すると Lexing.lexbuf -> lexeme 型を持つ lexer 関数が生成されます。このあと構文解析について考える時(308 ページ)にこ の lexer 関数の使い方について説明します。

構文 字句解析を利用すれば入力流の構造を切り分けるのが簡単にできます。そうやって得ら れた語を組み立てて、想定されている言語の文法的に正しい文を形成するのが次の問題 です。それを行うのが構文解析の役割です。文を組み立てる規則は文法規則によって定 義されます。この形式化手法はもともとは言語学の分野で発展してきたものですが、や がて数理言語学者とコンピュータ科学者にとっても極めて役に立つことが分かって来て います。文法規則については既に 160 ページで Basic 言語についての例を見てきました が、ここではその例をさらに進めながら文法についての基本的な概念を勉強することに しましょう。

文法 形式的には文法とは次の四要素によってできています。

1.

終端記号と呼ばれるシンボルの集合。終端記号とは対象としている言語の字句要 素(語)を表しています。Basic 言語の場合、字句要素とはマイナス演算子-、算 術的、論理的記号(+、&、<、<=等)、キーワード(GOTO、PRINT、IF、THEN)、整 数、変数になります。

2.

非終端記号と呼ばれるシンボルの集合。非終端記号とは対象としている言語の構 文要素を表しています。例えば Basic のプログラムは行(Line)から成り立って おり、行は式(Expression)を含んでいます。

3.

生成規則の集合。生成規則とは終端記号と非終端記号からどのように構文要素を 生成するかを示しているものです。Basic の行は行番号に続いて命令が書かれてい ますが、これは次のような規則によって表現されます。 Line ::= integer Instruction

298

Chapter 11 : 字句解析と構文解析のためのツール 同じ終端記号は異なった記号列から生成されることがありえます。その時は個々の ケースを区別するために — という記号を使います。例えば Instruction ::= LET variable = Expression — GOTO integer — PRINT Expression のようになります。

4.

開始記号と呼ばれる特別な非終端記号。開始記号は対象としている言語の正しい 文全体(プログラム)を指定します。開始記号の生成規則が構文解析のスタート地 点となります。

生成と認識 生成規則は、言語の字句要素の列をどう認識するかという問題と深く関わっています。 ここでは、数式のための単純な言語を考えてみましょう。

Exp

::= — — —

integer Exp + Exp Exp * Exp ( Exp )

(R1) (R2) (R3) (R4)

ただし右辺にある (R1)、(R2)、(R3)、(R4) はそれぞれの規則の名前を表しています。 1*(2+3) という式を字句解析すると、次のような語の列が得られます。

integer * ( integer + integer ) この文を解析して、数式言語に間違いなく属していることを確認するためには、数式の 生成規則を右から左に見ていきます。もしある部分式が生成規則のどれかの右辺とマッ チすれば、その部分式を対応する右辺と置き換えます。この書き換えを繰り返し適用し て、最終的に式を非終端記号 start (ここでは Exp)まで還元します。ここに還元の例 を示します。4

integer * ( integer + integer )

(R1)

←−

(R1)

←−

(R1)

←−

(R2)

←−

(R4)

←−

(R3)

←−

Exp * ( integer + integer ) Exp * ( Exp + integer ) Exp * ( Exp + Exp ) Exp * ( Exp ) Exp * Exp Exp

4. 還元する部分式には下線が引いてあります。またその時に使った規則名も書いてあります。

299

構文

Exp だけを含む最終行から始めて、矢印に従って下から上に読んでいけば、開始記号 Exp からどのように最初の式が生成されたのか理解することができます。つまり、最初 の式はこの文法によって定義された言語に含まれていることが分かります。 文法から、その文法によって定義された言語に属する文を認識するプログラムを生成す るのは、正規言語の場合よりもかなり複雑な問題になります。過去の研究の結果から分 かっているのは正規言語の形式によって定義された言語は決定性有限オートマトンと本 質的に等価であるということです。そしてその決定性有限オートマトンとして動作する プログラムを書くのはかなり簡単です。しかし、一般の文法に対してはそのような結果 は得られませんでした。例えばある文法のクラスに対しては決定性有限オートマトンよ りも複雑なプッシュダウンオートマトンと等価であることが分かっています。ただしここ ではこの話題にこれ以上関わりません。オートマトンの厳密な定義も行いません。ここ では単に、我々の構文解析器生成器で使われている文法について明確に説明を行います。

トップダウン解析 前段で登場した 1*(2+3) という式の構文解析は一通りだけではありません。integers の 還元を右から左に行うこともできました。その場合さらに 2+3 を規則(R2)で先に還元 することもできたでしょう。これらの二通りの方式はトップダウン解析(右から左)とボ トムアップ解析(左から右)と呼ばれているものと対応しています。トップダウン解析は Stream モジュールを使えば簡単に実装できます。一方ボトムアップ解析は ocamlyacc ツールによって実装されています。ボトムアップ解析は、Basic の構文解析の時に既に 解説したようにスタックを利用しています。構文解析にどちらの方式を選択するかとい う判断は重要です。対象となる言語を定義している文法によってはトップダウン解析が 不可能な場合もあります。

簡単な場合 トップダウン解析の最も標準的な例とは次のように定義される数式の前置記法です。

Expr

::= — —

integer + Expr Expr * Expr Expr

この場合、先頭の語さえ分かればどの生成規則を使うのか決まります。したがってトップ ダウン解析を行うプログラムは単純な再帰呼び出しを行う関数によって実装することが できます。Genlex モジュールと Stream モジュールを使って実装した例を以下に示しま す。関数 infix of は前置記法の式を受け取り、意味が等価な中置記法の式を返します。 # let lexer s = let ll = Genlex.make lexer ["+";"*"] in ll (Stream.of string s) ; ; val lexer : string -> Genlex.token Stream.t = # let rec stream parse s = match s with parser

300

Chapter 11 : 字句解析と構文解析のためのツール

| | | |

[<’Genlex.Ident x>] → x [<’Genlex.Int n>] → string of int n [<’Genlex.Kwd "+"; e1=stream parse; e2=stream parse>] → "("^e1^"+"^e2^")" [<’Genlex.Kwd "*"; e1=stream parse; e2=stream parse>] → "("^e1^"*"^e2^")" [<>] → failwith "Parse error"

;; Characters 52-54: [<’Genlex.Ident x>] -> x ^^ Syntax error # let infix of s = stream parse (lexer s) ; ; Characters 17-29: let infix_of s = stream_parse (lexer s) ;; ^^^^^^^^^^^^ Unbound value stream_parse # infix of "* +3 11 22"; ; Characters 0-8: infix_of "* +3 11 22";; ^^^^^^^^ Unbound value infix_of

ただし、この構文解析器はかなり融通のないものになっています。語の間に空白がない と正しく解釈ができません。 # infix of "*+3 11 22"; ; Characters 1-9: infix_of "*+3 11 22";; ^^^^^^^^ Unbound value infix_of

やや単純でない例 ストリームを使った構文解析の動作はかなり予測しやすいものになっています。このよ うな構文解析を行うためには文法は二つの条件を満たしていなければなりません。

1.

文法は左再帰規則を含んでいてはなりません。左再帰規則とは右辺の式が左辺と 同じ非終端記号から始まっている規則です。例えば Exp ::= Exp + Exp このよう な規則です。

2.

二つの規則が同じ式から始まってはなりません。

298 ページの通常の数式の文法に対してはトップダウン解析を直接適用することはでき ません。この文法はトップダウン解析の制約を満たしていません。したがってトップダ ウン解析を行うためには文法を書き換えて左再帰と非決定性を除去する必要があります。 例えば次のような文法になります。

301

構文

Expr NextExpr

Atom

::= ::= — — — — ::= —

Atom NextExpr + Atom - Atom * Atom / Atom ² integer ( Expr )

NextExpr の定義に空語 ² がないと整数だけの式を解釈できないことに注意してくだ さい。 この文法は、生成規則を直接実装した構文解析器を実装できるようになっています。以 下の実装では数式を表す抽象構文木を生成します。 # let rec rest = parser [< ’Lsymbol "+"; e2 = atom >] → Some (PLUS,e2) [< ’Lsymbol "-"; e2 = atom >] → Some (MINUS,e2) [< ’Lsymbol "*"; e2 = atom >] → Some (MULT,e2) [< ’Lsymbol "/"; e2 = atom >] → Some (DIV,e2) [< >] → None atom = parser [< ’Lint i >] → ExpInt i | [< ’Lsymbol "("; e = expr ; ’Lsymbol ")" >] → e and expr s = match s with parser [< e1 = atom >] → match rest s with None → e1 | Some (op,e2) → ExpBin(e1,op,e2) ; ; Characters 28-30: [< ’Lsymbol "+"; e2 = atom >] -> Some (PLUS,e2) ^^ Syntax error | | | | and

トップダウン解析の主要な問題とは、極めて制限された形式の文法を使わなければなら ないということです。もし対象としている言語が左再帰文法を使って自然に書ける(例 えば中置記法の式)場合はその文法と等価(すなわち同じ言語を定義している)であっ て、トップダウン解析の制約を満たしている文法を見付けるのは必ずしも自明ではあり ません。このため yacc と ocamlyacc はボトムアップ解析を採用しています。ボトム アップ解析ではより人間が自然に見える文法を定義することができます。ただし、ボト ムアップ解析であってもやはり限界はあります。

302

Chapter 11 : 字句解析と構文解析のためのツール

ボトムアップ解析 本書の 165 ページから始まる部分でボトムアップ解析の動作に関する直観的な解説を行 いました。ボトムアップ解析は、スタックの状態を書き換えるシフトと還元という二つ の動作を基本としています。文法がボトムアップ解析の制約を満たしている限り、シフ トと還元の動作列から文の認識することができます。ここでもやはり問題は文法が持つ 非決定性です。この節では広く使われている後置記法と中置記法の数式を例に取り、ボ トムアップ解析の内部で行われている動作と、その失敗の原因について解説します。 ボトムアップ解析の利点 is:

The simplified grammar for postfix arithmetic expressions

Expr

::= | |

integer Expr Expr + Expr Expr *

(R1) (R2) (R3)

この文法は前置記法の数式の文法のちょうど逆になっています。解析の最後になるまで どの規則を適用すべきだったのか分かりませんが、その時に適用できる規則は一つだけ しかありません。実際にこのような式のボトムアップ解析はスタックを利用した数式の 計算過程ととてもよく似ています。計算結果をスタックに積む代わりに文法記号を積む ことになります。ボトムアップ解析は空の状態のスタックから始まり、入力がすべて解 析されると開始記号だけを含む状態で終了します。シフトする時は現在の非終端記号を スタックに積みます。還元は、ある規則の右辺の要素が逆順でスタックの先頭に積まれ ている時に起こります。この場合、それらの要素を対応する左辺の非終端記号で置き換 えます。 図 11.2 は 1 2 + 3 * 4 + という式のボトムアップ解析が進行する過程を説明していま す。入力となる語には下線が引かれています。入力列の最後を $ 記号で表しています。 ボトムアップ解析の限界 文法から言語を認識するプログラムを作り出す際の困難は適 用する動作を決定する点に尽きています。この問題について三種類の非決定性を持つ例 を使って説明します。 最初の例は足し算だけを含む式の文法です。

E0

::= |

integer E0 + E0

(R1) (R2)

この文法の非決定性は規則(R2)に起因しています。次のような状況を考えてみましょう。 動作 .. .

.. .

入力

スタック

+. . .

[E0 + E0 . . . ]

303

構文 動作

入力

スタック

12+3*4+$

[]

シフト

2+3*4+$ [1] 還元(R1) 2+3*4+$ [Expr] シフト +3*4+$ [2Expr] 還元(R1) +3*4+$ [Expr Expr] シフト、還元(R2) 3*4+$ [Expr] シフト、還元(R1) *4+$ [Expr Expr] シフト、還元(R3) 4+$ [Expr] シフト、還元(R1) +$ [Expr Expr] シフト、還元(R2) $ [Expr] 図 11.2: ボトムアップ解析の例 このような場合シフト動作を行って + 記号をスタックに積むべきなのか、規則(R2)を 適用してスタックの中の E0 + E0 を還元するべきなのか、決定できません。これをシフ トと還元の衝突 shift-reduce conflict と呼びます。この例の場合は integer + integer + integer という式が二通りの導出ができることに原因があります。 導出 1

E0

(R2)

−→

(R1)

−→

(R2)

−→

E0 + E0 E0 + integer E0 + E0 + integer

etc. 導出 2

E0

(R2)

−→

(R2)

−→

(R1)

−→

E0 + E0 E0 + E0 + E0 E0 + E0 + integer

etc. それぞれの導出によって得られる式は式の値の計算という観点では同じように見えるか もしれません。

304

Chapter 11 : 字句解析と構文解析のためのツール

(integer + integer) + integer と integer + (integer + integer) しかし構文木の上では両者は異なっています(166 ページの図 6.3 を参照)。 シフトと還元の衝突が起こる文法の二番目の例は、式に暗黙に付けられている括弧に関 係するという意味では一番目の例と同じですが、括弧の付け方によって式の意味が変わっ てしまうような場合です。次のような文法を考えてみましょう。

E1

::= | |

integer E1 + E1 E1 * E1

(R1) (R2) (R3)

この文法では一番目の例で述べた衝突が + 記号と * 記号に関して起こることは分かり ますが、それに加えて + 記号と * 記号の間でも衝突が発生します。異なった導出方法に よって同じ式を生成することが可能です。次の式を見てください。 integer + integer * integer 導出 1

E1

(R3)

−→

(R1)

−→

(R2)

−→

E1 * E1 E1 * integer E1 + E1 * integer

以下略 導出 2

E1

(R2)

−→

(R3)

−→

(R1)

−→

E1 + E1 E1 + E1 * E1 E1 + E1 * integer

以下略 この場合(暗黙の)括弧によって式の意味が変わってしまいます。

(integer + integer) * integer 6= integer + (integer * integer) この問題は Basic の式に関しても既に指摘してありました(165 ページ参照)。その時 は演算子に別々の優先順位を付加することで解決しました。規則(R3)を規則(R2)よ り先に必ず還元することにします。これはすなわち掛け算に括弧を付けることに相当し ます。

+ 記号と * 記号の選択に関する問題は、文法を書き換えることでも解決できます。新し い非終端記号 T (項を表す)と F (因数を表す)を導入した、次のような文法を考え ます。 E T F

::= | ::= | ::=

E+T T T*F F integer

(R1) (R2) (R3) (R4) (R5)

305

構文

この文法では integer + integer * integer を解釈するには規則(R1)を使うしかありま せん。 三番目の例はプログラミング言語の条件文と関係があります。Pascal などの言語では if .. then と if .. then .. else の二種類の条件文があります。次のような文法を考 えてみましょう。

Instr

::= — —

if Exp then Instr if Exp then Instr else Instr (以下略)

(R1) (R2)

ここで次のような状況を考えてみましょう。 動作 .. .

入力

else. . .

スタック

[Instr then Exp if. . . ]

.. . この時にスタックの先頭部分が規則(R1)の条件文なのか(その場合、還元しなければ ならない)それとも規則(R2)の一番目の Instr なのか(その場合、シフトしなければ ならない)決定できません。 シフトと還元の衝突に加えて、ボトムアップ解析は還元と還元が衝突することもあります。

We now introduce the ocamlyacc tool which uses the bottom-up parsing technique and may find these conflicts.

ocamlyacc ツール ocamlyacc ツールは ocamllex と使い方は同じです。ocamlyacc ツールは、文法規則と その意味動作を記述したファイルを入力として受け取り、Objective Caml のソースプロ グラムを生成します。生成されるのは構文解析を行うプログラムを含むファイル(.ml ファイル)とそのインターフェース(.mli)です。 フォーマット ocamlyacc の文法記述ファイルの拡張子は今までの慣習で.mly になっ ています。文法記述ファイルは次のような構造でなければなりません。

306

Chapter 11 : 字句解析と構文解析のためのツール

%{ ヘッダ

}% 宣言

%% 文法規則

%% 後処理 文法規則は次のようになっています。 非終端記号 : 記号. . . 記号 { 意味動作 } | ... | 記号. . . 記号 { 意味動作 } ; 「記号」は終端記号か非終端記号のどちらでも構いません。ocamllex の場合と同様に ヘッダ部と後処理部は補助的な定義を書くのに使います。ただしヘッダ部で定義した内 容は文法規則からのみ参照できます。したがってヘッダ部でモジュールをオープンして も宣言部ではその効果がありません。そのため宣言部では型名などを常に正式名称で記 述しなければなりません。 意味動作 意味動作とは、ある文法規則に結びつけられた Objective Caml のプログラ ムであり、構文解析器がその文法規則を還元した時に実行されます。意味動作の中では 文法規則の右辺の要素を番号で参照することができます。番号は 1 から始まり、左から 右へ一つずつ増えていきます。最初の要素は $1 で参照でき、二番目の要素は $2 で参照 できます。 開始記号

宣言部に(複数の)開始記号を宣言することができます。

%start 非終端記号 .. 非終端記号 これらの非終端記号ごとに構文解析を行う関数が生成されます。非終端記号の型は宣言 部に記述しなければなりません。

%type 非終端記号 この output-type は正式名称でなければなりません。 警告

非終端記号は構文解析を行う関数の名前になります。そ のため大文字で始めることはできません(大文字で始ま る記号は構築子と見なされるため)。

字句要素

文法規則は終端記号である字句要素を参照する必要があります。

終端記号を宣言するには次のように記述します。

307

構文

%token PLUS MINUS MULT DIV MOD 識別子のようなある種の字句要素は文字列の集合を表しています。識別子は、それがど んな文字列であるのかという情報が重要です。ある字句要素が含んでいる内容の型は< と > で囲んで指定します。

%token IDENT

警告

ocamlyacc はこれらの宣言を処理して token 型の構築 子に変換します。したがって字句要素は大文字で始めな ければなりません。

文字列を暗黙の終端記号として使うこともできます。

expr : expr "+" expr | expr "*" expr | ... ;

{ ... } { ... }

この場合、それ自身がその内容を表しているので宣言を行う意味はありません。暗黙の 終端記号は字句解析器に渡されることなく、構文解析器が直接処理をします。字句解析 器との整合性を保つためこの表記法は推奨されていません。

優先順位、結合律 ボトムアップ解析で衝突が発生する原因の多くは演算子の間の暗黙 の結合法則、優先順位の扱いにあることを見てきました。このような衝突を扱うために 演算子の優先順位と結合法則(右結合、左結合、非結合)を宣言することができます。以 下の宣言では+ 演算子(記号 PLUS)と * 演算子(記号 MULT)を左結合の演算子として 宣言しています。また MULT が PLUS の後に宣言しているので* の方が + よりも高い優 先順位を持っています。

%left PLUS %left MULT 同じ行で宣言された演算子は同じ優先順位を持ちます。

コマンドオプション

ocamlyacc のオプションは二つあります。



-b name: 生成される Objective Caml のファイルの名前が name.ml と name.mli になります。



-v: .output という拡張子のファイルを生成します。このファイルの中には文法規 則に割り振られた番号、文法を認識するオートマトンの状態、衝突の原因が記録さ れます。

308

Chapter 11 : 字句解析と構文解析のためのツール

ocamllex との連携 ocamllex と ocamlyacc を一緒に組み合わせて、入力された文字 列を字句の列に変換し、それを構文解析器に送ることもできます。これを行うためには字 句を表す型 OCtypelexeme を双方のツールが知っていなければなりません。この型は、 .mly の拡張子を持つファイルから ocamlyacc によって生成された.mli と .ml ファイ ルで定義されています。.mll ファイルはこの型をインポートし、ocamllex はこのファ イルを Lexing.lexbuf -> lexeme という型を持つ関数に変換します。309 ページの例 はこの相互依存の仕方について具体的に手順を踏んで説明しています。

文脈依存文法 ocamlyacc が処理する文法によって生成される言語のクラスは文脈自由文法です。文脈 自由文法の構文解析器はある字句要素を処理する時にその結果が以前に処理された構文 要素に依存していません。たとえば次の L のような言語では以前の処理の結果に依存す る文法になっています。 L ::= wCw | w ただし w ∈ (A|B)∗ ここで A、 B 、 C は終端記号です。(A|B)∗ C(A|B)∗ ではなくて wCw と書いたのは中 央の C の左右に同じ語を置くことを表したかったからです。

L に含まれる語を認識するためには語 C 以前に読んだものを覚えておき、語 C 以後の 部分と全く同じかどうか調べる必要があります。ここではストリームを利用した解決法 を説明します。基本的なアイデアは全く同じ語を認識する構文解析関数を語 C より前の 部分を認識する過程で作り出すというものです。 字句を表す型を定義します。 # type token = A | B | C ; ; 関数 parse w1 はストリーム解析関数を利用して最初の w を覚えておく関数を作り出しま す。その関数は一文字だけを認識するストリーム関数のリストとして定義されています。 # let rec parse w1 s = match s with parser [<’A; l = parse w1 >] → (parser [<’A >] → "a") :: l | [<’B; l = parse w1 >] → (parser [<’B >] → "b") :: l | [< >] → [] ; ; Characters 48-50: [<’A; l = parse_w1 >] -> (parser [<’A >] -> "a")::l ^^ Syntax error

parse w1 が返す関数の返り値は字句に対応する文字列です。 関数 parse w2 は関数 parse w1 が生成したリストを受け取り、個々の要素を一つの解析 関数へと組み合わせます。

Basic 再考

309

# let rec parse w2 l = match l with p :: pl → (parser [< x = p; l = (parse w2 pl) >] → x^l) | [] → parser [<>] → "" ; ; Characters 58-60: p::pl -> (parser [< x = p; l = (parse_w2 pl) >] -> x^l) ^^ Syntax error

parse w2 を適用した結果は語 w を表す文字列となります。parse w2 の定義より、この 関数は parse w1 が読み込んだ語だけを認識し、それ以外の語を認識することはありま せん。 ストリームの中間結果を束縛する機構を使うと、言語 L の語を認識する関数を次のよう に書くことができます。 # let parse L = parser [< l = parse w1 ; ’C; r = (parse w2 l) >] → r ; ; Characters 23-25: let parse_L = parser

[< l = parse_w1 ; ’C; r = (parse_w2 l) >] -> r ;; ^^

Syntax error

例として二つの語の解析を行います。最初の結果では C を囲む語を返しています。二番 目の例では C の両側の語が違っているため認識に失敗しています。 # parse L [< ’A; Characters 9-11: parse_L [< ’A; ^^ Syntax error # parse L [< ’A; Characters 8-10: parse_L [< ’A; ^^ Syntax error

’B; ’B; ’C; ’A; ’B; ’B >]; ; ’B; ’B; ’C; ’A; ’B; ’B >];;

’B; ’C; ’B; ’A >]; ; ’B; ’C; ’B; ’A >];;

Basic 再考 ocamllex と ocamlyacc を使って 169 ページに記載されている Basic のための構文解析 関数を書き直してみましょう。今回は字句定義と文法定義ファイルから生成された関数 を利用して、抽象的に記述することができます。 ここでは以前の字句定義を再利用することはできません。演算子、命令、キーワードを 区別できるようにより正確な型を定義しなければなりません。 同様に、抽象構文を定義する型宣言 (sentences 型と sentences 型を定義するのに必要 な型) を basic types.mli ファイルの中に保存します。

310

Chapter 11 : 字句解析と構文解析のためのツール

basic parser.mly ファイル ヘッダー部 ヘッダー部では抽象構文を定義する型宣言をインポートします。それから 文字列を抽象構文要素へと変換する補助関数を二つ定義します。 open Basic types ; ; let phrase of cmd c = match c with "RUN" → Run | "LIST" → List | "END" → End | _ → failwith "line : unexpected command" ;; let bin op of rel r = match r with "=" → EQUAL | "<" → INF | "<=" → INFEQ | ">" → SUP | ">=" → SUPEQ | "<>" → DIFF | _ → failwith "line : unexpected relation symbol" ;;

宣言部 宣言部は字句宣言と字句の結合規則と優先順位、開始記号の三つの部分からな ります。この例では開始記号は命令を意味する line です。 字句宣言部は次のようになります。 %token %token %token %token %token %token %token %token %token %token %token %token

Lint Lident Lstring Lcmd Lplus Lminus Lmult Ldiv Lmod Lrel Land Lor Lneg Lpar Rpar Lrem Lrem Llet Lprint Linput Lif Lthen Lgoto Lequal Leol

名前から意味を想像できると思います。これらの定義は basic lexer.mll に記述され ています (312 ページ参照)。

Basic 再考

311

演算子の優先順位規則は priority uop 関数と priority binop 関数で定義されていま した (160 ページ参照) が、ここでは以下のようになります。 %right Lneg %left Land Lor %left Lequal Lrel %left Lmod %left Lplus Lminus %left Lmult Ldiv %nonassoc Lop

最後のシンボル Lop は単項マイナス演算子を表すのに使います。このシンボルは終端記 号ではありませんが、同じ記号が文脈によって別の意味と優先順位を持つ時に疑似的な 終端記号として導入します。この点には文法規則を説明する時にまた触れます。 開始記号が line なので生成された関数は行を表す構文木を返すことになります。

%start line %type line 文法規則 Basic の文法規則は三つの非終端記号で表されています。行を表す line、命 令を表す inst と式を表す exp です。文法規則と関連した意味動作は単純に文法規則に 対応する抽象構文を組み立てているだけです。 %% line : Lint inst Leol | Lcmd Leol ; inst : Lrem | Lgoto Lint | Lprint exp | Linput Lident | Lif exp Lthen Lint | Llet Lident Lequal exp ; exp : Lint | Lident | Lstring | Lneg exp | exp Lplus exp | exp Lminus exp | exp Lmult exp | exp Ldiv exp | exp Lmod exp

{ Line {num=$1; inst=$2} } { phrase of cmd $1 }

{ { { { {

{ { { { { { { { {

Rem $1 } Goto $2 } Print $2 } Input $2 } If ($2, $4) } { Let ($2, $4) }

ExpInt ExpVar ExpStr ExpUnr ExpBin ExpBin ExpBin ExpBin ExpBin

$1 } $1 } $1 } (NOT, $2) } ($1, PLUS, $3) } ($1, MINUS, $3) } ($1, MULT, $3) } ($1, DIV, $3) } ($1, MOD, $3) }

312

Chapter 11 : 字句解析と構文解析のためのツール

| exp Lequal exp | exp Lrel exp | exp Land exp | exp Lor exp | Lminus exp %prec Lop | Lpar exp Rpar ; %%

{ ExpBin ($1, EQUAL, $3) } { ExpBin ($1, (bin op of rel $2), $3) } { ExpBin ($1, AND, $3) } { ExpBin ($1, OR, $3) } { ExpUnr(OPPOSITE, $2) } { $2 }

これらの文法規則の意味について特に説明はいらないでしょう。ただし次の規則

exp : ... | Lminus exp %prec Lop { ExpUnr(OPPOSITE, $2) } については説明が必要かもしれません。この規則は単項マイナス演算子と関係していま す。%prec キーワードはこの命令が Lop の優先順位を持つべきであると指定しています。

basic lexer.mll ファイル 字句解析はひとつしか規則がありません。この lexer という規則は以前の関数 lexer (165 ページ) と密接に対応しています。それぞれの字句要素と結びつけられた動作規則 は単純にその字句要素のコンストラクタを呼び出しているだけです。字句要素の型は文 法規則のファイルで定義されてるので、そのファイルをインポートします。また単にダ ブルコーテーション記号を取り除く関数をここで定義します。 {

open Basic parser ; ; let string chars s = String.sub s 1 ((String.length s)-2) ; ; } rule lexer = parse [’ ’ ’\t’]

{ lexer lexbuf }

| ’\n’

{ Leol }

| | | | | | | | |

{ { { { { { { { {

’!’ ’&’ ’|’ ’=’ ’%’ ’+’ ’-’ ’*’ ’/’

| [’<’ ’>’]

Lneg } Land } Lor } Lequal } Lmod } Lplus } Lminus } Lmult } Ldiv }

{ Lrel (Lexing.lexeme lexbuf) }

Basic 再考

313

| "<=" | ">="

{ Lrel (Lexing.lexeme lexbuf) } { Lrel (Lexing.lexeme lexbuf) }

| | | | | | |

{ { { { { { {

"REM" [^ ’\n’]* "LET" "PRINT" "INPUT" "IF" "THEN" "GOTO"

Lrem (Lexing.lexeme lexbuf) } Llet } Lprint } Linput } Lif } Lthen } Lgoto }

| "RUN" | "LIST" | "END"

{ Lcmd (Lexing.lexeme lexbuf) } { Lcmd (Lexing.lexeme lexbuf) } { Lcmd (Lexing.lexeme lexbuf) }

| [’0’-’9’]+ | [’A’-’z’]+ | ’"’ [^ ’"’]* ’"’

{ Lint (int of string (Lexing.lexeme lexbuf)) } { Lident (Lexing.lexeme lexbuf) } { Lstring (string chars (Lexing.lexeme lexbuf)) }

ここで =記号は式と代入の両方で使われます。 やや複雑な正規表現を使っている二つの規則についてだけ説明します。最初の規則 ("REM" [^ ’\n’]*) はコメント行に関するものです。この規則はキーワード REM の後で改行 ’\n’ を含まな い文字列とマッチします。また最後の規則 (’"’ [^ ’"’]* ’"’) はダブルコーテーショ ン記号に囲まれた文字列を認識します。

コンパイルとリンク 字句解析器と構文解析器のコンパイルは決まった順序で行わなければなりません。なぜ なら字句などのデータ構造の定義が相互に依存しているからです。この例のプログラム をコンパイルするには次のような順序でコマンドを実行する必要があります。

ocamlc -c basic_types.mli ocamlyacc basic_parser.mly ocamllex basic_lexer.mll ocamlc -c basic_parser.mli ocamlc -c basic_lexer.ml ocamlc -c basic_parser.ml この結果二つのファイル basic lexer.cmo と basic parser.cmo が生成されます。こ れらのファイルをリンクしてアプリケーションから利用できます。 以上で Basic の構文解析器を書き直す準備はすべて整いました。 「字句解析」(163 ページ) と「構文解析」(165 ページ) に記述されている型や関数の定 義はここでは省略します。174 ページに記述されている関数 one command の中の次の式

314

Chapter 11 : 字句解析と構文解析のためのツール

match parse (input line stdin) with を以下のように書き換えてください。 match line lexer (Lexing.from string ((input line stdin)^"\n")) with

ここで行の終わりに改行コード ’\n’ を付け加える必要があります。(input line 関数 は改行コードを取り除いてしまいます。) 改行コードは命令行の終わりを表すシンボル (Leol) として必要なものです。

練習問題 コメントの除去 Objective Caml ではコメントは階層的です。つまりコメントを含むコメントを記述でき ます。コメントは (* で始まり *) で終わります。例えば以下のようなコメントを記述で きます。 (* comment spread over several lines *)

let succ x = (* successor function *) x + 1; ; (* level 1 commented text let old_succ y = (* level 2 successor function level 2 *) y +1; ; level 1 *) succ 2; ;

この練習問題ではコメントを除去したテキストを書き出すプログラムを作ります。どん な字句解析器を使っても構いません。

1.

Objective Caml のコメントを認識できる字句解析器 を書きなさい。コメントの 始まりと終わりがきちんと対応していなければなりません。コメント以外の部分 に含まれている語については自由とします。

2.

ファイルを開き、コメントを除去し、残りの部分を新しいファイルに買きだすプロ グラム を書きなさい。

3.

Objective Caml では文字列の中に任意の文字を含むことができます。例えば "what(*ever te*)xt" という文字列は有効で、この場合 (* と *) はコメントとは解釈されませ ん。文字列を正しく解釈する字句解析器 を書きなさい。

4.

新しい字句解析器と プログラムを統合したプログラム を作りなさい。

評価器 ocamlyacc を式の評価に利用することができます。基本的なアイデアは式の評価を文法 規則で直接実行することです。

まとめ

315

ここでは必ず括弧で括られた前置記法の数式を対象にします。例えば (ADD e1 e2 .. en) という式は e1 + e2 + .. + en と等価です。ただし加算と乗算は右結合、減算と 除算は左結合とします。

1.

式の構文解析と評価規則を opn parser.mly ファイルに定義しなさい。

2.

式の字句解析器を opn lexer.mll ファイルに定義しなさい。

3.

標準入力から式を一行読み込み、評価して結果を表示するプログラム opn を書き なさい。

まとめ この章では Objective Caml で利用できる字句解析と構文解析のためのツールを紹介し ています。



正規表現を扱うモジュール Str



単純な字句解析器を定義できるモジュール Genlex



lex ツールに型システムを導入したツール ocamllex



yacc ツールに型システムを導入したツール ocamlyacc



文脈依存構文解析器を含むトップダウン解析器を定義できるストリーム

159 ページで定義した Basic 言語の構文解析器は ocamllex と ocamlyacc を利用する とより簡潔で直観的に分かりやすい形式で記述できます。

もっと知りたい人へ 字句解析と構文解析のための標準的な本は親しみを込めて「ドラゴンブック」と呼ばれ ています。この本は正しくは Compilers: principles, techniques and tools ([ASU86]) と いう題名であり、表紙にはドラゴンの絵が使われています。この本はコンパイラの設計 と実装のすべての面をカバーしています。文脈自由言語を解釈するオートマトンの構築 法とその最小化の技法なども明解に説明されています。lex と yacc について詳しく説 明されている本は何冊かありますが文献 [LMB92] を参照してください。ocamllex と ocamlyac がオリジナルのツールと比べて興味深い点は Objective Caml 言語と完全に統 合されていることであり、とりわけ型付きの字句解析器と構文解析器を定義できること です。ストリームに関しては Michel Mauny と Daniel de Rauglaudre が明解な操作的意 味を与え [MdR92] 、[CM98] が実装法について解説しています。新しい文法を Objective Caml 言語自体に組み込む方法については camlp4 ツールを参照してください。 リンク: http://caml.inria.fr/camlp4/

12 Interoperability with C Developing programs in a given language very often requires one to integrate libraries written in other languages. The two main reasons for this are: •

to use libraries that cannot be written in the language, thus extending its functionality;



to use high-performance libraries already implemented in another language.

A program then becomes an assembly of software components written in various languages, where each component has been written in the language most appropriate for the part of the problem it addresses. Those software components interoperate by exchanging values and requesting computations. The Objective Caml language offers such a mechanism for interoperability with the C language. This mechanism allows Objective Caml code to call C functions with Camlprovided arguments, and to get back the result of the computation in Objective Caml. The converse is also possible: a C program can trigger an Objective Caml computation, then work on its result. The choice of C as interoperability language is justified by the following reasons: •

it is a standardized language (ISO C);



C is a popular implementation language for operating systems (Unix, Windows, MacOS, etc.);



a great many libraries are written in C;



most programming languages offer a C interface, thus it is possible to interface Objective Caml with these languages by going through C.

The C language can therefore be viewed as the esperanto of programming languages. Cooperation between C and Objective Caml raises a number of difficulties that we review below.

318

Chapter 12 : Interoperability with C



Machine representation of data For instance, values of base types (int, char, float) have different machine representations in the two languages. This requires conversion between the representations, in both directions. The same holds for data structures such as records, sum types1 , or arrays.



The Objective Caml garbage collector Standard C does not provide garbage collection. (However, garbage collectors are easily written in C.) Moreover, calling a C function from Objective Caml must not modify the memory in ways incompatible with the Objective Caml GC.



Aborted computations Standard C does not support exceptions, and provides different mechanisms for aborting computations. This complicates Objective Caml’s exception handling.



Sharing common resources For instance, files and other input-output devices are shared between Objective Caml and C, but each language maintains its own input-output buffers. This may violate the proper sequencing of input-output operations in mixed programs.

Programs written in Objective Caml benefit from the safety of static typing and automatic memory management. This safety must not be compromised by improper use of C libraries and interfacing with other languages through C. The programmer must therefore adhere to rather strict rules to ensure that both languages coexist peacefully.

Chapter outline This chapter introduces the tools that allow interoperability between Objective Caml and C by building executables containing code fragments written in both languages. These tools include functions to convert between the data representations of each language, allocation functions using the Objective Caml heap and garbage collector, and functions to raise Objective Caml exceptions from C. The first section shows how to call C functions from Objective Caml and how to build executables and interactive toplevel interpreters including the C code implementing those functions. The second section explores the C representation of Objective Caml values. The third section explains how to create and modify Objective Caml values from C. It discusses the interactions between C allocations and the Objective Caml garbage collector, and presents the mechanisms ensuring safe allocation from C. The fourth section describes exception handling: how to raise exceptions and how to handle them. The fifth section reverses the roles: it shows how to include Objective Caml code in an application whose main program is written in C.

1. Objective Caml’s sum types are discriminated unions. Refer to chapter 2, page 44 for a full description.

Communication between C and Objective Caml

319

注意 This chapter assumes a working knowledge of the C language. Moreover, reading chapter 9 can be helpful in understanding the issues raised by automatic memory management.

Communication between C and Objective Caml Communication between parts of a program written in C and in Objective Caml is accomplished by creating an executable (or a new toplevel interpreter) containing both parts. These parts can be separately compiled. It is therefore the responsibility of the linking phase2 to establish the connection between Objective Caml function names and C function names, and to create the final executable. To this end, the Objective Caml part of the program contains external declarations describing this connection. Figure 12.1 shows a sample program composed of a C part and an Objective Caml part. Each part comprises code (function definitions and toplevel expressions for Objective

C part value f_c (value x, value y, value z) { return Val_long( Long_val(x) + Long_val(y) + Long_val(z)); }

                                                                                                                                                                                       dynamic allocation heap

Objective Caml part linking call

external f : int -> int -> int -> int = "f_c"

return

let r = f 2 6 9;;

          garbage collected heap

図 12.1: Communication between Objective Caml and C.

Caml) and a memory area for dynamic allocation. Calling the function f with three Objective Caml integer arguments triggers a call to the C function f c. The body of the C function converts the three Objective Caml integers to C integers, computes their sum, and returns the result converted to an Objective Caml integer.

2. Linking is performed differently for the bytecode compiler and the native-code compiler.

320

Chapter 12 : Interoperability with C

We now introduce the basic mechanisms for interfacing C with Objective Caml: external declarations, calling conventions for C functions invoked from Objective Caml, and linking options. Then, we show an example using input-output.

External declarations External function declarations in Objective Caml associate a C function definition with an Objective Caml name, while giving the type of the latter. The syntax is as follows: 構文 :

external caml name : type = "C name"

This declaration indicates that calling the function caml name from Objective Caml code performs a call to the C function C name with the given arguments. Thus, the example in figure 12.1 declares the function f as the Objective Caml equivalent of the C function f c. An external function can be declared in an interface (i.e., in an .mli file) either as an external or as a regular value: 構文 :

external caml name : type = "C name" val caml name : type

In the latter case, calls to the C function first go through the general function application mechanism of Objective Caml. This is slightly less efficient, but hides the implementation of the function as a C function.

Declaration of the C functions C functions intended to be called from Objective Caml must have the same number of arguments as described in their external declarations. These arguments have type value, which is the C type for Objective Caml values. Since those values have uniform representations (第 9 章参照), a single C type suffices to encode all Objective Caml values. On page 325, we will present the facilities for encoding and decoding values, and illustrate them by a function that explores the representations of Objective Caml values. The example in figure 12.1 respects the constraints mentioned above. The function f c, associated with an Objective Caml function of type int -> int -> int -> int, is indeed a function with three parameters of type value returning a result of type value.

Communication between C and Objective Caml

321

The Objective Caml bytecode interpreter evaluates calls to external functions differently, depending on the number of arguments3 . If the number of arguments is less than or equal to five, the arguments are passed directly to the C function. If the number of arguments is greater than five, the C function’s first parameter will get an array containing all of the arguments, and the C function’s second parameter will get the number of arguments. These two cases must therefore be distinguished for external C functions that can be called from the bytecode interpreter. On the other hand, the Objective Caml native-code compiler always calls external functions by passing all the arguments directly, as function parameters.

External functions with more than five arguments For external functions with more than five arguments, the programmer must provide two C functions: one for bytecode and the other for native-code. The syntax of external declarations allows the declaration of one Objective Caml function associated with two C functions: 構文 :

external caml name : type = "C name bytecode" "C name native"

The function C name bytecode takes two parameters: an array of values of type value (i.e. a C pointer of type value*) and an integer giving the number of elements in this array.

Example The following C program defines two functions for adding together six integers: plusnative, callable from native code, and plus bytecode, callable from the bytecode compiler. The C code must include the file mlvalues.h containing the definitions of C types, Objective Caml values, and conversion macros. #include #include value plus_native (value x1,value x2,value x3,value x4,value x5,value x6) { printf("<< NATIVE PLUS >>\n") ; fflush(stdout) ; return Val_long ( Long_val(x1) + Long_val(x2) + Long_val(x3) + Long_val(x4) + Long_val(x5) + Long_val(x6)) ; } value plus_bytecode (value * tab_val, int num_val) { int i; long res; 3. Recall that a function such as fst, of type ’a * ’b -> ’a, does not have two arguments, but only one that happens to be a pair; on the other hand, a function of type int -> int -> int has two arguments.

322

Chapter 12 : Interoperability with C

printf("<< BYTECODED PLUS >> : ") ; fflush(stdout) ; for (i=0,res=0;i
The following Objective Caml program exOCAML.ml calls these two C functions. external plus : int → int → int → int → int → int → int = "plus_bytecode" "plus_native" ; ; print int (plus 1 2 3 4 5 6) ; ; print newline () ; ;

We now compile these programs with the two Objective Caml compilers and a C compiler that we call cc. We must give it the access path for the mlvalues.h include file. $ cc -c -I/usr/local/lib/ocaml

exC.c

$ ocamlc -custom exC.o exOCAML.ml -o ex_byte_code.exe $ ex_byte_code.exe << BYTECODED PLUS >> : 21 $ ocamlopt exC.o exOCAML.ml -o ex_native.exe $ ex_native.exe << NATIVE PLUS >> : 21 注意 To avoid writing the C function twice (with the same body but different calling conventions), it suffices to implement the bytecode version as a call to the native-code version, as in the following sketch: value prim nat (value x1, ..., value xn) { ... } value prim bc (value *tbl, int n) { return prim nat(tbl[0],tbl[1],...,tbl[n-1]) ; }

Linking with C The linking phase creates an executable from C and Objective Caml files compiled with their respective compilers. The result of the native-code compiler is shown in figure 12.2. The compilation of the C and Objective Caml sources generates machine code that is stored in the static allocation area of the program. The dynamic allocation area contains the execution stack (corresponding to the function calls in progress) and the heaps for C and Objective Caml.

Communication between C and Objective Caml

static allocation area

dynamic allocation area

323

void main (int argc, ...

C code

function x -> ...

Objective Caml code

                                                                                                     

C static variables

C heap

Objective Caml heap (with GC)

runtime stack

図 12.2: Mixed-language executable.

Run-time libraries The C functions that can be called from a program using only the standard Objective Caml library are contained in the execution library of the abstract machine (see figure 7.3 page 200). For such a program, there is no need to provide additional libraries at link-time. However, when using Objective Caml libraries such as Graphics, Num or Str, the programmer must explicitly provide the corresponding C libraries at link-time. This is the purpose of the -custom compiler option (see 第 7 章参照, page 207). Similarly, when we wish to call our C functions from Objective Caml, we must provide the object file containing those C functions at link-time. The following example illustrates this.

The three linking modes The linking commands differ slightly between the native-code compiler, the bytecode compiler, and the construction of toplevel interactive loops. The compiler options relevant to these linking modes are described in chapter 7. To illustrate these linking modes, we consider again the example in figure 12.1. Assume the Objective Caml source file is named progocaml.ml. It uses the external function f c defined in the C file progC.c. In turn, the function f c refers to a C library

324

Chapter 12 : Interoperability with C

a C library.a. Once all these files are compiled separately, we link them together using the following commands: •

bytecode: ocamlc -custom -o vbc.exe progC.o a_C_library.a progocaml.cmo



native code: ocamlopt progC.o -o vn.exe a_C_library.a progocaml.cmx

We obtain two executable files: vbc.exe for the bytecode version, and vn.exe for the native-code version.

Building an enriched abstract machine Another possibility is to augment the run-time library of the abstract machine with new C functions callable from Objective Caml. This is achieved by the following commands: ocamlc -make-runtime -o new_ocamlrun progC.o a_C_library.a We can then build a bytecode executable vbcnam.exe targeted to the new abstract machine: ocamlc -o vbcnam.exe -use-runtime new_ocamlrun progocaml.cmo To run this bytecode executable, either give it as the first argument to the new abstract machine, as in new_ocaml vbcnam.exe , or run it directly as vbcnam.exe 注意 Linking in -custom mode scans the object files (.cmo) to build a table of all external functions mentioned. The bytecode required to use them is generated and added to the bytecode corresponding to the Objective Caml code.

Building a toplevel interactive loop To be able to use an external function in the toplevel interactive loop, we must first build a new toplevel interpreter containing the C code for the function, as well as an Objective Caml file containing its declaration. We assume that we have compiled the file progC.c containing the function f c. We then build the toplevel loop ftop as follows: ocamlmktop -custom -o ftop progC.o a_C_library.a ex.ml The file ex.ml contains the external declaration for the function f. The new toplevel interpreter ftop then knows this function and contains the corresponding C code, as found in progC.o.

Exploring Objective Caml values from C

325

Mixing input-output in C and in Objective Caml The input-output functions in C and in Objective Caml do not share their file buffers. Consider the following C program: #include #include value hello_world (value v) { printf("Hello World !!");

fflush(stdout);

return v; }

Writes to standard output must be flushed explicitly (fflush) to guarantee that they will be printed in the intended order. # external caml hello world : unit → unit = "hello_world" external caml_hello_world : unit -> unit = "hello_world" # print string "<< " ; caml hello world () ; print string " >>\n" ; flush stdout ; ; The external function ‘hello_world’ is not available

;;

The outputs from C and from Objective Caml are not intermingled as expected, because each language buffers its outputs independently. To get the correct behavior, the Objective Caml part must be rewritten as follows: # print string "<< " ; flush stdout ; caml hello world () ; print string " >>\n" ; flush stdout ; ; The external function ‘hello_world’ is not available

By flushing the Objective Caml output buffer after each write, we ensure that the outputs from each language appear in the expected order.

Exploring Objective Caml values from C The machine representation of Objective Caml values differs from that of C values, even for fundamental types such as integers. This is because the Objective Caml garbage collector needs to record additional information in values. Since Objective Caml values are represented uniformly, their representations all belong to the same C type, named (unsurprisingly) value. When Objective Caml calls a C function, passing it one or several arguments, those arguments must be decoded before using them in the C function. Similarly, the result of this C function must be encoded before being returned to Objective Caml. These conversions (decoding and encoding) are performed by a number of macros and C functions provided by the Objective Caml runtime system. These macros and functions are declared in the include files listed in figure 12.3. These include files are part of the

326

Chapter 12 : Interoperability with C

Objective Caml installation, and can be found in the directory where Objective Caml libraries are installed4 caml/mlvalues.h caml/alloc.h caml/memory.h

definition of the value type and basic value conversion macros. functions for allocating Objective Caml values. macros for interfacing with the Objective Caml garbage collector. 図 12.3: Include files for the C interface.

Classification of Objective Caml representations An Objective Caml representation, that is, a C datum of type value, is one of: •

an immediate value (represented as an integer);



a pointer into the Objective Caml heap;



a pointer pointing outside the Objective Caml heap.

The Objective Caml heap is the memory area that is managed by the Objective Caml garbage collector. C code can also allocate and manipulate data structures in its own memory space, and communicate pointers to these data structures to Objective Caml. Figure 12.4 shows the macros for classifying representations and converting between C integers and their Objective Caml representation. Note that C offers several integer Is long(v) Is block(v)

is v an Objective Caml integer? is v an Objective Caml pointer?

Long val(v) Int val(v) Bool val(v)

extract the integer contained in v, as a C ”long” extract the integer contained in v, as a C ”int” extract the boolean contained in v (0 if false, non-zero if true)

図 12.4: Classification of representations and conversion of immediate values.

types of varying sizes (short, int, long, etc), while Objective Caml has only one integer type, int.

4. Under Unix, this directory is /usr/local/lib/ocaml by default, or sometimes /usr/lib/ocaml. Under Windows, the default location is C: \OCAML\LIB, or the value of the environment variable CAMLLIB, if set.

Exploring Objective Caml values from C

327

Accessing immediate values All Objective Caml immediate values are represented as integers: •

integers are represented by their value;



characters are represented by their ASCII code5 ;



constant constructors are represented by an integer corresponding to their position in the datatype declaration: the nth constant constructor of a datatype is represented by the integer n − 1.

The following program defines a C function inspect that inspects the representation of its argument: #include #include value inspect (value v) { if (Is_long(v)) printf ("v is an integer (%ld) : %ld", (long) v, Long_val(v)); else if (Is_block(v)) printf ("v is a pointer"); else printf ("v is neither an integer nor a pointer (???)"); printf(" "); fflush(stdout) ; return v ; }

The function inspect tests whether its argument is an Objective Caml integer. If so, it prints the integer twice, first viewed as a C long integer (without conversion), then converted by the Long val macro, which extracts the actual integer represented in the argument. On the following example, we see that the machine representation of integers in Objective Caml differs from that of C: # external inspect : external inspect : ’a # inspect 123 ; ; The external function # inspect max int; ; The external function

’a → ’a = "inspect" ; ; -> ’a = "inspect" ‘inspect’ is not available ‘inspect’ is not available

We can also inspect values of other predefined types, such as char and bool: # inspect ’A’ ; ; The external function ‘inspect’ is not available # inspect true ; ; The external function ‘inspect’ is not available 5. More precisely, by their ISO Latin-1 code, which is an 8-bit character encoding extending ASCII with accented letters and signs for Western languages. Objective Caml does not yet handle wider internationalized character sets such as Unicode.

328

Chapter 12 : Interoperability with C

# inspect false ; ; The external function ‘inspect’ is not available # inspect [] ; ; The external function ‘inspect’ is not available

Consider the Objective Caml type foo defined thus: # type foo = C1 | C2 of int | C3 | C4 ; ;

The inspect function shows that constant constructors and non-constant constructors of this type are represented differently: # inspect C1 ; ; The external function ‘inspect’ is not available # inspect C4 ; ; The external function ‘inspect’ is not available # inspect (C2 1) ; ; The external function ‘inspect’ is not available

When the function inspect detects an immediate value, it prints first the “physical” representation of this value (i.e. the representation viewed as a word-sized C integer of C type long); then it prints the “logical” contents of this value (i.e. the Objective Caml integer it represents, as returned by the decoding macro Long val). The examples above show that the “physical” and the “logical” contents differ. This difference is due to the tag bit6 used by the garbage collector to distinguish immediate values from pointers (see chapter 9, page 255).

Representation of structured values Non-immediate Objective Caml values are said to be structured values. Those values are allocated in the Objective Caml heap and represented as a pointer to the corresponding memory block. All memory blocks contain a header word indicating the kind of the block as well as its size expressed in machine words. Figure 12.5 shows the structure of a block for a 32-bit machine. The two “color” bits are used by the garbage collector for walking the memory graph (see chapter 9, page 256). The “tag” field, or “tag” for short, contains the kind of the block. The “size” field contains the size of the block, in words, excluding the header. The macros listed in figure 12.6 return the tag and size of a block. The tag of a memory block can take the values listed in figure 12.7. Depending on the block tag, different macros are used to access the contents of the blocks. These macros are described in figure 12.8. When the tag is less than No scan tag, the heap block is structured as an array of Objective Caml value representations. Each element of the array is called a “field” of the memory block. In accordance with C and Objective Caml conventions, the first field is at index 0, and the last field is at index Wosize val(v) - 1. 6. Here, the tag bit is the least significant bit.

Exploring Objective Caml values from C

329

header array of value representations size 31

color 10 9

tag 8 7 0

図 12.5: Structure of an Objective Caml heap block.

Wosize val(v) Tag val(v)

return the size of the block v (header excluded) return the tag of the block v

図 12.6: Accessing header information in memory blocks.

As we did earlier for immediate values, we now define a function to inspect memory blocks. The C function print block takes an Objective Caml value representation, tests whether it is an immediate value or a memory block, and in the latter case prints the kind and contents of the block. It is called from the wrapper function inspect block, which can be called from Objective Caml. #include #include void margin (int n) { while (n-- > 0) printf(".");

return; }

void print_block (value v,int m) {

from 0 to No scan tag-1 Closure tag String tag Double tag Double array tag Abstract tag Final tag

an array of Objective Caml value representations a function closure a character string a double-precision float an array of float an abstract data type an abstract data type equipped with a finalization function 図 12.7: Tags of memory blocks.

330

Chapter 12 : Interoperability with C Field(v,n) Code val(v) string length(v) Byte(v,n) Byte u(v,n) String val(v) Double val(v) Double field(v,n)

return the nth field of v. return the code pointer for a closure. return the length of a string. return the n th character of a string, with C type char. same, but result has C type unsigned char. return the contents of a string with C type (char *). return the float contained in v. return the n th float contained in the float array v.

図 12.8: Accessing the content of a memory block. int size, i; margin(m); if (Is_long(v)) { printf("immediate value (%d)\n", Long_val(v)); return; }; printf ("memory block: size=%d - ", size=Wosize_val(v)); switch (Tag_val(v)) { case Closure_tag : printf("closure with %d free variables\n", size-1); margin(m+4); printf("code pointer: %p\n",Code_val(v)) ; for (i=1;i=No_scan_tag) { printf("unknown tag"); break; }; printf("structured block (tag=%d):\n",Tag_val(v)); for (i=0;i
Exploring Objective Caml values from C

331

value inspect_block (value v) { print_block(v,4); fflush(stdout); return v; }

Each possible tag for a block corresponds to a case of the switch construct. In the case of a block containing an array of Objective Caml values, we recursively call print block on each field of the array. We then redefine the inspect function: # external inspect : ’a → ’a = "inspect_block" ; ; external inspect : ’a -> ’a = "inspect_block"

We can now explore the representations of Objective Caml structured values. We must be careful not to apply inspect block to a cyclic value, since the recursive traversal of the value would then loop indefinitely.

Arrays, tuples, and records Arrays and tuples are represented by structured blocks. The nth field of the block contains the representation of the nth element of the array or tuple. # inspect [| 1; 2; 3 |] ; ; The external function ‘inspect_block’ is not available # inspect ( 10 , true , () ) ; ; The external function ‘inspect_block’ is not available

Records are also represented as structured blocks. The values of the record fields appear in the order given at record declaration time. Mutable fields and immutable fields are represented identically. # type foo = { fld1: int ; mutable fld2: int } ; ; type foo = { fld1 : int; mutable fld2 : int; } # inspect { fld1=10 ; fld2=20 } ; ; The external function ‘inspect_block’ is not available

警告

Nothing prevents a C function from physically modifying an immutable record field. It is the programmers’ responsibility to make sure that their C functions do not introduce inconsistencies in Objective Caml data structures.

Sum types We previously saw that constant constructors are represented like integers. A nonconstant constructor is represented by a block containing the constructor’s arguments, with a tag identifying the constructor. The tag associated with a non-constant constructor represents its position in the type declaration: the first non-constant constructor has tag 0, the second one has tag 1, and so on. # type foo = C1 of int * int * int | C2 of int | C3 | C4 of int * int ; ; type foo = C1 of int * int * int | C2 of int | C3 | C4 of int * int # inspect (C1 (1,2,3)) ; ;

332

Chapter 12 : Interoperability with C

The external function ‘inspect_block’ is not available # inspect (C4 (1,2)) ; ; The external function ‘inspect_block’ is not available

注意 The type list is a sum type whose declaration is: type ’a list = [] | :: of ’a * ’a list. This type has only one non-constant constructor ( :: ). Thus, a non-empty list is represented by a memory block with tag 0.

Character strings Characters inside strings occupy one byte each. Thus, the memory block representing a string uses one word per group of four characters (on a 32-bit machine) or eight characters (on a 64-bit machine).

警告

Objective Caml strings can contain the null character whose ASCII code is 0. In C, the null character represents the end of a string, and cannot appear inside a string.

#include #include value explore_string (value v) { char *s; int i,size; s = (char *) v; size = Wosize_val(v) * sizeof(value); for (i=0;i31) && (p<128)) printf("%c",s[i]); else printf("(#%u)",p); } printf("\n"); fflush(stdout); return v; }

The length and position of last character of an Objective Caml string are determined not by looking for a terminating null character, as in C, but by combining the size of the memory block that contains the string with the last byte of the last word of this block, which indicates the number of unused bytes in the last word. The following examples clarify the role played by this last byte. # external explore : string → string = "explore_string" ; ; external explore : string -> string = "explore_string"

Exploring Objective Caml values from C

333

# ignore(explore ""); ignore(explore "a"); ignore(explore "ab"); ignore(explore "abc"); ignore(explore "abcd"); ignore(explore "abcd\000") ; ; The external function ‘explore_string’ is not available

In the last two examples ("abcd" and "abcd\000"), the strings are of length 4 and 5 respectively. This explains why the last byte takes two different values, although the other bytes of the string representations are identical.

Floats and float arrays Objective Caml offers only one type (float) of floating-point numbers. This type corresponds to 64-bit, double-precision floating point numbers in C (type double). Values of type float are heap-allocated and represented by a memory block of size 2 words (on a 32-bit machine) or 1 word (on a 64-bit machine). # inspect 1.5 ; ; The external function ‘inspect_block’ is not available # inspect 0.0; ; The external function ‘inspect_block’ is not available

Arrays of floats are represented specially to reduce their memory occupancy: the floats contained in the array are stored consecutively in the memory block, rather than having each float heap-allocated separately. Therefore, float arrays possess a specific tag and specific access macros. # inspect [| 1.5 ; 2.5 ; 3.5 |] ; ; The external function ‘inspect_block’ is not available

This optimized representation encourages the use of Objective Caml for numerical computations that manipulate many float arrays: operations on array elements are much more efficient than if each float was heap-allocated separately.

警告

When allocating an Objective Caml float array from C, the size of the block should be the number of array elements multiplied by Double wosize. The Double wosize macro represents the number of words occupied by a double-precision float (2 words on a 32-bit machine, but only 1 word on a 64-bit machine).

With the exception of float arrays, floating-point numbers contained in other data structures are always treated as a structured, heap-allocated value. The following example shows the representation of a list of floats. # inspect [ 3.14; 1.2; 7.6]; ; The external function ‘inspect_block’ is not available

The list is viewed as a block with size 2, containing its head and its tail. The head of the list is a float, which is also a block of size 2.

334

Chapter 12 : Interoperability with C

Closures A function value is represented by the code to be executed when the function is applied, and by its environment (see chapter 2, page 23). There are two ways to build a function value: either by explicit abstraction (as in fun x -> x+1) or by partial application of a curried function (as in (fun x -> fun y -> x+y) 1). The environment of a closure can contain three kinds of variables: those declared globally, those declared locally, and the function parameters already instantiated by a partial application. The implementation treats those three kinds differently. Global variables are stored in a global environment that is not explicitly part of any closure. Local variables and instantiated parameters can appear in closures, as we now illustrate. A closure with an empty environment is simply a memory block containing a pointer to the code of the function: # let f = fun x y z → x+y+z ; ; val f : int -> int -> int -> int = # inspect f ; ; The external function ‘inspect_block’ is not available

Functions with free local variables are represented by closures with non-empty environments. Here, the closure contains both a pointer to the code of the function, and the values of its free local variables. # let g = let x = 1 and y = 2 in fun z → x+y+z ; ; val g : int -> int = # inspect g ; ; The external function ‘inspect_block’ is not available

The Objective Caml virtual machine treats partial applications of functions specially for better performance. A partial application of an abstraction is represented by a closure containing a value for each of the instantiated parameters, plus a pointer to the closure for the initial abstraction. # let a1 = f 1 ; ; val a1 : int -> int -> int = # inspect (a1) ; ; The external function ‘inspect_block’ is not available # let a2 = a1 2 ; ; val a2 : int -> int = # inspect (a2) ; ; The external function ‘inspect_block’ is not available

Figure 12.9 depicts the result of the inspection above. The function f has no free variables, hence the environment part of its closure is empty. The code pointer for a function with several arguments points to the code that should be called when all arguments are provided. In the case of f, this is the code corresponding to x+y+z. Partial applications of this function result in intermediate closures that point to a shared code (it is the same code pointer for a1 and a2). The role of this code is

Exploring Objective Caml values from C

f

335

header

a1

header

fun x y z -> ...

1

code... a2

header

1

2

図 12.9: Closure representation.

to accumulate the arguments and detect when all arguments have been provided. If so, it pushes all arguments and calls the actual code for the function body; if not, it creates a new closure. For instance, the application of a1 to 2 fails to provide all arguments to the function f (the last argument is still missing), hence a closure is created containing the first two arguments, 1 and 2. Notice that the closures resulting from partial applications always contain, in the first environment slot, a pointer to the original closure. The original closure will be called when all arguments have been gathered. Mixing local declarations and partial applications results in the following representation: # let g x = let y=2 in fun z → x+y+z ; ; val g : int -> int -> int = # let a1 = g 1 ; ; val a1 : int -> int = # inspect a1 ; ; The external function ‘inspect_block’ is not available

Abstract types Values of an abstract type are represented like those of its implementation type. Actually, type information is used only during type-checking and compilation. During execution, the types are not needed – only the memory representation (tag bits on values, size and tag fields on memory blocks) needs to be communicated to the garbage collector. For instance, a value of the abstract type ’a Stack.t is represented as a reference to a list, since the type ’a Stack.t is implemented as ’a list ref. # let p = Stack.create () ; ; val p : ’_a Stack.t =

336

Chapter 12 : Interoperability with C

# Stack.push 3 p; ; - : unit = () # inspect p; ; The external function ‘inspect_block’ is not available

On the other hand, some abstract types are implemented by representations that cannot be expressed in Objective Caml. Typical examples include arrays of weak pointers and input-output channels. Often, values of those abstract types are represented as memory blocks with tag Abstract tag. # let w = Weak.create 10; ; val w : ’_a Weak.t = # Weak.set w 0 (Some p); ; - : unit = () # inspect w; ; The external function ‘inspect_block’ is not available

Sometimes, a finalization function is attached to those values. Finalization functions are C functions which are called by the garbage collector just before the value is collected. They are very useful to free external resources, such as an input-output buffer, just before the memory block referring to those resources disappears. For instance, inspection of the “standard output” channel reveals that the type out channel is represented by abstract memory blocks with a finalization function: # inspect (stdout) ; ; The external function ‘inspect_block’ is not available

Creating and modifying Objective Caml values from C A C function called from Objective Caml can modify its arguments in place, or return a newly-created value. This value must match the Objective Caml type for the function result. For base types, several C macros are provided to convert a C datum to an Objective Caml value. For structured types, the new value must be allocated in the Objective Caml heap, with the correct size, and its fields initialized with values of the correct types. Considerable care is required here: it is easy to construct bad values from C, and these bad values may crash the Objective Caml program. Any allocation in the Objective Caml heap can trigger a garbage collection, which will deallocate unused memory blocks and may move live blocks. Therefore, any Objective Caml value manipulated from C must be registered with the Objective Caml garbage collector, if they are to survive the allocation of a new block. These values must be treated as extra memory roots by the garbage collector. To this end, several macros are provided for registering extra roots with the garbage collector. Finally, C code can allocate Objective Caml heap blocks that contain C data instead of Objective Caml values. This C data will then benefit from Objective Caml’s automatic memory management. If the C data requires explicit deallocation, a finalization function can be attached to the heap block.

Creating and modifying Objective Caml values from C

337

Modifying Objective Caml values The following macros allow the creation of immediate Objective Caml values from the corresponding C data, and the modification of structured values in place. Val Val Val Val Val Val

long(l) int(i) bool(x) true false unit

Store field(b,n,v) Store double field(b,n,d)

return the value representing the long integer l return the value representing the integer l return false if x=0, true otherwise the representation of true the representation of false the representation of () store the value v in the n-th field of block b store the float d in the n-th field of the float array b

図 12.10: Creation of immediate values and modification of structured blocks.

Moreover, the macros Byte and Byte u can be used on the left-hand side of an assignment to modify the characters of a string. The Field macro can also be used for assignment on blocks with tag Abstract tag or Final tag; use Store field for blocks with tag between 0 and No scan tag-1. The following function reverses a character string in place: #include value swap_char(value v, int i, int j) { char c=Byte(v,i); Byte(v,i)=Byte(v,j); Byte(v,j)=c; } value swap_string (value v) { int i,j,t = string_length(v) ; for (i=0,j=t-1; i string = "swap_string" # mirror "abcdefg" ; ; The external function ‘swap_string’ is not available

Allocating new blocks The functions listed in figure 12.11 allocate new blocks in the Objective Caml heap. The function alloc array takes an array of pointers a, terminated by a null pointer, and a conversion function f taking a pointer and returning a value. The result of alloc array is an Objective Caml array containing the results of applying f in turn to each pointer in

338

Chapter 12 : Interoperability with C

alloc(n, t) alloc tuple(n) alloc string(n) copy string(s) copy double(d) alloc array(f, a)

copy string array(p)

return a new block of size n words and tag t same, with tag 0 return an uninitialized string of length n characters return a string initialized with the C string s return a block containing the double float d return a block representing an array, initialized by applying the conversion function f to each element of the C array of pointers a, null-terminated. return a block representing an array of strings, obtained from the C string array p (of type char **), null-terminated.

図 12.11: Functions for allocating blocks.

a. In the following example, the function make str array uses alloc array to convert a C array of strings. #include value make_str (char *s) { return copy_string(s); } value make_str_array (char **p) { return alloc_array(make_str,p) ; }

It is sometimes necessary to allocate blocks of size 0, for instance to represent an empty Objective Caml array. Such a block is called an atom. # inspect [| |] ; ; The external function ‘inspect_block’ is not available

Because atoms are allocated statically and do not reside in the dynamic part of the Objective Caml heap, the allocation functions in figure 12.11 must not be used to allocate atoms. Instead, atoms are created in C by the macro Atom(t), where t is the desired tag for the block of size 0.

Storing C data in the Objective Caml heap It is sometimes convenient to use the Objective Caml heap to store arbitrary C data that does not respect the constraints imposed by the garbage collector. In this case, blocks with tag Abstract tag must be used. A natural example is the manipulation of native C integers (of size 32 or 64 bits) in Objective Caml. Since these integers are not tagged as the Objective Caml garbage collector expects, they must be kept in one-word heap blocks with tag Abstract tag. #include #include

Creating and modifying Objective Caml values from C

339

value Cint_of_OCAMLint (value v) { value res = alloc(1,Abstract_tag) ; Field(res,0) = Long_val(v) ; return res ; } value OCAMLint_of_Cint (value v)

{ return Val_long(Field(v,0)) ; }

value Cplus (value v1,value v2) { value res = alloc(1,Abstract_tag) ; Field(res,0) = Field(v1,0) + Field(v2,0) ; return res ; } value printCint (value v) { printf ("%d",(long) Field(v,0)) ; fflush(stdout) ; return Val_unit ; } # type cint external cint of int : int → cint = external int of cint : cint → int = external plus cint : cint → cint → external print cint : cint → unit =

"Cint_of_OCAMLint" "OCAMLint_of_Cint" cint = "Cplus" "printCint" ; ;

We can now work on native C integers, without losing the use of the tag bit, while remaining compatible with Objective Caml’s garbage collector. However, such integers are heap-allocated, instead of being immediate values, which renders arithmetic operations less efficient. # let a = 1000000000 ; ; val a : int = 1000000000 # a+a ; ; - : int = -147483648 # let c = let b = cint of int a in plus cint b b ; ; The external function ‘Cint_of_OCAMLint’ is not available # print cint c ; print newline () ; ; Characters 11-12: print_cint c ; print_newline () ;; ^ This expression has type Gc.control but is here used with type cint The type constructor cint would escape its scope # int of cint c ; ; Characters 12-13: int_of_cint c ;;

340

Chapter 12 : Interoperability with C

^ This expression has type Gc.control but is here used with type cint The type constructor cint would escape its scope

Finalization functions Abstract blocks can also contain pointers to memory blocks allocated outside the Objective Caml heap. We know that Objective Caml blocks that are no longer used by the program are deallocated by the garbage collector. But what happens to a block allocated in the C heap and referenced by an abstract block that was reclaimed by the GC? To avoid memory leaks, we can associate a finalization function to the abstract block; this function is called by the GC before reclaiming the abstract block. An abstract block with an attached finalization function is allocated via the function alloc final (n, f, used, max) . •

n is the size of the block, in words. The first word of the block is used to store the finalization function; hence the size occupied by the user data must be increased by one word.



f is the finalization function itself, with type void f (value). It receives the abstract block as argument, just before this block is reclaimed by the GC.



used represents the memory space (outside the Objective Caml heap) occupied by the C data. used must be ¡= max.



max is the maximum memory space outside the Objective Caml heap that we tolerate not being reclaimed immediately.

For efficiency reasons, the Objective Caml garbage collector does not reclaim heap blocks as soon as they become unused, but some time later. The ratio used/max controls the proportion of finalized abstract blocks that the garbage collector may leave allocated while they are no longer used. A ratio of 0 (that is, used = 0) lets the garbage collector work at its usual pace; higher ratios (no greater than 1) cause it to work harder and spend more CPU time finding unused finalized blocks and reclaiming them. The following program manipulates arrays of C integers allocated in the C heap via malloc. To allow the Objective Caml garbage collector to reclaim these arrays automatically, the create function wraps them in a finalized abstract block, containing both a pointer to the array and the finalization function finalize it. #include #include #include typedef struct { int size ; long * tab ; } IntTab ; IntTab *alloc_it (int s)

Creating and modifying Objective Caml values from C { IntTab *res = malloc(sizeof(IntTab)) ; res->size = s ; res->tab = (long *) malloc(sizeof(long)*s) ; return res ; } void free_it (IntTab *p) { free(p->tab) ; free(p) ; } void put_it (int n,long q,IntTab *p) { p->tab[n] = q ; } long get_it (int n,IntTab *p) { return p->tab[n]; } void finalize_it (value v) { IntTab *p = (IntTab *) Field(v,1) ; int i; printf("reclamation of an IntTab by finalization [") ; for (i=0;isize;i++) printf("%d ",p->tab[i]) ; printf("]\n"); fflush(stdout) ; free_it ((IntTab *) Field(v,1)) ; } value create (value s) { value block ; block = alloc_final (2, finalize_it,Int_val(s)*sizeof(IntTab),100000) ; Field(block,1) = (value) alloc_it(Int_val(s)) ; return block ; } value put (value n,value q,value t) { put_it (Int_val(n), Long_val(q), (IntTab *) Field(t,1)) ; return Val_unit ; } value get (value n,value t) { long res = get_it (Int_val(n), (IntTab *) Field(t,1)) ; return Val_long(res) ; }

The C functions visible from Objective Caml are: create, put and get. # type c int array external cia create : int → c int array = "create" external cia get : int → c int array → int = "get" external cia put : int→ int → c int array → unit = "put" ; ;

We can now manipulate our new data structure from Objective Caml: # let tbl = cia create 10 and tbl2 = cia create 10 in for i=0 to 9 do cia put i (i*2) tbl done ; for i=0 to 9 do print int (cia get i tbl) ; print string " " done ; print newline () ;

341

342

Chapter 12 : Interoperability with C

for i=0 to 9 do cia put (9-i) (cia get i tbl) tbl2 done ; for i=0 to 9 do print int (cia get i tbl2) ; print string " " done ; ; The external function ‘create’ is not available

We now force a garbage collection to check that the finalization function is called: # Gc.full major () ; ; - : unit = ()

In addition to freeing C heap blocks, finalization functions can also be used to close files, terminate processes, etc.

Garbage collection and C parameters and local variables A C function can trigger a garbage collection, either during an allocation (if the heap is full), or voluntarily by calling void Garbage_collection_function (). Consider the following example. Can you spot the error? #include #include value identity (value x) { Garbage_collection_function() ; return x; } # external id : ’a → ’a = "identity" ; ; external id : ’a -> ’a = "identity" # id [1;2;3;4;5] ; ; The external function ‘identity’ is not available

The list passed as parameter to id, hence to the C function identity, can be moved or reclaimed by the garbage collector. In the example, we forced a garbage collection, but any allocation in the Objective Caml heap could have triggered a garbage collection as well. The anonymous list passed to id was reclaimed by the garbage collector, because it is not reachable from the set of known roots. To avoid this, any C function that allocates anything in the Objective Caml heap must tell the garbage collector about the C function’s parameters and local variables of type value. This is achieved by using the macros described next. For parameters, these macros are used within the body of the C function as if they were additional declarations:

Creating and modifying Objective Caml values from C CAMLparam1(v) CAMLparam2(v1,v2) ... CAMLparam5(v1,. . .,v5) CAMLparam0 ;

: : : :

343

for one parameter v of type value for two parameters ... for five parameters required when there are no value parameters.

If the C function has more than five value parameters, the first five are declared with the CAMLparam5 macro, and the remaining parameters with the macros CAMLxparam1, . . ., CAMLxparam5, used as many times as necessary to list all value parameters. CAMLparam5(v1,. . .,v5); CAMLxparam5(v6,. . .,v10); CAMLxparam2(v11,v12);

:

for 12 parameters of type value

For local variables, these macros are used instead of normal C declarations of the variables. Local variables of type value must also be registered with the garbage collector, using the macros CAMLlocal1, . . ., CAMLlocal5. An array of values is declared with CAMLlocalN(tbl,n) where n is the number of elements of the array tbl. Finally, to return from the C function, we must use the macro CAMLreturn instead of C’s return construct. Here is the corrected version of the previous example: #include #include value identity2 (value x) { CAMLparam1(x) ; Garbage_collection_function() ; CAMLreturn x; } # external id : ’a → ’a = "identity2" ; ; external id : ’a -> ’a = "identity2" # let a = id [1;2;3;4;5] ; ; The external function ‘identity2’ is not available

We now obtain the expected result.

Calling an Objective Caml closure from C To apply a closure (i.e. an Objective Caml function value) to one or several arguments from C, we can use the functions declared in the header file callback.h.

344

Chapter 12 : Interoperability with C callback(f,v) callback2(f,v1,v2) callback3(f,v1,v2,v3) callbackN(f,n,tbl)

: : : :

apply same, same, same,

the closure f to the argument v, to two arguments, to three arguments, to n arguments stored in the array tbl.

All these functions return a value, which is the result of the application.

Registering Objective Caml functions with C The callback functions require the Objective Caml function to be applied as a closure, that is, as a value that was passed as an argument to the C function. We can also register a closure from Objective Caml, giving it a name, then later refer to the closure by its name in a C function. The function register from module Callback associates a name (of type string) with a closure or with any other Objective Caml value (of any type, that is, ’a). This closure or value can be recovered from C using the C function caml named value, which takes a character string as argument and returns a pointer to the closure or value associated with that name, if it exists, or the null pointer otherwise. An example is in order: # let plus x y = x + y ; ; val plus : int -> int -> int = # Callback.register "plus3_ocaml" (plus 3); ; - : unit = () #include #include #include value plus3_C (value v) { CAMLparam1(v); CAMLlocal1(f); f = *(caml_named_value("plus3_ocaml")) ; CAMLreturn callback(f,v) ; } # external plusC : int → int = "plus3_C" ; ; external plusC : int -> int = "plus3_C" # plusC 1 ; ; The external function ‘plus3_C’ is not available # Callback.register "plus3_ocaml" (plus 5); ; - : unit = () # plusC 1 ; ; The external function ‘plus3_C’ is not available

Do not confuse the declaration of a C function with external and the registration of an Objective Caml closure with the function register. In the former case, the

Exception handling in C and in Objective Caml

345

declaration is static, the correspondence between the two names is established at link time. In the latter case, the binding is dynamic: the correspondence between the name and the closure is performed at run time. In particular, the name–closure binding can be modified dynamically by registering a different closure with the same name, thus modifying the behavior of C functions using that name.

Exception handling in C and in Objective Caml Different languages have different mechanisms for raising and handling exceptions: C relies on setjmp and longjmp, while Objective Caml has built-in constructs for exceptions (try ... with, raise). Of course, these mechanisms are not compatible: they do not keep the same information when setting up a handler. It is extremely hard to safely implement the nesting of exception handlers of different kinds, while ensuring that an exception correctly “jumps over” handlers. For this reason, only Objective Caml exceptions can be raised and handled from C; setjmp and longjmp in C cannot be caught from Objective Caml, and must not be used to skip over Objective Caml code. All functions and macros introduced in this section are defined in the header file fail.h.

Raising a predefined exception From a C function, it is easy to raise one of the exceptions Failure, Invalid argument or Not found from the Pervasives module: just use the following functions. failwith(s) invalid argument(s) raise not found()

: : :

raise the exception Failure(s) raise the exception Invalid argument(s) raise the exception Not found

In the first two cases, s is a C string (char *) that ends up as the argument to the exception raised.

Raising a user-defined exception A registration mechanism similar to that for closures enables user-defined exceptions to be raised from C. We must first register the exception using the Callback module’s register exception function. Then, from C, we retrieve the exception identifier using the caml named value function (see page 344). Finally, we raise the exception, using one of the following functions:

346 raise constant(e) raise with arg(e,v) raise with string(e,s)

Chapter 12 : Interoperability with C raise the exception e with no argument, raise the exception e with the value v as argument, same, but the argument is taken from the C string s.

Here is an example C function that raises an Objective Caml exception: #include #include #include value divide (value v1,value v2) { CAMLparam2(v1,v2); if (Long_val(v2) == 0) raise_with_arg(*caml_named_value("divzero"),v1) ; CAMLreturn Val_long(Long_val(v1)/Long_val(v2)) ; }

And here is an Objective Caml transcript showing the use of that C function: # external divide : int → int → int = "divide" ; ; external divide : int -> int -> int = "divide" # exception Division zero of int ; ; exception Division_zero of int # Callback.register exception "divzero" (Division zero 0) ; ; - : unit = () # divide 20 4 ; ; The external function ‘divide’ is not available # divide 22 0 ; ; The external function ‘divide’ is not available

Catching an exception In a C function, we cannot catch an exception raised from another C function. However, we can catch Objective Caml exceptions arising from the application of an Objective Caml function (callback). This is achieved via the functions callback exn, callback2 exn, callback3 exn and callbackN exn, which are similar to the standard callback functions, except that if the callback raises an exception, this exception is caught and returned as the result of the callback. The result value of the callback exn functions must be tested with Is exception result(v); this predicate returns “true” if the result value represents an uncaught exception, and “false” otherwise. The macro Extract exception(v) returns the exception value contained in an exceptional result value.

Exception handling in C and in Objective Caml

347

The C function divide print below calls the Objective Caml function divide using callback2 exn, and checks whether the result is an exception. If so, it prints a message and raises the exception again; otherwise it prints the result. #include #include #include #include #include



value divide_print (value v1,value v2) { CAMLparam2(v1,v2) ; CAMLlocal3(div,dbz,res) ; div = * caml_named_value("divide") ; dbz = * caml_named_value("div_by_0") ; res = callback2_exn (div,v1,v2) ; if (Is_exception_result(res)) { value exn=Extract_exception(res); if (Field(exn,0)==dbz) printf("division by 0\n") ; else printf("other exception\n"); fflush(stdout); if (Wosize_val(exn)==1) raise_constant(Field(exn,0)) ; else raise_with_arg(Field(exn,0),Field(exn,1)) ; } printf("result = %d\n",Long_val(res)) ; fflush(stdout) ; CAMLreturn Val_unit ; } # Callback.register "divide" (/) ; ; - : unit = () # Callback.register exception "div_by_0" Division by zero ; ; - : unit = () # external divide print : int → int → unit = "divide_print" ; ; external divide_print : int -> int -> unit = "divide_print" # divide print 42 3 ; ; The external function ‘divide_print’ is not available # divide print 21 0 ; ; The external function ‘divide_print’ is not available

As the examples above show, it is possible to raise an exception from C and catch it in Objective Caml, and also to raise an exception from Objective Caml and catch it in C. However, a C program cannot by itself raise and catch an Objective Caml exception.

348

Chapter 12 : Interoperability with C

Main program in C Until now, the entry point of our programs was in Objective Caml; the program could then call C functions. Nothing prevents us from writing the entry point in C, and having the C code call Objective Caml functions when desired. To do this, the program must define the usual C main function. This function will then initialize the Objective Caml runtime system by calling the function caml main(char **), which takes as an argument the array of command-line arguments that corresponds to the Sys.argv array in Objective Caml. Control is then passed to the Objective Caml code using callbacks (see page 343).

Linking Objective Caml code with C The Objective Caml compiler can output C object files (with extension .o) instead of Objective Caml object files (with extension .cmo or .cmx). All we need to do is set the -output-obj compiler flag. ocamlc -output-obj files.ml ocamlopt -output-obj.cmxa files.ml From the Objective Caml source files, an object file with default name camlprog.o is produced. The final executable is obtained by linking, using the C compiler, and adding the library -lcamlrun if the Objective Caml code was compiled to bytecode, or the library -lasmrun if it was compiled to native code. cc camlprog.o filesC.o -lcamlrun cc camlprog.o filesC.o -lasmrun Calling Objective Caml functions from the C program is performed as described previously, via the callback functions. The only difference is that the initialization of the Objective Caml runtime system is performed via the function caml startup instead of caml main.

Exercises Polymorphic Printing Function We wish to define a printing function print with type ’a -> unit able to print any Objective Caml value. To this end, we extend and improve the inspect function. 1.

In C, write the function print ws which prints Objective Caml as follows:

Exercises • • • • • • The 2.

349 immediate values: as C integers; strings: between quotes; floats: as usual; arrays of floats: between [| |] closures: as < code, env > everything else: as a tuple, between ( ) function should handle structured types recursively.

To avoid looping on circular values, and to display sharing properly, modify this function to keep track of the addresses of heap blocks it has already seen. If an address appears several times, name it when it is first printed (v = name), and just print the name when this address is encountered again. (a) Define a data structure to record the addresses, determine when they occur several times, and associate a name with each address. (b) Traverse the value once first to determine all the addresses it contains and record them in the data structure. (c) The second traversal prints the value while naming addresses at their first occurrences. (d) Define the function print combining both traversals.

Matrix Product 1.

Define an abstract type float matrix for matrices of floating-point numbers.

2.

Define a C type for these matrices.

3.

Write a C function to convert values of type float array array to values of type float matrix.

4.

Write a C function performing the reverse conversion.

5.

Add the C functions computing the sum and the product of these matrices.

6.

Interface them with Objective Caml and use them.

Counting Words: Main Program in C The Unix command wc counts the number of characters, words and lines in a file. The goal of this exercise is to implement this command, while counting repeated words only once. 1.

Write the program wc in C. This program will simply count words, lines and characters in the file whose name is passed on the command line.

2.

Write in Objective Caml a function add word that uses a hash table to record how many times the function was invoked with the same character string as argument.

3.

Write two functions num repeated words and num unique words counting respectively the number of word repetitions and the number of unique words, as determined from the hash table built by add word.

350

Chapter 12 : Interoperability with C

4.

Register the three previous functions so that they can be called from a C program.

5.

Rewrite the main function of the wc program so that it prints the number of unique words instead of the number of words.

6.

Write the main function and the commands required to compile this program as an Objective Caml program.

7.

Write the main function and the commands required to compile this program as a C program.

Summary This chapter introduced the interface between the Objective Caml language and the C language. This interface allows C functions to operate on Objective Caml values. Using abstract Objective Caml types, the converse is also possible. An important feature of this interface is the ability to use the Objective Caml garbage collector to perform automatic reclamation of values created in C. This interface supports the combination, in the same program, of components developed in the two languages. Finally, Objective Caml exceptions can be raised and (with some limitations) handled from C.

To Learn More For a better understanding of the C language, especially argument passing and data representations, the book C: a reference manual [HS94] is highly recommended. Concerning exceptions and garbage collection, several works add these missing features to C. The technical report [Rob89] describes an implementation of exceptions in C, based on open macros and on the setjmp and longjmp functions from the C library. Hans Boehm distributes a conservative collector with ambiguous roots that can be added (as a library) to any C program: リンク: http://www.hpl.hp.com/personal/Hans Boehm/gc/

Concerning interoperability between Objective Caml and C, the tools described in this chapter are rather low-level and difficult to use. However, they give the programmer full control on copying or sharing of data structures between the two languages. A higherlevel tool called CamlIDL is available; it automatically generates the Objective Caml “stubs” (encapsulation functions) for calling C functions and converting data types. The C types and functions are described in a language called IDL (Interface Definition Language), similar to a subset of C++ and C. This description is then passed through the CamlIDL compiler, which generates the corresponding .mli, .ml and .c files. This tool is distributed from the following page: リンク: http://caml.inria.fr/camlidl/

To Learn More

351

Other interfaces exist between Objective Caml and languages other than C. They are available on the “Caml hump” page: リンク: http://caml.inria.fr/humps/index.html

They include several versions of interfaces with Fortran, and also an Objective Caml bytecode interpreter written in Java. Finally, interoperability between Objective Caml and other languages can also be achieved via data exchanges between separate programs, possibly over the network. This approach is described in the chapter on distributed programming (see chapter 20).

13 Applications この章では、3章で説明しなかったプログラミングの概念を使って、2つのアプリケー ションを作ってみます。 最初に Awi(Application Window Interface) と呼ばれるグラフィックコンポーネントライ ブラリを作り、それを使って日本円からユーロへの換算ソフトを作ります。コンポーネ ントライブラリはユーザーの入力に反応してイベントハンドラを呼び出します。アルゴ リズムとしては簡単なアプリケーションですが、コンポーネント間通信の仕組みとして クロージャを使う利点が分かります。実際、様々なイベントハンドラが、環境を通して 値を共有しています。Awi の構造を理解するには、基となる Graphics ライブラリを理 解しておくのがいいでしょう。(5 章, 117 ページ参照). 2つ目は、有向グラフにおける最小コスト経路を探索するアプリケーションです。これ には、起点に繋がっている全てのノードについて最小コスト経路を計算するダイクスト ラ法を使います。weak pointers (see page 267) の表によるキャッシュ機能を使って探索 を高速化します。GC はこの表の要素をいつでも解放することができますが、表の要素 は必要に応じて再計算されます。グラフを表示する部分では、始点と終点を指定するた めに Awi ライブラリに含まれる簡単なボタンを使います。その後、キャッシュを使った 時と使わない時のアルゴリズムの実行効率を比較します。簡単に計測できるように、グ ラフ構造、始点、終点の情報が含まれるファイルを用意し、それをプログラムの引数に 渡します。最後に、探索プログラムに簡単なグラフィカルインターフェースを付け加え ます。

グラフィカルインターフェースの構築 Graphics ライブラリのように十分強力とは言えないツールしか使えない場合、プログ ラムにグラフィカルなインターフェースを実装することは退屈な作業です。プログラム の使いやすさの一部はそのインターフェースによります。グラフィカルインターフェー スを作る作業を簡単にするために、まず Graphics の上位に位置する Awi と呼ばれる新

354

Chapter 13 : Applications

しいライブラリを作ることから始めましょう。私達はこの単純なモジュールを使ってア プリケーションのインターフェースを作る事にします。 このグラフィカルインターフェースはコンポーネントを扱います。コンポーネントとは メインウィンドウ内のある範囲であり、グラフィカルなものを表示したり送られたイベ ントを処理したりします。コンポーネントには 2 種類あり、確認ボタンやテキスト入力 フィールドなどの単純なコンポーネントと、他のコンポーネントを含むことができるコ ンテナに分かれます。1 つのコンポーネントはただ 1 つのコンテナに所属することがで きます。このため、アプリケーションのインターフェースはメインコンテナ(グラフィッ クスウィンドウ)に対応するルート要素を持つ木として構築されます。節もコンテナで あり、葉は単純なコンポーネントまたは空のコンテナです。この木のような構造はユー ザーとのやりとりから生じるイベントを伝播させるのに適しています。あるコンテナが イベントを受け取ると、コンテナは自分の子要素がイベントを処理できるなら子要素に イベントを送信し、処理できないなら自分で処理します。 コンポーネントはこのライブラリに欠くことのできない要素です。私達はコンポーネント を、サイズ、グラフィックコンテキスト、親/子のコンポーネント、自分を描画する関数、 イベントを処理する関数を含むレコードとして定義します。コンテナは自分が持つコン ポーネントを描画させる関数を含みます。component 型を定義するために、グラフィッ クコンテキスト、イベント、初期化オプションのための型を定義することにします。グラ フィカルコンテキストは、背景色、前景色や表示位置、用いるフォントのような “グラフィ カルスタイル” を保持するのに使います。そして私達はコンポーネントに送られうるい ろいろな種類のイベントを定義する必要があります。これらは基になっている Graphics ライブラリで定義されているイベントよりも多くの種類からなります。私達はグラフィッ クコンテキストやコンポーネントを構成しやすいように単純なオプションの仕組みを取 り入れます。 一般的なイベント処理ループは Graphics ライブラリの入力関数から物理的なイベント を受け取り、これらのイベントの結果他のイベントが引き起こされるべきかどうかを決 め、それらをルートコンテナに送ります。ここではコンポーネントとしてテキスト、ボ タン、リストボックス、入力範囲、リッチコンポーネントを考えることにします。次で は、フランからユーロへの換算プログラムを用いてグラフィカルインターフェースを構 築するためのコンポーネントの組み立て方を見ていきます。

グラフィックスコンテキスト、イベント、オプション グラフィックスコンテキスト、イベント、オプションの初期化/変更関数を定義する前に、 まずそれらの基本となる型を定義しましょう。グラフィカルオブジェクトを作る関数を パラメタライズしやすいように、オプション型も定義することにします。

グラフィックスコンテキスト グラフィックスコンテキストは前景色、背景色、フォント、フォントサイズ、カーソル位 置、線の幅を保持することができます。これより、次の型が定義できます。 type

g context = { mutable bcol : Graphics.color;

グラフィカルインターフェースの構築 mutable mutable mutable mutable mutable mutable

355

fcol : Graphics.color; font : string; font size : int; lw : int; x : int; y : int }; ;

make default context 関数はデフォルト値で初期化された新しいグラフィックスコン テキストを生成します1 。 # let let let {

default font = "fixed" default font size = 12 make default context () = bcol = Graphics.white; fcol = Graphics.black; font = default font; font size = default font size; lw = 1; x = 0; y = 0;}; ; val default_font : string = "fixed" val default_font_size : int = 12 val make_default_context : unit -> g_context =

各フィールドにアクセスする関数は、その型の定義を知ることなく値を取得する事がで きます。 # let get gc bcol gc = gc.bcol let get gc fcol gc = gc.fcol let get gc font gc = gc.font let get gc font size gc = gc.font size let get gc lw gc = gc.lw let get gc cur gc = (gc.x,gc.y); ; val get_gc_bcol : g_context -> Graphics.color = val get_gc_fcol : g_context -> Graphics.color = val get_gc_font : g_context -> string = val get_gc_font_size : g_context -> int = val get_gc_lw : g_context -> int = val get_gc_cur : g_context -> int * int =

各フィールドの変更を行う関数は同じ原理で動きます。 # let set gc bcol gc c = gc.bcol <- c

1. フォント名はシステム環境に依存します。

356

Chapter 13 : Applications

let set gc fcol gc c = gc.fcol <- c let set gc font gc f = gc.font <- f let set gc font size gc s = gc.font size <- s let set gc lw gc i = gc.lw <- i let set gc cur gc (a,b) = gc.x<- a; gc.y<-b; ; val set_gc_bcol : g_context -> Graphics.color -> unit = val set_gc_fcol : g_context -> Graphics.color -> unit = val set_gc_font : g_context -> string -> unit = val set_gc_font_size : g_context -> int -> unit = val set_gc_lw : g_context -> int -> unit = val set_gc_cur : g_context -> int * int -> unit =

これにより、私達は g context 型の新しいコンテキストを生成し、各フィールドへの読 み書きすることができます。

use gc 関数はグラフィックコンテキストをグラフィカルウィンドウに適用します。 # let use gc gc = Graphics.set color (get gc fcol gc); Graphics.set font (get gc font gc); Graphics.set text size (get gc font size gc); Graphics.set line width (get gc lw gc); let (a,b) = get gc cur gc in Graphics.moveto a b; ; val use_gc : g_context -> unit =

背景色のような一部のデータは Graphics により直接使われないため、use gc 関数には 現れません。

イベント Graphics ライブラリには、マウスのクリック/移動、キーの押下という限られたイベン トしか定義されていません。コンポーネントから発生する統合されたイベントの種類を 豊富にしたいので、rich event を定義することにします。 # type rich event = MouseDown | MouseUp | MouseDrag | MouseMove | MouseEnter | MouseExit | Exposure | GotFocus | LostFocus | KeyPress | KeyRelease; ;

このようなイベントを生成するには、以前のイベントを覚えておく必要があります。 MouseDown、MouseMove イベントはマウスのクリック/移動に対応するイベントで、Graphics ライブラリにより生成されます。他のマウスイベントは、前回のイベントが MouseUp か、 物理的な MouseExit イベントを扱った最後のコンポーネントによって生成されます。 Exposure イベントはコンポーネントの再描画要求に対応します。focus の概念は、コン

グラフィカルインターフェースの構築

357

ポーネントがある種のイベントに関連づけられるというものです。典型的には、フォーカ スが設定されたコンポーネントへテキストが入力されるという事は、そのコンポーネン トだけが KeyPress/KeyRelease イベントを処理することができるという意味です。テ キスト入力コンポーネントに対する MouseDown イベントは、フォーカスをそのコンポー ネントに設定し、これまでフォーカスを持っていたコンポーネントからフォーカスを取 り除きます。 これらの新しいイベントは 362 で述べるイベント処理ループの中で生成されます。

オプション グラフィカルインターフェースには、グラフィカルオブジェクト(コンポーネント/グ ラフィカルコンテキスト)の生成オプションを記述するためのルールが必要です。ある 色を持つグラフィックコンテキストを作りたいなら、現状ではそれをまずデフォルトの 値で作成し、その後で色を変更する 2 つの関数を呼ぶ必要があります。もっと複雑なグ ラフィックオブジェクトの場合、この作業はすぐに退屈になります。私達はコンポーネ ントライブラリを強化するためにこれらのオプションを拡張したいので、“拡張可能な” 直和型が必要となります。そのようなもののうち Objective Caml から提供されている 唯一のものが、例外に使われる exn 型です。オプションを扱うために exn を使うとプロ グラムの明快さに影響を及ぼすので、この型は本当の例外のためにだけ使うことにしま す。その代わりに、文字列で表される疑似コンストラクタを使うことで拡張可能な直和 型をシミュレートすることにします。オプションの値のために、opt val を定義します。 オプションはタプルであり、最初の要素にオプション名、次の要素にその値が入ります。 lopt 型はこのようなオプションのリストを持つものとします。 # type opt val = Copt of Graphics.color | Sopt of string | Iopt of int | Bopt of bool; ; # type lopt = (string * opt val) list ; ;

オプションを解読する関数は、オプションのリストとオプション名とデフォルト値を引 数として受け取ります。もしオプション名がリストに所属していたら、対応する値を返 します。そうでなければデフォルト値を返します。ここでは整数値と真偽値のオプショ ン値を返す関数を示しますが、他の型についても同様です。 # exception OptErr; ; exception OptErr # let theInt lo name default = try match List.assoc name lo with Iopt i → i | _ → raise OptErr with Not found → default; ; val theInt : (’a * opt_val) list -> ’a -> int -> int = # let theBool lo name default = try

358

Chapter 13 : Applications

match List.assoc name lo with Bopt b → b | _ → raise OptErr with Not found → default; ; val theBool : (’a * opt_val) list -> ’a -> bool -> bool =

これで、次のようにオプションのリストを指定してグラフィックコンテキストを生成す る関数を書くことが出来るようになりました。 # let set gc gc lopt = set gc bcol gc (theColor lopt "Background" (get gc bcol gc)); set gc fcol gc (theColor lopt "Foreground" (get gc fcol gc)); set gc font gc (theString lopt "Font" (get gc font gc)); set gc font size gc (theInt lopt "FontSize" (get gc font size gc)); set gc lw gc (theInt lopt "LineWidth" (get gc lw gc)); ; val set_gc : g_context -> (string * opt_val) list -> unit =

これで、オプションの指定順序を気にしなくていいようになりました。 # let dc = make default context () in set gc dc [ "Foreground", Copt Graphics.blue; "Background", Copt Graphics.yellow]; dc; ; - : g_context = {bcol = 16776960; fcol = 255; font = "fixed"; font_size = 12; lw = 1; x = 0; y = 0}

あいにく部分的に型体系から逃げていますが、これでかなり柔軟なシステムになりまし た。オプション名は string 型なので存在しない名前のオプションを作ることも可能で すが、単にその値は無視されるだけです。

コンポーネントとコンテナ コンポーネントはこのライブラリの本質的な要素です。私達は、コンポーネントを生成 しそれを使って簡単にインターフェースを構築できるようにしたいです。コンポーネン トは自分自身を表示し、自分に送られたイベントを認識し、それを処理しなければなり ません。コンテナは他のコンポーネントとの間でイベントの受け渡しをする必要があり ます。コンポーネントはただ 1 つのコンテナにだけ属することができるものとします。

コンポーネントの構築 component 型の値は次のものを持ちます。サイズ(w、h)、メインウィンドウ内での絶 対座標(x、y)、描画される時に使われるグラフィックスコンテキスト(gc)、コンテ

グラフィカルインターフェースの構築

359

ナかどうかを表すフラグ(container)、コンテナに所属している場合はそのコンテナ (parent)、子コンポーネントのリスト(children)、コンポーネントの位置を操作する ための 4 つの関数。子コンポーネントをどう配置するか(layout)、コンポーネントが どう表示されるか(display)、与えられた点がコンポーネントの表示範囲に含まれるか どうかを返す関数(mem)、イベントを処理したら true、しなかったら false を返すイ ベントハンドラ(listener)。listener の引数は rich status 型で、これは Graphics モジュールからから来る低レベルのイベント情報、キーボードフォーカス、一般フォー カス、最後にイベントを処理したコンポーネントを含んでいます。というわけで、次の ような相互再帰的な定義になります。 # type component = { mutable info : string; mutable x : int; mutable y : int; mutable w :int ; mutable h : int; mutable gc : g context; mutable container : bool; mutable parent : component list; mutable children : component list; mutable layout options : lopt; mutable layout : component → lopt → unit; mutable display : unit → unit; mutable mem : int * int → bool; mutable listener : rich status → bool } and rich status = { re : rich event; stat : Graphics.status; mutable key focus : component; mutable gen focus : component; mutable last : component}; ;

コンポーネントのデータフィールドへのアクセスにはこれらの関数を使います。 # let get gc c = c.gc; ; val get_gc : component -> g_context = # let is container c = c.container; ; val is_container : component -> bool =

次の 3 つの関数はコンポーネントのデフォルトの振る舞いを定義しています。与えられた マウスポインタの座標があるコンポーネントの in rect に適用されると、その座標がコ ンポーネントで定義された矩形に含まれるかどうかを返します。デフォルトの描画関数 (display rect)はコンポーネントのグラフィックコンテキストに設定された背景色によ りコンポーネントの矩形を塗り潰します。デフォルトのレイアウト関数(direct layout) はコンテナの左上を基準とする相対座標にコンポーネントを配置します。有効なオプショ ン名は"PosY"と"PosX"で、これはコンテナ座標を基準とした相対座標に対応します。

360

Chapter 13 : Applications

# let in rect c (xp,yp) = (xp >= c.x) && (xp < c.x + c.w) && (yp >= c.y) && (yp < c.y + c.h) ; ; val in_rect : component -> int * int -> bool = # let display rect c () = let gc = get gc c in Graphics.set color (get gc bcol gc); Graphics.fill rect c.x c.y c.w c.h ; ; val display_rect : component -> unit -> unit = # let direct layout c c1 lopt = let px = theInt lopt "PosX" 0 and py = theInt lopt "PosY" 0 in c1.x <- c.x + px; c1.y <- c.y + py ; ; val direct_layout : component -> component -> (string * opt_val) list -> unit =

これで、パラメータとして幅と高さをとる create component 関数や前述の関数を使っ てコンポーネントを生成することができるようになりました。 # let create component iw ih = let dc = {info="Anonymous"; x=0; y=0; w=iw; h=ih; gc = make default context () ; container = false; parent = [] ; children = [] ; layout options = [] ; layout = (fun a b → () ); display = (fun () → () ); mem = (fun s → false); listener = (fun s → false);} in dc.layout <- direct layout dc; dc.mem <- in rect dc; dc.display <- display rect dc; dc ; ; val create_component : int -> int -> component =

それでは次のように空のコンポーネントを宣言してみます。 # let empty component = create component 0 0 ; ;

この値は、少なくとも 1 つのコンポーネントを含まなければならない値を生成する際の デフォルト値として使われます。(例えば rich status)

グラフィカルインターフェースの構築

361

子コンポーネントの追加 コンテナにコンポーネントを追加する上で厄介なのは、コンポーネントをコンテナのど の位置に配置するかという事です。layout フィールドはこの配置関数を保持します。こ の関数は子コンポーネントとオプションリストを受け取り、コンテナ内での子コンポー ネントの新しい座標を計算します。配置関数によっては異なるオプションが使われる事 もあります。この後 panel コンポーネントについて述べる時にいくつかの配置関数を説 明します。 (368 ページを参照)ここでは、コンポーネントツリー内での表示関数の伝搬、 座標の変化、イベントの伝搬の仕組みを簡単に説明します。アクションの伝搬には、リ ストの全ての要素に対して指定した関数を適用する List.iter 関数を使います。

change coord 関数は座標の相対的な変化をコンポーネントやその子コンポーネントの 座標に適用します。 # let rec change coord c (dx,dy) = c.x <- c.x + dx; c.y <- c.y + dy; List.iter (fun s → change coord s (dx,dy) ) c.children; ; val change_coord : component -> int * int -> unit =

add component 関数はコンポーネントを追加できるかどうかをチェックした上で親(c) と子(c1)を関連づけます。配置オプションのリストは子コンポーネントにより保持さ れ、親コンポーネントの配置関数が変化した時に再利用されます。この関数に渡された オプションリストは配置関数により使われます。コンポーネントを追加できない場合は 3 通りあり、それらはそのコンポーネントが既に子になっている場合と、親がコンテナ でない場合と、子が親より大きい場合です。 # let add component c c1 lopt = if c1.parent <> [] then failwith "add_component: already a parent" else if not (is container c ) then failwith "add_component: not a container" else if (c1.x + c1.w > c.w) || (c1.y + c1.h > c.h) then failwith "add_component: bad position" else c.layout c1 lopt; c1.layout options <- lopt; List.iter (fun s → change coord s (c1.x,c1.y)) c1.children; c.children <- c1 :: c.children; c1.parent <- [c] ; ; val add_component : component -> component -> lopt -> unit =

ツリーのある場所からのコンポーネントの除去は、次の関数により行われます。親子間 の関連付け、子やその全ての子の座標が変更されます。

362

Chapter 13 : Applications

# let remove component c c1 = c.children <- List.filter ((!=) c1) c.children; c1.parent <- List.filter ((!=) c) c1.parent; List.iter (fun s → change coord s (- c1.x, - c1.y)) c1.children; c1.x <- 0; c1.y <- 0; ; val remove_component : component -> component -> unit =

コンテナの配置関数が変化するときの処理は、そのコンテナが子を持っているかどうか に依存します。何も変化しなければ処理は簡単です。そうでなければまずコンテナの全 ての子を除去し、コンテナの配置関数を更新した後再び全ての子を以前と同じオプショ ンで追加します。 # let set layout f c = if c.children = [] then c.layout <- f else let ls = c.children in List.iter (remove component c) ls; c.layout <- f; List.iter (fun s → add component c s s.layout options) ls; ; val set_layout : (component -> lopt -> unit) -> component -> unit =

これが配置オプションのリストを保持しておく理由です。オプションリストが新しい配 置関数により認識されない場合は、デフォルト値が使われるでしょう。 コンポーネントが表示されると、子コンポーネントに対して表示イベントが伝搬されな ければなりません。子同士は重ならないので、表示の順番は重要ではありません。 # let rec display c = c.display () ; List.iter (fun cx → display cx ) c.children; ; val display : component -> unit =

イベントハンドリング 物理イベント(マウスクリック、キー押下、マウス移動)を処理するには、ユーザの操作 に反応して物理ステータス(Graphics.status 型)を返す Graphics.wait next event 関数を使います。 (132 ページ参照)この物理ステータスは、イベント(rich event 型)、 物理ステータス、キーボードフォーカスと一般フォーカスを処理しているコンポーネント や最後にこれらを正常に処理したコンポーネントを含むリッチステータス(rich status 型)を計算するのに使われます。一般フォーカスは全てのイベントを受け付けるフォー カスです。

グラフィカルインターフェースの構築

363

次にリッチイベントの操作、ステータス情報のコンポーネントへの伝搬、情報の生成、 メインイベント処理ループを行う関数を説明します。

ステータス関連の関数 これらの関数はマウスポインタの位置やフォーカスを持っているかどうかを取得します。 フォーカス関連の関数にはもう 1 つ、フォーカスを設定したり解除したりするコンポー ネントのパラメータが必要です。 # # # #

let let let let

get get get get

# # # # # #

let let let let let let

has key focus e c = e.key focus == c; ; take key focus e c = e.key focus <- c; ; lose key focus e c = e.key focus <- empty component; ; has gen focus e c = e.gen focus == c; ; take gen focus e c = e.gen focus <- c; ; lose gen focus e c = e.gen focus <- empty component; ;

event mouse mouse key e

e x y =

= e.re; ; e = e.stat.Graphics.mouse x; ; e = e.stat.Graphics.mouse y; ; e.stat.Graphics.key; ;

イベントの伝搬 リッチイベントはコンポーネントに送信され処理されます。前に述べた表示の仕組みに 似て、子コンポーネントは親よりも高い優先度で単純なマウス移動イベントを処理しま す。コンポーネントがイベントに関連づけられたステータス情報を受け取ると、それを 処理できる子がいるかどうかを調べます。いるなら子は true を返し、そうでなければ false を返します。もしイベントを処理できる子がいなければ、親は自分の listener フィールドに設定された関数を使います。 キーボードの働きに対応するステータス情報の伝搬方法はこれと異なります。親コンポー ネントはキーボードフォーカスを持っているならイベントを処理し、そうでなければ子 に伝搬させます。 あるイベントを処理した結果として生成されるイベントもあります。例えば、コンポー ネントがフォーカスを取得したという事は別のコンポーネントがフォーカスを失ったと いう事です。そのようなイベントは対象となるコンポーネントによりすぐに処理されま す。この仕組みは異なるコンポーネント間のマウス移動に伴う entry/exit イベントでも 同じです。

send event 関数は rich status 型の値とコンポーネントを受け取り、イベントが処理 されたかどうかを表す真偽値を返します。 # let rec send event rs c = match get event rs with

364

Chapter 13 : Applications

MouseDown | MouseUp | MouseDrag | MouseMove → if c.mem(get mouse x rs, get mouse y rs) then if List.exists (fun sun → send event rs sun) c.children then true else ( if c.listener rs then (rs.last <-c; true) else false ) else false | KeyPress | KeyRelease → if has key focus rs c then ( if c.listener rs then (rs.last<-c; true) else false ) else List.exists (fun sun → send event rs sun) c.children | _ → c.listener rs; ; val send_event : rich_status -> component -> bool =

コンポーネントの階層構造は木であって、循環的なグラフではありません。この事は、 send event 内の再帰が無限ループにならない事を保証します。

イベント生成 物理的なアクション(マウスクリックなど)によって生成されたイベントと過去の履歴 と関連するアクション(コンポーネント外にマウスカーソルが動いたなど)により発生 したイベントは区別されます。その結果、リッチイベントを生成する 2 つの関数が定義 される事になります。 前者を扱う関数は 2 つの物理ステータス情報からリッチイベントを生成します。 # let compute rich event s0 s1 = if s0.Graphics.button <> s1.Graphics.button then begin if s0.Graphics.button then MouseDown else MouseUp end else if s1.Graphics.keypressed then KeyPress else if (s0.Graphics.mouse x <> s1.Graphics.mouse x ) || (s0.Graphics.mouse y <> s1.Graphics.mouse y ) then begin if s1.Graphics.button then MouseDrag else MouseMove end else raise Not found; ; val compute_rich_event : Graphics.status -> Graphics.status -> rich_event =

後者の種類のイベントを生成する関数は最後に起きた 2 つのリッチイベントを引数に取 ります。 # let send new events res0 res1 = if res0.key focus <> res1.key focus then

グラフィカルインターフェースの構築

365

begin ignore(send event {res1 with re = LostFocus} res0.key focus); ignore(send event {res1 with re = GotFocus} res1.key focus) end; if (res0.last <> res1.last) && (( res1.re = MouseMove) || (res1.re = MouseDrag)) then begin ignore(send event {res1 with re = MouseExit} res0.last); ignore(send event {res1 with re = MouseEnter} res1.last ) end; ; val send_new_events : rich_status -> rich_status -> unit =

rich event 型の初期値を定義します。これはイベントループの履歴を初期化するのに使 われます。 # let initial re = { re = Exposure; stat = { Graphics.mouse x=0; Graphics.mouse y=0; Graphics.key = ’ ’; Graphics.button = false; Graphics.keypressed = false }; key focus = empty component; gen focus = empty component; last = empty component } ; ;

イベントループ イベントループはあるコンポーネントとの一連のやりとりを管理します。そのコンポー ネントは通常、インターフェースを構成する全てのコンポーネントの先祖です。インター フェースが各物理イベントが処理されるごとに再表示されるべきかどうか(b disp)、マ ウス移動を処理するかどうか(b motion)を表す真偽値が渡されます。最後の引数(c) はコンポーネントツリーの根っこです。 # let loop b disp b motion c = let res0 = ref initial re in try display c; while true do let lev = [Graphics.Button down; Graphics.Button up; Graphics.Key pressed] in let flev = if b motion then (Graphics.Mouse motion) :: lev else lev in let s = Graphics.wait next event flev in

366

Chapter 13 : Applications let res1 = {!res0 with stat = s} in try let res2 = {res1 with re = compute rich event !res0.stat res1.stat} in ignore(send event res2 c); send new events !res0 res2; res0 := res2; if b disp then display c with Not found → ()

done with e → raise e; ; val loop : bool -> bool -> component -> unit =

このループから抜けるのはイベントハンドラのどれかが例外を投げた時だけです。

テスト関数 マウス/キーボードイベントに対応するステータス情報を手動で生成するための 2 つの関 数を定義します。 # let make click e x y = {re = e; stat = {Graphics.mouse x=x; Graphics.mouse y=y; Graphics.key = ’ ’; Graphics.button = false; Graphics.keypressed = false}; key focus = empty component; gen focus = empty component; last = empty component}

let make key e ch c = {re = e; stat = {Graphics.mouse x=0; Graphics.mouse y=0; Graphics.key = c; Graphics.button = false; Graphics.keypressed = true}; key focus = empty component; gen focus = empty component; last = empty component}; ; val make_click : rich_event -> int -> int -> rich_status = val make_key : rich_event -> ’a -> char -> rich_status =

これでマウスイベントをコンポーネントに送るシミュレートができるようになりました。

グラフィカルインターフェースの構築

367

コンポーネントの定義 座標変化、イベント伝搬などいろいろな表示の仕組みが整いました。残ったのは、便利 で使いやすいコンポーネントを定義する事です。コンポーネントを次の 3 つのカテゴリ に分ける事にします。



テキストを表示するだけのような、イベントを処理しない単純なコンポーネント



テキスト入力のような、イベントを処理する単純なコンポーネント



コンテナとその様々なレイアウト方法

値はコンポーネント間やアプリケーション/コンポーネント間で共有データを変更する事 により受け渡しされます。共有は、変更されるべきデータを環境に持つクロージャによ り実現されます。さらにコンポーネントの振る舞いはイベント処理の結果によって変え られるので、コンポーネントは内部状態をイベントハンドラ内のクロージャに含める必 要があります。例えば、テキスト入力フィールドは入力されているテキストにアクセス します。このため、コンポーネントは次の様式に従って書くことにします。



コンポーネントの内部状態を表す型を定義する。



内部状態を操作する関数を定義する。



表示するための関数を定義する。与えられた座標がコンポーネント内にあるか判 定してイベントを処理する。



コンポーネントを生成する関数を定義する。これによりクロージャと内部状態が関 連づけられる。



イベントの到着をシミュレートすることでコンポーネントのテストを行う。

これらのコンポーネントの実装を説明します。



単純なテキスト (label);



単純なコンテナ (panel);



単純なボタン (button);



いくつかのテキストから選択する (choice);



テキスト入力 (textfield);



リッチコンポーネント (border).

ラベルコンポーネント ラベルと呼ばれる最も単純なコンポーネントです。これは文字列を画面に表示します。 イベントは処理しません。表示関数と生成関数を書くことから始めましょう。 表示には前景色と背景色とフォントを考慮しなければなりません。コンポーネントが占 める画面領域を消し、前景色とカーソル位置を設定するのはすのは display init 関数 の仕事です。display label 関数は display init を呼んだ後すぐに引数として渡され た文字列を表示します。 # let display init c =

368

Chapter 13 : Applications

Graphics.set color (get gc bcol (get gc c)); display rect c () ; let gc= get gc c in use gc gc; let (a,b) = get gc cur gc in Graphics.moveto (c.x+a) (c.y+b) let display label s c () = display init c; Graphics.draw string s; ; val display_init : component -> unit = val display_label : string -> component -> unit -> unit =

このコンポーネントはとても単純なので、内部状態を作る必要はありません。生成関数 から渡されて表示される文字列は、display label 関数だけが知っています。 # let create label s lopt = let gc = make default context () let (w,h) = Graphics.text size let u = create component w h u.mem <- (fun x → false); u.info <- "Label"; u.gc (string

in set gc gc lopt; use gc gc; s in in u.display <- display label s u; gc; * opt_val) list -> component =

このコンポーネントの色を変えるには、グラフィックコンテキストを直接操作する必要 があります。label l1 を表示させた図が 13.1 です。 # let courier bold 24 = Sopt "*courier-bold-r-normal-*24*" and courier bold 18 = Sopt "*courier-bold-r-normal-*18*"; ; # let l1 = create label "Login: " ["Font", courier bold 24; "Background", Copt gray1]; ;

図 13.1: label の表示

panel コンポーネント、コンテナ、レイアウト panel はコンテナになる事ができるグラフィカルな領域です。パネルを生成する関数はと ても単純で、一般的なコンポーネント生成関数にコンテナかどうかを表す真偽値引数を 加えたものです。panel 内かどうかを判別する関数と表示関数は create component に よりデフォルト関数に初期化されます。

グラフィカルインターフェースの構築

369

# let create panel b w h lopt = let u = create component w h in u.container <- b; u.info <- if b then "Panel container" else "Panel"; let gc = make default context () in set gc gc lopt; u.gc <- gc; u; ; val create_panel : bool -> int -> int -> (string * opt_val) list -> component =

子コンポーネントの配置はコンテナの巧妙な部分です。2 つのレイアウト関数 center layout、 grid layout を定義します。最初の center layout はコンポーネントをコンテナの中心 に配置します。 # let center layout c c1 lopt = c1.x <- c.x + ((c.w -c1.w) /2); c1.y <- c.y + ((c.h -c1.h) /2); ; val center_layout : component -> component -> ’a -> unit =

次の grid layout はコンテナを同じサイズの格子いくつかに分割します。レイアウトオ プションは列数"Col"と行数"Row"です。 # let grid layout (a, b) c c1 lopt = let px = theInt lopt "Col" 0 and py = theInt lopt "Row" 0 in if (px >= 0) && (px < a) && ( py >=0) && (py < b) then let lw = c.w /a and lh = c.h /b in if (c1.w > lw) || (c1.h > lh) then failwith "grid_placement: too big component" else c1.x <- c.x + px * lw + (lw - c1.w)/2; c1.y <- c.y + py * lh + (lh - c1.h)/2; else failwith "grid_placement: bad position"; ; val grid_layout : int * int -> component -> component -> (string * opt_val) list -> unit =

もちろんもっとたくさん定義することもできます。コンテナの set layout 関数によりレ イアウト関数をカスタマイズすることができます。図 13.2 はコンテナとして定義された panel で、2 つのラベルが追加されています。対応するするプログラムを次に示します。 # let l2 = create label "Passwd: " ["Font", courier bold 24; "Background", Copt gray1] ; ;

370

Chapter 13 : Applications

図 13.2: A panel component. # # # #

let set add add

p1 = create panel true 150 80 ["Background", Copt gray2] ; ; layout (grid layout (1,2) p1) p1; ; component p1 l1 ["Row", Iopt 1]; ; component p1 l2 ["Row", Iopt 0]; ;

コンポーネントは、インターフェースに統合できるように少なくとも 1 つの親を持って いないといけません。また、Graphics ライブラリは 1 つのウィンドウしか扱えないの でコンテナである本源的なウィンドウを定義しなければなりません。 # let open main window w h = Graphics.close graph () ; Graphics.open graph (" "^(string of int w)^"x"^(string of int h)); let u = create component w h in u.container <- true; u.info <- "Main Window"; u; ; val open_main_window : int -> int -> component =

ボタンコンポーネント ボタンは文字列を表示すると共にそこで起きたマウスクリックに反応できるコンポーネ ントです。この振る舞いを実現するために、ボタンコンポーネントは文字列とマウスの イベントハンドラを持つ button state 型の状態を保持します。 # type button state = { txt : string; mutable action :

button state → unit } ; ;

create bs 関数はこの状態を生成します。set bs action 関数はボタンが押された時に 実行されるアクション関数を設定し、get bs text 関数はボタンの文字列を取得します。 # let create bs s = {txt = s; action = fun x → () } let set bs action bs f = bs.action <- f

グラフィカルインターフェースの構築

371

let get bs text bs = bs.txt; ; val create_bs : string -> button_state = val set_bs_action : button_state -> (button_state -> unit) -> unit = val get_bs_text : button_state -> string =

表示関数はラベルのものと似ていますが、表示文字列をボタンが保持する状態から取 得する点が違います。イベント処理関数はマウスボタンが押された時、デフォルトでは set bs action により設定されたアクション関数を呼び出します。 # let display button c bs () = display init c; Graphics.draw string (get bs text bs) let listener button c bs e = match get event e with MouseDown → bs.action bs; c.display () ; true | _ → false; ; val display_button : component -> button_state -> unit -> unit = val listener_button : component -> button_state -> rich_status -> bool =

これでボタン生成関数に必要な関数は全て定義されました。 # let create button s lopt = let bs = create bs s in let gc = make default context () in set gc gc lopt; use gc gc; let w,h = Graphics.text size (get bs text bs) in let u = create component w h in u.display <- display button u bs; u.listener <- listener button u bs; u.info <- "Button"; u.gc <- gc; u,bs; ; val create_button : string -> (string * opt_val) list -> component * button_state =

この関数が返す値は、前半の要素が生成されたボタンコンポーネントで、後半の要素が ボタンの内部状態であるようなタプルです。 図 13.3 はボタンが追加されたパネルです。 ボタンに表示されている文字列を標準出力に出力するアクション関数がボタンに関連づ けられています。 # let b,bs = create button "Validation" ["Font", courier bold 24; "Background", Copt gray1]; ;

372

Chapter 13 : Applications

図 13.3: ボタンの表示とマウスクリックへの反応 # let p2 = create panel true 150 60 ["Background", Copt gray2]; ; # set bs action bs (fun bs → print string ( (get bs text bs)^ "..."); print newline () ); ; # set layout (center layout p2) p2; ; # add component p2 b [] ; ;

ラベルと違って、ボタンコンポーネントはマウスクリックにどう反応すべきか知ってい ます。これをテストするには “マウスクリック” イベントをパネル p2 の中心にあるボタ ンに送ります。これによりボタンに関連づけられたアクション関数が実行されます。 # send event (make click MouseDown 75 30) p2; ; - : bool = false

そしてイベントが実際に処理された事を示す true が返ってきます。

choice コンポーネント choice コンポーネントはマウスクリックにより選択肢の中から 1 つのだけ選ぶことがで きるコンポーネントです。必ず選択中の選択肢があります。ほかの選択肢に対してマウ スクリックすると、現在の選択は変更され、アクション関数が実行されます。ボタンで 使ったのと同じテクニックがここでも使えます。選択肢リストで必要な内部状態を定義 する事から始めます。 # type choice state = { mutable ind : int; values : string array; mutable sep : int; mutable height : int; mutable action : choice state → unit } ; ;

インデックス ind は選択肢 values の中で強調表示されるべき文字列を表します。sep は選択肢間の間隔、height は選択肢の高さをピクセル単位で指定します。アクション関 数は choice state 型の値を受け取り、index を使った処理をすることができます。 ここで、状態を生成するための関数とアクション関数を設定する関数を定義します。 # let create cs sa = {ind = 0; values = sa; sep = 2;

グラフィカルインターフェースの構築

373

height = 1; action = fun x → () } let set cs action cs f = cs.action <- f let get cs text cs = cs.values.(cs.ind); ; val create_cs : string array -> choice_state = val set_cs_action : choice_state -> (choice_state -> unit) -> unit = val get_cs_text : choice_state -> string =

表示関数は全ての選択肢を表示し、現在選択中のものを反転表示して強調します。イベ ント処理関数はマウスボタンが離された時にアクション関数を実行します。 # let display choice c cs () = display init c; let (x,y) = Graphics.current point () and nb = Array.length cs.values in for i = 0 to nb-1 do Graphics.moveto x (y + i*(cs.height+ cs.sep)); Graphics.draw string cs.values.(i) done; Graphics.set color (get gc fcol (get gc c)); Graphics.fill rect x (y+ cs.ind*(cs.height+ cs.sep)) c.w cs.height; Graphics.set color (get gc bcol (get gc c)); Graphics.moveto x (y + cs.ind*(cs.height + cs.sep)); Graphics.draw string cs.values.(cs.ind) ; ; val display_choice : component -> choice_state -> unit -> unit = # let listener choice c cs e = match e.re with MouseUp → let x = e.stat.Graphics.mouse x and y = e.stat.Graphics.mouse y in let cy = c.y in let i = (y - cy) / ( cs.height + cs.sep) in cs.ind <- i; c.display () ; cs.action cs; true | _ → false ; ; val listener_choice : component -> choice_state -> rich_status -> bool =

choice コンポーネントの生成関数は選択肢の文字列リストとオプションリストを受け取 りコンポーネント自身と内部状態を返します。 # let create choice lc lopt = let sa = (Array.of list (List.rev lc)) in let cs = create cs sa in let gc = make default context () in

374

Chapter 13 : Applications

set gc gc lopt; use gc gc; let awh = Array.map (Graphics.text size) cs.values in let w = Array.fold right (fun (x,y) → max x) awh 0 and h = Array.fold right (fun (x,y) → max y) awh 0 in let h1 = (h+cs.sep) * (Array.length sa) + cs.sep in cs.height <- h; let u = create component w h1 in u.display <- display choice u cs; u.listener <- listener choice u cs ; u.info <- "Choice "^ (string of int (Array.length cs.values)); u.gc <- gc; u,cs; ; val create_choice : string list -> (string * opt_val) list -> component * choice_state =

図 13.4 に示した 3 つの一連の図は choice コンポーネントが追加されたパネルです。選択 された文字列を標準出力に出力するアクション関数が関連づけられています。これらの 図はマウスクリックによるもので、次のプログラムによってシミュレートされています。

図 13.4: 選択肢リストの表示と選択

# let c,cs = create choice ["Helium"; "Gallium"; "Pentium"] ["Font", courier bold 24; "Background", Copt gray1]; ; # let p3 = create panel true 110 110 ["Background", Copt gray2]; ; # set cs action cs (fun cs → print string ( (get cs text cs)^"..."); print newline () ); ; # set layout (center layout p3) p3; ; # add component p3 c [] ; ;

さまざまなイベントを送ることでコンポーネントを直接テストする事もできます。次の 例は、図 13.4 中央の図ように選択を変更します。 # send event (make click MouseUp 60 55 ) p3; ;

グラフィカルインターフェースの構築

375

- : bool = false

次のイベントを送ると最初の選択肢が選択されます。 # send event (make click MouseUp 60 90 ) p3; ; - : bool = false

textfield コンポーネント テキスト入力フィールドまたは textfield は、文字列を入力することができる領域です。 文字列は左揃えや(電卓のように)右揃えにできます。そして、カーソルは次に文字が 挿入されることになる場所を表します。この動作にはもっと複雑な内部状態、すなわち 入力された文字列、入力の方向、カーソルの座標、文字がどう表示されるか、アクショ ン関数が必要になります。 # type textfield state = { txt : string; dir : bool; mutable ind1 : int; mutable ind2 : int; len : int; mutable visible cursor : bool; mutable cursor : char; mutable visible echo : bool; mutable echo : char; mutable action : textfield state → unit } ; ;

この内部状態を生成するには初期文字列、入力できる文字数、テキストの入力方向を指 定します。 # let create tfs txt size dir = let l = String.length txt in (if size < l then failwith "create_tfs"); let ind1 = if dir then 0 else size-1-l and ind2 = if dir then l else size-1 in let n txt = (if dir then (txt^(String.make (size-l) ’ ’)) else ((String.make (size-l) ’ ’)^txt )) in {txt = n txt; dir=dir; ind1 = ind1; ind2 = ind2; len=size; visible cursor = false; cursor = ’ ’; visible echo = true; echo = ’ ’; action= fun x → () }; ; val create_tfs : string -> int -> bool -> textfield_state =

次の関数を使うと表示される文字列などいろいろなフィールドにアクセスする事ができ ます。 # let set tfs action tfs f = tfs.action <- f let set tfs cursor b c tfs = tfs.visible cursor <- b; tfs.cursor <- c

376

Chapter 13 : Applications

let set tfs echo b c tfs = tfs.visible echo <- b; tfs.echo <- c let get tfs text tfs = if tfs.dir then String.sub tfs.txt tfs.ind1 (tfs.ind2 - tfs.ind1) else String.sub tfs.txt (tfs.ind1+1) (tfs.ind2 - tfs.ind1); ;

set tfs text 関数は tf コンポーネントの内部状態 tfs が持つテキストを txt に変更し ます。 # let set tfs text tf tfs txt = let l = String.length txt in if l > tfs.len then failwith "set_tfs_text"; String.blit (String.make tfs.len ’ ’) 0 tfs.txt 0 tfs.len; if tfs.dir then (String.blit txt 0 tfs.txt 0 l; tfs.ind2 <- l ) else ( String.blit txt 0 tfs.txt (tfs.len -l) l; tfs.ind1 <- tfs.len-l-1 ); tf.display () ; ; val set_tfs_text : component -> textfield_state -> string -> unit =

表示操作に際しては、文字表示のされかた、カーソルがの可視/不可視を考慮に入れな ければなりません。display textfield 関数はカーソルがどこにあるのかを表示する display cursor 関数を呼び出します。 # let display cursor c tfs = if tfs.visible cursor then ( use gc (get gc c); let (x,y) = Graphics.current point () in let (a,b) = Graphics.text size " " in let shift = a * (if tfs.dir then max (min (tfs.len-1) tfs.ind2) 0 else tfs.ind2) in Graphics.moveto (c.x+x + shift) (c.y+y); Graphics.draw char tfs.cursor); ; val display_cursor : component -> textfield_state -> unit = # let display textfield c tfs () = display init c; let s = String.make tfs.len ’ ’ and txt = get tfs text tfs in let nl = String.length txt in if (tfs.ind1 >= 0) && (not tfs.dir) then Graphics.draw string (String.sub s 0 (tfs.ind1+1) ); if tfs.visible echo then (Graphics.draw string (get tfs text tfs)) else Graphics.draw string (String.make (String.length txt) tfs.echo); if (nl > tfs.ind2) && (tfs.dir) then Graphics.draw string (String.sub s tfs.ind2 (nl-tfs.ind2));

377

グラフィカルインターフェースの構築 display cursor c tfs; ; val display_textfield : component -> textfield_state -> unit -> unit =

この種のコンポーネントに対するイベントリスナは複雑です。入力方向(左か右)によっ て、すでに入力された文字列を動かす必要がでてくるかもしれません。フォーカスは入 力領域をマウスでクリックすることで取得されます。 # let listener text field u tfs e = match e.re with MouseDown → take key focus e u ; true | KeyPress → ( if Char.code (get key e) >= 32 then begin ( if tfs.dir then ( ( if tfs.ind2 >= tfs.len then ( String.blit tfs.txt 1 tfs.txt 0 (tfs.ind2-1); tfs.ind2 <- tfs.ind2-1) ); tfs.txt.[tfs.ind2] <- get key e; tfs.ind2 <- tfs.ind2 +1 ) else ( String.blit tfs.txt 1 tfs.txt 0 (tfs.ind2); tfs.txt.[tfs.ind2] <- get key e; if tfs.ind1 >= 0 then tfs.ind1 <- tfs.ind1 -1 ); ) end else ( ( match Char.code (get key e) with 13 → tfs.action tfs | 9 → lose key focus e u | 8 → if (tfs.dir && (tfs.ind2 > 0)) then tfs.ind2 <- tfs.ind2 -1 else if (not tfs.dir) && (tfs.ind1 < tfs.len -1) then tfs.ind1 <- tfs.ind1+1 | _ → () ))); u.display () ; true | _ → false; ; val listener_text_field : component -> textfield_state -> rich_status -> bool =

テキストフィールドを生成する関数はこれまでと似たようなものです。 # let create text field txt size dir lopt let tfs = create tfs txt size dir

=

378

Chapter 13 : Applications

and l = String.length txt in let gc = make default context () in set gc gc lopt; use gc gc; let (w,h) = Graphics.text size (tfs.txt) in let u = create component w h in u.display <- display textfield u tfs; u.listener <- listener text field u tfs ; u.info <- "TextField"; u.gc <- gc; u,tfs; ; val create_text_field : string -> int -> bool -> (string * opt_val) list -> component * textfield_state =

この関数はコンポーネント自身とその内部状態の組を返します。次のコードにより図 13.5 のようなコンポーネントを生成することができます。 # # # # # # # # #

let let set set set let set add add

tf1,tfs1 = create text field "jack" 8 true ["Font", courier bold 24]; ; tf2,tfs2 = create text field "koala" 8 false ["Font", courier bold 24]; ; tfs cursor true ’_’ tfs1; ; tfs cursor true ’_’ tfs2; ; tfs echo false ’*’ tfs2; ; p4 = create panel true 140 80 ["Background", Copt gray2]; ; layout (grid layout (1,2) p4) p4; ; component p4 tf1 ["Row", Iopt 1]; ; component p4 tf2 ["Row", Iopt 0]; ;

図 13.5: テキスト入力コンポーネント

拡張コンポーネント これまでに述べたコンポーネントを越えた新しいコンポーネントを構築することもでき ます。例えば 136 ページにある計算機のように斜めの縁がついたコンポーネントです。 あるコンポーネントより大きいパネルを作って、それを何らかの方法で塗り潰した後で コンポーネントを中央に追加すればこの効果を実現することができます。

グラフィカルインターフェースの構築

379

# type border state = {mutable relief : string; mutable line : bool; mutable bg2 : Graphics.color; mutable size : int}; ;

生成関数はオプションリストを受け取り内部状態を作ります。 # let create border state lopt = {relief = theString lopt "Relief" "Flat"; line = theBool lopt "Outlined" false; bg2 = theColor lopt "Background2" Graphics.black; size = theInt lopt "Border_size" 2}; ; val create_border_state : (string * opt_val) list -> border_state =

"Top"、"Bot"、"Flat"オプションを追加することで、図 5.6(130 ページ)のボックス で使われた縁の情報を宣言できます。 # let display border bs c1 c () = let x1 = c.x and y1 = c.y in let x2 = x1+c.w-1 and y2 = y1+c.h-1 in let ix1 = c1.x and iy1 = c1.y in let ix2 = ix1+c1.w-1 and iy2 = iy1+c1.h-1 in let border1 g = Graphics.set color g; Graphics.fill poly [| (x1,y1);(ix1,iy1);(ix2,iy1);(x2,y1) |] ; Graphics.fill poly [| (x2,y1);(ix2,iy1);(ix2,iy2);(x2,y2) |] in let border2 g = Graphics.set color g; Graphics.fill poly [| (x1,y2);(ix1,iy2);(ix2,iy2);(x2,y2) |] ; Graphics.fill poly [| (x1,y1);(ix1,iy1);(ix1,iy2);(x1,y2) |] in display rect c () ; if bs.line then (Graphics.set color (get gc fcol (get gc c)); draw rect x1 y1 c.w c.h); let b1 col = get gc bcol ( get gc c) and b2 col = bs.bg2 in match bs.relief with "Top" → (border1 b1 col; border2 b2 col) | "Bot" → (border1 b2 col; border2 b1 col) | "Flat" → (border1 b1 col; border2 b1 col) | s → failwith ("display_border: unknown relief: "^s) ;; val display_border : border_state -> component -> component -> unit -> unit =

380

Chapter 13 : Applications

縁を作る関数はコンポーネントとオプションリストを引数に取り、そのコンポーネント を含むパネルを構築します。 # let create border c lopt = let bs = create border state lopt in let p = create panel true (c.w + 2 * bs.size) (c.h + 2 * bs.size) lopt in set layout (center layout p) p; p.display <- display border bs c p; add component p c [] ; p; ; val create_border : component -> (string * opt_val) list -> component =

これで、前のテストで宣言したラベルコンポーネントとテキスト入力コンポーネント tf1 にボーダーを持たせたコンポーネントを生成することができるようになりました。図 13.6 のような結果になります。 component p1 l1; ; component p4 tf1; ; = create border l1 [] ; ; = create border tf1 ["Relief", Sopt "Top"; "Background", Copt Graphics.red; "Border_size", Iopt 4]; ; p5 = create panel true 140 80 ["Background", Copt gray2]; ; layout (grid layout (1,2) p5) p5; ; component p5 b1 ["Row", Iopt 1]; ; component p5 b2 ["Row", Iopt 0]; ;

# # # #

remove remove let b1 let b2

# # # #

let set add add

図 13.6: 拡張コンポーネント

Awi ライブラリのセットアップ ここまでで、このライブラリの重要な部分は完成しました。この章で宣言した全ての型 や値は 1 つのファイルにまとめる事ができます。2 このライブラリは 1 つのモジュールを 2. ただしテストするための宣言は除きます。

グラフィカルインターフェースの構築

381

構成します。このファイルを awi.ml と呼ぶなら awi モジュールになります。ファイル 名とモジュール名の対応については 14 章を参照してください。 このファイルをコンパイルすると、コンパイル済みインターフェースファイル awi.cmi とコンパイラによってバイトコード awi.cmo かネイティブコード awi.cmx が生成され ます。バイトコンパイラを使うには次のコマンドを入力します。

ocamlc -c awi.ml インタラクティブトップレベルでこのライブラリを使うには、#load "awi.cmo";; コマ ンドでバイトコードをロードする必要があります。これで、このモジュールで宣言され た関数を呼び出してコンポーネントを作ったりすることができるようになります。 この関数の返り値は Awi.component です。これについては 14 章に詳しく書いてあり ます。

Example: A フラン-ユーロ変換器 この新しいライブラリを使ってフラン-ユーロ間の通貨変換器を作ることにしましょう。 変換自体は取るに足らないものですが、インターフェースの構築部分を見るとコンポー ネント間でどのように通信が行われているかが分かります。また新しい通貨に慣れるま での間は両方向に変換できるようにしたいです。使うコンポーネントは次の通りです。



変換の方向を表す 2 つの選択肢



金額を入力したり結果を表示したりするための 2 つのテキストフィールド



変換を行うためのボタン



テキストフィールドの意味を表示するための 2 つのラベル

図 13.7 にこれらのコンポーネントが示されています。 コンポーネント間の通信は共有状態によって実現されています。そのためにフラン(a)、 ユーロ(b)、どちらの方向に変換されるか(dir)、変換するための係数(fa、fb)を持 つ state conv を定義します。 # type state conv = { mutable a:float; mutable b:float; mutable dir : bool; fa : float; fb : float } ; ;

初期状態を次のように定義します。 # let e = 6.55957074 let fe = { a =0.0; b=0.0; dir = true; fa = e; fb = 1./. e}; ; 変換関数は変換方向によって浮動小数点数の結果を返します。 # let calculate fe = if fe.dir then fe.b <- fe.a /. fe.fa else fe.a <- fe.b /. fe.fb; ;

382

Chapter 13 : Applications

val calculate : state_conv -> unit =

2 つの選択肢に対するマウスクリックは変換の方向を変えます。選択肢の文字列は"->" と "<-"です。 # let action dir fe cs = match get cs text cs with "->" → fe.dir <- true | "<-" → fe.dir <- false | _ → failwith "action_dir"; ; val action_dir : state_conv -> choice_state -> unit =

ボタンに関連づけられたアクションは変換を行い、2 つのテキストフィールドのうちど ちらかに結果を表示します。このため、アクション関数の引数としてこれらの 2 つのテ キストフィールドも渡します。 # let action go fe tf fr tf eu tfs fr tfs eu x = if fe.dir then let r = float of string (get tfs text tfs fr) in fe.a <- r; calculate fe; let sr = Printf.sprintf "%.2f" fe.b in set tfs text tf eu tfs eu sr else let r = float of string (get tfs text tfs eu) in fe.b <- r; calculate fe; let sr = Printf.sprintf "%.2f" fe.a in set tfs text tf fr tfs fr sr; ; val action_go : state_conv -> component -> component -> textfield_state -> textfield_state -> ’a -> unit =

あとはインターフェースを構築するだけです。次の関数は幅、高さ、変換の状態を受け 取って 3 つのアクティブコンポーネントを含んだメインコンテナを返します。 # let create conv w h fe = let gray1 = (Graphics.rgb 120 120 120) in let m = open main window w h and p = create panel true (w-4) (h-4) [] and l1 = create label "Francs" ["Font", courier bold 24; "Background", Copt gray1] and l2 = create label "Euros" ["Font", courier bold 24; "Background", Copt gray1] and c,cs = create choice ["->"; "<-"] ["Font", courier bold 18]

グラフィカルインターフェースの構築

383

and tf1,tfs1 = create text field "0" 10 false ["Font", courier bold 18] and tf2,tfs2 = create text field "0" 10 false ["Font", courier bold 18] and b,bs = create button " Go " ["Font", courier bold 24] in let gc = get gc m in set gc bcol gc gray1; set layout (grid layout (3,2) m ) m; let tb1 = create border tf1 [] and tb2 = create border tf2 [] and bc = create border c [] and bb = create border b ["Border_size", Iopt 4; "Relief", Sopt "Bot"; "Background", Copt gray2; "Background2", Copt Graphics.black] in set cs action cs (action dir fe); set bs action bs (action go fe tf1 tf2 tfs1 tfs2); add component m l1 ["Col",Iopt 0;"Row",Iopt 1]; add component m l2 ["Col",Iopt 2;"Row",Iopt 1]; add component m bc ["Col",Iopt 1;"Row",Iopt 1]; add component m tb1 ["Col",Iopt 0;"Row",Iopt 0]; add component m tb2 ["Col",Iopt 2;"Row",Iopt 0]; add component m bb ["Col",Iopt 1;"Row",Iopt 0]; m,bs,tf1,tf2; ; val create_conv : int -> int -> state_conv -> component * button_state * component * component =

次のコードによりコンテナ m の中でイベント処理ループが始まります。その結果図 13.7 のような画面になります。 # let (m,c,t1,t2) = create conv 420 150 fe ; ; # display m ; ;

図 13.7: 変換器のウィンドウ

384

Chapter 13 : Applications

全てのイベント処理クロージャは同じ状態を共有しているので、選択肢がクリックされ ると表示されている文字列と変換の方向の両方が変わります。

もっと知りたい方は クロージャによりコンポーネントに対するイベントアクション関数の登録ができます。 ですが、既にあるハンドラを “再利用” して追加の振る舞いをさせるようにすることはで きません。まったく新しいハンドラを定義する必要があります。16 章では、関数とオブ ジェクト指向パラダイムの比較をして、ハンドラ拡張の可能性について議論します。 ここまでのプログラムでは多くの構造体が同一の名前を持つフィールドを持っています。 (例えば txt)最後に宣言した内容はそれ以前の宣言を隠してしまうので、プログラム内 でフィールドに直接アクセスするのは難しいです。型を定義するたびにその型のフィー ルドへのアクセス関数も定義したのはそのためです。もう一つの解決方法はライブラリ をいくつかのモジュールに分けることです。こうするとモジュール名を指定することに よりフィールド名を区別することができます。いずれにせよアクセス関数のおかげでこ のライブラリの全ての機能を使うことができます。14 章では型の重ね合わせの話題に戻 り、抽象データ型の紹介をします。重ね合わせを使うと、環状になってはならないコン ポーネントの親子関係のような敏感なフィールドを修正させないことで厳密さを増すこ とができます。 このライブラリを拡張する方法はいくつもあります。 コンポーネントのデザインに関する基準の 1 つは、新しいものが書けなければならない ということです。新しい mem、display 関数を定義して任意の形のコンポーネントを作 るのはとても簡単です。この方法で楕円型、しずく型のボタンを作る事もできます。 これまでに紹介したいくつかのレイアウト関数はどれも貧弱なものです。各領域の高さ と幅が可変であるようなグリッドレイアウトを追加する事もできます。また、十分な空 きがある限りコンポーネントを並べて配置したいかもしれません。コンポーネントのサ イズ変更が子コンポーネントに伝わるようにする必要もあるでしょう。

最小コスト経路を見つける 多くのアプリケーションは重みつき有向グラフ上の最小コスト経路を見つける必要があ ります。問題はグラフの辺に関連づけられた非負の重みを用いて経路を見つけることで す。ここではダイクストラ法を用いて経路を求めてみましょう。 これまでに紹介したライブラリが使えます。出てくる順に並べると、次のモジュールが使 われます。入出力のために Genlex と Printf、キャッシュの実装のために Weak、キャッ シュによって節約できた時間を計るために Sys、グラフィカルユーザーインターフェー スを作るために Awi。Sys モジュールはコマンドライン引数としてグラフが記述された ファイルを受け取るためにも使われます。

最小コスト経路を見つける

385

グラフの表現 重みつき有向グラフはノードの集合、辺の集合、辺の集合から重み値への写像により定 義されます。重みつき有向グラフのデータの表現方法はたくさんあります。



隣接行列: 行列の各要素 (m(i, j)) はノード i からノード j への辺を表し、その値は辺の重み を表します。



隣接リスト: 各ノード i はリスト [(j1 , w1 ); ..; (jn , wn )] に関連づけられ、それぞれの組 (i, jk , wk ) は重みが wk である辺を表します。



3 種 (?): ノードリスト、辺リスト、それから辺の重みを求める関数です。

各表現を使ったときの性質はグラフの規模と辺の数によります。このアプリケーション の目標はいかにしてメモリを全部使わずに最後に行われた計算をキャッシュするかとい うことなので、重みつき有向グラフを表すのに隣接行列を使うことにします。この方法 だとリスト操作によってメモリ使用量が増えることはありません。 # type cost = Nan | Cost of float; ; # type adj mat = cost array array; ; # type ’a graph = { mutable ind : int; size : int; nodes : ’a array; m : adj mat}; ;

size フィールドはノード数の最大値、ind フィールドは実際のノード数です。 ここでグラフを生成する関数を定義しましょう。 グラフを作る関数は引数としてノードとノード番号の最大値をとります。 # let create graph n s = { ind = 0; size = s; nodes = Array.create s n; m = Array.create matrix s s Nan } ; ; val create_graph : ’a -> int -> ’a graph =

belongs to 関数はノード n がグラフ g に含まれるかどうかを調べて返します。 # let belongs to n g = let rec aux i = (i < g.size) & ((g.nodes.(i) = n) or (aux (i+1))) in aux 0; ; val belongs_to : ’a -> ’a graph -> bool =

386

Chapter 13 : Applications

index 関数はグラフ g におけるノード n のインデックスを返します。もしそのノードが 存在しなかったら例外 Not found が投げられます。 # let index n g = let rec aux i = if i >= g.size then raise Not found else if g.nodes.(i) = n then i else aux (i+1) in aux 0 ; ; val index : ’a -> ’a graph -> int =

次の 2 つの関数はノードとコスト c を持つ辺をグラフに追加します。 # let add node n g = if g.ind = g.size then failwith "the graph is full" else if belongs to n g then failwith "the node already exists" else (g.nodes.(g.ind) <- n; g.ind <- g.ind + 1) ; ; val add_node : ’a -> ’a graph -> unit = # let add edge e1 e2 c g = try let x = index e1 g and y = index e2 g in g.m.(x).(y) <- Cost c with Not found → failwith "node does not exist" ; ; val add_edge : ’a -> ’a -> float -> ’a graph -> unit =

これで、ノードと辺のリストから簡単に重みつき有向グラフを作ることができます。 test aho 関数は図 13.8 のグラフを生成します。 # let test aho () = let g = create graph "nothing" 5 in List.iter (fun x → add node x g) ["A"; "B"; "C"; "D"; "E"]; List.iter (fun (a,b,c) → add edge a b c g) ["A","B",10.; "A","D",30.; "A","E",100.0; "B","C",50.; "C","E",10.; "D","C",20.; "D","E",60.]; for i=0 to g.ind -1 do g.m.(i).(i) <- Cost 0.0 done; g; ; val test_aho : unit -> string graph = # let a = test aho () ; ; val a : string graph =

387

最小コスト経路を見つける {ind = 5; size = 5; nodes = [|"A"; "B"; "C"; "D"; "E"|]; m = [|[|Cost 0; Cost 10; Nan; Cost 30; Cost ...|]; ...|]}

図 13.8: テストグラフ

いろいろなグラフの構築 プログラム中で直接グラフを作るのでは退屈です。これを避けるために、グラフの簡単 なテキスト表現を定義しましょう。こうすると、グラフを記述したテキストファイルを アプリケーションが読み込むことで、そのグラフを生成することができます。 グラフのテキスト表現は次のような行の集まりです。



ノード数: SIZE number;



ノード名: NODE name;



辺の重み: EDGE name1 name2 cost;



コメント: # comment.

例えば、次の aho.dat ファイルは図 13.8 のグラフを表します。

SIZE 5 NODE A NODE B

388 NODE NODE NODE EDGE EDGE EDGE EDGE EDGE EDGE EDGE

Chapter 13 : Applications C D E A A A B C D D

B D E C E C E

10.0 30.0 100.0 50. 10. 20. 60.

グラフファイルを読むために字句解析モジュール Genlex を使います。字句解析器はキー ワードのリスト keywords から構築されます。

parse line 関数はキーワードに関連づけられたアクションを実行します。 # let keywords = [ "SIZE"; "NODE"; "EDGE"; "#"]; ; val keywords : string list = ["SIZE"; "NODE"; "EDGE"; "#"] # let lex line l = Genlex.make lexer keywords (Stream.of string l); ; val lex_line : string -> Genlex.token Stream.t = # let parse line g s = match s with parser [< ’(Genlex.Kwd "SIZE"); ’(Genlex.Int n) >] → g := create graph "" n | [< ’(Genlex.Kwd "NODE"); ’(Genlex.Ident name) >] → add node name !g | [< ’(Genlex.Kwd "EDGE"); ’(Genlex.Ident e1); ’(Genlex.Ident e2); ’(Genlex.Float c) >] → add edge e1 e2 c !g | [< ’(Genlex.Kwd "#") >] → () | [<>] → () ; ; Characters 44-46: [< ’(Genlex.Kwd "SIZE"); ’(Genlex.Int n) >] -> ^^ Syntax error

グラフファイルからグラフを生成する関数で解析器が使われます。 # let create graph name = let g = ref {ind=0; size=0; nodes =[||]; m = [||]} in let ic = open in name in try print string ("Loading "^name^": "); while true do print string "."; let l = input line ic in parse line g (lex line l) done; !g

最小コスト経路を見つける

389

with End of file → print newline () ; close in ic; !g ; ; Characters 238-248: let l = input_line ic in parse_line g (lex_line l) ^^^^^^^^^^ Unbound value parse_line

次のコマンドはファイル aho.dat からグラフを生成します。 # let b = create graph "PROGRAMMES/aho.dat" ; ; val b : int -> string graph =

ダイクストラ法 ダイクストラ法は 2 点間の最小コスト経路を求めるアルゴリズムです。ノード n1、n2 間 のコストはそれらのノード間の経路のコストを合計したものです。このアルゴリズムを 適用するにはコストは非負であることが必要です。このため同じノードを 2 回以上通っ てもコストが小さくなることはありません。 ダイクストラ法を使うと、ある起点ノード n1 からたどれる全てのノードについてその 2 点間の最小コスト経路を効率よく計算することができます。ダイクストラ法では n1 から の最小コスト経路がすでに分かっているようなノードの集合を考えます。集合に含まれ るノードから 1 つの辺をたどることで到達できるノードを考えることでこの集合は次々 と大きくなっていきます。集合に追加されるのは、そのようなノードの候補の中で最小 のコストを持つものです。 計算の状態を記録しておくための comp state 型と状態を生成する関数を定義します。 # type comp state = { paths : int array; already treated : bool array; distances : cost array; source : int; nn : int}; ; # let create state () = { paths = [||]; already treated = [||]; distances = [||]; nn = 0; source = 0}; ;

source フィールドは起点ノードを表します。already treated フィールドは起点ノード からそのノードまでの最小コスト経路がすでに分かっていることを表します。nn フィー ルドはノードの総数を表します。distances フィールドは起点ノードからの最小コスト を表します。path には最小コスト経路をたどるための先行ノードが入ります。起点ノー ドへの経路は path をたどることで構築することができます。

390

Chapter 13 : Applications

コスト関数 コストに関する 4 つの関数を定義します。a cost は辺が存在するかどうか、float of cost は浮動小数点数で表されたコスト値、add cost は 2 つのコストの合計、less cost は片 方のコストが他方のコストより小さいかどうかを返します。 # let a cost c = match c with Nan → false | _-> true; ; val a_cost : cost -> bool = # let float of cost c = match c with Nan → failwith "float_of_cost" | Cost x → x; ; val float_of_cost : cost -> float = # let add cost c1 c2 = match (c1,c2) with Cost x, Cost y → Cost (x+.y) | Nan, Cost y → c2 | Cost x, Nan → c1 | Nan, Nan → c1; ; val add_cost : cost -> cost -> cost = # let less cost c1 c2 = match (c1,c2) with Cost x, Cost y → x < y | Cost x, Nan → true | _, _ → false; ; val less_cost : cost -> cost -> bool =

Nan は計算や比較される際に特別な役割を持ちます。メイン関数を紹介する所(391)で この事について再び触れます。

アルゴリズムの実装 既知の最小コスト経路から次のノードを見つけるための手順は 2 つに分けることができま す。first not treated は最小コスト経路が既知のノード集合に含まれない最初のノー ドを返します。このノードは次の関数、least not treated の初期値として使われます。 この関数は、集合に含まれないノードのうち起点からのコストが最小のノードを返しま す。そのノードが集合に追加されることになります。 # exception Found of int; ; exception Found of int # let first not treated cs = try for i=0 to cs.nn-1 do if not cs.already treated.(i) then raise (Found i) done; raise Not found; 0 with Found i → i ; ; val first_not_treated : comp_state -> int = # let least not treated p cs =

最小コスト経路を見つける

391

let ni = ref p and nd = ref cs.distances.(p) in for i=p+1 to cs.nn-1 do if not cs.already treated.(i) then if less cost cs.distances.(i) !nd then ( nd := cs.distances.(i); ni := i ) done; !ni,!nd; ; val least_not_treated : int -> comp_state -> int * cost =

one round 関数は新しいノードを既知の集合に追加し、残りのノードについて必要なら ば起点からのコストを更新します。 # exception No way; ; exception No_way # let one round cs g = let p = first not treated cs in let np,nc = least not treated p cs in if not(a cost nc ) then raise No way else begin cs.already treated.(np) <- true; for i = 0 to cs.nn -1 do if not cs.already treated.(i) then if a cost g.m.(np).(i) then let ic = add cost cs.distances.(np) g.m.(np).(i) in if less cost ic cs.distances.(i) then ( cs.paths.(i) <- np; cs.distances.(i) <- ic ) done; cs end; ; val one_round : comp_state -> ’a graph -> comp_state =

さあ、残るのはこれまで定義した関数を繰り返し呼ぶことです。dij 関数は起点ノードと グラフを引数にとり起点から全てのノードまでの最小コスト経路情報を含む comp state 型の値を返します。 # let dij s g = if belongs to s g then begin let i = index s g in

392

Chapter 13 : Applications

let cs = { paths = Array.create g.ind (-1) ; already treated = Array.create g.ind false; distances = Array.create g.ind Nan; nn = g.ind; source = i} in cs.already treated.(i) <- true; for j=0 to g.ind-1 do let c = g.m.(i).(j) in cs.distances.(j) <- c; if a cost c then cs.paths.(j) <- i done; try for k = 0 to cs.nn-2 do ignore(one round cs g) done; cs with No way → cs end else failwith "dij: node unknown"; ; val dij : ’a -> ’a graph -> comp_state =

Nan は距離の初期値で、比較関数 less cost の定義からは無限大を表します。それに対 してコストの加算関数 add cost ではゼロ扱いされています。これにより距離テーブル の実装が単純になっています。 ここでダイクストラ法の実行を試すことができます。 # let g = test aho () ; ; # let r = dij "A" g; ;

返り値は # r.paths; ; - : int array = [|0; 0; 3; 0; 2|] # r.distances; ; - : cost array = [|Cost 0; Cost 10; Cost 50; Cost 30; Cost 60|]

結果の表示 結果を読みやすくするために、表示関数を作ってみましょう。

dij の返り値に含まれる paths テーブルは計算された経路の最後の 1 辺しか含んでいま せん。すべての経路を得るためには再帰的に起点まで戻る必要があります。 # let display state f (g,st)

dest =

最小コスト経路を見つける

393

if belongs to dest g then let d = index dest g in let rec aux is = if is = st.source then Printf.printf "%a" f g.nodes.(is) else ( let old = st.paths.(is) in aux old; Printf.printf " -> (%4.1f) %a" (float of cost g.m.(old).(is)) f g.nodes.(is) ) in if not(a cost st.distances.(d)) then Printf.printf "no way\n" else ( aux d; Printf.printf " = %4.1f\n" (float of cost st.distances.(d))); ; val display_state : (out_channel -> ’a -> unit) -> ’a graph * comp_state -> ’a -> unit =

この再帰関数はノードを正しい順番で表示するためにスタックを使っています。グラフ 表示の多相性を保てるように、"a"フォーマットは引数に関数パラメータ f をとります。

”A” (index 0) から ”E” (index 4)までの最小コスト経路は次のように表示されます。 # display state (fun x y → Printf.printf "%s!" y) (a,r) "E"; ; A! -> (30.0) D! -> (20.0) C! -> (10.0) E! = 60.0 - : unit = ()

経路上の各ノードと各辺のコストが表示されます。

キャッシュの紹介 ダイクストラ法は起点ノードから全てのノードについて最小コスト経路を計算します。 これらの最小コスト経路を、起点ノードが同じであるような次回の問い合わせに備えて 保存しておくというアイデアが浮かびます。しかしこれにはかなりの量のメモリーが必 要です。ここでは “ウィークポインタ” を紹介します。ある起点ノードに対する計算の結 果はウィークポインタのテーブルに保存されます。次の計算の時は、すでに計算された結 果があるかどうかを判定することができます。この種類のポインタは弱いので、その領 域に入っていた状態データは必要ならばガーベッジコレクタによって解放されます。こ の仕組みにより、大量のメモリーの割り当てによって残りのプログラムが中断されるこ とが避けられます。最悪の場合でも未来の問い合わせによって計算が繰り返されるだけ です。

キャッシュの実装 新しい型’a comp graph を定義します。

394

Chapter 13 : Applications

# type ’a comp graph = { g : ’a graph; w : comp state Weak.t } ; ;

g はグラフ、w フィールドには各起点ごとの計算状態を指すウィークポインタのテーブル を表します。 これらの値は create comp graph 関数により生成されます。 # let create comp graph g = { g = g; w = Weak.create g.ind } ; ; val create_comp_graph : ’a graph -> ’a comp_graph =

dij quick 関数は計算が既に行われたかどうかを判定します。既に行われていたら、保 存されている結果を返します。そうでなければ計算が行われ、結果がウィークポインタ のテーブルに保存されます。 # let dij quick s cg = let i = index s cg.g in match Weak.get cg.w i with None → let cs = dij s cg.g in Weak.set cg.w i (Some cs); cs | Some cs → cs; ; val dij_quick : ’a -> ’a comp_graph -> comp_state =

表示関数は以前と同じものが使えます。 # let cg a = create comp graph a in let r = dij quick "A" cg a in display state (fun x y → Printf.printf "%s!" y) (a,r) "E" ; ; A! -> (30.0) D! -> (20.0) C! -> (10.0) E! = 60.0 - : unit = ()

性能評価 ランダムな起点ノードリストのそれぞれを処理させて dij、dij quick 関数の性能を計っ てみましょう。この方法で頻繁に最小コスト経路を計算するアプリケーション、例えば 路線検索システムのようなものをシミュレートすることができます。 時間を計測する関数を定義します。 # let exe time f g ss = let t0 = Sys.time () in

最小コスト経路を見つける

395

Printf.printf "Start (%5.2f)\n" t0; List.iter (fun s → ignore(f s g)) ss; let t1 = Sys.time () in Printf.printf "End (%5.2f)\n" t1; Printf.printf "Duration = (%5.2f)\n" (t1 -. t0) ; ; val exe_time : (’a -> ’b -> ’c) -> ’b -> ’a list -> unit =

そして 20000 個のランダムなノードリストを作り、グラフ a に対して性能を測ります。 # let ss = let ss0 = ref [] in let i0 = int of char ’A’ in let new s i = Char.escaped (char of int (i0+i)) in for i=0 to 20000 do ss0 := (new s (Random.int a.size))::!ss0 done; !ss0 ; ; val ss : string list = ["A"; "B"; "D"; "A"; "E"; "C"; "B"; "B"; "D"; "E"; "B"; "E"; "C"; "E"; "E"; "D"; "D"; "A"; "E"; ...] # Printf.printf"Function dij :\n"; exe time dij a ss ; ; Function dij : Start ( 1.51) End ( 1.93) Duration = ( 0.42) - : unit = () # Printf.printf"Function dij_quick :\n"; exe time dij quick (create comp graph a) ss ; ; Function dij_quick : Start ( 1.93) End ( 1.99) Duration = ( 0.06) - : unit = ()

予想通りの結果になりました。保持された結果に直接アクセスするのは 2 回目の計算よ りかなり速いです。

グラフィカルインターフェース Awi ライブラリを使ってグラフを表示するグラフィカルインターフェースを作りましょ う。起点ノードと終点ノードを選択すると、その間の最小コスト経路が計算されて表示 されます。そのために、グラフィカルインターフェース、グラフ、計算状態をフィール ドに持つ’a gg 型を定義します。 #

#load "PROGRAMMES/awi.cmo"; ;

396

Chapter 13 : Applications

# type ’a gg = { mutable src : ’a * Awi.component; mutable dest : ’a * Awi.component; pos : (int * int) array; cg : ’a comp graph; mutable state : comp state; mutable main : Awi.component; to string : ’a → string; from string : string → ’a } ; ;

src、dest フィールドはノードとコンポーネントの組です。pos フィールドは各コンポー ネントの座標を持ちます。main はコンポーネント群のメインコンテナです。to string 関数、from string 関数は’a 型と文字列型の間の変換関数です。 これらの値を生成するために必要なのはグラフ情報、位置テーブル、変換関数です。 # let create gg cg vpos ts fs = {src = cg.g.nodes.(0),Awi.empty component; dest = cg.g.nodes.(0),Awi.empty component; pos = vpos; cg = cg; state = create state () ; main = Awi.empty component; to string = ts; from string = fs}; ; val create_gg : ’a comp_graph -> (int * int) array -> (’a -> string) -> (string -> ’a) -> ’a gg =

可視化 グラフを表示するためにはノードと辺を描画する必要があります。ノードは Awi のボタンコ ンポーネントで表し、辺は直接メインウィンドウに描画することにします。display edge 関数は辺を描画します。display shortest path は見つかった経路を異なる色で描画し ます。

Drawing Edges 辺は 2 つのノードを結び、重みに関連づけられます。2 つのノード が接続されている様子は線分で表すことができます。問題は線分の向きをどう表すかで すが、これは矢印で表すことにします。矢印は、正しい方向を向くように線分の角度だ け回転させます。最後に辺の重みが辺のとなりに描画されます。 辺の矢印を描画するために、座標を回転させる rotate 関数と移動させる translate 関 数を定義します。display arrow 関数により矢印を描画します。

最小コスト経路を見つける

397

# let rotate l a = let ca = cos a and sa = sin a in List.map (function (x,y) → ( x*.ca +. -.y*.sa, x*.sa +. y*.ca)) l; ; val rotate : (float * float) list -> float -> (float * float) list = # let translate l (tx,ty) = List.map (function (x,y) → (x +. tx, y +. ty)) l; ; val translate : (float * float) list -> float * float -> (float * float) list = # let display arrow (mx,my) a = let triangle = [(5.,0.); (-3.,3.); (1.,0.); (-3.,-3.); (5.,0.)] in let tr = rotate triangle a in let ttr = translate tr (mx,my) in let tt = List.map (function (x,y) → (int of float x, int of float y)) ttr in Graphics.fill poly (Array.of list tt); ; val display_arrow : float * float -> float -> unit =

辺の重みを表す文字列の位置は辺の角度に依存します。 # let display label (mx,my) a lab = let (sx,sy) = Graphics.text size lab in let pos = [ float(-sx/2),float(-sy) ] in let pr = rotate pos a in let pt = translate pr (mx,my) in let px,py = List.hd pt in let ox,oy = Graphics.current point () in Graphics.moveto ((int of float mx)-sx-6) ((int of float my) ); Graphics.draw string lab; Graphics.moveto ox oy; ; val display_label : float * float -> float -> string -> unit =

それではこれまでに定義した関数を display edge 関数で使いましょう。 # let display edge gg col i j = let g = gg.cg.g in let x,y = gg.main.Awi.x,gg.main.Awi.y in if a cost g.m.(i).(j) then ( let (a1,b1) = gg.pos.(i) and (a2,b2) = gg.pos.(j) in let x0,y0 = x+a1,y+b1 and x1,y1 = x+a2,y+b2 in let rxm = (float(x1-x0)) /. 2. and rym = (float(y1-y0)) /. 2. in let xm = (float x0) +. rxm and ym = (float y0) +. rym in Graphics.set color col; Graphics.moveto x0 y0;

398

Chapter 13 : Applications

Graphics.lineto x1 y1; let a = atan2 rym rxm in display arrow (xm,ym) a; display label (xm,ym) a (string of float(float of cost g.m.(i).(j)))); ; val display_edge : ’a gg -> Graphics.color -> int -> int -> unit =

経路の表示 経路を表示するには、経路に含まれる全ての辺を描画すればいいです。し たがって、経路の描画はこれまで辺を描いてきたやり方でできます。 # let rec display shortest path gg col dest = let g = gg.cg.g in if belongs to dest g then let d = index dest g in let rec aux is = if is = gg.state.source then () else ( let old = gg.state.paths.(is) in display edge gg col old is; aux old ) in if not(a cost gg.state.distances.(d)) then Printf.printf "no way\n" else aux d; ; val display_shortest_path : ’a gg -> Graphics.color -> ’a -> unit =

グラフの表示 display gg 関数はグラフを描画します。もし終点ノードが空でなけれ ば、起点ノードと終点ノード間の経路が描画されます。 let display gg gg () = Awi.display rect gg.main () ; for i=0 to gg.cg.g.ind -1 do for j=0 to gg.cg.g.ind -1 do if i<> j then display edge gg (Graphics.black) i j done done; if snd gg.dest != Awi.empty component then display shortest path gg Graphics.red (fst gg.dest); ; val display_gg : ’a gg -> unit -> unit = #

最小コスト経路を見つける

399

ノードコンポーネント ノードも描画される必要があります。ユーザーは起点ノードと終点ノードを指定するこ とができるのでノード用のコンポーネントを定義します。 ユーザーの主なアクションは終点ノードを指定することです。したがってノードはマウ スクリックに反応できるコンポーネントである必要があります。そのようなコンポーネ ントとして、マウスクリックに反応するボタンコンポーネントを選びました。

ノードのアクション ノードが選択されているかどうかを表す必要があります。このた めに、inverse 関数によりノードの背景色を変えられるようにします。 # let inverse b = let gc = Awi.get gc b in let fcol = Awi.get gc fcol gc and bcol = Awi.get gc bcol gc in Awi.set gc bcol gc fcol; Awi.set gc fcol gc bcol; ; val inverse : Awi.component -> unit =

ノード選択は action click 関数により行われます。この関数はノードがクリックされ た時に呼ばれます。引数としてボタンに関連づけられたノードとグラフが渡されます。 起点/終点ノードが両方選択されたら、dij quick 関数により最小コスト経路が計算され ます。 # let action click node gg b bs = let (s1,s) = gg.src and (s2,d) = gg.dest in if s == Awi.empty component then ( gg.src <- (node,b); inverse b ) else if d == Awi.empty component then ( inverse b; gg.dest <- (node,b); gg.state <- dij quick s1 gg.cg; display shortest path gg (Graphics.red) node ) else (inverse s; inverse d; gg.dest <- (s2,Awi.empty component); gg.src <- node,b; inverse b); ; val action_click : ’a -> ’a gg -> Awi.component -> ’b -> unit =

400

Chapter 13 : Applications

インターフェースの作成 インターフェースを作るメイン関数はグラフとオプションリ ストを引数にとり、いろいろなコンポーネントを作ってグラフの各要素に関連づけます。 引数はグラフ(gg)、幅と高さ(gw、gh)、グラフ/ノードオプションリスト(lopt)、 ノードの境界線オプションリスト(lopt2)です。 # let main gg gg gw gh lopt lopt2 = let gc = Awi.make default context () in Awi.set gc gc lopt; (* compute the maximal button size *) let vs = Array.map gg.to string gg.cg.g.nodes in let vsize = Array.map Graphics.text size vs in let w = Array.fold right (fun (x,y) → max x) vsize 0 and h = Array.fold right (fun (x,y) → max y) vsize 0 in (* create the main panel *) gg.main <- Awi.create panel true gw gh lopt; gg.main.Awi.display <- display gg gg; (* create the buttons *) let vb bs = Array.map (fun x → x,Awi.create button (" "^(gg.to string x)^" ") lopt) gg.cg.g.nodes in let f act b = Array.map (fun (x,(b,bs)) → let ac = action click x gg b in Awi.set bs action bs ac) vb bs in let bb = Array.map (function (_,(b,_)) → Awi.create border b lopt2) vb bs in Array.iteri (fun i (b) → let x,y = gg.pos.(i) in Awi.add component gg.main b ["PosX",Awi.Iopt (x-w/2); "PosY", Awi.Iopt (y-h/2)]) bb; () ; ; val main_gg : ’a gg -> int -> int -> (string * Awi.opt_val) list -> (string * Awi.opt_val) list -> unit =

ボタンは自動的に作成され、メインウィンドウに配置されます。

インターフェースのテスト インターフェースを作成する全ての準備が整いました。変 換関数を単純にするためにノードが文字列であるようなグラフを使います。グラフ gg を 次のようにして作ります。

401

最小コスト経路を見つける

# # # #

let id x = x; ; let pos = [| 200, 300; 80, 200 ; 100, 100; 200, 100; 260, 200 |]; ; let gg = create gg (create comp graph (test aho () )) pos id id; ; main gg gg 400 400 ["Background", Awi.Copt (Graphics.rgb 130 130 130); "Foreground",Awi.Copt Graphics.green] [ "Relief", Awi.Sopt "Top";"Border_size", Awi.Iopt 2]; ;

Awi.loop true false gg.main;; を実行すると Awi ライブラリのイベントループが始 まります。

図 13.9: ノードの選択 図 13.9 はノード"A"、"E"間の最小コスト経路が計算された結果です。経路の各辺は色が 変わっています。

スタンドアロンアプリケーションの作成 ここでスタンドアロンアプリケーションを作るのに必要な手順を紹介しましょう。アプ リケーションはグラフが保存されたファイルの名前を引数にとります。スタンドアロン アプリケーションを実行する環境に Objective Caml がインストールされている必要は ありません。

402

Chapter 13 : Applications

グラフ記述ファイル グラフ記述ファイルにはグラフそのものに関する情報に加えてグラフィカルインター フェースに関する情報も記述することにします。そこで後者の情報のために新たなフォー マットを定義します。これらの記述から g info 型の値を生成します。 # type g info = {npos : (int mutable opt mutable g w mutable g h

* : : :

int) array; Awi.lopt; int; int}; ;

グラフィカルインターフェースの情報のフォーマットはリスト key2 の 4 つのキーワー ドにより記述されます。 # let key2 = ["HEIGHT"; "LENGTH"; "POSITION"; "COLOR"]; ; val key2 : string list = ["HEIGHT"; "LENGTH"; "POSITION"; "COLOR"] # let lex2 l = Genlex.make lexer key2 (Stream.of string l); ; val lex2 : string -> Genlex.token Stream.t = # let pars2 g gi s = match s with parser [< ’(Genlex.Kwd "HEIGHT"); ’(Genlex.Int i) >] → gi.g h <- i | [< ’(Genlex.Kwd "LENGTH"); ’(Genlex.Int i) >] → gi.g w <- i | [< ’(Genlex.Kwd "POSITION"); ’(Genlex.Ident s); ’(Genlex.Int i); ’(Genlex.Int j) >] → gi.npos.(index s g) <- (i,j) | [< ’(Genlex.Kwd "COLOR"); ’(Genlex.Ident s); ’(Genlex.Int r); ’(Genlex.Int g); ’(Genlex.Int b) >] → gi.opt <- (s, Awi.Copt (Graphics.rgb r g b)) :: gi.opt | [<>] → () ; ; Characters 44-46: [< ’(Genlex.Kwd "HEIGHT"); ’(Genlex.Int i) >] -> gi.g_h <- i ^^ Syntax error

アプリケーションの作成 create graph 関数は入力ファイルの名前を受け取り、グラフとグラフィカルインター フェースに関する情報の組を返します。 # let create gg graph name = let g = create graph name in let gi = {npos = Array.create g.size (0,0); opt=[]; g w =0; g h = 0;} in let ic = open in name in try print string ("Loading (pass 2) " ^name ^" : ");

最小コスト経路を見つける

403

while true do print string "."; let l = input line ic in pars2 g gi (lex2 l) done ; g,gi with End of file → print newline () ; close in ic; g,gi; ; Characters 95-96: let gi = {npos = Array.create g.size (0,0); opt=[]; g_w =0; g_h = 0;} in ^ This expression has type int -> ’a graph but is here used with type ’b graph

create app 関数はグラフィカルインターフェースを構築します。 # let create app name = let g,gi = create gg graph name in let size = (string of int gi.g w) ^ "x" ^ (string of int gi.g h) in Graphics.open graph (" "^size); let gg = create gg (create comp graph g) gi.npos id id in main gg gg gi.g w gi.g h [ "Background", Awi.Copt (Graphics.rgb 130 130 130) ; "Foreground", Awi.Copt Graphics.green ] [ "Relief", Awi.Sopt "Top" ; "Border_size", Awi.Iopt 2 ] ; gg; ; Characters 37-52: let g,gi = create_gg_graph name in ^^^^^^^^^^^^^^^ Unbound value create_gg_graph

最後に、main 関数は入力ファイルをコマンドライン引数として受け取り、インターフェー スつきのグラフを構築し、メインコンポーネントのイベント処理ループを開始します。 # let main () = if (Array.length Sys.argv ) <> 2 then Printf.printf "Usage: dij.exe filename\n" else let gg = create app Sys.argv.(1) in Awi.loop true false gg.main; ; Characters 122-132: let gg = create_app Sys.argv.(1) in ^^^^^^^^^^ Unbound value create_app

このプログラムの最後の式は main をスタートさせます。

404

Chapter 13 : Applications

実行可能ファイル スタンドアロンアプリケーションを作る動機は配布が容易にしたいということです。こ のセクションで定義した型や関数を dij.ml にまとめました。このファイルをコンパイ ルして必要なライブラリをリンクします。Linux 環境でコンパイルするためのコマンド ラインは次のようになります。

ocamlc -custom -o dij.exe graphics.cma awi.cmo graphs.ml \ -cclib -lgraphics -cclib -L/usr/X11/lib -cclib -lX11 Graphics ライブラリを使ったスタンドアロンアプリケーションのコンパイル方法は 5 章 と 7 章に書いてあります。

最後に このアプリケーションの骨格は十分に一般的なものです。重みつきグラフで表現できる 種類の問題はたくさんあります。たとえば迷路中の経路は、各交差点がノードであるよ うなグラフとして表せます。迷路を解くことは、スタート/ゴール間の最小コスト経路を 計算することに対応します。

C 言語、Objective Caml で性能の比較を行うために私達はダイクストラ法のアルゴリズ ムを C 言語でも書いてみました。C のプログラムでは Objective Caml のと同じデータ 構造を使っています。 グラフィカルインターフェースをより良くするには、ファイル名を入力するテキストフィー ルドとグラフの読み込み/保存を行う 2 つのボタンを追加します。見た目を良くするため にノードの位置をマウスで修正できるといいかもしれません。

2 つめの改善点はノードの外見を変えられる機能です。ボタンを表示するときには矩形 を描画する関数が呼ばれます。ノードの描画にポリゴンを使うこともできます。

Part III

Application Structure

405

407 The third part of this work is dedicated to application development and describes two ways of organizing applications: modules and objects. The goal is to easily structure an application for incremental and rapid development, maintenance facilitated by the ability to change gracefully, and the possibility of reusing large parts for future development. We have already presented the language’s predefined modules (第 8 章参照) viewed as compilation units. Objective Caml’s module language supports on the one hand the definition of new simple modules in order to build one’s own libraries, perhaps including abstract types, and on the other hand the definition of modules parameterized by other modules, called functors. The advantage of this parameterization lies in being able to “apply” a module to different argument modules in order to create specialized modules. Communication between modules is thus explicit, via the parameter module signature, which contains the types of its global declarations. However, nothing stops you from applying a functor to a module with a more extended signature, as long as it remains compatible with the specified parameter signature. Besides, the Objective Caml language has an object-oriented extension. First of all object-oriented programming permits structured communication between objects. Rather than applying a function to some arguments, one sends a message (a request) to an object which knows how to deal with it. The object, an instance of a class (a structure gathering together data and methods), then executes the corresponding code. The main relation between classes is inheritance, which lets one describe subclasses which retain all the declarations of the ancestor class. Late binding between the name of a message and the corresponding code within the object takes place during program execution. Nevertheless Objective Caml typing guarantees that the receiving object will always have a method of this name, otherwise type inference would have raised a compile-time error. The second important relation is subtyping, where an object of a certain class can always be used in place of an object of another class. In this way a new type of polymorphism is introduced: inclusion polymorphism. Finally the construction of a graphical interface, begun in chapter 5, uses different event management models. One puts together in an interface several components with respect to which the user or the system can produce events. The association of a component with a handler for one or more events taking place on it allows one to easily add to and modify such interfaces. The component-event-handler association can be cloaked in several forms: definition of a function (called a callback), inheritance with redefinition of handler methods, or finally registration of a handling object (delegation model). Chapter 14 is a presentation of modular programming. The different prevailing terminologies of abstract data types and module languages are explained and illustrated by simple modules. Then the module language is detailed. The correspondence between modules (simple or not) and compilation units is made clear. Chapter 15 contains an introduction to object-oriented programming. It brings a new way of structuring Objective Caml programs, an alternative to modules. This chapters shows how the notions of object-oriented programming (simple and multiple inheritance, abstract classes, parameterized classes, late binding) are articulated with

408 respect to the language’s type system, and extend it by the subtyping relation to inclusion polymorphism. Chapter 16 compares the two preceding software models and explains what factors to consider in deciding between the two, while also demonstrating how to simulate one by the other. It treats various cases of mixed models. Mixing leads to the enrichment of each of these two models, in particular with parameterized classes using the abstract type of a module. Chapter 17 presents two classes of applications: two-player games, and the construction of a world of virtual robots. The first example is organized via various parameterized modules. In particular, a parameterized module is used to represent games for application of the minimax αβ algorithm. It is then applied to two specific games: Connect 4 and Stone Henge. The second example uses an object model of a world and of abstract robots, from which, by inheritence, various simulations are derived. This example is presented in chapter 21.

14 モジュールを使ったプロ グラミング Modular design and modular programming support the decomposition of a program into several software units, also called modules, which can be developed largely independently. ひとつひとつのモジュールは,プログラム全体を構成する他のモジュールと は別にコンパイルすることができます.その結果,あるモジュールを使ってプログラム を開発する人は,そのモジュールのソースコードを得る必要がなくなります — コンパ イルされたモジュールのコードがあれば実行できるプログラムをつくるのに十分なので す. しかし,プログラマは使われているモジュールのインターフェース,すなわち,そ のモジュールが,どんな値,関数,例外,果てはサブモジュールを,どんな名前や型で 提供しているかということを知らなくてはなりません. モジュールのインターフェースを明示的に書き下すことによって,そのモジュールを使 うプログラムから,モジュールの実装の詳細を隠蔽することができます.モジュールに ついて知ることができるのは,モジュール外に「輸出」された定義の名前と型で,その 正確な実装はわかりません.ですから,モジュールの保守の際に,高い柔軟性をもって モジュールの実装を変えていくことができます.つまり,インターフェースが変わらず, また挙動が保存されている限り,モジュールを使う側は実装が変わったことに気が付か ないでしょう.このことは,大きなプログラムの保守や変更を非常に容易にすることが できます.局所定義のように,モジュールインターフェースは,モジュール設計者が公 開したくない実装の一部を隠す支援をします.この隠蔽機構の重要な応用として,抽象 データ型の実装があります. 最後に,Objective Caml のモジュールシステムのように先進的なものは,汎用体とも呼 ばれる,パラメータつきモジュールの定義を支援しています.パラメータつきモジュー ルは他のモジュールをパラメータとしてとるモジュールで,コード再利用の機会を増や してくれます.

410

Chapter 14 : モジュールを使ったプログラミング

この章のあらまし 最初の章では標準ライブラリの Stack モジュールの例を使って Objective Caml モジュー ルを説明し,そして,このモジュールを別の実装・同じインターフェースで作成してみ ます.第 2 節は,簡単なモジュールに関する Objective Caml のモジュール言語を導入 し,その使い方のいくつかを示します.特に,モジュール間の型共有について議論しま す. 第 3 節で,Objective Caml ではファンクタと呼ばれる,パラメータつきモジュー ルを扱います.最後に第 4 節で,モジュールプログラミングの大きな例として,銀行・ 顧客からの複数の視点や,いくつかのパラメータがあるような銀行口座管理プログラム を作成します.

コンパイル単位としてのモジュール Objective Caml は,多数の定義済のモジュールとともに配布されています. 8 章ではこれらのモジュールをプログラム中でどのように使うかを見ました.ここでは, ユーザがどのようにして似たようなモジュールを定義できるかを示します.

インターフェースと実装 Stack という定義済モジュールはスタック—「後入れ先だし」の規則にしたがうような 待ち行列—の主要な機能を提供します. # let queue = Stack.create () ; ; val queue : ’_a Stack.t = # Stack.push 1 queue ; Stack.push 2 queue ; Stack.push 3 queue ; ; - : unit = () # Stack.iter (fun n → Printf.printf "%d " n) queue ; ; 3 2 1 - : unit = ()

Objective Caml はすべてのソースコードとともに配布されていますから,スタックの実 際の実装を見ることができます. ocaml-2.04/stdlib/stack.ml type ’a t = { mutable c : ’a list } exception Empty let create () = { c = [] } let clear s = s.c <- [] let push x s = s.c <- x :: s.c let pop s = match s.c with hd :: tl → s.c <- tl; hd let length s = List.length s.c let iter f s = List.iter f s.c

|

[]

→ raise Empty

コンパイル単位としてのモジュール

411

これをみると,スタックの型(Stack モジュールの外側では Stack.t,内側ではただ単 に t と書かれます)はリストを含む変更可能フィールドをひとつ持ったレコードである ことがわかります.このリストはスタックの内容を保持していて,リストの先頭がスタッ クの一番上に対応しています.スタック操作は,基本的なリスト操作をレコードのフィー ルドに適用することで実装されています. この内部に関する知識を使えば,スタックを表現しているリストに直接アクセスできそ うですが,Objective Caml ではそれは許されません. # let list = queue.c ; ; Characters 12-19: let list = queue.c ;; ^^^^^^^ Unbound record field label c

コンパイラは Stack.t が c フィールドを持ったレコード型であることを知らないかのご とく文句を言ってきます.Stack モジュールのインターフェースをみれば,コンパイラ は本当に知らないことがわかります. ocaml-2.04/stdlib/stack.mli (* Module [Stack]: last-in first-out stacks *) (* This module implements stacks (LIFOs), with in-place modification. *)

type ’a t

(* The type of stacks containing elements of type [’a]. *)

exception Empty

(* Raised when [pop] is applied to an empty stack. *)

val create: unit → ’a t (* Return a new stack, initially empty. *) val push: ’a → ’a t → unit (* [push x s] adds the element [x] at the top of stack [s]. *) val pop: ’a t → ’a (* [pop s] removes and returns the topmost element in stack [s], or raises [Empty] if the stack is empty. *) val clear : ’a t → unit (* Discard all elements from a stack. *) val length: ’a t → int (* Return the number of elements in a stack. *) val iter: (’a → unit) → ’a t → unit (* [iter f s] applies [f] in turn to all elements of [s], from the element at the top of the stack to the element at the bottom of the stack. The stack itself is unchanged. *)

モジュールの機能について述べたコメントに加えて,このファイルは,stack.ml ファ イルで定義された値,型,例外の識別子のうち,Stack モジュールを使う人に見えるべ きものを明示的に並べています.より正確には,インターフェースには,これら輸出さ れる定義の名前と型の仕様を宣言します.特に,t という名前の型が輸出されています

412

Chapter 14 : モジュールを使ったプログラミング

が,その型の表現(つまり c フィールドを持つレコード)はこのインターフェースには 与えられていません.なので,Stack モジュールを使う人は,Stack.t がどのように表 現されているかわからず,この型の値に直接アクセスすることはできません.このこと を,Stack.t は抽象的である,とか不透明であるといいます. インターフェースはスタックを操作する関数も,その名前と型を与えて宣言しています. (関数の型は,型検査器がそれらの関数が正しく使われているかを検査できるように,明 示的に与えられなければいけません. )インターフェースでの値や関数の宣言は次のよう な構文で行います. 構文 :

val nom : type

インターフェースと実装を関連づける 上に示したように,Stack は,様々な定義をしている実装部分と,エクスポートされる 定義の宣言をしているインターフェース部分二つの部分からなります.インターフェー スに宣言されているものは,全て,実装部分にマッチする定義がなければなりません.ま た,実装部分に定義されている値や関数の型は,インターフェースに宣言された型とマッ チしていなければなりません. インターフェースと実装の関係は対称的ではありません.実装はインターフェースが要 求するより異状の定義を含むことができます.典型的には,エクスポートされた関数定 義が,その名前がインターフェースに現れない補助関数を使うことができます.このよ うな補助関数はモジュールのクライアントからは直接呼ぶことができません.同様に, インターフェースで定義の型を制限することができます.let id x = x で定義される, 恒等関数 id を含むモジュールを考えてみましょう. このインターフェースは id を,(よ り一般的な型である ’a --> ’a の代りに) int --> int 型として宣言することができ ます.すると,このモジュールのクライアントは id を整数にしか適用することができ ません. モジュールのインターフェースは,その実装から,明確に切り離されていますから,ひと つのインターフェースにたいして,いくつかの実装を与えたりすることができます.例 えば,異なるアルゴリズムや,操作が同じであるデータ構造をテストしたりすることが できます.例として,Stack モジュールの別の実装をみてみましょう.これは,リスト の代りに配列を使って実装しています. type ’a t = { mutable sp : int; mutable c : ’a array } exception Empty let create () = { sp=0 ; c = [||] } let clear s = s.sp <- 0; s.c <- [||] let size = 5 let increase s = s.c <- Array.append s.c (Array.create size s.c.(0)) let push x s = if s.sp >= Array.length s.c then increase s ; s.c.(s.sp) <- x ; s.sp <- succ s.sp

413

コンパイル単位としてのモジュール let pop s = if s.sp = 0 then raise Empty else let x = s.c.(s.sp) in s.sp <- pred s.sp ; x let length s = s.sp let iter f s = for i = pred s.sp downto 0 do f s.sc.(i) done

この新しい実装は,インターフェースファイル stack.mli からの要求を満たしていま す.ゆえに,どんなプログラムでも,定義済の Stack の代りに,この実装を使うことが できます.

分割コンパイル ほとんどの modern プログラミング言語と同様に,Objective Caml は別々にコンパイル される複数のコンパイル単位へのプログラムの分割を support しています.ひとつのコ ンパイル単位は,拡張子 .ml をもつ実装ファイルと 拡張子 .mli をもつインターフェー スファイルのふたつのファイルから構成されます.各コンパイル単位はモジュールとし て見ることができます.name.ml という実装ファイルをコンパイルすると Name という モジュールが定義されます.1 モジュールで定義される,値,型,例外は,ドット記法 (qualified (限定された?) 識別 子) と呼ばれる Module.identifier という形,または open 構文を使って,参照するこ とができます. a.ml

type t = { x:int ; y:int } ; ; let f c = c.x + c.y ; ;

b.ml

let val = { A.x = 1 ; A.y = 2 } ; ; A.f val ; ; open A ; ; f val ; ;

インターフェースファイル (.mli ファイル) は,このインターフェースに依存するモ ジュール (つまり,実装ファイルやこのモジュールのクライアント) がコンパイルされる 前に ocamlc -c でコンパイルしなければなりません. もしも,実装ファイルに対しインターフェースファイルがない場合,Objective Caml は, このモジュールは全ての定義をエクスポートしてるとみなします. 実行形式ファイルを生成するためのリンクは 7 章に述べられているように行われます— ocamlc コマンドを,-c オプションなしで,プログラムを構成する全てのコンパイル単 位のオブジェクトファイルを引数として並べて起動します.警告: オブジェクトファイル 名は依存している順でコマンドラインに並べられなければなりません.すなわち,もし, モジュール B が別のモジュール A を参照しているとしたら,リンカに渡すコマンドライ ン上で,オブジェクトファイル a.cmo は b.cmo より前に来なければなりません.結果 として,ふたつのモジュール間の相互依存は禁止されます. 1. name.ml と Name.ml は,ともに同じモジュール名になります.

414

Chapter 14 : モジュールを使ったプログラミング

例えば,a.ml と b.ml というふたつのソースファイルと,それにマッチするインター フェースファイル a.mli,b.mli から実行形式ファイルを生成するには,次のようにコ マンドを起動します.

> > > > >

ocamlc ocamlc ocamlc ocamlc ocamlc

-c a.mli -c a.ml -c b.mli -c b.ml a.cmo b.cmo

インターフェースファイル,実装ファイルひとつずつからなるコンパイル単位は分割コ ンパイルと情報隠蔽をサポートします.しかし,プログラムを構造化する一般的なツー ルとしての能力は高くありません.特に,モジュールとファイルの間に一対一の関係が あるので,与えられたインターフェースにたいして複数の実装を同時に使ったり,逆に, 同じ実装にたいして複数のインターフェースを与えたりするのを妨げています.入れ子 のモジュールや,モジュールに関してパラメータ化もサポートされていません.これら の弱点をカバーするために,Objective Caml は特別な文法と構文によるモジュール言語 を提供して,言語自身の中でモジュールを操作できるようにしています.本章の残りで はこのモジュール言語を紹介します.

モジュール言語 The Objective Caml language features a sub-language for modules, which comes in addition to the core language that we have seen so far. In this module language, the interface of a module is called a signature and its implementation is called a structure. When there is no ambiguity, we will often use the word “module” to refer to a structure. The syntax for declaring signatures and structures is as follows:

構文 :

module type NAME = sig interface declarations end

構文 :

module Name = struct implementation definitions end

警告

The name of a module must start with an uppercase letter. There are no such case restrictions on names of signatures, but by convention we will use names in uppercase for signatures.

モジュール言語

415

Signatures and structures do not need to be bound to names: we can also use anonymous signature and structure expressions, writing simply 構文 :

sig declarations end

構文 :

struct definitions end

We write signature and structure to refer to either names of signatures and structures, or anonymous signature and structure expressions. Every structure possesses a default signature, computed by the type inference system, which reveals all the definitions contained in the structure, with their most general types. When defining a structure, we can also indicate the desired signature by adding a signature constraint (similar to the type constraints from the core language), using one of the following two syntactic forms: 構文 :

module Name : signature = structure

構文 :

module Name = (structure : signature)

When an explicit signature is provided, the system checks that all the components declared in the signature are defined in the structure structure, and that the types are consistent. In other terms, the system checks that the explicit signature provided is “included in”, or implied by, the default signature. If so, Name is viewed in the remainder of the code with the signature “signature”, and only the components declared in the signature are accessible to the clients of the module. (This is the same behavior we saw previously with interface files.) Access to the components of a module is via the dot notation: 構文 :

Name1 .name2

We say that the name name2 is qualified by the name Name1 of its defining module. The module name and the dot can be omitted using a directive to open the module: 構文 :

open Name

In the scope of this directive, we can use short names name2 to refer to the components of the module Name. In case of name conflicts, opening a module hides previously defined entities with the same names, as in the case of identifier redefinitions.

ふたつのスタックモジュール We continue the example of stacks by recasting it in the module language. The signature for a stack module is obtained by wrapping the declarations from the stack.mli file in a signature declaration: # module type STACK = sig type ’a t

416

Chapter 14 : モジュールを使ったプログラミング

exception Empty val create: unit → ’a t val push: ’a → ’a t → unit val pop: ’a t → ’a val clear : ’a t → unit val length: ’a t → int val iter: (’a → unit) → ’a t → unit end ; ; module type STACK = sig type ’a t exception Empty val create : unit -> ’a t val push : ’a -> ’a t -> unit val pop : ’a t -> ’a val clear : ’a t -> unit val length : ’a t -> int val iter : (’a -> unit) -> ’a t -> unit end

A first implementation of stacks is obtained by reusing the Stack module from the standard library: # module StandardStack = Stack ; ; module StandardStack : sig type ’a t = ’a Stack.t exception Empty val create : unit -> ’a t val push : ’a -> ’a t -> unit val pop : ’a t -> ’a val top : ’a t -> ’a val clear : ’a t -> unit val copy : ’a t -> ’a t val is_empty : ’a t -> bool val length : ’a t -> int val iter : (’a -> unit) -> ’a t -> unit end

We then define an alternate implementation based on arrays: # module MyStack = struct type ’a t = { mutable sp : int; mutable c : ’a array } exception Empty let create () = { sp=0 ; c = [||] } let clear s = s.sp <- 0; s.c <- [||] let increase s x = s.c <- Array.append s.c (Array.create 5 x) let push x s = if s.sp >= Array.length s.c then increase s x; s.c.(s.sp) <- x; s.sp <- succ s.sp let pop s =

モジュール言語

417

if s.sp =0 then raise Empty else (s.sp <- pred s.sp ; s.c.(s.sp)) let length s = s.sp let iter f s = for i = pred s.sp downto 0 do f s.c.(i) done end ; ; module MyStack : sig type ’a t = { mutable sp : int; mutable c : ’a array; } exception Empty val create : unit -> ’a t val clear : ’a t -> unit val increase : ’a t -> ’a -> unit val push : ’a -> ’a t -> unit val pop : ’a t -> ’a val length : ’a t -> int val iter : (’a -> ’b) -> ’a t -> unit end

These two modules implement the type t of stacks by different data types. # StandardStack.create () ; ; - : ’_a StandardStack.t = # MyStack.create () ; ; - : ’_a MyStack.t = {MyStack.sp = 0; MyStack.c = [||]}

To abstract over the type representation in Mystack, we add a signature constraint by the STACK signature. # module MyStack = (MyStack : STACK) ; ; module MyStack : STACK # MyStack.create () ; ; - : ’_a MyStack.t =

The two modules StandardStack and MyStack implement the same interface, that is, provide the same set of operations over stacks, but their t types are different. It is therefore impossible to apply operations from one module to values from the other module: # let s = StandardStack.create () ; ; val s : ’_a StandardStack.t = # MyStack.push 0 s ; ; Characters 15-16: MyStack.push 0 s ;; ^ This expression has type ’a StandardStack.t = ’a Stack.t but is here used with type int MyStack.t

Even if both modules implemented the t type by the same concrete type, constraining MyStack by the signature STACK suffices to abstract over the t type, rendering it incompatible with any other type in the system and preventing sharing of values and operations between the various stack modules.

418

Chapter 14 : モジュールを使ったプログラミング

# module S1 = ( MyStack : STACK ) ; ; module S1 : STACK # module S2 = ( MyStack : STACK ) ; ; module S2 : STACK # let s = S1.create () ; ; val s : ’_a S1.t = # S2.push 0 s ; ; Characters 10-11: S2.push 0 s ;; ^ This expression has type ’a S1.t but is here used with type int S2.t

The Objective Caml system compares abstract types by names. Here, the two types S1.t and S2.t are both abstract, and have different names, hence they are considered as incompatible. It is precisely this restriction that makes type abstraction effective, by preventing any access to the definition of the type being abstracted.

モジュールと情報隠蔽 This section shows additional examples of signature constraints hiding or abstracting definitions of structure components.

型の実装を隠蔽する Abstracting over a type ensures that the only way to construct values of this type is via the functions exported from its definition module. This can be used to restrict the values that can belong to this type. In the following example, we implement an abstract type of integers which, by construction, can never take the value 0. # module Int Star = ( struct type t = int exception Isnul let of int = function 0 → raise Isnul | n → n let mult = ( * ) end : sig type t exception Isnul val of int : int → t val mult : t → t → t end ) ;; module Int_Star : sig type t exception Isnul val of_int : int -> t val mult : t -> t -> t end

モジュール言語

419

値を隠蔽する We now define a symbol generator, similar to that of page 101, using a signature constraint to hide the state of the generator. We first define the signature GENSYM exporting only two functions for generating symbols. # module type GENSYM = sig val reset : unit → unit val next : string → string end ; ;

We then implement this signature as follows: # module Gensym : GENSYM = struct let c = ref 0 let reset () = c:=0 let next s = incr c ; s ^ (string of int !c) end; ; module Gensym : GENSYM

The reference c holding the state of the generator Gensym is not accessible outside the two exported functions. # Gensym.reset () ; ; - : unit = () # Gensym.next "T"; ; - : string = "T1" # Gensym.next "X"; ; - : string = "X2" # Gensym.reset () ; ; - : unit = () # Gensym.next "U"; ; - : string = "U1" # Gensym.c; ; Characters 0-8: Gensym.c;; ^^^^^^^^ Unbound value Gensym.c

The definition of c is essentially local to the structure Gensym, since it is hidden by the associated signature. The signature constraint achieves more simply the same goal as the local definition of a reference in the definition of the two functions reset s and new s on page 101.

420

Chapter 14 : モジュールを使ったプログラミング

モジュールの複数の「視点」 The module language and its signature constraints support taking several views of a given structure. For instance, we can have a “super-user interface” for the module Gensym, allowing the symbol counter to be reset, and a “normal user interface” that permits only the generation of new symbols, but no other intervention on the counter. To implement the latter interface, it suffices to declare the signature: # module type USER GENSYM = sig val next : string → string end; ; module type USER_GENSYM = sig val next : string -> string end

We then implement it by a mere signature constraint. # module UserGensym = (Gensym : USER GENSYM) ; ; module UserGensym : USER_GENSYM # UserGensym.next "U" ; ; - : string = "U2" # UserGensym.reset () ; ; Characters 0-16: UserGensym.reset() ;; ^^^^^^^^^^^^^^^^ Unbound value UserGensym.reset

The UserGensym module fully reuses the code of the Gensym module. In addition, both modules share the same counter: # Gensym.next "U" ; ; - : string = "U3" # Gensym.reset () ; ; - : unit = () # UserGensym.next "V" ; ; - : string = "V1"

モジュール間の型共有 As we saw on page 415, abstract types with different names are incompatible. This can be problematic when we wish to share an abstract type between several modules. There are two ways to achieve this sharing: one is via a special sharing construct in the module language; the other one uses the lexical scoping of modules.

制約による共有 The following example illustrates the sharing issue. We define a module M providing an abstract type M.t. We then restrict M on two different signatures exporting different subsets of operations. # module M = (

モジュール言語

421

struct type t = int ref let create () = ref 0 let add x = incr x let get x = if !x>0 then (decr x; 1) else failwith "Empty" end : sig type t val create : unit → t val add : t → unit val get : t → int end ) ;; # module type S1 = sig type t val create : unit → t val add : t → unit end ; ; # module type S2 = sig type t val get : t → int end ; ; # module M1 = (M:S1) ; ; module M1 : S1 # module M2 = (M:S2) ; ; module M2 : S2

As written above, the types M1.t and M2.t are incompatible. However, we would like to say that both types are abstract but identical. To do this, Objective Caml offers special syntax to declare a type equality over an abstract type in a signature. 構文 :

NAME with type t1 = t2 and . . .

This type constraint forces the type t1 declared in the signature NAME to be equal to the type t2 . Type constraints over all types exported by a sub-module can be declared in one operation with the syntax 構文 :

NAME with module Name1 = Name2

Using these type sharing constraints, we can declare that the two modules M1 and M2 define identical abstract types. # module M1 = (M:S1 with type t = M.t) ; ; module M1 : sig type t = M.t val create : unit -> t val add : t -> unit end # module M2 = (M:S2 with type t = M.t) ; ;

422

Chapter 14 : モジュールを使ったプログラミング

module M2 : sig type t = M.t val get : t -> int end # let x = M1.create () in M1.add x ; M2.get x ; ; - : int = 1

共有と入れ子モジュール Another possibility for ensuring type sharing is to use nested modules. We define two sub-modules (M1 et M2) sharing an abstract type defined in the enclosing module M. # module M = ( struct type t = int ref module M hide = struct let create () = ref 0 let add x = incr x let get x = if !x>0 then (decr x; 1) else failwith "Empty" end module M1 = M hide module M2 = M hide end : sig type t module M1 : sig val create : unit → t val add : t → unit end module M2 : sig val get : t → int end end ) ; ; module M : sig type t module M1 : sig val create : unit -> t val add : t -> unit end module M2 : sig val get : t -> int end end

As desired, values created by M1 can be operated upon by M2, while hiding the representation of these values. # let x = M.M1.create () ; ; val x : M.t = # M.M1.add x ; M.M2.get x ; ; - : int = 1

This solution is heavier than the previous solution based on type sharing constraints: the functions from M1 and M2 can only be accessed via the enclosing module M.

単純なモジュールを拡張する Modules are closed entities, defined once and for all. In particular, once an abstract type is defined using the module language, it is impossible to add further operations on the abstract type that depend on the type representation without modifying the

パラメータつきモジュール

423

module definition itself. (Operations derived from existing operations can of course be added later, outside the module.) As an extreme example, if the module exports no creation function, clients of the module will never be able to create values of the abstract type! Therefore, adding new operations that depend on the type representation requires editing the sources of the module and adding the desired operations in its signature and structure. Of course, we then get a different module, and clients need to be recompiled. However, if the modifications performed on the module signature did not affect the components of the original signature, the remainder of the program remains correct and does not need to be modified, just recompiled.

パラメータつきモジュール Parameterized modules are to modules what functions are to base values. Just like a function returns a new value from the values of its parameters, a parameterized module builds a new module from the modules given as parameters. Parameterized modules are also called functors. The addition of functors to the module language increases the opportunities for code reuse in structures. Functors are defined using a function-like syntax: 構文 :

functor ( Name : signature ) –> structure

# module Couple = functor ( Q : sig type t end ) → struct type couple = Q.t * Q.t end ; ; module Couple : functor (Q : sig type t end) -> sig type couple = Q.t * Q.t end

As for functions, syntactic sugar is provided for defining and naming a functor: 構文 :

module Name1 ( Name2 : signature ) = structure

# module Couple ( Q : sig type t end ) = struct type couple = Q.t * Q.t end ; ; module Couple : functor (Q : sig type t end) -> sig type couple = Q.t * Q.t end

A functor can take several parameters:

424

構文 :

Chapter 14 : モジュールを使ったプログラミング

functor ( Name1 : signature1 ) –> .. . functor ( Namen : signaturen ) –> structure

The syntactic sugar for defining and naming a functor extends to multiple-argument functors: 構文 :

module Name (Name1 : signature1 ) . . . ( Namen : signaturen ) = structure

The application of a functor to its arguments is written thus: 構文 :

module Name = functor ( structure1 ) . . . ( structuren )

Note that each parameter is written between parentheses. The result of the application can be either a simple module or a partially applied functor, depending on the number of parameters of the functor. 警告

There is no equivalent to functors at the level of signature: it is not possible to build a signature by application of a “functorial signature” to other signatures.

A closed functor is a functor that does not reference any module except its parameters. Such a closed functor makes its communications with other modules entirely explicit. This provides maximal reusability, since the modules it references are determined at application time only. There is a strong parallel between a closed function (without free variables) and a closed functor.

ファンクタとコード再利用 The Objective Caml standard library provides three modules defining functors. Two of them take as argument a module implementing a totally ordered data type, that is, a module with the following signature: # module type OrderedType = sig type t val compare: t → t → int end ; ; module type OrderedType = sig type t val compare : t -> t -> int end

Function compare takes two arguments of type t and returns a negative integer if the first is less than the second, zero if both are equal, and a positive integer if the first is greater than the second. Here is an example of totally ordered type: pairs of integers equipped with lexicographic ordering.

パラメータつきモジュール

425

# module OrderedIntPair = struct type t = int * int let compare (x1,x2) (y1,y2) = if x1 < y1 then -1 else if x1 > y1 then 1 else if x2 < y2 then -1 else if x2 > y2 then 1 else 0 end ; ; module OrderedIntPair : sig type t = int * int val compare : ’a * ’b -> ’a * ’b -> int end

The functor Make from module Map returns a module that implements association tables whose keys are values of the ordered type passed as argument. This module provides operations similar to the operations on association lists from module List, but using a more efficient and more complex data structure (balanced binary trees). # module AssocIntPair = Map.Make (OrderedIntPair) ; ; module AssocIntPair : sig type key = OrderedIntPair.t and ’a t = ’a Map.Make(OrderedIntPair).t val empty : ’a t val add : key -> ’a -> ’a t -> ’a t val find : key -> ’a t -> ’a val remove : key -> ’a t -> ’a t val mem : key -> ’a t -> bool val iter : (key -> ’a -> unit) -> ’a t -> unit val map : (’a -> ’b) -> ’a t -> ’b t val mapi : (key -> ’a -> ’b) -> ’a t -> ’b t val fold : (key -> ’a -> ’b -> ’b) -> ’a t -> ’b -> ’b end

The Make functor allows to construct association tables over any key type for which we can write a compare function. The standard library module Set also provides a functor named Make taking an ordered type as argument and returning a module implementing sets of sets of values of this type. # module SetIntPair = Set.Make (OrderedIntPair) ; ; module SetIntPair : sig type elt = OrderedIntPair.t and t = Set.Make(OrderedIntPair).t val empty : t val is_empty : t -> bool val mem : elt -> t -> bool val add : elt -> t -> t

426 val val val val val val val val val val val val val val val val val val val end

Chapter 14 : モジュールを使ったプログラミング singleton : elt -> t remove : elt -> t -> t union : t -> t -> t inter : t -> t -> t diff : t -> t -> t compare : t -> t -> int equal : t -> t -> bool subset : t -> t -> bool iter : (elt -> unit) -> t -> unit fold : (elt -> ’a -> ’a) -> t -> ’a -> ’a for_all : (elt -> bool) -> t -> bool exists : (elt -> bool) -> t -> bool filter : (elt -> bool) -> t -> t partition : (elt -> bool) -> t -> t * t cardinal : t -> int elements : t -> elt list min_elt : t -> elt max_elt : t -> elt choose : t -> elt

The type SetIntPair.t is the type of sets of integer pairs, with all the usual set operations provided in SetIntPair, including a set comparison function SetIntPair.compare. To illustrate the code reuse made possible by functors, we now build sets of sets of integer pairs. # module SetofSet = Set.Make (SetIntPair) ; ; # let x = SetIntPair.singleton (1,2) ; ; val x : SetIntPair.t = # let y = SetofSet.singleton SetIntPair.empty ; ; val y : SetofSet.t = # let z = SetofSet.add x y ; ; val z : SetofSet.t =

(* x = { (1,2) }

*)

(* y = { {} }

*)

(* z = { {(1,2)} ; {} } *)

The Make functor from module Hashtbl is similar to that from the Map module, but implements (imperative) hash tables instead of (purely functional) balanced trees. The argument to Hashtbl.Make is slightly different: in addition to the type of the keys for the hash table, it must provide an equality function testing the equality of two keys (instead of a full-fledged comparison function), plus a hash function, that is, a function associating integers to keys. # module type HashedType = sig type t val equal: t → t → bool val hash: t → int end ; ; module type HashedType = sig type t val equal : t -> t -> bool val hash : t -> int end

パラメータつきモジュール

427

# module IntMod13 = struct type t = int let equal = (=) let hash x = x mod 13 end ; ; module IntMod13 : sig type t = int val equal : ’a -> ’a -> bool val hash : int -> int end # module TblInt = Hashtbl.Make (IntMod13) ; ; module TblInt : sig type key = IntMod13.t and ’a t = ’a Hashtbl.Make(IntMod13).t val create : int -> ’a t val clear : ’a t -> unit val copy : ’a t -> ’a t val add : ’a t -> key -> ’a -> unit val remove : ’a t -> key -> unit val find : ’a t -> key -> ’a val find_all : ’a t -> key -> ’a list val replace : ’a t -> key -> ’a -> unit val mem : ’a t -> key -> bool val iter : (key -> ’a -> unit) -> ’a t -> unit val fold : (key -> ’a -> ’b -> ’b) -> ’a t -> ’b -> ’b end

モジュールの局所定義 The Objective Caml core language allows a module to be defined locally to an expression. 構文 :

let module Name = structure in expr

For instance, we can use the Set module locally to write a sort function over integer lists, by inserting each list element into a set and finally converting the set to the sorted list of its elements. # let sort l = let module M = struct type t = int let compare x y = if x < y then -1 else if x > y then 1 else 0 end in let module MSet = Set.Make(M) in MSet.elements (List.fold right MSet.add l MSet.empty) ; ; val sort : int list -> int list =

428

Chapter 14 : モジュールを使ったプログラミング

# sort [ 5 ; 3 ; 8 ; 7 ; 2 ; 6 ; 1 ; 4 ] ; ; - : int list = [1; 2; 3; 4; 5; 6; 7; 8]

Objective Caml does not allow a value to escape a let module expression if the type of the value is not known outside the scope of the expression. # let test = let module Foo = struct type t let id x = (x:t) end in Foo.id ; ; Characters 15-101: ..let module Foo = struct type t let id x = (x:t) end in Foo.id... This ‘let module’ expression has type Foo.t -> Foo.t In this type, the locally bound module name Foo escapes its scope

より大きな例: 銀行口座の管理 We conclude this chapter by an example illustrating the main aspects of modular programming: type abstraction, multiple views of a module, and functor-based code reuse. The goal of this example is to provide two modules for managing a bank account. One is intended to be used by the bank, and the other by the customer. The approach is to implement a general-purpose parameterized functor providing all the needed operations, then apply it twice to the correct parameters, constraining it by the signature corresponding to its final user: the bank or the customer.

プログラムの構成 The two end modules BManager and CManager are obtained by constraining the module Manager. The latter is obtained by applying the functor FManager to the modules Account, Date and two additional modules built by application of the functors FLog and FStatement. Figure 14.1 illustrates these dependencies.

モジュールパラメータのシグネチャ The module for account management is parameterized by four other modules, whose signatures we now detail.

より大きな例: 銀行口座の管理

429

BManager

CManager

Manager

FManager

FStatement

Account FLog

Date

図 14.1: Modules dependency graph.

銀行口座 This module provides the basic operations on the contents of the account. # module type ACCOUNT = sig type t exception BadOperation val create : float → float → t val deposit : float → t → unit val withdraw : float → t → unit val balance : t → float end ; ; This set of functions provide the minimal operations on an account. The creation operation takes as arguments the initial balance and the maximal overdraft allowed. Excessive withdrawals may raise the BadOperation exception. 順序つきの鍵 Operations are recorded in an operation log described in the next paragraph. Each log entry is identified by a key. Key management functions are described by the following signature: # module type OKEY = sig type t val create : unit → t val of string : string → t val to string : t → string val eq : t → t → bool val lt : t → t → bool val gt : t → t → bool end ; ;

430

Chapter 14 : モジュールを使ったプログラミング

The create function returns a new, unique key. The functions of string and to string convert between keys and character strings. The three remaining functions are key comparison functions. ヒストリ Logs of operations performed on an account are represented by the following abstract types and functions: # module type LOG = sig type tkey type tinfo type t val create : unit → t val add : tkey → tinfo → t → unit val nth : int → t → tkey*tinfo val get : (tkey → bool) → t → (tkey*tinfo) list end ; ;

We keep unspecified for now the types of the log keys (type tkey) and of the associated data (type tinfo), as well as the data structure for storing logs (type t). We assume that new informations added with the add function are kept in sequence. Two access functions are provided: access by position in the log (function nth) and access following a search predicate on keys (function get). 口座明細 The last parameter of the manager module provides two functions for editing a statement for an account: # module type STATEMENT = sig type tdata type tinfo val editB : tdata → tinfo val editC : tdata → tinfo end ; ; We leave abstract the type of data to process (tdata) as well as the type of informations extracted from the data (tinfo).

口座管理用パラメータつきモジュール Using only the information provided by the signatures above, we now define the generalpurpose functor for managing accounts. # module FManager = functor (C:ACCOUNT) → functor (K:OKEY) → functor (L:LOG with type tkey=K.t and type tinfo=float) → functor (S:STATEMENT with type tdata=L.t and type tinfo = (L.tkey*L.tinfo) list) → struct type t = { accnt : C.t; log : L.t }

より大きな例: 銀行口座の管理

431

let create s d = { accnt = C.create s d; log = L.create () } let deposit s g = C.deposit s g.accnt ; L.add (K.create () ) s g.log let withdraw s g = C.withdraw s g.accnt ; L.add (K.create () ) (-.s) g.log let balance g = C.balance g.accnt let statement edit g = let f (d,i) = (K.to string d) ^ ":" ^ (string of float i) in List.map f (edit g.log) let statementB = statement S.editB let statementC = statement S.editC end ; ; module FManager : functor (C : ACCOUNT) -> functor (K : OKEY) -> functor (L : sig type tkey = K.t and tinfo = float and t val create : unit -> t val add : tkey -> tinfo -> t -> unit val nth : int -> t -> tkey * tinfo val get : (tkey -> bool) -> t -> (tkey * tinfo) list end) -> functor (S : sig type tdata = L.t and tinfo = (L.tkey * L.tinfo) list val editB : tdata -> tinfo val editC : tdata -> tinfo end) -> sig type t = { accnt : C.t; log : L.t; } val create : float -> float -> t val deposit : L.tinfo -> t -> unit val withdraw : float -> t -> unit val balance : t -> float val statement : (L.t -> (K.t * float) list) -> t -> string list val statementB : t -> string list val statementC : t -> string list end

型共有 The type constraint over the parameter L of the FManager functor indicates that the keys of the log are those provided by the K parameter, and that the informations stored in the log are floating-point numbers (the transaction amounts). The type constraint over the S parameter indicates that the informations contained in the statement come from the log (the L parameter). The signature inferred for the FManager

432

Chapter 14 : モジュールを使ったプログラミング

functor reflects the type sharing constraints in the inferred signatures for the functor parameters. The type t in the result of FManager is a pair of an account (C.t) and its transaction log. 操作 All operations defined in this functor are defined in terms of lower-level functions provided by the module parameters. The creation, deposit and withdrawal operations affect the contents of the account and add an entry in its transaction log. The other functions return the account balance and edit statements.

パラメータの実装 Before building the end modules, we must first implement the parameters to the FManager module. 口座 The data structure for an account is composed of a float representing the current balance, plus the maximum overdraft allowed. The latter is used to check withdrawals. # module Account:ACCOUNT = struct type t = { mutable balance:float; overdraft:float } exception BadOperation let create b o = { balance=b; overdraft=(-. o) } let deposit s c = c.balance <- c.balance +. s let balance c = c.balance let withdraw s c = let ss = c.balance -. s in if ss < c.overdraft then raise BadOperation else c.balance <- ss end ; ; module Account : ACCOUNT

記録用鍵を選ぶ We decide that keys for transaction logs should be the date of the transaction, expressed as a floating-point number as returned by the time function from module Unix. # module Date:OKEY = struct type t = float let create () = Unix.time () let of string = float of string let to string = string of float let eq = (=) let lt = (<) let gt = (>) end ; ;

より大きな例: 銀行口座の管理

433

module Date : OKEY

記録 The transaction log depends on a particular choice of log keys. Hence we define logs as a functor parameterized by a key structure. # module FLog (K:OKEY) = struct type tkey = K.t type tinfo = float type t = { mutable contents : (tkey*tinfo) list } let create () = { contents = [] } let add c i l = l.contents <- (c,i) :: l.contents let nth i l = List.nth l.contents i let get f l = List.filter (fun (c,_) → (f c)) l.contents end ; ; module FLog : functor (K : OKEY) -> sig type tkey = K.t and tinfo = float and t = { mutable contents : (tkey * tinfo) list; } val create : unit -> t val add : tkey -> tinfo -> t -> unit val nth : int -> t -> tkey * tinfo val get : (tkey -> bool) -> t -> (tkey * tinfo) list end

Notice that the type of informations stored in log entries must be consistent with the type used in the account manager functor. 明細 We define two functions for editing statements. The first (editB) lists the five most recent transactions, and is intended for the bank; the second (editC) lists all transactions performed during the last 10 days, and is intended for the customer. # module FStatement (K:OKEY) (L:LOG with type tkey=K.t) = struct type tdata = L.t type tinfo = (L.tkey*L.tinfo) list let editB h = List.map (fun i → L.nth i h) [0;1;2;3;4] let editC h = let c0 = K.of string (string of float ((Unix.time () ) -. 864000.)) in let f = K.lt c0 in L.get f h end ; ; module FStatement : functor (K : OKEY) -> functor

434

Chapter 14 : モジュールを使ったプログラミング (L : sig type tkey = K.t and tinfo and t val create : unit -> t val add : tkey -> tinfo -> t -> unit val nth : int -> t -> tkey * tinfo val get : (tkey -> bool) -> t -> (tkey * tinfo) list end) -> sig type tdata = L.t and tinfo = (L.tkey * L.tinfo) list val editB : L.t -> (L.tkey * L.tinfo) list val editC : L.t -> (L.tkey * L.tinfo) list end

In order to define the 10-day statement, we need to know exactly the implementation of keys as floats. This arguably goes against the principles of type abstraction. However, the key corresponding to ten days ago is obtained from its string representation by calling the K.of string function, instead of directly computing the internal representation of this date. (Our example is probably too simple to make this subtle distinction obvious.) 目的のモジュール To build the modules MBank and MCustomer, for use by the bank and the customer respectively, we proceed as follows:

1.

define a common “account manager” structure by application of the FManager functor;

2.

declare two signatures listing only the functions accessible to the bank or to the customer;

3.

constrain the structure obtained in 1 with the signatures declared in 2.

# module Manager = FManager (Account) (Date) (FLog(Date)) (FStatement (Date) (FLog(Date))) ; ; module Manager : sig type t = FManager(Account)(Date)(FLog(Date))(FStatement(Date)(FLog(Date))).t = { accnt : Account.t; log : FLog(Date).t; } val create : float -> float -> t val deposit : FLog(Date).tinfo -> t -> unit val withdraw : float -> t -> unit val balance : t -> float

より大きな例: 銀行口座の管理

435

val statement : (FLog(Date).t -> (Date.t * float) list) -> t -> string list val statementB : t -> string list val statementC : t -> string list end # module type MANAGER BANK = sig type t val create : float → float → t val deposit : float → t → unit val withdraw : float → t → unit val balance : t → float val statementB : t → string list end ; ; # module MBank = (Manager:MANAGER BANK with type t=Manager.t) ; ; module MBank : sig type t = Manager.t val create : float -> float -> t val deposit : float -> t -> unit val withdraw : float -> t -> unit val balance : t -> float val statementB : t -> string list end # module type MANAGER CUSTOMER = sig type t val deposit : float → t → unit val withdraw : float → t → unit val balance : t → float val statementC : t → string list end ; ; # module MCustomer = (Manager:MANAGER CUSTOMER with type t=Manager.t) ; ; module MCustomer : sig type t = Manager.t val deposit : float -> t -> unit val withdraw : float -> t -> unit val balance : t -> float val statementC : t -> string list end

In order for accounts created by the bank to be usable by clients, we added the type constraint on Manager.t in the definition of the MBank and MCustomer structures, to ensure that their t type components are compatible.

436

Chapter 14 : モジュールを使ったプログラミング

練習問題 連想リスト In this first simple exercise, we will implement a polymorphic abstract type for association lists, and present two different views of the implementation. 1.

Define a signature ALIST declaring an abstract type with two type parameters (one for the keys, the other for the associated values), a creation function, an add function, a lookup function, a membership test, and a deletion function. The interface should be functional, i.e. without in-place modifications of the abstract type.

2.

Define a module Alist implementing the signature ALIST

3.

Define a signature ADM ALIST for “administrators” of association lists. Administrators can only create association lists, and add or remove entries from a list.

4.

Define a signature USER ALIST for “users” of association lists. Users can only perform lookups and membership tests.

5.

Define two modules AdmAlist and UserAlist for administrators and for users. Keep in mind that users must be able to access lists created by administrators.

パラメータつきベクトル This exercise illustrates the genericity and code reuse abilities of parameterized modules. We will define a functor for manipulating two-dimensional vectors (pairs of (x, y) coordinates) that can be instantiated with different types for the coordinates. Numbers have the following signature: # module type NUMBER = sig type a type t val create : a → t val add : t → t → t val string of : t → string end ; ;

1.

Define the functor FVector, parameterized by a module of signature NUMBER, and defining a type t of two-dimensional vectors over these numbers, a creation function, an addition function, and a conversion to strings.

2.

Define a signature VECTOR, without parameters, where the types of numbers and vectors are abstract.

3.

Define three structures Rational, Float et Complex implementing the signature NUMBER.

まとめ

4.

437

Use these structures to define (by functor application) three modules for vectors of rationals, reals and complex.

字句解析木 This exercise follows up on the lexical trees introduced in chapter 2, page 62. The goal is to define a generic module for handling lexical trees, parameterized by an abstract type of words. 1.

Define the signature WORD defining an abstract type alpha for letters of the alphabet, and another abstract type t for words on this alphabet. Declare also the empty word, the conversion from an alphabet letter to a one-letter word, the accessor to a letter of a word, the sub-word operation, the length of a word, and word concatenation.

2.

Define the functor LexTree, parameterized by a module implementing WORD, that defines (as a function of the types and operations over words) the type of lexical trees and functions exists, insert et select similar to those from chapter 2, page 62.

3.

Define the module Chars implementing the WORD signature for the types alpha = char and t = string. Use it to obtain a module CharDict implementing dictionaries whose keys are character strings.

まとめ In this chapter, we introduced all the facilities that the Objective Caml module language offers, in particular parameterized modules. As all module systems, it reflects the duality between interfaces and implementations, here presented as a duality between signatures and structures. Signatures allow hiding information about type, value or exception definitions. By hiding type representation, we can make certain types abstract, ensuring that values of these types can only be manipulated through the operations provided in the module signature. We saw how to exploit this mechanism to facilitate sharing of values hidden in closures, and to offer multiple views of a given implementation. In the latter case, explicit type sharing annotations are sometimes necessary to achieve the desired behavior. Parameterized modules, also called functors, go one step beyond and support code reuse through simple mechanisms similar to function abstraction and function application.

もっと学びたい人のために Other examples of modules and functors can be found in chapter 4 of the Objective Caml manual.

438

Chapter 14 : モジュールを使ったプログラミング

The underlying theory and the type checking for modules can be found in a number of research articles and course notes by Xavier Leroy, at リンク: http://cristal.inria.fr/˜xleroy

The Objective Caml module system follows the same principles as that of its cousin the SML language. Chapter 22 compares these two languages in more details and provides bibliographical references for the interested reader. Other languages feature advanced module systems, in particular Modula-3 (2 and 3), and ADA. They support the definition of modules parameterized by types and values.

15 オブジェクト指向プログ ラミング 言語名にも示されているように Objective Caml はオブジェクト指向プログラミングの 機能を持っています。命令の逐次実行によって進んでいく手続き型プログラミングや必 要な計算から簡約していく関数型プログラミングとは異なり、オブジェクト指向プログ ラミングはデータ駆動型と考えることができます。オブジェクトは、関連するデータの 集合を系統立てる新しい方法を提供しています。まず、データと操作を一まとめにする クラスがあります。操作は、メソッドとも呼ばれますが、オブジェクトの取り得る振る 舞いを定義します。メソッドはオブジェクトにメッセージを送ることによって起動され ます。オブジェクトはメッセージを受け取るとそのメッセージによって指定されたメソッ ドに対応する行動、計算を開始します。この働きは関数に引数を適用することとは違っ ています。なぜなら実際に実行されることになるコードを決めるのはオブジェクト自身 だからです。オブジェクトへ送られたメッセージにはメソッドの名前が含まれています。 名前とコードの間のこのような遅延束縛の働きによって、柔軟で再利用性の高いプログ ラムを記述することができます。 オブジェクト指向プログラミングでは集約 や 継承 などのクラスの間の関係を定義する ことができます。クラスはオブジェクト同士がメッセージによって通信する仕方を定義 します。クラス同士の関係はソフトウェアをモデル化する新しい手段を提供しています。 あるクラスを継承したクラスは、親クラスの定義をすべて含んでいますが、データやメ ソッドを拡張することで、型による制約で許された範囲で新しい振る舞いを再定義する こともできます。この本ではクラス間の関係を絵と記号によって表現します。1

Objective Caml のオブジェクト指向の機能は型システムと統合されています。クラス宣 言は同じ名前の型を定義することになります。二種類の多相性を同時に使うことができ ます。一つはパラメータ型多相性ですが、これはすでにパラメータ化された型として紹 介しました。型パラメータはクラスに対しても利用できます。もう一つは 包含的多相性 と呼ばれているもので、オブジェクトと遅延束縛の間の部分型関係を利用しています。ク ラス sc の型がクラス c の部分型である時に、クラス sc のオブジェクトは クラス c の オブジェクトの代わりに使うことができます。部分型に関する制約は明示的に記述する

1. クラス間の関係の表現には UML (Unified Modeling Language) など様々な記法が提案されている。

440

Chapter 15 : オブジェクト指向プログラミング

必要があります。包含的多相性を使うと、要素の型が同一でない、ある特定の型の部分 型になっている非均一リストを作ることができます。遅延束縛のおかげでそのような非 均一リストのすべての要素に同じメッセージを送り、要素の実際のクラスに応じた別々 のメソッドを起動させることができます。 一方で Objective Caml にはメソッドのオーバーロードという概念はありません。オー バーロードとは同じ名前のメソッドを複数定義することができる機能です。オーバーロー ドがあると自動的に型推論するのが困難な場合があり、プログラマから情報を補っても らう必要があります。

Objective Caml はパラメータ型多相性と包含的多相性の両方を持ち、型推論を行う静的 な型システムを持つ唯一の言語であることを強調しておきましょう。

この章のあらまし この章では Objective Caml のオブジェクト指向のための拡張機能について記述します。 この機能はこれまでの章で既に解説した言語機能を制限することはありません。オブジェ クト指向のためにいくつかの新しいキーワードが追加されます。 最初の節ではクラス宣言の構文、オブジェクト生成、メッセージ送信について記述しま す。第二節ではクラスの間に作られる関係について説明します。第三節ではオブジェク ト型の概念を明らかにし、抽象クラス、多重継承、型パラメータを持つクラスの豊かな 表現力について解説します。第四節では部分型と包含的多相性の能力を示します。第五 節では関数型スタイルのオブジェクト指向プログラミングについて扱います。関数型ス タイルではオブジェクトの内部状態は更新されず、更新された状態を持った新しいオブ ジェクトが返されます。第六節ではインターフェースや、クラス変数を作れる局所宣言 などの残りの機能について解説します。

クラス、オブジェクト、メソッド Objective Caml のオブジェクト指向の拡張機能は、言語の関数型と手続き型の核と型シ ステムも含めて統合されています。この、型システムとの統合という点がこの言語の他 の言語にない特徴と言えるでしょう。すなわちこの言語は静的に型付けられた、型推論 を持つ、オブジェクト指向言語なのです。この拡張機能はクラスとオブジェクトの定義、 (多重継承を含む)クラス継承、パラメータ化されたクラス、抽象クラスを含んでいます。 クラスのインターフェースはクラス定義から自動的に生成されますが、モジュールと同 様にプログラマが署名を書けばより正確なインターフェースを与えることもできます。

オブジェクト指向についての用語 オブジェクト指向プログラミングに関する基本的な用語を以下の表にまとめます。 クラス: クラスには、そのクラスに属するオブジェクトの内容が記述されています。内 容とはインスタンス変数と呼ばれるデータとメソッドと呼ばれる操作の集まりか らなります。

クラス、オブジェクト、メソッド

441

オブジェクト: オブジェクトとはクラスの要素(インスタンス)です。オブジェクトは、 その属するクラスによって定義された振る舞いを持っています。オブジェクトとは プログラムの実際の構成要素であり、一方クラスはオブジェクトがどのように作ら れ、振る舞うか仕様を与えます。 メソッド: メソッドとはオブジェクトが行うことができる振る舞いのことです。 メッセージ送信 オブジェクトへの メッセージ送信 とはメソッドを実行すなわち 起動 させるよう要求することです。

クラス宣言 クラスを定義する最も簡単な構文は以下のようなものです。この章ではこの構文を拡張 させながら解説を行います。

class クラス名 p1 . . . pn = object .. . インスタンス変数 .. . メソッド .. .

構文 :

end p1 , . . . , pn はこのクラスの構築子へのパラメータです。パラメータがない場合は省略で きます。 インスタンス変数は以下のように宣言します。

val 変数名 = 式 構文 :

または

val mutable 変数名 = 式 インスタンス変数を mutable と宣言した時はその値を変更することができます。そうで ない場合はオブジェクトが生成される時に式を評価した値に固定されます。 メソッドは以下のように宣言します。 構文 :

method メソッド名 p1 . . . pn = 式

val と method 以外の構文もクラス宣言の中で許されていますが、それらは必要に応じ て紹介していきます。

クラスの例

例としてやはり point クラスを使うことにします。

442

Chapter 15 : オブジェクト指向プログラミング



インスタンス変数 x と y が点の座標を表します。



メソッド get x と get y によって座標を知ることができます。



移動のためのメソッド(moveto 絶対座標、rmoveto 相対座標)があります。



データを string として表示するメソッド(to string)があります。



原点からの距離を求めるメソッド(distance)があります。

# class point (x init,y init) = object val mutable x = x init val mutable y = y init method get x = x method get y = y method moveto (a,b) = x <- a ; y <- b method rmoveto (dx,dy) = x <- x + dx ; y <- y + dy method to string () = "( " ^ (string of int x) ^ ", " ^ (string of int y) ^")" method distance () = sqrt (float(x*x + y*y)) end ; ;

get x や get y などのメソッドではパラメータが必要ありません。インスタンス変数に アクセスするためのメソッドはパラメータを取らないのが普通です。 point クラスを宣言すると、システムは次のようなメッセージを表示します。 class point : int * int -> object val mutable x : int val mutable y : int method distance : unit -> float method get_x : int method get_y : int method moveto : int * int -> unit method rmoveto : int * int -> unit method to_string : unit -> string end

このメッセージには二種類の情報が含まれています。まずこのクラスのオブジェクトの型 を表しています。 (実際には「point 型」として表示されます。)オブジェクトの型はクラ スに含まれるインスタンス変数とメソッドの型のリストになります。この例では point 型とは次のような型を省略したものです。 < distance : unit → unit; get x : int; get y : int; moveto : int * int → unit; rmoveto : int * int → unit; to string : unit → unit >

次に point クラスの構築子の情報があります。構築子の型は int*int --> point と なります。この構築子は整数の組(点の初期座標を意味する)を受け取り、point オブ ジェクトを生成します。構築子を呼び出すためにはキーワード new を使います。 クラスの型を次のように定義することもできます。 # type simple point = < get x : int; get y : int; to string : unit → unit > ; ;

443

クラス、オブジェクト、メソッド type simple_point = < get_x : int; get_y : int; to_string : unit -> unit >

注意 point 型はクラス宣言の情報をすべて含んでいません。インスタンス変数 は型には示されていません。メソッドだけがインスタンス変数にアクセス することができます。 警告

クラス宣言は型宣言でもある。したがって、未束縛の型 変数を含んでいてはならない。

この点については後で型制約(458 ページ)と型パラメータを持つクラス(464 ページ) を扱う時にまた説明します。

クラスの図表現 Objective Caml の型を表現するために UML 記法を採用します。クラスは三つの部分か らなる長方形によって示されます。 •

上部はクラスの名前を示す。



中段部は属性(インスタンス変数)のリストを表す。



下部はメソッドのリストを表す。

図 15.1 に caml クラスの図表現の例を示します。

caml color age eyes drinks runs sleeps

図 15.1: クラスの図表現 インスタンス変数やメソッドの型情報が図中に加えられることもあります。

444

Chapter 15 : オブジェクト指向プログラミング

インスタンス生成 オブジェクトとはクラスの値で、インスタンスと呼ばれます。インスタンスは総称的な 演算子である new によって生成されます。new 演算子は、クラス名と初期化のための値 を引数として取ります。 構文 :

new クラス名 式 1 . . . 式 n

以下に point クラスのインスタンスを、別々の初期値から生成する例を示します。 # let p1 = new point (0,0); ; val p1 : point = # let p2 = new point (3,4); ; val p2 : point = # let coord = (3,0); ; val coord : int * int = (3, 0) # let p3 = new point coord; ; val p3 : point =

Objective Caml ではクラスの構築子は一つしか定義できませんが、ユーザ独自の生成関 数を定義することができます。

# let make point x = new point (x,x) ; ; val make_point : int -> point = # make point 1 ; ; - : point =

メッセージ送信 オブジェクトにメッセージを送るには # 演算子を使います。2 構文 :

obj1 #メソッド名 p1 . . . pn

p1 , . . . , pn という引数を持ったメッセージがオブジェクトの指定されたメソッドに送ら れます。オブジェクトの属するクラスでは指定されたメソッドが定義されていなくては なりません。すなわち、そのメソッドはオブジェクトの型に現われている必要がありま す。引数の型もメソッドの仮引数の型に適合する必要があります。以下に point クラス のオブジェクトに対するメッセージ送信の例を示します。 # p1#get x; ; - : int = 0 # p2#get y; ; - : int = 4 # p1#to string () ; ; - : string = "( 0, 0)" # p2#to string () ; ; - : string = "( 3, 4)"

2. ほとんどのオブジェクト指向言語ではドット記法が使われている。しかしドット記法は既にレコードとモ ジュールで使われているため別の記号を使う必要があった。

クラスの間の関係

445

# if (p1#distance () ) = (p2#distance () ) then print string ("That’s just chance\n") else print string ("We could bet on it\n"); ; We could bet on it - : unit = ()

型の観点からいえば point 型のオブジェクトも、他の値とまったく同じように Objective Caml の多相型関数で取り扱うことができます。 # p1 = p1 ; ; - : bool = true # p1 = p2; ; - : bool = false # let l = p1::[]; ; val l : point list = [] # List.hd l; ; - : point =

オブジェクトの等価性は物理的等価性として定義されて います。

警告

この点については部分型関係について解説する時(474 ページ)に詳しく説明します。

クラスの間の関係 クラス同士には二種類の関係を定義することができます。

1.

Has-a と呼ばれる集約関係 C2 クラスが C1 クラスと Has-a 関係を持っているとは、C2 クラスが、型が C1 クラスであるインスタンス変数を持っていることです。この関係は、少なくとも一 つのインスタンス変数を持っている時に成り立ちます。

2.

Is-a と呼ばれる継承関係 C2 クラスが C1 クラスのサブクラスであるとは、C2 クラスが C1 クラスの振る舞 いを拡張したものである、ということを意味しています。オブジェクト指向プログ ラミングの大きな利点の一つは、ある既存のクラスのコードを再利用しながらそ の振る舞いを再定義できる能力にあります。クラスを拡張すると、新しいクラスは 元のクラスのすべてのインスタンス変数とメソッドを継承します。

集約 C1 クラスが C2 クラスを集約するとは、C1 クラスの少なくとも一つのインスタンス変 数の型が C2 であることを言います。同じ型のインスタンス変数を持っている数が分か る場合にはその数を付け加えることもあります。

446

Chapter 15 : オブジェクト指向プログラミング

集約の例 点の集合としての画像を定義してみましょう。そのために picture クラス(図 15.2 参 照)を宣言します。picture クラスは point クラスの配列を持っています。すなわち picture クラスは point クラスを一般化された Has-a 関係を利用して集約しています。 # class picture n = object val mutable ind = 0 val tab = Array.create n (new point(0,0)) method add p = try tab.(ind)<-p ; ind <- ind + 1 with Invalid argument("Array.set") → failwith ("picture.add:ind =" ^ (string of int ind)) method remove () = if (ind > 0) then ind <-ind-1 method to string () = let s = ref "[" in for i=0 to ind-1 do s:= !s ^ " " ^ tab.(i)#to string () done ; (!s) ^ "]" end ; ; class picture : int -> object val mutable ind : int val tab : point array method add : point -> unit method remove : unit -> unit method to_string : unit -> string end

画像を作り上げるには picture クラスのインスタンスを生成し、必要に応じて点を挿入 します。 # let pic = new picture 8; ; val pic : picture = # pic#add p1; pic#add p2; pic#add p3; ; - : unit = () # pic#to string () ; ; - : string = "[ ( 0, 0) ( 3, 4) ( 3, 0)]"

集約の図表現 picture クラスと point クラスの間の関係はグラフを利用して図 15.2 のように表すこと ができます。根本に菱型が付いた矢印は集約関係を意味しています。この例では picture クラスは 0 個以上の点を持っていますので図中の矢印にその数を表記します。

447

クラスの間の関係

point x : int y : int

picture 0..*

get_x : int get_y : int to_string : unit -> string moveto : (int * int) -> unit rmoveto : (int * int) -> unit distance : unit -> float

ind : int tab : point array add_point : point -> unit remove : unit -> unit to_string : unit -> string

図 15.2: 集約関係

継承関係 これはオブジェクト指向プログラミングで基本となる関係です。c2 クラスが c1 クラス を継承すると、c2 クラスは親クラスのすべてのインスタンス変数とメソッドを継承しま す。c2 クラスは新しいインスタンス変数やメソッドを定義することもできますし、継承 したものを自分の都合のために再定義することもできます。親クラスはまったく無変更 のままですから、親クラスを利用するアプリケーションを新しく定義したクラスに合わ せて変更する必要はありません。 継承についての構文は次のようになります。 構文 :

inherit 名前 1 p1 . . . pn [ as 名前 2 ]

p1 から pn までの引数は 名前 1 クラスの構築子に与える必要があるものです。親クラス のメソッドにアクセスしたい時には as キーワードを使って親クラスへの参照を受け取 る変数を指定します。この機能は親クラスのメソッドを再定義したときにとりわけ役に 立ちます。詳しくは 449 ページを参照してください。

単一継承の例 古典的な例にならって point クラスに色を表す新しい属性を付加した新しいクラスを 継承によって定義してみましょう。色は string 型を持つ c というインスタンス変数に よって表現します。点の色情報を問い合わせるメソッド get color を追加します。最後 に文字列に変換する関数を新しい属性を認識できるように再定義します。 注意 メソッド to string の中の変数 x と y はインスタンス変数であって初期 化引数ではありません。 # class colored point (x,y) c = object

448

Chapter 15 : オブジェクト指向プログラミング

inherit point (x,y) val mutable c = c method get color = c method set color nc = c <- nc method to string () = "( " ^ (string of int x) ^ ", " ^ (string of int y) ^ ")" ^ " [" ^ c ^ "] " end ; ; class colored_point : int * int -> string -> object val mutable c : string val mutable x : int val mutable y : int method distance : unit -> float method get_color : string method get_x : int method get_y : int method moveto : int * int -> unit method rmoveto : int * int -> unit method set_color : string -> unit method to_string : unit -> string end

colored point クラスの構築子の引数は、point クラスの構築子のために必要な座標と 新しいクラスのために必要な色属性になります。 継承されたメソッド、新しく定義されたメソッドおよび再定義されたメソッドはこのク ラスのインスタンスの振る舞いに期待通りよく適合しています。 # let pc = new colored point (2,3) "white"; ; val pc : colored_point = # pc#get color; ; - : string = "white" # pc#get x; ; - : int = 2 # pc#to string () ; ; - : string = "( 2, 3) [white] " # pc#distance; ; - : unit -> float =

このとき point クラスは colored point クラスの親クラスであると言い、逆に colored point クラスは point クラスの 子クラス であると言います。 警告

メソッドを子クラスで再定義するときは、親クラスでの メソッドの型を尊重しなければならない。

継承の図表現 クラスの間の継承関係は子クラスから親クラスへの矢印として表示されます。矢印の先端 は閉じた三角形になっています。継承の図表現では子クラスで新しく追加されたインスタ

449

その他のオブジェクト指向の機能

ンス変数とメソッド、再定義されたメソッドだけを表示します。図 15.3 は colored point クラスとその親である point の関係を表しています。

point

colored_point

x : int y : int

c : string

get_x : int get_y : int to_string : unit -> string moveto : (int * int) -> unit rmoveto : (int * int) -> unit distance : unit -> float

get_color : string set_color : string -> unit to_string : unit -> string

図 15.3: 継承関係 新しいメソッドを追加したため colored point 型は point 型とは異なっています。こ れらのクラスのインスタンスの間での等価性判定はそれぞれのクラスの型の違いを表示 するために長いエラーメッセージを表示します。 # p1 = pc; ; Characters 6-8: p1 = pc;; ^^ This expression has type colored_point = < distance : unit -> float; get_color : string; get_x : int; get_y : int; moveto : int * int -> unit; rmoveto : int * int -> unit; set_color : string -> unit; to_string : unit -> string > but is here used with type point = < distance : unit -> float; get_x : int; get_y : int; moveto : int * int -> unit; rmoveto : int * int -> unit; to_string : unit -> string > Only the first object type has a method get_color

その他のオブジェクト指向の機能 特別な参照 self と super クラスにメソッドを定義する時に親クラスのメソッドを起動することができると便利な 場合があります。このために Objective Caml では親クラス(のオブジェクト)への参 照に名前が付けることができます。同様にオブジェクト自身への参照にも名前を付けて 参照することができます。その場合、オブジェクト自身の参照名は object というキー

450

Chapter 15 : オブジェクト指向プログラミング

ワードの後に指定します。親クラスへの参照に名前を付けるのは継承関係を宣言する時 に行います。 例えば colored point クラスの to string メソッドを定義するには親クラスの to string メソッドの振る舞いを利用した方が便利でしょう。 # class colored point (x,y) c = object (self) inherit point (x,y) as super val c = c method get color = c method to string () = super#to string () ^ " [" ^ self#get color ^ "] " end ; ; 親クラスと子クラスのオブジェクトへの参照には好きな名前を付けられますが、慣習とし て現在のオブジェクトへの参照には self または this が、親クラスへの参照には super という名前が良く使われています。多重継承を行った時は複数の親クラスを区別するた めに別々の名前を付けた方が分かりやすいかもしれません。(461 ページ参照。) 警告

もし親クラスへの参照と同じ名前の変数を宣言した場合 には新しい変数が親クラスへの参照を隠蔽するため親ク ラスへ参照することができなくなります。

遅延束縛 遅延束縛 の働きによって実際にメッセージが送られるオブジェクトは実行時に決定され ます。これはコンパイル時に決定される静的束縛と対立する概念です。Objective Caml ではメソッドの選択に遅延束縛が採用されています。したがって実際に実行されること になるコードはメッセージを受け取るオブジェクトによって実行時に決定されます。 これまでに見て来た colored point クラスでは to string メソッドを再定義していま す。この新しいメソッドの定義では get color メソッドを呼び出しています。さてここ で colored point を継承した別の新しいクラス colored point 1 を定義することにし ましょう。この新しいクラスでは get color メソッドを再定義します。(色を意味する 文字列が正しいかどうかチェックします。)ただし to string メソッドは再定義しない ことにします。 # class colored point 1 coord c = object inherit colored point coord c val true colors = ["white"; "black"; "red"; "green"; "blue"; "yellow"] method get color = if List.mem c true colors then c else "UNKNOWN" end ; ;

色付き点を表すどちらのクラスでも to string メソッドは同じですが、それぞれのクラ スから生成されたオブジェクトは異なった振る舞いを示しています。 # let p1 = new colored point (1,1) "blue as an orange" ; ; val p1 : colored_point =

その他のオブジェクト指向の機能

451

# p1#to string () ; ; - : string = "( 1, 1) [blue as an orange] " # let p2 = new colored point 1 (1,1) "blue as an orange" ; ; val p2 : colored_point_1 = # p2#to string () ; ; - : string = "( 1, 1) [UNKNOWN] "

to string メソッドの中の get color の束縛は colored point クラスがコンパイルさ れた時点では固定されていません。get color メソッドが呼び出されたときに実際に実 行されるコードは colored point クラスと colored point 1 クラスのインスタンスに 関連付けられているメソッドから決定されます。たとえば colored point クラスのイン スタンスに to string メッセージを送ると、colored point クラスで定義されている get color が実行されます。一方、同じメッセージを colored point 1 のインスタン スに送ると親クラスで定義されている to string メソッドが実行されますが、その後、 子クラスに定義されている get color メソッドが実行され、色を表現している文字列が 適切かどうか判定されることになります。

オブジェクトの内部表現とメッセージの発送 オブジェクトは可変の部分と不変の部分の二つから構成されています。可変部にはレコー ドとまったく同様にインスタンス変数が含まれています。不変部はメソッド表に対応し、 あるクラスのすべてのインスタンスで共有されています。 メソッド表とは関数がところどころに入っているまばらな配列です。一つのアプリケーショ ンの中のすべてのメソッドには重ならない番号が割り振られていて、その番号がメソッド 表の索引として使われています。Objective Caml では次のような機械語の存在を仮定して います。それはオブジェクト o と索引 n を受け取り、そのオブジェクトのメソッド表の指 定された索引に登録されている関数を返す GETMETHOD(o,n) 命令です。GETMETHOD(o,n) 命令を呼び出した結果を f n と書くことにします。o#m というメッセージ送信をコンパ イルするには、まず m メソッドに該当する索引 n を求め、GETMETHOD(o,n) をオブジェ クト o に適用するコードを生成します。これはすなわち f n 関数をオブジェクト o に適 用することに対応しています。遅延束縛は実行時の GETMETHOD の計算の中に実装されて います。 メソッドの中でメッセージを self に送るのも同様にメッセージに対応する索引を検索 し、メソッド表の中で見つかった関数を呼び出すコードへとコンパイルされます。 継承の場合でも、同じメソッド名は常に同じ索引に対応しているのでメソッドの再定義 を気にせずに、再定義が反映されている新しいメソッド表を使って全く同じコードが生 成されます。したがって point クラスのインスタンスへ to string メッセージを送る 時は座標を文字列に変換する処理が行われますが、一方 colored point クラスのイン スタンスへ同じメッセージを送る場合は同じ索引を使って色属性を認識するように再定 義されたメソッドに対応する関数が選択されるでしょう。 索引の不変性のおかげで部分型(470 ページ参照)も実行に関して矛盾が生じないことが 保証されています。もちろん colored point クラスのインスタンスが明示的に point 型 と指定されている場合、to string メッセージを送ると point クラスのメソッド表の索引 が使われます。しかし、その索引は colored point クラスの索引と同一になるよう定義

452

Chapter 15 : オブジェクト指向プログラミング

されているので、実際に呼び出されるメソッドは、メッセージを受け取ったインスタンス のメソッド表の指定された索引に結びつけられているメソッド、すなわち colored point クラスの to string メソッドが起動されます。 もちろん Objective Caml の実際の実装は違いますが、呼び出されるメソッドの動的検 索の原則はこれまでに説明したものと変わりません。

初期化 オブジェクト生成の間に実行されるコードを指定するために initializer キーワード をクラス定義の中で利用することができます。初期化コードの中ではメソッドの中で許 されている計算ならばどんな計算でも行うことができます。 構文 :

initializer 式

point クラスをまた拡張してみましょう。今度はインスタンスが生成されたことを報告 する verbose point を定義してみましょう。 # class verbose point p = object(self) inherit point p initializer let xm = string of int x and ym = string of int y in Printf.printf ">> Creation of a point at (%s %s)\n" xm ym ; Printf.printf " , at distance %f from the origin\n" (self#distance () ) ; end ; ; # new verbose point (1,1); ; >> Creation of a point at (1 1) , at distance 1.414214 from the origin - : verbose_point =

初期化コードのおもしろく、ためになる利用例は継承されたクラスのインスタンス生成 時の動作を追跡することでしょう。例えばこのようになります。 # class c1 = object initializer print string "Creating an instance of c1\n" end ; ; # class c2 = object inherit c1 initializer print string "Creating an instance of c2\n" end ; ; # new c1 ; ; Creating an instance of c1 - : c1 =

その他のオブジェクト指向の機能

453

# new c2 ; ; Creating an instance of c1 Creating an instance of c2 - : c2 =

クラス c2 のインスタンスを生成する前にまず親クラスの生成を行っていることが分か るでしょう。

プライベートメソッド メソッドに private キーワードを付けて宣言するとプライベートメソッド になります。 プライベートメソッドはそのクラスのインターフェースには存在しますが、そのクラス のインスタンスには存在しません。プライベートメソッドは他のメソッドからのみ呼び 出すことができます。そのクラスのインスタンスに対してプライベートメソッドを呼び 出すことはできません。しかしプライベートメソッドを継承することは可能です。した がってクラス階層の定義の中で使うことはできます3 。 構文 :

method private 名前 = 式

point クラスを拡張してみましょう。最後の移動を取り消す undo メソッドを追加しま す。そのためには動く前の座標を覚えておく必要があります。このため新しいインスタン ス変数 old x と old y とこららの変数を更新するメソッドを新しく定義します。ユーザ にはこのメソッドを直接呼び出してほしくないのでプライベート宣言をします。moveto メソッドと rmoveto メソッドを再定義し、移動のための古いメソッドを呼び出す前に現 在の座標を覚えておくようにします。

# class point m1 (x0,y0) = object(self) inherit point (x0,y0) as super val mutable old x = x0 val mutable old y = y0 method private mem pos () = old x <- x ; old y <- y method undo () = x <- old x; y <- old y method moveto (x1, y1) = self#mem pos () ; super#moveto (x1, y1) method rmoveto (dx, dy) = self#mem pos () ; super#rmoveto (dx, dy) end ; ; class point_m1 : int * int -> object val mutable old_x : int val mutable old_y : int val mutable x : int val mutable y : int method distance : unit -> float method get_x : int method get_y : int method private mem_pos : unit -> unit method moveto : int * int -> unit method rmoveto : int * int -> unit

3. Objective Caml の private は Objective C、C++、Java の protected に対応しています。

454

Chapter 15 : オブジェクト指向プログラミング

method to_string : unit -> string method undo : unit -> unit end

point m1 型の表示の中で mem pos メソッドの前に private キーワードが付けられてい ます。このメソッドは別のメソッドからは呼び出すことができますが、たとえ同じクラ スであっても他のインスタンスからは呼び出すことはできません。この条件はインスタ ンス変数の場合と全く同様です。インスタンス変数 old x と old y はコンパイルされた 結果の中には表示されていますが、他のインスタンスから直接アクセスすることはでき ません(441 ページ参照)。 # let p = new point m1 (0, 0) ; ; val p : point_m1 = # p#mem pos () ; ; Characters 0-1: p#mem_pos() ;; ^ This expression has type point_m1 It has no method mem_pos # p#moveto(1, 1) ; p#to string () ; ; - : string = "( 1, 1)" # p#undo () ; p#to string () ; ; - : string = "( 0, 0)"

警告

型による制約のためにプライベート宣言されたメソッド がパブリックになることがある。

型と総称性 集約関係と継承関係を利用して問題をモデル化する能力に加えて、オブジェクト指向プ ログラミングには既存のクラスの振る舞いを再利用、変更できる興味深い能力がありま す。しかし Objective Caml ではクラスを拡張する時には静的な型付けに関する制約を 守る必要があります。 抽象クラスを使うとコードを機能分解し、複数のサブクラスが一つの「コミュニケーショ ンプロトコル」を守るように制約を与えることができます。抽象クラスは子クラスのイ ンスタンスが受け取る可能性のあるメッセージの名前と型を固定します。この技法は多 重継承と関連させて理解するとより実感できると思います。 開いたオブジェクト型(あるいは単に開いた型)の概念は総称的なメソッドを使ったプ ログラムを正しく機能させることができます。しかし、型に関する制約を明示的に正確 に指定する必要が生じるかもしれません。これはとりわけ型パラメータを持つクラスで は必要になるでしょう。型パラメータを持つクラスは、クラスに対してパラメータ型多 相性を提供するものです。Objective Caml ではオブジェクト指向拡張部分でもこのよう な機能を持っているおかげで真に総称的な言語と言えるのです。

型と総称性

455

抽象クラスと抽象メソッド 抽象クラスの中ではメソッドを本体なしに宣言することができます。そのようなメソッ ドを抽象メソッドと呼びます。抽象クラスのインスタンスを生成することは違法です。 つまり new することができません。抽象クラス、抽象メソッドであると指定するには virtual キーワードを使います。 構文 :

class virtual 名前 = object . . . end

抽象メソッドを含むクラスは必ず抽象メソッドと宣言しなければなりません。抽象メソッ ドは型のみを指定して宣言します。 構文 :

method virtual 名前 : 型

抽象クラスのサブクラスが親の抽象メソッドをすべて再定義している時そのクラスは通 常の具体的なクラスとなります。そうでない時はその子クラスも抽象クラスとして宣言 しなければなりません。 例として表示可能なオブジェクトの集合を作ってみましょう。表示可能なオブジェクト とはそのオブジェクトの内容を文字列に変換して表示する print メソッドを持っている オブジェクトです。そのようなオブジェクトには to string メソッドが必要です。まず printable クラスを定義します。オブジェクトの内容を表す文字列はそのオブジェクト の性質に依存するので printable クラスの to string メソッドは抽象メソッドになり ます。結果として printable クラスも抽象クラスになります。 # class virtual printable () = object(self) method virtual to string : unit → string method print () = print string (self#to string () ) end ; ; class virtual printable : unit -> object method print : unit -> unit method virtual to_string : unit -> string end

このクラスと to string メソッドが抽象的なのは型表示にもはっきり示されています。 このクラスから図 15.4 のクラス階層を定義してみましょう。

point クラス、colored point クラス、picture クラスを再定義するのは簡単です。こ れらのクラスの宣言に inherit printable () という行を付け加えれば完了です。継 承を通して print メソッドが各クラスに提供されます。 # let p = new point (1,1) in p#print () ; ; ( 1, 1)- : unit = () # let pc = new colored point (2,2) "blue" in pc#print () ; ; ( 2, 2) with color blue- : unit = () # let t = new picture 3 in t#add (new point (1,1)) ; t#add (new point (3,2)) ; t#add (new point (1,4)) ;

456

Chapter 15 : オブジェクト指向プログラミング printable

2

rectangle

0..n

point

picture

colored_point

図 15.4: 表示可能オブジェクトを構成するクラスの間の関係 t#print () ; ; [ ( 1, 1) ( 3, 2) ( 1, 4)]- : unit = ()

rectangle クラスは printable クラスを継承して to string メソッドを定義していま す。インスタンス変数 llc (同様に urc)は長方形の左下部(同様に右上部)の点の座 標を意味しています。

# class rectangle (p1,p2) = object inherit printable () val llc = (p1 : point) val urc = (p2 : point) method to string () = "[" ^ llc#to string () ^ "," ^ urc#to string () ^ "]" end ; ; class rectangle : point * point -> object val llc : point val urc : point method print : unit -> unit method to_string : unit -> string end

rectangle クラスは抽象クラス printable を継承し、print メソッドを引き継いでいま す。このクラスは point 型のインスタンス変数を二つ(左下部の点と右上部の点)持っ ています。to string メソッドは point 型のインスタンス変数 llc と urc に対して to string メッセージを送っています。

型と総称性

457

# let r = new rectangle (new point (2,3), new point (4,5)); ; val r : rectangle = # r#print () ; ; [( 2, 3),( 4, 5)]- : unit = ()

クラス、型、オブジェクト オブジェクトの型はメソッドの型から決定されると以前言いました。たとえば point 型 は point クラスの宣言から推論されますが、実際は以下のような型を省略したものとし て定義されます。 point = < distance : unit -> float; get_x : int; get_y : int; moveto : int * int -> unit; rmoveto : int * int -> unit; to_string : unit -> string >

これは閉じた型です。つまりすべてのメソッドに関連付けられている型は固定されてい ます。この型に、新しいメソッドや型を追加することはできません。型宣言時に型推論 の働きによってクラスに関連付けられている閉じた型が計算されます。

開いた型 オブジェクトにメッセージを送る機能は言語の一部ですから、まだ型が定まっていない オブジェクトに対してもメッセージを送る関数を定義することができます。 # let f x = x#get x ; ; val f : < get_x : ’a; .. > -> ’a =

f の引数に対して推論された型は、x に対してメッセージが送られているためオブジェク ト型ですが、このオブジェクト型は開いた型です。関数 f の引数 x は少なくとも get x というメソッドを持っていなければなりません。このメッセージを送った結果を関数 f の中では使っていないので、結果の型はできるだけ一般的なもの(型変数 ’a)でなけれ ばなりません。このため型推論によって get x メソッドを持つどんなオブジェクトに対 しても関数 f を利用できるようになっています。型 < get x : ’a; .. > の後ろにある 連続点「..」は x の型が開いていることを意味しています。 # f (new point(2,3)) ; ; - : int = 2 # f (new colored point(2,3) "emerald") ; ; - : int = 2 # class c () = object method get x = "I have a method get_x" end ; ; class c : unit -> object method get_x : string end # f (new c () ) ; ;

458

Chapter 15 : オブジェクト指向プログラミング

- : string = "I have a method get_x"

クラスの型を推論すると開いた型を出力することがあります。とりわけクラスのインス タンス生成時の初期値が開いた型になります。次の例では couple クラスを定義します。 このクラスの初期値 a と b は to string メソッドを持っています。 # class couple (a,b) = object val p0 = a val p1 = b method to string () = p0#to string () ^ p1#to string () method copy () = new couple (p0,p1) end ; ; class couple : (< to_string : unit -> string; .. > as ’a) * (< to_string : unit -> string; .. > as ’b) -> object val p0 : ’a val p1 : ’b method copy : unit -> couple method to_string : unit -> string end

a と b の両方とも型は to string メソッドを持つ開いた型です。ここで注意が必要な のはこの二つの型は別の型と認識されていることです。これらの型は “as ’a” と “as ’b” とそれぞれ名付けられています。型変数 ’a と ’b は推論された型によって制約され ています。 閉じた型から開いた型を生成するためにシャープ記号を使います。 構文 :

#オブジェクト型

この型全体でオブジェクト型のすべてのメソッドを持ち、最後に連続点が付いている開 いた型を意味しています。

型制約 関数型プログラミングの章(28 ページ)で、ある式が型推論によって生成された型より も正確な型を持つように制約を与えるにはどうすればいいか学びました。オブジェクト 型は、開いた型であれ閉じた型であれこのような制約を与えることができます。すでに 定義されているオブジェクト型を、その後別のメソッドに適用するためにあらかじめ開 いておきたい場合があるかもしれません。そのような時は開いた型による制約を利用す ることができます。 構文 :

(name:#type)

この構文を使って次のように書くことが出来ます。 # let g (x : #point) = x#message; ; val g : < distance : unit -> float; get_x : int; get_y : int; message : ’a; moveto : int * int -> unit; print : unit -> unit; rmoveto : int * int -> unit; to_string : unit -> string; .. > ->

型と総称性

459

’a =

#point という型制約によって x は少なくとも point クラスのメソッドをすべて持つこ とになります。それに加えて「message」メッセージを送っているために引数 x の型に は新しいメソッドが追加されています。 オブジェクト指向機能以外の部分と全く同様に Objective Caml ではオブジェクトに対 しても推論によって静的な型付けを与えます。この機構が式の型を決定するために充分 な情報を得られない時は型変数が割り当てられます。この方式がオブジェクトの型付け にも有効であることを今まで見て来ましたが、それでも時折曖昧な状況が生じます。そ の場合はプログラマが明確な型情報を与える必要があります。 # class a point p0 = object val p = p0 method to string () = p#to string () end ; ; Characters 6-89: ..... a_point p0 = object val p = p0 method to_string() = p#to_string() end... Some type variables are unbound in this type: class a_point : (< to_string : unit -> ’b; .. > as ’a) -> object val p : ’a method to_string : unit -> ’b end The method to_string has type unit -> ’a where ’a is unbound

このような曖昧性は引数 p0 が #point 型を持つと指定することで解消できます。 # class a point (p0 : #point) = object val p =