Title: The Elementary Lecture on ML, version alpha37
Last modified: Saturday, 2 December 2000
URL: http://www.gin.or.jp/users/daikoku/ml/mlkouza.txt
Author: Daikoku Manabu
E-mail: daikoku@gin.or.jp

初級ML講座version alpha37

Copyright (C) 1997-2000 Daikoku Manabu


===目次

序文

P.1 この文章について

P.2 ライセンス規定

P.3 謝辞

第0章 プログラミングを始める前の予備知識

0.1 プログラム

0.2 プログラミング言語

0.3 処理系

0.4 MLの対話型システム

練習問題

第1章 データの表現

1.1 ビット列

1.2 型

1.3 基本型のデータをあらわす定数

1.4 組をあらわす定数

練習問題

第2章 データの名前

2.1 識別子

2.2 束縛

2.3 式

2.4 パターン

練習問題

第3章 関数適用

3.1 関数についての基礎知識

3.2 組み込み関数

3.3 演算子

3.4 優先順位と結合規則

練習問題

第4章 関数の定義

4.1 fun宣言

4.2 スコープ

4.3 プログラムのファイル

4.4 演算子の定義

練習問題

第5章 選択

5.1 選択をめぐる基本概念

5.2 組み込み述語

5.3 andalsoとorelse

5.4 if式

5.5 パターンによる選択

練習問題

第6章 再帰

6.1 再帰をめぐる一般的考察

6.2 再帰的な関数

6.3 相互再帰

練習問題

第7章 例外

7.1 例外を発生させる方法

7.2 例外の捕獲

7.3 例外引数

練習問題

第8章 多相型と等値型

8.1 多相型

8.2 等値型

練習問題

第9章 リスト

9.1 リストの構造

9.2 リストと型

9.3 リストの処理

9.4 リストを扱う組み込み関数

練習問題

第10章 高階関数

10.1 高階関数への序曲

10.2 高階関数の作り方

10.3 カリー化

10.4 高階組み込み関数

練習問題

第11章 型の定義

11.1 datatype宣言

11.2 既存の型を利用した型の定義

11.3 引数を受け取る型構成子

11.4 再帰的な型の定義

練習問題

第12章 モジュール

12.1 ストラクチャー

12.2 限定識別子

12.3 open宣言

12.4 ライブラリー

練習問題

第13章 情報隠蔽

13.1 シグネチャー

13.2 シグネチャーによる情報隠蔽

13.3 抽象データ型

練習問題

第14章 モジュールを作り出すモジュール

14.1 プログラムの再利用

14.2 ファンクターの定義

14.3 ファンクター適用

14.4 未定義の型を持つファンクター

練習問題

第15章 手続き型計算モデル

15.1 計算モデル

15.2 参照

15.3 繰り返し

練習問題

第16章 テキストファイル

16.1 ストリーム

16.2 読み込み

16.3 出力

16.4 標準入出力

練習問題

第17章 ベクトルと配列

17.1 ベクトル

17.2 配列

練習問題

第18章 レコード

18.1 レコードの表現

18.2 レコードのパターン

練習問題

第19章 ビット列

19.1 ビット列の操作

19.2 バイナリーファイル

練習問題

第20章 オペレーティングシステム

20.1 ファイルシステム

20.2 コマンドと環境変数

20.3 日付と時刻

練習問題

付録A 練習問題の解答例

付録B Standard ML基本ライブラリーの主要な関数

付録C プログラミング用語英和辞典

参考文献

===序文

P.1---この文章について

Q P.1.1___この文章には、いったい何が書かれているのですか。

この文章の内容は、MLというプログラミング言語を使用した、 プログラミングというものについての説明です。

Q P.1.2___この文章は、どのような目的で書かれたのですか。

この文章の目的は、プログラミングというものがいかに 面白いものなのかということを読者に伝えることです。 ただし、残念ながら、筆者の能力が決定的に不足しておりまして、 そのような遠大な目的が達成できるとはとても思えませんので、 目的というよりは努力目標だと考えていただけるとありがたいです。

Q P.1.3___MLというプログラミング言語についての説明は、 この文章の目的ではないのですか。

はい、目的ではありません。 この文章では、プログラミングについて説明するためにMLという プログラミング言語を使用しています。したがって、この文章の 中には、プログラミングについての説明だけではなくて MLについての説明も含まれています。しかし、MLについての 説明は、この文章の目的ではなくて、プログラミングについて 説明するための単なる手段です。

Q P.1.4___この文章の著者は、プログラミングについて 説明するためのプログラミング言語としてなぜMLを選んだのですか。

無数にあるプログラミング言語の中から筆者がMLを選んだ理由の 半分は、それがプログラミングの面白さを伝えるのに適した言語の ひとつだから、ということです。そして残りの半分は、それが 筆者の好きな言語のひとつだからです。

Q P.1.5___この文章はFAQなんですか。

いいえ、違います。 この文章はFAQの形式に似せて書かれていますが、FAQでは ありません。FAQというのは、frequently asked questions and answersという意味ですが、この文章に登場する質問は、 プログラムを書く方法についての解説に読者を誘導するために 恣意的に作られたものですので、frequentlyにaskされる というわけではありません。

Q P.1.6___この文章を理解するためには、どのような予備知識が 必要ですか。

おそらく、専門的な知識は必要ではないと思います。 筆者は、プログラミングの経験がまったくない人でも 理解できるようにする、ということを心掛けてこの文章を 書いております。もしも、この文章の中で理解しにくいところが あるようでしたら、メールをお送りいただければ改善したい と思っております。 なお、この文章の中で解説されていることを、自分のコンピュータを 使って確かめてみたいという場合は、シェルのコマンドや エディターの使い方などについての予備知識が必要です。

Q P.1.7___この文章の中に間違った記述があるのを 発見したのですが、どうすればいいですか。

そのような場合は、ぜひともメールで お知らせくださいますようにお願いします。

P.2---ライセンス規定

Q P.2.1___この文章の著作権は誰が保有しているのですか。

この文章の著者(大黒学)が保有しています。

Q P.2.2___この文章を複製したいのですが、著作権者の許諾が 必要ですか。

いいえ、必要ではありません。この文章は、内容を改変しない限り、 自由に複製することができます。 なお、文章の整形のみにかかわる改変は、内容の改変には 相当しないものとします。ですから、この文章を、HTML、dvi、 PostScript、PDFなどの形式に変換したもの、またはそれを紙などに 印刷したものを複製することも自由です。 どうしてもこの文章の内容を改変したものを複製したい という方は、問い合わせのメールをこの文章の著者に お送りください。事情によっては許可したいと思っております。 なお、再配布に関しても、内容を改変しない限り、著作権者の許諾は 必要ではありません。

Q P.2.3___この文章の一部分を自分の著作物の中に 引用したいのですが、著作権者の許諾が必要ですか。

いいえ、必要ではありません。ただし、この文章の一部分を 自分の著作物の中に引用する場合は、この文章の題名と著者の 氏名を、その場所に明記しておいてください。

P.3---謝辞

Q P.3.1___この文章の作成には、この文章の著者以外に どのような人々が貢献しているのですか。

この文章の作成には、筆者以外に次のような方々が 貢献しています。 N.S.さん、T.Iさん、Yoshinori Itouさん、安達淳さん、 石井徹也さん、亀渕景司さん、くろだひさおさん、小山隆さん、 金野祥久さん、笹川賢一さん、田仲稔さん、辻洋一郎さん、 西原聡士さん、古瀬淳さん、山崎進さん。 以上の方々は、この文章のアルファ版を読んで、意見や感想や質問や 激励などのメールを送ってくださったり、ネットニュースに記事を 投稿してくださいました。みなさん、本当に ありがとうございました。そしてこれからもよろしくお願いします。

第0章===プログラミングを始める前の予備知識

0.1---プログラム

Q 0.1.1___コンピュータに何かをさせたいとき、その「何か」を コンピュータに伝えるためにはどうすればいいのですか。

そんなときは、プログラムというものをコンピュータに与えます。 プログラムというのは、コンピュータがどのように動作すれば いいのかということをあらわしている文字の列のことです。 コンピュータという機械は、与えられたプログラムにしたがって 動作するように作られています。ですから、コンピュータに何かを させたいときは、その「何か」をあらわすプログラムを自分で 書くか、または誰かが書いたものを入手して、コンピュータに 与えればいいわけです。 なお、「コンピュータがプログラムにしたがって動作する」 ということを、「プログラムがコンピュータの上で動作する」 というように、あたかもプログラムが動作の主体であるかのような 表現をする場合がけっこう多い、ということも 覚えておいてください。

Q 0.1.2___「コンピュータのソフトウェア」と「プログラム」とは 何が違うのですか。

「コンピュータのソフトウェア」(ここから先では、 ただ単に「ソフトウェア」と呼ぶことにします」)というのは、 コンピュータを使って何かをするために必要となるデータ、 という意味です。コンピュータを使って何かをするためには プログラムというデータが必要ですから、プログラムが ソフトウェアの一種だということに疑問の余地はありません。 でも、「ソフトウェア」と「プログラム」とは、けっして同じ 意味ではありません。 たとえば、ヘルプや画像や音声のデータなどは、コンピュータの 動作をあらわしているわけではありませんのでプログラムとは 言えませんが、コンピュータを使って何かをするときにそれらの データが必要だという場合は、それらもソフトウェアの一種だと 考えることができます。

0.2---プログラミング言語

Q 0.2.1___プログラムは、日本語とか英語とかで書けば いいのですか。

いいえ。プログラムは、プログラミング言語と呼ばれる 言語を使って書きます。プログラミング言語というのは、 プログラムを書くために使う、という限定された用途を持つ 言語のことです。 人間の社会の中で自然発生的に形成された言語のことを 「自然言語」と言います。日本語、英語、中国語などは自然言語に 属している言語です。自然言語は、人間と人間とが会話をしたり、 できごとを記録したりするときには便利ですが、プログラムを書く という用途には適していません。プログラムを書く場合は、 自然言語よりも、そのために作られた専用の言語を使うほうが 書きやすいのです。 自然言語と同様に、プログラミング言語にもさまざまな種類が あります。この文章で扱われているMLというのも プログラミング言語の一種ですし、そのほかにも、Pascal、Lisp、 Prolog、Logo、awk、Perl、Tcl、Pythonなど、たくさんの プログラミング言語があります。

Q 0.2.2___コンピュータは、どんなプログラミング言語でも 理解することができるのですか。

いいえ。コンピュータが理解することのできる プログラミング言語は、コンピュータごとに決まっている たったひとつのものだけです。 あるコンピュータが理解することのできる言語のことを、 そのコンピュータの「機械語」と言います。ほとんどの コンピュータの機械語は、人間にとっては扱いにくいものですので、 人間が機械語でプログラムを書くということはめったにありません。

0.3---処理系

Q 0.3.1___コンピュータに理解できない言語で書かれたプログラムを コンピュータに理解してもらえるようにするためにはどうすれば いいのですか。

そんなときは処理系というものを使います。 処理系というのは、コンピュータに理解できない言語で書かれた プログラムをコンピュータに理解してもらえるようにする プログラムのことです。 処理系は、プログラムをコンピュータに理解してもらう方法の 違いによって、コンパイラ(Q 0.3.2参照)と インタプリタ(Q 0.3.3参照)に分類することができます。 処理系は、英語ではlanguage processorと呼ばれます。日本語では、 「処理系」という言葉を使うのが普通ですが、language processorを 逐語訳した「言語処理系」という言葉が使われることもあります。

Q 0.3.2___コンパイラって何ですか。

コンパイラというのは、機械語以外の言語で書かれたプログラムを 読み込んで、それとまったく同じ意味を持つ、機械語で書かれた プログラムを出力する、というタイプの処理系のことです。 つまり、人間がどんな言語を使ってプログラムを書いたとしても、 その言語から機械語への翻訳をしてくれるコンパイラを 使うことによって、コンピュータに理解できるプログラムを 作り出すことができるわけです。 なお、プログラムを機械語に翻訳することを、プログラムを 「コンパイルする」と言います。

Q 0.3.3___インタプリタって何ですか。

インタプリタというのは、機械語以外の言語で書かれたプログラムの 意味を理解して、それをコンピュータに実行させる、というタイプの 処理系のことです。 インタプリタは、人間にとって容易に扱うことのできる言語を 機械語とする、見かけ上のコンピュータを作り出すプログラムだ、 と考えることができます。たとえば、Aという言語で書かれた プログラムを理解することのできるインタプリタを実行している 状態のコンピュータは、見かけ上、Aを機械語とするコンピュータに なっているわけです。

Q 0.3.4___仮想機械って何ですか。

仮想機械というのは、コンピュータの動作を模倣する プログラムのことです。 コンピュータの上で仮想機械というプログラムが動作しているとき、 その状態のコンピュータは、見かけ上、そのコンピュータ自身とは 別の、プログラムによって作り出された仮想的な コンピュータになっています。 仮想機械という概念は、どういう目的でコンピュータの動作を 模倣するのかということによって、いくつかの下位概念に 分類されます。たとえば、エミュレーターというのは、仮想機械の 下位概念のひとつです。エミュレーターの目的は、自分の手元にない コンピュータの上で動作するように書かれたプログラムを、自分の 手元にあるコンピュータの上で動作させる、ということです。 Q 0.3.3で説明したインタプリタも、仮想機械の下位概念の ひとつだと考えることができます。つまり、 インタプリタというのは、人間にとって扱いやすい言語を 機械語とするコンピュータを仮想的に作り出す、ということを 目的とする仮想機械だということです。 日本語の「仮想機械」という言葉は、英語のvirtual machineを 逐語訳したものですが、同じ言葉を意訳した「仮想計算機」という 言葉が使われることもあります。

Q 0.3.5___対話型システムって何ですか。

対話型システムというのは、人間からの指示を受け取って、 その指示にしたがって動作をする、ということを繰り返す プログラムのことです。 インタプリタは、たいてい、プログラムまたはプログラムの断片を 人間から受け取って、それがあらわしている動作を実行する、 ということを繰り返すように作られています。つまり、ほとんどの インタプリタは対話型システムとして作られているわけです。 「対話型システム」の対義語は、「バッチ型システム」です。 バッチ型システムというのは、人間からの指示をまったく 受け取らずに黙々と動作を続けて、自分の仕事が完了した時点で 動作を終了するプログラムのことです。 多くのコンパイラはバッチ型システムとして作られていますが、 対話型システムのコンパイラというのも存在します。それは、 プログラムを人間から受け取って、それをコンパイルして、 その結果としてできた機械語のプログラムを実行する、 ということを繰り返すように作られています。

Q 0.3.6___プロンプトって何ですか。

プロンプトというのは、文字列を使って人間と対話をする プログラムが、自分が人間からの入力を受け付ける状態になった ということを人間に知らせるために出力する文字または文字列の ことです。 MLの対話型システムは、普通、マイナス(-)という文字を プロンプトとして使っています。

Q 0.3.7___エラーって何ですか。

エラーというのは、プログラムの中に含まれている正しくない 記述のことです。 エラーは、 (1) 言語の使い方に問題のあるもの。 (2) プログラムを書いた人の意図と実際に記述されている動作との 間に食い違いがあるというタイプのもの。 の2種類に分類することができます。プログラムの中に(1)のエラーが 含まれていた場合は、どこに問題があるかということを処理系が 指摘してくれます(でも、どこをどのように修正すればいいか ということまでは教えてくれません。「問題がある」と処理系が 指摘した場所とは異なる場所を修正しなければならない場合も あります)。 なお、処理系が出力する、エラーについての報告のことを、 「エラーメッセージ」と言います。

Q 0.3.8___プログラミングって何ですか。

プログラミングというのは、プログラムを書いたり、それを処理系に 処理させたり、エラーの原因を究明したりというような、 プログラムを完成させるために必要となるさまざまな作業の ことです。

Q 0.3.9___MLの処理系にはどんなものがあるのですか。

MLの処理系にはさまざまなものがあるのですが、ML関連の文献に もっともよく登場するのは、Standard ML of New Jersey(略称は SML/NJ)という処理系です。 SML/NJは、AT&T Bell LaboratoriesのDavid MacQueenさんと Princeton UniversityのAndrew Appelさんによって書かれたMLの コンパイラです。SML/NJについてもっとくわしく知りたい読者は、 http://cm.bell-labs.com/cm/cs/what/smlnj/index.html を参照するといいでしょう。 SML/NJ以外で、よく知られているMLの処理系としては、 Objective Camlというのがあります。 Objective Camlは、Institut National de Recherche en Informatique et en Automatique(略称はINRIA)で開発された MLのコンパイラです。ただし、Objective Camlが 処理することのできるMLは、この文章(初級ML講座)で扱われている ものとはかなりの違いがあります。Objective Camlについては、 http://pauillac.inria.fr/ocaml/ に、くわしい情報があります。 そしてさらに、ここで紹介したもの以外にも、さまざまなMLの 処理系があります。MLの処理系にはどんなものがあって、どうやって 入手すればいいか、というようなことについては、 comp.lang.mlというニュースグループに定期的に投稿されている、 COMP.LANG.ML Frequently Asked Questions and Answers という記事が参考になると思います。

Q 0.3.10___私が使っている環境は、ちょっと 時代遅れのものなので、Standard ML of New Jerseyが動きません。 私の環境でも動くようなMLの処理系はありませんか。

かならず動くという保証はできませんが、Moscow MLという処理系を 試してみてはいかがでしょうか。 確かに、Standard ML of New Jerseyは、ちょっと時代遅れの 環境(DOSとか、68kのMacとか)では動きません。でも、あなたの 環境でSML/NJが動作しないとしても、そこであきらめるのは 早計です。SML/NJよりも小型軽量で、しかもレトロな環境に 対応しているMLの処理系というのも、いくつかあるのです。 そして、そのような処理系の代表と言えるのが、Moscow MLという 処理系です。 Moscow MLは、Russian Academy of Sciencesの Sergei Romanenkoさんと Royal Veterinary and Agricultural Universityの Peter Sestoftさんによって書かれたMLの処理系です。 Moscow MLについてのさらにくわしい情報は、 http://www.dina.kvl.dk/~sestoft/mosml.html にあります。

0.4---MLの対話型システム

Q 0.4.1___シェルって何ですか。

シェルというのは、人間が入力した「コマンド」と呼ばれる文字の 列を解釈して、それがあらわしている動作を オペレーティングシステムに実行させる、という機能を持っている プログラムのことです。 UNIXの場合はbashやtcshなど、DOSの場合はcommand.comや cmd.exeというプログラムが、シェルとして使われています。 シェルは、普通、対話型システムとして動作するように 作られています。つまり、シェルは、 (1) プロンプトを出力する。 (2) 人間が入力したコマンドを読み込む。 (3) そのコマンドを解釈して、それがあらわしている動作を オペレーティングシステムに実行させる。 という動作を延々と繰り返します。

Q 0.4.2___MLの対話型システムは、どうすれば 起動することができるのですか。

MLの対話型システムを起動したいときは、そのためのコマンドを シェルに入力します。 MLの対話型システムを起動するという動作をあらわすコマンドは、 それぞれの対話型システムによって異なっています。自分が 使おうとしている対話型システムを起動するコマンドについては、 おそらく、処理系とともに配布されている文書の中に書かれていると 思いますので、そちらを参照してください。 仮に、Yodogawa MLという架空のMLの処理系があって、 その対話型システムを起動するコマンドがyodomlだとしましょう。 yodomlというコマンドをシェルに入力すると、モニターの画面は 次のようになります(MLのどんな対話型システムでも、ほとんど 同じような画面になります)。 $ yodoml Yodogawa ML, version 5.10 - まず、$というのはシェルのプロンプトだと思ってください。 その右側のyodomlは、人間が入力したコマンドです。その下に 表示されているのは、起動したMLの対話型システムが 出力したものです。このように、MLの対話型システムは、 起動した直後に自分の名前やバージョン番号などを出力して、 さらにマイナス(-)という文字を出力します。このマイナスは、MLの 対話型システムのプロンプトです。ですから、MLの対話型システムに 理解することのできる文字の列をそのマイナスの右側に入力すると、 対話型システムはその文字の列の意味を解釈して、それが 意味している動作を実行して、その動作が終了すると、ふたたび マイナスを出力します。

Q 0.4.3___MLの対話型システムを終了させたいときは どうすればいいのですか。

MLの対話型システムを終了させたいときは、「対話を終了させる」 ということを意味する文字を入力します。 対話を終了させるということをあらわす文字は、 オペレーティングシステムによって次のように決まっています。 UNIXの場合、対話の終了はcontrol-Dという文字によって あらわされます。この文字は、コントロールキーを押しながら Dのキーを押すことによって入力することができます。MLの 対話型システムがマイナスというプロンプトを出力して人間からの 入力を待っている状態のときにcontrol-Dを入力すると、 MLの対話型システムの動作が終了して、シェルのプロンプトが 表示されます。 DOSの場合は、control-Zという文字を入力する(つまり、 コントロールキーを押しながらZのキーを押す)ことによって、 MLの対話型システムを終了させることができます(環境によっては、 そのあとさらに改行を入力しないといけないかもしれません)。

Q 0.4.4___MLの対話型システムは電卓としても使うことができる、 というのは本当ですか。

はい、それは本当です。 それでは、実際にMLの対話型システムを電卓として 使ってみましょう。まず最初に、4と3とを足し算した結果を 求めさせてみます。対話型システムが出力したマイナス(-)という プロンプトの右側に、 4+3; と入力して、さらに改行を入力してみてください。すると、 モニターの画面は、 - 4+3; > val it = 7 : int - というようになります。マイナスというプロンプトの右側に 表示されているのが人間によって入力された課題で、その下に 表示されているのが対話型システムによる応答です。イコール(=)と コロン(:)のあいだに出力されている7というのが、4と3とを 足し算した結果です。対話型システムは、応答を出力したのち、 ふたたびプロンプトとしてマイナスを出力します。 次に、20から14を引き算する、という計算を実行させてみましょう。 対話型システムに、 20-14; と入力して、改行を入力してみてください。すると、 > val it = 6 : int というように、引き算の結果が出力されるはずです。 次は掛け算です。4と3とを掛け算した結果を求めさせてみましょう。 対話型システムに掛け算を実行させたいときは、 アスタリスク(*)という文字を使います。たとえば、 4*3; というものを入力して改行を入力すると、 > val it = 12 : int というように、4と3とを掛け算した結果が出力されます。

Q 0.4.5___MLの対話型システムに、セミコロンを入力しないで改行を 入力したら、応答を返してくれなくなってしまったのですが、 そんなときはどうすればいいのですか。

そんなときは、改行を入力したあとでもかまいませんので、 セミコロンを入力して、さらに改行を入力してください。 実は、MLの対話型システムは、人間が改行を入力したとしても、 それまでに入力したものの中にセミコロンが含まれていなかった 場合は、人間による入力がまだ続いていると判断するのです。 ですから、セミコロンを入力するのを忘れて改行を 入力したとしても、あわてる必要はありません。その状態の 対話型システムに対してセミコロンと改行を入力すると、 それまでに入力された課題に対する応答がちゃんと 出力されるはずです。

Q 0.4.6___MLの対話型システムにマイナスの数値を入力したいときは どうすればいいのですか。

マイナスの数値を入力したいときは、まずチルダ(~)という文字を 入力してから、その絶対値を入力します。 たとえば、マイナスの14という数値を入力したいという場合は、 まずチルダを入力して、次に14を入力します。ですから、 20+ ~14; と入力すると(プラスとチルダとのあいだには空白が必要です)、 対話型システムは20とマイナスの14とを加算しますので、 その結果として6が出力されます。 対話型システムは、計算の結果を出力するときも、数値が マイナスだということをあらわすためにチルダという文字を 使います。ですから、 14-20; という計算の結果は、 > val it = ~6 : int というように出力されます。

Q 0.4.7___MLの対話型システムに小数点以下の桁を持っている数値を 入力したいときはどうすればいいのですか。

小数点以下の桁を持っている数値を入力したいときは、小数点の 位置にドット(.)という文字(「ピリオド」と呼ばれることも あります)を入力します。 たとえば、 1.8*0.3; というものを対話型システム入力すると、 > val it = 0.54 : real という応答が出力されます。 なお、ドットが含まれている数と含まれていない数とが 混在しているような計算はできませんので、注意してください。 たとえば、対話型システムに対して、 18*0.3; と入力したとすると、エラーメッセージが出力されてしまいます。 18と0.3とを掛け算したいときは、 18.0*0.3; というように、18の右側にドットと0を追加する必要があります。

Q 0.4.8___MLの対話型システムに割り算を実行させたいときは どうすればいいのですか。

MLの対話型システムに割り算を実行させたいときは、divまたは スラッシュ(/)を使います。 たとえば、63を9で割り算する、という計算をしたいときは、 対話型システムに、 63 div 9; と入力します(divの前後には空白が必要です)。すると 対話型システムは、 > val it = 7 : int というように結果を出力します。 divを使って割り算を実行させた場合、それが整数の範囲では 割り切れなかったとしても、結果はかならず整数になります。 たとえば、 46 div 8; という割り算の結果は、5.75ではなくて、5になります。 divは、ドットを含んでいる数に対しては使うことができません。 たとえば、対話型システムに、 4.6 div 0.8; と入力したとすると、エラーメッセージが出力されます。ドットを 含んでいる数に対して割り算をしたいときは、スラッシュ(/)を 使います。たとえば、4.6を0.8で割り算したいというときは、 4.6/0.8; と入力します。すると対話型システムは、 > val it = 5.75 : real というように結果を出力します。 divとは逆に、ドットを含んでいない数に対してスラッシュを 使うことはできません。たとえば、 46/8; という入力は、エラーになってしまいます。

Q 0.4.9___MLの対話型システムは、計算の結果に対してさらに 何らかの計算をするということもできるのですか。

はい、できます。 たとえば、対話型システムに、 6*3+4; と入力すると、対話型システムは、6と3とを掛け算した結果と4とを 足し算して、その結果として22を出力します。 人間が入力したものの中に+、-、*、div、/などが混在している 場合、MLの対話型システムは、*、div、/を、+や-よりも先に 実行します。計算が実行される順序を変更したいときは、先に 実行してほしい部分を丸括弧で囲んでください。たとえば、 6*(3+4); と入力すると、3と4とがまず足し算されて、その結果と6とが 掛け算されます。 足し算と引き算との組み合わせとか、引き算と引き算との 組み合わせとか、掛け算と割り算との組み合わせとかの場合は、 入力した順序のとおりに計算が実行されます。たとえば、 17-14+6; と入力すると、17から14を引き算した結果と6とを足し算した結果、 つまり9が出力されます。このような組み合わせの場合も、計算の 順序を変更したいときは丸括弧を使います。たとえば、 17-(14+6); と入力すると、14と6とを足し算した結果を17から引き算する という意味だと解釈されます。

---練習問題

0.1___MLの対話型システムに次のような計算の結果を 求めさせたいときは、どのようなものを入力すればいいですか。

(a) 53と27とを足し算する。 (b) 88から62を引き算する。 (c) 14と30とを掛け算する。 (d) 54からマイナスの33を引き算する。 (e) 5.08と7.15とを足し算する。 (f) 46を7で割り算する。 (g) 34.6を2.8で割り算する。 (h) 68を3で割り算した結果と4とを足し算する。 (i) 32から19を引き算した結果と15とを掛け算する。 (j) 22と45とを掛け算した結果を81で割り算する。 (k) 313を、18と7とを掛け算した結果で割り算する。

0.2___MLの対話型システムに次のようなものを入力すると、 エラーになります。その理由は何ですか。

(a) 93+4.2; (b) 50.7 div 19.2; (c) 33/14; (d) 64*~7; (e) 82 div15;

第1章===データの表現

1.1---ビット列

Q 1.1.1___ビットって何ですか。

ビットというのは、二とおりの状態を持つことのできる もののことです。 たとえば、1個の電球は、点燈している状態とそうでない状態という 二とおりの状態を持つことができますので、ビットの一例だと 考えることができます。 ビットが持っている状態のそれぞれは、普通、0と1という数字で 書きあらわされます。

Q 1.1.2___ビット列って何ですか。

ビット列というのは、ビットを並べることによってできる 列のことです。 ビット列を構成するビットの個数を、そのビット列の「長さ」 と言います。長さがnのビット列があるとすると、そのビット列は、 2のn乗とおりの状態を持つことができます。たとえば、長さが8の ビット列は、256とおりの状態を持つことになります。 「ビット」という言葉には、ビット列の長さの最小の単位という 意味もあります。ですから、「長さがnのビット列」と言う代わりに 「nビットのビット列」と言っても、同じ意味になります。

Q 1.1.3___ビット列のパターンって何ですか。

ビット列のパターンというのは、ビット列が持つことのできる 個々の状態のことです。

Q 1.1.4___ビット列というのは何のために使われるものなんですか。

ビット列は、さまざまなものごとを表現するという目的で 使われるものです。 コンピュータの内部では、ものごとを表現するための手段として ビット列が使われています。つまり、数値も文字も音声も画像も、 コンピュータの内部ではビット列のパターンという形で 存在しているということです。

Q 1.1.5___バイトって何ですか。

バイトというのはビット列の長さの単位のひとつで、 1バイトというのは8ビットと同じ長さです。

Q 1.1.6___2進数って何ですか。

2進数というのは、0または1という数字を並べることによってできる 列のことです。たとえば、 011110100110 という数字の列は2進数の一例です。 2進数は、ビット列のパターンを書きあらわすという目的でしばしば 使われます。

Q 1.1.7___16進数って何ですか。

16進数というのは、0、1、2、3、4、5、6、7、8、9、A、B、C、D、 E、Fという16種類の数字を並べることによってできる 列のことです(A、B、C、D、E、Fは、小文字を 使ってもかまいません)。たとえば、 5E7A という数字の列は16進数の一例です。 16進数を作るための16種類の数字は、それぞれ、 0 0000 4 0100 8 1000 C 1100 1 0001 5 0101 9 1001 D 1101 2 0010 6 0110 A 1010 E 1110 3 0011 7 0111 B 1011 F 1111 というように、長さが4のビット列のパターンに対応しています。 この対応を使うことによって、16進数も、2進数と同じように ビット列のパターンを書きあらわすための手段として 利用することができます。 ビット列のパターンを16進数で書きあらわしたいときは、まず、 そのビット列を右端から順番に4ビットごとに区切っていって、 いくつかの部分ビット列を作ります。左端の部分ビット列の長さが 4よりも短い場合は、その左側に0を補うことによって長さを 4にします。そして次に、それぞれの部分ビット列を、それに 対応する数字であらわしていきます。たとえば、 01110110110100 というビット列のパターンを16進数で書きあらわしたいときは、 まずそれを、 01 1101 1011 0100 というように、右端から順番に4ビットごとに区切ります。左端の 01という部分ビット列は長さが4よりも短いので、左側に0を補って 0001にします。0001というパターンは1という数字に 対応していて、同じように1101はDに、1011はBに、0100は4に 対応しています。したがって、01110110110100というビット列の パターンをあらわす16進数は、1DB4だということになります。

Q 1.1.8___ビット列を使って0またはプラスの整数を 表現したいときはどうすればいいのですか。

ビット列を使って0またはプラスの整数を表現する方法としては、 普通、「2進法」と呼ばれるものが使われます。 2進法では、ビット列を構成するそれぞれのビットは、あらかじめ 定められた整数に対応していると考えます。ビット列の長さを nとすると、それぞれのビットに対応する整数は、左から順番に、 2の(n-1)乗、2の(n-2)乗、2の(n-3)乗、……、2の0乗 というようになっています。たとえば、長さが8のビット列の 場合、それを構成するそれぞれのビットは、 128、64、32、16、8、4、2、1 という整数に対応しています。 2進法を使って0またはプラスの整数をビット列のパターンで 表現するというのは、状態が1になっているビットに対応している 整数を加算した結果と、表現したい整数とが一致するような パターンを作る、ということです。たとえば、45という整数を 8ビットのビット列で表現したいとしましょう。45は、32と8と4と1を 加算することによって得られます。ですから、32と8と4と1に 対応しているビットが1で、それら以外のビットが 0になっているようなビット列のパターンを作ればいいわけです。 つまり、00101101というビット列のパターンで、45という整数を 表現することができるということになります。

Q 1.1.9___ビット列のパターンを10進数で書きあらわすということは 可能ですか。

はい、可能です。 どんなビット列も、0またはプラスの整数を表現している とみなすことができます。ですから、ビット列のパターンを、それと 同じ整数を表現している10進数を書くことによって記述する、 ということが可能です。たとえば、 01001011 というビット列のパターンは、75という整数を表現している とみなすことができますので、75という10進数を書くことによって それを記述することができます。

Q 1.1.10___ビット列を使って文字を表現したいときは どうすればいいのですか。

ビット列を使って文字を表現したいときは、文字コードというものを 使います。 文字コードというのは、文字の集合からビット列のパターンの 集合への写像のことです。表現したい文字のそれぞれが異なる パターンに対応するように文字コードをあらかじめ 決めておくことによって、ビット列を使って文字を表現することが できるようになります。たとえば、 ○ 00 □ 10 △ 01 ☆ 11 という簡単な文字コードを決めておくことによって、"○"、"△"、 "□"、"☆"という4種類の文字を2ビットのビット列で 表現することができるようになります。 また、「文字コード」という言葉は、「文字をあらわしている ビット列のパターン」という意味で使われることもあります。 たとえば、「"△"の文字コードは01である」というような 使い方です。 なお、文字コードについての話をするとき、ビット列のパターンを 2進数であらわすということは、あまりしないようです。16進数で あらわすか、4ビットごとに区切ったものを10進数であらわす というのが一般的です。たとえば、01001101というパターンは、 16進数で4Dと言ったり、10進数で4/13と言ったりします。

Q 1.1.11___ASCIIって何ですか。

ASCIIというのは、文字コードの国際的な標準規格です。 コンピュータとコンピュータとが通信をするとき、それらの コンピュータが採用している文字コードが異なっていると、 スムーズにデータを交換することができません。そこで、 すべてのコンピュータの文字コードを統一するために、 標準規格が制定されました。 文字コードの国際的な標準規格は、ASCII(American Standard Code for Information Interchange)と呼ばれています。ASCIIは、英字、 数字、特殊文字、制御文字(改行や改ページなど)という、どんな 国でも使われているような文字を長さが8のビット列のパターンに 対応させる文字コードです。たとえば、ASCIIでは、"M"が4/13に、 "7"が3/7に、"+"が2/11に対応しています。 ASCIIに含まれていない文字を使っている国は、ASCIIを基本として、 独自の標準規格を制定しています。日本の場合、日本で使われている 文字(英字、数字、漢字、カタカナ、ひらがななど)をビット列の パターンに対応させる文字コードの標準規格として、 「JIS X0201ローマ字」「JIS X0201片仮名」「JIS X0208」 と呼ばれるものが制定されています。 「JIS X0201ローマ字」は、ほとんどASCIIそのものと言っていい 文字コードですが、違っている点が二つだけあります。それは、 (1) 5/12というパターンに対応する文字が、ASCIIでは バックスラッシュ(左上から右下へ向かう斜線という形を持つ 文字)なのに対して、「JIS X0201ローマ字」では円マーク(Yと イコール(=)とを組み合わせた形を持つ文字)である。 (2) 7/14というパターンに対応する文字が、ASCIIではチルダ(波の ような形を持つ文字)なのに対して、「JIS X0201ローマ字」では オーバースコア(高い位置に描かれた横棒という形を持つ 文字)である。 という二つの点です。 日本語版のオペレーティングシステムを使っている場合、 バックスラッシュが出力されるべきところに円マークが 出力されたり、チルダが出力されるべきところにオーバースコアが 出力されたりすることがありますが、それは以上のような 事情によるものです。

1.2---型

Q 1.2.1___型って何ですか。

型というのは、データが所属している集合のことです。 MLでは、すべてのデータはなんらかの型に所属していると考えます。 たとえば、14とか651というような整数は、intという名前を持つ型に 所属しています。 ただし、型に「所属している」という言い方をすることは めったになくて、型を「持っている」という言い方をするのが 普通です。

Q 1.2.2___型にはどんなものがあるのですか。

MLには、「基本型」と呼ばれるいくつかの単純な型と、 基本型を組み合わせることによって作り出されるさまざまな型とが あります。

Q 1.2.3___基本型にはどんなものがあるのですか。

基本型には、int、real、word、string、char、bool、unit、exn、 instream、outstreamなどがあります(処理系によっては、 これら以外の基本型が追加されているかもしれません)。 intというのは整数の集合です。ただし、コンピュータは、絶対値が あまりにも大きな整数を扱うことができませんので、intというのは 有限集合です。コンピュータが扱うことのできる整数の範囲は、 どんなコンピュータの上でどんな処理系を使っているかによって 違いますが、かならずどこかに限界があります。 realというのは実数の集合です。ただし、ここで言う 実数というのは、数学で言う実数とは意味が微妙に違います。 数学の場合、無限に多くの桁を持つ数(循環小数と無理数)も実数の 集合に含まれますが、コンピュータというのは有限のものしか 扱うことができませんので、realには限られた桁数を持つ数しか 含まれません。 wordというのは0またはプラスの整数の集合です。intと同じように、 wordに含まれる整数の範囲は、コンピュータや処理系によって 異なっています。また、wordは、ビット列のパターンの集合だと 考えることもできます。 stringというのは、文字列、つまり文字を並べることによってできる 列の集合です。 charというのは文字の集合です。ただし、charに含まれるのは 1バイトのビット列であらわされる文字だけです。英字や数字や 特殊文字は1バイトのビット列であらわされますのでcharに 含まれますが、漢字やひらがななどの日本語特有の文字は 2バイトのビット列であらわされるものなので、charには 含まれません。 boolというのは、条件が真であるということをあらわすデータと 偽であるということをあらわすデータ、という二つのデータだけを 要素とする集合です。boolに含まれているそれぞれのデータの ことを真偽値と呼びます。条件や真偽値については、第5章で さらにくわしく説明します。 unitというのは、「ユニット」と呼ばれるただひとつのデータだけを 要素とする集合です。ユニットというのは、意味を持たない形だけの データとしてしばしば使われます。 exnというのは、「例外」と呼ばれるデータの集合です。 例外については、第7章でくわしく解説します。 instreamというのは、コンピュータの外部からデータを 読み込むために必要となる、「入力ストリーム」と呼ばれるデータの 集合です。また、outstreamというのは、コンピュータの外部に データを出力するために必要となる、「出力ストリーム」と呼ばれる データの集合です。データの読み込みや出力については、第16章に 説明があります。

Q 1.2.4___型式って何ですか。

型式(かたしき)というのは、型をあらわすためにいくつかの単語を 並べたもののことです。 あらゆる型は、型式によって書きあらわすことができます。 基本型の場合、型式というのはその名前のことです。基本型以外の 型の型式は、2個以上の単語を組み合わせることによって 作られます。

Q 1.2.5___型というのは、何のためにあるのですか。

型というものの第一の目的は、プログラムの中のエラーを できるだけ多く処理系に指摘させることによって、プログラムを 修正する作業を容易にすることです。 データに対してどのような操作をすることができるかというのは、 そのデータが持っている型によってかなり違っています。たとえば、 加算とか乗算というような計算は、intやrealなどの型を持つ データに対しては実行することができますが、stringやboolなどの 型を持つデータに対して実行することはできません。もしも、 プログラムの中に、データと操作とのあいだで型が一致していない 記述があった場合、処理系は、それをエラーとして 指摘することができます。 型という概念がないとか、すべてのデータがひとつの型を持つ というようなプログラミング言語(注)の場合、処理系は、純粋に 文法的なエラーならば指摘することができますが、それ以外の エラーについては指摘することができません。処理系がエラーを まったく指摘しないにもかかわらずプログラムが意図した 動作をしない場合、そのプログラムのどこに エラーがあるかというのは、プログラムの動作から 推測するしかありませんので、そのプログラムを修正する作業は、 かなり困難なものになります。 (注) 型のないプログラミング言語は、開発の途中での大幅な仕様の 変更が簡単にできるというメリットがありますので、そのような 柔軟性を必要とする分野では、なくてはならない存在です。 また、型には、プログラムが効率よく実行されるようにするという 第二の目的もあります。処理系は、プログラムの中に含まれている 型の情報を使うことによって、そのプログラムができるだけ短い 時間で実行されるようにしたり、コンピュータのメモリーを できるだけ消費しないようにしたりすることができるのです。

1.3---基本型のデータをあらわす定数

Q 1.3.1___MLの処理系に理解できる形で特定のデータを 書きあらわしたいときはどうすればいいのですか。

そんなときは定数というものを書きます。 定数というのは、特定のデータを書きあらわすためにMLの規則に したがって文字を並べたもののことです。

Q 1.3.2___0またはプラスの整数をあらわす定数はどう書けば いいのですか。

0またはプラスの整数をあらわす定数は、0から9までの数字のみから 構成されている10進数です。 たとえば、58という整数を定数で記述したいときは、58という 10進数を書けばいいわけです。 0またはプラスの整数をあらわす定数は、16進数を使って 作ることもできます。その場合は、まず、定数で記述したい整数を あらわすビット列のパターンを作ります。次に、それを16進数で 記述して、さらにその左側に0xという接頭辞を追加します。 たとえば、58という整数をあらわす定数を16進数を使って書くと、 0x3Aになります。 接頭辞のない10進数、および0xという接頭辞のある16進数は、 intという型を持つデータをあらわしているとみなされます。 wordという型を持つ0またはプラスの整数を定数であらわす 方法については、Q 1.3.6を参照してください。

Q 1.3.3___マイナスの整数をあらわす定数はどう書けば いいのですか。

マイナスの整数をあらわす定数は、その整数の絶対値をあらわす 定数の左側にチルダという文字(~)を追加することによって 作ることができます(Q 1.1.11で説明したように、日本語版の オペレーティングシステムを使っている場合は、ASCIIで チルダに対応しているビット列のパターンがオーバースコアで 出力されることもあります)。 たとえば、マイナスの58という整数を定数で記述したいときは、 その絶対値をあらわす58または0x3Aという定数の左側にチルダを 追加して、~58または~0x3Aと書けばいいわけです。

Q 1.3.4___実数をあらわす定数はどう書けばいいのですか。

実数をあらわす定数は、ドットまたはピリオドと呼ばれる文字(.)が 小数点の位置に書かれた10進数です。 たとえば、347016という整数の10000分の1に相当する実数を 書きあらわしたいときは、 34.7016 という定数を書きます。 マイナスの実数を書きあらわしたいときは、整数の場合と 同じように、その絶対値をあらわす定数の左側にチルダを 書きます。たとえば、マイナスの34.7016をあらわす定数は、 ~34.7016 と書きます。 87.0とか26.00とか53.000のように、ドットの右側が0の列に なっている定数は、数学的には整数をあらわしているわけですが、 MLでは、それらの定数があらわしているデータの型は intではなくてrealになりますので、注意が必要です。

Q 1.3.5___何々掛ける10の何乗という形の定数を書くことは 可能ですか。

はい、可能です。 「何々掛ける10の何乗」という形の定数を書きたいときは、 まず「何々」の部分(仮数部)を書いて、その右側にeまたはEを 書いて、さらにその右側に「何乗」の部分(指数部)を書きます。 たとえば、47掛ける10の6乗は、47e6という定数であらわすことが できます。 小数点の位置をあらわすドットは、仮数部に書くのは かまいませんが、指数部には書くことができません。たとえば、 19.584e7というのは正しい定数ですが、25e3.41というのは正しい 定数ではありません。 マイナスをあらわすチルダは、仮数部にも指数部にも書くことが できます。たとえば、~3e8、3e~8、~3e~8は、いずれも正しい 定数です。 なお、数値をあらわす定数でeまたはEを含んでいるものが あらわしているデータの型は、常にrealになります。45e7のように 小数点が含まれていない場合でもrealになるという点に 注意してください。

Q 1.3.6___wordという型を持つ0またはプラスの整数をあらわす 定数は、どう書けばいいのですか。

wordという型を持つ0またはプラスの整数をあらわす定数は、 その整数をあらわしている10進数の左側に0wという接頭辞を 追加したものです。 たとえば、58という10進数は、そのままだとintのデータをあらわす 定数になるわけですが、それに0wという接頭辞を付けて、0w58と 書けば、wordのデータをあらわす定数になります。 wordという型を持つ0またはプラスの整数をあらわす定数は、 16進数を使って作ることもできます。その場合は、まず、定数で 記述したい整数をあらわすビット列のパターンを作ります。次に、 それを16進数で記述して、さらにその左側に0wxという接頭辞を 追加します。たとえば、58というwordの整数をあらわす定数を 16進数を使って書くと、0wx3Aになります。 Q 1.2.3でも書きましたが、wordという型は、ビット列のパターンの 集合だと考えることもできます。ですから、0w58とか0wx3Aという 定数は、ビット列の長さを8だと仮定すると、 00111010 というパターンをあらわしていると考えてもいいわけです。

Q 1.3.7___真偽値をあらわす定数はどう書けばいいのですか。

真はtrue、偽はfalseと書きます。

Q 1.3.8___文字列をあらわす定数はどう書けばいいのですか。

文字列をあらわす定数は、その文字列(つまりデータとして 扱ってほしい文字列)を二重引用符(")で囲んだものです。 たとえば、"mikan"は、文字列をあらわす定数です。この定数は、 mikanという文字列をあらわしています。 なお、これからは、文字列をあらわす定数のことを「文字列定数」と 呼ぶことにします。 二重引用符のあいだには、英字だけではなく、数字や空白や ほとんどの特殊文字も、そのまま書くことができます。たとえば、 "~583 means minus 583 in ML." という定数には、数字や空白やチルダ(~)やドット(.)が 含まれていますが、この定数があらわしている文字列は、 二重引用符のあいだに書かれている文字列とまったく同じものです。 また、漢字やカタカナやひらがななどの日本語が扱える環境で、 日本語が扱える処理系を使っている場合は、文字列定数の中に 日本語の文字を書くことも可能です。

Q 1.3.9___二重引用符を含む文字列をあらわす定数や、改行を含む 文字列をあらわす定数を書きたいのですが、うまくいきません。 そんなときはどうすればいいのですか。

そんなときは、エスケープ・シーケンスというものを書きます。 二重引用符や改行は、そのまま文字列定数の中に書くということが できません。たとえば、MLの処理系は、 "print "file: " ^ fname" "This string contains newline." というようなものを正しい文字列定数とは認めてくれません。 二重引用符や改行を含む文字列をあらわす定数を書きたいときは、 エスケープ・シーケンスというものを使います。 エスケープ・シーケンスというのは、1個の文字をあらわすために 書かれる、バックスラッシュという文字(\)で始まる 文字列のことです(Q 1.1.11で説明したように、日本語版の オペレーティングシステムを使っている場合は、ASCIIで バックスラッシュに対応しているビット列のパターンが円マークで 出力されることもあります)。たとえば、改行という文字をあらわす エスケープ・シーケンスは、\nと書きます。ですから、 This string contains newline. という、改行を含んでいる文字列を書きあらわしたいときは、 "This string contains\nnewline." という文字列定数を書けばいいわけです。 同じように、二重引用符は\"というエスケープ・シーケンスで あらわされます。たとえば、 print "file: " ^ fname という文字列をあらわす定数は、 "print \"file: \" ^ fname" と書くことになります。 なお、バックスラッシュ自身も、文字列を構成する文字として 扱ってほしい場合はエスケープ・シーケンスを使う必要があります。 バックスラッシュをあらわすエスケープ・シーケンスは、 \\と書きます。たとえば、 c:\karashi\bin\wasabi.exe という文字列をあらわす定数を書きたいときは、 "c:\\karashi\\bin\\wasabi.exe" というようにエスケープ・シーケンスを使います。

Q 1.3.10___とても長い文字列をあらわす定数を書きたいのですが、 それを複数の行に分割して書くことはできないのでしょうか。

できます。そんなときは、改行で分割したい場所に、前後を バックスラッシュで挟まれた改行を挿入します。そうすると、 そのバックスラッシュと改行は、存在しないものとみなされます。 たとえば、 "I love Yoshie, Marina, Rieko, Sayaka, Tomomi, and so on." という文字列定数を2行に分割したいときは、 "I love Yoshie, Marina, Rieko, \ \Sayaka, Tomomi, and so on." というように、分割したい場所に、バックスラッシュと改行と バックスラッシュを挿入します。

Q 1.3.11___空文字列って何ですか。

空文字列というのは、0個の文字から構成される文字列のことです。 空文字列をあらわす文字列定数は、""です。つまり、あいだに 何も書かずに2個の二重引用符を書けばいいわけです。

Q 1.3.12___文字をあらわす定数はどう書けばいいのですか。

文字をあらわす定数は、 #"文字" と書きます。たとえば、#"M"は、"M"という文字をあらわす 定数です。 文字列定数の場合と同じように、二重引用符、改行、 バックスラッシュなどを定数であらわしたいときは、 エスケープ・シーケンスを使います。ですから、二重引用符は #"\""、改行は#"\n"、バックスラッシュは#"\\"という定数で あわらされることになります。

Q 1.3.13___ユニットをあらわす定数はどう書けばいいのですか。

ユニットをあらわす定数は、()と書きます。つまり、左丸括弧の 右側に右丸括弧を書く、ということです。

1.4---組をあらわす定数

Q 1.4.1___MLでは、いくつかのデータが組み合わさって できているような、構造を持ったデータを扱うことも できるのですか。

はい、できます。 MLでは、「組」と呼ばれるデータを扱うことができるように なっています。組というのは、2個以上のデータを一列に 並べたもののことです。組を構成するそれぞれのデータのことを、 その組の「要素」と呼びます。 また、MLでは、「リスト」と呼ばれるデータを扱うこともできます。 不特定の構造を持っているデータを扱いたいときは、組ではなくて リストを使うのが普通です。リストについては、第9章で 解説します。

Q 1.4.2___組を書きあらわしたいときはどうすればいいのですか。

組を書きあらわしたいときは、そのための定数を書きます。 組をあらわす定数は、 ( 定数 , 定数 , 定数 , …… ) という構文にしたがって作ります。つまり、定数をコンマで 区切って並べたものを丸括弧で囲めばいいわけです。それぞれの 定数の前後には、空白や改行を好きなだけ書いてもかまいません。 組をあらわす定数は、その中の定数があらわしているデータを その通りの順序で並べることによってできる組をあらわします。 たとえば、 (810,2299,~511,7653) は、組をあらわす定数のひとつの例です。この定数は、 810と2299と~511と7653を、この順序で並べることによってできる 組をあらわしています。

Q 1.4.3___ひとつの組の要素は、型が同じでないと いけないのですか。

いいえ、型が同じである必要はありません。 たとえば、 (~461,50.188,"Lisbon") というように、~461という整数と50.188という実数と "Lisbon"という文字列を並べることによって組を作っても 問題はありません。つまり、ひとつの組を構成するそれぞれの データは、型がまちまちであってもかまわないのです。

Q 1.4.4___組を要素として含んでいる組を作ることは可能ですか。

はい、可能です。 組というのは、その全体がひとつのデータになりますので、 要素として組を持っている組を作っても、問題はまったく ありません。組の中に組を作ることによって、階層的な構造を持つ データを作ることが可能になります。 要素として組を含んでいる組を書きあらわしたいときは、 組をあらわす定数の中に組をあらわす定数を書きます。たとえば、 (118,("Copenhagen",20.102),"Nairobi") は、118という整数と("Copenhagen",20.102)という組と "Nairobi"という文字列を、この順序で並べることによってできる 組をあらわしています。

Q 1.4.5___直積って何ですか。

直積というのは数学上の概念で、集合の列に1個の集合を対応させる 演算のひとつです。 (S1,S2,S3,……,Sn)という、n個の集合から構成される列が あるとするとき、 (S1の要素, S2の要素, S3の要素, ……, Snの要素) というように要素を並べることによって作ることのできるすべての 列の集合のことを、(S1,S2,S3,……,Sn)の「直積」と言います。 たとえば、S1={0,1}、S2={a,b}、S3={X,Y}とするとき、 (S1,S2,S3)の直積は、 {(0,a,X),(0,a,Y),(0,b,X),(0,b,Y), (1,a,X),(1,a,Y),(1,b,X),(1,b,Y)} という集合になります。

Q 1.4.6___組はどのような型を持つのですか。

組は、要素の型の直積であるような型を持ちます。そのような型は 「積型」と呼ばれます。 たとえば、(27,31.6)という組の型は、(int,real)という列の 直積です。

Q 1.4.7___積型はどのような型式であらわされるのですか。

積型をあらわす型式は、それを構成する型の型式を アスタリスク(*)で結合することによって作ります。 たとえば、(95,0.018)という組の型はint * realという型式で あらわされ、(6.13,"Rangoon",903)という組の型は real * string * intという型式であらわされます。 積型の構成要素として積型が含まれている場合は、構成要素の 積型の型式を丸括弧で囲みます。たとえば、 (("Santiago",717),"Addis Ababa")という組の型をあらわす型式は、 (string * int) * stringと書きます。 積型の型式に含まれている丸括弧は、省略すると意味が 変わってしまう、という点に注意してください。 int * (real * string)とint * real * stringのそれぞれは、 まったく異なる型をあらわしています。

Q 1.4.8___要素の個数が0個とか1個とかの組を作ることも できるのですか。

いいえ、できません。 ()とか(25)というような定数を書くことは可能ですが、それらが あらわしているデータは組ではありません。()という定数が あらわしているのはユニットと呼ばれるデータですし(Q 1.2.3、 Q 1.3.13参照)、(25)という定数があらわしているのは25という intのデータです。

---練習問題

1.1___長さが16のビット列が持つことのできる状態の種類は、 ぜんぶで何とおりですか。

1.2___2進数であらわされている次のビット列のパターンと同じ パターンをあらわす16進数と10進数を求めてください。

(a) 1110 (b) 101 (c) 0110110 (d) 00111010 (e) 1011110100

1.3___次のデータをあらわすMLの定数を書いてください。

(a) 2671(10進数)という整数(型はint)。 (b) 8bc3(16進数)という整数(型はint)。 (c) マイナスの5018(10進数)という整数(型はint)。 (d) 8.10334(10進数)という実数(型はreal)。 (e) マイナスの22.107(10進数)という実数(型はreal)。 (f) 1717掛ける10の5乗(10進数)という実数(型はreal)。 (g) 6666掛ける10のマイナスの6乗(10進数)という実数(型は real)。 (h) 9001(10進数)という整数(型はword)。 (i) f2dd(16進数)という整数(型はword)。 (j) 7個のセミコロン(;)という文字から構成される文字列(型は string)。 (k) 3個の改行という文字から構成される文字列(型はstring)。 (l) 4個の二重引用符(")という文字から構成される文字列(型は string)。 (m) 5個のバックスラッシュ(\)という文字から構成される 文字列(型はstring)。 (n) スラッシュ(/)という文字(型はchar)。

1.4___次の定数によってあらわされる組の型をあらわす型式を 書いてください。

(a) (182,"Gregor Samsa") (b) ("Lewis Carroll",3505) (c) (7114,"Nicholas Copernicus",6e8,#"{") (d) ((580,"Nicolas Bourbaki"),#"}") (e) (1027,("Etaoin Shrdlu",#"[")) (f) (#";",(0.5011,(false,273),("Themsky",0wxC3)),())

第2章===データの名前

2.1---識別子

Q 2.1.1___データに名前を付けておいて、定数ではなくて名前で データをあらわすことは可能ですか。

はい、可能です。 この章では、データに付ける名前の作り方や、データに名前を付ける 方法について説明していきたいと思います。

Q 2.1.2___識別子って何ですか。

識別子というのは、データなどの名前として使うことのできる 文字の並びのことです。 データに付ける名前は、どんな文字の並びでもかまわない というわけではありません。名前を作るためには、MLの文法的な 規則にしたがう必要があります。規則にしたがって作られた、 名前として使うことのできる文字の並びのことを「識別子」 と呼びます。

Q 2.1.3___識別子にはどのような種類があるのですか。

識別子は、(1)英数字識別子、(2)記号識別子、という2種類に 分類することができます。 英数字識別子についてはQ 2.1.4からQ 2.1.7まで、 記号識別子についてはQ 2.1.8を参照してください。

Q 2.1.4___英数字識別子は、どのようにして作ればいいのですか。

英数字識別子は、その名前のとおり、英字または数字を 並べることによって作ります。 たとえば、 namako Kaijin20Mensou などは、正しい英数字識別子です。 英字の大文字と小文字は区別されますので、namakoとNamakoと NAMAKOは、それぞれ異なる識別子とみなされます。 なお、数字を英数字識別子の先頭の文字として使うことは できません。つまり、 9GatsuGejun のように先頭が数字になっている英数字の列は英数字識別子とは みなされない、ということです。 また、英数字識別子として正しいものであっても、 「予約語」と呼ばれるいくつかの単語は、データなどの名前として 使うことができません(予約語については、Q 2.1.5を 参照してください)。たとえば、valというのは正しい 英数字識別子なのですが、予約語のひとつなので、 データなどの名前としてそれを使うことはできません。 ただし、予約語を一部分として含んでいる識別子は、名前として 使うことが可能です。たとえば、Vivaldiという識別子は予約語を その一部分として含んでいますが、それを名前として使っても 問題はありません。

Q 2.1.5___予約語って何ですか。

予約語というのは、プログラミング言語の文法で定められている、 特殊な用途で使われる単語のことです。 英数字識別子のうちで、MLの予約語として定められている ものとしては、 abstype and andalso as case datatype do else end eqtype exception fn fun functor handle if in include infix infixr let local nonfix of op open orelse raise rec sharing sig signature struct structure then type val where while with withtype などがあります(ただし、処理系によって多少の異同が あるかもしれません)。

Q 2.1.6___intとかrealとかstringというような型の名前は、 予約語ではないのですか。

はい、予約語ではありません。 ですから、intやrealなどの型名をデータの名前として使うことも 可能です(でも、プログラムを読みにくくする危険性が ありますので、避けたほうがいいでしょう)。

Q 2.1.7___英数字識別子を作るときに使うことのできる文字は 英字と数字だけなのですか。

いいえ、アンダースコア(_)とアポストロフィー(')も使うことが できます(アポストロフィーは、「引用符」とか「プライム」 と呼ばれることもあります)。 ですから、 sekaide_ichiban_utsukushii_shima Grandfather'sClock などのような英数字識別子を作ることも可能です。 アンダースコアとアポストロフィーは、英数字識別子の先頭の 文字として使ってもかまいません。つまり、 _umiushi 'kaimen などは、正しい英数字識別子です。ただし、英数字識別子の先頭の 文字をアポストロフィーにすると、「型変数」と呼ばれる特殊な 識別子になってしまって、データの名前として使うことが できなくなってしまいますので、注意してください (型変数についてのくわしい説明は、第8章にあります)。 また、1個のアンダースコアだけで英数字識別子を作ると、 「匿名変数」と呼ばれる特殊な識別子になります (匿名変数については、Q 2.4.5とQ 2.4.6を参照してください)。 匿名変数も、データの名前として使うことはできません。

Q 2.1.8___記号識別子は、どのようにして作ればいいのですか。

記号識別子は、 : ! ? - / | \ $ % & # @ * = + < > ~ ^ ` という20種類の文字を並べることによって作ります。 たとえば、 %#$@& |+|+|+|+|+| などは、正しい記号識別子です。 英数字識別子に使うことのできる文字と記号識別子に使うことの できる文字とが混在しているような識別子を作ることは できません。たとえば、 sun&moon Why? などは、正しい識別子とは言えません。 記号識別子の中にも、 予約語として定められているために名前としては使うことの できないものがいくつかあります。たとえば、 | = => -> # :> などの記号識別子は予約語なので、データの名前として 使うことはできません。

2.2---束縛

Q 2.2.1___束縛って何ですか。

束縛というのは、識別子をデータの名前にする、という コンピュータの動作のことです。 「束縛」という言葉は、「識別子をデータに束縛する」 というように、直接目的語と間接目的語をともなう動詞の形で 使われることが多いようです。たとえば、maguroという識別子を 38011というデータの名前にするという場合、「maguroを38011に 束縛する」という言い方をします。

Q 2.2.2___MLの対話型システムを使っていて、識別子をデータに 束縛したくなったときはどうすればいいのですか。

そんなときは、val宣言というものを入力します。 val宣言というのは、 val パターン = 式 という構文を持つ記述のことです。この構文の中には、 「式」という構文要素が含まれていますが、今のところは、 式というのは定数のことだと考えておくことにします。同じように、 この構文に含まれている「パターン」という構文要素は、識別子の ことだと考えていただきたいと思います。つまり、とりあえずの ところ、val宣言の構文は、 val 識別子 = 定数 だということになります。 なお、式については第2.3節で、パターンについては第2.4節で くわしく説明することにします。 MLの対話型システムにval宣言を入力すると、対話型システムは、 イコールの左側に書かれた識別子を、イコールの右側に書かれた 定数があらわしているデータに束縛する、という動作を コンピュータに実行させます。たとえば、MLの対話型システムに、 val namako = 601 というval宣言を入力したとすると、コンピュータは、namakoという 識別子を601というデータに束縛します。 なお、対話型システムに何かを入力するときは、 「実行してほしいものの入力は以上で終わりです」ということを 対話型システムに伝えるために、最後にセミコロン(;)を入力する 必要があります。たとえば、上に書いたval宣言の例を コンピュータに実行させたいときは、 - val namako = 601; というように、val宣言に続けてセミコロンを 入力しないといけません。 MLの対話型システムは、val宣言が入力された場合の応答として、 val宣言によってデータに束縛された識別子、そのデータをあらわす 定数、そしてそのデータの型を、 > val 識別子 = 定数 : 型 という形で出力します。ですから、上に書いたval宣言の例を 対話型システムに入力したとすると、対話型システムは、 > val namako = 601 : int というように、識別子と定数と型を出力します。

Q 2.2.3___識別子を組に束縛することは可能ですか。

はい、可能です。 イコールの右側が組をあらわす定数になっているようなval宣言を MLの対話型システムに入力したとすると、コンピュータは、 イコールの左側にある識別子をその定数があらわしている組に 束縛します。たとえば、 val tarako = (171,"Umeda",38.018) というval宣言を入力することによって、tarakoという識別子を (171,"Umeda",38.018)という組に束縛することができます。

Q 2.2.4___val宣言の途中に空白や改行を好きなだけ 入れたいのですが、それは可能ですか。

はい、可能です。 val宣言を書くときは、単語の前後であれば、空白、タブ、改行を 何個でも挿入することができます。たとえば、 val tarako = (171,"Umeda",38.018) というval宣言は、 val tarako = ( 171 , "Umeda" , 38.018 ) と書いてもかまいませんし、 val tarako = (171, "Umeda", 38.018) と書いてもかまいません。 ただし、空白や改行を単語の途中に挿入することはできません。 たとえば、tarakoという識別子を、t a r a k oと書くことは できません。

2.3---式

Q 2.3.1___式って何ですか。

式というのは、データを処理するという動作をあらわすために 書かれる文字の並びのことです。 定数や識別子は、式の一種です。定数というのは、それが あらわしているデータを求めるという動作をあらわしている式です。 同じように、識別子というのは、それが束縛されているデータを 求めるという動作をあらわしている式です。

Q 2.3.2___値って何ですか。

値というのは、式があらわしている動作をコンピュータが実行した 結果として得られるデータのことです。 式があらわしている動作をコンピュータが実行すると、かならず、 その結果として1個のデータが得られます。定数の場合は それによってあらわされているデータが値になり、識別子の場合は それが束縛されているデータが値になります。

Q 2.3.3___評価って何ですか。

評価というのは、式があらわしている動作をコンピュータが 実行することです。 「評価」という言葉は、「評価する」という動詞の形でも よく使われます。たとえば、「hotarugaikeという識別子を 評価すると、その値として6776が得られる」というような 使い方です(式を評価する主体はコンピュータですから、 「コンピュータに式を評価させる」という言い方のほうが 正確なのですが、あたかも自分が評価するかのような言い方を することがけっこう多いようです)。

Q 2.3.4___対話型システムを使っていて、すでに束縛されている 識別子と同じデータに、別の識別子を束縛したくなったときは どうすればいいのですか。

そんなときは、val宣言のイコールの右側に、すでに束縛されている 識別子を書きます。 Q 2.2.2で少しだけ言及しましたが、val宣言のイコールの右側には、 1個の式を書くことができます。つまり、その場所には定数を 書いてもいいし、識別子を書いてもいいのです。val宣言を 対話型システムに入力すると、コンピュータは、イコールの右側の 式を評価して、イコールの左側の識別子をその式の値に束縛します。 ですから、namakoという識別子がすでに何らかのデータに 束縛されていて、kurageという識別子をそれと同じデータに 束縛したい、というときは、 val kurage = namako というval宣言を対話型システムに入力すればいいわけです。

Q 2.3.5___対話型システムを使っていて、コンピュータに式を 評価させたくなったときはどうすればいいのですか。

そんなときは、その式を対話型システムに入力します。 対話型システムは、式が入力された場合、それを、 val it = 式 の省略形だと解釈します。ですから、コンピュータは、入力された 式を評価して、itという識別子をその式の値に束縛します。 たとえば、namakoという識別子がどんなデータに 束縛されているのかということを調べたい、というときは、 対話型システムに、 namako と入力すればいいわけです。するとそれは、 val it = namako の省略形だと解釈されますので、コンピュータは、namakoという 識別子を式として評価して、itという識別子をその値に束縛します。 もしもnamakoが束縛されているデータが601だったとすると、 対話型システムは、 > val it = 601 : int という応答を出力することになります。 定数は、それがあらわしているデータを求めるという コンピュータの動作をあらわす式です。ですから、 - 8331; > val it = 8331 : int というように、定数だけを対話型システムに入力する、ということも 可能です。

Q 2.3.6___式の値を組の要素にしたいときはどうすれば いいのですか。

そんなときは、組をあらわす式を書きます。 組をあらわす式というのは、式をコンマで区切って並べて、 その全体を丸括弧で囲むことによってできる式です。たとえば、 (hamaguri,(238,asari),751) は、組をあらわす式のひとつの例です。 組をあらわす式のすべての要素が定数になっている場合、それは、 組をあらわす定数になります。つまり、組をあらわす 定数というのは、組をあらわす式の一種だったわけです。 組をあらわす式は、式の一種ですから、コンピュータに 評価させることができます。組をあらわす式の値は、それに 含まれているすべての式を評価することによって得られた値を 要素とする組です。たとえば、hamaguriが103に、asariが (202,911)に束縛されているとするとき、上に書いた式を コンピュータに評価させると、(103,(238,(202,911)),751)という 定数であらわされる組が値として得られます。

Q 2.3.7___要素の番号を指定することによって、組から要素を 取り出すことは可能ですか。

はい、可能です。 組の要素を、その番号を指定することによって取り出したいときは、 # 番号 式 という形の式を書きます。この中の「式」というところには、要素を 取り出したい組を値とする式を書き、「番号」というところには、 取り出したい要素の番号(左から順番に、1、2、3、4、……と 数えます)を書きます。 この形の式をコンピュータに評価させると、コンピュータは、 その中の式を評価して、その式の値として得られた組から、 指定された番号の要素を取り出して、それを式の値にします。 たとえば、 #4 (503,2121,887,3091,~366) という式をコンピュータに評価させると、4番目の要素である3091が 値として得られます。 同じように、nikujagaという識別子が、 (1805,664,6060,543,~919,111,28) という組に束縛されているとするとき、対話型システムに、 val yakisoba = #3 nikujaga というval宣言を入力したとすると、nikujagaの3番目の要素は 6060ですので、 > val yakisoba = 6060 : int という応答が出力されることになります。

2.4---パターン

Q 2.4.1___パターンって何ですか。

パターンというのは、識別子と定数を組み合わせることによって 作られた、データの構造をあらわしている文字の並びのことです。 特定のデータという構造(「構造」という言葉のイメージからは 離れていますが、これも立派な構造です)をあらわすパターンは、 そのデータをあらわす定数そのものです。たとえば、3035という 特定のデータが持っている構造は、3035という定数によって あらわされます。 どんな構造のデータでもいいという構造をあらわすパターンは、 1個の識別子です。たとえば、kurageという1個の識別子は、 まったくの任意の構造をあらわすパターンです。 限定された構造をあらわすパターンは、識別子と定数を丸括弧や コンマなどを使って組み合わせることによって作ります。たとえば、 ちょうど2個の要素を持つ組という構造は、 (x,y) というパターンによってあらわされます(xとyのところは、どんな 識別子でもかまいません)。同じように、1個目の要素が2017という 整数で、2個目の要素がちょうど3個の要素を持つ組であるような、 2個の要素から構成される組、という構造は、 (2017,(x,y,z)) というパターンによってあらわされます。

Q 2.4.2___パターンとデータとを照合するっていうのは どういうことですか。

パターンとデータとを照合するというのは、パターンが あらわしている構造とデータが持っている構造とが 一致しているかどうかを調べることです。 パターンとデータとを照合して、それらの構造が一致していた場合、 その照合は「成功した」と言われ、そうでなかった場合は 「失敗した」と言われます。

Q 2.4.3___データの中に含まれているデータに識別子を 束縛するためにはどうすればいいのですか。

val宣言のイコールの左側に、その識別子を含むパターンを 書きます。 Q 2.2.2でちょっとだけ触れたように、val宣言のイコールの 左側には1個のパターンを書くことができます。コンピュータに val宣言を実行させると、コンピュータは、イコールの左側の パターンとイコールの右側の式の値とを照合して、それが 成功したならば、パターンの中に書かれている識別子をそれに 対応するデータに束縛します。 今、futatsuという識別子が2個の要素を持つ組(たとえば (335,607)とか("Schubert",(2.11,6.13))など)に 束縛されているとします。このとき、ikkomeという識別子を 1個目の要素に、nikomeという識別子を2個目の要素に束縛したい、 という場合は、 val (ikkome,nikome) = futatsu というval宣言を書きます。そうすることによって、もしもfutatsuが 束縛されている組が(335,607)だったとすると、 - val (ikkome,nikome) = futatsu; > val ikkome = 335 : int val nikome = 607 : int というように識別子の束縛が実行されます。

Q 2.4.4___val宣言を実行したとき、イコールの左側と右側との 照合が失敗した場合はどうなるのですか。

その場合、そのval宣言は実行できなくなります。 たとえば、mittsuという識別子が(26,81,17)という3個の要素を 持つ組に束縛されているときに、 val (ikkome,nikome) = mittsu というval宣言を対話型システムに入力したとすると、 パターンとデータとの照合が失敗するためにこのval宣言は 実行できなくなりますので、対話型システムは、そのことを 通知するエラーメッセージを出力します。

Q 2.4.5___データの中に含まれているいくつかのデータのうちで、 必要なものと必要ではないものとがあるのですが、識別子の束縛が 必要なデータだけについて実行されるようにすることは できないのですか。

それは可能です。匿名変数というものを使うことによって、 パターンとデータの構造が一致したとしても束縛が 実行されないようにすることができます。 匿名変数というのは、1個のアンダースコア(_)だけから構成される 識別子のことです。この識別子は、パターンの中に書かれた場合、 普通の識別子と同じように任意の構造をあらわすことに なるのですが、照合が成功したとしても、対応するデータには 束縛されません。 たとえば、futatsuという識別子が2個の要素を持つ組に 束縛されているとき、ikkomeという識別子をその1個目の要素に 束縛する必要があるのだけれども、2個目の要素については束縛の 必要性はない、という場合は、 val (ikkome,_) = futatsu というように、2個目の要素に対応する場所には匿名変数を 書くことができます。もしもfutatsuが(321,654)に束縛されていた とすると、ikkomeは321に束縛されますが、匿名変数が654に 束縛されるということはありません。

Q 2.4.6___照合の対象となるデータの中に束縛の必要性のない部分が 2箇所以上ある場合、それらに対応する複数の匿名変数を1個の パターンの中に書いてもいいのですか。

はい、かまいません。 原則として、1個のパターンの中に同一の識別子を2個以上書く ということはできないのですが、匿名変数だけはその例外で、1個の パターンの中に何個でも書くことができます。 ですから、mittsuという識別子が3個の要素を持つ組に 束縛されているとき、sankomeという識別子をその3個目の要素に 束縛したい、という場合は、 val (_,_,sankome) = mittsu というval宣言を書けばいいわけです。

---練習問題

2.1___次の文字の列は識別子として正しいですか。正しくない ものについては、その理由も答えてください。

(a) dvi2ps (b) 12monkeys (c) the_queen's_bench (d) <=:=> (e) umeda=>ikeda (f) ^^;

2.2___次のval宣言をMLの対話型システムに入力したとき、 対話型システムがどのような応答を出力するか、 予想してください。

(a) val tact = 4081 (b) val therblig = ("Frank Bunker Gilbreth",1911) (c) val scalogram = #2 (1916,37.804,"Guttman",#"F") (d) val x = #3 ((601,833),(112,979),(400,520),(131,188)) (e) val (resolve,conflict) = ("Kurt Lewin",1948) (f) val ((x,y),z) = (("Barker","Gump"),1964) (g) val (x,y) = ("Ruth Fulton Benedict",(1887,1948)) (h) val (introversion,_) = ("Carl Gustav Jung",1959) (i) val (_,x,_) = ("David Shakow","Anatol Rapaport",1964) (j) val (x,(_,y),_) = (1978,(7e~7,#"G"),"Jean Piaget")

第3章===関数適用

3.1---関数についての基礎知識

Q 3.1.1___関数って何ですか。

関数というのは、動作をあらわすデータのことです。 もう少し厳密に言うと、関数は、最初に1個のデータを受け取って、 そのデータに対して何らかの処理を実行して、最後に1個のデータを 返す、という動作をあらわすデータです。 関数というのはデータの一種ですから静的な存在なのですが、でも 普通は、それ自体が動作をする主体であるかのようなイメージで 考えます。ですから、「関数が何々する」とか「何々する関数」 という言い方がよく使われます。

Q 3.1.2___引数(ひきすう)って何ですか。

引数というのは、関数が処理を実行する前に受け取るデータの ことです。

Q 3.1.3___戻り値って何ですか。

戻り値というのは、関数が処理を実行したあとで返すデータの ことです。

Q 3.1.4___関数をデータに適用するっていうのは どういうことですか。

関数をデータに適用するというのは、そのデータを引数として 関数に渡して、関数に処理を実行させることです。

Q 3.1.5___定義域とか値域とかっていうのは何のことですか。

定義域というのは引数の型のことで、値域というのは戻り値の型の ことです。 どんな関数でも、定義域と値域とが厳密に定まっています。 たとえば、定義域がintであるような関数をrealやstringのデータに 適用することはできませんし、値域がrealであるような関数が intのデータを返したりstringのデータを返したりすることは ありません。

Q 3.1.6___関数はどんな型を持っているのですか。

関数は、定義域と値域とを組み合わせることによって作られる型を 持ちます。そのような型は「関数型」と呼ばれます。

Q 3.1.7___関数型はどのような型式であらわされるのですか。

関数型をあらわす型式は、マイナスと大なり(->)の左側に定義域の 型式、右側に値域の型式を書くことによって作られます。 たとえば、定義域がstringで値域がintである関数の型は、 string -> intという型式であらわされます。 要素として関数が含まれているような組の型をあらわす型式を作る 場合は、その中の関数型の型式を丸括弧で囲む必要があります。 たとえば、1個目の要素の型がstringで、2個目の要素の型が int -> stringで、3個目の要素の型がrealであるような、3個の 要素を持つ組の型をあらわす型式は、 string * (int -> string) * real になります。それに対して、 string * int -> string * real という、括弧のない型式は、定義域がstring * intで値域が string * realであるような関数型をあらわします。

Q 3.1.8___関数をデータに適用したいときはどうすれば いいのですか。

そんなときは関数適用というものを書きます。 関数適用というのは、 式1 式2 という構文を持つ式のことです。この形の式をコンピュータに 評価させると、コンピュータは、まず式2を評価して、次に式1を 評価します(式1の値は関数でないといけません)。次に、式1の 値として得られた関数を式2の値に適用します。そして、その関数が 返した戻り値を、関数適用全体の値にします。 具体的な例で考えてみることにしましょう。sizeという識別子が、 文字列の長さ(含まれている文字の個数)を求める関数(型は string -> int)に束縛されているとします。たとえば、 "wasabi"という文字列は、6個の文字から構成されていますので、 sizeを"wasabi"に適用すると、sizeは、6という整数を戻り値として 返すことになります。 さて、それでは、sizeという関数を"wasabi"という文字列に 適用したいときは、どのような関数適用を書けばいいのでしょうか。 sizeを"wasabi"に適用する関数適用は、 size "wasabi" と書くことができます。この関数適用をコンピュータに 評価させると、コンピュータは、まず"wasabi"という定数を 評価して、次にsizeという識別子を評価して、次にsizeの値として 得られた関数を"wasabi"の値に適用します。すると、sizeは 戻り値として6という整数を返しますので、その整数が、 関数適用全体の値になります。 ですから、sizeという関数が定義されているとすると、 - size "wasabi"; というように、関数適用を対話型システムに入力することによって、 > val it = 6 : int という応答を得ることができます。 it以外の識別子を関数適用の値に束縛したいというときは、 省略形ではないval宣言の中に関数適用を書きます。たとえば、 asariという識別子をsize "mayonnaise"という関数適用の値に 束縛したいときは、 val asari = size "mayonnaise" というval宣言を対話型システムに入力すればいいわけです。

Q 3.1.9___関数適用の値に対して関数を適用したいときは どうすればいいのですか。

そんなときは関数適用の中に関数適用を書きます。 関数適用の値に対して関数を適用したいときは、 式1 ( 式2 式3 ) という形の式を書きます。このような式をコンピュータに 与えると、コンピュータは、まず式3の値に式2の値を適用して、 その値として得られたデータに対して、式1の値を適用します。 それでは、具体的な式を書いてみましょう。realという識別子が、 整数を、それと同じ数値をあらわす実数に変換する関数(型は int -> real)に束縛されているとします。たとえば、3という整数に realを適用すると、realは、その整数と同じ数値をあらわす 3.0という実数を戻り値として返します。 sizeという識別子が、文字列の長さを求める関数に束縛されていて、 realという識別子が、整数を実数に変換する関数に 束縛されているとするとき、それらの関数を使って、 "mustard"という文字列の長さを求めて、その結果を実数に変換した 結果を求めたいというときは、どのような式を書けば いいのでしょうか。 その場合は、まずsizeを"mustard"に適用して、sizeが返した 戻り値をrealで実数に変換すればいいわけですから、 real (size "mustard") という式を書きます。sizeとrealが定義されているとするならば、 対話型システムにこの式を入力することによって、 > val it = 7.0 : real という応答が得られるはずです。 関数適用の値に対して関数を適用する場合、 式1 ( 式2 式3 ) という式の中の、式2と式3を囲む丸括弧は、省略できません。 丸括弧を省略して、 式1 式2 式3 という式を書いたとすると、 式1 式2 という関数適用の値として得られた関数を式3の値に適用する、 という意味だと解釈されてしまいます。たとえば、 real size "mustard" という式は、realをsizeに適用して、その結果を"mustard"に 適用する、という意味になります。

Q 3.1.10___関数に束縛されている識別子を式として評価することは 可能ですか。

はい、可能です。 たとえば、sizeという識別子が関数に束縛されているとすると、 対話型システムに、 - size; と入力することによって、コンピュータにsizeという識別子を 評価させることができます。この場合、値として得られるのは、 sizeが束縛されている関数そのものです。 ただし、残念ながら、関数をあらわす定数というものは 存在しませんので、MLの対話型システムは、入力された式の値が 関数だった場合は、定数の代わりにfnという単語を出力します。 ですから、sizeという識別子が、string -> intという型を持つ 関数に束縛されているとすると、その識別子を対話型システムに 入力することによって、 > val it = fn : string -> int という応答を得ることができます。

3.2---組み込み関数

Q 3.2.1___組み込み関数って何ですか。

組み込み関数というのは、処理系に付属している関数のうちで、 1個の識別子で指定できるもののことです。 MLの場合、プログラムを書くというのは、関数をいくつも 作っていくというのとほとんど同じ意味だと考えることができます。 関数は、第4章で説明することになる方法で 作ることができるのですが、まったく何もないところから新しい 関数を作るということは、ほとんどありません。新しい関数は、 普通、すでに存在する別の関数を組み合わせることによって 作られます。そして、関数を作っていくための基本的な材料として 最初に与えられているのが、処理系に付属している関数なのです。 MLの処理系に付属している関数は、基本的には、 識別子.識別子 識別子.識別子.識別子 というように、2個以上の識別子をドット(.)で結合したものによって 指定されるのですが、いくつかの関数は、左側の識別子とドットを 省略して、1個の識別子だけで指定できるようになっています。 この文章の中では、「組み込み関数」という言葉は、「MLの処理系に 付属している関数のうちで、1個の識別子で指定できるもの」という 意味で使うことにします。 MLの処理系には、たくさんの関数が付属しているのですが、 それらのうちで、1個の識別子で指定することのできるもの、 つまり組み込み関数は、全体のほんの一部分にすぎません。 識別子.識別子とか、識別子.識別子.識別子という形のものを 書くことによって利用することのできる関数の中にも、とても便利な 関数がたくさん含まれています(付録Bで、MLの処理系に 付属している関数の一部を紹介していますので、 参照してみてください)。 なお、2個以上の識別子をドットで結合したもの、という構文が どういう意味なのかということについては、第12章で 説明することにします。

Q 3.2.2___組み込み関数にはどんなものがあるのですか。

組み込み関数には、real、trunc、floor、ceil、round、abs、~、 size、print、chr、ord、str、……などがあります。 それでは、組み込み関数を、いくつか 紹介していくことにしましょう(それぞれの関数の見出しは、 コロン(:)の左側が関数の名前、右側が関数の型を あらわしています)。 real : int -> real realは、整数を、同じ数値をあらわす実数に変換する関数です。 たとえば、71という整数にrealを適用したとすると、realは、 - real 71; > val it = 71.0 : real というように、71.0という実数を返します。 trunc : real -> int floor : real -> int ceil : real -> int round : real -> int これらの関数は、実数を、それとほとんど等しい整数に変換します。 ただし、これらの関数の動作は、それぞれ微妙に異なっています。 truncは、小数点の右側を単純に切り捨てる関数です。たとえば、 truncを7.3に適用すると結果は7になり、~7.3に適用すると結果は ~7になります。 floorとceilは、床と天井という名前が示しているとおり、対照的な 動作をする関数です。floorは、引数を上回らない最大の整数を 求め、ceilは、引数を下回らない最小の整数を求めます。たとえば、 これらの関数を7.3と~7.3に適用すると、 7.3 ~7.3 floor 7 ~8 ceil 8 ~7 という結果が得られます。 roundは、0.1の桁を四捨五入することによって求まる整数を返す 関数です。たとえば、7.4、7.5、~7.4、~7.5のそれぞれにroundを 適用すると、 7.4 7.5 ~7.4 ~7.5 round 7 8 ~7 ~8 という結果が得られます。 abs : int -> int abs : real -> real absは、引数の絶対値を求める関数です。たとえば、absは、~3に 適用された場合は3を返し、3に適用された場合は3をそのまま 返します。 absという名前を持つ関数は二つあって、intのデータに対しては int -> intという型のabsが適用され、realのデータに対しては real -> realという型のabsが適用されます。このように、2個以上の 関数が同一の名前を持っている場合、それらの関数は 「多重定義されている」と言われます。 ~ : int -> int ~ : real -> real ~は、引数の符号(プラスかマイナスか)を反転させる関数です。 たとえば、~を3に適用すると戻り値は~3になり、~3に適用すると 戻り値は3になります。 size : string -> int sizeは、文字列の長さ(含まれている文字の個数)を求める 関数です。たとえば、"wasabi"という文字列にsizeを 適用したとすると、sizeは、 - size "wasabi"; > val it = 6 : int というように、その文字列の長さである、6という整数を返します。 print : string -> unit printは、文字列を出力する関数です(出力された文字列は、普通は モニターの画面に表示されます)。たとえば、"karashi"という 文字列にprintを適用したとすると、printは、 - print "karashi"; karashi> val it = () : unit というように、モニターの画面に"karashi"という文字列を 表示します(二重引用符は表示しません)。 改行という文字(#"\n")を含んでいる文字列を、printを使って 出力した場合、モニターの画面の上では、改行という文字は、 \nではなく、次の行に進むという形によってあらわされます。 たとえば、"mirin\nponzu\n"という文字列にprintを適用する式を 対話型システムに入力すると、そのあとの画面は、 - print "mirin\nponzu\n"; mirin ponzu > val it = () : unit というようになります。 chr : int -> char chrは、整数に適用されると、その整数をあらわすビット列の パターンと同じパターンによってあらわされる文字を返す関数です。 たとえば、77という整数にchrを適用したとすると、chrは、 - chr 77; > val it = #"M" : char というように、77と同じく、4Dというビット列のパターンで あらわされる#"M"という文字を返します。 ord : char -> int ordは、文字に適用されると、その文字をあらわすビット列の パターンと同じパターンによってあらわされる整数を返す関数です。 たとえば、#"M"という文字と77という整数は、どちらも4Dという ビット列のパターンであらわされますので、#"M"にordを 適用すると、ordは、 - ord #"M"; > val it = 77 : int というように、77を返します。 str : char -> string strは、文字を文字列に変換する関数です。たとえば、#"d"という 文字にstrを適用すると、strは、 - str #"d"; > val it = "d" : string というように、"d"という文字列を返します。

Q 3.2.3___組み込み関数の名前というのは予約語なんですか。

いいえ、予約語ではありません。 組み込み関数の名前というのは、組み込み関数に束縛されている 普通の識別子です。ですから、組み込み関数の名前を別のデータに 束縛してもかまいません。たとえば、 - val size = 8055; > val size = 8055 : int というように、sizeという組み込み関数の名前を別のデータに 束縛したとしても、エラーにはなりません。

Q 3.2.4___副作用って何ですか。

副作用というのは、状態の変化を引き起こす動作のことです。 たとえば、データを出力するという動作は副作用の一例です。 なぜなら、コンピュータが何らかのデータを出力することによって、 そのデータがまだ出力されていない状態からすでに 出力されてしまった状態へ、という変化が引き起こされるからです。 動作の中に副作用を含んでいる関数のことを「副作用のある関数」 と言ったり、評価にともなう動作が副作用を含んでいる式のことを 「副作用のある式」と言ったりすることがある、ということも 覚えておいてください。たとえば、「printというのは副作用のある 関数だ」とか、「print "mikan"というのは副作用のある式だ」 というような言い方をすることが、しばしばあります。

Q 3.2.5___整数や実数を、同じ数値をあらわす文字列に 変換したいのですが、そんなときはどうすればいいのですか。

整数を文字列に変換したいときはInt.toStringという関数を使い、 実数を文字列に変換したいときはReal.toStringという関数を 使います。 MLの処理系には、整数や実数を文字列に変換する関数も 付属しているのですが、残念ながら、それらは 組み込み関数ではありませんので、それらの関数を 利用したいときは、識別子.識別子という形のものを 書く必要があります。 Int.toString(型はint -> string)は、整数に適用されると、 それと同じ数値をあらわす文字列を戻り値として返す関数です。 たとえば、471という整数にInt.toStringを適用すると、 Int.toStringは、 - Int.toString 471; > val it = "471" : string というように、その整数と同じ数値をあらわす文字列を、 戻り値として返します。 同じように、Real.toString(型はreal -> string)は、実数に 適用されると、それと同じ数値をあらわす文字列を戻り値として返す 関数です。たとえば、6.03という実数にReal.toStringを 適用すると、Real.toStringは、 - Real.toString 6.03; > val it = "6.03" : string というように、6.03という数値をあらわす文字列を返します。

3.3---演算子

Q 3.3.1___演算子って何ですか。

演算子というのは、MLの処理系が構文を解析するときに 特別扱いされるような設定を施されている識別子のことです。 MLの処理系は、 式1 演算子 式2 という形の式を、この中の演算子に束縛されている関数を (式1,式2)という式の値に対して適用する、という意味だと 解釈します。 たとえば、func1というのは普通の識別子で、int * intを 定義域とする関数に束縛されているとしましょう。 このとき、(345,701)という組にfunc1を適用する式は、 func1 (345,701) と書けばいいわけです。 それに対して、func2というのが演算子で、int * intを 定義域とする関数に束縛されているとすると、その関数を (345,701)に適用する式は、 345 func2 701 と書くことになります。 なお、演算子が束縛されている関数のことを「演算子」と 呼ぶこともあります。

Q 3.3.2___左辺とか右辺とかっていうのは何のことですか。

左辺というのは演算子の左に書かれている式(またはその値)の ことで、右辺というのは演算子の右に書かれている式(またはその 値)のことです。 つまり、演算子を組に適用する、 式1 演算子 式2 という式があるとするとき、その中に含まれている式1(または その値)のことを「左辺」と言い、式2(またはその値)のことを 「右辺」と言う、ということです。

Q 3.3.3___組み込み演算子って何ですか。

組み込み演算子というのは、組み込み関数に束縛されている演算子、 または演算子を名前として持っている組み込み関数のことです。

Q 3.3.4___演算子を名前として持っている組み込み関数には どんなものがあるのですか。

演算子を名前として持っている組み込み関数には、+、-、*、/、 div、mod、^、……などがあります。 それでは、演算子を名前として持っている組み込み関数を、いくつか 紹介していくことにしましょう(それぞれの関数の見出しは、 コロン(:)の左側が関数の名前、右側が関数の型を あらわしています)。 + : int * int -> int + : real * real -> real + : word * word -> word +は、左辺と右辺とを加算する関数です。たとえば、5+2という式の 値は7になり、3.6+6.8の値は10.4になります。 なお、定義域がint * realとかreal * intであるような+は 多重定義されていませんので、5+6.8とか3.6+2というような式は エラーになります。 - : int * int -> int - : real * real -> real - : word * word -> word -は、左辺から右辺を減算する関数です。たとえば、5-2という式の 値は3になり、3.6-6.8の値は~3.2になります。 * : int * int -> int * : real * real -> real * : word * word -> word *は、左辺と右辺とを乗算する関数です。たとえば、5*2という式の 値は10になり、3.6*6.8の値は24.48になります。 / : real * real -> real /は、左辺を右辺で除算したときの商を求める関数です。たとえば、 24.48/6.8という式の値は3.6になります。 div : int * int -> int div : word * word -> word divは、整数の範囲で、左辺を右辺で除算したときの商を求める 関数です。たとえば、59 div 13という式の値は4になります。 mod : int * int -> int mod : word * word -> word modは、整数の範囲で、左辺を右辺で除算したときのあまりを 求める関数です。たとえば、59 mod 13という式の値は7になります。 ^ : string * string -> string ^は、左辺の右側に右辺を連結する関数です。たとえば、 "reba" ^ "nira"という式の値は"rebanira"になります。

Q 3.3.5___普通の関数適用の構文を使って演算子をデータに 適用したいのですが、どうすればいいのですか。

そんなときはopという予約語を使います。 演算子というのは、単独では式になることができないのですが、 op 演算子 という式を書くことができて、この式を評価すると、この中の 演算子が束縛されている関数が値として得られます。たとえば、 対話型システムに、 op^ というものを入力したとすると、 > val it = fn : string * string -> string という応答が出力されます。 ですから、 op 演算子 式 という形の関数適用を書くことによって、演算子をデータに 適用することが可能です。たとえば、 op^ ("poly","ethylene") という関数適用を書くことによって、"poly"と"ethylene"とを 連結することができます。同じように、futatsuという識別子が、 2個の整数から構成される組に束縛されているとするとき、 op mod futatsu という関数適用で、1個目の整数を2個目の整数で除算したときの あまりを求めることができます。

3.4---優先順位と結合規則

Q 3.4.1___exp1、exp2、exp3が式で、opr1、opr2が演算子だと するとき、 exp1 opr1 exp2 opr2 exp3 という式はどのように解釈されるのですか。

そのような式は、opr1とopr2が持っている優先順位というものに したがって解釈されます。 すべての演算子は、「優先順位」と呼ばれる0またはプラスの 整数を、自分の属性として持っています。演算子は、その優先順位が 大きいほど、強い力で左右の式と結合します。たとえば、7という 優先順位を持つ演算子は、4という優先順位を持つ演算子よりも 左右の式と結合する力が強い、ということになります。 「優先順位」という言葉からは、「数値が小さいほど強い」という 印象を受けますが、MLの演算子に関してはその逆で、優先順位は 「数値が大きいほど強い」ですので、 間違えないようにしてください。 ですから、 exp1 opr1 exp2 opr2 exp3 という式は、opr1がopr2よりも優先順位の大きい演算子だとすると、 op opr2 (op opr1 (exp1,exp2),exp3) という意味だと解釈されます。逆に、opr2のほうがopr1よりも 優先順位が大きい場合は、 op opr1 (exp1,op opr2 (exp2,exp3)) と解釈されます。

Q 3.4.2___組み込み関数に束縛されている演算子は、どのような 優先順位を持っているのですか。

組み込み関数に束縛されている演算子のうちで、Q 3.3.4で 紹介したものの優先順位は、+と-と^が6で、*と/とdivとmodが 7です。 ですから、 5*3+7 という式は、*のほうが+よりも強力に左右の式と結合しますので、 op+ (op* (5,3),7) という意味になります。同じように、 22 - 108 mod 13 という式は、modのほうが-よりも優先順位が大きいですから、 op- (22,op mod (108,13)) と解釈されることになります。

Q 3.4.3___exp1、exp2、exp3が式で、opr1とopr2とが等しい 優先順位を持つ演算子だとするとき、 exp1 opr1 exp2 opr2 exp3 という式はどのように解釈されるのですか。

そのような式は、opr1とopr2が持っている結合規則にしたがって 解釈されます。 すべての演算子は、「結合規則」と呼ばれる規則を持っています。 結合規則には、「左結合」と「右結合」という二つの種類が あります。左結合の演算子は、左に書かれているものほど左右の式と 結合する力が強くなり、右結合の演算子は、それとは逆に、右に 書かれているものほど強い結合力を持ちます。 ですから、opr1とopr2とが等しい優先順位を持つ演算子だと するとき、 exp1 opr1 exp2 opr2 exp3 という式は、opr1とopr2が左結合の演算子ならば、 op opr2 (op opr1 (exp1,exp2),exp3) という意味だと解釈されます。そうではなくて、opr1とopr2が 右結合の演算子だとすると、この式は、 op opr1 (exp1,op opr2 (exp2,exp3)) と解釈されます。

Q 3.4.4___組み込み関数に束縛されている演算子は、どのような 結合規則を持っているのですか。

組み込み関数に束縛されている演算子のうちで、Q 3.3.4で 紹介したものの結合規則は、すべて左結合です。 ですから、たとえば、 310 mod 17 * 5 という式は、 op* (op mod (310,17),5) という意味になります。

Q 3.4.5___演算子を含んでいる式に関して、優先順位や結合規則とは 無関係に、特定の解釈を強要したいときはどうすればいいのですか。

そんなときは、ひとつの式だと解釈してほしい部分を丸括弧で 囲みます。 exp1、exp2、exp3が式で、opr1とopr2が演算子だとするとき、 opr1とopr2がどのような優先順位や結合規則を持っていたとしても、 ( exp1 opr1 exp2 ) opr2 exp3 という式は、 op opr2 (op opr1 (exp1,exp2),exp3) と解釈され、 exp1 opr1 ( exp2 opr2 exp3 ) という式は、 op opr1 (exp1,op opr2 (exp2,exp3)) と解釈されます。 ですから、たとえば、7から、5と3とを加算した結果を減算したい、 というときは、 7-(5+3) という式を書けばいいわけです。

Q 3.4.6___exp1、exp2、exp3が式で、oprが演算子だとするとき、 exp1 exp2 opr exp3 とか、 exp1 opr exp2 exp3 とかっていう式はどのように解釈されるのですか。

関数適用を構成する二つの式は、どんな演算子よりも強い力で 結合します。 ですから、 exp1 exp2 opr exp3 は、oprの左辺にexp1 exp2という関数適用がある、と解釈され、 exp1 opr exp2 exp3 は、oprの右辺にexp2 exp3という関数適用がある、と解釈されます。 たとえば、 abs a * b という式は、aの絶対値とbとを乗算する、という意味になり、 a * abs b という式は、aと、bの絶対値とを乗算する、という意味になります。 演算子と関数適用とが組み合わさっているような式についても、 特定の解釈を強要したいときは、やはり丸括弧を使います。 たとえば、aとbとを乗算した結果の絶対値を求める式は、 abs (a * b) というように、括弧を使って書くことができます。

---練習問題

3.1___次の式をMLの対話型システムに入力したときに、 その結果としてどのようなものが画面に表示されるか、 予想してください。

(a) real 35 (b) trunc ~6.4 (c) abs ~10.8 (d) ~ ~71 (e) size "Pleiades" (f) print "Hyades" (g) print "Capella\nMenkalinan\nAl Maaz\n" (h) str #"+" (i) Int.toString ~5738 (j) Real.toString 9.133 (k) "Procyon" ^ "Gomeisa" (l) ord (chr 100) (m) chr (ord #"d") (n) print (Int.toString 6010) (o) print (Int.toString (size "Canopus")) (p) size (Int.toString (7012*3883)) (q) "NGC" ^ Int.toString 2158 (r) print (str #"M" ^ Int.toString 35)

3.2___次の動作をあらわす式を書いてください。

(a) betelgeuseという識別子の値である整数を実数に変換した結果を 求める。 (b) rigelという識別子の値である実数を上回らない最大の整数を 求める。 (c) bellatrixという識別子の値である整数の絶対値を求める。 (d) mintakaという識別子の値である実数の符号を反転させた結果を 求める。 (e) alnilamという識別子の値である文字列の長さを求める。 (f) alnitakという識別子の値である文字列を出力する。 (g) saiphという識別子の値である整数をあらわすビット列の パターンと同じパターンによってあらわされる文字を求める。 (h) trapeziumという識別子の値である文字をあらわすビット列の パターンと同じパターンによってあらわされる整数を求める。 (i) meissaという識別子の値である文字を文字列に変換した結果を 求める。 (j) algenibという識別子の値である整数と同じ数値をあらわす 文字列を求める。 (k) algolという識別子の値である文字列の右側に、menkhibという 識別子の値である文字列を連結した結果を求める。 (l) miramという識別子の値である整数をあらわす文字列を 出力する。 (m) schedarという識別子の値である文字をあらわすビット列の パターンと同じパターンによってあらわされる整数をあらわす 文字列を出力する。 (n) chaphという識別子の値である文字列の右側に、cihという 識別子の値である整数をあらわす文字列を連結した結果を求める。 (o) siriusという識別子の値である文字列の右側に、mirzamという 識別子の値である文字列を連結した結果を出力する。

3.3___$、%、!、**、//という識別子が演算子であって、それぞれの 優先順位と結合規則が、 演算子 優先順位 結合規則 $ 4 左 % 5 左 ! 5 左 ** 6 右 // 6 右 というように定められているとするとき、次の式の中に 含まれている、 式1 演算子 式2 という形の式を、すべて、 op 演算子 (式1,式2) という形に書き換えてください。

(a) pully $ fossil (b) sulphur $ leviathan % trepang (c) mucus % griffin $ monolith (d) marsupial % taciturnity ** exoneration (e) persimmon ** shamrock % obsidian (f) skepticism % providence ! inscription (g) valerian ! saturation % heteronym (h) orangutan ** cephalopod // dulcimer (i) alluvium // hyacinth ** lithosphere

第4章===関数の定義

4.1---fun宣言

Q 4.1.1___関数を定義するっていうのはどういうことですか。

関数を定義するというのは、新しい関数を作って、識別子をそれに 束縛するということです。 関数というのはデータの一種ですから、「関数を定義する」 というのは、識別子をデータに束縛するという動作の バリエーションだと考えることができます。

Q 4.1.2___MLの対話型システムを使っていて、関数を 定義したくなったときはどうすればいいのですか。

そんなときは、対話型システムにfun宣言というものを入力します。 fun宣言というのは、 fun 識別子 パターン = 式 という構文を持つ記述のことです。fun宣言を対話型システムに 入力すると、コンピュータは、funという単語の右側に書かれた 識別子を、 パターン = 式 という部分であらわされる関数に束縛します。その関数というのは、 (1) イコールの左側のパターンと引数とを照合する。 (2) 照合が成功したならば、パターンの中の識別子を、それに 対応するデータに束縛する。 (3) イコールの右側の式を評価する。 (4) その式の値を戻り値として返す。 という動作をします。 たとえば、 fun sanbai n = n*3 というfun宣言を対話型システムに入力したとすると、 コンピュータは、 (1) nというパターンと引数とを照合する。 (2) nという識別子を引数に束縛する。 (3) n*3という式を評価する。 (4) その式の値を戻り値として返す。 という動作をする関数(型はint -> int)を作って、sanbaiという 識別子をその関数に束縛します。 ですから、そののち、対話型システムに、 sanbai 7 という式を入力することによって、sanbaiを7に適用したとすると、 nが7に束縛されたのちにn*3という式が評価されて、その値として 得られた21が戻り値として返ってくることになりますので、 対話型システムは、 val it = 21 : int という応答を出力するはずです。

Q 4.1.3___2個以上のデータを引数として受け取る関数を 定義したいときはどうすればいいのですか。

関数が2個以上のデータを引数として受け取るようにしたいときは、 それらのデータを要素とする1個の組を引数として受け取るように 関数を定義します。 MLでは、関数が受け取ることのできる引数は1個だけです。でも、 関数が1個の組を引数として受け取るということは可能です。 たとえば、台形の上底と下底と高さを受け取って、その台形の面積を 返す、daikeiNoMensekiという関数(型はreal * real * real -> real)は、上底と下底と高さを要素とする1個の組を引数として 受け取るように定義すればいいわけです。ですから、 daikeiNoMensekiを定義するfun宣言は、 fun daikeiNoMenseki (joutei,katei,takasa) = (joutei+katei)*takasa/2.0 と書くことができます。 daikeiNoMensekiを使って台形の面積を求めたいときは、上底と 下底と高さを要素とする組にその関数を適用すれば いいわけですから、たとえば、上底が3.0、下底が7.0、高さが5.0の 台形の面積を求めたいという場合は、 daikeiNoMenseki (3.0,7.0,5.0) という関数適用を書くことになります。

Q 4.1.4___2個以上のデータを戻り値として返す関数を 定義したいときはどうすればいいのですか。

関数が2個以上のデータを戻り値として返すようにしたいときは、 それらのデータを要素とする1個の組を戻り値として返すように 関数を定義します。 MLでは、関数が返すことのできる戻り値は1個だけ、 と決まっています。でも、関数が1個の組を戻り値として 返すということは可能です。たとえば、2個の整数を受け取って、 それらの和と差と積と商を返す、kagenjoujoという関数(型は、 int * int -> int * int *int * int)を定義するfun宣言は、 fun kagenjoujo (n,m) = (n+m,n-m,n*m,n div m) と書くことができます。この関数を(30,7)という組に 適用したとすると、戻り値として、 (37,23,210,4) という組が得られることになります。

Q 4.1.5___実数の2乗を求める関数を定義したいのですが、 fun nijou x = x*x というfun宣言だと、nijouの型がint -> intになってしまいます。 定義域と値域をrealにするためにはどうすればいいのですか。

関数を定義するときに、その関数の型が期待したとおりにならない 場合は、fun宣言の中で使われている識別子に対して、型を明示的に 指定しないといけません。識別子に対して型を明示的に 指定したいときは、その識別子の右側に、 : 型名 というものを書きます。たとえば、実数の2乗を求める関数を 定義したいという場合は、 fun nijou x:real = x*x というfun宣言を書きます。 型の明示的な指定は、fun宣言の中のどこに書いてもかまいません。 ですから、上に書いたnijouのfun宣言は、 fun nijou x = x*x:real と書いたとしても、同じ関数を定義することになります。ただし、 fun nijou x = x:real*x と書いたとすると、 fun nijou x = x:(real*x) と解釈されてしまいますので、*の左側のxに型の指定を 書きたいという場合は、 fun nijou x = (x:real)*x というように、識別子とコロンと型名を丸括弧で囲まないと いけません。

Q 4.1.6___関数の定義というのが、識別子をデータに 束縛するということのバリエーションにすぎないのだとすれば、 val宣言を使って関数を定義することもできるような気が するんですが、いかがでしょうか。

はい、そのとおりです。val宣言を使って関数を定義することは 可能です。 val宣言というのは、 val パターン = 式 という構文のことでした。この構文で書かれた記述は、 (1) イコールの右側の式を評価する。 (2) イコールの左側のパターンと、(1)で求めた式の値とを 照合する。 (3) 照合が成功したならば、パターンの中の識別子を、それに 対応するデータに束縛する。 という動作をあらわしています。だとすれば、val宣言のイコールの 右側に、「新しい関数を作って、それを自分の値にする」という 動作をあらわす式を書くことができれば、val宣言で関数の定義が できることになります。 MLでは、fn式という構文を使うことによって、「新しい関数を 作って、それを自分の値にする式」というものを書くことが できるようになっています。fn式は、 fn パターン => 式 と書きます。この式は、 (1) =>の左側のパターンと引数とを照合する。 (2) 照合が成功したならば、パターンの中の識別子をそれに対応する データに束縛する。 (3) =>の右側の式を評価する。 (4) その式の値を戻り値にする。 という動作をする関数を作って、その関数を値とします。たとえば、 fn n => n*3 というfn式は、1個の整数を受け取って、それを3倍したものを返す、 という動作をする関数を作って、その関数を値とします。ですから、 (fn n => n*3) 7 という関数適用を評価すると、左側のfn式を評価することによって 得られた関数が7に適用されますので、値として21が得られることに なります。 ですから、val宣言を使って関数を定義したいときは、val宣言の イコールの右側にfn式を書けばいいわけです。たとえば、 val sanbai = fn n => n*3 というval宣言は、 fun sanbai n = n*3 というfun宣言とまったく同じ動作(つまり、整数を3倍する関数を 作って、sanbaiという識別子をその関数に束縛する、という動作)を あらわすことになります。

Q 4.1.7___副作用を目的とするいくつかの式を順番に評価する関数を 定義したいのですが、どうすればいいのですか。

そんなときは複合式というものを使います。 複合式というのは式の一種で、 ( 式 ; 式 ; …… 式 ) という構文を持っています。複合式の中のセミコロンは、式と式との あいだの境目を明確にするためのものなので、最後の式のうしろには セミコロンを書かない、という点に注意してください。 複合式をコンピュータに評価させると、コンピュータは、その中の 式を先頭から順番に評価していって、最後にある式の値を 複合式全体の値にします(最後以外の式の値は、完全に 消滅してしまいます)。たとえば、 ( print "Nobody knows "; print "what "; print "programming is."; 275 ) という複合式を評価すると、 Nobody knows what programming is. が出力されるという副作用が引き起こされて、式全体の値は 275になります。 今、副作用のある三つの関数(型はいずれもint -> unit)が 次のように定義されているとします。 fun printNibai n = print (Int.toString (n*2) ^ "\n") fun printHanbun n = print (Int.toString (real n / 2.0) ^ "\n") fun printNijou n = print (Int.toString (n*n) ^ "\n") さて、これらの関数を使って、整数に適用すると、その2倍と 2分の1と2乗をこの順番で出力する、printIroiroという関数(型は int -> unit)を定義するためにはどうすればいいでしょうか。 そんな場合は、複合式を使って、それぞれの関数を整数に適用する 式が順番に評価されるようにします。つまり、 fun printIroiro n = ( printNibai n; printHanbun n; printNijou n ) というようなfun宣言を書けばいいわけです。

4.2---スコープ

Q 4.2.1___複数のfun宣言のそれぞれで、パターンの中に同一の 識別子が使われていたとすると、何か困ったことが起きるのでは ありませんか。

いいえ、困るようなことは何も起きません。 MLの処理系は、fun宣言のパターンの中に書かれている識別子を、 そのfun宣言の中だけで有効なものだとみなします。ですから、 複数のfun宣言のそれぞれで、パターンの中に同一の識別子が 使われていたとしても、ひとつのfun宣言の中の識別子は、ほかの fun宣言の中の識別子とはまったく無関係の存在として 扱われるのです。 具体的な例を使って説明しましょう。3から9までのいずれかの整数に 適用すると、その整数を1の位として持ち、左の桁から右の桁に 向かって1ずつ大きくなっていく3桁の10進数であらわされる整数を 返す、straightという関数を定義したい、とします。たとえば、 straight 7 という関数適用によって、567という整数が得られるように したいわけです。そこで、 fun hyakubai n = n*100 fun juubai n = n*10 fun straight n = hyakubai(n-2)+juubai(n-1)+n というように、三つのfun宣言を書いたとしましょう。 ところで、これらのfun宣言は、いずれも、nという識別子を パターンとして使っています。もしも、MLの処理系が、それぞれの fun宣言の中で使われているnのあいだには密接な関係が あるのだとみなしたならば、straightという関数の動作は、 fun宣言を書いた人間の意図とはかけ離れたものに なっていたでしょう。でも実際には、hyakubaiのnとjuubaiのnと straightのnは、互いに無関係な存在だとみなされますので、 straightの動作が不可解なものになるという心配は無用です。 ですから、fun宣言を書くときに、「この識別子は、ほかの fun宣言を書いたときにパターンの中で使ったものだから、 ここでふたたび使うのはマズいだろうな」などと考えることは まったく無意味です。fun宣言のパターンの中にどんな識別子を 書いたかということは、そのfun宣言の外にはまったく影響を 及ぼさないのです。 識別子はかならず、自分が有効である範囲というものを持っていて、 そのような範囲のことを、普通、「スコープ」と呼びます。 fun宣言の中でパターンとして使われている識別子は、そのfun宣言の 内部がスコープになります。それに対して、fun宣言によって関数に 束縛された識別子や、val宣言のパターンの中に書かれた識別子は、 もっと広いスコープを持つことになります。

Q 4.2.2___fun宣言を書こうとしているのですが、パターンの中で 使われている識別子以外にも、fun宣言の中だけがスコープになる 識別子を作りたいのです。そんなときはどうすればいいのですか。

そんなときはlet式というものを使います。 let式というのは式の一種で、局所的なスコープを持つ識別子を 作りたいときに使われるものです。let式は、 let 宣言 宣言 …… 宣言 in 式 ; 式 ; …… 式 end というように書きます。letとinのあいだには、val宣言や fun宣言などを何個でも書くことができます。inとendのあいだには、 式を何個でも書くことができます。2個以上の式を書くときは、 複合式の場合と同じように、それらの式をセミコロンで区切ります。 let式をコンピュータに評価させると、コンピュータは、まず最初に letとinのあいだに書かれたval宣言やfun宣言を先頭から順番に 実行していきます。そして次に、inとendのあいだに書かれた式を 先頭から順番に評価していきます。そして、最後の式の値を、 let式全体の値にします。 let式の中に書かれたval宣言やfun宣言によってデータに 束縛された識別子は、そのlet式の中だけがスコープになります。 ですから、それらと同じ識別子が別の場所で使われていたとしても、 互いに無関係のものとして扱われることになります。 それでは、let式を使ったfun宣言の例を書いてみましょう。 ひとつ目は、商品の本体価格から税込価格を求める、zeikomiという 関数を定義するfun宣言です。 fun zeikomi hontai = let val rhontai = real hontai val zeiritsu = 0.2 in floor (rhontai+rhontai*zeiritsu) end let式の中に書かれたval宣言によってデータに束縛されている、 rhontaiとzeiritsuという識別子は、このlet式の中だけが スコープになります。ですから、このfun宣言の外で同じ識別子が 使われていたとしても、お互いに影響が及ぶということは ありません。 二つ目は、文字列のうしろに、その文字列の長さを丸括弧で 囲んだものを連結する、nagasaという関数を定義するfun宣言です。 fun nagasa s = let fun kakko s = "(" ^ s ^ ")" in s ^ kakko (Int.toString (size s)) end この例では、文字列を丸括弧で囲むkakkoという関数を、 let式の中で定義しています。このkakkoという識別子は、let式の 中だけがスコープになりますので、その外側には何の影響も 与えません。

Q 4.2.3___let式のletとinのあいだに、副作用を目的とする式を 書こうと思うのですが、val宣言の、 val 識別子 = という部分を省略して、式だけを書いてもかまわないのですか。

いいえ、それはできません。 対話型システムに式だけを入力した場合は、 val it = 式 というval宣言の省略形だと解釈されるのですが、let式のletとinの あいだに式だけを書いてもそのようには解釈されず、 エラーになってしまいます。 let式のletとinのあいだに式を書きたいのだけれども、その式は 副作用だけが目的で、その値は必要ではない、という場合は、 普通、 val _ = 式 というように、匿名変数を使ったval宣言を書きます。 例として、文字列に適用すると、その文字列と改行を出力して、 さらにその文字列を戻り値として返す、teeという関数を 定義してみましょう。この関数は、複合式を使って定義するほうが 自然ですが、どうしてもlet式のletとinのあいだに文字列を 出力する式を書きたいのだとします。そんな場合は、 fun tee s = let val _ = print (s ^ "\n") in s end というように、匿名変数を使ったval宣言をletとinのあいだに 書けばいいわけです。

4.3---プログラムのファイル

Q 4.3.1___MLの対話型システムにval宣言やfun宣言を 入力することによって識別子をデータに束縛したとしても、 対話型システムを終了させてからふたたび起動すると、 以前の束縛は無効になってしまっています。val宣言やfun宣言を 何度も入力しなくてもいいようにする方法はないのですか。

あります。val宣言やfun宣言をファイルに入れておいて、ファイルの 中のものをコンピュータに実行させればいいのです。 ファイルを指定して、その中のものをコンピュータに実行させたい ときは、useという組み込み関数を使います。ファイルのパス名 (型はstring)にuseを適用すると、useは、そのファイルの中に 書かれているval宣言やfun宣言を、上から順番にひとつずつ コンピュータに実行させていきます。 たとえば、カレントディレクトリ(現在の作業場所となっている ディレクトリ)にあるkarasu.smlという名前のファイルの中に 書かれているval宣言やfun宣言をコンピュータに実行させたい、 という場合は、 use "karasu.sml" というval宣言を対話型システムに入力します。 カレントディレクトリ以外のディレクトリにあるファイルを 指定したいときは、ディレクトリの名前とファイルの名前を 組み合わせることによってパス名を作ります。たとえば、 ルートディレクトリの下のyasaiというディレクトリにある nasubi.smlというファイルの中のものをコンピュータに 実行させたいとしましょう。オペレーティングシステムとして UNIXを使っている場合、val宣言は、 use "/yasai/nasubi.sml" と書きます。使っているオペレーティングシステムがDOSの場合は、 use "\\yasai\\nasubi.sml" というval宣言を書きます(バックスラッシュを\\というように 二重に書く理由については、Q 1.3.9を参照してください)。

Q 4.3.2___宣言って何ですか。

宣言というのは、単独で、MLの処理系による処理の対象に なることのできる、構文上の単位のことです。val宣言や fun宣言は、宣言の一種です。 MLの対話型システムは、「何か」を読み込んで、それを コンピュータに実行させる、ということを繰り返すように 作られているわけですが、この場合の「何か」に相当するのが、 「宣言」と呼ばれるものです。

Q 4.3.3___MLでは、どのようなものを「プログラム」と 呼ぶのですか。

MLでは、宣言の列のことを「プログラム」と呼びます。 つまり、プログラムというのは、 宣言 宣言 …… 宣言 という構文を持つ記述のことです。 useという組み込み関数を使うことによって、ファイルの中に あるものをコンピュータに実行させることができるわけですが、 この場合、ファイルの内容はかならずプログラムになっていないと いけません。

Q 4.3.4___プログラムの中に注釈を書いておきたいのですが、 MLの文法にしたがっていない文字列をプログラムの中に入れると、 MLの処理系がそれを解釈しようとしてエラーを出してしまいます。 エラーが出ないように注釈を入れるためには どうすればいいのですか。

注釈にしたい文字列の左側に(*を書き、右側に*)を書きます。 つまり、 (*文字列*) というように書けばいいわけです。 このように、文字列を(*と*)で囲んでおくと、MLの処理系は、その 文字列を注釈だとみなして、解釈しないで読み飛ばしてしまいます。 注釈は、空白を書くことができる場所ならばどこにでも入れることが できます。たとえば、 fun sanbai n = (* nの3倍を返す関数です。*) n*3 というように、fun宣言の途中に注釈を入れることもできます。 注釈にする文字列は、改行を含んでいてもかまいません。ですから、 (* この関数は、物語テキストを分析して、それをファーブラに 変換します。そのアルゴリズムについては、Umberto Eco教授の 講義草稿、Six Walks in the Fictional Woodsからアイデアを 拝借しています。 *) というように、複数の行にまたがるような注釈を書くことも できるわけです。 また、注釈は入れ子にすることも可能です。つまり、 (* 注釈は、 入れ子 (* こんな感じ *) にすることもできます。 *) というように、(*と*)の内側に(*と*)のペアを書いても かまいません。

Q 4.3.5___今、MLでプログラムを書いているところなんですが、 プログラムの一部分を削除して、どんな結果が得られるかを 試してみたいのです。でも、本当にその部分を削除してしまう というのは、それを復元する労力のことを考えると、ちょっと 躊躇してしまいます。削除以外で、同じ効果が得られる方法 というのはないのでしょうか。

MLでプログラムを書いていて、その一部分を削除した場合の動作を 調べてみたいという場合は、その部分を(*と*)で囲みましょう。 そうすれば、MLの処理系はその部分を注釈だとみなしますので、 削除したのと同じ効果を得ることができます。 プログラムの一部分を、処理系が注釈だと認識する形式に 書き換えることによって、その部分が処理系によって 解釈されないようにすることを、その部分を「コメントアウトする」 と言います。MLの場合は、プログラムの一部分を(*と*)で 囲むことによって、その部分をコメントアウトすることができます。

4.4---演算子の定義

Q 4.4.1___演算子を定義したいときはどうすればいいのですか。

演算子を定義したいときは、infix宣言というものを書きます。 infix宣言というのは、 infix 優先順位 識別子 という構文を持つ宣言です。MLの処理系にこの宣言を与えると、 MLの処理系は、その中の識別子を、指定された優先順位を持つ 左結合の演算子にします。たとえば、 fun abssub (x,y) = abs (x-y) というfun宣言によって定義されたabssubという 関数があるとするとき、 infix 8 abssub というinfix宣言をMLの処理系に与えたとすると、MLの処理系は、 abssubという識別子を、8という優先順位を持つ左結合の 演算子にします。ですから、そののちは、 - 3 abssub 9; > val it = 6 : int - 1 abssub 7 abssub 10; > val it = 4 : int - 1 abssub (7 abssub 10); > val it = 2 : int - 20 mod 7 abssub 15; > val it = 4 : int - (20 mod 7) abssub 15; > val it = 9 : int というように、abssubという識別子を演算子として 使うことができます。

Q 4.4.2___右結合の演算子を定義したいときは どうすればいいのですか。

右結合の演算子を定義したいときは、infixr宣言という宣言を 書きます。 infixr宣言は、 infixr 優先順位 識別子 という構文を持つ宣言です。この宣言は、infix宣言と同じように、 識別子を演算子にするのですが、その演算子が左結合ではなく 右結合になるという点が、infix宣言とは異なっています。 たとえば、abssubという識別子が、Q 4.4.1で書いたfun宣言によって 関数に束縛されているとするとき、 infixr 8 abssub というinfixr宣言をMLの処理系に与えたとすると、abssubは、 - 1 abssub 7 abssub 10; > val it = 2 : int - (1 abssub 7) abssub 10; > val it = 4 : int というように、右結合の演算子になります。

Q 4.4.3___演算子を、演算子ではない普通の識別子に 戻したいときは、どうすればいいのですか。

演算子を普通の識別子に戻したいときは、nonfix宣言という宣言を 書きます。 nonfix宣言は、 nonfix 演算子 という構文を持つ宣言です。MLの処理系にこの宣言を与えると、 MLの処理系は、その中の演算子を、演算子ではない普通の識別子に 戻します。たとえば、 nonfix + というnonfix宣言をMLの処理系に与えたとすると、+は、 - + (5,3); > val it = 8 : int というように、演算子ではない普通の識別子に戻ります。

---練習問題

4.1___時間の長さを何分という形式であらわしている実数(型は real)に適用すると、それを何時間という形式に変換した結果(型は real)を返す、mtohという関数(型はreal -> real)を定義する fun宣言を書いてください。

実行例 - mtoh 135.0; > val it = 2.25 : real

4.2___時間の長さを(何時間,何分)という形式であらわしている 組(型はint * int)に適用すると、それを何分という形式に 変換した結果(型はint)を返す、mtohmという関数(型は int * int -> int)を定義するfun宣言を書いてください。

実行例 - hmtom (2,15); > val it = 135 : int

4.3___時間の長さを何分という形式であらわしている整数(型は int)に適用すると、それを(何時間,何分)という形式に変換した 結果(型はint * int)を返す、mtohmという関数(型は int -> int * int)を定義するfun宣言を書いてください。

実行例 - mtohm 135; > val it = (2,15) : int * int

4.4___分数を(分子,分母)という形式であらわしている組(型は int * int)に適用すると、それを"[分母]分の[分子]"という形式の 文字列(型はstring)に変換する、bunsuuToStringという関数(型は int * int -> string)を定義するfun宣言を書いてください。

実行例 - bunsuuToString (17,38); > val it = "38分の17" : string

4.5___ユニット(型はunit)に適用すると、 "私は文字列を出力する関数です。\n" という文字列を出力してユニット(型はunit)を返す、shutsuという 関数(型はunit -> unit)を定義するfun宣言を書いてください。

実行例 - shutsu (); 私は文字列を出力する関数です。 > val it = () : unit

4.6___文字列(型はstring)に適用すると、その文字列と その長さを、 "[文字列]の長さは[長さ]です。\n" という形式で出力して、ユニット(型はunit)を返す、 printNagasaという関数(型はstring -> unit)を定義するfun宣言を 書いてください。

実行例 - printNagasa "Congratulations!"; Congratulations!の長さは16です。 > val it = () : unit

第5章===選択

5.1---選択をめぐる基本概念

Q 5.1.1___選択って何ですか。

選択というのは、あらかじめ列挙されているいくつかの動作の うちから、現在の状況に関する判断にもとづいて、ひとつを選んで 実行することです。 たとえば、 雨が降っているならば映画館へ行き、そうでなければ海岸へ行く。 という動作は、雨が降っているかどうかという現在の状況に 関する判断にもとづいて、「映画館へ行く」という動作と「海岸へ 行く」という動作からひとつを選んで実行する、 ということですから、選択の一例だと考えることができます。 「選択」という言葉は、日常的には、「何らかの判断にもとづいて、 2個以上の対象の中から何個かを選び取ること」という意味で 使われるのですが、プログラミングの世界では、上に書いたように、 それよりもちょっとだけ特殊な意味でこの言葉を使います。

Q 5.1.2___条件って何ですか。

条件というのは、成り立っているかいないかを判断することの できる対象のことです。 たとえば、人間は、 今、この場所で雨が降っている。 ということについて、それが成り立っているかいないかを 判断することができますので、これは条件の一例だと考えることが できます。それに対して、 霊魂は実在する。 というような問題は、成り立っているかいないかを調べる方法が ありませんので、条件だとは言えません。 選択という動作を記述するときには、実行する動作を 決定するための判断がどのようなものなのかということについて書く 必要があるわけですが、それは、言い換えれば、条件について 記述するということなのです。

Q 5.1.3___真偽値って何ですか。

真偽値というのは、条件が成り立っているかいないか、という ことです。 なお、「条件が真である」という言い方が「条件が成り立っている」 と同じ意味で使われ、「条件が偽である」という言い方が「条件が 成り立っていない」と同じ意味で使われるということも、 覚えておいてください。

Q 5.1.4___MLでは、真偽値はどのようなデータで 表現されるのですか。

MLでは、boolという型のデータによって真偽値を表現します。 boolは、「条件が真である」ということをあらわすデータと、 「条件が偽である」ということをあらわすデータ、という2個の データのみを要素とする集合です。 真偽値は、定数を使って書きあらわすことができます。真をあらわす 定数はtrueで、偽をあらわす定数はfalseです。

Q 5.1.5___trueとfalseというのは予約語なんですか。

いいえ、予約語ではありません。 trueとfalseは、予約語ではない普通の識別子です。ただし、 組み込み関数の名前とは違って、trueやfalseを別のデータに 束縛するということはできません。つまり、 val true = 35 というval宣言はエラーになるということです。

Q 5.1.6___述語って何ですか。

述語というのは、値域がboolであるような関数のことです。 別の見方をすれば、述語というのは、データの性質やデータと データとの関係についての条件が成り立っているかいないかを 調べる、という動作をする関数のことだと言うこともできます。 選択という動作の中には、現在の状況についての判断という動作が 含まれています。判断という動作を実行したいときは、現在の状況を 構成しているデータに対して述語を適用します。そうすると、 述語は、そのデータについての判断を実行して、その結果を 戻り値として返します。 「どのような判断をするのか」というのは、「条件が何であるか」 というのと同じ意味です。ですから、データに述語を適用する 式というのは、何らかの条件について記述しているものなのだ と考えることができます。

Q 5.1.7___組み込み述語って何ですか。

組み込み述語というのは、述語であるような組み込み関数の ことです。 組み込み関数の中には、述語であるものもたくさん含まれています。 それらの述語は、データの性質やデータとデータとの関係についての 基本的な判断の機能を提供しています。

Q 5.1.8___述語を自分で定義することは可能ですか。

はい、可能です。 組み込み述語が調べることのできる性質や関係は、基本的なものに 限られています。ですから、プログラムを書いているとき、こんな 述語があれば便利なのになあ、と思うことがしばしばあります。 そんなときは、そのような述語を自分で作ってしまいましょう。 述語というのは関数の一種ですから、fun宣言を書くことによって 定義することができます(値域をboolにすればいいだけです)。

5.2---組み込み述語

Q 5.2.1___数値の大小関係について調べたいときは、どんな 組み込み述語を使えばいいのですか。

数値(intまたはrealまたはword)の大小関係について 調べたいときは、 > : int * int -> bool > : real * real -> bool > : word * word -> bool < : int * int -> bool < : real * real -> bool < : word * word -> bool というような組み込み述語を使うことができます。 これらの述語に束縛されている識別子は演算子ですので、 exp1 > exp2 というような式を書くことによって、2個の数値の組に適用します。 >は、exp1の値のほうがexp2の値よりも大きいならばtrueを返し、 そうでなければfalseを返します。たとえば、 8>5 というval宣言を対話型システムに入力したとすると、 > val it = true : bool という応答が得られます。 <は、>とは逆の動作をする述語です。 exp1 < exp2 という式の値は、exp1の値のほうがexp2の値よりも小さいならば true、そうでなければfalseになります。 >や<と同じような動作をする述語で、 >= : int * int -> bool >= : real * real -> bool >= : word * word -> bool <= : int * int -> bool <= : real * real -> bool <= : word * word -> bool というのもあります。これらの述語も、引数に含まれている二つの 数値のあいだの大小関係について調べるのですが、それらの数値が 等しい場合もtrueを返すという点が、>や<とは違っています。 つまり、>=は「左のほうが大きいか、または左右が等しい」という 関係を調べる述語で、<=は「左のほうが小さいか、または左右が 等しい」という関係を調べる述語なのです。言い換えれば、>=や <=が調べる関係というのは、日本語の「以上」とか「以下」という 言葉があらわしているものと同じだということです。たとえば、 5>5 5>=5 という二つの式を評価したとすると、上の式の値はfalseになり、 下の式の値はtrueになります。 >、<、>=、<=は、優先順位が4の演算子です。それに対して、 +、-は6で、*、/、div、modは7ですから、たとえば、 a+b>c という式は、 aとbとを加算した結果がcよりも大きいかどうかを調べる、という 意味だと解釈されます。 述語というのは戻り値として型がboolのデータを返す 関数のことですから、述語が返したデータをそのまま返すような 関数を作ると、その関数もまた述語になります。たとえば、 fun plus n = n>0 というfun宣言を書いたとすると、plusという名前の新しい 述語(型はint -> bool)ができることになります。plusを整数に 適用すると、plusは、それが0よりも大きいならば(つまり正の 数ならば)trueを返し、そうでなければfalseを返します。 ですから、plusの戻り値は、 - plus 5; > val it = true : bool というように5に適用した場合はtrueになり、 - plus ~7; > val it = false : bool というように~7に適用した場合はfalseになります。

Q 5.2.2___二つの文字列を辞書の順序で並べたときに、どちらが 前でどちらがうしろになるかを調べたいときは、どんな 組み込み述語を使えばいいのですか。

文字列の辞書式順序について調べたいときは、 > : string * string -> bool < : string * string -> bool >= : string * string -> bool <= : string * string -> bool という組み込み演算子を使うことができます。 exp1とexp2の値が両方とも文字列の場合、 exp1 > exp2 の値は、exp1の値のほうがexp2の値よりも辞書式順序でうしろに なるならばtrue、そうでなければfalseになります。 exp1 < exp2 はその逆で、exp2のほうがうしろになるならばtrueです。 たとえば、 "aaaaa" < "aaaab" というval宣言を対話型システムに入力したとすると、 > val it = true : bool という応答が出力されます。 >=は「左のほうがうしろになるか、または左右が同じ文字列である」 という関係を調べる述語で、<=は「右のほうがうしろになるか、 または左右が同じ文字列である」という関係を調べる述語です。

Q 5.2.3___二つのデータが同じものかどうかを調べたいときは、 どんな組み込み述語を使えばいいのですか。

データの同一性について調べたいときは、 = : ''a * ''a -> bool という組み込み演算子を使います。 この演算子の定義域には、''aという奇妙な型式が 含まれていますが、とりあえずここでは、「''aというのは、 同一性について調べることのできる任意の型という意味の型式だ」 と考えておいてください(''aという型式についてのもう少し くわしい説明は、第8.2節にあります)。 同じ型を持つ二つのデータの組に=を適用すると、=は、それらが 同一ならばtrue、そうでなければfalseを返します。たとえば、 25=25 の値はtrueになり、 25=24 の値はfalseになります。 =の左右の式の値は、型が同じでないといけません。ですから、 25=25.0 というような、異なる型を持つ二つのデータの組に=を適用する 式は、エラーになります。 なお、=は、>や<などと同じで、優先順位が4の演算子です。 =を利用することによって、たとえば、整数がゼロかどうかを調べる 述語(型はint -> bool)を、 fun zero n = n=0 というように定義することができます。この述語は、引数として 1個の整数を受け取って、それがゼロならばtrue、そうでなければ falseを返します。つまり、 - zero 0; > val it = true : bool - zero 3; > val it = false : bool というような動作をします。

Q 5.2.4___どんな型のデータでも、同一性について 調べることができるのですか。

いいえ、同一性について調べることのできない型もあります。 int、real、word、string、char、boolなどのデータは 同一性について調べることができますが、それができない 型というのもあって、たとえば関数型というのはその一例です。 ですから、 size=size というような、二つの関数の組に=を適用しようとする式は、関数の 同一性について調べることが不可能ですので、エラーに なってしまいます。 同じ型を持つ二つの組があって、それらのすべての要素が 同一性について調べることのできる型を持っているならば、 それらの組のあいだで同一性について調べることも可能です。 たとえば、a、b、x、yという識別子が、同一性について 調べることのできるひとつの型のデータに束縛されている とするとき、 (a,b) = (x,y) の値は、aとxの値が同じでbとyの値も同じならばtrueになり、 そうでなければfalseになります。 なお、同一性について調べることのできる型とそうでない 型とがあるという問題には、第8.2節で、 もう一度登場してもらうことにします。

Q 5.2.5___二つのデータが同じものではないという関係について 調べたいときは、どんな組み込み述語を使えばいいのですか。

データが同じではないという関係は、 <> : ''a * ''a -> bool という組み込み演算子を使うことによって調べることができます。 <>は、左右の式の値が同じものではないならばtrue、同じものならば falseを返します。たとえば、 25<>24 の値はtrueになり、 25<>25 の値はfalseになります。

Q 5.2.6___「何々である」という条件について調べる述語がある とするとき、「何々ではない」という条件について調べたい場合は どうすればいいのですか。

そんなときは、 not : bool -> bool という組み込み述語を使います。 真偽値にnotを適用すると、notは、それとは逆の真偽値を 返します。つまり、trueに適用するとfalseを返し、falseに 適用するとtrueを返すということです。 こんな例で考えてみましょう。今、 fun goNoBaisuu n = n mod 5 = 0 というfun宣言によって、「5の倍数である」という性質について 調べるgoNoBaisuuという述語が定義されているとしましょう。 goNoBaisuu 35 の値はtrueになり、 goNoBaisuu 37 の値はfalseになります。さて、それでは、「5の倍数ではない」 という性質について調べたいときはどうすればいいでしょうか。 そんなときは、notという組み込み述語を使います。つまり、 not (goNoBaisuu exp) の値は、expの値が5の倍数ではないならばtrueになり、5の 倍数ならばfalseになるのです。ですから、 not (goNoBaisuu 37) の値はtrueになり、 not (goNoBaisuu 35) の値はfalseになります。

5.3---andalsoとorelse

Q 5.3.1___2個の条件があって、それらが両方とも真である ときだけ真になる、という条件を記述するためにはどうすれば いいのですか。

そんなときはandalso式というものを書きます。 andalso式というのは式の一種で、 式1 andalso 式2 と書きます。式1と式2は、値の型がboolでないといけません。 andalso式の値は、式1の値と式2の値が両方ともtrueだったときだけ trueになり、それ以外の場合はfalseになります。たとえば、 8>5 andalso 7>6 というandalso式は、8>5と7>6の両方がtrueですから、全体の値は trueになります。それに対して、 5>8 andalso 7>6 8>5 andalso 6>7 5>8 andalso 6>7 は、いずれも、左右が両方ともtrueというわけではありませんので、 全体の値はfalseです。 ですから、条件1と条件2という2個の条件があって、それらの両方が 真になるときだけ真になる、という条件を記述したいときは、 条件1をあらわす式 andalso 条件2をあらわす式 というandalso式を書けばいいわけです。たとえば、aという識別子の 値が50以上でかつ100以下であるという条件は、 a>=50 andalso a<=100 というandalso式で書きあらわすことができます。

Q 5.3.2___コンピュータは、andalso式をどのような手順で 評価するのですか。

コンピュータにandalso式を評価させると、コンピュータは、 まず最初に左の式を評価します。そして、その値がtrueならば、 次に右の式を評価して、右の式の値を式全体の値にします。左の式の 値がfalseだった場合は、右の式は評価しないで、falseを式全体の 値にします。 つまり、左の式を評価した段階で結論が出せる場合は、右の式は 評価しないということです。andalsoの右側の式は評価されない 場合がある、というのはちょっと重要なことですので、 覚えておいてください。

Q 5.3.3___andalsoの右側の式は評価されない場合がある というのがそれほど重要なことだとは思えないのですが、 どんなときに問題になるのですか。

式が評価される場合とされない場合とがあるということが問題に なるのは、その式が副作用を持っている場合です。副作用のある式を andalsoの右側に書いたとすると、その式の副作用は実行される 場合と実行されない場合とがある、ということになってしまいます。 具体的な例で考えてみましょう。ちょっと作為的ですが、 fun guusuu n = ( print (Int.toString n ^ "\n"); n mod 2 = 0 ) というように定義されたguusuuという関数(型はint -> bool)が あるとします。guusuuを整数に適用すると、guusuuはまずその整数を 出力して、次にその整数が偶数かどうかを調べて、その結果を 戻り値として返します。このguusuuを使って、 a>100 andalso guusuu a という式を書いたとします。この式の値は、aの値が153の場合も 76の場合もfalseになります。でも、この式の副作用は、aの値が 153の場合と76の場合とでは違います。153の場合はguusuu aという 式が評価されますのでその整数がモニターの画面に出力されますが、 76の場合はguusuu aが評価されませんので何も出力されません。 つまり、aの値が何なのかで、副作用が実行される場合と 実行されない場合とができる、ということになります。 副作用のある式をandalsoの右側に意図的に書くこともありますが、 うっかりしていて書いてしまうということがないように注意する 必要があります。

Q 5.3.4___aという識別子の値が50以上でかつ100以下であるという 条件を、 50 <= a <= 100 という式であらわすことはできないのですか。

はい、残念ながらできません。 <=は左結合の演算子ですので、 50 <= a <= 100 という式は、 op <= (op <= (50,a),100) という意味だと解釈されます。ですから、「aが50以上で かつ100以下である」という条件について調べることには ならないわけです。 ちなみに、この式は、bool * intという型のデータに<=を適用する ことになるわけですが、その型は<=の定義域とは一致しませんので、 エラーになってしまいます。

Q 5.3.5___何個かの条件があって、それらがすべて真である ときだけ真になる、という条件を記述するためにはどうすれば いいのですか。

そのような条件は、それぞれの条件をあらわす式をandalsoで つないでいくことによってあらわすことができます。 条件をあらわすn個の式があって、それらの条件がすべて真である という条件をあらわしたいときは、 式1 andalso 式2 andalso 式3 andalso …… andalso 式n というような式を書きます。このような式をコンピュータに 評価させると、コンピュータは、その中の式を左から右へ順番に 評価していって、値がfalseになる式が見つかったならば式全体の 値をfalseにします(残りの式は評価しません)。すべての式の 値がtrueだった場合は、式全体の値もtrueになります。 たとえば、a、b、c、dという4個の識別子がすべて100に 束縛されているという条件は、 a=100 andalso b=100 andalso c=100 andalso d=100 という式を書くことによってあらわすことができます。

Q 5.3.6___2個の条件があって、それらが両方とも真であるか、 またはそれらのうちのどちらか一方が真であるときに真になる、 という条件を記述するためにはどうすればいいのですか。

そんなときはorelse式というものを書きます。 orelse式というのは式の一種で、 式1 orelse 式2 と書きます。式1と式2のところには、値の型がboolになるような式を 書きます。orelse式の値は、式1の値と式2の値が両方とも trueだったならばtrueになり、それらのうちの一方がfalseで他方が trueであるときもtrueになり、両方ともfalseだった場合だけ falseになります。たとえば、 8>5 orelse 7>6 というorelse式は、8>5と7>6の両方がtrueですから、全体の値は trueになります。そして、 5>8 orelse 7>6 8>5 orelse 6>7 の場合、一方はfalseですが他方がtrueですので、やはり全体の値は trueです。それに対して、 5>8 orelse 6>7 は、左右が両方ともfalseですので、全体の値はfalseになります。 ですから、条件1と条件2という2個の条件があって、それらの 両方または片方が真になるときに真になる、という条件は、 条件1をあらわす式 orelse 条件2をあらわす式 というorelse式を書くことによって記述することができます。 たとえば、aという識別子の値が5の倍数であるかまたは7の 倍数であるという条件を記述したいならば、 a mod 5 = 0 orelse a mod 7 = 0 というorelse式を書けばいいわけです。

Q 5.3.7___コンピュータは、orelse式をどのような手順で 評価するのですか。

コンピュータにorelse式を評価させると、コンピュータは、 まず最初に左の式を評価します。そして、その値がfalseならば、 次に右の式を評価して、右の式の値を式全体の値にします。左の式の 値がtrueだった場合は、右の式は評価しないで、trueを式全体の 値にします。 andalso式の場合と同じように、orelse式でも、右側の式が 評価されない場合がありますので、式の副作用に気を付ける必要が あります。

Q 5.3.8___何個かの条件があって、それらのうちの少なくとも 1個の条件が真ならば真になる、という条件を記述するためには どうすればいいのですか。

そのような条件を記述したいときは、それぞれの条件をあらわす式を orelseでつないでいくことによってできる式を書きます。 条件をあらわすn個の式があって、それらのうちの少なくとも1個の 条件が真であるという条件は、 式1 orelse 式2 orelse 式3 orelse …… orelse 式n というような式を書くことによってあらわすことができます。 このような式をコンピュータに評価させると、コンピュータは、 その中の式を左から右へ順番に評価していって、値がtrueになる式が 見つかったならば式全体の値をtrueにします(残りの式は 評価しません)。すべての式の値がfalseだった場合は、式全体の 値もfalseになります。 たとえば、aという識別子が束縛されているデータが5、9、14、23の いずれかであるという条件をあらわす式は、 a=5 orelse a=9 orelse a=14 orelse a=23 というように書くことができます。

Q 5.3.9___exp1、exp2、exp3が式だとするとき、 exp1 andalso exp2 orelse exp3 とか、 exp1 orelse exp2 andalso exp3 とかっていう式は、どのように解釈されるのですか。

andalsoとorelseというのは演算子ではないのですが、それらを 含んでいる式を解釈するときは、あたかも演算子であるかのように 扱われます。andalsoは、すべての演算子よりも低い優先順位を 持っていて、orelseは、andalsoよりもさらに低い優先順位を 持っているとみなされます。ですから、 exp1 andalso exp2 orelse exp3 は、「exp1とexp2の両方が真であるという条件と、exp3が真である という条件の、両方または片方が真である」という条件を あらわしていると解釈され、 exp1 orelse exp2 andalso exp3 は、「exp1が真であるという条件と、exp2とexp3の両方が真である という条件の、両方または片方が真である」という条件を あらわしていると解釈されます。

5.4---if式

Q 5.4.1___MLでは、どのようなものを書けば選択を記述することが できるのですか。

MLでは、if式というものを書くことによって選択を記述することが できます。 if式というのは式の一種で、 if 式1 then 式2 else 式3 と書きます。この中の式1というところに書く式は、値の型が boolでないといけません。式2と式3のところは、どんな型の式を 書いてもいいのですが、式2の型と式3の型は同一でないと いけません。 コンピュータにif式を評価させると、コンピュータは、まず最初に その中の式1を評価します。式1の値がtrueだった場合は、次に式2を 評価して、その値をif式全体の値にします。この場合、式3は 評価されません。式1の値がfalseだった場合は、式2を 評価しないで式3を評価して、式3の値をif式全体の値にします。 たとえば、 if 8>5 then 30 else 40 という式をコンピュータに評価させたとしましょう。 コンピュータは、まず最初に8>5という式を評価します。すると、 その値としてtrueが得られますので、コンピュータは次に30という 式を評価して、その値をif式全体の値にします。ですから、この if式の値は30になります。 if 3>5 then 30 else 40 の場合は、3>5の値がfalseですので、コンピュータは40という式を 評価して、その値をif式全体の値にします。 ですから、条件が真であるならば動作1を実行して、偽ならば動作2を 実行する、という選択は、 if 条件をあらわしている式 then 動作1 else 動作2 というif式を書くことによって記述することができます。たとえば、 aの値のほうがbの値よりも大きいならばaとcとを乗算して、 そうでなければbとcとを乗算する、という選択は、 if a>b then a*c else a*c と書くことができます。 それでは、if式を使ったfun宣言の例を書いてみましょう。 ひとつ目は、整数に適用すると、それが偶数ならば"偶数"という 文字列を返し、そうでなければ"奇数"という文字列を返す、 evenOrOddという関数(型はint -> string)です。fun宣言は、 fun evenOrOdd n = if n mod 2 = 0 then "偶数" else "奇数" と書くことになります。 二つ目は、2個の整数の組に適用すると、それらの整数のうちの 大きいほう(両者が等しい場合はその等しい整数)を返す、 majorという関数(型はint * int -> int)です。この関数は、 fun major (a,b) = if a>b then a else b というfun宣言で定義できます。 三つ目は、何時何分という時刻をあらわす整数の組に適用すると、 その時刻を「何時何分」という形式で出力する、printTimeという 関数(型はint * int -> unit)です。ただし、(7,0)のように、 何分の部分がゼロの場合は、「ちょうど何時」という形のものを 出力します。fun宣言は次のようになります。 fun printTime (h,m) = let val sh = Int.toString h ^ "時" val sm = Int.toString m ^ "分" in print (if m=0 then "ちょうど" ^ sh else sh^sm) end

Q 5.4.2___二つの条件があって、それらの真偽値の 組み合わせ(真真、真偽、偽真、偽偽)に応じて、4個の動作の 中からひとつを選んで実行する、という選択は、どのように 書けばいいのでしょうか。

そんなときは、if式の中にif式を書きます。つまりif式を入れ子に すればいいわけです。 式aと式bという式によってあらわされている二つの条件の真偽値が、 真真ならば式1、真偽ならば式2、偽真ならば式3、偽偽ならば式4が 評価されるようにしたいという場合は、 if 式a then if 式b then 式1 else 式2 else if 式b then 式3 else 式4 というように、if式の中にif式を入れ子にします。 たとえば、a=1とb=1という二つの条件の真偽値が、真真ならば "spring"、真偽ならば"summer"、偽真ならば"autumn"、偽偽ならば "winter"という定数を評価する、という選択は、 if a=1 then if b=1 then "spring" else "summer" else if b=1 then "autumn" else "winter" というif式であらわすことができます。

Q 5.4.3___何個かの条件があって、それらの真偽値を順番に 調べていって、真になるものが見つかったらそれに対応する動作を 実行する、という選択は、どのように書けばいいのでしょうか。

そんなときは、if式のelseの次にif式を書き、そのif式のelseの 次にif式を書き、そのif式のelseの次にif式を書き、というように if式をつなげていきます。 条件をあらわしているn個の式(c1、c2、c3、……、cn)と、 それぞれの条件に1対1で対応しているn個の式(e1、e2、e3、……、 en)があって、条件の式を順番に評価していって、値がtrueに なるものが見つかったならばそれに対応している式を評価する、 という選択は、 if c1 then e1 else if c2 then e2 else if c3 then e3 …… else if cn then en else em というif式であらわすことができます(最後のemというのは、c1から cnまでのすべての条件がfalseだった場合に評価される式です)。 たとえば、wという識別子の値が1ならば"Monday"、2ならば "Tuesday"、3ならば"Wednesday"、4ならば"Thursday"、5ならば "Friday"、6ならば"Saturday"、7ならば"Sunday"、 それら以外ならば"unknown number"という定数を評価する、 という選択を記述したいならば、 if w=1 then "Monday" else if w=2 then "Tuesday" else if w=3 then "Wednesday" else if w=4 then "Thursday" else if w=5 then "Friday" else if w=6 then "Saturday" else if w=7 then "Sunday" else "unknown number" というif式を書けばいいわけです。 次に、もうひとつの例として、hという識別子の値が500以上ならば "絶対確実"、350以上で500未満ならば"安全圏内"、250以上で 350未満ならば"ボーダーライン"、100以上で250未満ならば "あと一歩"、100未満ならば"志望校の変更を要す"という定数を 評価する、という選択をif式で記述してみましょう。 素直に書くと、 if h>=500 then "絶対確実" else if h>=350 andalso h<500 then "安全圏内" else if h>=250 andalso h<350 then "ボーダーライン" else if h>=100 andalso h<250 then "あと一歩" else if h<100 then "志望校の変更を要す" else "" というようになるわけですが、よく考えてみると、この式には余計な 記述が含まれていることがわかります。実は、「何々未満」という 条件は書かなくてもいいのです。なぜなら、このif式の中に 書かれているそれぞれの条件は、上から順番に評価されて、 その値がfalseだったときだけ次の条件が評価されるからです。 たとえば、 h>=350 andalso h<500 という式が評価されるのは、h>=500が評価されて、それが falseだったときだけですから、h<500は、わざわざ調べなくても 必然的にtrueになるはずです。同じように、 h>=250 andalso h<350 という式が評価されるのは、h>=350がfalseだった ときだけですから、やはりh<350というのは余計です。 ですから、上に書いたif式は、 if h>=500 then "絶対確実" else if h>=350 then "安全圏内" else if h>=250 then "ボーダーライン" else if h>=100 then "あと一歩" else "志望校の変更を要す" と書いても同じ意味になります。

5.5---パターンによる選択

Q 5.5.1___選択を記述するとき、式の値がtrueかfalseか ということではなくて、パターンとデータとの照合が成功したか 失敗したかということによって実行する動作を選択する、 というようなことはできないのですか。

できます。case式というものを書くことによって、パターンと データとの照合が成功したか失敗したかという判断にもとづいた 選択を記述することができます。 case式というのは式の一種です。case式の構文はかなり複雑なので、 少しずつ段階を追って説明していきましょう。 まず、「規則」と呼ばれる構文について説明します。 規則というのは、 パターン => 式 という構文です。たとえば、 8 => "August" というのは規則の一例です。 次は「照合子」です(注)。照合子というのは、 規則 | 規則 | 規則 | …… | 規則 という構文です。つまり、1個以上の規則を縦棒(|)で区切りながら 並べたもの、ということです。ただし、ひとつの照合子に 含まれているそれぞれの規則は、=>の右側に書かれた式の値が すべて同一の型を持つようになっていないといけません。つまり、 1 => "one" | 2 => "two" | 3 => "three" | _ => "many" は、=>の右側に書かれた式の値の型がすべてstringに なっているので正しい照合子なのですが、 1 => "one" | 2 => 2.0 | 3 => true | _ => () は、型が一致していないので正しい照合子ではない、 ということです。 同じように、=>の左側のパターンについても、それらに一致する データの型は、ひとつに統一されている必要があります。つまり、 1 => "one" | 2 => "two" | 3 => "three" | 3.14 => "pi" | _ => "many" というような照合子を書くと、エラーになってしまう ということです。 (注) 照合子は、「照合」というのが正式な呼び方です。でも、 「照合」だと、「照合すること」という意味の「照合」と まぎらわしいので、この文章の中では「照合子」と呼ぶことに しました。 それではいよいよcase式です。case式は、 case 式 of 照合子 と書きます。 コンピュータにcase式を評価させると、コンピュータは、まず caseとofのあいだに書かれた式を評価します。そして次に、 1個目の規則の中のパターンと、さきほどの式の値とを照合します。 そして、その照合が成功した場合は、そのパターンの右側に 書かれた式を評価して、その値をcase式全体の値にして、 それでcase式の評価は終わりになります。照合が失敗した場合は、 次の規則の中のパターンと、さきほどの式の値とを照合します。 つまり、コンピュータは、caseとofの間に書かれた式の値に 一致するパターンを照合子の先頭から順番に探していって、それが 見つかったら、そのパターンに対応する式を評価して、その値を case式全体の値にするわけです。 たとえば、nという識別子の値が3だとするとき、 case n of 1 => "one" | 2 => "two" | 3 => "three" | _ => "many" というcase式をコンピュータに評価させると、コンピュータは、 まず最初にnという識別子を評価して、3という値を求めます。 次に、コンピュータは、1というパターンと3というデータとを 照合します。この照合は失敗しますので、次は2と3とを照合します。 この照合も失敗しますので、次に3と3とを照合します。すると、 今度は照合が成功しますので、コンピュータは、3というパターンの 右側にある"three"という定数を評価して、その値をcase式全体の 値にします。 ですから、case式を書くことによって、データがどのような パターンに一致するかということにもとづく選択、というものを 記述することができるわけです。 それでは、case式を使ったfun宣言の例を書いてみましょう。 次のfun宣言は、引数が1ならば"Monday"、2ならば"Tuesday"、 3ならば"Wednesday"というように、整数を曜日の名前に変換する、 ntoweekという関数(型はint -> string)を定義します。 fun ntoweek n = case n of 1 => "Monday" | 2 => "Tuesday" | 3 => "Wednesday" | 4 => "Thursday" | 5 => "Friday" | 6 => "Saturday" | 7 => "Sunday" | _ => "unknown number"

Q 5.5.2___規則の中に識別子を含むパターンを書いたとすると、 照合が成功した場合、その識別子はやはりそれに対応するデータに 束縛されるんですか。

はい、そのとおりです。 たとえば、nという識別子が(5,3)という組に束縛されているとき、 case n of (a,1) => a+1 | (a,2) => a*2 | (a,3) => a*a | (a,_) => a というcase式を評価したとすると、(a,3)というパターンと (5,3)というデータとの照合が成功するわけですが、そのとき、 aという識別子は5という整数に束縛されます。ですから、この case式の値は25になります。 それでは、fun宣言の例も書いてみましょう。 fun shisoku kumi = case kumi of ("tasu", (a,b)) => a+b | ("hiku", (a,b)) => a-b | ("kakeru", (a,b)) => a*b | ("waru", (a,b)) => a div b | _ => 0 というfun宣言によって定義される関数(型は string * (int * int) -> int)は、引数の中の2個の整数に対して、 引数の中の文字列が"tasu"ならば加算を、"hiku"ならば減算を、 "kakeru"ならば乗算を、"waru"ならば除算を実行して、その結果を 返します。たとえば、 shisoku ("kakeru",(7,8)) という関数適用を評価すると、56という値が得られます。

Q 5.5.3___case式と同じように、fun宣言を書くときも、引数と 照合されるパターンを2個以上書くことによって、照合が成功するか 失敗するかということによる動作の選択が記述できればとっても 便利だと思うのですが、そんなことはできないのですか。

いいえ、それも可能です。 実は、fun宣言というのは、 fun 識別子 パターン1 = 式1 | 識別子 パターン2 = 式2 | 識別子 パターン3 = 式3 …… | 識別子 パターンn = 式n というように、パターンと式とのカップルを2個以上書くことが できるのです(この中の「識別子」のところは、すべて同一の 識別子でないといけません。この識別子が、新しくできた関数に 束縛されるわけです)。 このようなfun宣言によって定義された関数をデータに適用すると、 コンピュータは、まず最初に引数とパターン1とを照合します。 もしも照合が成功したならば、式1を評価して、その値を戻り値に します(パターンの中に識別子が含まれている場合は、 その識別子は対応するデータに束縛されます)。引数と パターン1との照合が失敗した場合は、次に引数とパターン2とを 照合します。そして、それも失敗した場合はパターン3、 というように照合が成功するまで下に向かって順番にパターンを 試していくわけです。 ですから、Q 5.5.1で書いたntoweekという関数の定義は、 fun ntoweek 1 = "Monday" | ntoweek 2 = "Tuesday" | ntoweek 3 = "Wednesday" | ntoweek 4 = "Thursday" | ntoweek 5 = "Friday" | ntoweek 6 = "Saturday" | ntoweek 7 = "Sunday" | ntoweek _ = "unknown number" というように書いてもかまわないわけです。 同じように、Q 5.5.2で書いたshisokuという関数の定義も、 fun shisoku ("tasu", (a,b)) = a+b | shisoku ("hiku", (a,b)) = a-b | shisoku ("kakeru", (a,b)) = a*b | shisoku ("waru", (a,b)) = a div b | shisoku _ = 0 というように書き換えることができます。

---練習問題

5.1___整数に適用すると、その整数が100と等しいという条件の 真偽値を返す、hundredという述語(型はint -> bool)を定義する fun宣言を書いてください。

実行例(1) - hundred 100; > val it = true : bool 実行例(2) - hundred 317; > val it = false : bool

5.2___2個の整数の組に適用すると、2個目の整数が1個目の整数の 約数であるという条件の真偽値を返す、measureという述語(型は int * int -> bool)を定義するfun宣言を書いてください。

実行例(1) - measure (21,7); > val it = true : bool 実行例(2) - measure (22,7); > val it = false : bool

5.3___2個の整数の組に適用すると、それらの整数が両方とも 偶数であるという条件の真偽値を返す、bothEvenという述語 (型はint * int -> bool)を定義するfun宣言を書いてください。

実行例(1) - bothEven (52,46); > val it = true : bool 実行例(2) - bothEven (34,27); > val it = false : bool

5.4___2個の整数の組に適用すると、それらの整数のうちのどちらか 一方が偶数であるか、または両方が偶数であるという条件の真偽値を 返す、eitherEvenという述語(型はint * int -> bool)を 定義するfun宣言を書いてください。

実行例(1) - eitherEven (34,27); > val it = true : bool 実行例(2) - eitherEven (52,46); > val it = true : bool 実行例(3) - eitherEven (41,27); > val it = false : bool

5.5___整数に適用すると、それがプラスの整数ならば "プラスです。"という文字列を返し、そうでなければ "プラスではありません。"という文字列を返す、plusOrNotという 関数(型はint -> string)を定義するfun宣言を書いてください。

実行例(1) - plusOrNot 89; > val it = "プラスです。" : string 実行例(2) - plusOrNot ~51; > val it = "プラスではありません。" : string 実行例(3) - plusOrNot 0; > val it = "プラスではありません。" : string

5.6___一般教養の試験の点数と体力テストの点数との組(型は int * int)に適用すると、一般教養と体力が両方とも80以上ならば "合格です。"という文字列を返し、一般教養が80以上で体力が 80未満ならば"もっと体を鍛えましょう。"という文字列を返し、 一般教養が80未満で体力が80以上ならば"もっと本を読みましょう。" という文字列を返し、一般教養と体力が両方とも80未満ならば "教養も体力もいまいちです。"という文字列を返す、 passExamという関数(型はint * int -> string)を定義する fun宣言を書いてください。

実行例(1) - passExam (97,83); > val it = "合格です。" : string 実行例(2) - passExam (86,45); > val it = "もっと体を鍛えましょう。" : string 実行例(3) - passExam (53,94); > val it = "もっと本を読みましょう。" : string 実行例(4) - passExam (47,59); > val it = "教養も体力もいまいちです。" : string

5.7___西暦の年をあらわす整数に適用すると、 710年よりも前 飛鳥時代またはそれ以前 710年〜794年 奈良時代 794年〜1192年 平安時代 1192年〜1336年 鎌倉時代 1336年〜1392年 南北朝時代 1392年〜1573年 室町時代 1573年〜1603年 安土桃山時代 1603年〜1867年 江戸時代 1867年以降 明治時代またはそれ以降 という対応にもとづいて、その年が属している時代区分をあらわす 文字列を返す、jidaikubunという関数(型はint -> string)を 定義するfun宣言を書いてください。

実行例(1) - jidaikubun 646; > val it = "飛鳥時代またはそれ以前" : string 実行例(2) - jidaikubun 939; > val it = "平安時代" : string 実行例(3) - jidaikubun 1378; > val it = "南北朝時代" : string 実行例(4) - jidaikubun 1936; > val it = "明治時代またはそれ以降" : string

5.8___月の番号をあらわす整数に適用すると、その月の英語の名前を 返す、monthToEngという関数(型はint -> string)を定義する fun宣言を書いてください。

実行例 - monthToEng 8; > val it = "August" : string

5.9___整数と文字列の組に適用すると、その整数が1ならばその 文字列を"("と")"で囲んだものを返し、2ならば"["と"]"で 囲んだものを返し、3ならば"{"と"}"で囲んだものを返し、 それら以外ならばその文字列をそのまま返す、encloseという 関数(型はint * string -> string)を定義するfun宣言を 書いてください。

実行例(1) - enclose (2,"ぽよよん"); > val it = "[ぽよよん]" : string 実行例(2) - enclose (4,"ふわわん"); > val it = "ふわわん" : string

第6章===再帰

6.1---再帰についての一般的考察

Q 6.1.1___再帰的な構造って何ですか。

再帰的な構造というのは、全体と同じ構造を持つものをその 一部分として含んでいる構造のことです。 再帰的な構造を持っているものは、その一部分が全体と同じ構造を 持っているわけですから、その一部分の中の一部分として、 ふたたび全体と同じ構造があって、その一部分の中にさらに、 というように内側に向かって延々と同じ構造が続くことになります。 テレビカメラと、それがとらえた映像を映し出しているモニターが あるとしましょう。このとき、テレビカメラをそのモニターに 向けると、モニターの中にそのモニター自身が映ることになります。 そして、そのモニターの中のモニターには、やはりモニターが 映っていて、というように、内側に向かって延々と同じモニターの 映像が続いていきます。この場合、そのモニターの映像は再帰的な 構造を持っていると考えることができます。 テレビカメラをモニターに向けたときの映像というのは、 理論的には同じ構造が無限に続くことになりますが、有限の回数で 終了する場合でも、再帰的な構造を持っていると 言ってかまいません。 例として、 ( 式 演算子 式 ) または 定数 という構文を持つ式について考えてみましょう。たとえば、 ((35+27)*(43-81)) という式は、その一部分として、(35+27)と(43-81)という式を 含んでいます。そしてそれらの二つの式は、その一部分として、 35と27と43と81という式を含んでいます。しかし、それらの4個の 式は定数ですので、それ以上は分解することができません。つまり、 内側へ向かう連鎖は、2回で終了することになります。このように、 有限の長さを持つ式というのは、同じ構造のものを含むという連鎖が 有限回で終了するのですが、このような場合でも再帰的な構造だと 言っていいわけです。 有限の大きさで再帰的な構造を持つものは、かならず、それ以上は 全体と同じ構造を含まない、芯のようなものを持っています。 式の場合は、定数というのが芯に相当します。これからは、 そのような芯のことを「基底」と呼ぶことにします。

Q 6.1.2___再帰的な定義って何ですか。

再帰的な定義というのは、定義しようとしている対象を 使用している定義のことです。再帰的な構造を持つものを 定義するという場合、定義の方法としてはさまざまなものが 考えられますが、再帰的な定義というのがそれらのうちでもっとも 自然な方法です。 具体的な例で説明しましょう。 () (()) ((())) (((()))) というように、何個かの左丸括弧と、それと同じ数の右丸括弧とを 並べることによってできる文字列、という構文のことを「括弧列」 と呼ぶことにします。括弧列は再帰的な構造を持っていますので、 その定義は再帰的に書くというのが自然です。括弧列の再帰的な 定義は、 ● ()は括弧列である。 ● 括弧列の左側に"("、右側に")"を書いたものは括弧列である。 ● 以上の記述から導かれるもの以外は括弧列ではない。 というように書くことができます。つまり、括弧列という、 定義される対象を定義の中で使うわけです。

Q 6.1.3___定義が再帰的だと、無限の循環に陥ってしまうことが あるのではありませんか。

はい、その危険性は確かに存在します。ですから、ものごとを 再帰的に定義する場合には、循環がかならずどこかで ストップするような歯止めを作っておく必要があります。 たとえば、Q 6.1.2に登場した括弧列を、 ● 括弧列の左側に"("、右側に")"を書いたものは括弧列である。 ● 以上の記述から導かれるもの以外は括弧列ではない。 と定義したとすると、この定義は無限に循環してしまいますので、 ((((……)))) という無限の長さの文字列だけが括弧列だということに なってしまいます。括弧列を再帰的に定義する場合には、 循環をストップさせる記述として、 ● ()は括弧列である。 という文が不可欠なのです。 なお、循環をストップさせる歯止めというのは、再帰的な構造の 基底についての記述だと考えることができます。

Q 6.1.4___再帰って何ですか。

再帰というのは、定義の中で、定義しようとしている対象に 言及することです。この言葉は、「再帰する」という動詞の形で 使われることもあります。

Q 6.1.5___アルゴリズムって何ですか。

アルゴリズムというのは、問題を解決するための方法のことです。 何種類かの単純な問題を解決するための方法をすでに知っている 機械があるとしましょう。そして、その機械は、方法さえ 教えてあげれば、自分がすでに知っている方法を 組み合わせることによって、さらに多くの問題を解決することが できるとします。何らかの問題が与えられたとき、その問題を その機械に解決させるためには、その機械がすでに知っている 方法をどのように組み合わせればいいのかという方法、 つまりその問題のアルゴリズムを、その機械に教える必要が あります。 関数というのはデータの一種ですが、それが表現しているのは アルゴリズムだと考えることができます。つまり、 コンピュータという機械に対して問題を解決する方法を 教えるために、コンピュータに理解できる形式でその方法を 表現することによってできたデータが関数なのです。

Q 6.1.6___アルゴリズムが再帰的な構造を持つこともあるのですか。

はい、あります。再帰的な構造を持っている問題を解決する場合は、 再帰的な構造を持つアルゴリズムを使うというのが自然です。 たとえば、乗算をするという問題は、再帰的な構造を持っています。 今、加算と減算の方法は知っているけれども乗算の方法は 知らないという機械に乗算をさせる必要があるとしましょう。 さて、その機械に乗算の方法を教えるためには、 いったいどんなふうに言えばいいのでしょうか。解答は こんなふうになります。 ● aと0とを乗算すると、その結果は0である。 ● aとbとを乗算すると、その結果は、bから1を減算した結果とaとを 乗算した結果に、aを加算した結果である。 つまり、乗算をするという再帰的な構造を持っている問題を 解決するための自然なアルゴリズムは、やはり再帰的な構造を 持つことになるということです。

Q 6.1.7___ものごとの繰り返しというのは、全体と同じ構造の ものを一部分として含んでいると思うのですが、それも再帰的な 構造を持っていると考えていいのですか。

はい、かまいません。 たとえば、 a aa aaa aaaa というような、aという文字を並べることによってできる 文字列のことを「a列」と呼ぶことにしましょう。aaaaというa列の 中には、aaaという部分が含まれていますが、このaaaというのは、 やはりa列です。つまり、一部分として全体と同じ構造のものを 含んでいるわけですから、a列というのは再帰的な構造を 持っていると考えることができます。 ものごとの繰り返しというのは再帰的な構造を 持っているわけですから、再帰的に定義することができます。 たとえば、a列の定義は、 ● aはa列である。 ● a列の右側にaを書いたものはa列である。 ● 以上の記述から導かれるもの以外はa列ではない。 というように再帰的に書くことができます。

6.2---再帰的な関数

Q 6.2.1___再帰的なアルゴリズムを利用して問題を解決する関数を 定義するためにはどうすればいいのですか。

そんなときは、関数の定義を再帰的に書きます。つまり、全体と同じ 構造になっている部分は、定義しようとしている関数を使って あらわせばいいのです。 例として、Q 6.1.2で再帰的な定義について説明するときに使った 括弧列というのをもう一度使うことにします。nが1以上の 整数だとするとき、n個の左丸括弧の右側にn個の 右丸括弧を書くことによってできる文字列のことを 「n重の括弧列」と呼ぶことにします。たとえば、 ((((())))) は5重の括弧列です。 さて、nが1以上の整数だとするとき、nに適用するとn重の括弧列を 返す、kakkoretsuという関数(型はint -> string)を 定義してみましょう。たとえば、 kakkoretsu 7 というように7にkakkoretsuを適用したとすると、 ((((((())))))) という括弧列が返ってくるようにしたいわけです。 n重の括弧列を求めるアルゴリズムは、 ● nが1ならば、n重の括弧列は1個の左丸括弧の右側に1個の 右丸括弧を書いたものである。 ● nが1よりも大きいならば、n重の括弧列は、1個の左丸括弧の 右側に(n-1)重の括弧列を書いて、そのさらに右側に1個の右丸括弧を 書いたものである。 というものです。kakkoretsuを定義するfun宣言は、 このアルゴリズムをMLの式の形に書き換えたものになります。 (n-1)重の括弧列を求めるところは、 kakkoretsu (n-1) というように、kakkoretsuを(n-1)に適用する式を書きます。 つまり、定義しようとしている関数の名前を、その定義の中で 使うわけです。kakkoretsuを定義するfun宣言は、 fun kakkoretsu n = if n=1 then "()" else if n>1 then "(" ^ kakkoretsu (n-1) ^ ")" else "" と書くことができます。 整数に適用される再帰的な関数を定義する場合は、0とかマイナスの 整数に適用した場合も困ったことが起きないようにしておく必要が あります。もしもkakkoretsuが、 fun kakkoretsu n = if n=1 then "()" else "(" ^ kakkoretsu (n-1) ^ ")" と定義されていたとすると、0またはマイナスの整数にkakkoretsuを 適用した場合、再帰するたびにnが1から遠ざかっていきますので、 いつまでたっても動作が終了しないということになってしまいます。 なお、関数は自分にとって不都合なデータに適用された場合に どうするべきかという問題については、第7章の「例外」 というところでもう一度考えてみることにします。

Q 6.2.2___再帰的なアルゴリズムをあらわしている関数は、 自分がまだ動作している途中で、自分自身を別のデータに 適用することになるわけですが、そんなことをしても混乱に 陥ったりしないのですか。

はい、大丈夫です。 関数が自分自身をデータに適用するというのは、自分とまったく 同じもの(言わば分身)を作って、それをデータに適用することだ と考えると、理解しやすいかもしれません(ただし、この考え方は、 コンピュータの物理的な動作としては正確ではありません)。 たとえば、Q 6.2.1で定義したkakkoretsuという関数は、7という 整数に適用された場合、自分の分身を作って、そいつに6重の 括弧列を作らせます。その分身も、やはり自分の分身を作って、 そいつに5重の括弧列を作らせます。そのようにして次々と分身が 作られていくのですが、1に適用された分身は、それ以上は 分身を作らずに"()"という1重の括弧列を返します。そして、 それぞれの分身は、自分が作った分身が返した括弧列を丸括弧で 囲んだものを返していくわけです。

Q 6.2.3___val宣言を使って関数を定義する場合でも、 定義しようとしている関数の名前を再帰的に使うことは 可能なんですか。

はい、可能です。ただし、val宣言を使って再帰的な関数を定義する 場合は、valという予約語の右側にrecという予約語を書く 必要があります。 例として、Q 6.2.1でfun宣言を使って書いたkakkoretsuの定義を val宣言で書き直すと、 val rec kakkoretsu = fn n => if n=1 then "()" else if n>1 then "(" ^ kakkoretsu (n-1) ^ ")" else "" というようになります。

Q 6.2.4___副作用のある再帰的な関数を作ることは可能ですか。

はい、可能です。 それでは、副作用のある再帰的な関数の例を実際に 作ってみましょう。たとえば、nが0またはプラスの 整数だとするとき、nに適用すると、n、(n-1)、(n-2)、……、0を この順番で出力する、countdownという関数(型はint -> unit)を 作ってみます。アルゴリズムは、 ● nがマイナスならば、何もしない。 ● nが0またはプラスならば、nを出力してから、(n-1)、(n-2)、 ……、0をこの順番で出力する。 ということになります。この中の「(n-1)、(n-2)、……、0を この順番で出力する」という部分は、(n-1)にcountdownを適用すれば いいわけです。ですから、 fun countdown n = if n>=0 then ( print (Int.toString n ^ " "); countdown (n-1) ) else () というfun宣言を書くことによってcountdownを定義することが できます。countdownを10に適用する式を対話型システムに 入力すると、 10 9 8 7 6 5 4 3 2 1 0 > val it = () : unit というように結果が出力されます。

6.3---相互再帰

Q 6.3.1___相互再帰的な構造って何ですか。

相互再帰的な構造というのは、2個以上のものがあって、それらが お互いに、自分以外のどれかを自分の一部分として含んでいる、 という構造のことです。たとえば、AとBとが相互再帰的な構造を 持っているとすると、Aの中にはBが含まれていて、Bの中にはAが 含まれているということになります。 具体的な例を使って説明しましょう。たとえば、 ([([([([()])])])]) のように、中央に丸括弧の対を書いて、その外側を角括弧と 丸括弧で交互に囲んでいって、もっとも外側に丸括弧の対を 書くことによってできる文字列のことを「丸角括弧列」 と呼ぶことにします。それから、 [([([([([()])])])])] のような、丸角括弧列と同じような作り方だけれども、もっとも 外側が角括弧になっている文字列のことを「角丸括弧列」 と呼ぶことにします。丸角括弧列と角丸括弧列は、相互再帰的な 構造を持っています。なぜなら、丸角括弧列はその一部分として 角丸括弧列を持っていて、角丸括弧列はその一部分として 丸角括弧列を持っているからです。

Q 6.3.2___相互再帰的な定義って何ですか。

相互再帰的な定義というのは、相互再帰的な構造を持っている いくつかのものを、それぞれがお互いを利用するという形で 定義することです。 たとえば、Q 6.3.1で説明した丸角括弧列と角丸括弧列の定義は、 ● ()は丸角括弧列である。 ● 角丸括弧列の左側に"("、右側に")"を書いたものは 丸角括弧列である。 ● 以上の記述から導かれるもの以外は丸角括弧列ではない。 ● 丸角括弧列の左側に"["、右側に"]"を書いたものは 角丸括弧列である。 ● 以上の記述から導かれるもの以外は角丸括弧列ではない。 というように相互再帰的に定義することができます。 相互再帰的な定義は、相互再帰的な構造を持っている いくつかのものを定義するための唯一の手段、というわけでは ありません。それらを別々に定義してもかまわないのです。 たとえば、丸角括弧列と角丸括弧列は、 ● ()は丸角括弧列である。 ● 丸角括弧列の左側に"(["、右側に"])"を書いたものは 丸角括弧列である。 ● 以上の記述から導かれるもの以外は丸角括弧列ではない。 ● [()]は角丸括弧列である。 ● 角丸括弧列の左側に"[("、右側に")]"を書いたものは 角丸括弧列である。 ● 以上の記述から導かれるもの以外は角丸括弧列ではない。 というように別々に定義することも可能です。しかし、相互再帰的な 構造を持っているいくつかのものを定義するときは、やはり 相互再帰的に定義するというのがもっとも自然です。

Q 6.3.3___相互再帰って何ですか。

相互再帰というのは、いくつかのものの定義のそれぞれが、 お互いに別のメンバーに言及することです。

Q 6.3.4___いくつかの関数を相互再帰的に定義するためには どうすればいいのですか。

そんなときは、それらの関数の定義を、andという予約語を 使って連結することによって、1個のfun宣言の中に詰め込みます。 具体的な例として、n重の丸角括弧列とn重の角丸括弧列を 求めるそれぞれの関数を相互再帰的に定義する、という問題について 考えてみましょう。n重の丸角括弧列とn重の角丸括弧列というのは、 ● nが1のとき、n重の丸角括弧列は"()"である。 ● nが2以上の整数のとき、n重の丸角括弧列は、(n-1)重の 角丸括弧列の左側に"("、右側に")"を書いたものである。 ● nが1以上の整数のとき、n重の角丸括弧列は、n重の丸角括弧列の 左側に"["、右側に"]"を書いたものである。 というように相互再帰的に定義されます。たとえば、 ([([([([()])])])]) は5重の丸角括弧列で、 [([([([([()])])])])] は5重の角丸括弧列です。 さて、nが1以上の整数だとするとき、nに適用するとn重の 丸角括弧列を返すmarukakuという関数と、nに適用するとn重の 角丸括弧列を返すkakumaruという関数は、どのように相互再帰的に 定義すればいいのでしょうか。もしも、 fun marukaku n = if n=1 then "()" else if n>=2 then "(" ^ kakumaru (n-1) ^ ")" else "" fun kakumaru n = if n>=1 then "[" ^ marukaku n ^ "]" else "" と書いたとするとどうなるでしょうか。MLの処理系は、宣言を 上から順番に処理しますので、marukakuを定義するfun宣言を 処理系が処理しているとき、kakumaruという識別子はまだ関数に 束縛されていません。ところが、marukakuを定義するfun宣言の 中には、そのkakumaruという識別子が書かれているわけですから、 処理系はそれをエラーだと判断します。 いくつかの関数を相互再帰的に定義したいときは、それらの定義を 1個のfun宣言の中に詰め込む必要があります。fun宣言は、 fun 識別子1 パターン1 = 式1 and 識別子2 パターン2 = 式2 …… and 識別子n パターンn = 式n というような書き方ができるようになっています。このような fun宣言を書いた場合、その中に書かれたn個の識別子は、それぞれの 関数に同時に束縛されることになります。したがって、marukakuと kakumaruの定義は、 fun marukaku n = if n=1 then "()" else if n>=2 then "(" ^ kakumaru (n-1) ^ ")" else "" and kakumaru n = if n>=1 then "[" ^ marukaku n ^ "]" else "" と書けばいいということになります。

---練習問題

6.1___nが1以上の整数だとするとき、次のように定義される 文字列のことを「n重の彼の発言」と呼ぶことにします。 ● nが1ならば、n重の彼の発言は、"私は何者なのだ。"である。 ● nが2以上ならば、n重の彼の発言は、"彼は「"の右側に(n-1)重の 彼の発言を連結して、その右側に"」と言った。"を 連結したものである。 nが1以上の整数だとするとき、nに適用するとn重の彼の発言を返す、 hesaidという関数(型はint -> string)を定義するfun宣言を 書いてください。

実行例 - hesaid 4; > val it = "彼は「彼は「彼は「私は何者なのだ。」と言った。」 と言った。」と言った。" : string

6.2___nが0またはプラスの整数だとするとき、次のように定義される 文字列のことを「nからの下降上昇整数列」と呼ぶことにします。 ● nが0ならば、nからの下降上昇整数列は、"0"である。 ● nが1以上ならば、nからの下降上昇整数列は、nをあらわす 10進数、1個の空白、(n-1)からの下降上昇整数列、1個の空白、 nをあらわす10進数を、この順序で左から右へ連結したものである。 nが0またはプラスの整数だとするとき、nに適用するとnからの 下降上昇整数列を返す、downupという関数(型はint -> string)を 定義するfun宣言を書いてください。

実行例 - downup 8; > val it = "8 7 6 5 4 3 2 1 0 1 2 3 4 5 6 7 8" : string

6.3___nが0またはプラスの整数だとするとき、nに適用するとnの 階乗を返す、kaijouという関数(型はint -> int)を定義する fun宣言を書いてください。

実行例 - kaijou 6; > val it = 720 : int [ヒント] nの階乗というのは、 n * (n-1) * (n-2) * …… * 1 という計算の結果のことですが、これは、 n * ((n-1)の階乗) という再帰的な構造を持っています。ですから、階乗を求める 関数は、(n-1)の階乗を自分の分身に求めさせて、自分自身は 分身が求めた(n-1)の階乗とnとを乗算したものを返せばいい、 ということになります。 階乗を求める関数は、nが0の場合も、正しい結果を返さないと いけません(0の階乗は1です)。そのためには、nが0のときは 分身を使わずに1を返す、と書いておく必要があります。 また、この記述は再帰の基底としても使われることになります。

6.4___aが整数で、bが0またはプラスの整数だとするとき、 (a,b)という組に適用するとaのb乗を返す、bekijouという関数(型は int * int -> int)を定義するfun宣言を書いてください。

実行例 - bekijou (3,5); > val it = 243 : int

6.5___aとbが0またはプラスの整数だとするとき、(a,b)という組に 適用するとaとbの最大公約数を返す、gcmという関数(型は int * int ->int)を定義するfun宣言を書いてください。

実行例 - gcm (60,84); > val it = 12 : int [ヒント] aとbが0またはプラスの整数だとするとき、aとbの最大公約数を gcm (a,b)と書くことにして、aをbで除算したときのあまりを a mod bと書くことにします。そうすると、aとbとgcm (a,b)との あいだには、 ● bが0ならば、gcm (a,b)はaと等しい。 ● bが1以上ならば、gcm (a,b)はgcm (b,a mod b)と等しい。 という関係が成り立ちます。この関係を使って最大公約数を 求めるというアルゴリズムのことを「ユークリッドの互除法」 と呼びます。

6.6___nが0またはプラスの整数だとするとき、nに適用すると フィボナッチ数列の第n項を返す、fibonaという関数(型は int -> int)を定義するfun宣言を書いてください。 フィボナッチ数列というのは、

● nが0または1ならば、第n項は1である。 ● nが2以上ならば、第n項は、第n-2項と第n-1項とを加算した 結果である。 というように再帰的に定義される数列のことです。 実行例 - fibona 8; > val it = 34 : int

6.7___nが0またはプラスの整数だとするとき、nに適用するとnを あらわす2進数を返す、binaryという関数(型はint -> string)を 定義するfun宣言を書いてください。

実行例 - binary 83; > val it = "1010011" : string [ヒント] 0またはプラスの整数を2進数に変換するための基本的な アルゴリズムは、 ● nが0ならば、nをあらわす2進数は"0"である。 ● nが1以上ならば、nをあらわす2進数は、nを2で除算した商を あらわす2進数の右側に、nを2で除算したあまりをあらわす数字 (0または1)を連結したものである。 ということになるのですが、これをそのままbinaryの定義にすると、 1以上の整数にbinaryを適用した結果の左端に余分な"0"が 付いてしまいます。ですから、基底の部分を、 ● nが0ならば、nをあらわす2進数は""である。 に変えたアルゴリズムをbinary1という補助的な関数にしておいて (let式のletとinのあいだにfun宣言を書きます)、 binaryは、 if n=0 then "0" else binary1 n という式で定義するといいでしょう。

6.8___nが2以上の整数だとするとき、nに適用するとnを 素因数分解した結果をあらわす文字列を返す、soinsuuという 関数(型はint -> string)を定義するfun宣言を書いてください。

実行例 - soinsuu 11319; > val it = "3 7 7 7 11 " : string [ヒント] まず、soinsuu1という補助的な関数を定義します。soinsuu1は、 nとmが2以上の整数で、nはmよりも小さな素因数を持っていない とするとき、(n,m)という組に適用すると、nを素因数分解した 結果を返します。この関数は、 ● nが1の場合は空文字列を返す。 ● nをmで除算したときに割り切れる場合は、nをmで除算したときの 商とmから構成される組に自分の分身を適用して、mをあらわす 文字列の右側に分身の戻り値を連結したものを返す。 ● nをmで除算したときに割り切れない場合は、nと、mに1を加算した 結果から構成される組に自分の分身を適用して、分身の戻り値を そのまま返す。 というアルゴリズムを使うことによって定義することができます。 このようにsoinsuu1を定義しておけば、soinsuuは、 soinsuu1 (n,2) という式の値を返せばいいだけ、ということになります。

6.9___Francois Edouard Anatole Lucas (1842-1891)という 数学者によって作られた、「ハノイの塔」と呼ばれる パズルがあります。まず、このパズルで使われる2種類の道具を 紹介しましょう。

(a) 垂直に立てられた3本の棒。それらの棒には、向かって左から 順番に、A、B、Cという名前が付けられています。 (b) その棒に嵌め込むことのできる、中央に穴のあいた円盤。円盤は n枚あって(nは0またはプラスの整数)、1番目、2番目、……、 n番目、というように番号が付いています。それらの円盤は、1番目の 直径がもっとも小さくて、2番目、3番目と番号が 大きくなるにしたがって直径も大きくなっていきます。 ハノイの塔というのは、これらの道具を使って作られた状態を、 定められた規則にしたがって初期状態から最終状態へ 移行させるための手順を求めよ、というパズルです。 初期状態と最終状態と規則は、次のようになっています。 (a) 初期状態では、棒Aのみにすべての円盤が嵌め込まれています。 それらの円盤は、上から下へ向かって、1番目、2番目、3番目、 という順番になっています。つまり、全体が円錐のような 形になるように重ねられているわけです。 (b) 最終状態というのは、棒Cのみにすべての円盤が 嵌め込まれている、という状態です。それらの円盤が重なっている 順番は、初期状態と同じです。 (c) 1回の操作では、いずれかの棒に嵌め込まれた円盤のうち、 もっとも上にある1枚を棒から取り出して、別の棒に 嵌め込むことができます。2枚以上の円盤を1回の操作で 移動させることはできません。 (d) どの円盤も、自分よりも直径の小さな円盤の上に載ることは できません。つまり、下にある円盤のほうが上にある円盤よりも 直径が大きい、という関係がいつでも成り立っていないといけない ということです。 さて、それでは問題です。nが0またはプラスの整数だとするとき、 nに適用すると、n枚の円盤を持つハノイの塔の解答をあらわす 文字列を返す、hanoiという関数(型はint -> string)を定義する fun宣言を書いてください。 実行例 - hanoi 4; > val it = "[A->B][A->C][B->C][A->B][C->A][C->B][A->B][A->C] [B->C][B->A][C->A][B->C][A->B][A->C][B->C]" : string [ヒント] まず、hanoi1という補助的な関数を定義することにします。 hanoi1は、円盤の枚数、円盤の初期位置の棒の名前、作業用の棒の 名前、目的地の棒の名前、という4個のデータから構成される組に 適用すると、円盤を移動させる手順を返す、という関数です (型はint * string * string * string -> string)。 ハノイの塔の初期状態では、n枚の円盤が円錐の形に 積み重なっているわけですが、その円錐を、いちばん底にある n番目の円盤と、その上に積み重なっている円錐、という二つの 部分に分けて考えることにしましょう。なお、1番目から (n-1)番目までの円盤で構成される円錐のことを「(n-1)円錐」 と呼ぶことにします。すると、ハノイの塔を解く手順は、 (イ) (n-1)円錐を初期位置から作業用の棒へ移動させる。 (ロ) n番目の円盤を初期位置から目的地へ移動させる。 (ハ) (n-1)円錐を作業用の棒から目的地へ移動させる。 という三つの部分に分けることができます。これらの部分のうちの (イ)と(ハ)は全体の構造と同じですので、hanoi1は、それらを自分の 分身にやらせることによって定義することができます。 hanoi1が定義できたならば、hanoiは、 hanoi1 (n,"A","B","C") という式を書くことによってhanoi1にハノイの塔を解かせればいい、 ということになります。

6.10___nが0またはプラスの整数だとするとき、nに適用すると、0、 1、2、3、……、nをこの順番で出力する、countupという関数(型は int -> unit)を定義するfun宣言を書いてください。

実行例 - countup 10; 0 1 2 3 4 5 6 7 8 9 10 > val it = () : unit

6.11___nが1以上の整数だとするとき、「n重の彼女の発言」という 文字列と「n重の彼の発言」という文字列を、次のように 定義することにします。

● nが1ならば、n重の彼女の発言は、"わたしはいったい誰なの。" である。 ● nが2以上ならば、n重の彼女の発言は、"彼は「"の右側に (n-1)重の彼の発言を連結して、その右側に"」と言った。"を 連結したものである。 ● nが1以上ならば、n重の彼の発言は、"彼女は「"の右側にn重の 彼女の発言を連結して、その右側に"」と言った。"を 連結したものである。 nが1以上の整数だとするとき、nに適用すると、n重の彼女の発言を 返す、herspeechという関数(型はint -> string)と、nに 適用すると、n重の彼の発言を返す、hisspeechという関数(型は int -> string)を相互再帰的に定義するfun宣言を書いてください。 herspeechの実行例 - herspeech 4; > val it = "彼は「彼女は「彼は「彼女は「彼は「彼女は「わたしは いったい誰なの。」と言った。」と言った。」と言った。」 と言った。」と言った。」と言った。" : string hisspeechの実行例 - hisspeech 4; > val it = "彼女は「彼は「彼女は「彼は「彼女は「彼は「彼女は 「わたしはいったい誰なの。」と言った。」と言った。」 と言った。」と言った。」と言った。」と言った。」と言った。" : string

第7章===例外

7.1---例外を発生させる方法

Q 7.1.1___関数が、自分にとって不都合なデータに適用された場合、 自分をそのデータに適用しようとした関数に対して、それが不都合な データだということを知らせるためのいい方法はないのですか。

あります。そんなときは例外というものを発生させれば いいのです。 例外というのは、exnという型を持つデータのことです。関数は、 「こういう条件が成り立ったときはこういう例外を発生させる」 というように定義しておくことができます。関数が例外を 発生させると、その関数は、戻り値を返さずにその時点で動作を 終了します。そして、関数が発生させた例外は、その関数を データに適用した関数へ移動します。 また、関数は、「この関数がこういう例外を発生させた場合は こういう動作をする」というように定義しておくということも できます。つまり、 ● func1という関数は、特定の条件が成り立った場合はExceptという 例外を発生させる。 ● func2という関数は、func1をデータに適用して、func1が普通に 終了した場合はAという動作をして、func1がExceptという例外を 発生させた場合はBという動作をする。 というように二つの関数を定義しておくことができるのです。 関数は、定義域に属しているデータならばどんなものにでも 適用することができます。でも、定義域に属しているからと言って、 それに対応する妥当な値域の要素がかならず存在するとは 限りません。関数は、そのような不都合なデータに適用された 場合、自分を使って何かをしようとした関数にそのことを知らせる 必要があります。例外を発生させるというのは、不都合なデータに 適用された関数が、自分をデータに適用した関数にそのことを 知らせるための、もっとも適切な方法なのです。 たとえば、divという組み込み演算子は、(42,0)とか (63,0)というような、右側が0になっている組に適用された場合、 妥当な戻り値を返すことができませんので、そのことを 知らせるためにDivという識別子であらわされる例外を 発生させます。ですから、MLの対話型システムに、 42 div 0 というval宣言を入力したとすると、対話型システムは、Divという 例外が発生したということを知らせるメッセージを出力します。

Q 7.1.2___関数を定義するとき、その関数が独自の例外を 発生させるようにしたいのですが、そんなときはどうすれば いいのですか。

独自の例外を発生させる関数を定義したいときは、まず、 exnという型に独自の例外を追加しておきます。そして、例外を 発生させるための式をfun宣言の中に書きます。 exnに独自の例外を追加する方法についてはQ 7.1.3で、例外を 発生させる式の書き方についてはQ 7.1.5で説明します。

Q 7.1.3___exnに独自の例外を追加するためにはどうすれば いいのですか。

exnに独自の例外を追加したいときは、exception宣言というものを 書きます。 exception宣言というのは、宣言の一種で、 exception 識別子 という構文を持っています。exception宣言をコンピュータに 実行させると、コンピュータは、その宣言の中に書かれた 識別子によって指定することのできる新しい例外をexnに 追加します。たとえば、 exception Komatta という宣言をコンピュータに実行させたとすると、コンピュータは、 Komattaという例外名によって指定することのできる新しい例外を exnに追加します(注)。 (注) MLでは、例外名にする識別子は先頭を大文字にする、という慣例が ありますので、この文章でもそれに従うことにします。 exception宣言があらわしている動作をもう少し厳密に書くと、 (1) 新しい例外をexnに追加する。 (2) (1)で追加された例外を作り出すという動作をするものを新しく ひとつ作る。 (3) exception宣言の中に書かれた識別子を、(2)で作られたものに 束縛する。 ということになります。なお、例外を作り出すという 動作をするもののことを、「例外構成子」と呼びます。

Q 7.1.4___1個のexception宣言で2個以上の例外をexnに 追加したいのですが、どうすればいいのですか。

1個のexception宣言で2個以上の例外をexnに追加したい というときは、追加したい例外と同じ個数の識別子をandで区切って 並べます。 つまり、exception宣言は、 exception 識別子 and 識別子 and …… and 識別子 というように書くことができて、このようなexception宣言が 実行されると、それぞれの識別子によって指定できる、識別子と同じ 個数の例外がexnに追加されます。たとえば、 exception TooLarge and TooSmall という宣言を書くことによって、2個の例外がexnに追加されて、 それぞれをTooLargeとTooSmallという例外名で指定することが できるようになります。

Q 7.1.5___例外を発生させたいときはどうすればいいのですか。

例外を発生させたいときは、raise式というものを書きます。 raise式というのは、式の一種で、 raise 例外名 と書きます。raise式をコンピュータに評価させると、 コンピュータは、その式の中に書かれた例外名によって指定される 例外を発生させます。たとえば、 raise Komatta という式をコンピュータに評価させたとすると、コンピュータは、 Komattaという名前の例外を発生させます。

Q 6.2.1で書いた、n重の括弧列を作るkakkoretuという関数を 定義するfun宣言は、0またはマイナスの整数にkakkoretsuが 適用されたときは空文字列を返すようになっていました。しかし、 nが0またはマイナスのとき、n重の括弧列というのは 存在しないわけですから、その場合は例外を発生させるというのが 望ましい動作です。

それでは、0またはマイナスの整数に適用された場合に、 Kakkoretsuという例外を発生させるように、kakkoretsuの定義を 修正してみましょう。まず、 exception Kakkoretsu というexception宣言で、例外名を定義しておきます。次に、 kakkoretsuを定義するfun宣言を、 fun kakkoretsu n = if n=1 then "()" else if n>1 then "(" ^ kakkoretsu (n-1) ^ ")" else raise Kakkoretsu というように書きなおします。こうしておけば、たとえば、 kakkoretsu ~7 という式でkakkoretsuを~7に適用したとすると、Kakkoretsuという 例外が発生することになります。

Q 7.1.6___raise式を評価すると、どのような値が 得られるのですか。

raise式の評価は、例外を発生させることによって終了しますので、 raise式には値というものがありません。

Q 7.1.7___raise式を書く場合、その場所が、特定の型の値を生じる 式を必要としていたとしても、何も問題はないのですか。

はい、問題はまったくありません。 たとえば、 if a=0 then 0 else raise Komatta という式の場合は、intの値を生じる式が要求される場所にraise式が 書かれていて、 if a=0 then "OK" else raise Komatta という式の場合は、stringの値を生じる式が要求される場所に raise式が書かれているわけですが、どちらもエラーには なりません。

7.2---例外の捕獲

Q 7.2.1___例外を捕獲するというのはどういうことですか。

例外を捕獲するというのは、動作中の関数を通過していく例外の 移動をストップさせることです。 例外というデータは、自分が捕獲されるまでのあいだ、動作中の 関数を次々と終了させながら通過していく、という性質を 持っています。 たとえば、Aという関数がBという関数をデータに適用して、Bが Cという関数をデータに適用して、CがDという関数をデータに 適用して、DがEという関数をデータに適用したとき、Eが例外を 発生させたとしましょう。もしも、それらの関数のいずれもが、 その例外を捕獲しなかったとすると、その例外は、E、D、C、B、 Aという順番で、それらの関数を終了させながら通過していきます。 しかし、もしもCが、その例外を捕獲するように定義されていた とすると、その例外は、EとDを終了させたのちにCによって 捕獲されますので、CとBとAは動作を続行することになります。

Q 7.2.2___例外を捕獲したいときはどうすればいいのですか。

例外を捕獲したいときは、handle式というものを書きます。 handle式というのは、式の一種で、 式0 handle 例外名1 => 式1 というように書きます。handle式をコンピュータに評価させると、 コンピュータは、まず、その先頭に書かれた式0を評価します。 式0の評価が、例外を発生させずに終了した場合は、その値を handle式全体の値にします。そうではなくて、式0の評価の途中で 例外が発生した場合は、その例外の名前と、例外名1とを 照合します。その照合が成功した場合は、=>の右側に書かれた式1を 評価して、その値をhandle式全体の値にします(この場合、 その例外は捕獲されたということになります)。そうではなくて、 発生した例外の名前とhandle式の中の例外名との照合が 成功しなかった場合、その例外は捕獲されません。 たとえば、 fun msgdiv (a,b) = a div b handle Div => (print "division by zero\n"; 0) というfun宣言で、msgdivという関数を定義したとしましょう。 msgdivを(52,7)に適用した場合、msgdivは、52を7で除算した結果を 返します。 それでは、msgdivを(52,0)に適用した場合はどうなるでしょうか。 その場合、divが例外を発生させるわけですが、その例外の名前と Divという例外名との照合は成功しますので、その例外は 捕獲されることになります。したがって、msgdivは、 division by zero という文字列を出力して、戻り値として0を返します。 なお、handle式を書くとき、handleという単語の左側に書く式と、 =>の右側に書く式とは、型が一致していないといけません。 ですから、 a div b handle Div => "division by zero" というhandle式は、その中の式の型が一致していませんので、 エラーになってしまいます。

Q 7.2.3___exception宣言で追加した独自の例外も、捕獲の方法は 組み込み関数が発生させる例外と同じなんですか。

はい、同じです。 たとえば、kakkoretsuという関数が、Q 7.1.5で書いたfun宣言で 定義されているとするとき、 fun msgkakkoretsu n = kakkoretsu n handle Kakkoretsu => (print "The argument is zero or minus.\n"; "") というfun宣言で、msgkakkoretsuという関数を定義した としましょう。すると、 - msgkakkoretsu ~7; The argument is zero or minus. > val it = "" : string というように、kakkoretsuが発生させた例外はhandle式によって 捕獲されて、メッセージが出力されることになります。

Q 7.2.4___1個の式が発生させる可能性のある2個以上の異なる 例外を捕獲したいときはどうすればいいのですか。

2個以上の異なる例外を捕獲したいときは、handle式の中に、 必要なだけの例外名と式のペアを、縦棒で区切って並べます。 handle式の中には、 式0 handle 例外名1 => 式1 | 例外名2 => 式2 | …… | 例外名n => 式n というように、例外名と式のペアを何個でも書くことができます。 その中の式0が例外を発生させた場合、コンピュータは、handle式の 中に書かれた例外名とその例外の名前との照合が 成功するかどうかを、先頭から順番に調べていきます。そして、 成功するものがあった場合は、その例外名とペアになっている式を 評価して、その値をhandle式全体の値にします。 具体的な例を使って説明しましょう。まず、 exception Zero and Minus fun kakkoretsu n = if n=1 then "()" else if n>1 then "(" ^ kakkoretsu (n-1) ^ ")" else if n=0 then raise Zero else raise Minus というように、kakkoretsuという関数が定義されているとします。 この場合、kakkoretsuをデータに適用する式は、そのデータが 0だった場合はZeroという例外を発生させ、マイナスだった場合は Minusという例外を発生させます。 さて、それでは、それらの例外を捕獲して、それぞれの場合に応じた メッセージを出力したい、というときはどうすれば いいのでしょうか。そんなときは、ZeroとMinusの それぞれに対応する例外名と式のペアを、縦棒で区切って並べます。 つまり、 kakkoretsu a handle Zero => (print "The argument is zero.\n"; "") | Minus => (print "The argument is minus.\n"; "") というように書けばいいわけです。

Q 7.2.5___局所的なスコープを持つ例外名を作ることは可能ですか。

はい、可能です。 exception宣言をlet式の中に書くと、それによってできた例外名は、 そのlet式の中だけがスコープになります。たとえば、 fun msgkakkoretsu n = let exception Undefined fun kakkoretsu n = if n=1 then "()" else if n>1 then "(" ^ kakkoretsu (n-1) ^ ")" else raise Undefined in kakkoretsu n handle Undefined => (print "The argument is zero or minus.\n"; "") end というfun宣言でmsgkakkoretsuを定義したとすると、 この中で使われているUndefinedという例外名はlet式の中だけが スコープになりますので、ほかの関数を定義するときに、 Undefinedという識別子を別の用途で使うことができます。

7.3---例外引数

Q 7.3.1___不都合な事態が生じたために関数が例外を 発生させるとき、それがどんな事態なのかということについての 具体的な情報を、その例外を捕獲する関数に伝えることができれば とても便利だと思うのですが、そんなことは可能ですか。

はい、可能です。 例外は、関数から関数へと移動していくときに、伴走者として1個の データを連れていくことができます。そのようなデータのことを 「例外引数」と呼びます。例外引数を伴う例外を発生させる 方法についてはQ 7.3.2で、捕獲した例外に付属している例外引数を 利用する方法についてはQ 7.3.3で説明します。

Q 7.3.2___例外引数を伴う例外を発生させるためには どうすればいいのですか。

例外引数を伴う例外を発生させたいときは、引数を受け取る 例外構成子、というものを使います。 引数を受け取る例外構成子というのは、データに適用すると、 そのデータを例外引数として伴う例外を作り出す、という動作をする 例外構成子のことです。 引数を受け取る例外構成子は、 exception 識別子 of 型式 という構文を持つexception宣言を書くことによって作ることが できます。ofの右側には、例外引数にしたいデータの型をあらわす 型式を書きます。 このようなexception宣言をコンピュータに実行させると、 コンピュータは、引数を受け取る例外構成子を作って、 exception宣言の中に書かれた識別子をその例外構成子に 束縛します。たとえば、 exception Impossible of int というexception宣言を書くことによって、整数に 適用することのできる例外構成子が作られて、Impossibleという 識別子が、その例外構成子に束縛されます。 例外引数を伴う例外は、 例外名 式1 という形の式を書くことによって作り出すことができます。この式が 評価されると、例外名が束縛されている例外構成子が式1の値に 適用されて、その例外名を名前として持ち、式1の値を 例外引数として伴う例外が、例外構成子によって作り出されます。 たとえば、 Impossible 64 という式を評価すると、Impossibleという例外構成子が64という 整数に適用されて、その結果として、Impossibleという名前を 持つ、64という例外引数を伴う例外が作り出されます。 ですから、 raise 例外名 式 というraise式を評価すれば、例外引数を伴う例外が 発生することになります。たとえば、 raise Impossible 47 というraise式によって、Impossibleという名前を持つ、47という 整数を例外引数として伴う例外を発生させることができます。

Q 7.3.3___例外引数を伴う例外を捕獲して、その例外引数を 利用するためにはどうすればいいのですか。

そんなときは、handle式の中に、例外引数と照合させることのできる パターンを書きます。 例外引数を伴う例外を捕獲するためのhandle式は、 式0 handle 例外名1 パターン1 => 式1 と書きます。このようなhandle式をコンピュータが評価していて、 もしも式0の評価の途中で例外が発生したとすると、 コンピュータは、発生した例外の名前と例外名1とを照合して、 さらに、その例外に付属している例外引数とパターン1とを 照合します。そして、例外名と例外引数の照合が両方とも成功した 場合、式1を評価します。 それでは、具体的な例を使って説明しましょう。まず、 exception Kakkoretsu of int fun kakkoretsu n = if n=1 then "()" else if n>1 then "(" ^ kakkoretsu (n-1) ^ ")" else if n=0 then raise Kakkoretsu 1 else raise Kakkoretsu 2 というように、kakkoretsuという関数を定義したとします。この kakkoretsuは、0に適用された場合は1、マイナスの整数に適用された 場合は2を例外引数として伴う例外を発生させます。この kakkoretsuが発生させる例外を捕獲して、それに付属している 例外引数を利用したいときは、たとえば、 kakkoretsu n handle Kakkoretsu code => (print ("error: " ^ Int.toString code ^ "\n"); "") というようなhandle式を書きます。このhandle式によって整数に 適用されたkakkoretsuが例外を発生させた場合は、その例外の名前と Kakkoretsuという名前とが照合され、さらに、その例外に 付属している例外引数とcodeという識別子とが 照合されることになります。そして、もしも例外名と例外引数の 両方の照合が成功したならば、=>の右側の式が評価されて、 codeの値、つまり例外引数が出力されることになります。

Q 7.3.4___例外引数がどんなパターンと一致するか ということによって動作が選択されるようにしたいのですが、 そんなときはどうすればいいのですか。

そんなときは、handle式の中に、例外名とパターンと式の 組み合わせを縦棒で区切って並べます。 handle式の中に、 式0 handle 例外名1 パターン1 => 式1 | 例外名2 パターン2 => 式2 | …… | 例外名n パターンn => 式n というように、例外名とパターンと式の組み合わせを縦棒で区切って 並べておくと、例外が発生した場合、コンピュータは、その例外と 例外引数に一致する組み合わせがあるかどうかを先頭から順番に 調べていきます。 たとえば、kakkoretsuという関数が、例外引数として1または2を 伴う例外を発生させるように定義されているとするとき、 kakkoretsu n handle Kakkoretsu 1 => (print "The argument is zero.\n"; "") | Kakkoretsu 2 => (print "The argument is minus.\n"; "") というhandle式を書いておけば、例外が発生した場合、それに 付属する例外引数が1なのか2なのかということによって、出力する メッセージが選択されることになります。

---練習問題

7.1___練習問題6.3で作ったkaijouという関数(nが0またはプラスの 整数だとするとき、nに適用するとnの階乗を返す関数)を改良して、 引数がマイナスだった場合はKaijouという名前の例外を 発生させるようにしてください。

実行例(1) - kaijou 6; > val it = 720 : int 実行例(2) - kaijou ~6 handle Kaijou => (print "error\n"; 0); error > val it = 0 :int

7.2___chrという組み込み関数(整数に適用すると、その整数と同じ ビット列のパターンであらわされる文字を返す関数)は、マイナスの 整数に適用したり、対応する文字が存在する最大の整数よりも大きい 整数に適用したりすると、Chrという名前の例外を発生させます。

整数に適用すると、その整数と同じビット列のパターンで あらわされる文字を返す、safetychrという関数(型は int -> char)を定義するfun宣言を、chrを使って書いてください。 ただし、chrがChrという例外を発生させた場合は、それを捕獲して、 ピリオド(#".")を返すようにしてください。 実行例(1) - safetychr 77; > val it = #"M" : char 実行例(2) - safetychr ~63; > val it = #"." : char

7.3___整数に適用すると、その整数にkaijou(練習問題7.1で 定義したもの)を適用して、その戻り値を返す、msgkaijouという 関数(型はint -> int)を定義するfun宣言を書いてください。 ただし、kaijouがKaijouという例外を発生させた場合は、それを 捕獲して、 "The argument is minus.\n" というメッセージを出力して、0を返すようにしてください。

実行例(1) - msgkaijou 6; > val it = 720 : int 実行例(2) - msgkaijou ~6; The argument is minus. > val it = 0 : int

7.4___nが0またはプラスの整数で、mが0以上でn以下の 整数だとするとき、(n,m)という組に適用すると、n個の異なるものの 中からm個のものを取り出す組み合わせの個数を返す、combiという 関数(型はint * int -> int)を定義するfun宣言を 書いてください。ただし、引数が適切なものではなかった場合は、 例外引数として1個の整数を伴うCombiという名前の例外を 発生させるようにしてください。Combiに伴う例外引数は、nが マイナスだった場合は1、nは0またはプラスだけれどもmが マイナスだった場合は2、nもmも0またはプラスだけれどもmがnよりも 大きかった場合は3です。

[ヒント] 組み合わせの個数を求めるアルゴリズムは、再帰的な構造を 持っています。 n個の異なるものの中からm個のものを取り出す組み合わせの個数を 求める場合、まず、それらのn個のものを、1個だけのグループと (n-1)個のグループに分けます。そうすると、組み合わせの個数は、 (a) 1個だけのグループからは要素を取り出さない場合 (b) 1個だけのグループからその1個を取り出す場合 という二つの場合に分けて考えることができます。全体の個数は、 それぞれの場合の組み合わせの個数を加算すればいいわけです。 (a)の場合は(n-1)個の中からm個を取り出す組み合わせの個数と いうことになり、(b)の場合は(n-1)個の中から(m-1)個を取り出す 組み合わせの個数ということになります。 なお、このアルゴリズムの基底は、「mが0であるかまたはnとmとが 等しい場合、組み合わせの個数は1個しかない」というものです。 実行例(1) - combi (4,2); > val it = 6 : int 実行例(2) - combi (~4,2) handle Combi ec => (print "error\n"; ec); error > val it = 1 : int 実行例(3) - combi (4,~2) handle Combi ec => (print "error\n"; ec); error > val it = 2 : int 実行例(4) - combi (4,5) handle Combi ec => (print "error\n"; ec); error > val it = 3 : int

7.5___2個の整数から構成される組に適用すると、その組に combi(練習問題7.4で作ったもの)を適用して、その戻り値を 返す、msgcombiという関数(型はint * int -> int)を定義する fun宣言を書いてください。ただし、combiがCombiという例外を 発生させた場合は、それを捕獲して、 ● 例外引数が1の場合: "n is minus.\n" ● 例外引数が2の場合: "m is minus.\n" ● 例外引数が3の場合: "m is larger than n.\n" というメッセージを出力して、いずれの場合も戻り値として0を 返すようにしてください。

実行例(1) - msgcombi (4,2); > val it = 6 : int 実行例(2) - msgcombi (~4,2); n is minus. > val it = 0 : int 実行例(3) - msgcombi (4,~2); m is minus. > val it = 0 : int 実行例(4) - msgcombi (4,5); m is larger than n. > val it = 0 : int

第8章===多相型と等値型

8.1---多相型

Q 8.1.1___関数の定義域は、かならず何らかの特定の 型でなければいけないのですか。

いいえ、そんなことはありません。どんな型のデータにも 適用することのできる関数、というものを定義することも可能です。 たとえば、 fun hundred _ = 100 というfun宣言で、hundredという関数を定義したとしましょう。 この関数は、 - hundred 37; > val it = 100 : int - hundred 8.011; > val it = 100 : int - hundred "Michelangelo"; > val it = 100 : int - hundred (56,(#"U",177.7)); > val it = 100 : int というように、どんな型のデータにも適用することができます。

Q 8.1.2___恒等関数って何ですか。

恒等関数というのは、どんな型のデータにも適用することができて、 そのデータをそのまま返すという動作をする関数のことです。 それでは、identityという名前で恒等関数を 定義してみることにしましょう。fun宣言は、 fun identity x = x というように書くことができます。このidentityは、 - identity 95; > val it = 95 : int - identity 7.23; > val it = 7.23 : real - identity "Rembrandt"; > val it = "Rembrandt" : string - identity ((106.1,#"8"),33); > val it = ((106.1, #"8"), 33) : (real * char) * int というように、どんな型のデータにも適用することができて、 そのデータがそのまま戻り値になります。

Q 8.1.3___組に適用することのできる関数で、その組の要素の個数は 決まっているけれども要素の型は何でもいい、というものを 定義することは可能ですか。

はい、可能です。 たとえば、 fun replace (x,y) = (y,x) というfun宣言によって定義されるreplaceという関数は、組に 適用することができて、その組の要素の個数は決まっているけれども 要素の型は何でもいい、という関数の一例です。replaceは、 - replace (66,23.47); > val it = (23.47, 66) : real * int - replace (9.97,"Botticelli"); > val it = ("Botticelli", 9.97) : string * real - replace (38,(#"?",22.2)); > val it = ((#"?", 22.2), 38) : (char * real) * int というように、2個の要素を持つ組に適用すると、それらの要素の 順番を逆にすることによってできる組を返します。

Q 8.1.4___多相型関数って何ですか。

多相型関数というのは、任意の型、または構造の一部分として任意の 型を含む型が定義域になっているような関数のことです。 たとえば、Q 8.1.1で定義したhundredや、Q 8.1.2で定義した identityは、任意の型が定義域になっていますので、 多相型関数だということになります。同じように、Q 8.1.3で 定義したreplaceという関数も、任意の型のデータを要素とする組が 定義域になっていますので、やはり多相型関数だとみなすことが できます。

Q 8.1.5___型変数って何ですか。

型変数というのは、先頭の文字がアポストロフィー(')であるような 識別子のことです。 たとえば、 'a 'namako '239 というような識別子は、型変数だとみなされます。

Q 8.1.6___型変数をデータに束縛することはできるのですか。

いいえ、それはできません。 型変数は、特別な用途で使用される識別子ですので、普通の 識別子とは違って、データに束縛するという使い方はできません。 たとえば、 val 'medaka = 463 というval宣言や、 fun 'gobai n = n*5 というfun宣言をMLの処理系に与えたとすると、MLの処理系は 何らかのエラーメッセージを出力します。

Q 8.1.7___多相型関数の型は、どのような型式で あらわされるのですか。

多相型関数の型は、型変数を含む型式によってあらわされます。 たとえば、Q 8.1.1で書いたhundredという関数を定義するfun宣言を MLの対話型システムに入力したとすると、対話型システムは、 > val hundred = fn : 'a -> int という応答を出力します。つまり、対話型システムは、hundredの 型を'a -> intという型式で表現しているわけです。この型式の中に 含まれている'aという型変数は、「任意の型」という型を あらわしています。

Q 8.1.8___引数の型によって戻り値の型が決まるような多相型関数の 値域は、どのような型式によってあらわされるのですか。

そのような関数の値域は、定義域をあらわすために使われたものと 同じ型変数を使うことによってあらわされます。 Q 8.1.3で書いた、identityを定義するfun宣言を対話型システムに 入力すると、対話型システムは、 > val identity = fn : 'a -> 'a という応答を出力します。つまり、対話型システムは、identityの 定義域と値域とを'aという同じ型変数であらわすことによって、 「この関数の戻り値は引数と同じ型である」ということを 述べているわけです。また、 fun replace (x,y) = (y,x) というfun宣言を対話型システムに入力した場合は、 > val replace = fn : 'a * 'b -> 'b * 'a という応答が出力されます。この場合は、「戻り値の1個目の要素の 型は引数の2個目の要素と同じで、戻り値の2個目の要素の 型は引数の1個目の要素と同じである」という意味になります。

8.2---等値型

Q 8.2.1___等値型って何ですか。

何らかの型があって、その型に属している二つのデータが 同一であるかどうかを調べることができる場合、 その型は「等値型である」と言われます。 基本型のうちのint、real、word、string、char、boolなどは、 =または<>という組み込み演算子を使うことによって、同じ型に 属する二つのデータが同一かどうかということを 調べることができますので、等値型だとみなすことができます。 それに対して、関数型やexnは、それに属するデータのあいだの 同一性について調べることができませんので、 等値型ではありません。

Q 8.2.2___積型は等値型なんですか。

積型は、それを構成している型がすべて等値型ならば、その全体も 等値型になります。それに対して、等値型ではない型を ひとつでも含んでいる積型は、等値型ではありません。 たとえば、 int * int int * real * string (string * char) * (int * real) などの積型は、等値型のみから構成されていますので、その全体も 等値型です。しかし、 (int -> int) * int string * (int -> char) real * (string -> int) * string などの積型は、関数型という等値型ではない型を含んでいますので、 等値型とは言えません。

Q 8.2.3___組み込み関数の=や<>のように、引数として受け取った 組を構成するデータのあいだに同一性があるかどうかを調べるという 動作を含んでいる関数で、なおかつ、引数を構成する型が、 等値型でありさえすれば何でもかまわない、というようなものを 定義することは可能ですか。

はい、可能です。 たとえば、 fun equal (x,y) = if x=y then 1 else 0 というfun宣言によって定義されるequalという関数は、引数を 構成しているデータのあいだに同一性があるかどうかを調べる という動作を含んでいますが、引数となる組を構成する二つの データは、同一の等値型であれば何でもかまいません。つまり、 equalは、 - equal (43,43); > val it = 1 : int - equal (2.31,2.41); > val it = 0 : int - equal ("Monet","Monet"); > val it = 1 : int - equal ((18,#"M"),(18,#"R"); > val it = 0 : int というように、 任意の等値型 * それと同じ型 という型を持っているどんなデータにでも適用することができる ということです。

Q 8.2.4___等値型変数って何ですか。

等値型変数というのは、先頭の文字がアポストロフィー(')で、 2番目の文字もアポストロフィーであるような識別子のことです。 たとえば、 ''a ''umiushi ''958 というような識別子は、等値型変数だとみなされます。

Q 8.2.5___任意の等値型から構成される型を定義域とする関数の 型は、どのような型式によってあらわされるのですか。

そのような関数の型は、等値型変数を含む型式によって あらわされます。 たとえば、Q 8.2.2で書いた、equalという関数を定義するfun宣言を MLの対話型システムに入力したとすると、対話型システムは、 > val equal = fn : ''a * ''a -> int という応答を出力します。equalの定義域を記述するために 使われている''aという等値型変数は、「任意の等値型」という型を あらわしています。つまり、対話型システムは、''aという 等値型変数を使うことによって、「equalの定義域は、任意の 等値型と、それと同じ型から構成される積型である」ということを 述べているわけです。 同じように、 fun doubleEqual (w,x,y,z) = if w=x andalso y=z then 1 else 0 というfun宣言を対話型システムに入力した場合は、 > val doubleEqual = fn : ''a * ''a * ''b * ''b -> int という応答が出力されます。ここで、doubleEqualの定義域は、 ''a * ''a * ''b * ''b という型式であらわされていますが、この型式は、「4個の任意の 等値型から構成される積型で、1個目と2個目は同じ型、3個目と 4個目も同じ型でなければならないが、左の2個と右の2個は異なる 型でもかまわない」という意味です。

---練習問題

8.1___任意の型を持つデータに適用することができて、受け取った 引数を2個並べることによってできる組を返す、twiceという関数 (型は'a -> 'a * 'a)を定義するfun宣言を書いてください。

実行例(1) - twice 721; > val it = (721, 721) : int * int 実行例(2) - twice (87,"Greg Bear"); > val it = ((87, "Greg Bear"), (87, "Greg Bear")) : (int * string) * (int * string)

8.2___任意の型を持つ3個のデータから構成される組に 適用することができて、受け取った引数の1個目と2個目のデータから 構成される組と1個目と3個目のデータから構成される組から 構成される組を返す、distributeという関数(型は 'a * 'b * 'c -> ('a * 'b) * ('a * 'c))を定義するfun宣言を 書いてください。

実行例(1) - distribute (38,0.02,"Mervyn Peake"); > val it = ((38, 0.02), (38, "Mervyn Peake")) : (int * real) * (int * string) 実行例(2) - distribute ((#"@",60),1.007,18); > val it = (((#"@", 60), 1.007), ((#"@", 60), 18)) : ((char * int) * real) * ((char * int) * int)

8.3___任意の同じ等値型を持つ2個のデータと任意の同じ型を持つ 2個のデータから構成される組に適用することができて、受け取った 引数の1個目と2個目のデータが同じものならば3個目のデータを 返し、そうでなければ4個目のデータを返す、 selectByEqualityという関数(型は''a * ''a * 'b * 'b -> 'b)を 定義するfun宣言を書いてください。

実行例(1) - selectByEquality (35,35,7.29,0.303); > val it = 7.29 : real 実行例(2) - selectByEquality ((10.5,#"+"),(10.5,#"="),999,222); > val it = 222 : int

8.4___任意の同じ等値型を持つ3個のデータから構成される組に 適用することができて、受け取った引数が3個とも同じものならば 2を返し、2個が同じもので残りの1個はそれと異なるならば1を返し、 3個とも異なるものならば0を返す、testSlotMachineという関数 (型は''a * ''a * ''a -> int)を定義するfun宣言を 書いてください。

実行例(1) - testSlotMachine (67,67,67); > val it = 2 : int 実行例(2) - testSlotMachine (#"X",#"W",#"X"); > val it = 1 : int 実行例(3) - testSlotMachine (5.11,0.004,728.3); > val it = 0 : int

第9章===リスト

9.1---リストの構造

Q 9.1.1___リストって何ですか。

リストというのは、いくつかのデータを再帰的に 組み合わせることによってできる列のことです。 リストは、リストの中にリストがあって、その内側のリストの中にも さらにリストがある、というように再帰的な構造を持っています。 たとえば、513、~239、441、106という4個の整数がこの順序で 並んでいるリストがあるとしましょう。そのリストは、 513 と ~239と441と~106から構成されるリスト という二つの部分に分解することができます。そして、その内側の リストは、さらに、 ~239 と 441と106から構成されるリスト という二つの部分に分解することができます。

Q 9.1.2___1個のデータだけから構成されるリストは、どのような 構造になっているのですか。

1個のデータだけから構成されるリストは、そのデータと、0個の データから構成されるリスト、という二つの部分から構成されます。 たとえば、106という1個のデータだけから構成されるリストは、 106 と 0個のデータから構成されるリスト という二つの部分に分解することができます。

Q 9.1.3___データを組み合わせることによってリストを 作りたいときはどうすればいいのですか。

データを組み合わせることによってリストを作りたいときは、 [ 式 , 式 , …… , 式 ] という形の式をコンピュータに評価させます。そうすると、その式の 中のそれぞれの式の値から構成されるリストができて、そのリストが 式全体の値として得られます。 たとえば、 [513,~239,441,106] という式をコンピュータに評価させると、513、~239、441、 106という4個の整数がこの順序で並んでいるリストができて、その リストがこの式の値になります。同じように、 [36-34,34 mod 7,size "Lodge"] という式をコンピュータに評価させた場合は、その値として、2、6、 5という3個の整数をこの順序で並べることによってできるリストが 得られます。 なお、リストを作る式を書くとき、角括弧やコンマの前後に何個かの 空白や改行を加えたとしても、それらの空白や改行は式の意味に 影響を与えません。たとえば、 [300,285,~987,501] という式を、その角括弧やコンマの前後に空白や改行を挿入して、 [ 300, 285, ~987, 501 ] に書き換えたとしても、その意味は変化しません。

Q 9.1.4___リストから構成されるリストを作ることは可能ですか。

はい、可能です。 たとえば、 [[201,35,63],[116,97],[72,10,44],[6,312,61,900]] というように、整数のリストから構成されるリストを作る、 ということができます。

Q 9.1.5___空リストって何ですか。

空リストというのは、0個のデータから構成される リストのことです。 リストという概念は、空リストを基底として使うことによって、 次のように再帰的に定義することができます。 ● 空リストはリストである。 ● リストの左側にデータを連結したものはリストである。 ● 以上の記述から導かれるもの以外はリストではない。

Q 9.1.6___空リストをあらわす定数はどう書けばいいのですか。

空リストをあらわす定数は、[]またはnilと書きます。

Q 9.1.7___リストの頭部とか尾部とかっていうのは何のことですか。

リストの頭部というのはそのリストの先頭のデータのことで、 尾部というのはそのリストから頭部を取り除いたあとに残る リストのことです。 たとえば、 ["north","east","south","west"] というリストの場合、"north"という文字列が頭部で、 ["east","south","west"] というリストが尾部です。同じように、 ["deer"] というリストの場合、頭部は"deer"という文字列で、 尾部は空リストです。

Q 9.1.8___リストの要素っていうのは何のことですか。

リストの要素というのは、そのリストを構成しているそれぞれの データのことです。 たとえば、 ["pyr","ge","hydor","aer"] というリストの場合、"pyr"、"ge"、"hydor"、"aer"という4個の 文字列のそれぞれが、そのリストの要素です。 どんなリストでも、そのもっとも内側には空リストが あるわけですが、その空リストはリストの要素ではありません。 たとえば、 ["Empedokles"] というリストは、"Empedokles"と空リストという二つの部分から 構成されているわけですが、このリストの要素としては "Empedokles"がただひとつあるだけです。 リストから構成されるリストの場合は、その全体から見て1段階だけ 内側のリストのそれぞれを、全体のリストの要素だと考えます。 たとえば、 [ [[1,8,2],[5,3,7,2]], [[6],[7,5,5],[3,2],[6,6]], [[4,9,7],[3,6,3,7,5,2]] ] というリストの要素は、[[1,8,2],[5,3,7,2]]、 [[6],[7,5,5],[3,2],[6,6]]、[[4,9,7],[3,6,3,7,5,2]]という 3個のリストのそれぞれです。

Q 9.1.9___リストの長さっていうのは何のことですか。

リストの長さというのは、そのリストを構成している要素の 個数のことです。 たとえば、 ["Africa","America","Antarctica","Eurasia","Oceania"] という5個の文字列から構成されるリストの長さは5で、 [[513,~239,441,106],[33],[~22,381]] という3個のリストから構成されるリストの長さは3です。 なお、空リストの長さは0です。

9.2---リストと型

Q 9.2.1___型が異なるいくつかのデータを 組み合わせることによってひとつのリストを作るということは 可能ですか。

いいえ、それはできません。 ひとつのリストを構成するそれぞれの要素は、すべて同一の 型でないといけません。つまり、整数から構成されるリストとか、 文字列から構成されるリストとか、何個かの同じ型の組から 構成されるリストとか、何個かの同じ型の関数から構成される リストとかを作ることは可能ですが、整数と文字列から構成される リストとか、組と関数から構成されるリストとかを作ることは できないのです。ですから、 [315,"gold",277,"silver"] という式をコンピュータに評価させようとすると、 エラーになってしまいます。

Q 9.2.2___リストというのはどのような型を持つのですか。

リストは、同じ型の要素を持つすべてのリストの集合という型を 持ちます。 たとえば、 [768,104,~93,221] という4個の整数から構成されるリストは、「intのデータを 要素とするすべてのリストの集合」という型を持ち、 ["red","green","blue"] という3個の文字列から構成されるリストは、「stringのデータを 要素とするすべてのリストの集合」という型を持ちます。 なお、リストの集合であるような型の総称として、「リスト型」 という言葉が使われることもあります。 どんなリストに関しても、その全体とその要素とは異なる型を 持ちます。したがって、リストであるものとリストでないものとが 混在しているリストを作る、ということはできません。たとえば、 [43,61,[82,11,79],37,92,15] というような、整数のリストと整数とが混在しているリストを 作ろうとすると、エラーになってしまいます。

Q 9.2.3___リストの型はどのような型式であらわされるのですか。

「xxxxという型のデータを要素とするすべてのリストの集合」という 型は、xxxx listという型式であらわされます。 たとえば、整数を要素とするリストの型はint listという型式で あらわされ、文字列を要素とするリストの型はstring listという 型式であらわされます。 組を要素とするリストや、関数を要素とするリストの型をあらわす 型式を書くときは、要素の型の名前を丸括弧で囲む必要があります。 たとえば、int * stringという型の組を要素とするリストの型を あらわす型式は(int * string) listと書き、string -> intという 型の関数を要素とするリストの型をあらわす型式は (string -> int) listと書きます。 リストを要素とする組とか、リストを要素とするリストとか、 定義域や値域がリストであるような関数とかの型をあらわす型式を 書くときに、その中に含まれるxxxx listという型式を丸括弧で 囲む必要はありません。たとえば、1個目の要素が整数で2個目の 要素が文字列のリストであるような組の型をあらわす型式は、 int * string listと書くことができます。同じように、文字列の リストを要素とするリストの型はstring list listという型式で あらわすことができ、文字列を受け取って整数のリストを返す関数の 型はstring -> int listという型式であらわすことができます。

Q 9.2.4___リストを要素とするリストの中に、空リストを 要素として入れることは可能ですか。

はい、可能です。 たとえば、 [[33,21],[503,~20,111],[],[60,19,55]] というように、整数のリストを要素とするリストの中に、要素として 空リストを入れても問題はありません。

Q 9.2.5___空リストはどのような型を持つのですか。

空リストの型は、それが置かれている状況によって決まります。 たとえば、 [[270],[~66,53],[4,301],[],[611,~772,5],[188],[3,18]] というリストの中に要素として含まれている空リストは int listという型を持ち、 [[#"E",#"3",#"="],[],[#"W",#"M"]] というリストの中に要素として含まれている空リストは char listという型を持ちます。 周囲の状況によって空リストの型を特定することができない場合、 その空リストの型は、'a listのような型変数を使った型式で あらわされます。ですから、MLの対話型システムに、 - []; というように空リストを入力した場合、対話型システムはその型を 特定することができませんので、 > val it = [] : 'a list という応答を出力します。

9.3---リストの処理

Q 9.3.1___個数が定まっていないデータから構成される列に 適用することのできる関数を定義したいのですが、そんなときは どうすればいいのですか。

そんなときは、リストに適用することができるようにその関数を 定義します。 第1.4節で説明したように、構造を持っているデータを 扱いたいときは、普通、それらを組み合わせることによって組という データを作ります。でも、組に適用できるように関数を定義した 場合、その関数が扱うことのできるデータの構造は、定義の段階で 完全に固定されてしまいます。ですから、任意の個数のデータから 構成される列にその関数を適用する、ということはできません。 つまり、組というのは、個数がしっかりと固定されている データから構成される列を扱う場合にのみ使われるものなのです。 それに対して、個数が定まっていないデータから構成される列を 扱いたいという場合は、普通、リストを使います。リストに 適用できるように作られた関数は、そのリストに含まれている データが何個であっても、柔軟に対応することができます。 もう少し具体的な例を使って説明しましょう。 fun sumTuple (w,x,y,z) = w+x+y+z と定義されたsumTupleという関数は、4個の整数から構成される 組に適用することができて、それらの整数を加算した結果を 返します。けれども、3個の整数から構成される組にも、5個の 整数から構成される組にも、この関数を適用することは できません。 整数の組ではなくて整数のリストに適用できるような関数を 定義すれば、その関数は任意の個数の整数を扱うことが できますから、sumTupleよりも汎用的なものになります。たとえば、 整数のリストに適用すると、それを構成しているそれぞれの整数の 合計を返す、sumListという関数を作ったとしましょう。 そうすると、sumListを使うことによって、 - sumList [3,5,2]; > val it = 10 : int - sumList [8,1,3,2]; > val it = 14 : int - sumList [2,4,6,2,3]; > val it = 17 : int というように、任意の個数の整数を加算することが できるようになります。

Q 9.3.2___リストを頭部と尾部に分解したいときは どうすればいいのですか。

リストを頭部と尾部に分解したいときは、 パターン :: パターン という形のパターンと、そのリストとを照合します。 たとえば、 val atama::shippo = [34,17,22,51]; というval宣言を実行したとすると、atamaという識別子は34という 整数に束縛され、shippoという識別子は[17,22,51]というリストに 束縛されます。同じように、 val atama::shippo = ["Anaxagoras"]; というval宣言の場合は、atamaが"Anaxagoras"に束縛されて、 shippoが空リストに束縛されます。 空リストは頭部と尾部に分解することができませんので、 val atama::shippo = []; というval宣言を実行したとすると、パターンとデータとの照合が 失敗することになります。 それでは、以上のことを応用して、listToTupleという関数の 定義を書いてみることにしましょう。この関数は、 - listToTuple [35,271,~100,59]; > val it = (35, [271, ~100, 59]) : int * int list - listToTuple ["Demokritos"]; > val it = ("Demokritos", []) : string * string list というように、リストに適用すると、そのリストの頭部と尾部から 構成される組を返します(型は'a list -> 'a * 'a list)。 空リストは頭部と尾部に分解することができませんので、 listToTupleは、空リストに適用された場合、Emptyという例外を 発生させるということにしましょう。そうすると、listToTupleを 定義するfun宣言は、 fun listToTuple [] = raise Empty | listToTuple (x::xs) = (x,xs) というように書くことができます。 なお、fun宣言を書くときに、funという予約語の右側に、 識別子 パターン1 :: パターン2 という形のものを書くと、MLの処理系は、識別子とパターン1とが 結合したものの右側に::とパターン2がある、 と解釈してしまいます。ですから、そのような場合は、 listToTupleのfun宣言の中にあるx::xsがそうなっているように、 パターン :: パターン という形のパターンを丸括弧で囲む必要があります。

Q 9.3.3___特定の長さのリストとしか照合できないパターンを 書きたいときは、どうすればいいのですか。

そんなときは、「パターンのリスト」と呼ばれるパターンを 書きます。 パターンのリストというのは、 [ パターン , パターン , …… , パターン ] という形のパターンのことです。パターンのリストは、それと同じ 長さのリストとの照合ならば成功しますが、長さの違うリストとの 照合には失敗します。 たとえば、[mikan,ringo,ichigo,suika]という長さが4のパターンの リストは、長さが4のリストとしか照合できません。たとえば、 val [mikan,ringo,ichigo,suika] = [34,17,22,51]; というval宣言をコンピュータに実行させた場合、照合は成功して、 mikanが34に、ringoが17に、ichigoが22に、suikaが51に 束縛されます。しかし、 val [mikan,ringo,ichigo,suika] = [34,17,22,51,19]; というfun宣言をコンピュータに実行させたとすると、照合が 失敗しますので、識別子はデータに束縛されません。 それでは、以上のことを応用して、swapListという関数を 定義してみましょう。swapListは、 - swapList [53,~82]; > val it = [~82, 53] : int list - swapList [74]; > val it = [74] : int list - swapList [114,53,~46]; > val it = [114, 53, ~46] : int list というように、長さが2のリストに適用された場合はその1個目の 要素と2個目の要素とを入れ替えたリストを返し、長さが2以外の リストに適用された場合は引数をそのまま返します(型は 'a list -> 'a list)。swapListを定義するfun宣言は、 fun swapList [first,second] = [second,first] | swapList x = x というように、パターンのリストを使って書くことができます。

Q 9.3.4___任意の長さのリストを処理する関数を作りたいときは どうすればいいのですか。

任意の長さのリストを処理する関数は、そのリストを再帰的に 分解していくという動作をするように定義します。 もう少し具体的に書くと、任意の長さのリストを処理する関数を 作りたいときは、 ● リストが空リストならば、基底となる処理をする。 ● リストが空リストではないならば、それを頭部と尾部に 分解して、その尾部を再帰的に処理する。 というアルゴリズムを実行するように定義すればいい、 ということです。 例として、整数のリストに適用すると、そのリストの要素の合計を 返す、sumListという関数(型はint list -> int)を 定義してみましょう。xが整数のリストだとするとき、xの要素の 合計は、 ● xが空リストならば、0を返す。 ● xが空リストではないならば、xの尾部の合計にxの頭部を加算した 結果を返す。 というアルゴリズムによって求めることができます。したがって、 sumListを定義するfun宣言は、 fun sumList [] = 0 | sumList (atama::shippo) = atama + sumList shippo と書けばいいということになります。

9.4---リストを扱う組み込み関数

Q 9.4.1___リストからその頭部を取り出すという動作をする 組み込み関数はありますか。

はい、あります。リストからその頭部を取り出したいときは、 hdという組み込み関数(型は'a list -> 'a)を 使うことができます。 たとえば、 hd [381,200,533,116] というようにhdをリストに適用したとすると、hdは、そのリストの 頭部、つまり381を戻り値として返します。 なお、hdを空リストに適用すると、hdはEmptyという例外を 発生させます。

Q 9.4.2___リストからその尾部を取り出すという動作をする 組み込み関数はありますか。

はい、あります。リストからその尾部を取り出したいときは、 tlという組み込み関数(型は'a list -> 'a list)を 使うことができます。 たとえば、 tl [381,200,533,116] という式でtlをリストに適用したとすると、tlはそのリストから 尾部を取り出して返しますので、式の値は、 [200,533,116] になります。 なお、hdと同じように、tlも、空リストに適用された場合は Emptyという例外を発生させます。

Q 9.4.3___リストが空リストかどうかを調べる組み込み関数は ありますか。

はい、あります。リストが空リストかどうかを調べたいときは、 nullという組み込み関数を使うことができます。 nullは、リストに適用すると、それが空リストならばtrue、 そうでなければfalseを返す組み込み関数です(型は 'a list -> bool)。 リストが空リストかどうかで動作を選択したいときは、そのリストと 空リストとが同一かどうかを=という演算子に調べさせても かまいません。しかし、=を使うと、等値型のリストしか扱うことが できなくなるという点に注意してください。=ではなくnullを 使うことによって、等値型ではないリスト(たとえば関数の リスト)を扱うこともできるようになります。

Q 9.4.4___リストの左側に1個のデータを連結することによって リストを成長させたいときはどうすればいいのですか。

リストの左側にデータを連結したいときは、::という 組み込み演算子を使います。 ::というのは、リストの左側にデータを連結する演算子です(型は 'a * 'a list -> 'a list)。データとリストから構成される 組に対して::を適用すると、::は、左側のデータを頭部とし、 右側のリストを尾部とするリストを返します。たとえば、 335::[27,881,202] という式で、335と[27,881,202]から構成される組に::を 適用したとすると、::は、335を頭部とし、[27,881,202]を 尾部とするリスト、つまり、 [335,27,881,202] というリストを返します。同じように、 131::[] という式で、131と空リストから構成される組に::を適用した 場合は、その値として、 [131] というリストが得られます。 ::の優先順位は、5です。つまり、::の優先順位は、+や-よりも 低くて、>=や=よりも高い位置に設定されているわけです。 ですから、 a+b::c という式は、cの値の左側にa+bの値を連結するという意味になり、 a=b::c という式は、aの値とb::cの値とが等しいかどうかを調べるという 意味になります。 ::の結合規則は、右結合です。たとえば、 ~496::228::[] という式は、空リストの左側に228を連結することによってできる リストの左側に~496を連結する、という意味だと解釈されます。 MLの演算子の大多数は左結合ですから、::というのはかなり例外的な 演算子だということになります。でも、リストというのは右から 左へと成長していくものですから、リストを成長させる演算子である ::が右結合だというのは、実はとても自然なことなのです。 何かを処理した結果としてリストを返す関数を定義する場合、 リストを成長させる::は必要不可欠な演算子だと 言っていいでしょう。具体的な例として、nが0またはプラスの 整数だとするとき、nに適用すると、 [n, (n-1), (n-2), ……, 0] というリストを返す、countDownListという関数(型は int -> int list)を定義してみましょう。この関数は、再帰的な 関数適用によって求められた、 [(n-1), (n-2), ……, 0] というリストの左側にnを連結しないといけないわけですが、 その連結をするためには::が必要になります。::を 使うことによって、countDownListを定義するfun宣言は、 fun countDownList n = if n=0 then [0] else if n>0 then n :: countDownList (n-1) else raise CountDownList というように書くことができます。

Q 9.4.5___リストとリストとを連結することによって1個のリストを 作る、という動作をする組み込み関数はありますか。

はい、あります。リストとリストとを連結したいときは、@という 組み込み演算子を使うことができます。 @は、'a list * 'a list -> 'a listという型を持つ演算子です。 同じ型を持つ2個のリストから構成される組に対して@を適用すると、 @は、1個目のリストを2個目のリストの左側に 連結することによってできるリストを返します。たとえば、 [503,~21,99,432] @ [191,~303,26] という式を書くことによって、[503,~21,99,432]というリストを、 [191,~303,26]というリストの左側に連結することによって1個の リストを作る、という作業を@に実行させることができます。 この場合、@は、 [503,~21,99,432,191,~303,26] というリストを、作業の結果として返します。 xがリストだとするとき、x@[]の値と[]@xの値は、どちらも xになります(数学的に言えば、「空リストは、@という演算に関する 単位元である」ということになります)。 @の優先順位は5で、結合規則は右結合です。つまり、@は、 優先順位も結合規則も::と同じ、ということになります。

Q 9.4.6___文字列を個々の文字に分解して、それらの文字から 構成されるリストを作りたいのですが、そんなときはどうすれば いいのですか。

文字列を文字のリストに変換したいときは、explodeという 組み込み関数を使います。 explodeは、string -> char listという型の関数です。explodeは、 引数として受け取った文字列と同じ順序で文字が並んでいるような リストを返します。たとえば、 explode "mikan" という式で、"mikan"という文字列にexplodeを適用したとすると、 explodeは、その文字列の中の文字が同じ順序で並んでいるリスト、 つまり、 [#"m",#"i",#"k",#"a",#"n"] というリストを返します。

Q 9.4.7___文字のリストから、それを構成する文字が同じ順序で 並んでいるような文字列を作りたいのですが、そんなときは どうすればいいのですか。

文字のリストを文字列に変換したいときは、implodeという 組み込み関数を使います。 implodeは、文字のリストに適用すると、それを構成する文字を 同じ順序で並べることによってできる文字列を返す関数です (型はchar list -> string)。たとえば、 [#"m",#"i",#"k",#"a",#"n"] という文字のリストにimplodeを適用したとすると、implodeは、 それを構成する文字を同じ順序で並べることによってできる、 "mikan"という文字列を返します。 explodeとimplodeを使うことによって、文字列を加工する関数を 作ることができるようになります。与えられた文字列をexplodeで リストに変換して、そのリストを加工して、その結果をimplodeで 文字列に変換したものを返すように定義すればいいわけです。 文字列を加工する関数の例として、packstringという関数を 作ってみることにしましょう。packstringは、文字列からすべての 空白を除去する関数です(型はstring -> string)。たとえば、 "I am a cat." という文字列にpackstringを適用したとすると、packstringは、 "Iamacat." という文字列を返します。 まず、packstringを定義するための補助的な関数として、 packcharlistという関数を作ることにしましょう。packcharlistは、 文字のリストに適用すると、そのリストから空白を 除去することによってできるリストを返す関数です(型は char list -> char list)。たとえば、 [#"I",#" ",#"a",#"m",#" ",#"a",#" ",#"c",#"a",#"t",#"."] という文字のリストにpackcharlistを適用したとすると、 packcharlistは、 [#"I",#"a",#"m",#"a",#"c",#"a",#"t",#"."] という文字のリストを返します。packcharlistが定義できれば、 あとはexplodeとpackcharlistとimplodeを 組み合わせることによって、packstringが完成します。 xが文字のリストだとするとき、xから空白を除去するという処理は、 ● xが空リストならば、空リストを返す。 ● xが空リストではなくて、xの頭部が空白ならば、xの尾部から 空白を除去した結果を返す。 ● xが空リストではなくて、xの頭部が空白ではないならば、xの 尾部から空白を除去した結果にxの頭部を連結したものを返す。 というアルゴリズムで実現することができます。 それでは実際に定義を書いてみましょう。packstringを定義する fun宣言は、 fun packstring s = let fun packcharlist [] = [] | packcharlist (#" "::shippo) = packcharlist shippo | packcharlist (atama::shippo) = atama :: packcharlist shippo; in implode (packcharlist (explode s)) end というような感じになります。

Q 9.4.8___リストに適用すると、要素が並んでいる順番がそれとは ちょうど逆になっているようなリストを返す、という動作をする 組み込み関数はありますか。

はい、あります。与えられたリストから、その要素を逆順に 並べ換えることによってできるリストを作りたい、というときは、 revという組み込み関数(型は'a list -> 'a list)を 使うことができます。 たとえば、 rev [51,33,29,74,18] という式でrevをリストに適用したとすると、revは、それとは逆の 順序で要素が並んでいる、 [18,74,29,33,51] というリストを返します。

Q 9.4.9___リストの長さを求めたいのですが、それができる 組み込み関数はありますか。

はい、あります。リストの長さを求めたいときは、lengthという 組み込み関数(型は'a list -> int)を使うことができます。 たとえば、 length ["Achernar","Cursa","Zaurak","Azha","Acamal"] という式でlengthをリストに適用したとすると、lengthは、その リストの長さである5という整数を返します。

---練習問題

9.1___組み込み関数のlengthと同じ動作をする、lenという関数を 定義するfun宣言を書いてください。

実行例 - len ["Odradek","A Bao A Qu","Bahamut","Humbaba"]; > val it = 4 : int

9.2___eが等値型のデータで、xがそれと同じ型のデータの リストだとするとき、(e,x)という組に適用すると、eがxの中に 要素として含まれているならばtrue、そうでなければfalseを返す、 memberという関数(型は''a * ''a list -> bool)を定義する fun宣言を書いてください。

実行例(1) - member ("Medusa",["Stheno","Euryale","Medusa"]); > val it = true : bool 実行例(2) - member ("Chimaira",["Stheno","Euryale","Medusa"]); > val it = false : bool

9.3___整数のリストに適用すると、そのリストの要素のうちで 最大のものを返す、maxElemという関数(型はint list -> int)を 定義するfun宣言を書いてください。なお、空リストに適用された 場合は、Emptyという例外を発生させるようにしてください

(Emptyは、処理系によってすでにexnに追加されていますので、 exception宣言を書く必要はありません)。 実行例 - maxElem [38,~67,54,21,43]; > val it = 54 : int

9.4___nが整数で、xが任意の型のデータだとするとき、(n,x)という 組に適用すると、n個のxを並べることによってできるリストを返す、 cultureという関数(型はint * 'a -> 'a list)を定義する fun宣言を書いてください。

実行例 - culture (4,"lactobacillus"); > val it = ["lactobacillus", "lactobacillus", "lactobacillus", "lactobacillus"] : string list

9.5___組み込み演算子の@と同じ動作をする、appendという関数を 定義するfun宣言を書いてください。

実行例 - append ([35,27,66],[95,11,64,37]); > val it = [35, 27, 66, 95, 11, 64, 37] : int list

9.6___組み込み関数のrevと同じ動作をする、reverseという関数を 定義するfun宣言を書いてください。

実行例 - reverse [62,~19,~83,21,95,17]; > val it = [17, 95, 21, ~83, ~19, 62] : int list [ヒント] xがリストだとするとき、xを逆順にしたリストを作るという処理は、 ● xが空リストならば、空リストを返す。 ● xが空リストではないならば、xの頭部を要素とするリストの 左側に、xの尾部を逆順にしたリストを連結したものを返す。 というアルゴリズムによって実現することができます。 なお、リストを逆順にする関数を定義する方法としては、上に書いた アルゴリズムを使うというもののほかに、それよりももうちょっと 技巧的なもうひとつの方法があります。その方法でreverseを 定義したいときは、まず、reverse1という補助的な関数を 定義します。reverse1は、xとyという2個のリストから構成される 組を引数として受け取って、 ● xが空リストならば、yを返す。 ● xが空リストではないならば、xの尾部と、yの左側にxの頭部を 連結したものから構成される組にreverse1を適用して、その戻り値を そのまま返す。 というアルゴリズムを実行する関数です。このreverse1を 使うことによってリストを逆順にしたいときは、 reverse1 ([83,26,44,103,91],[]) というように、逆順にしたいリストと空リストから構成される組に reverse1を適用します。するとreverse1は、自分の分身を連鎖的に 呼び出して引数を中継していくわけですが、その引数は、 本体 reverse1 ([83,26,44,103,91], []) 分身1 reverse1 ( [26,44,103,91], [83]) 分身2 reverse1 ( [44,103,91], [26,83]) 分身3 reverse1 ( [103,91], [44,26,83]) 分身4 reverse1 ( [91], [103,44,26,83]) 分身5 reverse1 ( [], [91,103,44,26,83]) というように変化していきます。xが空リストだった場合、 reverse1はそのときのy(これは求めるべき逆順の リストになっています)を戻り値として返します。そしてその 戻り値は分身たちによってそのまま中継されて、本体へ 帰っていくことになります。

9.7___等値型のデータのリストに適用すると、そのリストの要素が すべて同一のものならばtrueを返し、そうでなければfalseを返す、 allEqualという関数(型は''a list -> bool)を定義するfun宣言を 書いてください。なお、allEqualは、要素が1個だけのリストとか 空リストとかに適用された場合もtrueを返すようにしてください。

実行例(1) - allEqual [71,71,71,71,71,71]; > val it = true : bool 実行例(2) - allEqual [71,71,71,46,71,71]; > val it = false : bool [ヒント] まず、次のような動作をするallEqual1という関数を定義します。 allEqual1は、(x,y)という組(xは1個の等値型のデータで、yはxと 同じ型のデータのリスト)に適用すると、yの要素がすべてxと 同一ならばtrueを返し、そうでなければfalseを返します。ただし、 yが空リストだった場合は無条件でtrueを返します。 allEqualは、受け取ったリストが空リストだった場合はtrueを返し、 空リストではなかった場合はそのリストの頭部と尾部から構成される 組にallEqual1を適用して、その戻り値をそのまま返します。

9.8___#"0"と#"1"という数字から構成されるリストに適用すると、 それを2進数とみなした場合にそれがあらわしている0または プラスの整数を返す、binaryToIntという関数(型は char list -> int)を定義するfun宣言を書いてください。 なお、リストの中に#"0"でも#"1"でもない文字が含まれていた 場合は、その文字を引数として伴うIllegalCharという例外を 発生させるようにしてください。

実行例(1) - binaryToInt [#"1",#"0",#"1",#"1",#"0",#"0",#"1"]; > val it = 89 : int 実行例(2) - binaryToInt [#"1",#"0",#"1",#"1",#"x",#"0",#"1"] handle IllegalChar ic => (print ("illegal char: " ^ Int.toString ic ^ "\n"); 0); illegal char: #"x" > val it = 0 : int [ヒント] 準備として、それぞれの文字が通常とは逆の順序で並んでいる 2進数を整数に変換する、revBinaryToIntという関数を定義します。 rbが逆順の2進数だとするとき、 ● rbが空リストならば、0を返す。 ● rbが空リストではないならば、rbの尾部を整数に変換して 2倍したものと、rbの頭部を整数に変換したものとを加算した結果を 返す。 というアルゴリズムを実行すれば、それを整数に 変換することができます。 binaryToIntは、引数として受け取ったリストを組み込み関数のrevを 使って逆順に並べ換えたものにrevBinaryIntを 適用すればいいだけです。

9.9___整数のリストに適用すると、そのリストに含まれるすべての 要素を偶数と奇数に分類して、偶数のリストと奇数のリストから 構成される組を返す、divideEvenOddという関数(型は int list -> int list * int list)を定義するfun宣言を 書いてください。

実行例 - divideEvenOdd [21,33,54,91,10,94,17]; > val it = ([54, 10, 94], [21, 33, 91, 17]) : int list * int list

9.10___リストに適用すると、そのリストの奇数番目の要素と 偶数番目の要素とを入れ替えることによってできるリストを返す、 swapNeighborsという関数(型は'a list -> 'a list)を定義する fun宣言を書いてください。なお、リストの長さが奇数だった 場合は、最後の要素の位置がそのままになるようにしてください。

実行例(1) - swapNeighbors [92,78,36,19,84,51]; > val it = [78, 92, 19, 36, 51, 84] : int list 実行例(2) - swapNeighbors [#"e",#"c",#"l",#"i",#"p",#"s",#"e"]; > val it = [#"c", #"e", #"i", #"l", #"s", #"p", #"e"] : char list

9.11___リストのリストに適用すると、それを構成するそれぞれの リストから頭部を取り出して並べることによってできるリストを 返す、multiHeadという関数(型は'a list list -> 'a list)を 定義するfun宣言を書いてください。

実行例 - multiHead [[64,37,11],[33],[52,17],[],[94,53,12,44]]; > val it = [64,33,52,94] : int list

9.12___リストのリストに適用すると、それを構成するそれぞれの リストから尾部を取り出して並べることによってできるリストを 返す、multiTailという関数(型は 'a list list -> 'a list list)を定義するfun宣言を 書いてください。

実行例 - multiTail [[64,37,11],[33],[52,17],[],[94,53,12,44]]; > val it = [[37, 11], [], [17], [53, 12, 44]] : int list list

9.13___リストのリストに適用すると、それを構成するそれぞれの リストから1番目の要素を取り出して並べることによってできる リストを作り、次に2番目の要素から構成されるリストを作り、 というように、同じ位置の要素から構成されるリストを 作っていって、それらのリストから構成されるリストを返す、 transposeという関数(型は'a list list -> 'a list list)を 定義するfun宣言を書いてください。

実行例 - transpose [[64,37,11],[33],[52,17],[],[94,53,12,44]]; > val it = [[64, 33, 52, 94], [37, 17, 53], [11, 12], [44]] : int list list [ヒント] transposeは、練習問題9.11と9.12で作ったmultiHeadとmultiTailを 使えば簡単にできます。xというリストをtransposeしたいときは、 ● xが空リストの場合は空リストを返す。 ● xが空リストではない場合は、xのmultiHeadが空リストならば xのmultiTailをtransposeしたものを返し、そうでなければ、xの multiTailをtransposeしたものの左側にxのmultiHeadを連結した ものを返す。 というアルゴリズムを使います。

9.14___整数のリストに適用すると、その要素を 並べ換えることによって、頭部がもっとも大きくて、 右へいくほど要素がしだいに小さくなっていくようなリストを 作って、その結果を返す、sortIntListという関数(型は int list -> int list)を定義するfun宣言を書いてください。

実行例 - sortIntList [39,54,21,43,66,39,10,72,54]; > val it = [72, 66, 54, 54, 43, 39, 39, 21, 10] : int list [ヒント] データの列が与えられたとき、何らかの大小関係(数学的に言えば 「全順序関係」)にもとづいて、大きなものから小さなものへ、 または小さなものから大きなものへ、という順番になるように その列の順序を並べ換えることを、データの列を「ソートする」 と言います。なお、大きなものから小さなものへという順序のことを 「降順」と言い、小さなものから大きなものへという順序のことを 「昇順」と言います。 ソートのアルゴリズムとして知られているものはかなりたくさん あるのですが、ここでは、「バブルソート」と 「クイックソート」という2種類を紹介します。そのほかの アルゴリズムに興味のある人は、たとえば[Aho,1983]などを 参照するといいでしょう。なお、下のアルゴリズムの紹介は、 リストを降順にソートするという場合で説明しています。 ○ バブルソート xがリストだとするとき、xをバブルソートするというのは、 ● xが空リストならば、空リストを返す。 ● xの要素が1個だけならば、xをそのまま返す。 ● xの要素が2個以上あるならば、次のことを実行する。 (i) xの要素のうちでもっとも大きなものが頭部に来るようにxを 並べ換えて、その結果をbとする。 (ii) bの尾部をバブルソートした結果の左側にbの頭部を 連結したものを返す。 というアルゴリズムを実行することです。 xをバブルソートする関数を作るときは、そのための補助的な 関数として、リストを並べ換えて、その要素のうちでもっとも 大きなものが頭部に来るようにする関数が必要になります。 そこで、その関数をbubbleという名前で定義することにしましょう。 bubbleは、xというリストに適用されたとすると、 ● xが空リストならば、空リストを返す。 ● xの要素が1個だけならば、xをそのまま返す。 ● xの要素が2個以上あるならば、次のことを実行する。 (i) xの尾部にbubbleを適用して、その戻り値をbとする。 (ii) xの頭部とbの頭部とを比較して、bの頭部のほうが 大きいならば、bの尾部の左側にxの頭部を連結して、その結果の 左側にbの頭部を連結することによってできるリストを返し、 そうでなければ、bの左側にxの頭部を連結することによってできる リストを返す。 というアルゴリズムを実行します。「バブルソート」という名前は、 このアルゴリズムによってデータの位置が変化していくようすが 液体の中を気泡が上昇していくのに似ているところから 命名されたと言われています。 ○ クイックソート xがリストだとするとき、xをクイックソートするというのは、 ● xが空リストならば、空リストを返す。 ● xの要素が1個だけならば、xをそのまま返す。 ● xの要素が2個以上あるならば、次のことを実行する。 (i) xをaとbという2個のリストに分割する。ただし、aとbは、aに 含まれるすべての要素はbに含まれるどの要素よりも大きい、という 条件を満足していないといけない。 (ii) bをクイックソートした結果の左側にaをクイックソートした 結果を連結して、その結果を返す。 というアルゴリズムを実行することです。 リストをクイックソートする関数を作る場合、それを補助する 関数として次のようなものを定義するといいでしょう。 ・ findPivot : int list -> int リストを大きさによって二つに分割するためには、まず、そのための 基準となるデータをリストの要素の中から選び出す必要があります。 この場合の基準となるデータは、普通、「枢軸」と呼ばれます。 findPivotは、受け取ったリストから枢軸を選び出す関数です。 この関数は、リストの要素を先頭から順番に調べていって、最初に 見つかった2個の異なる要素のうちで大きなほうを返します。 たとえば、受け取ったリストが[71,28,92]だった場合は71を返し、 [43,43,43,62,81]だった場合は62を返します。 なぜこのような方法で枢軸を選び出さないといけないのかという 理由は、こうすることによって、リストがそれ自身と空リストに 分割されてしまったために再帰が終わらなくなってしまう、という 可能性を排除することができるからです。たとえば、[53,71,82] というリストが与えられたとき、枢軸として53を選び出して、 それ以上の要素とそれより小さい要素に分割したとすると、 [53,71,82]と[]になってしまいます。でも、枢軸として71を 選び出せば、[71,82]と[53]に分割されることになります。 findPivotは、受け取ったリストが、たとえば[53,53,53,53,53] というようにすべて同一の要素から構成されていた場合、枢軸を 選び出すことができませんので、PivotUnfoundという例外を 発生させます。リストをクイックソートする関数は、findPivotが PivotUnfoundを発生させた場合は、それを捕獲して、受け取った リストをそのまま返す、というように定義します。 ・ partition : int * int list -> int list * int list partitionは、枢軸とリストから構成される組を受け取って、 そのリストの要素を枢軸以上のものと枢軸未満のものに分類して、 その結果を2個のリストの組という形で返す関数です。

9.15___eが1個のデータで、xがeと同じ型のデータのリストの リストだとするとき、(e,x)という組に適用すると、xを構成する それぞれの要素の左側にeを連結することによってできるリストを 返す、growAllListsという関数(型は 'a * 'a list list -> 'a list list)を定義するfun宣言を 書いてください。

実行例 - growAllLists (81,[[63,27],[],[49],[22,39,16]]); > val it = [[81, 63, 27], [81], [81, 49], [81, 22, 39, 16]] : int list list

9.16___同じ型のデータから構成される集合は、リストを使って 記述することができます。たとえば、35と87と23と59という4個の 整数から構成される集合は、 [35,87,23,59] というリストで書きあらわすことができます。ただし、ひとつの 集合をあらわすリストはひとつだけとは限りません。要素を並べる 順番が違っていたり要素が重複して含まれていたりしていても、 同一の集合を記述しているとみなすことができます。たとえば、 [87,59,35,23] [87,59,87,35,23,87,35,59] という2個のリストは、それぞれ、上に書いたリストと同じ集合を あらわしています。 2個の等値型のリストから構成される組に適用すると、それらの リストがあらわしている集合の共通部分をあらわすリストを返す、 intersectionという関数(型は ''a list * ''a list -> ''a list)を定義するfun宣言を 書いてください。

実行例 - intersection ([92,83,11,56,72,11],[89,72,18,11,89,83]); > val it = [83, 11, 72, 11] : int list [ヒント] 練習問題9.2で作ったmemberという関数を使うといいでしょう。

9.17___2個の等値型のリストから構成される組に適用すると、 それらのリストを集合とみなした場合に1個目が2個目の 部分集合になっているならばtrueを返し、そうでなければfalseを 返す、subsetという関数(型は''a list * ''a list -> bool)を 定義するfun宣言を書いてください。

実行例(1) - subset ([16,53,21,39],[53,21,77,16,39]); > val it = true : bool 実行例(2) - subset ([16,53,21,39],[53,21,77,16,48]); > val it = false : bool [ヒント] これも、memberを使えば簡単にできます。

9.18___集合をあらわしているリストに適用すると、その集合の すべての部分集合の集合をあらわしているリストを返す、 powerSetという関数(型は'a list -> 'a list list)を定義する fun宣言を書いてください。

実行例 - powerSet [53,18,77]; > val it = [[], [77], [18], [18, 77], [53], [53, 77], [53, 18], [53, 18, 77]] : int list list [ヒント] xが集合だとするとき、xのすべての部分集合から構成されるpxという 集合を求めるためには、次のようなアルゴリズムを実行します。 ● xが空集合ならば、pxは空集合のみを要素とする集合である。 ● xが空集合ではないならば、pxは次のようにして求める。 (a) xから1個の要素を取り出す。取り出した要素をeと呼び、xから eを取り除いてできる集合をx1と呼ぶことにする。 (b) x1のすべての部分集合から構成される集合を求める。 その結果として得られた集合をpx1と呼ぶことにする。 (c) px1を構成するそれぞれの集合にeを追加する。 そのようにしてできた集合の集合をepx1と呼ぶことにする。 (d) px1とepx1の和集合を求める。その結果がpxである。 なお、このアルゴリズムの中の(c)は、練習問題9.15で作った growAllListsという関数に実行させるといいでしょう。

第10章===高階関数

10.1---高階関数への序曲

Q 10.1.1___高階関数って何ですか。

高階関数というのは、関数(または関数を含むデータ)を引数として 受け取る関数、あるいは関数(または関数を含むデータ)を 戻り値として返す関数のことです。

Q 10.1.2___多相型関数は、任意の型のデータ(または任意の型の データを含むデータ)に適用することができるわけですから、 関数(または関数を含むデータ)に適用することもできますよね。 だとすれば、すべての多相型関数は高階関数だということに なりますけど、そう考えていいのですか。

「高階関数」という言葉を広い意味で使うならば、そのように 考えてもかまいません。 多相型関数は、任意の型のデータ(または任意の型のデータを含む データ)に適用することができるわけですから、関数(または関数を 含むデータ)に適用することも可能です。たとえば、 fun thrice x = (x,x,x) と定義されたthriceという関数は多相型関数ですから、 - thrice size; > val it = (fn, fn, fn) : (string -> int) * (string -> int) * (string -> int) というように、関数に適用することもできます。したがって、 「高階関数」という言葉を広く解釈すれば、すべての多相型関数は 高階関数だと考えることも可能です。 しかし、「高階関数」という言葉は、「関数(または関数を含む データ)をかならず引数として受け取る関数、あるいは関数(または 関数を含むデータ)をかならず戻り値として返す関数」というように 少しだけ狭く解釈されるのが普通です。このように狭く解釈した 場合、すべての多相型関数は高階関数だ、とは言えなくなります。 狭い意味での高階関数というのは、別の言い方をすれば、 定義域または値域をあらわしている型式の中に->という単語が 含まれている関数のことだ、ということになります。たとえば、 ('a -> bool) * 'a list -> 'a list ('a * 'a -> 'a) * 'a list -> 'a int -> int -> int ('b -> 'c) * ('a -> 'b) -> 'a -> 'c というような型を持つ関数は、狭い意味での高階関数です。

Q 10.1.3___xxxx -> yyyy -> zzzzという型式であらわされる型を 持つ関数は、関数を引数として受け取るのですか、それとも関数を 戻り値として返すのですか。

xxxx -> yyyy -> zzzzという型式は、yyyy -> zzzzという型の関数を 返す関数の型をあらわしています。 ですから、値域が関数型であるような高階関数の型を型式で記述する 場合、xxxx -> (yyyy -> zzzz)というように値域の型を丸括弧で 囲んでもかまいませんし、xxxx -> yyyy -> zzzzというように 丸括弧を省略してもかまいません。 それとは逆に、定義域が関数型であるような高階関数の型を型式で 記述するときは、(xxxx -> yyyy) -> zzzzというように、定義域の 型式を丸括弧で囲む必要があります。

Q 10.1.4___高階関数を定義することで、どんなメリットが 得られるのですか。

高階関数を定義することは、プログラムを人間にとって 理解しやすいものにする上で、大きな効果があります。 人間というのは、特殊な動作が特殊なまま表現されている プログラムを理解することがとても苦手だ、という性質を 持っています。ですから、理解しやすいプログラムを書くための 鉄則は、できる限り一般的な動作について記述した上で、 この動作はそれをこのように特殊化したものだ、という書き方を することです。 高階関数を定義することは、一般的な動作、つまり個々の特殊な 動作を抽象化した動作を記述することを可能にします。ですから、 いきなり特殊な関数を定義するのではなく、あらかじめ高階関数を 定義しておいて、それを使って特殊な関数を定義する、という ステップを踏むことによって、プログラムの理解しやすさが格段に 向上することになるのです。

10.2---高階関数の作り方

Q 10.2.1___引数として関数を受け取る高階関数は、どのようにして 作ればいいのですか。

fun宣言の中に、引数として受け取ったデータを何らかのデータに 適用する式を書けば、そのfun宣言によって定義される関数は、 引数として関数を受け取る高階関数になります。 それでは、例として、定義域がintである関数に適用すると、 100というintのデータにその関数を適用して、その結果を返す、 applyToHundredという関数(型は(int -> 'a) -> 'a)を 定義してみましょう。この関数を定義するfun宣言は、 fun applyToHundred f = f 100 と書くことができます。このように、受け取ったデータを100に 適用する、という式を書くことによって、applyToHundredは、 int -> 'aという定義域を持つことになるわけです。 定義域がintであるような組み込み関数にapplyToHundredを 適用してみると、 - applyToHundred real; > val it = 100.0 : real - applyToHundred chr; > val it = #"d" : char というように、applyToHundredは、引数として受け取った 組み込み関数を100に適用した結果を返します。もちろん、 組み込み関数ではない関数にapplyToHundredを適用することも 可能です。たとえば、 fun plus x = x>0 というようにplusという関数を定義しておいて、applyToHundredを plusに適用すると、 - applyToHundred plus; > val it = true : bool という結果が得られます。

Q 10.2.2___引数として関数を受け取る高階関数を、fn式によって 作られた関数に適用することは可能ですか。

はい、可能です。 たとえば、整数を2乗する関数をfn式で作って、Q 10.2.1で定義した applyToHundredをその関数に適用する、ということができます。 実際にやってみると、 - applyToHundred (fn x => x*x); > val it = 10000 : int というように、結果として100の2乗が得られます。

Q 10.2.3___戻り値として関数を返す高階関数は、どのようにして 作ればいいのですか。

何らかの新しい関数を作って、その関数を戻り値として返す、 という動作をするように関数を定義すれば、その関数は、 戻り値として関数を返す高階関数になります。 戻り値として関数を返す高階関数を定義するためのもっとも単純な 方法は、fn式の値が戻り値になるようなfun宣言を書くことです。 たとえば、 fun squareFun () = fn x => x*x というようにsquareFunという関数を定義するfun宣言を 書いたとすると、squareFunは、整数を2乗する関数(型は int -> int)を戻り値として返す高階関数になります(型は unit -> int -> int)。ですから、squareFunをユニットに 適用すると、 - squareFun (); > val it = fn : int -> int というように、squareFunは戻り値として関数を返します。 そこで、 val square = squareFun () というval宣言を書いて、squareFunが返した関数にsquareという 識別子を束縛したとしましょう。すると、 - square 9; > val it = 81 : int というように、整数を2乗する関数をsquareという名前で 利用することができるようになります。 なお、高階関数が返した関数に識別子を束縛して、その識別子を 使って関数をデータに適用する、というのではなくて、高階関数が 返した関数をすぐにデータに適用する、ということも可能です。 そんなときは、 式1 式2 式3 という形の式を書きます。この式は、式1の値を式2の値に 適用することによって得られた関数を式3の値に適用する、という 意味だと解釈されます。たとえば、 squareFun () 9 という式を書くことによって、squareFunの戻り値を、直接、9に 適用することができます。

Q 10.2.4___どんな引数を受け取ったかということによって、 戻り値として返す関数が変化する、というような高階関数を 定義するためにはどうすればいいのですか。

そのような高階関数を定義したいときは、fun宣言の中で新しい 関数を作るときに、引数(または引数の一部分)に束縛されている 識別子を使います。 例として、divideという関数(型はint -> int -> int)を使って 説明することにしましょう。divideは、xが整数だとするとき、xに 適用すると、「yが整数だとするとき、yに適用すると、xをyで 除算した結果を返す関数(型はint -> int)」を戻り値として 返す、という動作をする関数です。 divideは、どんな引数を受け取ったかということによって異なる 関数を戻り値として返す関数です。たとえば、divideを100に 適用した場合の戻り値は「整数に適用すると100をその整数で 除算した結果を返す関数」で、divideを60に適用した場合の戻り値は 「整数に適用すると60をその整数で除算した結果を返す関数」です。 ですから、100に適用されたdivideが返した関数を8という整数に 適用すると、 - divide 100 8; > val it = 12 : int というように100を8で除算した結果が得られ、60に適用された divideが返した関数を8に適用すると、 - divide 60 8; > val it = 7 : int というように60を8で除算した結果が得られます。 divideを定義するfun宣言は、 fun divide x = fn y => x div y と書くことができます。つまり、引数に束縛されているxという 識別子を含むfn式を書いて、そのfn式の値を戻り値として返す、 というように定義すればいいわけです。

Q 10.2.5___fun宣言の中に書かれたfun宣言によって定義された 関数を戻り値として返すことは可能ですか。

はい、可能です。 例として、powerOfという関数(型はint -> int -> int)を使って 説明することにしましょう。powerOfは、aが整数だとするとき、aに 適用すると、「bが整数だとするとき、bに適用すると、aのb乗を返す 関数(型はint -> int)」を返す、という動作をする関数です。 たとえば、 val powerOfTwo = powerOf 2 というval宣言で定義されたpowerOfTwoは、 - powerOfTwo 4; > val it = 16 : int - powerOfTwo 5; > val it = 32 : int というように2の引数乗を求める関数になり、 val powerOfThree = powerOf 3 というval宣言で定義されたpowerOfThreeは、 - powerOfThree 4; > val it = 81 : int - powerOfThree 5; > val it = 243 : int というように3の引数乗を求める関数になります。 powerOfを定義するfun宣言は、次のように書くことができます。 fun powerOf a = let fun powerOf1 b = if b=0 then 1 else if b>=1 then a * powerOf1 (b-1) else 0 in powerOf1 end このように、fun宣言の中に書かれたfun宣言によって定義された 関数を戻り値として返したいときは、ただ単に、関数に 束縛されている識別子を式として書けばいいだけです。 powerOfについて考えたついでに、それとよく似ているけれども ちょっとだけ違うpowerという関数についても 考えてみることにしましょう。powerOfというのは決まった整数を 引数の回数だけ乗算する関数を作る関数だったわけですが、 powerは、引数を決まった回数だけ乗算する関数を作る関数です。 たとえば、 val square = power 2 というval宣言で定義されたsquareは、 - square 7; > val it = 49 : int - square 10; > val it = 100 : int というように引数の2乗を求める関数になり、 val cube = power 3 というval宣言で定義されたcubeは、 - cube 7; > val it = 343 : int - cube 10; > val it = 1000 : int というように引数の3乗を求める関数になります。 powerも、powerOfと同じように、整数のべき乗を求める関数を 定義するfun宣言をfun宣言の中に書いて、内側のfun宣言で 定義された関数を戻り値として返すことによって 定義することができます。ただし、それらの関数のあいだには、 powerOfの内側の関数は自分自身を使うことによって 定義されるのに対して、powerの内側の関数は自分の外側にある powerを使うことによって定義されるという相違点があります。 powerを定義するfun宣言は、 fun power a = let fun power1 b = if a=0 then 1 else if a>=1 then b * power (a-1) b else 0 in power1 end と書くことができます。このように、powerの内側にある power1という関数は、その外側にあるpowerを使って 定義されています。

10.3---カリー化

Q 10.3.1___カリー化って何ですか。

カリー化というのは、定義域が積型である関数を、同じ目的のために 使うことのできる高階関数に変換することです。 例として、 fun enclose (a,b) = b^a^b と定義されたencloseという関数を使って説明することにします。 encloseは、aとbが文字列だとするとき、(a,b)という組に 適用すると、aの前後にbを連結することによってできる文字列を 返す関数(型はstring * string -> string)です。たとえば、 ("@@@","+++")という組にencloseを適用すると、encloseは、 - enclose ("@@@","+++"); > val it = "+++@@@+++" : string というように、"+++@@@+++"という文字列を返します。 encloseは定義域が積型ですから、カリー化することが可能です。 そこで、encloseをカリー化することによって得られた関数が curriedEncloseだ、としましょう。curriedEncloseというのは、 string -> string -> stringという型を持つ関数です。aが 文字列だとするとき、curriedEncloseをaに適用すると、 curriedEncloseは、「bが文字列だとするとき、bに適用するとaの 前後にbを連結することによってできる文字列を返す関数」を 返します。たとえば、curriedEncloseを"@@@"に適用して、 その結果を"+++"に適用すると、 - curriedEnclose "@@@" "+++"; > val it = "+++@@@+++" : string というように、その結果として"+++@@@+++"が得られます。 なお、curriedEncloseを定義するfun宣言は、 fun curriedEnclose a = fn b => b^a^b と書くことができます。 ちなみに、「カリー化」という言葉は、その言葉が あらわしているような関数の形式に関する研究で大きな功績を 残した、Haskell Brooks Curry (1900-1982)という数学者にちなんで 名付けられたものです。

Q 10.3.2___非カリー化って何ですか。

非カリー化というのは、カリー化の逆変換のことです。 つまり、関数を返す高階関数が与えられたとき、それと同じ 目的のために使うことのできる、定義域が積型で、関数を返さない 関数にそれを変換することを、関数を「非カリー化する」 と言います。

Q 10.3.3___カリー化形式のfun宣言って何ですか。

カリー化形式のfun宣言というのは、関数を返す高階関数の定義を 簡潔なものにするために使われる、fun宣言の特殊な 構文のことです。 カリー化形式のfun宣言は、 fun 関数名 パターン1 パターン2 …… パターンn = 式 という構文を持ちます。この構文を持つfun宣言によって定義された 関数は、 fun 関数名 ( パターン1 , パターン2 , …… , パターンn ) = 式 というfun宣言によって定義される関数を カリー化したものになります。たとえば、 fun enclose (a,b) = b^a^b というfun宣言を、 fun curriedEnclose a b = b^a^b というようにカリー化形式に書き換えると、curriedEncloseは、 encloseをカリー化した関数になります。 ところで、Q 10.2.5で定義したpowerOfという関数は、 fun uncurriedPowerOf (a,b) = if b=0 then 1 else if b>=1 then a * uncurriedPowerOf (a,b-1) else 0 というように定義されたuncurriedPowerOfという関数を カリー化したものです。したがって、powerOfの定義は、 カリー化形式のfun宣言で書くことも可能です。 powerOfの定義をカリー化形式のfun宣言で書くと、 fun powerOf a b = if b=0 then 1 else if b>=1 then a * powerOf a (b-1) else 0 というように、Q 10.2.5で書いたfun宣言に比べると、かなり すっきりしたものになります。 powerも、カリー化形式のfun宣言を使えば、 fun power a b = if a=0 then 1 else if a>=1 then b * power (a-1) b else 0 というように、Q 10.2.5で書いたものよりもすっきりした 定義になります。

10.4---高階組み込み関数

Q 10.4.1___mapというのはどういう組み込み関数なんですか。

mapは、リストのそれぞれの要素に対して何らかの関数を適用して、 要素をその関数の戻り値に置き換える、という動作をする関数を 作るために使われる組み込み関数です。 mapは、('a -> 'b) -> 'a list -> 'b listという型を持つ 関数です。fが何らかの関数(型は'a -> 'b)だとするとき、mapを fに適用すると、mapは、「fの定義域のデータから構成される リストに適用すると、そのリストを構成するそれぞれの要素にfを 適用して、fの戻り値から構成されるリストを返す関数(型は 'a list -> 'b list)」を戻り値として返します。 例として、mapEvenという関数(型はint list -> bool list)を mapを使って定義してみましょう。mapEvenは、整数のリストに 適用すると、そのリストを構成するそれぞれの整数を、 それが偶数ならばtrueに、奇数ならばfalseに 置き換えることによってできるリストを返します。つまり、 - mapEven [25,37,42,70,33]; > val it = [false, false, true, true, false] : bool list というような動作をする関数です。 mapを使って関数を定義したいときは、まず、リストのそれぞれの 要素に適用する関数を作って、それにmapを 適用しないといけません。mapEvenの場合は、「整数に適用すると、 それが偶数ならばtrue、そうでなければfalseを返す関数」を リストのそれぞれの要素に適用すればいいわけですから、 fn n => n mod 2 = 0 というfn式によってあらわされる関数にmapを適用します。 そうすると、mapは、リストのそれぞれの要素にその関数を適用する 関数を戻り値として返します。ですから、mapEvenという識別子を mapが返した関数に束縛することによって、mapEvenを 定義することができる、ということになります。つまり、 val mapEven = map (fn n => n mod 2 = 0) というval宣言を書けば、mapEvenが定義されることになるわけです。 関数を返す高階関数を使って関数を定義するというとき、 このmapEvenのように、val宣言を使って定義を書くことができる 場合もけっこう多いのですが、そんな場合でもval宣言ではなくて fun宣言を使うほうが好ましいと思われます。なぜなら、fun宣言を 使うことによって、「関数を定義している」ということが一目で わかるからです。ちなみに、mapEvenの定義をfun宣言で書くと、 fun mapEven ilst = map (fn n => n mod 2 = 0) ilst というようになります。 ところで、mapEvenの場合は、mapを使っていきなり特殊な関数を 定義したわけですが、そうではなくて、mapを少しだけ 特殊化したような別の高階関数をmapを使って定義する、 ということも可能です。 そのような関数の例として、mapLargerという関数(型は int -> int list -> bool list)をmapを使って 定義してみましょう。mapLargerは、borderが整数だとするとき、 borderに適用すると、「ilstが整数のリストだとするとき、ilstに 適用すると、ilstを構成するそれぞれの整数を、それがborderよりも 大きいならばtrueに、そうでなければfalseに 置き換えることによってできるリストを返す関数(型は int list -> bool list)」を返します。つまり、 mapLargerというのは、 - mapLarger 50 [81,77,34,12,63]; > val it = [true, true, false, false, true] : bool list というような動作をする関数です。 xとborderが整数だとするとき、xに適用すると、xがborderよりも 大きいならばtrue、そうでなければfalseを返す関数は、 fn x => x>border というfn式によってあらわすことができます。ですから、この関数に mapを適用すると、目的とする関数がmapによって 作り出されることになりますので、 fun mapLarger border ilst = map (fn x => x>border) ilst というように、fun宣言を使って、mapが返した関数に識別子を 束縛すれば、mapLargerが完成します。

Q 10.4.2___foldrというのはどういう組み込み関数なんですか。

foldrは、リストを処理するさまざまな関数を作り出すことのできる 関数です。つまり、「万能リスト処理関数」という感じの関数だと 考えていいでしょう。 foldrというのは、('a * 'b -> 'b) -> 'b -> 'a list -> 'bという 型を持つ関数です。foldrを使ってリストを処理する関数を 作りたいときは、まず、 ( リストの頭部 , リストの尾部を処理した結果 ) という組に適用すると、リスト全体を処理した結果を返す関数 (型は'a * 'b -> 'b)を作って、その関数にfoldrを適用します。 するとfoldrは、戻り値として1個の関数(型は 'b -> 'a list -> 'b)を返します。そこで次に、リストが 空リストだった場合の処理の結果に、foldrが返した関数を 適用します。すると、その結果としてリストを処理する関数(型は 'a list -> 'b)が得られます。 例として、concatRightという関数(型はstring list -> string)を foldrを使って定義してみましょう。concatRightは、文字列の リストに適用すると、そのリストを構成するそれぞれの文字列を 中括弧で囲みながら連結することによってできる文字列を返す 関数です。たとえば、 ["eidos","steresis","hyle","arche"] というリストにconcatRightを適用したとすると、concatRightは、 "{eidos{steresis{hyle{arche}}}}" という文字列を返します。 foldrを使ってconcatRightを定義するためにまず必要となるのは、 リストの頭部と、リストの尾部を処理した結果、という二つの データから、リスト全体を処理した結果を求める関数を作る、 ということです。concatRightの場合、リスト全体を処理した 結果は、リストの尾部を処理した結果の左側にリストの頭部を 連結したものを中括弧で囲むことによって 求めることができますから、 (fn (x,y) => "{" ^ x ^ y ^ "}") というfn式を書くことによって、その関数を作ることができます。 この関数にfoldrを適用すると、foldrは、1個の関数を返します。 次に必要なことは、foldrが返した関数を1個のデータに 適用することです。そのデータというのは、concatRightを 空リストに適用した場合にconcatRightが返すべきデータです。 concatRightの場合、空リストに適用された場合に 返さないといけないデータというのは空文字列です。そこで、 foldrの戻り値を空文字列に適用します。するとfoldrの戻り値は、 目的とするリスト処理関数を返しますので、concatRightという 識別子をその関数に束縛すれば、定義は完成です。 つまり、foldrを使ってconcatRightを定義したいというときは、 fun concatRight slst = foldr (fn (x,y) => "{" ^ x ^ y ^ "}") "" slst というfun宣言を書けばいいということになります。 それでは、もうひとつの例として、insertRightという関数(型は 'a -> 'a list -> 'a list)をfoldrを使って定義してみましょう。 insertRightは、aが任意の型のデータだとするとき、aに 適用すると、「lstがaと同じ型の要素から構成される リストだとするとき、lstに適用すると、lstのそれぞれの要素の 右側にaを追加することによってできるリストを返す関数(型は 'a list -> 'a list)」を返す関数です。つまり、 - insertRight 100 [23,58,17,44]; > val it = [23, 100, 58, 100, 17, 100, 44, 100] : int list - insertRight 100 []; > val it = [] : int list というような動作をする関数だということです。 insertRightが作る関数の場合、リストの頭部と、リストの尾部を 処理した結果、という二つのデータからリスト全体を処理した結果を 求める関数は、追加するデータをaとすると、 fn (x,y) => x::a::y というfn式であらわすことができます。ですから、まず最初に、 そのfn式の値にfoldrを適用します。 次に、処理の対象が空リストだった場合、insertRightが作った 関数はどんな戻り値を返さないといけないのか、ということについて 考える必要があります。insertRightが作る関数は、リストの それぞれの要素の右側に新しい要素を追加するわけですが、リストが 空リストの場合は何も追加する必要がありませんので、空リストを 戻り値として返すことになります。ですから、foldrが返した関数を 空リストに適用すると、その結果として目的とする関数が 得られます。 以上のことから、insertRightを定義するfun宣言は、 fun insertRight a lst = foldr (fn (x,y) => x::a::y) [] lst と書くことができるということになります。 さて、最後にもうひとつ、foldを使って関数を定義してみましょう。 3番目の例は、addTailという関数(型は 'a -> 'a list -> 'a list)です。addTailは、aが任意の型の データだとするとき、aに適用すると、「lstがaと同じ型の要素から 構成されるリストだとするとき、lstに適用すると、lstの末尾にaを 追加することによってできるリストを返す関数(型は 'a list -> 'a list)」を返します。つまり、addTailというのは、 - addTail "Genova" ["Verona","Livorno","Padova"]; > val it = ["Verona", "Livorno", "Padova", "Genova"] : string list - addTail "Bolzano" []; > val it = ["Bolzano"] : string list というような動作をする関数です。 xがリストの頭部で、yがリストの尾部を処理した結果だとすると、 addTailが作る関数の場合、yの左側にxを連結することによって、 リスト全体を処理した結果をもとめることができます。ですから、 addTailを定義するためにfoldrに与える関数は、 fn (x,y) => x::y というfn式であらわすことができます。 さて、次は、addTailによって作られた関数が空リストに適用された 場合、その関数はどんなデータを返さないといけないのか、という 問題です。addTailが作るのはリストの末尾に新しい要素を追加する 関数ですから、そのリストが空リストだった場合は、新しい 要素のみから構成されるリストを返さないといけません。リストの 末尾に追加したいデータをaとすると、空リストの末尾にaを 追加した結果は[a]と書くことができますので、foldrが返した 関数を[a]というリストに適用すると、その結果として目的とする 関数が得られることになります。 最後に、addTailという識別子を、foldrが返した関数によって 作られた関数に束縛すれば、定義は完成です。つまり、addTailは、 fun addTail a lst = foldr (fn (x,y) => x::y) [a] lst というfun宣言を書くことによって定義することができる、 ということになります。

Q 10.4.3___foldlというのはどういう組み込み関数なんですか。

foldlは、引数として受け取る関数の解釈が異なるということ以外、 foldrとほとんど同じ動作をする関数です。 foldrが引数として受け取る関数は、 ( 頭部 , 尾部を処理した結果 ) という組を処理すると解釈されるわけですが、foldlが引数として 受け取る関数は、 ( 末尾の要素 , 先頭から末尾の1個手前までを処理した結果 ) という組を処理すると解釈されます。 foldrは、リストは右から左へ成長するものだという自然な考え方に 沿った処理をする関数を作ることは得意なのですが、そうではない 不自然な処理をする関数の作成は得意ではありません。そんな foldrの欠点を補完するために存在するのがfoldlなのです。 例として、concatLeftという関数(型はstring list -> string)を 定義してみることにしましょう。foldrの説明のところで定義を 書いたconcatRightは、右にある文字列のほうが左にある 文字列よりも多くの中括弧に囲まれることになるわけですが、 concatLeftはそれとは逆に、左へ行けば行くほど多くの中括弧に 囲まれるように文字列を連結します。たとえば、 ["eidos","steresis","hyle","arche"] というリストにconcatLeftを適用したとすると、concatLeftは、 "{{{{eidos}steresis}hyle}arche}" という文字列を返します。 concatLeftの処理は、リストの要素というものは右にあるものほど 内側にある、というリストの自然な構造をそのまま反映している とは言えないものです。ですから、こんなときこそfoldlの 出番です。concatLeftを定義するfun宣言は、 fun concatLeft slst = foldl (fn (x,y) => "{" ^ y ^ x ^ "}") "" slst というように書くことができます。

Q 10.4.4___oというのはどういう組み込み演算子なんですか。

oは、二つの関数を合成する演算子です。 oというのは、('b -> 'c) * ('a -> 'b) -> 'a -> 'cという型を 持つ演算子です。fとgという二つの関数があって、fの値域とgの 定義域とは同じ型だとするとき、(g,f)という組にoを適用すると、 oは、「データに適用すると、そのデータにfを適用して、fの 戻り値にgを適用して、gの戻り値を返す関数」を戻り値として 返します。 例として、evenLengthという関数(型はstring -> bool)をoを 使って定義してみましょう。evenLengthは、文字列に適用すると、 その文字列の長さが偶数ならばtrue、そうでなければfalseを 返す関数です。たとえば、evenLengthを"orange"に適用した場合の 戻り値はtrueで、"grape"の場合はfalseです。 evenLengthは、組み込み関数のsizeと、 fn n => n mod 2 = 0 という関数とを合成することによって作ることができます。 ですから、evenLengthの定義をoを使って書くと、 fun evenLength s = ((fn n => n mod 2 = 0) o size) s というようになります。 なお、oの優先順位は3で、結合規則は左結合です。

Q 10.4.5___appというのはどういう組み込み関数なんですか。

appは、リストを構成するすべての要素に対して副作用を目的とする 関数を適用する関数を作りたい、というときに使われる 組み込み関数です。 appは、('a -> unit) -> 'a list -> unitという型を持つ関数です。 値域がunitであるような関数(つまり副作用を目的とする関数)に appを適用すると、appは、引数として受け取ったリストのすべての 要素にその関数を適用する関数(型は'a list -> unit)を 返します。 例として、printListという関数(型はstring list -> unit)を、 appを使って定義してみることにしましょう。printListは、文字列の リストに適用すると、そのリストを構成するそれぞれの文字列を、 右側に改行を追加して出力します。つまり、 - printList ["Lullus","Bruno","Bisterfield","Leibniz"]; Lullus Bruno Bisterfield Leibniz > it = () : unit というような動作をする関数です。 appを使ってprintListを定義したいときは、まず、文字列と改行を 出力してユニットを返す関数を作ります。この関数は、 fn x => print (x ^ "\n") というfn式を書くことによって作ることができます。そして次に、 その関数にappを適用します。するとappは、リストを構成する それぞれの要素にその関数を適用する関数を作って、それを 戻り値として返します。ですから、printListという識別子を、 appが返した戻り値に束縛すれば、printListが 定義されたことになります。つまり、 fun printList slst = app (fn x => print (x ^ "\n")) slst というfun宣言を書くことによって、printListを 定義することができるわけです。

---練習問題

10.1___fが等値型のデータを返す関数で、xとyがfの定義域と 同じ型のデータだとするとき、(f,x,y)という組に適用すると、 xにfを適用した結果とyにfを適用した結果とが等しいならばtrue、 等しくなければfalseを返す、equalByFunという関数(型は ('a -> ''b) * 'a * 'a -> bool)を定義するfun宣言を 書いてください。

実行例(1) - equalByFun (size,"ringo","apple"); > val it = true : bool 実行例(2) - equalByFun (size,"mikan","orange"); > val it = false : bool 実行例(3) - equalByFun (fn n => n mod 7,31,66); > val it = true : bool 実行例(4) - equalByFun (fn n => n mod 7,31,68); > val it = false : bool

10.2___fがintを定義域とする関数で、nが0またはプラスの 整数だとするとき、(f,n)という組に適用すると、n、n-1、n-2、 ……、nのそれぞれにfを適用した結果から構成されるリストを返す、 manyValuesという関数(型は(int -> 'a) * int -> 'a list)を 定義するfun宣言を書いてください。

実行例 - manyValues (fn n => n*n,9); > val it = [81, 64, 49, 36, 25, 16, 9, 4, 1, 0] : int list

10.3___次のような動作をするreduceという関数(型は ('a * 'a -> 'a) * 'a list -> 'a)を定義するfun宣言を 書いてください。

fが'a * 'a -> 'aという型の関数で、lstが'a listという型を持つ リストだとするとき、(f,lst)という組にreduceを適用すると、 reduceは、lstが空リストならばReduceという例外を発生させ、lstの 要素が1個だけならばその要素を返します。lstの長さが2以上の 場合は、まず、fとlstの尾部から構成される組にreduceを適用した 結果を求めて、次に、lstの頭部と、lstの尾部を処理した結果から 構成される組にfを適用して、その結果を返します。 実行例(1) - reduce (op+,[27,34,12,61,59]); > val it = 193 : int 実行例(2) - reduce (op^,["Cetus","Volans","Cancer","Delphinus"]); > val it = "CetusVolansCancerDelphinus" : string

10.4___nが整数だとするとき、nに適用すると、「mが 整数だとするとき、mに適用すると、nをmで除算したときのあまりが 0ならばtrue、そうでなければfalseを返す関数(型は int -> bool)」を返す、divisibleという関数(型は int -> int -> bool)を定義するカリー化形式のfun宣言を 書いてください。

実行例(1) - divisible 42 7; > val it = true : bool 実行例(2) - divisible 42 5; > val it = false : bool

10.5___fが'a * 'b -> 'cという型の関数だとするとき、fに 適用すると、fをカリー化した結果を返す、curryという関数(型は ('a * 'b -> 'c) -> 'a -> 'b -> 'c)を定義するカリー化形式の fun宣言を書いてください。

実行例(1) - curry op+ 20 36; > val it = 56 : int 実行例(2) - curry (fn (x,y) => size x = size y) "corvus" "aquila"; > val it = true : bool

10.6___fが'a -> 'b -> 'cという型の関数だとするとき、fに 適用すると、fを非カリー化した結果を返す、uncurryという 関数(型は('a -> 'b -> 'c) -> 'a * 'b -> 'c)を定義する カリー化形式のfun宣言を書いてください。

実行例(1) - uncurry (curry op+) (37,40); > val it = 77 : int 実行例(2) - uncurry (fn x => fn y => size x = size y) ("ursa","leo"); > val it = false : bool

10.7___組み込み演算子のoと同じ動作をする、compositeという 関数を定義するカリー化形式のfun宣言を書いてください。

実行例 - composite (fn n => n mod 2 = 0,ord) #"M"; > val it = false : bool

10.8___fが'a -> 'aという型の関数だとするとき、fに適用すると、 「xが'aという型のデータだとするとき、xに適用すると、xにfを 適用した結果にさらにfを適用して、その結果を返す関数(型は 'a -> 'a)」を返す、duplicateという関数(型は ('a -> 'a) -> 'a)を定義するカリー化形式のfun宣言を 書いてください。

実行例 - duplicate (fn x => "[" ^ x ^ "]") "aulos"; > val it = "[[aulos]]" : string

10.9___組み込み関数のmapと同じ動作をする、applyToAllElemという 関数を定義するカリー化形式のfun宣言を、foldrを使って 書いてください。

実行例 - applyToAllElem (fn n => n-1) [58,63,21,94,72,12]; > val it = [57, 62, 20, 93, 71, 11] : int list

10.10___組み込み関数のfoldrと同じ動作をする、listFunという 関数を定義するカリー化形式のfun宣言を書いてください。

実行例 - listFun (fn (x,y) => x::x::y) [] [53,21,34,88]; > val it = [53, 53, 21, 21, 34, 34, 88, 88] : int list

10.11___整数のリストに適用すると、それを構成するそれぞれの 要素に2を乗算することによってできるリストを返す、 mapDoubleという関数(型はint list -> int list)を定義する fun宣言を、mapを使って書いてください。

実行例 - mapDouble [15,7,31,18,60]; > val it = [30, 14, 62, 36, 120] : int list

10.12___nが整数だとするとき、nに適用すると、「整数のリストに 適用すると、それを構成するそれぞれの要素にnを 乗算することによってできるリストを返す関数(型は int list -> int list)」を返す、mapTimesという関数(型は int -> int list -> int list)を定義するカリー化形式の fun宣言を、mapを使って書いてください。

実行例 - mapTimes 10 [15,7,31,18,60]; > val it = [150, 70, 310, 180, 600] : int list

10.13___文字列のリストに適用すると、それを構成するそれぞれの 文字列を空文字列の左と右の両側に連結していくことによってできる 文字列を返す、concatBothSidesという関数(型は string list -> string)を定義するfun宣言を、foldrを使って 書いてください。

実行例 - concatBothSides ["iso","para","poly","meta","hemi"]; > val it = "isoparapolymetahemihemimetapolyparaiso" : string

10.14___文字列のリストに適用すると、それを構成するそれぞれの 文字列をもとの順序と同じ順序で連結することによってできる 文字列と、もとの順序とは逆の順序で連結することによってできる 文字列、という二つの文字列から構成される組を返す、 concatLeftRightという関数(型は string list -> string * string)を定義するfun宣言を、foldrを 使って書いてください。

実行例 - concatLeftRight ["Nile","Amazon","Indus","Volga"]; > val it = ("NileAmazonIndusVolga", "VolgaIndusAmazonNile") : string * string

10.15___組み込み関数のlengthと同じ動作をするhowLongという 関数を定義するfun宣言を、foldrを使って書いてください。

実行例 - howLong ["Ulithi","Yap","Ngulu","Fais","Sorol","Faraulep", "Gaferut"]; > val it = 7 : int

10.16___リストに適用すると、そのリストを構成するそれぞれの 要素を2個ずつ並べることによってできるリストを返す、 duplicateElemという関数(型は'a list -> 'a list)を定義する fun宣言を、foldrを使って書いてください。

実行例 - duplicateElem [79,11,96,34]; > val it = [79, 79, 11, 11, 96, 96, 34, 34] : int list

10.17___lst1がリストだとするとき、lst1に適用すると、「lst2が リストだとするとき、lst2に適用すると、lst2の左側にlst1を 連結することによってできるリストを返す関数(型は 'a list -> 'a list)」を返す、concatenateという関数(型は 'a list -> 'a list -> 'a list)を定義するカリー化形式の fun宣言を、foldrを使って書いてください。

実行例 - concatenate ["semi","quasi","pseudo"] ["super","hyper"]; > val it = ["semi", "quasi", "pseudo", "super", "hyper"] : string list

10.18___fが'a -> boolという型の関数だとするとき、fに 適用すると、「lstが'a listという型のリストだとするとき、lstに 適用すると、fを適用した結果がtrueになるlstの要素をlstから 削除することによってできるリストを返す関数(型は 'a list -> 'a list)」を返す、deleteTrueという関数(型は ('a -> bool) -> 'a list -> 'a list)を定義するカリー化形式の fun宣言を、foldrを使って書いてください。

実行例 - deleteTrue (fn n => n>=50) [43,27,61,33,78,45]; > val it = [43, 27, 33, 45] : int list

10.19___flstが('a -> 'b) listという型のリストだとするとき、 flstに適用すると、「sampleが'aという型のデータだとするとき、 sampleに適用すると、flstを構成するそれぞれの関数をsampleに 適用することによって得られる値から構成されるリストを返す 関数(型は'a -> 'b list)」を返す、examineという関数(型は ('a -> 'b) list -> 'a -> 'b list)を定義するカリー化形式の fun宣言を、foldrを使って書いてください。

実行例 - examine [fn n => n+1,fn n => n-1,fn n => n*2,fn n => n*n, fn n => n div 2,fn n => n mod 2] 7; > val it = [8, 6, 14, 49, 3, 1] : int list

10.20___fが'a -> boolという型の関数だとするとき、fに 適用すると、「lstが'a listという型のリストだとするとき、lstに 適用すると、fを適用した結果がtrueになるxの要素のリストと falseになるlstの要素のリストを作って、それらのリストから 構成される組を返す関数(型は'a list -> 'a list * 'a list)」を 返す、divideByBoolという関数(型は ('a -> bool) -> 'a list -> 'a list * 'a list)を定義する カリー化形式のfun宣言を、foldrを使って書いてください。

実行例 - divideByBool (fn n => n>=50) [43,27,61,33,78,45]; > val it = ([61, 78], [43, 27, 33, 45]) : int list * int list

10.21___nが整数だとするとき、nに適用すると、「slstが文字列の リストだとするとき、slstに適用すると、長さがn以上であるslstの 要素のリストと、nよりも短いslstの要素のリストから構成される 組を返す関数(型は string list -> string list * string list)」を返す、 divideLongShortという関数(型は int -> string list -> string list * string list)を 定義するカリー化形式のfun宣言を、練習問題10.20で作った divideByBoolを使って書いてください。

実行例 - divideLongShort 8 ["Lagrange","Galileo","Kepler","Messier", "Copernicus","Cassini","Herschel"]; > val it = (["Lagrange", "Copernicus", "Herschel"], ["Galileo", "Kepler","Messier", "Cassini"]) : string list * string list

第11章===型の定義

11.1---datatype宣言

Q 11.1.1___型を定義するっていうのはどういうことですか。

型を定義するというのは、 (1) 型(つまりデータの集合)を作り出すという動作をするもの (2) その型のデータを作り出すという動作をするもの という2種類のものを作って、識別子をそれらに 束縛するということです。

Q 11.1.2___型構成子って何ですか。

型構成子というのは、型を作り出すという動作をするもの、 またはそれに束縛された識別子のことです。 実は、intやboolやstringというような、「型名」と呼ばれている ものは、型構成子に束縛されている識別子なのです。さらに、 listというのも型構成子に束縛されている識別子です。listという 識別子は、何らかの既存の型からリスト型を作り出すという 動作をする型構成子に束縛されています。

Q 11.1.3___データ構成子って何ですか。

データ構成子というのは、データを作り出すという 動作をするもの、またはそれに束縛された識別子のことです。

Q 11.1.4___型を定義したいときはどうすればいいのですか。

型を定義したいときは、datatype宣言というものを書きます。 datatype宣言は宣言の一種で、新しい型を定義するという動作を 記述するために使われるものです。 datatype宣言の構文は、厳密に説明しようとするとちょっと 複雑なので、とりあえず、きわめて単純な型を定義する場合に 限定して説明することにしましょう。その場合、datatype宣言は、 datatype 識別子T = 識別子1 | 識別子2 | …… | 識別子n というように書きます。このようなdatatype宣言をコンピュータに 実行させると、コンピュータは、 (1) 新しい型(つまりデータの集合)を作り出す型構成子を作って、 識別子Tをその型構成子に束縛する。 (2) n個(つまりイコールの右側に書いた識別子と同じ個数)の データ構成子を作って、識別子1から識別子nまでのそれぞれを、 それらのデータ構成子に束縛する。 という動作をします。 たとえば、 datatype signal = Green | Yellow | Red というdatatype宣言をコンピュータに実行させると、 コンピュータは、 (1) 新しい型を作り出す型構成子を作って、signalという識別子を その型構成子に束縛する。 (2) 3個のデータ構成子を作って、Green、Yellow、Redという 識別子のそれぞれをそれらのデータ構成子に束縛する。 という動作をします(注)。 (注) データ構成子に束縛する識別子は、例外の名前と同じように、 先頭を大文字にするという慣習があります。 上に書いたdatatype宣言の例を対話型システムに入力したとすると、 それ以降から、GreenとYellowとRedというデータ構成子が実際に 使えるようになります。つまり、 - Green; > val it = Green : signal - (Yellow,Green,Red): > val it = (Yellow, Green, Red) : signal * signal * signal - [Red,Yellow,Yellow,Green,Red]; > val it = [Red, Yellow, Yellow, Green, Red] : signal list というように、データ構成子を使ってsignalを構成する要素を 作り出すことができるわけです。

Q 11.1.5___定義域または値域を構成する型として、datatype宣言で 定義された型を含んでいるような関数を作ることは可能ですか。

はい、可能です。 datatype宣言で定義された新しい型は、最初から処理系に 組み込まれている型とまったく同じように使うことができます。 ですから、datatype宣言によって新しく作られた型を定義域や 値域の中に含んでいる関数も、普通のfun宣言を書くことによって 定義することができます。 それでは、Q 11.1.3で定義されたsignalという型のデータを扱う 関数をいくつか作ってみましょう。 ひとつ目の例は、hazardLevelという関数(型は int -> signal)です。この関数は、整数に適用すると、それが 80未満ならばGreenを、そうでなくて100未満ならばYellowを、 そうでなければRedを返します。つまり、 - hazardLevel 60; > val it = Green : signal - hazardLevel 90; > val it = Yellow : signal - hazardLevel 110; > val it = Red : signal というような動作をする関数です。 hazardLevelは、 fun hazardLevel n = if n<80 then Green else if n<100 then Yellow else Red というように、ごく普通のfun宣言を書くことによって 定義することができます。 二つ目の例として、signalToStringという関数(型は signal -> string)を作ってみましょう。この関数は、signalの データに適用すると、そのデータを文字列に変換した結果を 返します。たとえば、signalToStringをYellowに適用したとすると、 - signalToString Yellow; > val it = "yellow" : string というように、"yellow"という文字列が得られます。 データ構成子は、パターンとして使うことも可能です。ですから、 signalToStringを定義するfun宣言は、 fun signalToString Green = "green" | signalToString Yellow = "yellow" | signalToString Red = "red" というように、GreenとYellowとRedをパターンとして 使うことによって書くことができます。 三つ目の例は、定義域がsignal listである、dangerousという 関数です。この関数は、 - dangerous [Green,Green,Yellow,Yellow,Red,Green]; > val it = true : bool というようにリストの中にRedが含まれている場合はtrueを返し、 - dangerous [Yellow,Green,Green,Yellow,Yellow,Green]; > val it = false : bool というようにリストの中にRedが含まれていない場合はfalseを 返します。dangerousを定義するfun宣言は、 fun dangerous [] = false | dangerous (Red::xs) = true | dangerous (_::xs) = dangerous xs と書くことができます。

11.2---既存の型を利用した型の定義

Q 11.2.1___すでに存在する型を部分集合として含む新しい型を 定義する、ということは可能ですか。

はい、可能です。 datatype宣言の構文を、Q 11.1.4に書いたものよりもさらに厳密に 書くと、 datatype 識別子T = 識別子1 of 型式1 | 識別子2 of 型式2 | 識別子3 of 型式3 …… | 識別子n of 型式n というようになります。つまり、イコールの右側には、 識別子 of 型式 という形のものを書くことができるわけです。この形の datatype宣言をコンピュータに実行させると、コンピュータは、 (1) 型式1から型式nまでのそれぞれがあらわしている型の 和集合であるような新しい型を作り出す型構成子を作って、 識別子Tをその型構成子に束縛する。 (2) 「識別子 of 型式」のそれぞれについて、ofの右側の 型式によって指定された型のデータから新しい型のデータを作り出す データ構成子を作って、ofの左側の識別子をそのデータ構成子に 束縛する。 という動作をします。 たとえば、 datatype intstring = ISI of int | ISS of string というdatatype宣言をコンピュータに実行させると、 コンピュータは、 (1) intとstringの和集合であるような新しい型を作り出す 型構成子を作って、intstringという識別子をその型構成子に 束縛する。 (2) intのデータからintstringのデータを作り出すデータ構成子を 作って、ISIという識別子をそのデータ構成子に束縛する。さらに、 stringのデータからintstringのデータを作り出すデータ構成子を 作って、ISSという識別子をそのデータ構成子に束縛する。 という動作をします。

Q 11.2.2___データ構成子を使って既存の型のデータから新しい型の データを作り出したいときは、どうすればいいのですか。

そんなときは、データに関数を適用するのと同じように、既存の型の データにデータ構成子を適用します。 Q 11.2.1で登場したISIやISSのような、 識別子 of 型式 という記述によって作られたデータ構成子は、既存の型のデータから 新しい型のデータを作り出すという動作をします。このような データ構成子は、けっして関数ではないのですが、あたかも 関数であるかのようにデータに適用することができます。 データ構成子を既存の型のデータに適用すると、その データ構成子は、引数として受け取ったデータから新しい型の データを作り出して、それを戻り値として返します。データ構成子を データに適用したいときは、関数をデータに適用する場合と 同じように、 データ構成子 式 という形の式を書きます。この形の式を評価すると、右側の式の値に データ構成子が適用されて、そのデータ構成子が作り出したデータが 式全体の値になります。ですから、Q 11.2.1で定義を書いた intstringという型のデータを作り出したいときは、 - ISI 35; > val it = ISI 35 : intstring - ISS "harmonike"; > val it = ISS "harmonike" : intstring というように、ISIというデータ構成子をintのデータに 適用するか、またはISSというデータ構成子をstringのデータに 適用すればいいわけです。 intstringのデータから構成される組とか、intstringのデータの リストとかも、同じようにして作り出すことができます。たとえば、 - (ISI ~21,ISS "geometria"); > val it = (ISI ~21, ISS "geometria") : intstring * intsrting - [ISI 63,ISS "astrologia",ISS "mechanike",ISI ~90]; > val it = [ISI 63, ISS "astrologia", ISS "mechanike", ISI ~90] : intstring list というような感じです。intstringは整数の集合と文字列の集合との 和集合ですから、それを使うことによって整数と文字列とが 混在しているようなリストを作ることができる、 ということになります。

Q 11.2.3___別の型のデータからデータ構成子によって作り出された データがあるとするとき、そのデータをもとの型に戻したいときは どうすればいいのですか。

そんなときは、 データ構成子 パターン という形のパターンとそのデータとを照合します。この照合が 成功すると、データ構成子の右側のパターンは、そのデータをもとの 型に戻したものに束縛されることになります。 Q 11.2.1で定義したintstringという型を使って、もう少し具体的に 説明しましょう。たとえば、 - val wasabi = ISI 21; > val wasabi = ISI 21 : intstring というように、21というintのデータから作り出されたintstringの データにwasabiという識別子を束縛したとしましょう。もしも このあとでwasabiをintに戻したいと思ったときは、 ISI xというようなパターンとintstringのデータとを照合します。 すると、 - val ISI x = wasabi; > val x = 21 : int というように照合は成功して、xは21というintのデータに 束縛されます。 ISI xというパターンはintから作り出されたintstringのデータを もとに戻すためのものです。stringから作り出されたintstringの データをもとに戻したいときは、ISS xというパターンを 使います。 それでは、intstringのデータに適用することのできる関数の 例として、intstringToStringという関数(型は intstring -> string)の定義を書いてみましょう。 intstringToStringは、intstringのデータに適用すると、 そのデータがintから作り出されたものならばそれをintに 戻してからInt.toStringを使って文字列に変換した結果を返します。 そうではなくてそのデータがstringから作り出されたものならば、 それをstringに戻したものを返します。つまり、 - intstringToString (ISI 66); > val it = "66" : string - intstringToString (ISS "Obi-Wan") > val it = "Obi-Wan" : string というような動作をする関数です。intstringToStringは、 fun intstringToString (ISI x) = Int.toString x | intstringToString (ISS x) = x というfun宣言を書くことによって定義することができます。

Q 11.2.4___既存の型に新しいデータを追加したような型を 定義することは可能ですか。

はい、可能です。 datatype宣言のイコールの右側には、データ構成子を作るための 記述を、縦棒で区切りながら何個でも書くことができます。 データ構成子を作るための記述は、 識別子 of 型式 という形のものと、識別子だけのもの、という2種類が あるわけですが、それらは自由に混在させることができます。 ですから、「of 型式」があるものとないものの両方を含んでいる datatype宣言を書くことによって、既存の型に新しいデータを 追加したような型を定義することができます。たとえば、 datatype age = AI of int | Unknown というdatatype宣言によって定義されるageという型構成子は、 intという既存の型にUnknownという新しいデータを追加した型を 作り出します。 それでは、ageのデータを扱う関数の例として、averageAgesという 関数(型はage list -> age)を定義してみましょう。 averageAgesは、ageのデータのリストに適用すると、そのリストの 要素を平均した結果を返します。ただし、リストの中に含まれている Unknownは計算の対象外です。リストが空だった場合や、リストの 要素がすべてUnknownだった場合は、Unknownを返します。つまり、 averageAgesは、 - averageAges [AI 23,AI 58,AI 39,Unknown,AI 18]; > val it = AI 34 : age - averageAges []; > val it = Unknown : age - averageAges [Unknown,Unknown,Unknown,Unknown]; > val it = Unknown : age というような動作をする関数です。averageAgesを定義する fun宣言は、 fun averageAges x = let fun totalAndCount [] = (0,0) | totalAndCount (Unknown::xs) = totalAndCount xs | totalAndCount (AI x::xs) = let val (total,count) = totalAndCount xs in (total+x,count+1) end val (total,count) = totalAndCount x in if count=0 then Unknown else AI (total div count) end と書くことができます。

Q 11.2.5___積型やリスト型を部分集合として含んでいるような 新しい型を定義することは可能ですか。

はい、可能です。 新しい型を定義するとき、datatype宣言の中に、 識別子 of 型式 という記述を書いたとすると、ofの右側の型式で指定された型が、 新しく定義される型の部分集合になるわけですが、ここのofの 右側には、どんな型式を書いてもかまいません。ですから、 string * int int list (string * int) list string list * int list というような、積型とかリスト型とかそれらを組み合わせた型を あらわす型式をofの右側に書けば、そのような型を部分集合として 含む型が新しく定義されることになります。 たとえば、 datatype ipsi = IPSII of int | IPSIPSI of string * int というdatatype宣言によって定義されたipsiという型は、 intとstring * intとの和集合です。ですからipsiを 使うことによって、 - [IPSII 79,IPSIPSI("ego",48),IPSIPSI("nos",16),IPSII 22]; > val it = [IPSII 79, IPSIPSI("ego", 48), IPSIPSI("nos", 16), IPSII 22] : ipsi list というような、整数と、文字列と整数の組とが混在しているリストを 作ることができます。 同じように、 datatype iil = IILI of int | IILIL of int list というdatatype宣言を書くことによって、intとint listとの 和集合であるような、iilという型を定義することができます。 iilは、 - [IILI 12,IILIL [48,~99,3,11],IILI 77,IILIL [29,33,14]]; > val it = [IILI 12, IILIL [48, ~99, 3, 11], IILI 77, IILIL [29, 33, 14]] : iil list というような、整数と、整数のリストとが混在しているリストを 作ることを可能にします。

Q 11.2.6___datatype宣言によって新しく定義された型を 部分集合として含んでいるような新しい型を定義することは 可能ですか。

はい、可能です。 datatype宣言によって定義された型構成子は、処理系の中に最初から 組み込まれているintやstringなどの型構成子と、まったく 同じように使うことができます。ということは、datatype宣言のofの 右側には、intやstringだけではなく、datatype宣言によって 定義された型構成子を書いてもかまわない、ということになります。 たとえば、 datatype season = Spring | Summer | Autumn | Winter というように定義されたseasonという型構成子は、intやcharや stringなどと同じように使うことができますので、 datatype japaneseSeason = JSS of season | Tsuyu というように、datatype宣言のofの右側にseasonという型構成子を 書くことによって、seasonを部分集合として含む新しい型を 定義するということができます。

11.3---引数を受け取る型構成子

Q 11.3.1___引数を受け取る型構成子っていうのは どんなものなんですか。

引数を受け取る型構成子というのは、型に適用すると、その型を 利用した別の型を作り出す型構成子のことです。 intやstringなどと同じように、listというのも型構成子の ひとつです。でも、intやstringと、listとのあいだには、ひとつの 顕著な相違点があります。それは、intやstringは引数を 受け取らない型構成子であるのに対して、listは引数を受け取る 型構成子である、という相違点です。listは、引数として1個の型を 受け取って、その型のデータを要素とするリストの型を作り出す、 という動作をする型構成子なのです。 引数を受け取る型構成子によって作り出される型は、 型式 型構成子 という形の型式によってあらわされます。この型式は、右側に 書かれた型構成子を、左側に書かれた型式があらわしている型に 適用したときに、その型構成子が作り出した型、という 意味になります。たとえば、 int list という型式は、listをintに適用したときにlistが作り出した型、 という意味です。

Q 11.3.2___引数を受け取る型構成子を定義するためにはどうすれば いいのですか。

引数を受け取る型構成子は、datatype宣言の中で型変数を 使うことによって定義することができます。 datatype宣言の構文を、以前に説明したときよりもさらに厳密に 書くと、 datatype 型変数 識別子T = 識別子1 of 型式1 | 識別子2 of 型式2 | 識別子3 of 型式3 …… | 識別子n of 型式n というようになります。つまり、datatypeという単語と、型構成子に 束縛される識別子とのあいだに、型変数を書くことができるのです。 このような、型変数を含むdatatype宣言で新しい型を定義すると、 引数を受け取る型構成子が作られて、識別子Tはその型構成子に 束縛されることになります。 datatypeという単語と、型構成子に束縛される識別子とのあいだに 書かれた型変数は、ofの右側の型式の中で使うことができます。 つまり、 datatype 'a singlePair = SPS of 'a | SPP of 'a * 'a というようなdatatype宣言を書くことができるということです。 この例の場合、singlePairという型構成子は、1個の型を引数として 受け取って、その型と、その型の2個のデータから構成される組との 和集合であるような型を作り出します。つまり、singlePairは、 intとint * intとの和集合であるような型を作り出したり、stringと string * stringとの和集合であるような型を 作り出したりすることができるということです。 上に書いたdatatype宣言で定義されたSPSというデータ構成子は、 任意の型のデータに適用することができます。つまり、 - SPS 20; > val it = SPS 20 : int singlePair - SPS "Verhoeven"; > val it = SPS "Verhoeven" : string singlePair というように、整数に適用したり文字列に 適用したりすることができるわけです。SPSは、整数に適用された 場合はint singlePairという型のデータを作り出し、文字列に 適用された場合はstring singlePairという型のデータを 作り出します。 SPPというデータ構成子は、同じ型を持つ2個のデータから 構成される組に適用することができます。つまり、 - SPP(53,81); > val it = SPP(53, 81) : int singlePair - SPP("Zemeckis","Verbinski"); > val it = SPP("Zemeckis", "Verbinski") : string singlePair というように、int * intという型を持つ組や、 string * stringという型を持つ組に 適用することができるということです。SPPは、int * intのデータに 適用された場合はint singlePairという型のデータを作り出し、 string * stringのデータに適用された場合は string singlePairという型のデータを作り出します。

Q 11.3.3___2個以上の異なる型を引数として受け取る型構成子を 定義することは可能ですか。

はい、可能です。 nが2以上の整数だとするとき、n個の異なる型を引数として受け取る 型構成子を定義したいときは、 datatype ( 型変数1 , 型変数2 , …… , 型変数n ) 識別子T = 識別子1 of 型式1 | 識別子2 of 型式2 | 識別子3 of 型式3 …… | 識別子n of 型式n という形のdatatype宣言を書きます。つまり、datatypeという 単語と、型構成子に束縛される識別子とのあいだに、引数として 受け取る型の個数と同じ個数の型変数をコンマで区切って並べて、 その全体を丸括弧で囲んだものを書けばいいわけです。 たとえば、 datatype ('a,'b) twoTypes = ABA of 'a | ABB of 'b | ABP of 'a * 'b というdatatype宣言を書くことによって、twoTypesという識別子は、 2個の型を引数として受け取る型構成子に束縛されます。 2個以上の型を引数として受け取る型構成子によって作り出される 型をあらわす型式は、 ( 型式 , 型式 , …… , 型式 ) 型構成子 と書きます。たとえば、上で定義したtwoTypesという 型構成子によって作り出される型は、 (int,string) twoTypes (string,string) twoTypes (int list,int * int) twoTypes というような型式によってあらわされます。

Q 11.3.4___引数を受け取る型構成子によって作り出される任意の 型のデータを扱うことのできる関数を定義することは可能ですか。

はい、可能です。 それでは、引数を受け取る型構成子によって作り出される任意の 型のデータを扱うことのできる関数の例として、classifyという 関数を定義してみましょう。これは、Q 11.3.1で定義を書いた singlePairという型構成子によって作り出される型のデータならば どんなものでも扱うことができる関数です。 singlePairによって作り出された型のデータのリスト(型は 'a singlePair list)があるとするとき、classifyをそのリストに 適用すると、classifyは、そのリストの要素を、'aという型を持つ データからから作り出されたものと、'a * 'aという型を持つ データから作り出されたもの、という二つのグループに分類して、 'aという型のデータのリストと'a * 'aという型のデータの リストから構成される組を返します(classifyの型は 'a singlePair list -> 'a list * ('a * 'a) listです)。 つまり、classifyは、 - [SPS 38,SPP(92,44),SPS 19,SPS 77,SPP(76,38),SPS 61]; > val it = ([38, 19, 77, 61], [(92, 44), (76, 38)]) : int list * (int * int) list - [SPP("Janet","Dennis"),SPS "Anthony",SPP("Dorothy", "Franklin"),SPS "Susan",SPS "Raymond"]; > val it = (["Anthony", "Susan", "Raymond"], [("Janet", "Dennis"), ("Dorothy", "Franklin")]) : string list * (string * string) list というように、int singlePairという型のデータも、 string singlePairという型のデータも扱うことが できるということです。 classifyを定義するfun宣言は、 fun classify [] = ([],[]) | classify (x::xs) = let val (s,p) = classify xs in case x of SPS x1 => (x1::s,p) | SPP x1 => (s,x1::p) end と書くことができます。

11.4---再帰的な型の定義

Q 11.4.1___型が再帰的である、というのは どういうことなんですか。

型が再帰的であるというのは、その型に属しているデータが再帰的な 構造を持っているという意味です。 たとえば、listという型構成子によって作り出される型、つまり リスト型は、再帰的な型です。なぜなら、リスト型の要素である リストというデータは再帰的な構造を持っているからです。

Q 11.4.2___再帰的な型を定義したいときは どうすればいいのですか。

再帰的な型を定義したいときは、datatype宣言の中に、 基底となるデータについての記述と、全体と同じ構造のものを 内部に含んでいるデータについての記述を書きます。 再帰的な型の定義について、具体的な例を使って 説明することにしましょう。MLの処理系にはリスト型という 再帰的な型が最初から組み込まれているわけですが、それと 同じようなものを作り出すcatalogという型構成子を新しく 定義する、ということについて考えてみます。 リストというのは、1個のデータ(頭部)と1個のリスト(尾部)から 構成される再帰的なデータです。リストの基底は、0個のデータから 構成されるリスト(空リスト)です。したがって、catalogという 型構成子を定義するdatatype宣言は、 datatype 'a catalog = CEmpty | CNode of 'a * 'a catalog と書くことができます。このdatatype宣言によって定義される catalogという型構成子がこのdatatype宣言の中で使われている、 という点に注意してください。このように、datatype宣言の中で、 その宣言によって定義される型構成子を使うと、その型構成子は 再帰的な型を作り出すことになります。 CEmptyは、空リストを作り出すデータ構成子です。そして頭部と 尾部から構成されるデータを作り出したいときは、 CNode ( 頭部 , 尾部 ) という式を書くことによって、リストの頭部と尾部から構成される 組にCNodeというデータ構成子を適用します。頭部の型が 'aだとすると、尾部の型は'a catalogでないといけません。 CEmptyとCNodeを使うことによって、 - CEmpty; > val it = CEmpty : 'a catalog - CNode("George Orwell",CEmpty); > val it = CNode("George Orwell", CEmpty) : string catalog - CNode(81,CNode(66,CEmpty)); > val it = CNode(81, CNode(66, CEmpty)) : int catalog - CNode(72,CNode(15,CNode(34,CEmpty))); > val it = CNode(72, CNode(15, CNode(34, CEmpty))) : int catalog というように、リストを作り出すことができます。

Q 11.4.3___木って何ですか。

木というのは、データを階層的に組み合わせることによって 作られたデータのことです。 木を構成するそれぞれのデータのことを「節点」と呼びます。 木は、節点を「親子関係」と呼ばれる関係によって別の節点に 連結していくことによって作られます。aとbが節点で、aからbへの 親子関係があるとするとき、aをbの「親」と呼び、bをaの「子供」と 呼びます。節点は、子供は何個でも持つことができるのですが、 2個以上の親を持つことはできません。 0個の節点から構成される木のことを「空の木」と呼びます。空の 木ではない木は、親を持たない節点をかならず1個だけ 含んでいます。そのような特別な節点のことを「根」と 呼びます。根を出発点として親子関係を親から子供へ たどっていくと、木を構成する、根以外のどの節点へも 到達することができます。 木は、階層的な構造を持っています。言い換えると、木を構成する それぞれの節点は、上下に重なっているいくつかの階層に 分類することができる、ということです。節点がどの階層に 属しているかということは、その節点が、根から数えて親子関係を 何段階だけたどったところにあるか、ということによって 決定されます。つまり、根はいちばん上の階層に属していて、根の 子供は上から2番目の階層に属していて、根の子供の子供は 上から3番目の階層に属している、というように考えるわけです。 木は、再帰的な構造を持っています。aという節点が木の根で、 bという節点がaの子供だとするとき、bと、bを出発点として 親子関係を親から子供へたどっていくことによって到達できる 節点から構成される集合は、bを根とする木になっています。 つまり、木というのは、1個の節点といくつかの木から 構成されている、と考えることができるわけです。 木の一部分であるような木のことを「部分木」と言います。 木というのは、すべて、1個の節点と何個かの部分木から 構成されている、と考えることができます。

Q 11.4.4___二分木って何ですか。

二分木というのは、根と、全体と同じ構造を持つ2個の部分木 (これらの部分木は、左か右かという位置の区別を 持っています)から構成される木のことです。 「二分木」は、次のように定義することができます。 まず、空の木は二分木です。 そして、空ではない木は、 ● 根と、左か右かという位置の区別を持つ2個の部分木、 という三つの部分から構成される。 ● 2個の部分木は、両方とも二分木である。 という条件を満足しているならば、その場合に限り、二分木であると みなすことができます。 さて、二分木を構成する2個の部分木は、かならず位置の区別を 持っています。それらの部分木のうち、左にあるものは 「左部分木」と呼ばれ、右にあるものは「右部分木」と呼ばれます。

Q 11.4.5___二分木の型を定義するためには、どのような datatype宣言を書けばいいのですか。

二分木の型は、再帰的なdatatype宣言を書くことによって 定義することができます。 二分木の型は、Q 11.4.4で書いた二分木の定義を、ほとんどそのまま datatype宣言の形で記述することによって定義することができます。 つまり、 datatype 'a tree = TEmpty | TNode of 'a * 'a tree * 'a tree というようなdatatype宣言を書けばいいわけです。 このように二分木の型を定義したとすると、TEmptyとTNodeという データ構成子を、二分木を作り出すために使うことができます。 たとえば、 - TEmpty; > val it = TEmpty : 'a tree - TNode("aleph",TEmpty,TEmpty); > val it = Tnode("aleph", TEmpty, TEmpty) : string tree - TNode(84,TNode(91,TEmpty,TEmpty),TNode(37,TEmpty,TEmpty)); > val it = TNode(84, TNode(91, TEmpty, TEmpty), TNode(37, TEmpty, TEmpty)) : int tree - TNode("Markab", TNode("Scheat", TEmpty, TNode("Algenib", TNode("Alpheratz",TEmpty,TEmpty), TNode("Enif",TEmpty,TEmpty))), TNode("Homam", TNode("Matar",TEmpty,TEmpty), TNode("Baham",TEmpty,TEmpty))); > val it = TNode("Markab", TNode("Scheat", TEmpty, TNode("Algenib", TNode("Alpheratz", TEmpty, TEmpty), TNode("Enif", TEmpty, TEmpty))), TNode("Homam", TNode("Matar", TEmpty, TEmpty), TNode("Baham", TEmpty, TEmpty))) : string tree というように作ればいいわけです。 それでは、二分木に適用することのできる関数の例として、 二分木に含まれている節点の個数を数える、treeSizeという 関数(型は'a tree -> int)の定義を書いてみましょう。 treeSizeを二分木に適用すると、treeSizeは、 - treeSize TEmpty; > val it = 0 : int - treeSize (TNode(28, TNode(19,TEmpty,TEmpty), TNode(67,TEmpty,TEmpty))); > val it = 3 : int - treeSize (TNode("Melvin", TNode("Simon", TNode("Frank",TEmpty,TEmpty), TNode("Vincent",TEmpty,TEmpty)), TNode("Carol",TEmpty,TEmpty))); > val it = 5 : int というように、二分木の節点の個数を戻り値として返します。 treeSizeを定義するfun宣言は、 fun treeSize TEmpty = 0 | treeSize (TNode(_,left,right)) = 1 + treeSize left + treeSize right と書くことができます。

Q 11.4.6___相互再帰的な構造を持ついくつかの型を 定義したいときは、どのようなdatatype宣言を書けばいいのですか。

相互再帰的な構造を持ついくつかの型は、1個のdatatype宣言の 中に、それぞれの型についての記述をandで接続して 並べることによって定義することができます。 datatype宣言は、 datatype 定義したい型についての記述1 and 定義したい型についての記述2 …… and 定義したい型についての記述n という書き方ができるようになっています。この書き方を使えば、 2個以上の型を同時に定義することができますので、それらの型が 相互再帰的な構造を持っていたとしても不都合は生じません。 それでは、相互再帰的な構造を持つデータの例として、 「偶数リスト」と「奇数リスト」というものについて 考えてみましょう。 要素の個数がかならず偶数になっているようなリストのことを 「偶数リスト」と呼び、要素の個数がかならず 奇数になっているようなリストのことを「奇数リスト」と 呼ぶことにします。それらのリストは相互再帰的な構造を 持っていますので、 ● 空リストは偶数リストである。 ● 奇数リストに1個の要素を連結したものは偶数リストである。 ● 以上の記述から導かれるもの以外は偶数リストではない。 ● 偶数リストに1個の要素を連結したものは奇数リストである。 ● 以上の記述から導かれるもの以外は奇数リストではない。 というように、相互再帰的に定義することができます。 偶数リストの型と奇数リストの型は、上に書いた日本語による定義を datatype宣言の形に書き直すことによって定義することができます。 したがって、 datatype 'a evenList = EEmpty | ENode of 'a * 'a oddList and 'a oddList = ONode of 'a * 'a evenList というdatatype宣言を書けば、偶数リストの型を作り出す evenListという型構成子と、奇数リストの型を作り出す oddListという型構成子が定義されることになります。 偶数リストや奇数リストは、EEmptyとENodeとONodeという データ構成子を使うことによって作り出すことができます。 つまり、 - EEmpty; > val it = EEmpty : 'a enenList - ONode(48,EEmpty); > val it = ONode(48, EEmpty) : int oddList - ENode("Bibby",ONode("Cardelli",EEmpty)); > val it = ENode("Bibby", ONode("Cardelli", EEmpty)) : string evenList - ONode(93,ENode(78,ONode(31,EEmpty))); > val it = ONode(93, ENode(78, ONode(31, EEmpty))) : int oddList というようにすればいいわけです。

---練習問題

11.1___加算、減算、乗算、除算という演算のそれぞれをあらわす 4個のデータから構成される型を作り出す、operatorという 型構成子を定義するdatatype宣言を書いてください。

なお、 operatorのデータを作り出すデータ構成子は、加算がAdd、減算が Subtract、乗算がMultiply、除算がDivideだとします。

11.2___operator(練習問題11.1で定義したもの)とintとの 和集合であるような型を作り出す、tokenという型構成子を定義する datatype宣言を書いてください。

なお、operatorのデータから tokenのデータを作り出すデータ構成子はTO、intのデータから tokenのデータを作り出すデータ構成子はTIだとします。

11.3___0を含む自然数の集合であるような型を作り出す、 numberという型構成子を定義するdatatype宣言を書いてください。

なお、0をあらわすデータを作り出すデータ構成子は Zeroだとします。そして、numberのデータから、それよりも1だけ 大きいデータを作り出すデータ構成子はSuccだとします。 たとえば、0から4までの自然数をZeroとSuccを使ってあらわすと、 0 Zero 1 Succ Zero 2 Succ(Succ Zero) 3 Succ(Succ(Succ Zero)) 4 Succ(Succ(Succ(Succ Zero))) というようになります。

11.4___aとbが、練習問題11.3で定義されたnumberの データだとするとき、(a,b)という組に適用すると、aとbとを 加算した結果を返す、addNumberという関数(型は number * number -> number)を定義するfun宣言を書いてください。

実行例 - addNumber (Succ(Succ(Succ(Succ Zero))), Succ(Succ(Succ Zero))); > val it = Succ(Succ(Succ(Succ(Succ(Succ(Succ Zero)))))) : number

11.5___aとbが、練習問題11.3で定義されたnumberの データだとするとき、(a,b)という組に適用すると、aとbとを 乗算した結果を返す、multiplyNumberという関数(型は number * number -> number)を定義するfun宣言を、練習問題11.4で 定義したaddNumberを使って書いてください。

実行例 - multiplyNumber (Succ(Succ(Succ(Succ Zero))), Succ(Succ(Succ Zero))); > val it = Succ(Succ(Succ(Succ(Succ(Succ(Succ(Succ(Succ(Succ (Succ(Succ Zero))))))))))) : number

11.6___Q 11.4.5で定義された二分木に適用すると、それを構成する すべての節点から構成されるリストを返す関数(型は 'a tree -> 'a list)を定義するfun宣言を書いてください。

ただし、1個だけではなく、preorder、inorder、postorderという 3個の関数を定義してください。それらの関数は、どれもほとんど 同じ動作をするのですが、二分木の節点をどのような順序で たどっていくのかという点だけが異なります。それぞれの関数が 二分木の節点をたどっていく順序は、 preorder 根 --> 左部分木 --> 右部分木 inorder 左部分木 --> 根 --> 右部分木 postorder 左部分木 --> 右部分木 --> 根 だとします。 実行例 - preorder (TNode(58, TNode(81, TNode(99,TEmpty,TEmpty), TNode(76, TEmpty, TNode(34,TEmpty,TEmpty))), TNode(12,TEmpty,TEmpty))); > val it = [58, 81, 99, 76, 34, 12] : int list - inorder (TNode(58, TNode(81, TNode(99,TEmpty,TEmpty), TNode(76, TEmpty, TNode(34,TEmpty,TEmpty))), TNode(12,TEmpty,TEmpty))); > val it = [99, 81, 76, 34, 58, 12] : int list - postorder (TNode(58, TNode(81, TNode(99,TEmpty,TEmpty), TNode(76, TEmpty, TNode(34,TEmpty,TEmpty))), TNode(12,TEmpty,TEmpty))); > val it = [99, 34, 76, 81, 12, 58] : int list

11.7___TがQ 11.4.5で定義された二分木だとするとき、Tの 「鏡像」というのは、 ● TがTEmptyならば、Tの鏡像はTである。 ● TがTNode(x,left,right)という二分木ならば、Tの鏡像は、 TNode(x,rightの鏡像,leftの鏡像)という二分木である。 ● 以上の記述から導かれるもの以外はTの鏡像ではない。 というように定義される二分木のことだとします。たとえば、 TNode("Ishikawa", TNode("Kusanagi",TEmpty,TEmpty), TNode("Togusa", TNode("Aramaki", TNode("Yano",TEmpty,TEmpty), TNode("Miyazaki",TEmpty,TEmpty)), TEmpty)) という二分木の鏡像は、 TNode("Ishikawa", TNode("Togusa", TEmpty, TNode("Aramaki", TNode("Miyazaki",TEmpty,TEmpty), TNode("Yano",TEmpty,TEmpty)), TNode("Kusanagi",TEmpty,TEmpty)) という二分木です。 二分木に適用するとその鏡像を返す、mirrorという関数(型は 'a tree -> 'a tree)を定義するfun宣言を書いてください。

実行例 - mirror (TNode(56, TNode(39, TEmpty, TNode(14,TEmpty,TEmpty)), TNode(87,TEmpty,TEmpty))); val it = TNode(56, TNode(87,TEmpty, TEmpty), TNode(39, TNode(14, TEmpty, TEmpty), TEmpty)) : int tree

11.8___二分木を構成するそれぞれの階層を、上から順番に第1階層、 第2階層、第3階層、……、と呼ぶことにします。nが0またはプラスの 整数だとするとき、nに適用すると、第1階層の節点が1、第2階層の 節点が2と3、第3階層の節点が4と5と6と7、 というようになっている、n個の階層から構成される二分木を返す、 numberedTreeという関数(型はint -> int tree)を定義する fun宣言を書いてください。なお、nがマイナスだった場合は、 Minusという例外を発生させるようにしてください。

実行例 - numberedTree 3; > val it = TNode(1, TNode(2, TNode(4, TEmpty, TEmpty), TNode(5, TEmpty, TEmpty)), TNode(3, TNode(6, TEmpty, TEmpty), TNode(7, TEmpty, TEmpty))) : int tree

11.9___「整数演算木」と呼ばれるデータがあって、それらは、 ● nがintのデータだとするとき、TNode(TI n,TEmpty,TEmpty)という リストは整数演算木である。 ● fがoperatorのデータで、leftとrightが 整数演算木だとするとき、TNode(TO f,left,right)というリストは 整数演算木である。 ● 以上の記述から導かれるもの以外は整数演算木ではない。 というように定義されているとします(TNodeについてはQ 11.4.5、 operatorについては練習問題11.1、TOとTIについては練習問題11.2を 参照してください)。たとえば、 TNode(TI 68,TEmpty,TEmpty) TNode(TO Add, TNode(TI 27,TEmpty,TEmpty), TNode(TI 161,TEmpty,TEmpty)) TNode(TO Multiply, TNode(TO Subtract, TNode(TI 63,TEmpty,TEmpty), TNode(TI 48,TEmpty,TEmpty)), TNode(TI 25,TEmpty,TEmpty)) TNode(TO Divide, TNode(TI 805,TEmpty,TEmpty), TNode(TO Subtract, TNode(TO Add, TNode(TI 901,TEmpty,TEmpty), TNode(TI 184,TEmpty,TEmpty)), TNode(TO Multiply, TNode(TI 21,TEmpty,TEmpty), TNode(TI 50,TEmpty,TEmpty)))) というようなリストは整数演算木です。 そして、Tが整数演算木だとするとき、Tの「値」というのは、 ● TがTNode(TI n,TEmpty,TEmpty)という整数演算木ならば、 Tの値はnである。 ● TがTNode(TO f,left,right)という整数演算木ならば、leftの値と rightの値に対して、fがあらわしている演算を実行した結果が、Tの 値である。 というように定義されているとします。 整数演算木に適用するとその値を返す、evalという関数(型は token tree -> int)を定義するfun宣言を書いてください。ただし、 evalが、 TEmpty TNode(TI 35,TNode(TI 21,TEmpty,TEmpty),TEmpty) というような、正しくない整数演算木に適用された場合は、 IllegalTreeという例外が発生するようにしてください。

実行例 - eval (TNode(TO Multiply, TNode(TO Subtract, TNode(TI 63,TEmpty,TEmpty), TNode(TI 48,TEmpty,TEmpty)), TNode(TI 25,TEmpty,TEmpty))); > val it = 375 : int

11.10___リストを処理する関数を作るために組み込み関数の foldrを使うことができるのと同じように、Q 11.4.5で定義された 二分木を処理する関数を作るために使うことのできる、 foldTreeという関数(型は ('a * 'b * 'b -> 'b) -> 'b -> 'a tree -> 'b)を定義する カリー化形式のfun宣言を書いてください。

foldTreeの動作について、もう少しくわしく説明するために、 二分木を処理するfという関数(型は'a tree -> 'b)をfoldTreeを 使って作るためにはどうすればいいか、という話をしましょう。 foldTreeを使ってfを作りたいときは、まず、「二分木の根、 左部分木をfで処理した結果、右部分木をfで処理した結果、という 3個のデータから構成される組に適用すると、二分木全体をfで 処理した結果を返す関数(型は'a * 'b * 'b -> 'b)」を作って、 foldTreeをその関数に適用します。すると、foldTreeは1個の 関数(型は'b -> 'a tree -> 'b)を戻り値として返します。 次に、fを空の木に適用したとするとfはどんなデータを 返さないといけないかということを考えて、foldTreeが返した 関数をそのデータに適用します。すると、foldTreeが返した 関数は、戻り値としてfを返します。 実行例 - foldTree (fn (x,y,z) => x :: y @ z) [] (TNode("sitar", TNode("banjo", TNode("shamisen",TEmpty,TEmpty), TEmpty), TNode("koto", TEmpty, TNode("kalimba",TEmpty,TEmpty)))); > val it = ["sitar", "banjo", "shamisen", "koto", "kalimba"] : string list

11.11___fが'a -> 'bという型の関数だとするとき、fに適用すると、 「tが'a treeという型の二分木だとするとき、tに適用すると、tを 構成するすべての節点に対してfを適用することによって得られた データから構成される、もとの二分木と同じ構造を持つ二分木を返す 関数(型は'a tree -> 'b tree)」を返す、mapTreeという 関数(型は('a -> 'b) -> 'a tree -> 'b tree)を定義する カリー化形式のfun宣言を、練習問題11.10で定義したfoldTreeを 使って書いてください。

実行例 - mapTree (fn n => n mod 2 = 0) (TNode(58, TNode(31, TNode(29,TEmpty,TEmpty), TEmpty), TNode(85, TNode(40,TEmpty,TEmpty), TNode(77,TEmpty,TEmpty)))); > val it = TNode(true, TNode(false, TNode(false, TEmpty, TEmpty), TEmpty), TNode(false, TNode(true, TEmpty, TEmpty), TNode(false, TEmpty, TEmpty))) : bool tree

11.12___「二分偶数木」と呼ばれるデータと「二分奇数木」 と呼ばれるデータがあって、それらは、 ● 空の木は二分偶数木である。 ● 1個のデータと2個の二分奇数木から構成されるデータは 二分偶数木である。 ● 以上の記述から導かれるもの以外は二分偶数木ではない。 ● 1個のデータと2個の二分偶数木から構成されるデータは 二分奇数木である。 ● 以上の記述から導かれるもの以外は二分奇数木ではない。 というように相互再帰的に定義されているとします。 二分偶数木の型を作り出すevenTreeという型構成子と、二分奇数木の 型を作り出すoddTreeという型構成子を定義するdatatype宣言を 書いてください。

なお、空の二分偶数木を作り出すデータ構成子は ETEmpty、空ではない二分偶数木を作り出すデータ構成子はETNode、 二分奇数木を作り出すデータ構成子はOTNodeだとします。 二分偶数木の例 ETEmpty ETNode("Joss", OTNode("Hadden",ETEmpty,ETEmpty), OTNode("Constantine",ETEmpty,ETEmpty)) ETNode(37, OTNode(55, ETNode(18, OTNode(95,ETEmpty,ETEmpty), OTNode(34,ETEmpty,ETEmpty)), ETEmpty), OTNode(23,ETEmpty,ETEmpty)) 二分奇数木の例 OTNode("Arroway",ETEmpty,ETEmpty); OTNode(41, ETEmpty, ETNode(63, OTNode(77, ETNode(94, OTNode(22,ETEmpty,ETEmpty), OTNode(14,ETEmpty,ETEmpty)), ETEmpty), OTNode(81,ETEmpty,ETEmpty)));

第12章===モジュール

12.1---ストラクチャー

Q 12.1.1___モジュールって何ですか。

モジュールというのは、プログラムを構成するための 部品のことです。 大規模なプログラムというのは、それを扱うことが人間にとって きわめて困難になるほどの複雑さを持つことがあります。 そのような複雑な構造を持つプログラムを、人間にとって容易に 扱うことができるようにする方法のひとつが、それを モジュールに分割するという方法です。 プログラムを上手にモジュールに分割することによって、 そのプログラムは、いくつかのモジュールの単純な組み合わせとして 理解することができるようになります。モジュールの内部にいかに 複雑な構造が隠されているとしても、そのモジュールを使ってさらに 大きな構造を作るときには、その内部の構造については何も 考えなくてもかまわないのです。

Q 12.1.2___ストラクチャーって何ですか。

ストラクチャーというのは、MLの機能を使うことによって 作ることのできる、モジュールに相当するもののことです。 プログラミング言語の多くは、モジュールに相当するものを 作るための機能を持っています。ただし、モジュールに 相当するもののことを何と呼ぶかというのは、 それぞれのプログラミング言語によって異なっています。 MLでは、モジュールに相当するもののことを 「ストラクチャー」と呼びます。 ストラクチャーというのは、データや例外構成子や型構成子や データ構成子などの定義をその中に閉じ込めておくことができる 箱のようなものだと考えることができます。

Q 12.1.3___ストラクチャー式って何ですか。

ストラクチャー式というのは、ストラクチャーについて 記述するための構文のことです。 ストラクチャー式の書き方は何種類かあるのですが、 もっとも基本的なのは、 struct 宣言 宣言 …… end という書き方です。structとendとのあいだには、val宣言、 fun宣言、exception宣言、datatype宣言などを、何個でも好きなだけ 書くことができます。この形のストラクチャー式は、structとendの あいだに書かれた宣言があらわしている定義から構成される ストラクチャーをあらわしています。たとえば、 struct val hyaku = 100 fun hyakubai n = n*hyaku end というストラクチャー式は、hyakuという識別子を100という整数に 束縛するという定義と、hyakubaiという識別子を整数を100倍する 関数に束縛するという定義が閉じ込められているストラクチャーを あらわしています。

Q 12.1.4___structure宣言って何ですか。

structure宣言というのは、識別子をストラクチャーに 束縛するという動作をあらわす宣言のことです。 structure宣言は、 structure 識別子 = ストラクチャー式 と書きます。structure宣言をコンピュータに 実行させると、コンピュータは、イコールの左側に書かれた 識別子を、イコールの右側に書かれたストラクチャー式によって あらわされるストラクチャーに束縛します。たとえば、 structure Suika = struct val hyaku = 100 fun hyakubai n = n*hyaku end というstructure宣言をコンピュータに実行させたとすると、 コンピュータは、Suikaという識別子を、ストラクチャー式によって あらわされるストラクチャーに束縛します(注)。 (注) 例外構成子やデータ構成子と同じように、ストラクチャーに束縛する 識別子も、慣習的に先頭の文字を大文字にします。 なお、structure宣言によってストラクチャーに束縛された 識別子は、ストラクチャー式として使うことができます。

Q 12.1.5___structure宣言の中にstructure宣言を書くことは 可能ですか。

はい、可能です。 structure宣言は、何重にでも入れ子にすることができます。 たとえば、 structure Momo = struct structure Kuri = struct val comma = "," end fun commacat (a,b) = a ^ Kuri.comma ^ b end というようなstructure宣言を書くことが可能です。この中にある commaの定義はKuriによって閉じ込められていて、Kuriはさらに Momoによって閉じ込められています。

12.2---限定識別子

Q 12.2.1___ストラクチャーの中に閉じ込められている定義を そのストラクチャーの外側で利用したいときは どうすればいいのですか。

ストラクチャーの中に閉じ込められている定義を そのストラクチャーの外側で利用したいときは、 「限定識別子」と呼ばれるものを使います。 ストラクチャーの中では、同じストラクチャーの中にある定義を、 1個の単純な識別子を使うことによって利用することができます。 たとえば、 structure Suika = struct val hyaku = 100 fun hyakubai n = n*hyaku end というストラクチャーの中では、hyakuという識別子が、同じ ストラクチャーの中にある定義を利用するために使われています。 でも、ストラクチャーの中に閉じ込められている定義を そのストラクチャーの外側で利用することは、1個の単純な識別子を 書くだけではできません。そのためには「限定識別子」というものを 書く必要があるのです。限定識別子というのは、 ストラクチャー名 . 識別子 というように、ストラクチャーの名前と1個のドット(.)と識別子を 並べて書いたもののことです。ストラクチャーの中に 閉じ込められている定義をそのストラクチャーの外側で 利用するためには、そのストラクチャーの名前と、 そのストラクチャーの中で何かに束縛された識別子から構成される 限定識別子を書く必要があります。 たとえば、 structure Mikan = struct val namae = "Tokokuni Erika" fun kakko s = "[" ^ s ^ "]" end というstructure宣言によって作られたMikanというストラクチャーの 中に閉じ込められている定義をそのストラクチャーの外側で 利用したいというときは、 Mikan.namae Mikan.kakko という限定識別子を書けばいいわけです。これらの限定識別子を 使うことによって、 - Mikan.namae; > val it = "Tokokuni Erika" : string - Mikan.kakko "Madelin Gins"; > val it = "[Madelin Gins]" : string - Mikan.kakko Mikan.namae; > val it = "[Tokokuni Erika]" : string というように、Mikanというストラクチャーの中に 閉じ込められている定義を、そのストラクチャーの外側で 利用することができます。

Q 12.2.2___ストラクチャーの中に閉じ込められている定義を、 それとは別のストラクチャーの中で利用する、ということは 可能ですか。

はい、可能です。 ストラクチャーの中に閉じ込められている定義を、それとは別の ストラクチャーの中で利用するということも、限定識別子を 使えば可能です。たとえば、Ringoというストラクチャーの中の 関数を定義するために、Q 12.2.1で定義を書いたMikanという ストラクチャーの中に閉じ込められているkakkoという関数の 定義を利用したい、という場合は、 structure Ringo = struct fun kakkoRenketsu (a,b) = Mikan.kakko a ^ Mikan.kakko b end というように、Mikan.kakkoという限定識別子を 書けばいいわけです。

Q 12.2.3___二つ以上のストラクチャーのそれぞれで、同じ識別子を 違うものに束縛してもかまわないのですか。

はい、かまいません。 もしも、二つ以上のストラクチャーのそれぞれで、同じ識別子が 違うものに束縛されていたとしても、問題は何も生じません。 たとえば、 structure Biwa = struct val namae = "David Lodge" end structure Kaki = struct val namae = "Yura Kimiyoshi" end というように、BiwaとKakiという二つのストラクチャーの それぞれで、namaeという識別子を異なる文字列に 束縛したとしても、困ったことは何も発生しません。Biwaの中で 定義されたnamaeを利用したいときは、 Biwa.namae という限定識別子を書けばいいわけですし、同じように、 Kaki.namae という限定識別子を書くことによって、Kakiの中で定義された namaeを利用することができます。 ストラクチャーの中に定義を閉じ込めることによって得られる効果の ひとつは、識別子の重複を心配しなくてすむようになる ということです。大きなプログラムをストラクチャーを使わずに 書くというのは、ひとつの識別子をいくつかの異なるものに 束縛してしまわないように気を配らないといけませんので、 けっこう大変です。でも、プログラムをいくつかのストラクチャーに 分割している場合は、ひとつのストラクチャーの中で識別子が 重複しないように気を付けていれば、それで充分なのです。 二つ以上のストラクチャーのそれぞれで、同一の識別子が 異なるものに束縛されていたとしても、困ったことは何も 発生しません。

Q 12.2.4___入れ子になっているストラクチャーの中心部にある 定義を利用したいときはどうすればいいのですか。

そんなときは、入れ子になっているすべてのストラクチャーの名前を 含む限定識別子を書きます。 限定識別子というのは、一般的には、 ストラクチャー名 . ストラクチャー名 . …… . 識別子 という構文を持っています。ストラクチャーが何重にも 入れ子になっていて、その奥の奥のそのまた奥に幽閉されている 定義を利用したいときは、それらのストラクチャーの名前を、 外側にあるものから内側にあるものへという順番で左から右へ ドット(.)で区切りながら並べていって、そしてその右端に、 幽閉されているものに束縛されている識別子を書きます。 たとえば、 structure Momo = struct structure Kuri = struct val comma = "," end fun commacat (a,b) = a ^ Kuri.comma ^ b end というように定義されたストラクチャーがあるとしましょう。 この中にあるcommaの定義は、MomoとKuriによって二重に 閉じ込められているわけですが、でも、 Momo.Kuri.comma という限定識別子を書けば、commaの定義をMomoの外側で 利用することができます。

12.3---open宣言

Q 12.3.1___ひとつのストラクチャーの中に閉じ込められている 定義を何回も使う必要がある場合、そのストラクチャーの名前を含む 限定識別子を何回も書くというのは、冴えたやり方ではないと 思われます。ほかに、もっとエレガントな方法はないのですか。

あります。open宣言というものを使えば、限定識別子ではない普通の 識別子を書くことによって、ストラクチャーの中のものを 使うことができるようになります。 open宣言というのは、宣言の一種で、 open ストラクチャー名 と書きます。open宣言をコンピュータに実行させると、 コンピュータは、その宣言の中で指定されたストラクチャーの扉を 開放して、その中に閉じ込められていた定義を、限定識別子ではない 普通の識別子で利用することができるようにします。ただし、 open宣言によるストラクチャーの扉の開放が有効なのは、 そのopen宣言が含まれているスコープの中だけです。 具体的な例を使って説明しましょう。今、 structure Enclose = struct fun paren s = "(" ^ s ^ ")" fun brace s = "{" ^ s ^ "}" fun bracket s = "[" ^ s ^ "]" end というstructure宣言で定義されたEncloseというストラクチャーが あるとします。このストラクチャーの中に閉じ込められた定義を 使って、 - testEnclose "Qfwfq"; > val it = ("(Qfwfq)", "{Qfwfq}", "[Qfwfq]") : string * string * string というような動作をする、testEncloseという関数を 定義したいとしましょう。この関数を定義するためのfun宣言は、 fun testEnclose s = (Enclose.paren s, Enclose.brace s, Enclose.bracket s) というように限定識別子を使って書くこともできますが、 Encloseというストラクチャー名が3回も繰り返されているので、 ちょっと見苦しい感じがします。こんなときは、open宣言を使って Encloseを開放するというのが冴えたやり方です。open宣言を 使うと、testEncloseを定義するfun宣言は、 fun testEnclose s = let open Enclose in (paren s, brace s, bracket s) end というように書くことができます。

Q 12.3.2___二つのstructure宣言があって、一方のstructure宣言の 中で、他方のstructure宣言の中で定義されているものを 利用したい、という場合もopen宣言を使うことができるのですか。

はい、そのとおりです。 open宣言は、fun宣言の中だけではなくて、structure宣言の中に 書くことも可能です。ただし、structure宣言の中にopen宣言を書く 場合には、その効力が波及する範囲に関して充分に注意を払う 必要があります。たとえば、 structure Double = struct open Enclose fun doubleParen s = paren (s ^ s) fun doubleBrace s = brace (s ^ s) fun doubleBracket s = bracket (s ^ s) end というstructure宣言を書くことは可能ですが、これだと、 open宣言でDoubleを開放すると、それと同時にEncloseも 開放することになってしまいます。つまり、Doubleだけを開放して Encloseは開放しない、ということができなくなるのです。 structure宣言の中にopen宣言を書く場合には、普通、 local宣言という宣言を書くことによって、open宣言の効力が structure宣言の外へ波及しないようにします。 local宣言は、 local 宣言 …… 宣言 in 宣言 …… 宣言 end という構文を持つ宣言です。local宣言の機能は、効力を外へ 波及させる宣言と波及させない宣言とを明確に分離する ということです。local宣言のinとendのあいだに書かれた宣言の 効力は外へ波及しますが、localとinのあいだに書かれた宣言の 効力はlocal宣言の中だけに留まります。 ですから、上に書いた、Doubleというストラクチャーを定義する structure宣言は、local宣言を使って、 structure Double = struct local open Enclose in fun doubleParen s = paren (s ^ s) fun doubleBrace s = brace (s ^ s) fun doubleBracket s = bracket (s ^ s) end end と書き直すことによって、Doubleを開放したときに自動的に Encloseも開放されてしまうという問題点を 解消することができます。

Q 12.3.3___1個のopen宣言で、2個以上のストラクチャーを 開放することは可能ですか。

はい、可能です。 open宣言の中には、 open ストラクチャー名 …… ストラクチャー名 というように、ストラクチャー名を空白で区切って何個でも 書くことができます。ですから、 open Ninjin Kabocha Tamanegi というopen宣言を書くことによって、Ninjin、Kabocha、 Tamanegiという3個のストラクチャーを開放することができます。

12.4---ライブラリー

Q 12.4.1___ライブラリーって何ですか。

ライブラリーというのは、多くのプログラムで 利用することができるような、一般性のある機能を持つモジュールの 集合のことです。 どんなプログラミング言語でも、たいていの場合、その処理系には 「ライブラリー」と呼ばれるものが付属しています。ライブラリーの 中にはいくつかのモジュールが含まれていて、それらのモジュールの 中には、さまざまなものの定義が含まれています。ライブラリーの 中にある定義は、プログラムを書くときに、自分で書いた定義と 同じように自由に使うことができます。ですから、ほしいと思う 定義がすでにライブラリーの中にある場合は、それを自分で書く 必要はない、ということになります。

Q 12.4.2___標準ライブラリーって何のことですか。

標準ライブラリーというのは、プログラミング言語ごとに 定められている、処理系から独立したライブラリーの 規格のことです。 基本的には、処理系に付属しているライブラリーというのは、 その処理系に固有のものですから、対象とする言語が 同じであっても、異なる処理系は異なるライブラリーを 持つ、ということになります。 しかし、ひとつの処理系を使って開発されたプログラムを、それとは 異なる別の処理系を使って実行する場合に、そのプログラムに対して 加えなければならない修正は、少ないに越したことはありません。 ですから、処理系と処理系とのあいだにあるライブラリーの相違は、 できれば小さいほうがいい、ということになります。 そこで登場するのが、標準となるライブラリーの規格をあらかじめ 決めておいて、同じ言語を対象とする処理系はすべて、自分の ライブラリーの中に標準のライブラリーを部分集合として 持つようにしよう、という考え方です。それが実現すれば、 標準ライブラリーの規格にしたがっている定義のみを使って書かれた プログラムは、同じ言語を対象とするどんな処理系を使っても 実行することができる、ということになります。

Q 12.4.3___MLにも標準ライブラリーがあるのですか。

はい、あります。 MLという言語にはいくつかの方言があるのですが、それらのうちで もっともよく使われている方言であるStandard MLでは、 「Standard ML基本ライブラリー」と呼ばれる標準ライブラリーの 規格が定められています。SML/NJやMoscow MLなどの、Standard MLで 書かれたプログラムを処理することのできる処理系には、 その規格にもとづいたライブラリーが付属しています。 Standard ML基本ライブラリーについてもっとくわしく知りたい 読者は、 http://www.cs.bell-labs.com/~jhr/sml/basis/pages/ sml-std-basis.html を参照するといいでしょう。

Q 12.4.4___トップレベル環境って何ですか。

トップレベル環境というのは、いかなる宣言も実行されていない 時点で、すでに識別子が束縛されているものの集合のことです。 MLの対話型システムが起動した直後に、 - 5+3; と入力すると、5と3との加算が実行されます。つまり、その時点で すでに、+という識別子は加算を実行する関数に 束縛されているわけです。このように、処理系が起動した直後の 時点で、すでに、いくつかの識別子は何らかのものに 束縛されています。そのような、処理系の初期状態で、すでに 識別子が束縛されているものの集合のことを、 「トップレベル環境」と呼びます。 たとえば、MLでは、 関数 +、-、*、div、>、>=、not、size、rev、map、foldrなど データ構成子 true、falseなど 型構成子 int、real、char、string、bool、unit、exn、listなど 例外構成子 Div、Chr、Emptyなど というようなものが、トップレベル環境に属しています。 ですから、「組み込み関数」という言葉は、「トップレベル環境に 属しているもののうちで関数であるもの」という意味だと 考えることができます。 MLのトップレベル環境に属しているものの大多数は、実は、 Standard ML基本ライブラリーの中にあるストラクチャーの中で 定義されているものです。たとえば、^、size、explode、 implodeなどの関数はStringというストラクチャーの中で 定義されていて、rev、@、map、foldrなどの関数はListという ストラクチャーの中で定義されています。 ちなみに、Standard ML基本ライブラリーという規格の中には、 トップレベル環境にはどのようなものが属していないといけないか、 ということも定められています。

Q 12.4.5___MLで、ライブラリーには含まれているけれども トップレベル環境には属していないものを利用したいのですが、 そんなときはどうすればいいのですか。

ライブラリーには含まれているけれどもトップレベル環境には 属していないものを利用したいときは、open宣言を実行するか、 または限定識別子を使います。 MLの処理系に付属しているライブラリーは、いくつかの ストラクチャーから構成されていて、それらのストラクチャーの 中にはさまざまなものの定義が含まれています。 それらの定義のうちでトップレベル環境に属しているのは ほんの少数で、大多数の定義はストラクチャーの中に 閉じ込められた状態になっています。 ライブラリーの中にあるストラクチャーの中に閉じ込められている 定義を利用する方法は、自分で書いたストラクチャーの中にある 定義を利用する方法と、まったく同じです。つまり、open宣言を 使ってストラクチャーを開放した上で単純な識別子を書くか、 または限定識別子を書けばいい、ということです。 たとえば、Standard ML基本ライブラリーの中にはStringという ストラクチャーがあって、その中にはさまざまな関数の定義が 含まれています。しかし、それらの関数のうちでトップレベル環境に 属しているのは、^、size、explode、implodeなどのほんの一部分の 関数だけです。Stringには、たとえば、文字列から文字を取り出す subという関数の定義が含まれているのですが、subは トップレベル環境には属していません。 subは、string * int -> charという型を持つ関数です。sが 文字列で、nが0またはプラスの整数だとするとき、(s,n)という 組にsubを適用すると、subは、sのn番目の文字を取り出して、 その文字を戻り値として返します(先頭の文字を0番目と 数えます)。 subは、トップレベル環境には属していませんが、Stringという ストラクチャーに定義が含まれていますので、 - let open String in sub ("synecdoche",2) end; > val it = #"n" : char というようにopen宣言を実行するか、または、 - String.sub ("metonymia",5); > val it = #"y" : char というように限定識別子を使うことによって、 利用することができます。

---練習問題

12.1___文字の列で数値を表現する方法のひとつとして、 ローマ数字というものがあります。ローマ数字は、 I、V、X、L、C、D、Mという7種類の文字を使うことによって、 1から3999までの整数を表現することができます。 ローマ数字で使われるそれぞれの文字は、次のような意味を 持っています。 1 5 1の位 I V 10の位 X L 100の位 C D 1000の位 M まず、それぞれの位の1と2と3は、1をあらわす文字を 並べることによって表現します。たとえば、10はX、200はCC、 3000はMMMと書きます。 それぞれの位の4は、1をあらわす文字の右側に5をあらわす文字を 書くことによって表現します。ですから、40はXL、400はCDと 書くことになります。 それぞれの位の5は、その数値をあらわす1個の文字で 表現されます。つまり、5はV、50はL、500はDです。 それぞれの位の6と7と8は、5をあらわす文字の右側に1をあらわす 文字を並べることによって表現します。たとえば、6はVI、70はLXX、 800はDCCCと書きます。 それぞれの位の9は、1をあらわす文字の右側に、ひとつ上の位の 1をあらわす文字を書くことによって表現します。ですから、 90はXC、900はCMと書くことになります。 そして、それぞれの位の数値をあらわす文字列を、 1000の位 100の位 10の位 1の位 という順序で並べると、ひとつのローマ数字が完成します。 たとえば、1356はMCCCLVI、2908はMMCMVIII、3047はMMMXLVIIと 書くことができます。 さて、それでは問題です。ローマ数字を扱う関数から構成される、 Romanというストラクチャーを定義するstructure宣言を 書いてください。

なお、Romanには、最低限、intToRoman、 romanToIntという二つの関数が含まれているようにしてください。 intToRomanとromanToIntは、次のような動作をする関数です。 intToRoman : int -> string intのデータに適用すると、それと同じ整数をあらわしている ローマ数字を文字列で作って、その結果を返します。 実行例 - Roman.intToRoman 74; > val it = "LXXIV" : string なお、intToRomanは、自分が受け取った引数が0または マイナスだった場合はNotPlusという例外を発生させ、 4000以上だった場合はTooLargeという例外を発生させるように 定義してください。 romanToInt : string -> int 文字列に適用すると、それをローマ数字だとみなして、それと同じ 整数をあらわしているintのデータを返します。 実行例 - Roman.romanToInt "CMIII"; > val it = 903 : int なお、romanToIntは、"A"、"IIII"、"VV"、"IC"、 "VX"というような、正しくないローマ数字を引数として受け取った 場合は、InvalidRomanという例外を発生させるように 定義してください。 intToRomanとromanToIntを定義するための補助的な関数は、 fun宣言の中のfun宣言で定義するのではなく、独立したfun宣言で 定義するようにしてください。つまり、structure宣言の中に すべてのfun宣言が同格で並ぶようにしてほしい、ということです。

12.2___文字が演算子であるかどうかを判断するisOperatorという 関数、式を分解するexpToTokensという関数、および、 expToTokensを定義するための補助的な関数から構成される、 Tokenというストラクチャーを定義するstructure宣言を 書いてください。

式やプログラムを構成する、識別子や定数やコンマや括弧などの 部品のことを「トークン」と言います。expToTokensは、引数として 受け取った文字列を式だとみなして、それをトークンに分解して、 その結果を文字列のリストで返します(型は string -> string list)。ただし、expToTokensが 分解することのできる式というのは、演算子と、演算子によって 区切られた文字列から構成される文字列で、演算子というのは、 +、-、*、/という4種類の文字のことだとします。 isOperatorは、文字に適用すると、その文字が、+、-、*、/の いずれかであるならば真、そうでなければ偽を返します(型は char -> bool)。 実行例 - Token.expToTokens "group*ring-*field/"; > val it = ["group", "*", "ring", "-", "*", "field", "/"] : string list なお、expToTokensは、引数として受け取った文字列の中に空白が 含まれていたとしても、結果として返すトークンの中には その空白を入れないように定義してください。つまり、 - Token.expToTokens "meno presto + mezza voce"; > val it = ["menopresto", "+", "mezzavoce"] : string list というような動作をするようにしてほしいわけです。

12.3___練習問題12.1と12.2で定義したストラクチャーを使って、 ローマ数字と演算子から構成される式を評価するevalという 関数と、それを定義するための補助的な関数から構成される、 Evalというストラクチャーを定義するstructure宣言を 書いてください。

evalは、 ローマ数字 演算子 ローマ数字 演算子 …… ローマ数字 という構文を持つ式を文字列として受け取って、それを評価して、 その結果をあらわすローマ数字を返します(型は string -> string)。演算子は、+(加算)、-(減算)、 *(乗算)、/(整数の除算)という4種類で、優先順位はすべて 同一で、結合規則は左結合だとします。 たとえば、 "V+IV/II*III" という文字列にevalを適用したとすると、evalは、まずVとIVとを 加算して、その結果をIIで除算して、その結果とIIIとを 乗算して、その結果(XII)をローマ数字で返します。 実行例 - Eval.eval "VIII-VI*III+IV*VII"; > val it = "LXX" : string なお、evalは、"+VI"、"IX*"、"II/-VII"というような、 正しくない式を引数として受け取った場合は、 InvalidExpressionという例外を発生させるように 定義してください。

第13章===情報隠蔽

13.1---シグネチャー

Q 13.1.1___シグネチャーって何ですか。

シグネチャーというのは、外部から見たストラクチャーの 仕様のことです。 すべてのストラクチャーは、かならず自分の シグネチャーというものを持っています。シグネチャーは、 ストラクチャーを構成するさまざまな要素の仕様から 構成されています。たとえば、 structure Suika = struct val hyaku = 100 fun hyakubai n = n*hyaku end というストラクチャーは、 ● hyakuという識別子があって、それはintという型を持つデータに 束縛されている。 ● hyakubaiという識別子があって、それはint -> intという型を 持つデータに束縛されている。 という仕様から構成されるシグネチャーを持っています。

Q 13.1.2___仕様記述って何ですか。

仕様記述というのは、ストラクチャーを構成する要素の仕様について 記述するための構文のことです。 データの定義に対応する仕様記述は、 val 識別子 : 型式 と書きます。たとえば、 val gohyaku = 500 というval宣言があらわしているデータの定義に対応する 仕様記述は、 val gohyaku : int と書くことになります。 関数というのはデータの一種ですから、関数の仕様記述の書き方は、 普通のデータの場合と同じです。つまり、関数の仕様記述も、 val 識別子 : 型式 と書けばいいということです。たとえば、 fun even n = n mod 2 = 0 というfun宣言であらわされるevenという関数の定義に対応する 仕様記述は、 val even : int -> bool と書けばいいわけです。

Q 13.1.3___シグネチャー式って何ですか。

シグネチャー式というのは、シグネチャーを記述するための 構文のことです。 シグネチャー式は、 sig 仕様記述 仕様記述 …… end というように書きます。つまり、ストラクチャーのシグネチャーを シグネチャー式で記述したいときは、そのストラクチャーを構成する 定義に対応する仕様記述を列挙して(どんな順番でも かまいません)、その全体をsigとendで囲めばいいわけです。 たとえば、 structure Suika = struct val hyaku = 100 fun hyakubai n = n*hyaku end というストラクチャーのシグネチャーをあらわすシグネチャー式は、 sig val hyaku : int val hyakubai : int -> int end と書くことができます。

13.2---シグネチャーによる情報隠蔽

Q 13.2.1___情報隠蔽っていうのはどういうことなんですか。

情報隠蔽というのは、モジュールの内部をすべて 公開するのではなくて、その一部分を非公開にすることです。 モジュールというものについて考える場合には、二つの異なる立場を 設定することが必要です。ひとつはモジュールを外側から利用する 場合の立場で、もうひとつはモジュールの内部を作る場合の 立場です。 モジュールを構成する要素には、そのモジュールを利用する 立場にとって、利用できなければ困るものとそうでないものとが あります。利用できなくても困らないものというのは、利用できる 必要があるものを成立させるための補助的な要素です。 モジュールを作る立場にとっては、その外側に影響を 及ぼしてしまうのではないかと心配することなしに、好きなように 内部を改造することができる、というのが理想的なモジュールです。 そのような理想的なモジュールを作るために必要なことは、 モジュールを構成する要素のうち、外側から利用できなければ 困るもののだけを公開して、それ以外の補助的な要素を 秘匿してしまうことです。なぜなら、外側から利用されている要素が 一部分に限定されているならば、それらの要素が変更されないように 注意してさえいれば、モジュールの内部をいくら改造したとしても、 モジュールの外側には影響を及ぼさないからです。

Q 13.2.2___ストラクチャーの内部にある定義を非公開にする ということは可能ですか。

はい、可能です。 Q 13.1.1で説明したように、すべてのストラクチャーは、自分の シグネチャーを持っています。ストラクチャーが持つ シグネチャーは、そのストラクチャーの中にある定義に対応する 仕様から構成されるわけですが、すべての定義ではなく、一部分の 定義に対応する仕様から構成されるシグネチャーを持つ ストラクチャーを作る、ということも可能です。つまり、 ストラクチャーの中に定義があるにもかかわらず、その定義に 対応する仕様がシグネチャーの中にはない、 というストラクチャーを作ることもできるのです。 ストラクチャーの中にある定義のうちで、対応する仕様が シグネチャーの中に含まれていないものは、ストラクチャーの 外側からは見ることができない定義、つまり非公開の 定義になります。 ですから、ストラクチャーの内部にある定義を非公開にしたい 場合は、そのストラクチャーに対して、非公開にしたい定義に 対応する仕様を含まないシグネチャーを強制的に与えればいい、 ということになります。

Q 13.2.3___ストラクチャーに対してシグネチャーを強制的に 与えたいときはどうすればいいのですか。

ストラクチャーに対してシグネチャーを強制的に与えたいときは、 ストラクチャー式の右側にコロン(:)を書いて、そのさらに右側に、 ストラクチャーに与えたいシグネチャーをあらわすシグネチャー式を 書きます。 ストラクチャー式の構文にはいくつかの種類があるのですが、 そのうちのひとつとして、 ストラクチャー式 : シグネチャー式 という書き方があります。この形のストラクチャー式によって あらわされるストラクチャーは、コロンの右側に書かれた シグネチャー式によってあらわされるシグネチャーを 持つことになります。つまり、コロンの左側があらわしている ストラクチャーの中に含まれているすべての定義が 公開されるのではなく、コロンの右側があらわしている シグネチャーの中に仕様が含まれている定義だけが 公開されるのです。 それでは、具体的な例を使って説明しましょう。たとえば、 structure Himawari = struct val juuhachi = 18 fun juuhachibai n = n*juuhachi end というstructure宣言によって定義されるHimawariという ストラクチャーは、その中にあるjuuhachiの定義もjuuhachibaiの 定義も公開することになります。もしも、そうではなくて、 juuhachiの定義を非公開にして、juuhachibaiの定義だけを 公開したいという場合は、どうすればいいでしょうか。つまり、 - Himawari.juuhachibai 7; > val it = 126 : int というように、juuhachibaiの定義は利用できるけれども、 - Himawari.juuhachi; と入力した場合はエラーになるようにしたい、ということです。 そんな場合は、juuhachiの仕様を含まずに、juuhachibaiの 仕様だけを含んでいるシグネチャーを作って、それを ストラクチャーに与えます。つまり、 structure Himawari = struct val juuhachi = 18 fun juuhachibai n = n*juuhachi end : sig val juuhachibai : int -> int end というstructure宣言を書けばいいわけです。

Q 13.2.4___識別子をシグネチャーに束縛するということは 可能ですか。

はい、可能です。signature宣言というものを書くことによって、 識別子をシグネチャーに束縛することができます。 signature宣言というのは、宣言の一種で、 signature 識別子 = シグネチャー式 と書きます。この宣言が実行されると、イコールの左側に書かれた 識別子が、イコールの右側に書かれたシグネチャー式によって あらわされるシグネチャーに束縛されます。たとえば、 signature SUIKA = sig val hyakubai : int -> int end というsignature宣言を書くことによって、SUIKAという識別子が、 シグネチャー式によってあらわされるシグネチャーに 束縛されます(注)。 (注) MLの慣習では、シグネチャーに束縛する識別子は大文字のみを使って 作る、ということになっています。 signature宣言によってシグネチャーに束縛された識別子は、 シグネチャー式として使うことができます。たとえば、SUIKAという 識別子がシグネチャーに束縛されているとすると、 structure Suika = struct val hyaku = 100 fun hyakubai n = n*hyaku end : SUIKA というように、コロンの右側にSUIKAという識別子を 書くことによって、その識別子があらわしているシグネチャーを、 ストラクチャーに対して与えることができます。

Q 13.2.5___例外の仕様記述はどのように書けばいいのですか。

例外の仕様記述の書き方は、exception宣言の書き方と同じです。 たとえば、 exception NotFound という仕様記述を書くことによって、NotFoundというのが 例外構成子であるという仕様を記述することができます。

Q 13.2.6___型の仕様記述はどのように書けばいいのですか。

型の仕様記述の書き方は、datatype宣言の書き方と同じです。 たとえば、二分木の型の仕様記述は、 datatype 'a Tree = TEmpty | TNode of 'a * 'a tree * 'a tree というように、それを定義するdatatype宣言と同じものを 書けばいいわけです。

13.3---抽象データ型

Q 13.3.1___抽象データ型って何ですか。

抽象データ型というのは、内部の構造を非公開にして、操作の集合を 公開している型のことです(注)。 (注) この説明は、どちらかと言うと実践的な立場から抽象データ型を見た 場合のものです。もう少し理論的な文脈の中だと、 「抽象データ型というのは、操作の集合によって定義されるデータの 集合のことである」というように説明されるのが普通です。 それでは、スタックというものを例にして、抽象データ型について 説明することにしましょう。 スタックというのは、データの追加と削除が一方の端に 限定されているデータの列のことです。たとえば、左端に対してのみ データの追加と削除ができる、 53 21 64 73 19 というスタックがあるとするとき、このスタックに88を追加して、 88 53 21 64 73 19 というスタックを作ったり、左端を削除して、 21 64 73 19 というスタックを作ったりすることは可能ですが、左端以外の位置に データを追加して、 53 21 64 31 73 19 というスタックを作ったり、左端以外の位置にあるデータを 削除して、 53 21 64 19 というスタックを作ったりするということはできません。 なお、スタックにデータを追加することを「プッシュする」と言い、 スタックからデータを削除することを「ポップする」と言います。 そして、最後にプッシュしたデータのことを「トップ」と呼びます。 スタックのように、許されている操作が限定されている型を定義する ためには、データの内部の構造が、その型を利用する立場からは 見えないようにする必要があります。なぜなら、データの内部の 構造を公開すると、その型を利用するときに、許されていない操作を 実行することができてしてしまうからです。ですから、許されている 操作が限定されている型を定義するためには、それを 抽象データ型にする必要がある、ということになります。 抽象データ型としてのスタックは、 ● 空のスタックを作る。 ● スタックにデータをプッシュする。 ● スタックからデータをポップする。 ● トップがどんなデータであるかということを調べる。 ● スタックが空かどうかを調べる。 という5種類の操作のみを公開して、データの内部の構造を 非公開にすることによって定義することができます。

Q 13.3.2___MLで抽象データ型を定義するためには どうすればいいのですか。

MLで抽象データ型を定義したいときは、型の定義と、その型の データを操作する関数の定義をストラクチャーに閉じ込めて、 シグネチャーを使うことによって、データ構成子の定義を 非公開にします。 それでは、例として、スタックを抽象データ型として 定義するためのstructure宣言とsignature宣言の書き方について 説明することにしましょう。まずstructure宣言は、 structure Stack = struct datatype 'a stack = Empty | Node of 'a * 'a stack exception EmptyStack val create = Empty fun push (x,xs) = Node (x,xs) fun pop Empty = raise EmptyStack | pop (Node (_,xs)) = xs fun top Empty = raise EmptyStack | top (Node (x,_)) = x fun isEmpty Empty = true | isEmpty _ = false end : STACK というように書きます。つまり、stackという型構成子を定義する datatype宣言、EmptyStackという例外構成子を定義する exception宣言、そしてスタックに対する操作を定義するval宣言と fun宣言を閉じ込めているストラクチャーを作って、 そのストラクチャーに対してSTACKというシグネチャーを与えます。 そして、Stackという識別子をそのストラクチャーに束縛します。 それでは次に、STACKというシグネチャーはどのように 定義すればいいか、ということについて考えてみましょう。 スタックの場合、例外とデータに関してはすべて 公開しないといけませんので、STACKの中にはそれらの仕様記述が 含まれている必要があります。さて、それでは、datatype宣言に 対応する仕様記述は、いったいどう書けばいいのでしょうか。 datatype宣言に対応する仕様記述を完全に省略してしまう ということはできません。なぜなら、もしもそうしたとすると、 stackという型構成子が非公開になってしまうわけですが、stackが 非公開だと、stackという型を含むデータ(関数も含む)も、 公開することができなくなってしまうからです。逆に、 datatype 'a stack = Empty | Node of 'a * 'a stack という仕様記述を書くこともできません。なぜなら、この仕様記述を 書いたとすると、EmptyとNodeというデータ構成子が 公開されてしまいますので、スタックを抽象データ型として定義した ということにならないからです。 ですから、ここで必要なことは、stackという型構成子だけを 公開して、EmptyとNodeというデータ構成子を非公開にする、 ということです。これを実現するためには、 type 型変数 型構成子 という形の特殊な仕様記述を書く必要があります。つまり、 type 'a stack という仕様記述を書けば、stackという型構成子だけが公開されて、 EmptyとNodeは隠蔽される、ということです。したがって、 スタックを抽象データ型として定義したいときは、STACKという シグネチャーを、 signature STACK = sig type 'a stack exception EmptyStack val create : 'a stack val push : 'a * 'a stack -> 'a stack val pop : 'a stack -> 'a stack val top : 'a stack -> 'a val isEmpty : 'a stack -> bool end と定義すればいい、ということになります。 なお、2個以上の型を引数として受け取る型構成子を 公開したいときは、 type ( 型変数 , 型変数 , …… , 型変数 ) 型構成子 という仕様記述を書きます。

Q 13.3.3___スタックというのはどんなときに 使われるものなんですか。

一般論としては、スタックというのは、再帰的な構造を 解析する必要があるときに使われるものです。 それでは、スタックを利用する例として、式の構造を解析しながら 評価する、という動作をする関数の定義について 考えてみることにしましょう。 MLという言語の内部での「式」ではなくて、「演算を 記述するために演算子と演算対象とを並べて書いたもの」という、 もう少し広い意味での「式」は、次のような3種類の書き方のうちの いずれかを使って記述されます。 (1) 式 演算子 式 (2) 演算子 式 式 (3) 式 式 演算子 たとえば、74から21を減算するという演算は、減算をあらわす 演算子が-だとすると、(1)を使えば74-21、(2)を使えば74 21 -、 (3)を使えば- 74 21と記述されることになります。これらの 記述方法にはそれぞれ呼び名が付いていて、(1)は「中置記法」、 (2)は「ポーランド記法」または「前置記法」、(3)は 「逆ポーランド記法」または「後置記法」と呼ばれます。 さて、今、 datatype operator = Add | Subtract | Multiply | Divide datatype token = TO of operator | TI of int というように、tokenという型が定義されているとします。 このとき、tokenのリストに適用すると、そのリストを 逆ポーランド記法で記述された式だとみなして、その式を 評価することによって得られる値を返す、evalPolishという 関数を定義するためにはどうすればいいか、ということについて 考えてみましょう。 リストというのは再帰的な構造を持っているデータですが、 tokenのリストが持っている構造は、そのリストがあらわしている 式の構造と同じものではありません。ですから、tokenのリストを 評価するためには、それが持っている再帰的な構造を解析する 必要があり、したがってスタックを利用する必要がある、 ということになります。 evalPolishの定義は、Q 13.3.2で定義を書いたStackという ストラクチャーを使って、次のように書くことができます。 local open Stack in fun evalPolish exp = let fun operate (x,Add,y) = x+y | operate (x,Subtract,y) = x-y | operate (x,Multiply,y) = x*y | operate (x,Divide,y) = x div y fun evalPolish1 ([],st) = top st | evalPolish1 (TI x::xs,st) = evalPolish1 (xs,push (x,st)) | evalPolish1 (TO x::xs,st) = evalPolish1 (xs, push (operate (top (pop st),x,top st), pop (pop st))) in evalPolish1 (exp,create) end end evalPolishを定義するための補助的な関数として定義されている evalPolish1は、逆ポーランド記法で記述された式を評価するための アルゴリズムを素直に表現したものです。tokenのリストと スタックから構成される組にevalPolish1を適用すると、 evalPolish1は、 ● リストが空ならばスタックのトップを返す。 ● リストの頭部が演算対象ならば、リストの尾部と、頭部を プッシュすることによってできたスタックから構成される組に evalPolish1を適用して、その戻り値をそのまま返す。 ● リストの頭部が演算子ならば、1回だけポップしたスタックの トップと、そのままのスタックのトップとに対してその演算子を 適用して、リストの尾部と、2回ポップしたのちに演算の結果を プッシュすることによってできたスタックから構成される組に evalPolish1を適用して、その戻り値をそのまま返す。 というアルゴリズムを実行します。逆ポーランド記法で 記述された式と空のスタックから構成される組にevalPolish1を 適用した場合、evalPolish1は、その式を評価することによって 得られた値を戻り値として返すことになります。

---練習問題

13.1___練習問題12.1で定義したRomanというストラクチャーに 与えると、NotPlus、TooLarge、InvalidRomanという例外と、 intToRoman、romanToIntという関数の定義のみを公開して、 それら以外の定義を隠蔽する、ROMANというシグネチャーを定義する signature宣言を書いてください。

13.2___練習問題12.2で定義したTokenというストラクチャーに 与えると、isOperator、expToTokensという関数の定義のみを 公開して、それら以外の定義を隠蔽する、TOKENという シグネチャーを定義するsignature宣言を書いてください。

13.3___練習問題12.3で定義したEvalというストラクチャーに 与えると、InvalidExpressionという例外と、evalという関数の 定義のみを公開して、それら以外の定義を隠蔽する、EVALという シグネチャーを定義するsignature宣言を書いてください。

13.4___「待ち行列」と呼ばれる抽象データ型があります。 待ち行列というのは、一列に並んだデータから構成されている という点ではスタックに似ていますが、それに対して許されている 操作は、スタックとは少し違っています。待ち行列に対しては、 ● 空の待ち行列を作る。 ● 待ち行列の末尾にデータを追加する。 ● 待ち行列の先頭のデータを削除する。 ● 待ち行列の先頭がどんなデータであるかということを調べる。 ● 待ち行列が空かどうかを調べる。 という5種類の操作だけが許されています。たとえば、 43 82 17 33 61 という待ち行列があって、左端が先頭で、右端が末尾だとするとき、 この待ち行列の末尾に25を追加して、 43 82 17 33 61 25 という待ち行列を作ったり、先頭のデータを削除して、 82 17 33 61 という待ち行列を作ったりするということは可能ですが、先頭に データを追加したり、末尾のデータを削除したりするということは 許されていませんので、先頭に58を追加して、 58 78 44 26 11 39 という待ち行列を作ったり、末尾のデータを削除して、 78 44 26 11 という待ち行列を作ったりするということはできません。 待ち行列を実現するための、QUEUEという名前のシグネチャーを 定義するsignature宣言と、Queueという名前のストラクチャーを 定義するstructure宣言を書いてください。

なお、待ち行列を実現するための5種類の操作は、次のような 仕様にもとづいて定義してください。 create : 'a queue 空の待ち行列を値として持ちます。 enqueue : 'a queue * 'a -> 'a queue 待ち行列とデータから構成される組に適用すると、その待ち行列の 末尾にそのデータを追加することによってできた待ち行列を 返します。 dequeue : 'a queue -> 'a queue 待ち行列に適用すると、その先頭のデータを 削除することによってできた待ち行列を返します。その待ち行列が 空の場合は、EmptyQueueという例外を発生させます。 front : 'a queue -> 'a 待ち行列に適用すると、その先頭のデータを返します。 その待ち行列が空の場合は、EmptyQueueという例外を発生させます。 isEmpty : 'a queue -> bool 待ち行列に適用すると、それが空ならば真、そうでなければ偽を 返します。

13.5___「辞書」と呼ばれる抽象データ型があります。辞書は、 「キー」と呼ばれるデータと「値」と呼ばれるデータから構成される 組の集合で、辞書に対しては、 ● 空の辞書を作る。 ● キーと値の組を辞書に追加する(ただし、同一のキーを持つ組が すでに辞書の中に存在している場合は、古い組と新しい組とを 置き換える)。 ● 与えられたキーを持つ組を辞書の中から見つけ出して、その組を 辞書から削除する。 ● 与えられたキーを持つ組を辞書の中から見つけ出して、その組の 値を調べる。 という4種類の操作だけが許されています。たとえば、 ("pastor",273) ("miles",598) ("agricola",304) ("mercator",811) ("philosophus",620) という4個の組を含んでいる辞書があるとするとき、 その辞書に対して、("medicus",130)という新しい組を追加したり、 "mercator"というキーを持つ組を削除したり、"agricola"という キーを持つ組の値を調べたりする、というような操作ができます。 ("miles",433)という組を追加する場合は、同じキーを持つ組が すでに辞書の中に存在しますので、("miles",598)という古い組を 削除して、("miles",433)をそれと置き換えることになります。 辞書を実現するための、DICTという名前のシグネチャーを 定義するsignature宣言と、Dictという名前のストラクチャーを 定義するstructure宣言を書いてください。

なお、辞書を実現するための4種類の操作は、次のような 仕様にもとづいて定義してください。 create : ('a * 'b) dict 空の辞書を値として持ちます。 insert : (''a,'b) dict * ''a * 'b -> (''a,'b) dict 辞書とキーと値から構成される組に適用すると、そのキーと値から 構成される組をその辞書に追加することによってできた 辞書を返します。 delete : (''a,'b) dict * ''a -> (''a,'b) dict 辞書とキーから構成される組に適用すると、そのキーを持つ組を 辞書の中から見つけ出して、その組を削除することによってできた 辞書を返します。そのキーを持つ組がその辞書の中に 存在していなかった場合は、Undefinedという例外を発生させます。 lookup : (''a,'b) dict * ''a -> 'b 辞書とキーから構成される組に適用すると、そのキーを持つ組を 辞書の中から見つけ出して、その組の値を返します。そのキーを持つ 組がその辞書の中に存在していなかった場合は、Undefinedという 例外を発生させます。

第14章===モジュールを作り出すモジュール

14.1---プログラムの再利用

Q 14.1.1___プログラムを再利用するっていうのは どういうことなんですか。

プログラムの再利用というのは、新しいプログラムを書くときに、 過去に書かれたプログラムの一部分を切り取って、それをそのまま 新しいプログラムの一部分にすることです。 プログラムを書いているとき、これから実現させようとしている 機能が、過去に書かれたプログラムの中の機能と同じものだという 場合、過去のプログラムの一部分をそのまま利用しようと 考えるのは、ごく自然なことです。 ただし、プログラムの再利用は、過去のプログラムがどのように 書かれているかということによって、容易だったり 困難だったりします。ですから、プログラムを書くときに、再利用が 容易にできるものにすることに注意を払うというのは、とても 大切なことです。

Q 14.1.2___プログラムを再利用しやすいものにするために 注意を払わないといけないのはどんなことですか。

再利用しやすいプログラムを書くために 注意しないといけないことのうちでもっとも重要なのは、 プログラムをモジュールに整然と分割するということです。 プログラムを再利用するためには、ひとつの機能を実現させるための 部分を、プログラムの中から切り取らないといけません。 プログラムがモジュールに整然と分割されていれば、必要な機能を 実現させるためのモジュールだけを切り取るということが 簡単にできますが、プログラムを構成するさまざまな部分が複雑に からみあっているとすると、特定の機能を実現させるための部分を そこから切り取ることは困難です。

Q 14.1.3___モジュールを作り出すモジュールっていうのは どういうものなんですか。

モジュールを作り出すモジュールというのは、 自分の中に未完成の部分を持っていて、その部分にさまざまなものを 取り付けることによって、さまざまなモジュールを 作り出すことのできるモジュールのことです。

Q 14.1.4___モジュールを作り出すモジュールを作ることは、 そうでないモジュールを作ることに比べて、どんな メリットがあるのですか。

モジュールを作り出すモジュールを使ってプログラムを 構成すると、そのプログラムは再利用が容易になります。 過去のプログラムの中で必要とされた機能と、現在作成中の プログラムで必要とされる機能とが、似ているけれども微妙に違う、 というのはよくあることです。過去のプログラムが、特定の 機能のみを実現させるモジュールから構成されていたとすると、 それとは微妙に異なる機能を必要とするプログラムを書くために そのプログラムを再利用するということは不可能です。 しかし、もしも過去のプログラムが、モジュールを作り出す モジュールから構成されていたとすると、その中のモジュールは 再利用できるかもしれません。なぜなら、そのモジュールの未完成の 部分に、過去のプログラムとは違うものを取り付けることによって、 現在作成中のプログラムに必要な機能が 実現されるかもしれないからです。

14.2---ファンクターの定義

Q 14.2.1___MLで、モジュールを作り出すモジュールを定義する ということは可能ですか。

はい、可能です。 MLの場合、ファンクターというのが、モジュールを作り出す モジュールに相当します。ファンクターというのは、基本的には ストラクチャーのひとつの変種です。ですから、 ストラクチャーと同じように、さまざまな定義をファンクターの中に 閉じ込めておくということができます。 ファンクターとストラクチャーとは、似ているところもあるし、 違っているところもあります。ファンクターとストラクチャーとの あいだにあるもっとも大きな相違点は、ファンクターの中では、 そのファンクターが定義される時点ではまだ何にも 束縛されていない識別子を使うことができるということです。 ファンクターの中に閉じ込められている定義を利用するためには、 ファンクターにストラクチャーを製造させるというステップを 踏む必要があります。ファンクターは、自分の中にある定義から 構成されるストラクチャーを製造することができるという機能を 持っていて、ファンクターによって製造されたストラクチャーは、 普通のストラクチャーと同じように利用することができます。 ファンクターは、何らかのストラクチャーに適用されたときに 新しいストラクチャーを製造します。つまり、 ファンクターというのは、引数としてストラクチャーを受け取って 戻り値としてストラクチャーを返す、関数のようなものである と考えることができます。ファンクターの定義の中で使われている、 何にも束縛されていなかった識別子は、ファンクターが引数として 受け取ったストラクチャーの中の定義によって、何かに 束縛されます。 関数は、どんなデータに適用されたかということによってさまざまな データを戻り値として返すわけですが、それと同じように、 ファンクターも、どんなストラクチャーに適用されたか ということによってさまざまなストラクチャーを戻り値として 返します。ですから、プログラムの中でファンクターを 利用したいときは、必要とされる機能を持つストラクチャーが 製造されるように、適切なストラクチャーにそのファンクターを 適用すればいい、ということになります。そして、現在作成中の プログラムが、過去に書かれたプログラムの中の機能に 似ているけれども微妙に違う機能を必要とする場合は、過去に 適用したものとは異なるストラクチャーに同じファンクターを 適用すればいいわけです。

Q 14.2.2___ファンクターを定義したいときは どうすればいいのですか。

ファンクターは、functor宣言というものを書くことによって、 定義することができます。 functor宣言というのは、宣言の一種で、 functor 識別子f ( 識別子a : シグネチャー式 ) = ストラクチャー式 というように書きます。この宣言が実行されると、識別子fが、 ストラクチャー式によってあらわされるファンクターに 束縛されます。 コロンの右側に書かれたシグネチャー式は、 このfunctor宣言によって定義されるファンクターを適用する ストラクチャー(つまり引数となるストラクチャー)が 持たないといけないシグネチャーをあらわしています。 このfunctor宣言によって定義されるファンクターを、 そのシグネチャーを持つストラクチャーに適用すると、その時点で、 識別子aはそのストラクチャーに束縛されます。 たとえば、 signature RATE = sig val r : int end というシグネチャーが定義されているとするとき、 functor DiscountFUN (Rate : RATE) = struct fun discount n = n * (10 - Rate.r) div 10 end というfunctor宣言を書くことによって、discountという関数の 定義が閉じ込められているファンクターが作られて、 DiscountFUNという識別子が、そのファンクターに 束縛されます(注)。 (注) ファンクターの名前の付け方には慣習のようなものは ないようですが、この文章の中では、[Paulson,1991]にならって、 末尾に大文字のFUNを付ける、という規則を採用しています。 このDiscountFUNというファンクターの中で使われている Rate.rという限定識別子が、このファンクターが定義された 時点では、どんなものにもまだ束縛されていない、ということに 注意してください。Rateという識別子は、DiscountFUNが ストラクチャーに適用されたときに、そのストラクチャーに 束縛されます。ですから、Rate.rは、DiscountFUNが引数として 受け取ったストラクチャーの中の定義にしたがって何かに 束縛される、ということになります。 DiscountFUNを適用することができるストラクチャーは、かならず RATEというシグネチャーを持っていないといけません。 言い換えれば、DiscountFUNは、rという識別子をintのデータに 束縛する定義を含んでいるストラクチャーにしか 適用することができないということです。 RATEというシグネチャーを持つストラクチャーにDiscountFUNを 適用すると、DiscountFUNは、Rateという識別子を そのストラクチャーに束縛することによってdiscountの定義を 完成させて、その結果としてできたストラクチャーを戻り値として 返します。たとえば、 structure Three = struct val r = 3 end というstructure宣言によって定義されたThreeという ストラクチャーにDiscountFUNを適用したとすると、DiscountFUNは、 まず、Rateという識別子をThreeに束縛します。すると、Rate.rは、 Threeの中の定義にしたがって3に束縛されます。そして、 DiscountFUNは、引数を3割引した金額を返す関数として完成した discountの定義が閉じ込められているストラクチャーを、 戻り値として返すわけです。

Q 14.2.3___ファンクターを定義する場合も、ストラクチャーと 同じように、シグネチャーによる情報隠蔽は可能ですか。

はい、可能です。 ファンクターはストラクチャーの一種ですから、普通の ストラクチャーと同じように、かならず自分のシグネチャーを 持っています。そして、 ストラクチャー式 : シグネチャー式 という形のストラクチャー式を書くことによって、いくつかの 定義を非公開にするシグネチャーを強制的に与えることができる という点でも、ファンクターと普通のストラクチャーとは 同類です。ですから、 signature TANUKI = sig val karasu : int -> int end signature KITSUNE = sig val suzume : int end functor TanukiFUN (Kitsune : KITSUNE) = struct val hibari = 500 fun karasu n = n * hibari + Kitsune.suzume end : TANUKI というように定義されているTanukiFUNという ファンクターの中にある二つの定義のうち、公開されるのは karasuだけで、hibariの定義は非公開ということになります。 ファンクターの中の定義が非公開であるというのは、 そのファンクターが作り出したストラクチャーがその定義を 秘匿するということです。たとえば、 structure Kawauso = struct val suzume = 700 end structure Itachi = TanukiFUN (Kawauso) というように、TanukiFUNをKawausoに適用することによって Itachiというストラクチャーを作ったとすると、Itachiの中には hibariとkarasuの定義が含まれることになるわけですが、hibariの 定義は非公開ですから、Itachiの外側からItachiの中にあるhibariの 定義を利用するということはできません。

14.3---ファンクター適用

Q 14.3.1___ファンクターをストラクチャーに適用したいときは どうすればいいのですか。

ファンクターをストラクチャーに適用したいときは、 ファンクター適用というものを書きます。 ファンクター適用は、 ファンクター名 ( ストラクチャー式 ) と書きます。ファンクター適用が実行されると、その中の名前で 指定されたファンクターが、ストラクチャー式によって あらわされるストラクチャーに適用されます。たとえば、 MarimbaFUNというファンクターをCembaloというストラクチャーに 適用したいとすると、 MarimbaFUN (Cembalo) というファンクター適用を書けばいい、ということになります (関数適用とは違って、ファンクター適用の中の ストラクチャー式は、かならず丸括弧で囲まないといけません)。 ところで、 struct 宣言 宣言 …… end という形のものもストラクチャー式の一種ですから、 MarimbaFUN (struct val a = 38 end) というように書くことによって、名前のないストラクチャーに ファンクターを適用するということもできます。 さらに、ファンクター適用もストラクチャー式の一種なので、 MarimbaFUN (ClarinetFUN (Ukulele)) というように書くことによって、ファンクターが返した ストラクチャーに別のファンクターを適用する、 ということもできます。

Q 14.3.2___ファンクターをストラクチャーに 適用することによってできたストラクチャーに、識別子を 束縛したいときは、どうすればいいのですか。

そんなときは、structure宣言のイコールの右側に ファンクター適用を書きます。 たとえば、 functor DiscountFUN (Rate : RATE) = struct fun discount n = n * (10 - Rate.r) div 10 end と定義されているDiscountFUNというファンクターを使って、 引数を3割引した金額を求めるdiscountを閉じ込めているような ストラクチャーを作って、DiscountThreeという識別子を、 そのストラクチャーに束縛したい、としましょう。つまり、 その結果として、 - DiscountThree.discount 100; > val it = 70 : int というようなことができるようにしたいということです。 そのためには、discountの定義の中に書かれているRate.rが、 3という整数を値として持たないといけないわけですから、まず、 structure Three = struct val r = 3 end というように、Threeというストラクチャーを定義します。 そして次に、DiscountFUNをThreeに適用して、DiscountThreeという 識別子を、DiscountFUNが返したストラクチャーに束縛します。 ファンクターが返したストラクチャーに識別子を束縛したいときは、 structure宣言のイコールの右側に書くストラクチャー式として、 ファンクター適用を書きます。ですから、 structure DiscountThree = DiscountFUN (Three) というstructure宣言を書くことによって、DiscountThreeという 識別子を、DiscountFUNが返したストラクチャーに束縛する、 ということができます。

14.4---未定義の型を持つファンクター

Q 14.4.1___二分探索木って何ですか。

二分探索木というのは、効率のいい方法でデータを探索するために 使われる二分木の一種です。 「二分探索木」は、次のように定義することができます。 まず、空の二分木は二分探索木です。 そして、大小関係が定義されているデータの集合があるとするとき、 その要素から構成されている、空ではない二分木は、 ● 左部分木を構成するすべてのデータは、根よりも小さい。 ● 右部分木を構成するすべてのデータは、根よりも大きい。 ● 左部分木と右部分木は、それぞれ二分探索木である。 という条件を満足しているならば、その場合に限り、二分探索木だと みなすことができます。 二分探索木は、たくさんのデータの中からひとつのデータを 探し出すという操作をなるべく効率よく実行したい、という場合に しばしば使われます。たとえば、xというデータが集合の要素ならば 真、そうでなければ偽を返す、という操作を 実現したいとしましょう。この操作は、もしもそのデータの集合が 二分探索木を構成しているならば、 ● 木が空ならば偽を返す。 ● xと根とが等しいならば、真を返す。 ● xのほうが根よりも小さいならば、左部分木を探索して、 その結果を返す。 ● xのほうが根よりも大きいならば、右部分木を探索して、 その結果を返す。 というアルゴリズムによって実現することができます。これは、 データが直線的に並んでいる場合のアルゴリズムに比べると、 きわめて効率のいいアルゴリズムです。

Q 14.4.2___二分探索木を使ってデータの集合を扱うための ストラクチャーを作りたいのですが、そのストラクチャーが任意の 型のデータを取り扱うことができるようにするためには どうすればいいのですか。

その場合は、未定義の型を持つファンクターを作って、 それを使って特定の型のデータを取り扱うストラクチャーを作る、 というようにします。 二分探索木を使ってデータの集合を扱うためのストラクチャーを 作る場合、それを構成するすべての関数を多相型にする ということは、残念ながらできません。なぜなら、 それらの関数のうちのいくつかは、大小関係を判定する述語を使う 必要があるわけですが、それらの関数が取り扱うことのできる データの型は、大小関係を判定する述語の型によって 決定されてしまうからです。 したがって、二分探索木によるデータの集合を、 ファンクターではない普通のストラクチャーで実現した場合、 それが任意の型のデータを扱うということはできません。 しかし、取り扱うデータの型が未定義になっているファンクターを 作って、そのファンクターが、特定の型のデータを取り扱う ストラクチャーを作り出す、というようにすることは可能です。 それでは、実際に、二分探索木によるデータの集合を実現する、 SetFUNという名前のファンクターを定義してみることにしましょう。 SetFUNは、次のような定義から構成されるファンクターです。 tree 二分木の型 create 空の二分木 insert 集合に要素を追加する関数 member 与えられたデータが集合の要素ならば真、そうでなければ 偽を返す関数 関数は、もっとたくさん定義してもいいのですが、 あまりたくさんだとfunctor宣言が長くなってしまいますので、 insertとmemberの二つだけということにしておきます。 まず最初に、SetFUNに与えるシグネチャーはどう書けばいいか、 ということについて説明しましょう。treeの仕様記述は、 type 'a tree と書けばいいわけですが、それ以外は、ちょっと特殊な書き方が 必要になります。insertやmemberは、多相型関数ではなくて、特定の 型のデータしか扱えないのだけれども、その特定の型というのがまだ 定義されていない、という関数です。そのような関数の仕様記述を 書くためには、未定義の型をあらわす識別子を決めておいて、 その型の仕様記述をシグネチャー式の中に書いておく、ということが 必要です。未定義の型の仕様記述は、ただ単に、 type 識別子 と書くだけです。たとえば、Tという識別子を、未定義の型を あらわすために使うとするならば、 type T という仕様記述をシグネチャー式の中に書いておけばいいのです。 そうすると、たとえばinsertの仕様記述は、 val insert : T * T tree -> T tree というように、Tという識別子を使って書くことができます。 同じように、createというデータの仕様記述も、 val create : T tree と書けばいいわけです。 さて、それでは、SetFUNに与えるシグネチャーを、SETという名前で 定義することにしましょう。signature宣言は、 signature SET = sig type T type 'a tree val create : T tree val insert : T * T tree -> T tree val member : T * T tree -> bool end と書けばいい、ということになります。 それでは次に、SetFUNの引数にすることのできるストラクチャーが 持たないといけないシグネチャーの定義はどのように書けばいいか、 ということについて説明しましょう。 大小関係を判定する述語は、取り扱うデータの型が 確定していなければ定義することができませんから、その定義は、 SetFUNの中ではなくて、SetFUNの引数となるストラクチャーの中に 書かれている必要があります。したがって、そのストラクチャーが 持たないといけないシグネチャーは、大小関係を判定する述語の 仕様記述を含んでいる必要があるということになります。さて、 それでは、その述語の仕様記述はいったいどのように 書けばいいのでしょうか。 大小関係を判定する述語は、何らかの特定の型のデータを 取り扱うわけですが、その型は、SetFUNが特定のストラクチャーに 適用されるまでは未定義です。したがって、SetFUNに与える シグネチャーを定義したときと同じように、ここでも、未定義の 型をあらわす識別子を決めておいて、その型の仕様記述を シグネチャー式の中に書いておく、ということが必要になります。 ですから、SetFUNの引数にすることのできるストラクチャーが 持たないといけないシグネチャーを、ORDERという名前で 定義したいとすると、 signature ORDER = sig type T val less : T * T -> bool end というようなsignature宣言を書く必要がある、 ということになります。 次に、SetFUNの定義について説明します。未定義の型を含む ファンクターを定義する場合に注意を必要とする点は、 ひとつだけです。それは、「ファンクターの中の未定義の型と、 引数として受け取ったストラクチャーの中の具体的な型とを 結び付ける宣言を書く必要がある」ということです。 たとえば、未定義の型を含むファンクターを、 signature RES = sig type A (* 中略 *) end signature ARG = sig type B (* 中略 *) end functor ResFUN (Arg : ARG) = struct (* 中略 *) end : RES というように作ろうとしているならば、ファンクターの中で未定義の 型として扱われているAという型と、引数として受け取った ストラクチャーの中で未定義の型として扱われているBという型とが 同じものだということを記述するために、 type A = Arg.B という宣言を書く必要があります。 それでは、SetFUNを定義するfunctor宣言を書いてみましょう。 SETとORDERという二つのシグネチャーは、未定義の型を あらわすための識別子として、Tという同じ識別子を 使っていますが、それらを結び付ける宣言は、やはり必要です。 したがって、SetFUNを定義するfunctor宣言は、 functor SetFUN (Order : ORDER) = struct type T = Order.T datatype 'a tree = Empty | Node of 'a * 'a tree * 'a tree val create = Empty fun insert (x,Empty) = Node (x,Empty,Empty) | insert (x,Node (y,left,right)) = if Order.less (x,y) then Node (y,insert(x,left),right) else if Order.less (y,x) then Node (y,left,insert(x,right)) else Node (y,left,right) fun member (x,Empty) = false | member (x,Node (y,left,right)) = if Order.less (x,y) then member (x,left) else if Order.less (y,x) then member (x,right) else true end : SET というように書く必要がある、ということになります。 次に、SetFUNの引数にすることのできるストラクチャーを定義する 方法について説明します。そのようなストラクチャーは、 大小関係を判定するlessという関数の宣言を 含んでいなければならないわけですが、必要な宣言は、実は もうひとつあります。それは、未定義の型をあらわすために シグネチャーの中で使われているTという識別子を具体的な型に 束縛するための宣言です。 未定義の型をあらわすためにシグネチャーの中で使われている 識別子を具体的な型に束縛する宣言は、 type 識別子 = 型式 と書きます。たとえば、シグネチャーの中で使われているAという 識別子をintに束縛したいならば、 type A = int という宣言を書けばいいわけです。 それでは、SetFUNを使って、具体的な型の集合を実現する ストラクチャーをいくつか作ってみることにしましょう。 まず最初に、整数の集合を実現する、IntSetというストラクチャーを 作ってみましょう。そのためには、整数の大小関係を判定する 述語から構成されるストラクチャーが必要です。そこで、 structure IntOrder = struct type T = int fun less (x,y) = xQ 14.4.3___二分探索木による集合を実現するファンクターを 定義するときに、=という組み込み演算子を使うと エラーになってしまうのですが、それはなぜですか。 それは、=という組み込み演算子を使ったために、 取り扱うことのできる型が、「等値型でなければならない」という 制約を受けることになったからです。 Q 14.4.2で、二分探索木による集合を実現する、SetFUNという ファンクターを定義したとき、その中の関数の定義は、=という 組み込み演算子を使わずに書きましたが、=を使って書くことも 可能です。たとえば、memberという関数は、 fun member (x,Empty) = false | member (x,Node (y,left,right)) = if x=y then true else if Order.less (x,y) then member (x,left) else member (x,right) というように定義してもかまいません。ただし、このように=を 使った場合は、取り扱うことのできる型が、 「等値型でなければならない」という制約を受けることになります。 実は、シグネチャー式の中に書く未定義の型の仕様記述は、 その未定義の型が等値型の制約を持っていないならば、 type 識別子 と書けばいいのですが、等値型の制約を持っている場合は、 eqtype 識別子 と書かないといけないのです。 ですから、SetFUNを構成する関数を定義するときに=を 使ったにもかかわらず、SETやORDERを定義する シグネチャー式の中で、未定義の型の仕様記述として、 type T というものを書いたとすると、仕様記述と定義とが 一致しなくなりますので、エラーになってしまうのです。 したがって、SetFUNを構成する関数の中で=を使った場合は、 未定義の型の仕様記述を、 eqtype T に変更しないといけません。

---練習問題

14.1___練習問題12.2で定義したTokenというストラクチャーを 改良して、文字が演算子かどうかを判定するisOperatorという関数が 未定義になっている、TokenFUNというファンクターを定義する functor宣言と、式をトークンに分解するexpToTokensという 関数のみを公開するためにTokenFUNに与える、TOKENという シグネチャーを定義するsignature宣言を書いてください。

なお、TokenFUNを適用することのできるストラクチャーが 持たないといけないシグネチャーは、 signature OPERATOR = sig val isOperator : char -> bool end というように定義されているとします。isOperatorというのは、 文字に適用すると、それが演算子ならば真、そうでなければ偽を返す 関数です。TokenFUNは、文字が演算子かどうかということを、 isOperatorを使って判断します。 TokenFUNの使い方について説明するための例として、 ParenTokenというストラクチャーを、TokenFUNを使って 作ってみることにします。 ParenTokenは、丸括弧を演算子だとみなして式をトークンに分解する 関数を含んでいるようなストラクチャーにしたい、としましょう。 つまり、ParenTokenの中のexpToTokensという関数が、 - ParenToken.expToTokens "cum(de(sub)ex(ob)ante)post"; > val it = ["cum", "(", "de", "(", "sub", ")", "ex", "(", "ob", ")", "ante", ")", "post"] : string list というように、#"("または#")"を演算子だとみなして式を 分解するようにしたいわけです。 そのようなストラクチャーを、TokenFUNを使って 作りたいというときは、まず、 structure Paren = struct fun isOperator #"(" = true | isOperator #")" = true | isOperator _ = false end というstructure宣言で、#"("と#")"を演算子だと判定する、 isOperatorという関数を含む、Parenというストラクチャーを 定義します。そして次に、TokenFUNをParenに適用して、 ParenTokenという識別子を、TokenFUNが返したストラクチャーに 束縛します。つまり、 structure ParenToken = TokenFUN (Paren) というstructure宣言を書けばいいわけです。これで、丸括弧を 演算子だとみなして式を分解する関数を含む、ParenTokenという ストラクチャーが完成したということになります。

14.2___データが類似しているという関係が定義された集合、 というものを実現する、SimSetFUNというファンクターを定義する functor宣言と、データ構成子を隠蔽するためにSimSetFUNに与える、 SIMSETというシグネチャーを定義するsignature宣言を 書いてください。

SimSetFUNは、次のようなデータと関数から構成されているとします (Tという識別子は、未定義の型をあらわしています)。 create : T set 空の集合を値として持ちます。 insert : T * T set -> T set データと集合から構成される組に適用すると、そのデータを その集合に追加することによってできる集合を返します。 member : T * T set -> bool データと集合から構成される組に適用すると、そのデータが その集合の要素ならば真、そうでなければ偽を返します。 simMembers : T * T set -> T set データと集合から構成される組に適用すると、その集合の 要素のうちで、そのデータに類似しているものをすべて 取り出すことによってできる集合を返します。 なお、SimSetFUNを適用することのできるストラクチャーが 持たないといけないシグネチャーは、 signature SIMILAR = sig eqtype T val isSim : T * T -> bool end というように定義されているとします。isSimというのは、二つの データから構成される組に適用すると、それらが類似しているならば 真、そうでなければ偽を返す関数です。SimSetFUNは、二つの データが類似しているかどうかということを、isSimを使って 判断します。 SimSetFUNを使ってストラクチャーを作る方法について、 類似しているという関係が定義された文字列の集合を実現する、 StringSimSetというストラクチャーを作るという例を使って 説明することにしましょう。 そのためには、まず、二つの文字列が類似しているというのは どういうことなのか、ということを決めておく必要があります。 文字列の類似性についてはさまざまな定義を 考えることができますが、ここでは、二つの文字列が、 ● 長さが同じである。 ● それぞれの文字列の中の同じ位置から文字を取り出して、 それらの二つの文字から構成される組を作ったとすると、 すべての組が同一の文字から構成されているか、または、ひとつの 組だけが異なる文字から構成されていて、残りのすべての組は同一の 文字から構成されている。 という条件を両方とも満足しているならば、その場合に限り、 それらの文字列は類似している、と定義することにします。 たとえば、"senectus"という文字列に類似しているのは、 "senectus" "senactus" "senecdus" というような文字列で、類似していないのは、 "senectu" "pesectus" "stnactes" "sapientia" というような文字列です。 類似しているという関係が定義された文字列の集合を実現する ストラクチャーを、SimSetFUNを使って作るためには、まず最初に、 二つの文字列が類似しているならば真、そうでなければ 偽を返す、isSimという関数の定義を含んでいるストラクチャーを 定義する必要があります。 それでは、StringSimilarという名前で、isSimという関数を含む ストラクチャーを定義することにしましょう。StringSimilarを 定義するstructure宣言は、 structure StringSimilar = struct type T = string fun isSimList ([],[]) = true | isSimList ([],_) = false | isSimList (_,[]) = false | isSimList (x::xs,y::ys) = if x=y then isSimList (xs,ys) else xs=ys fun isSim (x,y) = isSimList (explode x,explode y) end というように書くことができます。 StringSimilarの定義が完成したら、次に、SimSetFUNを StringSimilarに適用して、StringSimSetという識別子を、 SimSetFUNが返したストラクチャーに束縛します。つまり、 structure StringSimSet = SimSetFUN (StringSimilar) というstructure宣言を書けばいいわけです。これで、 類似しているという関係が定義された文字列の集合を実現する、 StringSimSetというストラクチャーが 完成したということになります。

第15章===手続き型計算モデル

15.1---計算モデル

Q 15.1.1___計算モデルって何ですか。

計算モデルというのは、プログラムを書いている人間の 頭の中にある、そのプログラムを実行することのできる仮想的な 機械の構造のことです。 人間がプログラムを書いているとき、その人間は、自分が書いている プログラムがどのようなメカニズムによって実行されるのか ということに関するイメージ、すなわち計算モデルを自分の頭の中に 持っています。計算モデルは、現実に存在するコンピュータの 物理的な構造によって規定されるのではなくて、使われている プログラミング言語によって規定されます。つまり、MLという言語を 使ってプログラムを書いている人はMLによって規定される 計算モデルを脳裏に浮かべていて、Pascalという言語を使って プログラムを書いている人はPascalによって規定される計算モデルを 念頭に置いている、ということです。 計算モデルは、細かく分類すればプログラミング言語の数と 同じ数だけ種類があるということになりますが、もう少し大きく 分類することも可能です。たとえば、Gofer、Scheme、Haskell、ML、 FP、Hope、Sisalなどのプログラミング言語が規定する 計算モデルは、顕著な類似点を持っていますので、それらは、 ひとつの抽象的な計算モデルを共有する個々の具体例だと 考えることができます。それらの計算モデルが共有している 抽象的な計算モデルというのは、「関数型計算モデル」 と呼ばれるものです。 計算モデルを大きく分類するための抽象的な計算モデルとしては、 関数型計算モデルのほかに、論理型計算モデル、 オブジェクト指向計算モデル、手続き型計算モデルなどがあります。 計算モデルと同様に、プログラミング言語も、どのような 計算モデルを規定するかということにもとづいて、大きく 分類することができます。規定する計算モデルが ○○計算モデルであるような言語は、「○○言語である」 と言われます。たとえば、MLは関数型計算モデルを規定する 言語ですから、「MLは関数型言語である」と言うことができます。

Q 15.1.2___関数型計算モデルっていうのはどんな 計算モデルなんですか。

関数型計算モデルというのは、関数をデータに適用することが プログラムの実行であると考える計算モデルのことです。 関数型言語(MLもそのひとつです)でプログラムを書くというのは、 関数をデータに適用するという動作を組み合わせることによって 目的とする動作が実現されるようにする、ということです。 ですから、そのプログラムを実行することのできる機械は、関数を データに適用して、その関数の戻り値に別の関数を適用して、 その戻り値にさらに別の関数を適用して、ということができるような メカニズムを持つことになります。関数型計算モデルというのは、 そのようなメカニズムのことです。

Q 15.1.3___論理型計算モデルっていうのはどんな 計算モデルなんですか。

論理型計算モデルというのは、命題を証明することが プログラムの実行だと考える計算モデルのことです。 論理型言語でプログラムを書くというのは、与えられた問題を命題の 集合として記述するということです。そのようにして完成した プログラムは、ひとつの公理系を表現していると 考えることができます。論理型言語で書かれたプログラムを 実行するというのは、何らかの命題が定理であるということを、 そのプログラムがあらわしている公理系を使って 証明するということです。ですから、公理系と命題が 与えられたときにその命題が定理であるということを 証明することができるようなメカニズムが論理型計算モデルだと 考えることができます。

Q 15.1.4___オブジェクト指向計算モデルっていうのはどんな 計算モデルなんですか。

オブジェクト指向計算モデルというのは、動作の主体が いくつもあって、それらがお互いにメッセージを 交換することによってプログラムが実行されていく、と考える 計算モデルのことです。 オブジェクト指向計算モデルでは、動作の主体のことを 「オブジェクト」と呼びます。オブジェクトの内部には、データを 記憶する場所と、「メソッド」と呼ばれる、オブジェクトが 実行することのできる動作の定義とが含まれています。 オブジェクトは、別のオブジェクトから送られてきた メッセージにもとづいて自分の中のメソッドを実行します。 メソッドは、オブジェクトの内部の状態を変化させたり、別の オブジェクトにメッセージを送ったりする、という動作を あらわしています。 オブジェクト指向言語で書かれたプログラムは、いくつかの オブジェクトの定義から構成されています。そのようなプログラムの 実行は、オブジェクトのうちのひとつにメッセージを 送ることによって開始されます。そのあとは、 それぞれのオブジェクトがお互いにメッセージを 交換することによって、動作が継続されていきます。 オブジェクト指向計算モデルというのは、そのような、動作の主体が 複数のオブジェクトに分散している計算モデルのことです。

Q 15.1.5___手続き型計算モデルっていうのはどんな 計算モデルなんですか。

手続き型計算モデルというのは、データを記憶することのできる 場所の状態を少しずつ変化させていくことがプログラムの実行だと 考える計算モデルのことです。 手続き型計算モデルでは、データを記憶することのできる場所 (少し短くして、「記憶場所」と呼ぶことにしましょう)を いくつでも作ることができると考えます。それぞれの記憶場所に 記憶されているデータは、別のデータに置き換えることができます。 つまり、記憶場所に対して、その状態を変化させるという 操作ができる、ということです。 手続き型言語で書かれたプログラムは、「どこそこの記憶場所の 状態をこのように変化させる」という操作をあらわす記述が一列に 並んでいる、という構造を持っています。そのようなプログラムは、 その中に記述されている操作の列にしたがって記憶場所の状態を 少しずつ変化させていく、というメカニズムによって実行されます。 手続き型計算モデルというのは、そのようなメカニズムのことです。 なお、現実に物理的な機械として存在するコンピュータの構造は、 手続き型計算モデルという抽象的な計算モデルを 具体化したものになっています。

Q 15.1.6___プログラミングのパラダイムっていうのは 何のことですか。

プログラミングのパラダイムというのは、与えられた問題が 記述されている形式を、計算モデルによって実行することのできる 形式へ変換するための思考方法のことです。 プログラムを書くという作業は、与えられた問題を何らかの プログラミング言語を使って記述するということです。この作業は、 与えられた問題が、最初から、使っている言語によって規定される 計算モデルによって実行することのできる形式になっているならば、 きわめてやさしい作業ということになります。しかし、現実には、 問題の形式と計算モデルの形式とのあいだには大きな距離があるのが 普通です。与えられた形式を計算モデルの形式へ変換するためには、 高度な思考力が必要となります。プログラミングの パラダイムというのは、その変換をするためにはどのように 思考すればいいのか、という方法のことです。 プログラミングのパラダイムは、計算モデルによって規定されます。 計算モデルはプログラミング言語によって規定されるわけですから、 それらの三つの概念はお互いに密接な関係にある、 ということになります。 関数型計算モデルによって規定されるパラダイムは、 「関数プログラミング」と呼ばれます。同じように、 論理型計算モデルによって規定されるパラダイムは 「論理プログラミング」、オブジェクト指向計算モデルによって 規定されるパラダイムは「オブジェクト指向プログラミング」、 手続き型計算モデルによって規定されるパラダイムは 「手続き型プログラミング」と呼ばれます。

Q 15.1.7___どうして、プログラミング言語には 数え切れないほどの種類があるのですか。

その理由はひとつではありませんが、おそらく、万能の 計算モデルというものが存在しないから、というのが最大の理由だと 思われます。 問題の形式と計算モデルの形式とのあいだの距離は、問題の性質と 計算モデルの性質の両方によって決定されます。つまり、 それぞれの計算モデルには得意な問題と不得意な問題とがあって、 どんな問題が得意でどんな問題が不得意なのかということは 計算モデルによって異なる、ということです。 プログラムを書くとき、使うべきプログラミング言語が最初から 決められている場合は、問題の形式と計算モデルの形式とのあいだに どんなに大きな距離があったとしても 我慢しないといけないわけですが、いくつかの プログラミング言語の中から選択して使ってもいいという 状況ならば、形式と形式とのあいだの距離がもっとも 小さくなるような言語を選択することによって、プログラミングの 労力を多少は軽減することができます。 もしも、与えられた問題に関して、現存するどんな プログラミング言語を使ったとしても、形式と形式とのあいだの 距離が大きすぎて満足できないならば、残された方法は、新しい 言語を設計することしかありません。新しい言語が次から次へと 生産され続けているという状況は、どんな問題に関しても プログラミングの労力が最小になるようにしようという理想が 追求されることによって生じているのです。

Q 15.1.8___問題の性質によって二つ以上の計算モデルを 使い分けることができるようにプログラミング言語を設計することは 可能ですか。

はい、可能です。 ひとつの問題をいくつかの部分に分解したとき、それぞれの部分に 適した計算モデルがすべて同じになるとは限りません。つまり、 問題を構成する部分のうち、ひとつは関数型計算モデルに、別の ひとつは論理型計算モデルに適しているというようなことも 起こりうる、ということです。そんな場合、プログラミング言語が、 いくつかの計算モデルを 使い分けることができるようになっていると、とっても便利です。 そのような理由で、プログラミング言語の多くは、いくつかの 計算モデルの使い分けができるように設計されています。

Q 15.1.9___MLも、計算モデルの使い分けができるように 設計されているのですか。

はい、そうです。 MLは、基本的には関数型計算モデルを規定する プログラミング言語ですが、問題の性質に応じて 手続き型計算モデルを使うこともできるように設計されています。 つまり、MLでも、手続き型言語と同じように、記憶場所の状態を 変化させるという記述を書くことができる、ということです。

15.2---参照

Q 15.2.1___参照って何ですか。

参照というのは、記憶場所を指示するデータのことです。 手続き型計算モデルでは、データを記憶することのできる場所を 作って、そこに対してさまざまな操作を実行する、という動作を 記述していく必要があります。MLでは、記憶場所を操作するために、 「参照」と呼ばれる、記憶場所を指示するデータを使います。 参照は、「参照型」と呼ばれる型を持つデータです。

Q 15.2.2___記憶場所を作りたいとき、MLでは、どのような記述を 書けばいいのですか。

MLでは、参照を作るという記述によって、記憶場所を 作ることができます。 参照を作りたいときは、refというデータ構成子を使います。refを 何らかのデータに適用すると、refは、そのデータと同じ型の データを記憶することのできる場所を作って、そのデータを その場所に記憶させて、その場所を指示する参照を返します。 たとえば、 ref 91 という式で、refというデータ構成子を91という整数に 適用したとすると、refは、整数を記憶することのできる場所を 作って、その場所に91を記憶させて、その場所を指示する参照を 返します。 参照型は、 型式 ref という型式によってあらわされます(ということは、refというのは データ構成子であると同時に型構成子でもある、 ということになります)。refの左側の型式は、参照が 指示している場所に記憶させることのできるデータの型を あらわしています。たとえば、 - ref "scrith"; > val it = ref "scrith" : string ref というように、refを文字列に適用することによって作り出された 参照の型は、string refという型式によってあらわされます。

Q 15.2.3___関数を記憶することのできる場所を作ることは 可能ですか。

はい、可能です。 たとえば、 - ref (fn n => n*2); > val it = ref fn : (int -> int) ref というように、int -> intという型の関数にrefを 適用することによって、その型の関数を記憶することのできる場所を 作ることができます。 ただし、refを多相型関数に適用することによって記憶場所を作る ということはできません。たとえば、 ref (fn x => x) というような、多相型関数にrefを適用する式は、 エラーになってしまいます。

Q 15.2.4___記憶場所から、その場所に記憶されているデータを 取り出したいときは、どうすればいいのですか。

記憶場所からデータを取り出したいときは、!という組み込み関数を 使います。 !というのは、'a ref -> 'aという型を持つ組み込み関数です。!を 参照に適用すると、!は、その参照によって指示される場所に 記憶されているデータを戻り値として返します。たとえば、 - val ir = ref 598; > val ir = ref 598 : int ref というようにして、irという識別子を、598を記憶している場所を 指示する参照に束縛したとするとき、!をirに適用すると、 - !ir; > val it = 598 : int というように、!は、irが指示している場所に記憶されている データを返します。 なお、refというのは普通のデータ構成子と同じ性質を 持っていますので、 ref 識別子 というパターンと参照とを照合したとすると、そのパターンの中の 識別子は、その参照が指示している場所に記憶されているデータに 束縛されます。ですから、 - val sr = ref "psychohistory"; > val sr = ref "psychohistory" : string ref - val ref s = sr; > val s = "psychohistory" : string というように、パターンと参照とを照合することによって、参照が 指示している場所からデータを取り出す、ということも可能です。

Q 15.2.5___記憶場所に記憶されているデータを別のデータに 置き換えたいときは、どうすればいいのですか。

記憶場所に記憶されているデータを別のデータに 置き換えたいときは、:=という組み込み演算子を使います。 手続き型計算モデルというのは、プログラムが 実行されていくにつれて、記憶場所の状態が少しずつ変化していく、 という計算モデルですから、手続き型計算モデルを使って プログラムを書くためには、記憶場所の状態を変化させるという 操作を記述することができる必要があります。 記憶場所の状態を変化させるという操作は、普通、「代入」と 呼ばれます。その動詞形は、「代入する」です。sというのが 記憶場所で、dというのがデータだとするとき、sに記憶されている データをdに置き換えることを、「sにdを代入する」と言います。 MLでは、:=という組み込み演算子を使うことによって、記憶場所に データを代入することができるようになっています。 :=というのは、'a ref * 'a -> unitという型を持つ 組み込み演算子です。rが参照で、dがデータだとするとき、 r := d という式で、(r,d)という組に:=を適用すると、:=は、rによって 指示される場所にdを代入して、戻り値としてユニットを返します。 それでは、対話型システムを使って試してみましょう。まず最初に、 - val ir = ref 304; > val ir = ref 304 : int ref というようにして、ひとつの記憶場所を作って、そこに304という 整数を記憶させて、irという識別子を、その記憶場所を指示する 参照に束縛します。次に、 - ir := 717; > val it = () : unit というように、irと717から構成される組に:=を適用します。 それから、!を使って、記憶場所の現在の状態を調べてみます。 すると、 - !ir; > val it = 717 : int というように、irによって指示される場所の記憶が304から717に 変更されている、ということがわかります。つまり、:=によって、 irによって指示される場所に717が代入されたわけです。

Q 15.2.6___記憶場所に対して、現在の状態を基準とする相対的な 変化を与えることは可能ですか。

はい、可能です。 手続き型計算モデルでは、記憶場所に対して、現在の状態を 基準とする相対的な変化を与える、という操作(たとえば、整数を 記憶している場所に対して、その整数を1だけ大きいものに 変更するとか、その整数を2倍したものに変更する、というような 操作)は、きわめて頻繁に使われるものです。MLでも、!と:=の 両方を使えば、そのような操作を記述することができます。 現在、irという識別子が参照に束縛されていて、 - !ir; > val it = 318 : int というように、その参照によって指示される場所に318が 記憶されているとしましょう。さて、それでは、その場所の状態を、 現在の整数よりも100だけ大きいものに変更するためには どうすればいいでしょうか。そんなときは、 - ir := !ir + 100; > val it = () : unit というように、!を使って記憶場所から取り出した整数と100とを 加算して、それから:=を使って、もとの記憶場所にその加算の結果を 代入します。すると、 - !ir; > val it = 418 : int というように、記憶場所は、もとの整数よりも100だけ大きい整数が 記憶されている状態に変化します。

Q 15.2.7___参照に適用することのできる関数とか、参照を 戻り値として返す関数を作ることは可能ですか。

はい、可能です。 それでは、参照に適用することのできる関数の定義を、実際に 書いてみることにします。たとえば、int refという型の参照に 適用すると、その参照が指示する場所に100を代入して、 戻り値としてユニットを返す、assignHundredという関数(型は int ref -> unit)を定義してみましょう。対話型システムで その動作を試してみると、 - val mikan = ref 277; > val mikan = ref 277 : int ref - assignHundred mikan; > val it = () : unit - !mikan; > val it = 100 : int というようになります。assignHundredは、 fun assignHundred ir = ir := 100 というfun宣言を書くことによって定義することができます。 次に、記憶場所の状態を相対的に変化させる関数を作ってみます。 たとえば、int refという型の参照に適用すると、その参照が 指示する場所の状態を、もとのデータを2乗したものに変更して、 戻り値としてユニットを返す、assignSquareという関数(型は int ref -> unit)を定義してみましょう。つまり、 assignSquareは、 - val ringo = ref 70; > val ringo = ref 70 : int ref - assignSquare ringo; > val it = () : unit - !ringo; > val it = 4900 : int というような動作をするわけです。assignSquareを定義する fun宣言は、 fun assignSquare ir = ir := !ir * !ir というように書くことができます。 次に、参照を戻り値として返す関数を作ってみます。たとえば、 参照に適用すると、その参照が指示する場所と同じ状態を持つ、 もうひとつの記憶場所を作って、その場所を指示する参照を返す、 copyStorageという関数(型は'a ref -> 'a ref)を 定義してみましょう。つまり、 - val kaki = ref "tanj"; > val kaki = ref "tanj" : string ref - val momo = copyStorage kaki; > val momo = ref "tanj" : string ref - kaki := "falan"; > val it = () : unit - !kaki; > val it = "falan" : string - !momo; > val it = "tanj" : string というように、copyStorageを使って記憶場所の複製を 作ることができるということです。copyStorageは、 fun copyStorage r = ref (!r) というfun宣言を書くことによって定義することができます。

Q 15.2.8___記憶場所に対するいくつかの操作を順番に 実行していくという動作は、どのように記述すればいいのですか。

そんなときは、記憶場所に対する操作をあらわす式を、複合式 またはlet式の中に、セミコロン(;)で区切って並べます。 手続き型計算モデルというのは記憶場所の状態を少しずつ 変化させていくという計算モデルですから、 手続き型計算モデルでは、記憶場所に対するいくつかの操作を順番に 実行していくという記述を書く必要が頻繁に生じます。 式をセミコロンで区切って並べて、その全体を丸括弧で 囲んだもの、つまり、 ( 式 ; 式 ; …… ; 式 ) という形のものは、複合式と呼ばれるひとつの式になります (Q 4.1.7参照)。複合式を構成するそれぞれの式は、先頭から 順番に評価されますから、複合式を書くことによって、 記憶場所に対するいくつかの操作を順番に実行していくという 動作を記述することができます。 それでは、記憶場所に対するいくつかの操作を順番に 実行していくという動作をする関数の例として、 multiplyAndAddという関数(型はint ref * int ref -> unit)を 作ってみましょう。multiplyAndAddは、aとbがint refという型の 参照だとするとき、(a,b)という組に適用すると、aが指示する場所の 整数とbが指示する場所の整数とを乗算した結果をaが指示する場所に 代入して、それから、bが指示する場所の整数を1だけ大きいものに 変更して、戻り値としてユニットを返します。たとえば、aが 指示する場所に3が記憶されていて、bが指示する場所に4が 記憶されているとするとき、 - multiplyAndAdd (a,b); > val it = () : unit というように、(a,b)という組にmultiplyAndAddを 適用したとすると、!aは12に、!bは5に変化します。 multiplyAndAddを定義するfun宣言は、複合文を使うことによって、 fun multiplyAndAdd (rx,ry) = ( rx := !rx * !ry; ry := !ry + 1 ) と書くことができます。 let式も、inとendのあいだに、セミコロンで区切りながら式を 何個でも並べて書くことができますので、let式を使っても、 記憶場所に対するいくつかの操作を順番に実行していくという 動作を記述することができます。 それでは、例として、swapという関数(型は 'a ref * 'a ref -> unit)を作ってみましょう。swapは、aとbが 参照だとするとき、(a,b)という組に適用すると、aが指示する場所の データとbが指示する場所のデータとを交換して、戻り値として ユニットを返す関数です。たとえば、aが指示する場所に"tempus"が 記憶されていて、bが指示する場所に"spatium"が 記憶されているとするとき、(a,b)という組にswapを適用すると、 !aが"spatium"で!bが"tempus"というように、それらの記憶場所の データが交換されます。 二つの記憶場所のデータを交換するためには、代入される場所の データが失われないように、代入をする前に、識別子をそのデータに 束縛しておかないといけません。ですから、swapを 定義するためには、let式を書いて、letとinのあいだにval宣言を 書く必要がある、ということになります。この場合、データを 交換するための代入をあらわす二つの式は、let式のinと endのあいだに、コンマで区切って並べることができます。つまり、 fun swap (rx,ry) = let val w = !rx in rx := !ry; ry := w end というfun宣言を書けばいい、ということです。

15.3---繰り返し

Q 15.3.1___繰り返しって何ですか。

繰り返しというのは、ひとつの記述によってあらわされる動作を 何回も実行する、という動作のことです。 手続き型計算モデルでプログラムを書く場合、ひとつの記述によって あらわされる動作を何回も実行するという動作、つまり繰り返しが、 しばしば必要になります。そのため、手続き型計算モデルを規定する プログラミング言語は、普通、繰り返しを記述するための構文を 持っています。

Q 15.3.2___MLでは、繰り返しを記述したいとき、どのような記述を 書けばいいのですか。

MLでは、「while式」と呼ばれる式を書くことによって、繰り返しを 記述することができます。 while式というのは、 while 式1 do 式2 という構文を持つ式のことです。この中に書く二つの式のうち、 式1は、値の型がboolでないといけません(式2のほうは、どんな 型でもかまいません)。while式をコンピュータに評価させると、 コンピュータは、「式1を評価してから式2を評価する」という 動作を何回も実行します。ただし、式1の値がfalseだった場合は、 式2を評価しないで、while式の評価を終了します。ですから、 while 繰り返しを続行する条件 do 何回も実行させたい動作 というwhile式を書くことによって、繰り返しを 記述することができるということになります。 なお、while式を評価することによって得られる値は、常に ユニットです。 それでは、例として、整数の階乗を求めるpfactという関数を、 while式を使って定義してみましょう。aとbが、整数を 記憶することのできる場所を指示する参照だとするとき、 (a,b)という組にpfactを適用すると、pfactは、!aの階乗を求めて、 bが指示する場所にその結果を代入して、戻り値としてユニットを 返します。つまり、pfactというのは、 - val a = ref 5; > val a = ref 5 : int ref - val b = ref 0; > val b = ref 0 : int ref - pfact (a,b); > val it = () : unit - !b; > val it = 120 : int というような動作をする関数だということです。 pfactは、nrという識別子を、引数として受け取った参照のうちの 1個目に束縛して、frという識別子を2個目に束縛する、 としましょう。つまり、pfactは、!nrの階乗を求めて、その結果を、 frが指示する場所に代入すればいいわけです。 手続き型計算モデルを使って階乗を求めるためには、繰り返しの 内部が実行されるたびに、その状態が、2、3、4、5、6、……と 変化していくような、もうひとつの記憶場所を作る必要があります。 そこで、 val ir = ref 2 という宣言で、irという参照を作ることにします。 !nrの階乗は、まず、frが指示する場所に1を代入して、それから、 (1) !frと!irとを乗算して、frが指示する場所にその結果を 代入する。 (2) !irに1を加算した結果を、irが指示する場所に代入する。 という動作を、!ir <= !nrという条件が成り立っているあいだ、 何回も実行することによって求めることができます。 この繰り返しが終了すると、frが指示する場所は、!nrの階乗が 記憶されている状態になるはずです。 以上のことを総合すると、pfactを定義するfun宣言は、 fun pfact (nr,fr) = let val ir = ref 2 in fr := 1; while !ir <= !nr do ( fr := !fr * !ir; ir := !ir + 1 ) end というように書けばいい、ということになります。

---練習問題

15.1___string refという型の参照に適用すると、その参照が 指示する場所に"dummy"という文字列を代入して、戻り値として ユニットを返す、assignDummyという関数(型は string ref -> unit)を定義するfun宣言を書いてください。

実行例 - val a = ref "Sophie"; > val a = ref "Sophie" : string ref - assignDummy a; > val it = () : unit - !a; > val it = "dummy" : string

15.2___string refという型の参照に適用すると、その参照が 指示する場所の状態を、もとの文字列を二つ連結したものに 変更して、戻り値としてユニットを返す、assignDupStringという 関数(型はstring ref -> unit)を定義するfun宣言を 書いてください。

実行例 - val a = ref "Vladimir"; > val a = ref "Vladimir" : string ref - assignDupString a; > val it = () : unit - !a; > val it = "VladimirVladimir" : string

15.3___xとyが、string refという型の参照だとするとき、 (x,y)という組に適用すると、xが指示する場所に記憶されている 文字列の右側に、yが指示する場所に記憶されている文字列を 連結することによってできる文字列を、xが指示する場所に 代入して、戻り値としてユニットを返す、assignCatStringという 関数(型はstring ref * string ref -> unit)を定義するfun宣言を 書いてください。

実行例 - val a = ref "Bartok"; > val a = ref "Bartok" : string ref - val b = ref "Marie"; > val b = ref "Marie" : string ref - assignCatString (a,b); > val it = () : unit - !a; > val it = "BartokMarie" : string - !b; > val it = "Marie" : string

15.4___xとyとzが参照だとするとき、(x,y,z)という組に 適用すると、 ● xが指示する場所に記憶されているデータを、識別子をそれに 束縛することによって待避させる。 ● yが指示する場所に記憶されているデータを、xが指示する場所に 代入する。 ● zが指示する場所に記憶されているデータを、yが指示する場所に 代入する。 ● 待避させてあったデータを、zが指示する場所に代入する。 という動作を上から順番に実行して、戻り値としてユニットを返す、 rotateという関数(型は string ref * string ref * string ref -> unit)を定義する fun宣言を書いてください。

実行例 - val a = ref "Dimitri"; > val a = ref "Dimitri" : string ref - val b = ref "Rasputin"; > val b = ref "Rasputin" : string ref - val c = ref "Anastasia"; > val c = ref "Anastasia" : string ref - rotate (a,b,c); > val it = () : unit - !a; > val it = "Rasputin" : string - !b; > val it = "Anastasia" : string - !c; > val it = "Dimitri" : string

15.5___xとyとzがint refという型の参照だとするとき、 (x,y,z)という組に適用すると、!xの!y乗を求めて、zが指示する 場所にその結果を代入して、戻り値としてユニットを返す、 ppowerという関数(型はint ref * int ref * int ref -> unit)を 定義するfun宣言を、再帰を使わずに書いてください。

実行例 - val a = ref 3; > val a = ref 3 : int ref - val b = ref 4; > val b = ref 4 : int ref - val c = ref 0; > val c = ref 0 : int ref - ppower (a,b,c); > val it = () : unit - !c; > val it = 81 : int

15.6___xとyとzがint refという型の参照だとするとき、 (x,y,z)という組に適用すると、!xと!yの最大公約数を求めて、zが 指示する場所にその結果を代入して、戻り値としてユニットを返す、 pgcmという関数(型はint ref * int ref * int ref -> unit)を 定義するfun宣言を、再帰を使わずに書いてください。

実行例 - val a = ref 90; > val a = ref 90 : int ref - val b = ref 84; > val b = ref 84 : int ref - val c = ref 0; > val c = ref 0 : int ref - pgcm (a,b,c); > val it = () : unit - !c; > val it = 6 : int

15.7___xとyがint refという型の参照だとするとき、(x,y)という 組に適用すると、フィボナッチ数列の第!x項を求めて、yが 指示する場所にその結果を代入して、戻り値としてユニットを返す、 pfibonaという関数(型はint ref * int ref -> unit)を定義する fun宣言を、再帰を使わずに書いてください。

実行例 - val a = ref 8; > val a = ref 8 : int ref - val b = ref 0; > val b = ref 0 : int ref - pfibona (a,b); > val it = () : unit - !b; > val it = 34 : int

第16章===テキストファイル

16.1---ストリーム

Q 16.1.1___ファイルって何ですか。

ファイルというのは、データを永続的に記録することのできる 媒体(ハードディスク、フロッピーディスク、CD-ROM、MOなど)の 上に作られた、データを格納することのできる容器のことです。 オペレーティングシステムは、ハードディスクや フロッピーディスクなどの媒体の上にデータを記録するとき、 そのデータを管理しやすくするために、「ファイル」と呼ばれる 容器を媒体の上に作って、その中にデータを格納します。

Q 16.1.2___テキストファイルって何ですか。

テキストファイルというのは、文字のデータのみを格納している ファイルのことです。 別の言い方をすれば、テキストファイルというのは、エディターを 使うことによってその内容を作ったり修正したりすることのできる ファイルのことです。つまり、エディターは、メールを書いたり プログラムを書いたりするときに使うわけですから、メールや プログラムが格納されているファイルはテキストファイルである、 ということになります。 テキストファイルではないファイル、つまり文字ではないものを 含むデータが格納されているファイルは、「バイナリーファイル」 と呼ばれます。機械語で書かれたプログラム、アーカイバーによって 圧縮されたデータ、音声や画像のデータなどは、文字ではない データを含んでいますので、それらのデータを格納している ファイルはバイナリーファイルであるということになります。

Q 16.1.3___ストリームって何ですか。

ストリームというのは、ファイルを指示するデータのことです。 プログラミング言語のいくつかは、「ストリーム」と呼ばれる データを扱うことができるようになっています。ただし、 「ストリーム」という言葉の意味は、それぞれの プログラミング言語によって微妙に違っています。MLの場合、 「ストリーム」という言葉は、「ファイルを指示するデータ」 という意味で使われています。 ファイルからデータを読み込んだり、ファイルにデータを 出力したりするという動作は、そのファイルを指示する ストリームを使うことによって実行されます。 ストリームが存在しているとき、そのストリームによって指示される ファイルは、「ファイルの中の現在位置」という状態を 持っています。ストリームを使ってファイルからの読み込みを 実行すると、データは、そのファイルの中の現在位置から 取り出されます。同じように、ストリームを使ってファイルに データを出力すると、そのデータは、そのファイルの現在位置に 格納されます。 ファイルからデータを読み込む場合に必要となるストリームと、 ファイルにデータを出力する場合に必要となるストリームとは、 同じものではありません。読み込みに使われるのは 「入力ストリーム」と呼ばれるストリームで、出力に使われるのは 「出力ストリーム」と呼ばれるストリームです。入力ストリームは instreamという型を持つデータで、出力ストリームは outstreamという型を持つデータです。

Q 16.1.4___ファイルの終わりっていうのは何のことですか。

ファイルの終わりというのは、ファイルに格納されている文字列の 最後の文字の次の位置のことです。 たとえば、ファイルの中に、 "Metamagical Themas" という文字列が格納されているとすると、その文字列の最後にある #"s"という文字の次の、文字が何もない位置が、そのファイルの 終わりという位置です。

Q 16.1.5___ファイルをオープンするっていうのは どういうことですか。

ファイルをオープンするというのは、ファイルからデータを 読み込んだりファイルにデータを出力したりするための 準備をすることです。 ファイルに対する読み込みや出力を実行するためには、そのための 準備として、 ● そのファイルを指示するストリームを作る。 ● そのファイルの現在位置を、適切な場所に設定する。 という二つのことを実行する必要があります。ファイルに対して その二つを実行することを、ファイルを「オープンする」 と言います。

Q 16.1.6___バッファーって何ですか。

バッファーというのは、ファイルから読み込んだデータや、これから ファイルに出力するデータを一時的に置いておく場所のことです。 ファイルからのデータの読み込みは、普通、時間的な効率を よくするために、プログラムが一回で読み込むデータの大きさとは 関係なく、できる限り大きなデータを一回で読み込むように 実行されます。読み込んだデータは、「バッファー」と呼ばれる 記憶場所に一時的に置かれます。そして、プログラムが必要とする データは、バッファーから少しずつ取り出されることになります。 ファイルにデータを出力する場合も、バッファーが使われます。 プログラムが出力したデータは、すぐにファイルに 出力されるのではなく、バッファーに追加されます。そして、 バッファーが満杯になると、バッファーの中のデータがまとめて 出力されるのです。 出力のために使われているバッファーの内容をファイルに 出力することを、バッファーを「フラッシュする」と言います。

Q 16.1.7___ファイルをクローズするっていうのは どういうことですか。

ファイルをクローズするというのは、ファイルに対する読み込みや 出力の後始末をするということです。 ファイルをクローズすることによって何が起こるかというのは、 そのファイルが読み込みのためにオープンしたものなのか 出力のためにオープンしたものなのかということによって、 少し異なります。 読み込みのためにオープンしたファイルをクローズすると、 そのファイルの現在位置が、ファイルの終わりへ強制的に 移動させられます。 出力のためにオープンしたファイルをクローズすると、 そのファイルを指示していた出力ストリームが無効になり、さらに、 出力のために使われていたバッファーがフラッシュされます。 また、ファイルのクローズには、ファイルの使用が終了したことを オペレーティングシステムに通知するという動作も 含まれています。 実は、ひとつのプログラムがファイルをオープンすると、 オペレーティングシステムは、それ以外のプログラムが そのファイルにデータを出力することを禁止するのです。 その状態は、ファイルをオープンしたプログラムがそのファイルを クローズするまで続きます。ファイルがクローズされると、 オペレーティングシステムは、そのファイルへの出力を ほかのプログラムに対して許可します。 ですから、ファイルを扱うプログラムを書く場合は、 ファイルに対する操作が終了したら、できるだけすみやかに そのファイルをクローズして、ほかのプログラムがそのファイルに データを出力することができるようにする必要があります。

Q 16.1.8___Standard ML基本ライブラリーの中に、 テキストファイルに対する読み込みや出力を実行する関数を 含んでいるストラクチャーはあるのですか。

はい、あります。 Standard ML基本ライブラリーの中には、TextIOという ストラクチャーがあって、その中には、テキストファイルに対する 読み込みや出力を実行するさまざまな関数の定義が含まれています。

Q 16.1.9___テキストファイルを、そこからデータを 読み込むことができるようにオープンしたいときは、 どうすればいいのですか。

テキストファイルを読み込みのためにオープンしたいときは、 TextIO.openInという関数をファイルの名前に適用します。 openInは、string -> instreamという型を持つ関数です。パス名 (ファイル名、またはディレクトリ名とファイル名を 組み合わせたもの)であるような文字列にopenInを 適用すると、openInは、そのファイルを読み込みのために オープンして、そのファイルを指示する入力ストリームを 戻り値として返します。たとえば、 TextIO.openIn "oitium.txt" というように、"oitium.txt"という文字列にopenInを 適用したとすると、openInは、oitium.txtという名前のファイルを オープンして、それを指示する入力ストリームを戻り値として 返します。 openInを適用する名前は、ファイルをあらわしているパス名ならば、 どんなものでもかまいません。たとえば、 オペレーティングシステムとしてUNIXを使っていて、 /etc/hosts というファイルをオープンしたいならば、 TextIO.openIn "/etc/hosts" というように、そのパス名にopenInを適用すればいいわけです。 openInをパス名に適用したとき、そのパス名によって指定される ファイルが存在しなかった場合、または、指定されるファイルは 存在するのだけれども、そのファイルからのデータの読み込みが 許可されていないという場合、openInは、Ioという例外を 発生させます。 openInによってファイルがオープンされた時点では、ファイルの 現在位置は、ファイルに格納されている文字列の先頭の文字が 記録されている位置になっています。

Q 16.1.10___テキストファイルを、そこにデータを 出力することができるようにオープンしたいときは、 どうすればいいのですか。

テキストファイルを出力のためにオープンしたいときは、 TextIO.openOutまたはTextIO.openAppendという関数をファイルの 名前に適用します。 openOutとopenAppendは、ともに、string -> outstreamという型を 持つ関数です。パス名であるような文字列にopenOutまたは openAppendを適用すると、それらの関数は、そのファイルを 出力のためにオープンして、そのファイルの出力ストリームを 戻り値として返します。たとえば、 TextIO.openOut "clavis.txt" というように、"clavis.txt"という文字列にopenOutを 適用したとすると、openOutは、clavis.txtという名前の ファイルをオープンして、それを指示する出力ストリームを 戻り値として返します。 openOutまたはopenAppendをパス名に適用したとき、 そのパス名によって指定されるファイルが存在しなかった場合、 それらの関数は、そのパス名によって指定することのできる新しい ファイルを作って、そのファイルを指示する出力ストリームを 作ります。 openOutまたはopenAppendを適用したパス名が、存在するけれども データの出力が許可されていないファイルを指定していた場合、 それらの関数は、Ioという例外を発生させます。 openOutとopenAppendとの相違点は、ファイルをどのような 状態にするかという点にあります。openOutは、ファイルの中に 格納されていた文字列をすべて消去して、ファイルの先頭を 現在位置にします。それに対して、openAppendは、ファイルの中の 文字列をそのままにして、その末尾の文字の次の位置を 現在位置にします。ですから、これから出力する文字列だけが ファイルの内容になるようにしたいときはopenOutを使い、 ファイルの中にすでに存在している文字列の末尾に、これから 出力する文字列を連結したい、というときはopenAppendを 使えばいいわけです。

Q 16.1.11___ファイルをクローズしたいときは どうすればいいのですか。

ファイルをクローズしたいときは、TextIO.closeInまたは TextIO.closeOutという関数をストリームに適用します。 読み込みのためにオープンしたファイルをクローズするための関数が TextIO.closeInで、出力のためにオープンしたファイルを クローズするための関数がTextIO.closeOutです(型は、closeInが instream -> unitで、closeOutがoutstream -> unitです)。 入力ストリームにcloseInを適用すると、closeInは、 その入力ストリームが指示するファイルをクローズします。 たとえば、instrという識別子が入力ストリームに束縛されていて、 その入力ストリームが指示しているファイルを クローズしたいならば、 TextIO.closeIn instr というように、その入力ストリームにcloseInを適用します。 closeOutの使い方も、closeInと同様です。たとえば、outstrという 識別子が出力ストリームに束縛されていて、その出力ストリームが 指示しているファイルをクローズしたいならば、 TextIO.closeOut outstr というように、その出力ストリームにcloseOutを適用します。

16.2---読み込み

Q 16.2.1___オプション型って何ですか。

オプション型というのは、「何もない」という意味を持つデータを 既存の型に追加することによって作られた型のことです。 オプション型は、1個の型を引数として受け取る、optionという 型構成子によって作り出されます。ですから、オプション型は、 int option string option (int * string) option というような型式によってあらわされることになります。 オプション型の要素は、SOMEまたはNONEというデータ構成子によって 作り出されます。SOMEは、既存の型のデータからオプション型の データを作り出すデータ構成子です。たとえば、 - SOME 38; > val it = SOME 38 : int option というように、38という整数にSOMEを適用することによって、 SOME 38というオプション型のデータを作り出すことができます。 NONEは、引数を受け取らないデータ構成子です。NONEによって 作り出されるデータは、「何もない」ということをあらわすために 使われます。

Q 16.2.2___オプション型のデータを扱うための組み込み関数、 というようなものは存在するのですか。

はい、存在します。 MLの処理系では、オプション型のデータを容易に 扱うことができるように、getOpt、valOf、isSomeという 組み込み関数が使えるようになっています。 getOpt : 'a option * 'a -> 'a getOptは、オプション型のデータをオプション型ではないデータに 変換したい、というときに使うことができます。 getOptは、2個のデータから構成される組に適用されます。その組を 構成するデータは、1個目が、オプション型ではないデータに 変換したいオプション型のデータで、2個目が、もしも1個目の データがNONEだった場合の戻り値にするデータです。たとえば、 weaknessという識別子がstring optionという型のデータに 束縛されていて、そのデータを、オプション型ではないデータに 変換したいならば、 getOpt (weakness,"unbelievable") という式で、データの組にgetOptを適用します。もしもweaknessの 値がSOME "wife"だったとすると、getOptは戻り値として"wife"を 返します。もしもweaknessの値がNONEだった場合、戻り値は "unbelievable"になります。 valOf : 'a option -> 'a getOptと同様に、valOfも、オプション型のデータを オプション型ではないデータに変換する関数です。getOptと valOfとの相違点は、getOptの定義域が 'a option * 'aであるのに対して、valOfの定義域は 'a optionである、という点です。つまり、getOptを使うためには、 オプション型のデータがNONEだった場合の戻り値にするデータが 必要だったわけですが、valOfを使う場合、オプション型の データのほかには、どんなデータも必要ではないのです。ですから、 weaknessという識別子が束縛されているオプション型のデータを、 オプション型ではないデータに変換したいならば、 valOf weakness という式で、weaknessの値にvalOfを適用すればいい、 ということになります。もしもweaknessの値が SOME "sweets"だったとすると、valOfは戻り値として"sweets"を 返します。もしもweaknessの値がNONEだった場合、valOfは、 Optionという例外を発生させます。 isSome : 'a option -> bool isSomeは、オプション型のデータがSOMEなのか NONEなのかということを調べる述語です。オプション型のデータに isSomeを適用すると、isSomeは、そのデータがSOMEによって 作り出されたものならばtrueを返し、NONEならばfalseを返します。

Q 16.2.3___テキストファイルから文字を読み込みたいときは どうすればいいのですか。

テキストファイルからの文字の読み込みは、TextIO.input1という 関数を使うことによって実行することができます。 TextIO.input1は、instream -> char optionという型を持つ 関数です。テキストファイルを指示する入力ストリームに input1を適用すると、input1は、そのファイルの現在位置にある 文字を読み込んで、次の文字の位置へ現在位置を移動させます。 input1が返す戻り値は、読み込んだ文字にSOMEを 適用することによって作り出されたオプション型のデータです。 instrという識別子が入力ストリームに束縛されていて、 その入力ストリームが指示しているファイルの中に、 "zxc,spqr,wombat" という文字列が格納されていて、その先頭の#"z"の位置が 現在位置になっているとしましょう。このとき、 TextIO.input1 instr という式で、input1をinstrに適用したとすると、input1は、 ファイルから#"z"を読み込んで、次の文字である#"x"の位置へ 現在位置を移動させます。そして、input1は、戻り値として SOME #"z"というデータを返します。 ファイルの最後の文字が現在位置になっているときにinput1で そこから文字を読み込んだとすると、現在位置はファイルの終わりへ 移動します。現在位置がファイルの終わりになったのちにinput1で 文字を読み込もうとしても、その位置には文字がありませんので、 何も読み込むことができません。その場合、input1は、戻り値として NONEを返します。 それでは、テキストファイルの内容を読み込む関数を定義する 例として、fileToCharListという関数について 考えてみることにしましょう。fileToCharListは、 テキストファイルの名前に適用されると、そのファイルの内容を 読み込んで、それを文字のリストにして返します(型は string -> char list)。たとえば、 "Cleon II" という文字列が格納されている、emperor.txtという名前の ファイルがあるとするとき、fileToCharListをそのファイル名に 適用したとすると、fileToCharListは、 - fileToCharList "emperor.txt"; > val it = [#"C", #"l", #"e", #"o", #"n", #" ", #"I", #"I"] : char list というように、ファイルの内容を文字のリストにして返します。 fileToCharListを定義するfun宣言は、 fun fileToCharList filename = let open TextIO val instr = openIn filename fun fileToCharList1 c = if isSome c then valOf c :: fileToCharList1 (input1 instr) else [] val cl = ref [] in cl := fileToCharList1 (input1 instr); closeIn instr; !cl end というように書くことができます。

Q 16.2.4___TextIOの中には、現在位置がファイルの 終わりにあるかどうかを調べる述語もあるのですか。

はい、あります。 TextIOの中には、現在位置がファイルの終わりにあるかどうかを 調べる、endOfStreamという述語があります(型は、 instream -> bool)。テキストファイルを指示する入力ストリームに endOfStreamを適用すると、endOfStreamは、そのファイルの 現在位置がファイルの終わりならばtrueを返し、そうでなければ falseを返します。 Q 16.2.3で定義を書いたfileToCharListは、input1の戻り値を 調べることによって現在位置がファイルの終わりにあるかどうかを 判断していましたが、endOfStreamを使ってそれを 判断するということも可能です。endOfStreamを使うように fileToCharListの定義を書き直すと、 fun fileToCharList filename = let open TextIO val instr = openIn filename fun fileToCharList1 () = if endOfStream instr then [] else valOf (input1 instr) :: fileToCharList1 () val cl = ref [] in cl := fileToCharList1 (); closeIn instr; !cl end というようになります。

Q 16.2.5___テキストファイルのすべての内容を1個の文字列として 読み込む関数があると便利だと思うのですが、TextIOの中に そのような関数はあるのですか。

はい、あります。 TextIOの中には、テキストファイルのすべての内容を1個の 文字列として読み込む、inputAllという関数があります(型は instream -> string)。テキストファイルを指示する 入力ストリームにinputAllを適用すると、inputAllは、 そのファイルの現在位置にある文字から末尾の文字までを 読み込んで、現在位置をファイルの終わりへ移動させます。 inputAllが返す戻り値は、読み込んだすべての文字から構成される 文字列です。 instrという識別子が入力ストリームに束縛されていて、 その入力ストリームが指示しているファイルの中に、 "cubitis magikia" という文字列が格納されていて、その先頭の#"c"の位置が 現在位置になっているとしましょう。このとき、 TextIO.inputAll instr という式で、inputAllをinstrに適用したとすると、inputAllは、 ファイルの内容をすべて読み込んで、現在位置をファイルの終わりへ 移動させます。そして、inputAllは、 "cubitis magikia" という文字列、つまりファイルの内容を、そのまま戻り値として 返します。 Q 16.2.3で定義を書いたfileToCharListは、 fun fileToCharList filename = let open TextIO val instr = openIn filename val cl = ref [] in cl := explode (inputAll instr); closeIn instr; !cl end というように、inputAllを使って定義することも可能です。

Q 16.2.6___ファイルの現在位置を移動させないで現在位置の文字を 読み込みたいのですが、そんなときはどうすればいいのですか。

TextIO.lookaheadという関数を使えば、ファイルの現在位置を 移動させないで現在位置の文字を読み込むことができます。 TextIO.lookaheadは、instream -> char optionという型を持つ 関数です。テキストファイルを指示する入力ストリームに lookaheadを適用すると、lookaheadは、そのファイルの 現在位置にある文字を読み込んで、その文字にSOMEを 適用することによって作り出されたオプション型のデータを 返します。ですから、lookaheadの動作はinput1に よく似ています。ただし、input1は現在位置を次の文字の位置へ 移動させますが、lookaheadは現在位置を移動させません。

Q 16.2.7___行って何ですか。

行というのは、改行をまったく含まないか、または末尾に改行を 1個だけ含む文字列のことです。 たとえば、 "The Amazing Pyraminx" "Inside Rubik's Cube and Beyond\n" "\n" というような文字列は、行であると考えることができます。 文字列は、その中に含まれる改行に注目することによって、 いくつかの行に分割することができます。たとえば、 "Salvor Hardin\nLimmar Ponyets\nHober Mallow" という文字列は、その中に含まれている改行によって、 "Salvor Hardin\n"、"Limmar Ponyets\n"、"Hober Mallow"という 三つの行に分割することができます。

Q 16.2.8___TextIOの中には、テキストファイルから1個の行を 読み込む関数もあるのですか。

はい、あります。 TextIOの中には、テキストファイルから1個の行を読み込む、 inputLineという関数があります(型はinstream -> string)。 テキストファイルを指示する入力ストリームにinputLineを 適用すると、inputLineは、そのファイルの現在位置にある 文字から最も近い改行までを読み込んで、現在位置を、その改行の 次の文字の位置へ移動させます(現在位置から文字列の 末尾までのあいだに改行がまったく存在していない場合は、末尾の 文字まで読み込んで、現在位置をファイルの終わりへ 移動させます)。inputLineが返す戻り値は、読み込んだすべての 文字(改行も含みます)から構成される文字列です。 instrという識別子が入力ストリームに束縛されていて、 その入力ストリームが指示しているファイルの中に、 "euclidean space\nparallelopiped\northogonal projection\n" という文字列が格納されていて、その先頭の#"e"の位置が 現在位置になっているとしましょう。このとき、 TextIO.inputLine instr という式で、inputLineをinstrに適用したとすると、inputLineは、 先頭の文字から最初の改行までを読み込んで、その改行の次にある #"p"という文字の位置へ現在位置を移動させます。そして、 inputAllは、 "euclidean space\n" という文字列、つまりファイルの中の最初の行を、戻り値として 返します。

Q 16.2.9___テキストファイルから読み込んだ数字の列を整数や 浮動小数点数に変換したいのですが、そのような変換をしてくれる 関数はライブラリーの中に含まれているのですか。

はい、含まれています。 Standard ML基本ライブラリーには、intのデータを扱う関数から 構成される、Intという名前のストラクチャーが含まれています。 同じように、wordのデータを扱う関数から構成されるWordや、 realのデータを扱う関数から構成されるRealという ストラクチャーもあります。 Int、Word、Realというストラクチャーの中には、 Int.fromString : string -> int option Word.fromString : string -> word option Real.fromString : string -> real option という、共通の名前を持つ関数が含まれています。fromStringを 文字列に適用すると、fromStringは、その文字列を数値のデータに 変換して、そのデータにSOMEを適用した結果を返します。たとえば、 - Int.fromString "37"; > val it = SOME 37 : int option というように、"37"という文字列にInt.fromStringを適用すると、 Int.fromStringはその文字列を整数に変換して、その整数にSOMEを 適用した結果を返します。 fromStringは、数値のデータに変換することができない文字列に 適用された場合、戻り値としてNONEを返します。たとえば、 - Int.fromString "WIMP"; > val it = NONE : int option というように、"WIMP"という、整数に変換することのできない 文字列にInt.fromStringを適用した場合、Int.fromStringは 戻り値としてNONEを返すことになります。

16.3---出力

Q 16.3.1___テキストファイルに文字列を出力したいときは どうすればいいのですか。

テキストファイルに文字列を出力したいときは、 TextIO.outputという関数を使います。 TextIO.outputは、outstream * string -> unitという型を持つ 関数です。outstrがテキストファイルを指示する出力ストリームで、 sが文字列だとするとき、(outstr,s)という組にoutputを 適用すると、outputは、outstrが指示するファイルの 現在位置からうしろにsを出力して、出力した文字列の次の位置へ 現在位置を移動させます。 それでは、outputを使った関数の例として、outputStringという 関数を定義してみましょう。filenameがファイルのパス名で、sが 文字列だとするとき、(filename,s)という組にoutputStringを 適用すると、outputStringは、filenameによって指定される ファイルをopenOutでオープンして、そのファイルにsを出力して、 そしてそのファイルをクローズします(型は string * string -> unit)。 たとえば、 - outputString ("libido.txt","Principia Mathematica"); > val it = () : unit というように、2個の文字列から構成される組にoutputStringを 適用したとすると、outputStringは、libido.txtという名前の ファイルをオープンして、そのファイルに、 "Principia Mathematica" という文字列を出力して、そしてそのファイルをクローズします。 outputStringは、 fun outputString (filename,s) = let open TextIO val outstr = openOut filename in output (outstr,s); closeOut outstr end というfun宣言を書くことによって定義することができます。 outputStringを使ってファイルに文字列を出力すると、 出力した文字列だけがそのファイルの内容になります。つまり、 そのファイルの以前の内容は完全に失われてしまうわけです。 そうではなくて、ファイルに以前から格納されていた文字列と新しい 文字列とを連結した結果がファイルに格納されるようにしたい、 というときはどうすればいいのでしょうか。 outputStringは、ファイルをオープンするためにopenOutという 関数を使っていますが、openOutではなくてopenAppendを使って ファイルをオープンすることによって、ファイルの以前の内容を そのままにして、出力した文字列を以前の内容のうしろに 連結する、ということができます。ですから、 fun appendString (filename,s) = let open TextIO val outstr = openAppend filename in output (outstr,s); closeOut outstr end というように定義されたappendStringという関数は、ファイルの 以前の内容のうしろに新しい文字列を出力します。たとえば、 "The Cream of the Jest" という文字列が格納されている、tolero.txtという名前の ファイルがあるとするとき、 - appendString ("tolero.txt",", Second Edition"); > val it = () : unit というように、そのファイルのパス名と、出力したい文字列から 構成される組にappendStringを適用したとすると、ファイルの以前の 内容と、appendStringによって出力された文字列とが連結されて、 その結果としてできる、 "The Cream of the Jest, Second Edition" という文字列が、tolero.txtの内容になります。

Q 16.3.2___テキストファイルに、文字列ではなくて文字を 出力したいときはどうすればいいのですか。

テキストファイルに文字を出力したいときは、TextIO.output1という 関数を使います。 TextIO.output1は、outstream * char -> unitという型を持つ 関数です。outstrがテキストファイルを指示する出力ストリームで、 cが文字だとするとき、(outstr,c)という組にoutput1を適用すると、 output1は、outstrが指示するファイルの現在位置にcを出力して、 出力した文字の次の位置へ現在位置を移動させます。 それでは、テキストファイルへ文字を出力する関数を、 いくつか定義してみることにしましょう。 まず最初は、charListToFileという関数です。filenameがファイルの パス名で、clistが文字のリストだとするとき、 (filename,clist)という組にcharListToFileを適用すると、 charListToFileは、filenameによって指定されるファイルを openOutでオープンして、そのファイルにclistを出力して、 そしてそのファイルをクローズします(型は string * char list -> unit)。たとえば、 - charListToFile ("general.txt",[#"B", #"e", #"l", #" ", #"R", #"i", #"o", #"s", #"e"]); > val it = () : unit というように、パス名と文字のリストから構成される組に charListToFileを適用したとすると、charListToFileは、 general.txtという名前のファイルをオープンして、 そのファイルに、 "Bel Riose" という文字列を出力して、そしてそのファイルをクローズします。 charListToFileを定義するfun宣言は、 fun charListToFile (filename,clist) = let open TextIO val outstr = openOut filename fun charListToFile1 [] = () | charListToFile1 (x::xs) = ( output1 (outstr,x); charListToFile1 xs ) in charListToFile1 clist; closeOut outstr end というように書くことができます。 次に、fileToFileという関数を定義してみましょう。infilenameと outfilenameがファイルのパス名だとするとき、 (infilename,outfilename)という組にfileToFileを適用すると、 fileToFileは、infilenameによって指定されるファイルから、 outfilenameによって指定されるファイルへ、すべての文字を コピーします(型はstring * string -> unit)。たとえば、 - fileToFile ("ratio.txt","odium.txt"); > val it = () : unit というように、二つのパス名から構成される組にfileToFileを 適用したとすると、fileToFileは、ratio.txtという名前の ファイルの内容を、odium.txtという名前のファイルへ コピーします。 fileToFileを定義するfun宣言は、 fun fileToFile (infilename,outfilename) = let open TextIO val instr = openIn infilename val outstr = openOut outfilename in while not (endOfStream instr) do output1 (outstr,valOf (input1 instr)); closeIn instr; closeOut outstr end というように書くことができます。 次に、spaceToDotという関数を定義してみましょう。infilenameと outfilenameがファイルのパス名だとするとき、 (infilename,outfilename)という組にspaceToDotを適用すると、 spaceToDotは、infilenameによって指定されるファイルの 内容に対して、すべての空白をドットに変換するという処理を 実行した結果を、outfilenameによって指定されるファイルに 出力します(型はstring * string -> unit)。たとえば、 space.txtというファイルの中に、 "This string contains many spaces." という文字列が格納されているとするとき、 - spaceToDot ("space.txt","dot.txt"); > val it = () : unit というように、二つのパス名から構成される組にspaceToDotを 適用したとすると、spaceToDotは、dot.txtという名前の ファイルに、 "This........string...contains......many.......spaces." という文字列を出力します。 spaceToDotは、 fun spaceToDot (infilename,outfilename) = let fun spaceToDot1 #" " = #"." | spaceToDot1 c = c open TextIO val instr = openIn infilename val outstr = openOut outfilename in while not (endOfStream instr) do output1 (outstr, spaceToDot1 (valOf (input1 instr))); closeIn instr; closeOut outstr end というfun宣言を書くことによって定義することができます。

16.4---標準入出力

Q 16.4.1___標準入出力って何ですか。

標準入出力というのは、オペレーティングシステムによって特定の ファイルに結び付けられる、仮想的なテキストファイルのことです。 標準入出力は、「標準入力」「標準出力」「標準エラー」と呼ばれる 三つのテキストファイルの総称です。それらは特定の ファイルではなく、オペレーティングシステムによって特定の ファイルに結び付けられる、仮想的なファイルです。たとえば、 標準入力から文字を読み込むように書かれたプログラムは、 起動された時点で標準入力に結び付けられているファイルから文字を 読み込むことになります。 標準入力は読み込み専用で、標準出力と標準エラーは出力専用です。 ですから、標準入力に文字を出力したり標準出力や標準エラーから 文字を読み込むということはできません。 通常、標準入力はキーボードに、標準出力と標準エラーはモニターに 結び付けられています(キーボードやモニターも、 テキストファイルの一種であると考えることができます)。しかし、 この関係は自由に変更することが可能です。標準入出力と特定の ファイルとの結び付きを変更することを、標準入出力をファイルに 「リダイレクトする」と言います(名詞形は 「リダイレクション」)。 標準出力は、通常はモニターに結び付けられていますが、 モニター以外のファイルにリダイレクトされる頻度がきわめて高い と考えておく必要があります。ですから、たとえば エラーメッセージのように、できる限りモニターに出力したい という文字列は、標準出力ではなく標準エラーに出力するほうが いいでしょう。

Q 16.4.2___標準入力を特定のファイルにリダイレクトしたいときは どうすればいいのですか。

標準入力を特定のファイルにリダイレクトしたいときは、 プログラムを起動するコマンドに、大なり(>)と、そのファイルの パス名を追加します。 標準入力から文字列を読み込む、pantherという名前の プログラムがあるとしましょう。このpantherが、rostrum.txtという 名前のファイルから文字列を読み込むようにしたいときは、 panther < rostrum.txt というコマンドで、pantherを起動します。

Q 16.4.3___標準出力を特定のファイルにリダイレクトしたいときは どうすればいいのですか。

標準出力を特定のファイルにリダイレクトしたいときは、 プログラムを起動するコマンドに、大なり(>)または 大なり大なり(>>)と、そのファイルのパス名を追加します。 tigerという名前のプログラムがあって、このプログラムは、 標準出力に何らかの文字列を出力するように書かれている とします。このプログラムを、 tiger > gazette.txt というコマンドで起動すると、tigerが標準出力に出力した 文字列は、gazette.txtという名前のファイルに格納されます。 大なり(>)を使って標準出力を特定のファイルに リダイレクトすると、そのファイルにもとから格納されていた 文字列は消去されて、プログラムが標準出力に出力した文字列だけが そのファイルの内容になります。それに対して、大なり大なり(>>)を 使うと、ファイルにもとから格納されていた文字列は 消去されないで残り、その文字列の右側に、プログラムが出力した 文字列が連結されます。 たとえば、nutgall.txtという名前のファイルがあって、 その内容が"GPL"という文字列だとします。そして、pumaという 名前のプログラムがあって、pumaは"RMS"という文字列を標準出力に 出力するとします。このとき、 puma >> nutgall.txt というコマンドでpumaを起動すると、pumaの動作が終了したのち、 nutgall.txtの中には、"GPLRMS"という文字列が 格納されているはずです。

Q 16.4.4___キーボードから文字または文字列を読み込みたいときは どうすればいいのですか。

キーボードから文字または文字列を読み込みたいときは、 TextIO.stdInという入力ストリームを使います。 TextIO.stdInは、標準入力を指示している入力ストリームです。 通常の状態では、標準入力はキーボードに 結び付けられていますので、stdInを使うことによって、 キーボードから文字または文字列を読み込むことができます。 それでは、実際にMLの対話型システムを使って試してみましょう。 まず、標準入出力をリダイレクトしないで(つまり、ごく普通に) MLの対話型システムを起動します。そして、 - TextIO.inputLine TextIO.stdIn; と入力してみましょう。そうすると、それに対する応答が 出力されないまま、対話型システムの動作は ストップしてしまいます。これは、キーボードから何らかの行が 入力されるのをinputLineが待っている状態なのです。そこで、 たとえば、 If any of you are standing, please sit down. という行を入力したとすると、inputLineはその行を読み込んで、 読み込んだ行をそのまま戻り値として返します。ですから、 対話型システムは、 > val it = "If any of you are standing, please sit down.\n" : string という応答を出力します。

Q 16.4.5___標準出力に文字列を出力したいときは どうすればいいのですか。

標準出力に文字列を出力したいときは、組み込み関数のprintを 使います。 組み込み関数のprintというのは、実は、引数として受け取った 文字列を標準出力に出力する関数なのです。

Q 16.4.6___TextIOの中には、標準出力を指示している 出力ストリームも含まれているのですか。

はい、含まれています。 TextIOの中には、stdOutという、標準出力を指示している 出力ストリームが含まれています。ですから、 - TextIO.output (TextIO.stdOut,"Seldon Crisis\n"); Seldon Crisis > val it = () : unit というように、printの代わりにoutputを使って文字列を標準出力に 出力する、ということも可能です。

Q 16.4.7___標準エラーに文字または文字列を出力したいときは どうすればいいのですか。

標準エラーに文字または文字列を出力したいときは、 TextIO.stdErrという出力ストリームを使います。 TextIO.stdErrは、標準エラーを指示している出力ストリームです。 たとえば、 TextIO.output (TextIO.stdErr,"Kakenhi Macro\n"); Kakenhi Macro > val it = () : unit というように、出力ストリームとしてstdErrを使うことによって、 標準エラーに文字列を出力することができます。

---練習問題

16.1___ファイルのパス名に適用すると、そのファイルが 空ではないならばそれに格納されている文字列の先頭の文字にSOMEを 適用した結果を返し、ファイルが空ならばNONEを返す、 headOfFileという関数(型はstring -> char option)を定義する fun宣言を書いてください。

実行例 - TextIO.inputAll (TextIO.openIn "white.txt"); > val it = "Encyclopedia Galactica, 116th Edition" : string - headOfFile "white.txt"; > val it = SOME #"E" : char option

16.2___filenameがファイルのパス名で、cが文字だとするとき、 (filename,c)という組に適用すると、filenameによって指定される ファイルの中の文字列に含まれているcの個数を返す、 countCharという関数(型はstring * char -> int)を定義する fun宣言を書いてください。

実行例 - TextIO.inputAll (TextIO.openIn "ivory.txt"); > val it = "Programming Parlor" : string - countChar ("ivory.txt",#"r"); > val it = 4 : int

16.3___改行によって区切られた何個かの10進数から構成される 文字列を内容とするファイルのパス名に適用すると、それらの 10進数を整数に変換して加算した結果を返す、sumIntegersという 関数(型はstring -> int)を定義するfun宣言を書いてください。

実行例 - TextIO.inputAll (TextIO.openIn "snow.txt"); > val it = "473\n391\n~802\n166\n" : string - sumIntegers "snow.txt"; > val it = 228 : int

16.4___nが整数で、infilenameとoutfilenameがファイルの パス名だとするとき、(n,infilename,outfilename)という組に 適用すると、infilenameによって指定されるファイルから行を 読み込んで、n個の空白の右側にその行を連結したものを、 outfilenameによって指定されるファイルに出力する、ということを ファイルの終わりに達するまで繰り返して、ユニットを返す、 leftMarginという関数(型はint * string * string -> unit)を 定義するfun宣言を書いてください。

実行例 処理される前の文字列 David Lodge, The Art of Fiction. Wayne C. Booth, The Rhetoric of Fiction. Joseph Campbell and Bill Moyers, The Power of Myth. Mircea Eliade, Myth and Reality. 処理されたあとの文字列(nが4の場合) David Lodge, The Art of Fiction. Wayne C. Booth, The Rhetoric of Fiction. Joseph Campbell and Bill Moyers, The Power of Myth. Mircea Eliade, Myth and Reality.

16.5___infilenameとoutfilenameがファイルのパス名だとするとき、 (infilename,outfilename)という組に適用すると、 infilenameによって指定されるファイルから行を読み込んで、 その行の番号とその行を、outfilenameによって指定される ファイルに出力する、ということをファイルの終わりに達するまで 繰り返して、ユニットを返す、addLineNumberという関数(型は string * string -> unit)を定義するfun宣言を書いてください。

実行例 処理される前の文字列 Life with Namako: A Guide for Everyone Tanaka's Commentary on Namako The Namako Programming Environment D Window System User's Guide for D11 Release 4 処理されたあとの文字列 1 Life with Namako: A Guide for Everyone 2 Tanaka's Commentary on Namako 3 The Namako Programming Environment 4 D Window System User's Guide for D11 Release 4

16.6___infilenameとoutfilenameがファイルのパス名だとするとき、 (infilename,outfilename)という組に適用すると、 infilenameによって指定されるファイルから文字を読み込んで、 その文字と1個の空白を、outfilenameによって指定されるファイルに 出力する、ということをファイルの終わりに達するまで繰り返して、 ユニットを返す、insertSpaceという関数(型は string * string -> unit)を定義するfun宣言を書いてください。

実行例 - TextIO.inputAll (TextIO.openIn "oldlace.txt"); > val it = "Gormenghast" : string - insertSpace ("oldlace.txt","seashell.txt"); > val it = () : unit - TextIO.inputAll (TextIO.openIn "seashell.txt"); > val it = "G o r m e n g h a s t " : string

16.7___nが整数で、infilenameとoutfilenameがファイルの パス名だとするとき、(n,infilename,outfilename)という組に 適用すると、infilenameによって指定されるファイルに 格納されている文字列のn番目の文字を角括弧([])で 囲むことによってできる文字列を、outfilenameによって指定される ファイルに出力して、ユニットを返す、encloseNthCharという 関数(型はint * string * string -> unit)を定義するfun宣言を 書いてください。

実行例 - TextIO.inputAll (TextIO.openIn "beige.txt"); > val it = "intertextuality" : string - encloseNthChar (8,"beige.txt","honeydew.txt"); > val it = () : unit - TextIO.inputAll (TextIO.openIn "honeydew.txt"); > val it = "interte[x]tuality" : string

16.8___左丸括弧または右丸括弧が、丸括弧の対によって何重に 囲まれているかということを示す0またはプラスの整数のことを、 その丸括弧の「深さ」と呼ぶことにします。たとえば、 pink,(orchid,(crimson),(bisque,(red,(brown)),maroon)),wheat という文字列の中にある、brownという単語の左右にある丸括弧は、 丸括弧の対によって3重に囲まれていますので、深さは 3だということになります。 infilenameとoutfilenameがファイルのパス名だとするとき、 (infilename,outfilename)という組に適用すると、 infilenameによって指定されるファイルに格納されている 文字列の中に含まれているそれぞれの丸括弧の右側に、 その丸括弧の深さを示す整数を挿入することによってできる 文字列を、outfilenameによって指定されるファイルに出力して、 ユニットを返す、parenDepthという関数(型は string * string -> unit)を定義するfun宣言を書いてください。

実行例 処理される前の文字列 moccasin,(khaki,(salmon,(tan,(tomato,peru),(coral, (orange,chocolate)),(sienna,(yellow,(olive,green), lime),aquamarine),turquoise),(cyan,(teal,(navy, violet)),purple),(blue,plum)),indigo,(thistle, magenta)),fuchsia 処理されたあとの文字列 moccasin,(0khaki,(1salmon,(2tan,(3tomato,peru)3,(3coral, (4orange,chocolate)4)3,(3sienna,(4yellow,(5olive,green)5, lime)4,aquamarine)3,turquoise)2,(2cyan,(3teal,(4navy, violet)4)3,purple)2,(2blue,plum)2)1,indigo,(1thistle, magenta)1)0,fuchsia

16.9___filenameがファイルのパス名だとするとき、filenameに 適用すると、標準入力から1個の行を読み込んで、filenameによって 指定されるファイルにその行を出力して、ユニットを返す、 stdInToFileという関数(型はstring -> unit)を定義するfun宣言を 書いてください。

実行例 - stdInToFile "linen.txt"; 5001: The Odyssey Forever > val it = () : unit - TextIO.inputAll (TextIO.openIn "linen.txt"); > val it = "5001: The Odyssey Forever\n" : string

16.10___ファイルのパス名に適用すると、そのファイルに 格納されている文字列を読み込んで、その文字列を標準エラーに 出力して、ユニットを返す、fileToStdErrという関数(型は string -> unit)を定義するfun宣言を書いてください。

実行例 - TextIO.inputAll (TextIO.openIn "azure.txt"); > val it = "The Ringworld Architects\n" : string - fileToStdErr "azure.txt"; The Ringworld Architects > val it = () : unit

第17章===ベクトルと配列

17.1---ベクトル

Q 17.1.1___ベクトルって何ですか。

ベクトルというのは、同じ型を持つ0個以上のデータを一列に 並べることによってできるデータの一種です。 ベクトルは、それを構成する要素の型にvectorという型構成子を 適用することによって作り出される型を持ちます。たとえば、 何個かの整数を並べることによってひとつのベクトルを 作ったとすると、そのベクトルは、int vectorという型を 持つことになります。 ベクトルを構成するデータは、先頭から数えて何番目にあるか ということを示す番号を持っています。ベクトルの先頭の要素は 0番目という番号を持ち、そのひとつ右の要素は1番目という番号を 持ちます。ベクトルは、要素の番号を指定することによって、それを 構成する要素のひとつをダイレクトに取り出すことができる、という 性質を持っています。 ベクトルは、0個以上のデータの列であるという点で、リストに よく似ています。ベクトルとリストとの最大の相違点は、要素を 取り出す操作がダイレクトかどうか、というところにあります。 ベクトルから要素を取り出す操作は、それが何番目の要素であっても ダイレクトなものですが、それに対して、リストから要素を取り出す 操作は、先頭の要素だけはダイレクトですが、それ以外の 要素については、リストを頭部と尾部に分解するという操作が何回か 必要になりますので、ダイレクトなものとは言えません。 なお、ベクトルを構成する要素の個数のことを、ベクトルの「長さ」 と言います。

Q 17.1.2___データを組み合わせることによってベクトルを 作りたいときはどうすればいいのですか。

データを組み合わせることによってベクトルを作りたいときは、 #[ 式 , 式 , …… , 式 ] という形の式をコンピュータに評価させます。そうすると、その式の 中のそれぞれの式の値から構成されるベクトルができて、 そのベクトルが式全体の値として得られます。 たとえば、 #[271,~508,~332,946] という式をコンピュータに評価させると、271、~508、~332、 946という4個の整数がこの順序で並んでいるベクトル(型は int vector)ができて、そのベクトルがこの式の値になります。

Q 17.1.3___ひとつのリストがあって、そのリストと同じ要素から 構成されるベクトルを作りたいのですが、そんなときは どうすればいいのですか。

リストをベクトルに変換したいときは、vectorという関数を 使います。 vectorは、'a list -> 'a vectorという型を持つ関数です。 vectorをリストに適用すると、vectorは、そのリストを構成する 要素を同じ順序で並べることによってできるベクトルを作って、 その結果を返します。vectorを使うことによって、たとえば、 - vector [118,503,~292,678,~433]; > val it = #[118, 503, ~292, 678, ~433] : int vector - vector ["S","SB","E","Ir"]; > val it = #["S", "SB", "E", "Ir"] : string vector - vector [#[5,1,3],#[7,2,9,1]]; > val it = #[#[5, 1, 3], #[7, 2, 9, 1]] : int vector vector - vector (tl [533]); > val it = #[] : int vector というように、リストからベクトルを作り出すことができます。

Q 17.1.4___一定の規則にしたがってデータが並んでいるような ベクトルを作りたいのですが、そんなときは どうすればいいのですか。

データが規則的に並んでいるようなベクトルを作りたいときは、 Vector.tabulateという関数を使います。 Standard ML基本ライブラリーの中には、Vectorという名前の ストラクチャーがあって、その中には、ベクトルを扱うときに 役に立つさまざまな関数の定義が含まれています。tabulateも、 そのひとつです。 tabulateは、int * (int -> 'a) -> 'a vectorという型を持つ 関数です。tabulateが受け取る引数は、ベクトルの長さをあらわす 整数と、データを作り出す規則をあらわす関数、という二つの データから構成される組です。 データを作り出す規則は、定義域がintである関数か、または 定義域の型式が1個の型変数であるような多相型関数によって あらわされます。たとえば、 int -> int int -> string 'a -> string 'a -> 'a * 'a というような型を持つ関数で、規則を指定することができます。 lenが長さで、ruleが規則だとするとき、(len,rule)という組に tabulateを適用すると、tabulateは、0、1、2、3、……、 len-1という整数のそれぞれにruleを適用したときの戻り値から 構成されるベクトルを作って、そのベクトルを戻り値として 返します。tabulateを使うことによって、たとえば、 - Vector.tabulate (10,fn n => n); > val it = #[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] : int vector - Vector.tabulate (8,fn n => n*3); > val it = #[0, 3, 6, 9, 12, 15, 18, 21] : int vector - Vector.tabulate (7,fn _ => 0); > val it = #[0, 0, 0, 0, 0, 0, 0] : int vector - Vector.tabulate (5,fn _ => "pH"); > val it = #["pH", "pH", "pH", "pH", "pH"] : string vector - fun nSlants n = if n>=1 then "/" ^ nSlants (n-1) else ""; > val nSlants = fn : int -> string - Vector.tabulate (4,nSlants); > val it = #["", "/", "//", "///"] : string vector というように、データが規則的に並んでいるベクトルを 作ることができます。 lenが、マイナスであるか、または、その長さのベクトルを 作ることができないほど大きな整数であるときに、(len,rule)という 組にtabulateを適用したとすると、tabulateは、Sizeという例外を 発生させます。 作ることのできるもっとも長いベクトルの長さは、環境によって 決定されます。現在の環境で作ることのできるもっとも長い ベクトルの長さは、Vectorというストラクチャーの中にある maxLenという識別子を評価することによって 調べることができます。もしも、 - Vector.maxLen; > val it = 6371904 : int という結果が得られたとすれば、作ることのできるもっとも長い ベクトルの長さは6371904である、ということです。

Q 17.1.5___ベクトルを連結することによってひとつのベクトルを 作りたいのですが、そんなときはどうすればいいのですか。

ベクトルを連結したいときは、Vector.concatという関数を 使います。 Vector.concatは、'a vector list -> 'a vectorという型を持つ 関数です。ベクトルを連結したいときは、連結したい順序で それらのベクトルを並べることによってできるリストにconcatを 適用します。たとえば、 #[38,27,53] #[44,10] #[16,61,33] という3個のベクトルを、この順序で連結したいならば、 - Vector.concat [#[38,27,53],#[44,10],#[16,61,33]]; > val it = #[38, 27, 53, 44, 10, 16, 61, 33] : int vector というように、それらのベクトルから構成されるリストに concatを適用すればいいわけです。 それでは、concatを利用して関数を定義する例として、 fileToVectorという関数を作ってみましょう。 fileToVectorは、ファイルに格納されている10進数の列を 読み込んで、それを整数のベクトルに変換する関数です(型は string -> int vector)。たとえば、numbers.txtという名前の ファイルがあって、その中に、 "387\n61074\n~3011\n24\n1837\n" というように、改行で区切られた10進数から構成される文字列が 格納されているとするとき、fileToVectorを"numbers.txt"に 適用すると、fileToVectorは、 - fileToVector "numbers.txt"; > val it = #[387, 61074, ~3011, 24, 1837] : int vector というように、読み込んだ10進数を整数に変換した結果から 構成されるベクトルを返します。 fileToVectorは、concatを利用することによって、 fun fileToVector filename = let open TextIO fun inputInteger instr = getOpt (Int.fromString (inputLine instr),0) val v = ref #[] val instr = openIn filename in while not (endOfStream instr) do v := Vector.concat [!v,#[inputInteger instr]]; !v end というように定義することができます。

Q 17.1.6___ベクトルの長さを求めたいときは どうすればいいのですか。

ベクトルの長さを求めたいときは、Vector.lengthという関数を 使います。 Vector.lengthは、ベクトルに適用されると、そのベクトルの長さを 求めて、その結果を返す関数です(型は'a vector -> int)。 ですから、ベクトルの長さを調べたいときは、 - Vector.length #["UMa","UMi","Crv","CrB","CrA","CVn","Del"]; > val it = 7 : int というように、Vector.lengthを使えばいいわけです。

Q 17.1.7___要素の番号を指定することによって、ベクトルから 要素を取り出したいのですが、そんなときは どうすればいいのですか。

番号を指定してベクトルから要素を取り出したいときは、 Vector.subという関数を使います。 Vector.subは、'a vector * int -> 'aという型を持つ関数です。 vがベクトルで、iが0またはプラスの整数だとするとき、 (v,i)という組にsubを適用すると、subは、vのi番目の要素を 戻り値として返します。たとえば、unitという識別子が、 #["AU","nm","nT","hPa","Hz","ppm","yen","pt"] というベクトルに束縛されているとするとき、unitから3番目の 要素を取り出したいならば、 - Vector.sub (unit,3); > val it = "hPa" : string というように、unitと3から構成される組にsubを 適用すればいいわけです。 ベクトルにi番目の要素が存在しない場合(iがマイナスであるか、 または、iがベクトルの長さと等しいか、または、iのほうが ベクトルの長さよりも大きい場合)、subは、Subscriptという例外を 発生させます。たとえば、beltという識別子が、 #["Mintaka","Alnilam","Alnitak"] という、長さが3のベクトルに束縛されているとするとき、beltの ~1番目の要素とか3番目の要素というのは存在しませんので、 Vector.sub (belt,~1) Vector.sub (belt,3) という式は、どちらも、Subscriptという例外を 発生させることになります。 それでは、subを使って関数を定義する例として、 isMemberという関数を定義してみることにしましょう。 vが'a vectorという型のベクトルで、xが'aという型の データだとするとき、(v,x)という組にisMemberを適用すると、 isMemberは、vの中にxが要素として含まれているならばtrue、 そうでなければfalseを返します(型は ''a vector * ''a -> bool)。つまり、isMemberというのは、 - isMember (#["Babe","Ferdinand","Thelonius"],"Ferdinand"); > val it = true : bool - isMember (#["Babe","Ferdinand","Thelonius"],"Randolph"); > val it = false : bool というような動作をする関数です。 ベクトルvの中に要素xが含まれているかどうかというのは、 ● vの末尾に、要素としてxを追加する。 ● vの先頭から順番に、その要素とxとを比較していく、という 繰り返しを実行する。xと等しい要素が見つかったならば、 繰り返しを終了する。 というアルゴリズムによって調べることができます。見つかった 要素の位置がvの末尾ならば、vの中にxは含まれていない ということになり、vの末尾よりも左側ならば、vの中にxが 含まれていた、ということになります。 このように、データの列の中で特定のデータを探索するという アルゴリズムでは、探索の対象になっているデータを列の末尾に 追加することによって、繰り返しが列の末尾でかならず 停止するようにする、という手法が使われます。 この場合に列の末尾に追加されたデータは、普通、「番兵」 と呼ばれます。 isMemberを定義するfun宣言は、 fun isMember (v,x) = let open Vector val v1 = concat [v,#[x]] val ir = ref 0 in while sub (v1,!ir) <> x do ir := !ir + 1; !ir < length v end というように書くことができます。

Q 17.1.8___ベクトルから一部分を取り出すことによってできる ベクトルを作りたいのですが、そんなときは どうすればいいのですか。

ベクトルから一部分を取り出したいときは、Vector.extractという 関数を使います。 Vector.extractは、 'a vector * int * int option -> 'a vectorという型を持つ 関数です。extractを使うことによってベクトルから一部分を 取り出したいときは、そのベクトルと、取り出したい部分の先頭の 要素の番号と、そして取り出したい部分の要素の個数にSOMEを 適用した結果、という3個のデータから構成される組にextractを 適用します。そうすると、extractは、指定された部分を ベクトルから取り出して、その結果を戻り値として返します。 たとえば、latexという識別子が、 #["rm","bf","it","sl","pi","xi","mu","le","ge","in","ni"] というベクトルに束縛されているとするとき、latexの4番目、 5番目、6番目という3個の要素から構成されるベクトルをlatexから 取り出したいならば、(latex,4,SOME 3)という組にextractを 適用します。そうすると、extractは、 - Vector.extract (latex,4,SOME 3); > val it = #["pi", "xi", "mu"] : string vector というように、latexの一部分を取り出して、その結果を返します。 extractは、引数として受け取った組の3個目の要素がNONEだった 場合は、指定された位置から末尾までの要素によって構成される ベクトルを返します。たとえば、unixという識別子が、 #["ls","cp","mv","rm","su","vi","ps","od","wc"] というベクトルに束縛されているとするとき、(unix,4,NONE)という 組にextractを適用すると、extractは、 - Vector.extract (unix,4,NONE); > val it = #["su", "vi", "ps", "od", "wc"] : string vector というように、unixの4番目から末尾までの要素によって構成される ベクトルを返します。 extractは、存在しない要素が指定された場合は、Subscriptという 例外を発生させます。たとえば、interroという識別子が、 #["quis","qui","ubi","quo","quando","cur","quare"] という長さが6のベクトルに束縛されているとき、 (interro,5,SOME 3)という組にextractを適用したとすると、 extractは、interroの5番目、6番目、7番目の要素から構成される ベクトルを返さないといけないわけですが、interroには7番目の 要素というのは存在しません。ですから、この場合、extractは Subscriptという例外を発生させることになります。

17.2---配列

Q 17.2.1___配列って何ですか。

配列というのは、記憶場所の列のことです。 MLでは、同一の型を持つ0個以上のデータを記憶することのできる 記憶場所の列、というものを作ることができるようになっています。 そのような記憶場所の列は、「配列」と呼ばれます。 配列を構成する記憶場所の個数のことを、配列の「長さ」と 呼びます。 配列を構成するそれぞれの記憶場所は、0から始まる番号によって 識別されます。たとえば、長さが5である配列のそれぞれの 記憶場所は、0、1、2、3、4という番号を持っています。 記憶場所を扱うためには、その場所を指示するデータが必要です。 配列というのは記憶場所の列ですから、それを扱う場合もやはり、 それを指示するデータを必要とします。配列を指示するデータは、 その配列のそれぞれの記憶場所が記憶することのできるデータの型に arrayという型構成子を適用することによって作り出される型を 持ちます。たとえば、整数を記憶することのできる配列を指示する データは、int arrayという型を持つことになります。

Q 17.2.2___配列を作りたいときはどうすればいいのですか。

配列を作る方法はいくつかあるのですが、それらのうちでもっとも 基本的なのは、Array.arrayという関数を使うという方法です。 Standard ML基本ライブラリーの中のArrayという ストラクチャーには、配列を扱うさまざまな関数の定義が 含まれています。Arrayの中には、配列を作るための関数も いくつか含まれていて、arrayもそのひとつです。 arrayは、int * 'a -> 'a arrayという型を持つ関数です。lenが 0またはプラスの整数で、initが何らかのデータだとするとき、 (len,init)という組にarrayを適用すると、arrayは、長さがlenの 配列を作って、そのそれぞれの記憶場所にinitを格納します。 そして、戻り値として、その配列を指示するデータを返します。 たとえば、 val sevenStrings = Array.array (7,"Iapetus") というval宣言を書くことによって、長さが7の配列を作って、 そのそれぞれの記憶場所に"Iapetus"を格納して、 sevenStringsという識別子を、その配列を指示するデータ(型は string array)に束縛する、ということができます。 arrayを(len,init)に適用したとき、lenが、マイナスであるか、 または、作ることのできるもっとも長い配列の長さよりも 大きかったならば、arrayは、Sizeという例外を発生させます。 Arrayというストラクチャーの中には、maxLenという 識別子があって、それは、作ることのできるもっとも長い配列の 長さに束縛されています。

Q 17.2.3___ひとつのリストがあって、そのリストの要素が それぞれの記憶場所に記憶されている配列を作りたいのですが、 そんなときはどうすればいいのですか。

リストの要素を記憶している配列を作りたいときは、 Array.fromListという関数を使います。 Array.fromListは、'a list -> 'a arrayという型を持つ関数です。 fromListをリストに適用すると、fromListは、そのリストと同じ 長さの配列を作って、そのそれぞれの記憶場所に、リストを構成する それぞれの要素を格納します。そして、その配列を指示するデータを 戻り値として返します。たとえば、 val autumnRect = Array.fromList ["Markab","Scheat","Algenib","Alpheratz"] というval宣言を書くことによって、長さが4の配列を作って、 0番目の記憶場所に"Markab"、1番目の記憶場所に"Scheat"、2番目の 記憶場所に"Algenib"、3番目の記憶場所に"Alpheratz"を 格納することができます。

Q 17.2.4___一定の規則にしたがって作り出されたデータの列が 格納されている配列を作りたいのですが、そんなときは どうすればいいのですか。

一定の規則にしたがって作り出されたデータの列が格納されている 配列を作りたいときは、Array.tabulateという関数を使います。 Array.tabulateは、int * (int -> 'a) -> 'a arrayという型を持つ 関数です。lenが整数で、ruleが、整数に適用することのできる 関数だとするとき、(len,rule)という組にtabulateを適用すると、 tabulateは、長さがlenの配列を作って、記憶場所の番号にruleを 適用した結果を、それぞれの記憶場所に格納します。そして、 戻り値として、その配列を指示するデータを返します。たとえば、 val fourIntegers = Array.tabulate (4,fn n => (n+1)*5) というval宣言を書くことによって、0番目の記憶場所に5、1番目の 記憶場所に10、2番目の記憶場所に15、3番目の記憶場所に20が 格納されている配列を作って、fourIntegersという識別子を、 その配列を指示するデータに束縛することができます。

Q 17.2.5___配列の長さを求めたいときはどうすればいいのですか。

配列の長さを求めたいときは、Array.lengthという関数を使います。 Array.lengthは、'a array -> intという型を持つ関数です。 配列を指示するデータにlengthを適用すると、lengthは、その配列の 長さを求めて、その結果を返します。たとえば、 val thirtyBools = Array.array (30,false) というval宣言で、thirtyBoolsという識別子が、配列を指示する データに束縛されているとするとき、lengthをthirtyBoolsに 適用すると、lengthは、 - Array.length thirtyBools; > val it = 30 : int というように、その配列の長さを返します。

Q 17.2.6___nが0またはプラスの整数だとするとき、配列のn番目の 記憶場所から、そこに記憶されているデータを 取り出したいのですが、そんなときはどうすればいいのですか。

配列のn番目の記憶場所からデータを取り出したいときは、 Array.subという関数を使います。 Array.subは、'a array * int -> 'aという型を持つ関数です。 aが配列を指示するデータで、iが0またはプラスの 整数だとするとき、(a,i)という組にsubを適用すると、subは、aの i番目の記憶場所から、そこに格納されているデータを取り出して、 そのデータを戻り値として返します。たとえば、 val extension = Array.fromList ["txt","tex","eps","wrl","bat","sml"] というval宣言で作られた配列の3番目の記憶場所からデータを 取り出したいならば、(extension,3)という組にsubを 適用します。そうすることによって、 - Array.sub (extension,3); > val it = "wrl" : string というように、3番目の記憶場所に格納されている"wrl"という データが得られます。 Array.subは、指定された番号を持つ記憶場所が配列の中に 存在しない場合は、Subscriptという例外を発生させます。

Q 17.2.7___配列から1個以上のデータを取り出して、 それらのデータから構成されるベクトルを作りたいのですが、 そんなときはどうすればいいのですか。

配列に格納されているデータをベクトルの形で取り出したいときは、 Array.extractという関数を使います。 Array.extractは、 'a array * int * int option -> 'a arrayという型を持つ 関数です。aが配列を指示するデータで、iとlenが0またはプラスの 整数だとするとき、(a,i,SOME len)という組にextractを 適用すると、extractは、aのi番目、i+1番目、i+2番目、……、 i+len-1番目の記憶場所からデータを取り出して、 それらのデータから構成されるベクトルを戻り値として返します。 たとえば、 val ps = Array.fromList ["dup","exch","moveto","lineto","arc","dict","def"] というval宣言によって作られた配列の2番目、3番目、4番目の 記憶場所からデータを取り出して、それらのデータから構成される ベクトルを作りたいならば、 - Array.extract (ps,2,SOME 3); > val it = #["moveto", "lineto", "arc"] : string vector というようにextractを使います。 extractを使うことによって、「指定した場所から末尾まで」という 形で記憶場所を指定して配列からデータを取り出す、ということも 可能です。aが配列を指示するデータで、iが0またはプラスの データだとするとき、(a,i,NONE)という組にextractを適用すると、 extractは、aのi番目から末尾までの記憶場所からデータを 取り出して、それらのデータから構成されるベクトルを返します。 たとえば、 val edu = Array.fromList ["umd","uiowa","ukans","du","cmu","mit","uiuc","umn"] というval宣言によって作られた配列があるとするとき、 (edu,4,NONE)という組にextractを適用すると、 - Array.extract (edu,4,NONE); > val it = #["cmu", "mit", "uiuc", "umn"] : string vector というように、extractは、eduの4番目から末尾までの記憶場所から データを取り出して、それらのデータから構成されるベクトルを 返します。 Array.extractは、存在しない記憶場所が指定された場合は、 Subscriptという例外を発生させます。

Q 17.2.8___配列の中の記憶場所に格納されているデータを別の データに置き換えたい(つまり、配列の中の記憶場所にデータを 代入したい)ときは、どうすればいいのですか。

配列の中の記憶場所にデータを代入したいときは、 Array.updateという関数を使います。 Array.updateは、'a array * int * 'a -> unitという型を持つ 関数です。aが配列を指示するデータだとするとき、aのi番目の 記憶場所にxというデータを代入したいならば、(a,i,x)という 組にupdateを適用します。たとえば、 val nineStrings = Array.array (9,"") というval宣言によって作られた配列の6番目の記憶場所に "Tethys"という文字列を代入したいならば、 - Array.update (nineStrings,6,"Tethys"); > val it = () : unit というようにupdateを使います。 Array.updateは、指定された番号を持つ記憶場所が配列の中に 存在しない場合は、Subscriptという例外を発生させます。 それでは、updateを使う関数の例として、frequencyという 関数(型はint vector -> int vector)を 定義してみることにしましょう。vが、0から9までの10種類の整数を 使って作ることのできるベクトルだとするとき、frequencyをvに 適用すると、frequencyは、0から9までのそれぞれの整数がvの中に 含まれている個数を数えて、それらの個数から構成されるベクトルを 返します。たとえば、 #[3,8,7,3,8,7,3,7,0,7,8,8,0,3,7,3,0,7,0,7] というベクトルの中には、0が4個、3が5個、7が7個、8が4個、 含まれていますので、このベクトルにfrequencyを適用すると、 frequencyは、 #[4,0,0,5,0,0,0,7,4,0] というベクトルを返します。 ベクトルに含まれている0から9までの整数の個数を数えるためには、 まず、長さが10の配列を作って、そのそれぞれの記憶場所に0を 格納しておく必要があります。そこで、 val freq = Array.array (10,0) というval宣言で、配列を作ったとしましょう。次に必要なことは、 引数として受け取ったベクトルを構成するそれぞれの整数について、 その整数を番号とするfreqの記憶場所に対して1を加算する、 ということです。たとえば、ベクトルの中に3という整数が 含まれていたならば、freqの3番目の記憶場所に格納されている 整数を1だけ大きくします。ベクトルを構成するすべての 整数についてその操作が完了すると、配列のそれぞれの 記憶場所には、ベクトルの中に含まれていたそれぞれの整数の個数が 格納されているはずです。ですから、最後に、ベクトルの形で 配列からデータを取り出して、それを戻り値として返せばいい、 ということになります。 frequencyを定義するfun宣言は、 fun frequency v = let open Array fun count (a,i) = update (a,i,sub (a,i)+1) val vlen = Vector.length v val freq = array (10,0) val ir = ref 0 in while !ir&lt;vlen do ( count (freq,Vector.sub (v,!ir)); ir := !ir + 1 ); extract (freq,0,NONE) end というように書くことができます。

---練習問題

17.1___ベクトルに適用すると、それを構成するそれぞれの要素を 逆の順序に並べ換えたベクトルを返す、reverseという関数(型は 'a vector -> 'a vector)を定義するfun宣言を書いてください。

実行例 - reverse #["Flick","Atta","Dot","Heimlich","Molt"]; > val it = #["Molt", "Heimlich", "Dot", "Atta", "Flick"] : string vector

17.2___fが'a * 'b -> 'cという型の関数だとするとき、fに 適用すると、「vaが'a vectorという型のベクトルで、vbが 'b vectorという型のベクトルだとするとき、(va,vb)という組に 適用すると、同じ番号を持つvaの要素とvbの要素から構成される組に fを適用した結果から構成されるベクトルを返す関数(型は 'a vector * 'b vector -> 'c vector)」を返す、 unifyVectorsという関数(型は ('a * 'b -> 'c) -> 'a vector * 'b vector -> 'c vector)を 定義するカリー化形式のfun宣言を書いてください。なお、vaの 長さとvbの長さとが等しくない場合は、NotSameLengthという例外を 発生させるようにしてください。

実行例 - unifyVectors op + (#[38,14,73,56],#[22,18,63,84]); > val it = #[60, 32, 136, 140] : int vector - unifyVectors op ^ (#["NE","NW","SE","SW"],#["bE","bN","bW", "bS"]); > val it = #["NEbE", "NWbN", "SEbW", "SWbS"] : string vector - unifyVectors (fn (x,y) => (x,y)) (#["Postlethwaite", "Griffiths","Thornton","Jarvis"],#[61,87,55,72]); > val it = #[("Postlethwaite", 61), ("Griffiths", 87), ("Thornton", 55), ("Jarvis", 72)] : (string * int) vector

17.3___vがベクトルで、divisorがプラスの整数だとするとき、 (v,divisor)という組に適用すると、vを、長さがlenの何個かの ベクトルと、divisorよりも短い1個のベクトルに分割して(vの 長さがdivisorの倍数である場合、長さはすべて divisorになります)、それらのベクトルから構成されるベクトルを 返す、divideという関数(型は 'a vector * int -> 'a vector vector)を定義するfun宣言を 書いてください。なお、divisorが0またはマイナスだった場合は、 NotPlusという例外を発生させるようにしてください。

実行例 - divide (#["Del","Her","Hya","Ori","Peg","And","Aql","Cyg", "Crv","Cas","Per","CrB","Oph","Aur","Cep"],4); > val it = #[#["Del", "Her", "Hya", "Ori"], #["Peg", "And", "Aql", "Cyg"], #["Crv", "Cas", "Per", "CrB"], #["Oph", "Aur", "Cep"]] : string vector vector

17.4___rとcが0またはプラスの整数で、fがint -> int -> 'aという 型を持つ関数だとするとき、(r,c,f)という組に適用すると、 #[#[f 0 0, f 0 1, f 0 2, …… , f 0 c-1], #[f 1 0, f 1 1, f 1 2, …… , f 1 c-1], #[f 2 0, f 2 1, f 2 2, …… , f 2 c-1], …… #[f r-1 0, f r-1 1, f r-1 2, …… , f r-1 c-1]] というベクトルを返す、matrixTabulateという関数(型は int * int * (int -> int -> 'a) -> 'a vector vector)を 定義するfun宣言を書いてください。

実行例 - matrixTabulate (5,6,fn n => fn m => n*m); > val it = #[#[0, 0, 0, 0, 0, 0], #[0, 1, 2, 3, 4, 5], #[0, 2, 4, 6, 8, 10], #[0, 3, 6, 9, 12, 15], #[0, 4, 8, 12, 16, 20]] : int vector vector - matrixTabulate (4,4,fn n => fn m => if n=m then 1 else 0); > val it = #[#[1, 0, 0, 0], #[0, 1, 0, 0], #[0, 0, 1, 0], #[0, 0, 0, 1]] : int vector vector

17.5___vが''a vectorという型のベクトルで、xとyが''aという型の データだとするとき、(v,x,y)という組に適用すると、vの中に 含まれているすべてのxをyに置き換えることによってできる ベクトルを返す、replaceという関数(型は ''a vector * ''a * ''a -> ''a vector)を定義するfun宣言を 書いてください。

実行例 - replace (#["Fe","Au","Au","Fe","Fe","Au","Fe"],"Au","Ca"); > val it = #["Fe", "Ca", "Ca", "Fe", "Fe", "Ca", "Fe"] : string vector

17.6___fが'a -> boolという型の関数だとするとき、fに 適用すると、「vが'a vectorという型のベクトルだとするとき、vに 適用すると、vの中に含まれている要素のうちで、fを適用した結果が trueになるものの個数を返す関数(型は'a vector -> int)」を 返す、countという関数(型は ('a -> bool) -> 'a vector -> int)を定義するカリー化形式の fun宣言を書いてください。

実行例 - count (fn x => x>50) #[21,38,84,19,33,72,26,39,65,17,44]; > val it = 3 : int - count (fn x => x mod 2 = 0) #[21,38,84,19,33,72,26,39,65,17,44]; > val it = 5 : int - count (fn x => x="Cu") #["Ag","Cu","Ag","Cu","Cu","Cu", "Ag","Ag","Cu","Cu","Ag","Ag"]; > val it = 6 : int

17.7___fが'a -> boolという型の関数だとするとき、fに 適用すると、「vが'a vectorという型のベクトルだとするとき、vに 適用すると、vの中に含まれている要素のうちで、fを適用した結果が trueになるもののみから構成されるベクトルを返す関数(型は 'a vector -> 'a vector)」を返す、filterという関数(型は ('a -> bool) -> 'a vector -> 'a vector)を定義する カリー化形式のfun宣言を書いてください。

実行例 - filter (fn x => x>50) #[21,38,84,19,33,72,26,39,65,17,44]; > val it = #[84, 72, 65] : int vector - filter (fn x => x mod 2 = 0) #[21,38,84,19,33,72,26,39,65,17,44]; > val it = #[38, 84, 72, 26, 44] : int vector - filter (fn x => x="Cu") #["Ag","Cu","Ag","Cu","Cu","Cu", "Ag","Ag","Cu","Cu","Ag","Ag"]; > val it = #["Cu", "Cu", "Cu", "Cu", "Cu", "Cu"] : string vector

17.8___ベクトルに適用すると、そのベクトルのそれぞれの要素が 格納されている配列を作って、その配列を指示するデータを返す、 VectorToArrayという関数(型は'a vector -> 'a array)を 定義するfun宣言を書いてください。

実行例 - Array.extract (VectorToArray (#[43,22,18,64,53],0,NONE)); > val it = #[43, 22, 18, 64, 53] : int vector

17.9___100点満点の試験の点数から構成されるベクトルに 適用すると、0点から9点まで、10点から19点まで、20点から 29点まで、……、90点から100点まで、というそれぞれの区間に 該当する点数の個数から構成されるベクトルを返す、 distributionという関数(型はint vector -> int vector)を 定義するfun宣言を書いてください。なお、引数として受け取った ベクトルの中に、0よりも小さいかまたは100よりも大きい点数が 含まれていた場合は、その要素の番号を例外引数として伴う Rangeという例外を発生させるようにしてください。

実行例 - distribution #[58,63,52,56,47,50,58,64,43,51,74,59,63]; > val it = #[0, 0, 0, 0, 2, 7, 3, 1, 0, 0] : int vector

17.10___0から9までの10種類の整数を使って作ることのできる ベクトルに適用すると、10種類の整数のうちでそのベクトルの中に まったく含まれていないものから構成されるベクトルを返す、 unusedという関数(型はint vector -> int vector)を定義する fun宣言を書いてください。なお、引数として受け取った ベクトルの中に、0よりも小さいかまたは9よりも大きい整数が 含まれていた場合は、その要素の番号を例外引数として伴う Rangeという例外を発生させるようにしてください。

実行例 - unused #[2,5,9,5,8,0,8,2,0,5,9,1,2,5,0,1,5,8,5]; > val it = #[3, 4, 6, 7] : int vector

17.11___nが2以上の整数だとするとき、nに適用すると、2から nまでの整数のうちで素数であるもののみから構成されるベクトルを 返す、primeNumbersという関数(型はint -> int vector)を 定義するfun宣言を書いてください。なお、nが2よりも小さかった 場合はLessThanTwoという例外を発生させるようにしてください。

実行例 - primeNumbers 20; > val it = #[2, 3, 5, 7, 11, 13, 17, 19] : int vector [ヒント] 2からnまでの整数のうちで素数であるものをすべて求めるための アルゴリズムとしては、「エラトステネスのふるい」 と呼ばれるものがよく使われます。これは、 ● ○または×の印を記入することのできるn-1個の欄の列を作って、 それぞれの欄に、2、3、4、……、nという番号を付ける。 ● まだ何の印も記入されていない欄のうちで、番号がもっとも 小さいものに○印を記入して、列の中にあるその番号の倍数の番号を 持つすべての欄に×印を記入する、ということを、列の中のすべての 欄にどちらかの印が記入されるまで繰り返す。 というアルゴリズムです。このアルゴリズムが停止した時点で、 ○印が記入されている欄の番号は、すべて素数になっています。 ちなみに、エラトステネスというのは人名で、紀元前275年に 生まれて紀元前194年に没したアレキサンドリアの数学者です。 彼は、地球の大きさをはじめて測定した人物として よく知られています。

17.12___nが0またはプラスの整数だとするとき、nを10進数で あらわしたときのそれぞれの桁を、1の桁を0番目に、10の桁を 1番目に、100の桁を2番目に、という順序で 並べることによってできるベクトルのことを、nの 「正規10進数ベクトル」と呼ぶことにします。 0またはプラスの整数に適用すると、それを正規10進数ベクトルに 変換した結果を返す、intToDecimalという関数(型は int -> int vector)を定義するfun宣言を書いてください。 なお、nがマイナスだった場合は、Minusという例外を 発生させるようにしてください。

実行例 - intToDecimal 67209438; > val it = #[8, 3, 4, 9, 0, 2, 7, 6] : int vector

17.13___dが、0またはプラスの整数から構成される ベクトルで、nが、 sub (d,0) * 1 + sub (d,1) * 10 + sub (d,2) * 100 …… という計算の結果だとするとき、dのことを、nの「10進数ベクトル」 と呼ぶことにします。10進数ベクトルは、10以上の整数を 要素として含んでいてもかまいません。したがって、 練習問題17.12で定義した正規10進数ベクトルというのは、 どの要素も0以上かつ9以下であるような、特殊な 10進数ベクトルのことだということになります。 10進数ベクトルに適用すると、それと同じ数をあらわす 正規10進数ベクトルを返す、normalizeという関数(型は int vector -> int vector)を定義するfun宣言を書いてください。 なお、引数として受け取ったベクトルの中にマイナスの整数が 含まれていた場合は、その要素の番号を例外引数として伴う Minusという例外を発生させるようにしてください。

10進数ベクトルは、intのデータではあらわすことができないような 大きな整数を表現することも可能です。ですから、normalizeを 定義するときは、10進数ベクトルの全体をintのデータに 変換することができない場合もある、という点に注意する 必要があります。 実行例 - normalize #[45,63,28,79]; > val it = #[5, 7, 4, 2, 8] : int vector

17.14___daとdbが、練習問題17.13で定義した 10進数ベクトルだとするとき、(da,db)という組に適用すると、 daがあらわしている整数とdaがあらわしている整数とを加算した 結果をあらわす正規10進数ベクトルを返す、addという関数(型は int vector * int vector -> int vector)を定義するfun宣言を 書いてください。なお、daがマイナスの整数を含んでいた場合は MinusAという例外を発生させ、dbがマイナスの整数を含んでいた 場合はMinusBという例外を発生させるようにしてください。

実行例 - add (#[9,2,8,4,7,9,4],#[6,8,3,1,9,7]); > val it = #[5, 1, 2, 6, 6, 7, 5] : int vector

17.15___daとdbが、練習問題17.13で定義した 10進数ベクトルだとするとき、(da,db)という組に適用すると、 daがあらわしている整数とdaがあらわしている整数とを加算した 結果をあらわす正規10進数ベクトルを返す、multiplyという 関数(型はint vector * int vector -> int vector)を定義する fun宣言を書いてください。なお、daがマイナスの整数を含んでいた 場合はMinusAという例外を発生させ、dbがマイナスの整数を 含んでいた場合はMinusBという例外を 発生させるようにしてください。

実行例 - multiply (#[9,1,2,6],#[8,5,7]); > val it = #[2, 0, 0, 4, 1, 7, 4] : int vector

第18章===レコード

18.1---レコードの表現

Q 18.1.1___レコードって何ですか。

レコードというのは、識別子または数字の列と、データとを 結び付けたものを要素とする集合のことです。 レコードというのは集合の一種です。レコードを構成する要素は、 「フィールド」と呼ばれます。1個のフィールドは、1個の 識別子または数字の列と、1個のデータから構成されます。 フィールドを構成する識別子または数字の列は、フィールドの 「ラベル」と呼ばれ、フィールドを構成するデータは、フィールドの 「値」と呼ばれます。 フィールドのラベルは、レコードから特定のデータを取り出すときの 目印として機能しますので、1個のレコードに含まれるそれぞれの フィールドは、互いに異なったラベルから構成されている 必要があります。つまり、同一のラベルを持つ2個以上の フィールドを含んでいるレコードを作ることはできない、 ということです。ちなみに、フィールドの値のほうにはそのような 制約はありませんので、同一の値を持つ2個以上のフィールドを 含んでいるレコードを作るというのは可能です。 1個のレコードに含まれるそれぞれのフィールドの値は、型が 同じでなくてもかまいません。ですから、たとえば、整数の値を 持つフィールドと文字列の値を持つフィールドから構成される レコードを作る、ということも可能です。 レコードは、1個のデータとして扱うことができます。ですから、 レコードを要素として含むリストやベクトルやレコードを作ったり、 レコードに適用することのできる関数を定義したり、レコードを 戻り値として返す関数を定義したりすることができます。 レコードは、どのようなフィールドから構成されているか ということによって異なる型を持つのですが、それらの型は、 総称して「レコード型」と呼ばれます。

Q 18.1.2___データを組み合わせることによってレコードを 作りたいときはどうすればいいのですか。

データを組み合わせることによってレコードを作りたいときは、 { ラベル = 式 , ラベル = 式 , …… , ラベル = 式 } という形の式をコンピュータに評価させます。そうすると、 その式の中に書かれたラベルと式の値から構成される レコードができて、そのレコードが式全体の値として得られます。 レコードを作る式の中に書く、 ラベル = 式 という形のものは、「ラベル」のところに書かれた識別子または 数字をラベルとして持ち、「式」のところに書かれた式の値を 値として持つフィールドを意味しています。たとえば、 author="Mervyn Peake" という記述は、ラベルがauthorで値が"Mervyn Peake"である フィールドをあらわしていて、 61647=0.00191 という記述は、ラベルが61647で値が0.00191であるフィールドを あらわしています。 フィールドをあらわす記述をコンマで区切って並べて、その全体を 中括弧で囲めば、それがレコードを作る式になります。たとえば、 {name="Shakespeare",birth=1564,death=1616} という式をコンピュータに評価させると、ラベルがnameで値が "Shakespeare"であるフィールド、ラベルがbirthで値が1564である フィールド、ラベルがdeathで値が1616であるフィールド、という 三つのフィールドから構成されるレコードができて、そのレコードが この式の値になります。 なお、レコードというのは列ではなくて集合ですから、上に書いた 式と、 {birth=1564,death=1616,name="Shakespeare"} という、フィールドの順序だけが異なる式とは、同じレコードを あらわすことになります。

Q 18.1.3___レコード型は、どのような型式によって あらわされるのですか。

レコード型は、 { ラベル : 型式 , ラベル : 型式 , …… , ラベル : 型式 } という形の型式によってあらわされます。 レコード型をあらわす型式に含まれる、 ラベル : 型式 という形のものは、レコードを構成するそれぞれのフィールドの ラベルと、そのフィールドの値の型をあらわしています。たとえば、 height:real という記述は、heightというラベルを持つフィールドがあって、 そのフィールドの値の型はrealである、ということを あらわしています。 レコード型は、その型のレコードがどのようなフィールドから 構成されているのかということをすべて記述することによって あらわされます。たとえば、 {name="Stevenson",birth=1850,death=1894} というレコードの型は、 {name:string,birth:int,death:int} という型式によってあらわされます。 なお、レコードを作る式の場合と同じように、レコード型を あらわす型式の場合も、フィールドを並べる順序は意味を 持ちませんので、上の型式は、 {death:int,birth:int,name:string} と書いても、同じ意味になります。

Q 18.1.4___レコードから特定のフィールドの値を 取り出したいときはどうすればいいのですか。

レコードから特定のフィールドの値を 取り出したいときは、「レコード選択子」と呼ばれるものを 使います。 レコード選択子というのは、 # 識別子または数字の列 という構文を持つ記述のことです。たとえば、 #distance #5876 というようなものは、レコード選択子だとみなすことができます。 レコードから特定のフィールドの値を取り出したいときは、 そのレコードを値とする式の左側にレコード選択子を書きます。 つまり、 # 識別子または数字の列 式 という形のものを書くわけです。この形の記述は、式として 評価することができます。この式をコンピュータに評価させると、 コンピュータは、まずその中の式を評価して、その値として得られた レコードから、指定された識別子または数字の列をラベルとして持つ フィールドの値を取り出して、そして、そのフィールドの値を、 式全体の値にします。たとえば、 #minute {hour=22,minute=41,second=19} という式を評価すると、41という整数が値として得られます。 レコードからフィールドの値を取り出すための方法としては、 レコード選択子を使うという方法のほかに、パターンと照合する という方法もあります。レコードと一致するパターンの 書き方については、第18.2節で説明します。

Q 18.1.5___組がレコードの一種だというのは本当ですか。

はい、本当です。 組というのは、実は、定められた条件を満足する特殊な レコードのことなのです。レコードが組であるために 満足しないといけない条件というのは、そのレコードを構成する フィールドの個数をnとすると、 ● 識別子をラベルとして持つフィールドを含まない。 ● フィールドのラベルが、それぞれ、1、2、3、4、……、nという、 1からスタートする連続する整数に対応している。 というものです。たとえば、 {1=2704,2=5.8017,3= #"M",4="Setaria glauca",5=8833} というレコードは、条件を満足していますので、組だと みなすことができます。それに対して、 {1=5243,2=9.0011,3= #"Y",4="Solidago altissima",price=4831} というレコードは、priceという識別子をラベルとして持つ フィールドを含んでいますので、組ではありません。同じように、 {2=7008,3=1.1411,4= #"T",5="Veronica persica",6=1234} というレコードは、ラベルが1からスタートしていませんので、 やはり、組ではありません。同じように、 {1=3131,2=8.0808,3= #"H",5="Lycoris radiata",6=9876} というレコードは、4というラベルを持つフィールドがないために、 整数が連続していませんので、やはり、 組ではないということになります。 データを組み合わせることによって組を作るための構文、つまり、 ( 式1 , 式2 , 式3 , …… , 式n ) という形の式は、 { 1 = 式1 , 2 = 式2 , 3 = 式3 , …… , n = 式n } という、レコードを作る式の略記法だと考えることができます。 たとえば、 (383,"Corona Borealis",0.00041,#"+",2.2212) という式は、 {1=383,2="Corona Borealis",3=0.00041,4= #"+",5=2.2212} という式と同じ意味を持つことになります。 型式についても同じように、 型式1 * 型式2 * 型式3 * …… * 型式n という、積型をあらわす型式は、 { 1 : 型式1 , 2 : 型式2 , 3 : 型式3 , …… , n : 型式n } という、レコード型をあらわす型式の略記法だと みなすことができます。たとえば、 int * string * real * int * char という型式は、 {1:int,2:string,3:real,4:int,5:char} という型式と同じ意味になります。

18.2---レコードのパターン

Q 18.2.1___レコードからフィールドを取り出したいのですが、 レコード選択子を使う以外に、そのための方法はないのですか。

いいえ、あります。レコードからフィールドを取り出したいときは、 パターンとレコードとを照合するという方法も使うことができます。 レコードの中のフィールドは、 ラベル = パターン という形のパターンと照合することができます。この形のパターンと フィールドとの照合は、それぞれのラベルが一致して、かつ、 イコールの右側に書かれたパターンとフィールドの値との照合が 成功した場合にのみ成功します。 パターンとレコードとを照合することによってレコードから フィールドを取り出したいときは、フィールドと 照合することのできるパターンをコンマで区切って並べて、 その全体を中括弧で囲むことによってできるパターン、つまり、 { ラベル = パターン , …… , ラベル = パターン } という形のパターンを、レコードと照合します。たとえば、 {name="peru",red=205,green=133,blue=63} というレコードからフィールドを取り出したいならば、 {name=n,red=r,green=g,blue=b} というようなパターンと、そのレコードとを照合します。すると、 パターンの中のそれぞれのフィールドが、それに対応する レコードの中のフィールドと照合されて、rという識別子が "peru"に、rという識別子が205に、gという識別子が133に、bという 識別子が63に、それぞれ束縛されます。なお、パターンと レコードとを照合するとき、パターンに含まれるラベルの集合と、 レコードに含まれるラベルの集合とは、同一でないといけません。 ですから、 {name=n,red=r,green=g,blue=b,favor=f} というパターンとか、 {red=r,green=g,blue=b} というパターンと、 {name="brown",red=165,green=42,blue=42} というレコードとを照合する、ということはできません。 それでは、レコードのパターンを使う例として、 populationDensityという関数を定義してみることにしましょう。 町の名前と人口と面積から構成されるレコード(型は {name:string,population:int,area:real})にpopulationDensityを 適用すると、populationDensityは、その人口と面積から人口密度を 求めて、町の名前と人口密度から構成されるレコード(型は {name:string,density:real})を返します。つまり、 populationDensityは、 - populationDensity {name="Pamekasan",population=700, area=1400.0}; > val it = {density = 0.5, name = "Pamekasan"} : {density : real, name : string} というような動作をする、ということです。 populationDensityを定義するfun宣言は、 fun populationDensity {name=n,population=p,area=a} = {name=n,density=real p / a} と書くことができます。

Q 18.2.2___パターンとレコードとの照合によってレコードから フィールドを取り出すとき、ラベルとして使われている識別子と 同じ識別子を、フィールドの値に束縛するために使うことは 可能ですか。

はい、可能です。 レコードのパターンの中に、フィールドと照合される パターンとして、 識別子 = 識別子 という形のものを書く場合、イコールの両側の識別子は、 同一のものであってもかまいません。つまり、 {name=name,red=red,green=green,blue=blue} というようなパターンを書いてもかまわないのです。 このパターンと、 {name="salmon",red=250,green=128,blue=114} というレコードとを照合すると、nameという識別子が"salmon"に、 redという識別子250に、greenという識別子が128に、blueという 識別子が114に、それぞれ束縛されます。 なお、ラベルと同一の識別子をフィールドの値に束縛したいという 場合には、 識別子 = 識別子 という形で、左右の識別子が同一になっているパターンを 書くことになるわけですが、そのようなパターンには、1個の 識別子を書くだけ、という省略形があります。つまり、 weight=weight というような、イコールの左右に同一の識別子が書かれている パターンを書く代わりに、 weight という省略形を書いてもいい、ということです。ですから、 {name,red,green,blue} というパターンと、 {name="coral",red=255,green=127,blue=80} というレコードとを照合すると、nameという識別子が"coral"に、 redという識別子が255に、greenという識別子が127に、blueという 識別子が80に、それぞれ束縛されます。

Q 18.2.3___パターンとレコードとの照合によって、レコードを 構成する何個かのフィールドのうちのいくつかを 取り出したいのですが、必要ではないフィールドについても パターンを書かないといけないのですか。

いいえ、必要ではないフィールドについては、パターンを 省略することができます。 レコードからフィールドを取り出すためのパターンを書くとき、 そのレコードを構成するフィールドのいくつかが不必要ならば、 それらのフィールドのパターンは省略してもかまいません。ただし、 フィールドのパターンを省略する場合は、そのことを あらわすために、「ワイルドカード」と呼ばれる3個の ドット(...)を、右中括弧の左側に書く必要があります。たとえば、 {name="aliceblue",red=240,green=248,blue=255} というレコードからnameというラベルを持つフィールドだけを 取り出したいというときは、redとgreenとblueのパターンを 省略して、 {name=n,...} というパターンを書けばいい、ということになります。 このように、レコードのパターンを書くとき、ワイルドカードを 使うことによって、必要ではないフィールドのパターンを 省略することができるわけなのですが、ただし、ワイルドカードを 使って関数を定義するときには、ひとつだけ、 注意しないといけないことがあります。それは、関数の型を 決定することができなくなるようなワイルドカードの 使い方をするとエラーになってしまう、ということです。たとえば、 fun getName {name=n,...} = n というfun宣言を書くことによって、nameというラベルを持つ フィールドをレコードから取り出す関数を 定義することができそうに思われますが、実際には、このfun宣言は エラーになってしまいます。その理由は、MLの処理系にはこの関数の 定義域の型を決定することができないからです。 それでは、ワイルドカードを使った関数の例として、 percentageOfVictoryという関数を定義してみることにしましょう。 この関数は、チームの名前、すべての試合の回数、勝った試合の 回数から構成されるレコード(型は {name:string,total:int,victory:int})に適用されると、 その勝率を求めて、チームの名前と、勝率にSOMEを適用した結果から 構成されるレコード(型は {name:string,percentage:real option})を返します。ただし、 すべての試合の回数が0だった場合は、NONEを勝率とするレコードを 返します。つまり、percentageOfVictoryというのは、 - percentageOfVictory {name="Cranes",total=30,victory=6}; > val it = {name = "Cranes", percentage = SOME 0.2} : {name : string, percentage : real option} - percentageOfVictory {name="Trepangs",total=0,victory=0}; > val it = {name = "Trepangs", percentage = NONE} : {name : string, percentage : real option} というような動作をする関数だということです。 percentageOfVictoryは、 fun percentageOfVictory {name,total=0,...} = {name=name,percentage=NONE} | percentageOfVictory {name,total,victory} = {name=name,percentage=SOME (real victory / real total)} というfun宣言を書くことによって定義することができます。 totalというフィールドが0だった場合、victoryというフィールドの 値は必要ではなくなりますので、totalが0の場合に照合を成功させる パターンは、 {name,total=0,...} というように、ワイルドカードを使うことによって、victoryの フィールドを省略することができます。MLの処理系が このfun宣言によって定義される関数の定義域の型を 決定することができる理由は、もうひとつのパターンとして、 {name,total,victory} という完全な形のパターンが書かれているからです。

---練習問題

18.1___stが、学生の番号と名前から構成されるレコードを 要素とするリスト(型は{stnum:int,stname:string} list)で、nが 整数だとするとき、(st,n)という組に適用すると、番号がnである 学生の名前を返す、lookupStudentという関数を定義するfun宣言を 書いてください。なお、番号がnである学生を 見つけることができなかった場合は、NotFoundという例外を 発生させるようにしてください。

実行例 - lookupStudent ([{stnum=7, stname="McDougall"}, {stnum=9, stname="Sechenov"}, {stnum=5, stname="Zajonc"}, {stnum=6, stname="Tajfel"}],5); > val it = "Zajonc" : string

18.2___takeが、学生がどの科目を受講しているかということを あらわす、科目の番号と学生の番号から構成されるレコードを 要素とするリスト(型は{subnum:int,stnum:int} list)で、nが 整数だとするとき、(take,n)という組に適用すると、番号がnである 科目を受講しているすべての学生の番号から構成されるリスト(型は int list)を返す、extractSubjectという関数を定義するfun宣言を 書いてください。

実行例 - extractSubject ([{subnum=14,stnum=5},{subnum=6,stnum=8}, {subnum=14,stnum=7},{subnum=6,stnum=5},{subnum=14,stnum=2}, {subnum=6,stnum=10},{subnum=14,stnum=12},{subnum=14,stnum=3}, {subnum=6,stnum=7}],6); > val it = [8, 5, 10, 7] : int list

18.3___stが、学生の番号と名前から構成されるレコードを 要素とするリスト(型は{stnum:int,stname:string} list)で、 numlistが、学生の番号を要素とするリスト(型は int list)だとするとき、(st,numlist)という組に適用すると、 numlistに番号が含まれているすべての学生について、その番号と 名前から構成されるレコードを作って、それらのレコードを 要素とするリスト(型は{stnum:int,stname:string} list)を返す、 rollWithNameという関数を定義するfun宣言を、練習問題18.1で 定義したlookupStudentを使って書いてください。

実行例 - rollWithName ([{stnum=14, stname="Schwenk"}, {stnum=11, stname="Harary"}, {stnum=2, stname="Dossey"}, {stnum=1, stname="Mendelson"}, {stnum=13, stname="Macrakis"}, {stnum=8, stname="Hietaniemi"}, {stnum=3, stname="Wirzenius"}, {stnum=10, stname="Luckenbach"}, {stnum=4, stname="Brittenham"}, {stnum=12, stname="Abarabich"}], [3,11,2,8,14]); > val it = [{stnum=3, stname="Wirzenius"}, {stnum=11, stname="Harary"}, {stnum=2, stname="Dossey"}, {stnum=8, stname="Hietaniemi"}, {stnum=14, stname="Schwenk"}] : {stnum : int, stname : string} list

18.4___takeが、学生がどの科目を受講しているかということを あらわす、科目の番号と学生の番号から構成されるレコードを 要素とするリスト(型は {subnum:int,stnum:int} list)だとするとき、takeに適用すると、 takeに番号が含まれているすべての科目について、その番号と、 それを受講しているすべての学生の番号を要素とするリストから 構成されるレコードを作って、それらのレコードを要素とする リスト(型は{stnum:int,stlist:int list} list)を返す、 allRollsという関数を定義するfun宣言を、練習問題18.2で定義した extractSubjectを使って書いてください。

実行例 - allRolls [{subnum=12, stnum=3}, {subnum=8, stnum=7}, {subnum=15, stnum=6}, {subnum=11, stnum=3}, {subnum=10, stnum=9}, {subnum=10, stnum=7}, {subnum=10, stnum=6}, {subnum=12, stnum=9}, {subnum=12, stnum=1}, {subnum=15, stnum=4}, {subnum=11, stnum=2}, {subnum=8, stnum=11}, {subnum=12, stnum=5}, {subnum=11, stnum=10}, {subnum=10, stnum=12}, {subnum=8, stnum=14}]; > val it = [{subnum = 12, stlist = [3, 9, 1, 5]}, {subnum = 8, stlist = [7, 11, 14]}, {subnum = 15, stlist = [6, 4]}, {subnum = 11, stlist = [3, 2, 10]}, {subnum = 10, stlist = [9, 7, 6, 12]}] : {subnum : int, stlist : int list} list

18.5___学生の番号と試験の点数から構成されるレコード(型は {stnum:int,mark:int})を要素とするリストに適用すると、 学生の番号と順位(点数のもっとも高い学生が1、その次が2、 という整数)から構成されるレコード(型は {stnum:int,ranking:int})を要素とするリストを返す、 examRankingという関数を定義するfun宣言を書いてください。

実行例 - examRanking [{stnum=3, mark=65}, {stnum=10, mark=27}, {stnum=14, mark=31}, {stnum=2, mark=16}, {stnum=1, mark=58}, {stnum=11, mark=31}, {stnum=8, mark=72}]; > val it = [{stnum = 3, ranking = 2}, {stnum = 10, ranking = 6}, {stnum = 14, ranking = 4}, {stnum = 2, ranking = 7}, {stnum = 1, ranking = 3}, {stnum = 11, ranking = 4}, {stnum = 8, ranking = 1}] : {stnum : int, ranking : int} list

第19章===ビット列

19.1---ビット列の操作

Q 19.1.1___wordという型を持つデータは、長さがいくらの ビット列のパターンなんですか。

型がwordであるデータが何ビットのビット列のパターンなのか というのは、ハードウェアや処理系などの環境によって 決定されます。 MLでは、ビット列のパターンを、wordという型を持つデータとして 扱うことができます。ただし、そのデータの長さが何ビットなのか というのは、固定ではなくて、プログラムが動作する環境によって 変化します。 現在の環境でwordのデータが何ビットのビット列の パターンなのかということを調べたいときは、 Standard ML基本ライブラリーのWordというストラクチャーの 中にある、wordSizeという識別子を使います。この識別子は、 現在の環境でのwordの長さ(型はint)に束縛されていますので、 その識別子を評価することによって、wordのデータの長さを 知ることができます。

Q 19.1.2___ビット演算って何ですか。

ビット演算というのは、ビットの状態(0または1)の集合の直積から ビットの状態の集合への写像のことです。 B1、B2、B3、……、Bnがビットの状態だとするとき、 (B1,B2,B3,……,Bn)という列に対して1個のビットの状態を 対応させる写像のことを、「ビット演算」と言います。 よく使われるビット演算の例としては、AND、OR、XOR、 NOTなどがあります。 AND、OR、XORは、2個のビットの状態に対して1個のビットの 状態を対応させる写像です。AとBがビットの状態の 集合だとするとき、AとBの直積の要素を、AND、OR、XORによって 写した像は、それぞれ、 A B AND OR XOR 0 0 0 0 0 0 1 0 1 1 1 0 0 1 1 1 1 1 1 0 というようになります。つまり、ANDは両方が1ならば1でそれ以外は 0、ORはどちらかが1ならば1で両方が0のときだけ0、XORは両者が 異なっているならば1で同じならば0、という写像です。 NOTは、1個のビットの状態に対して1個のビットの状態を対応させる 写像です。Aがビットの状態の集合だとするとき、Aの要素を NOTによって写した像は、 A NOT 0 1 1 0 というようになります。つまり、NOTというのは、ビットの状態を 反転させる写像のことです。

Q 19.1.3___MLで、ビット列を構成するそれぞれのビットに対して ANDやORなどのビット演算を実行して、その結果から構成される ビット列のパターンを求める、という動作を記述したいのですが、 そんなときはどうすればいいのですか。

ビット列に対するビット演算をMLで記述したいときは、 Standard ML基本ライブラリーのWordの中で定義されている そのための関数を使います。 Standard ML基本ライブラリーのWordの中には、ビット列を構成する それぞれのビットに対してAND、OR、XOR、NOTを実行する、 andb : word * word -> word orb : word * word -> word xorb : word * word -> word notb : word -> word という関数が含まれています。 aとbがwordのデータだとするとき、(a,b)という組にandb、orb、 xorbを適用すると、それらの関数は、aを構成するそれぞれの ビットの状態と、bを構成するそれと同じ位置のビットの 状態に対してビット演算を実行して、その結果を 並べることによってできるビット列のパターンを戻り値として 返します。たとえば、wordの長さが16だと仮定して、aとbが、 a 0110101101001101 (0wx6B4D) b 0000011111100000 (0wx7E0) というビット列のパターンだとするとき、(a,b)に、andb、orb、 xorbを適用すると、それらの関数は、それぞれ、 andb (a,b) 0000001101000000 (0wx340) orb (a,b) 0110111111101101 (0wx6FED) xorb (a,b) 0110110010101101 (0wx6CAD) というビット列のパターンを戻り値として返します。 wordのデータにnotbを適用すると、notbは、そのデータを構成する それぞれのビットの状態を反転させることによってできるビット列の パターンを戻り値として返します。たとえば、wordの長さが16だと 仮定して、 0000000010111000 (0wxB8) というビット列のパターンにnotbを適用すると、notbは、 1111111101000111 (0wxFF47) というビット列のパターンを戻り値として返します。

Q 19.1.4___シフトって何ですか。

シフトというのは、ビット列のパターンを、右または左に移動させる 操作のことです。 ビット列のパターンを右へ移動させる操作のことを「右シフト」 と呼び、左へ移動させる操作のことを「左シフト」と呼びます。 nが0またはプラスの整数だとするとき、ビット列のパターンを nビットだけ右へシフトするというのは、そのビット列に対して 相対的に、そのパターンを右へnビットだけ移動させる ということです。たとえば、 パターン 1101011000101110 ビット列 XXXXXXXXXXXXXXXX というビット列のパターンを右へ5ビットだけシフトする というのは、 パターン 1101011000101110 ビット列 XXXXXXXXXXXXXXXX というようにパターンを移動させる操作です。ただし、ビット列から はみ出した5ビットのパターンは失われ、空欄になったビットは 0という状態で埋められますので、 パターン 0000011010110001 ビット列 XXXXXXXXXXXXXXXX というパターンが、シフトの結果として得られることになります。 左シフトというのは、移動の方向が異なる以外は、右シフトと 同じようにパターンを移動させる操作のことです。

Q 19.1.5___MLで、ビット列のパターンをシフトさせるという動作を 記述したいのですが、そんなときはどうすればいいのですか。

シフトをMLで記述したいときは、Standard ML基本ライブラリーの Wordの中で定義されているそのための関数を使います。 Standard ML基本ライブラリーのWordの中には、ビット列の パターンをシフトさせる、 >> : word * word -> word << : word * word -> word という関数が含まれています。 wとnがwordのデータだとするとき、(w,n)という組にWord.>>を 適用すると、Word.>>は、wのパターンを右へnビットだけ シフトして、その結果を戻り値として返します。たとえば、wordの 長さが16だと仮定して、wが、 1101011000101110 (0wxD62E) というビット列のパターンで、nが0wx5だとするとき、(w,n)に Word.>>を適用すると、Word.>>は、wを5ビットだけ右へシフトした 結果、つまり、 0000011010110001 (0wx6B1) というビット列のパターンを戻り値として返します。 同じように、Word.<<は、ビット列のパターンを左へシフトさせる 関数です。たとえば、wordの長さが16だと仮定して、wが、 1101011000101110 (0wxD62E) というビット列のパターンで、nが0wx9だとするとき、(w,n)に Word.<<を適用すると、Word.<<は、 0101110000000000 (0wx5C00) というように、wを9ビットだけ左へシフトした結果を 返すことになります。

Q 19.1.6___word以外の型のデータをwordのデータに変換したり、 wordのデータをword以外の型のデータに変換したいときは、 どうすればいいのですか。

wordとそれ以外の型とのあいだでデータの変換を実行したいときは、 Standard ML基本ライブラリーのWordの中で定義されている そのための関数を使います。 Standard ML基本ライブラリーのWordの中には、wordとそれ以外の 型とのあいだでデータの変換を実行する、 fromInt : int -> word toInt : word -> int fromString : string -> word option toString : word -> string というような関数が含まれています。これらの関数を 使うことによって、 - Word.fromInt 6; > val it = 0wx6 : word - Word.toInt 0wxC; > val it = 12 : int - Word.fromString "BAD"; > val it = SOME 0wxBAD : word option - Word.fromString "GOOD"; > val it = NONE : word option - Word.toString 0wxA1CF; > val it = "A1CF" : string というように、word以外の型のデータをwordのデータに変換したり、 wordのデータをword以外の型のデータに変換したりする、 ということができます。

Q 19.1.7___1の補数って何ですか。

nがマイナスの整数だとするとき、nの1の補数というのは、nの 絶対値をあらわすビット列のパターンを構成するそれぞれのビットの 状態を反転させることによってできるパターンのことです。 マイナスの整数をビット列で表現するための方法のひとつとして、 1の補数を使うという方法があります。1の補数でマイナスの整数を 表現する場合、まず、ビット列の左端のビットが0の場合はプラスの 整数をあらわし、1の場合はマイナスの整数をあらわすと 決めておきます。そして、マイナスの整数は、その絶対値を あらわしているパターンを構成するそれぞれのビットを 反転させることによってできるパターンで表現します。 たとえば、1の補数を使って、長さが16のビット列で~5を表現したい としましょう。その絶対値である5は、 0000000000000101 というパターンで表現されますので、~5は、それぞれのビットを 反転させた結果、つまり、 1111111111111010 というパターンで表現されることになります。 1の補数を使ってマイナスの整数を表現するという方法には、 すべてのビットが0であるパターンと、すべてのビットが1である パターン、つまり、 0000000000000000 と 1111111111111111 という二とおりのパターンが、両方とも0という整数を 表現することになる、という問題点があります。

Q 19.1.8___2の補数って何ですか。

nがマイナスの整数だとするとき、nの2の補数というのは、 nの絶対値よりも1だけ小さい整数をあらわすビット列のパターンを 構成するそれぞれのビットの状態を反転させることによってできる パターンのことです。 2の補数も、マイナスの整数をビット列で表現するために 使うことができます。2の補数でマイナスの整数を表現する場合も、 1の補数の場合と同じように、ビット列の左端のビットが0の場合は プラスの整数をあらわし、1の場合はマイナスの整数をあらわす と決めておく必要があります。そして、マイナスの整数は、 その絶対値よりも1だけ小さい整数をあらわしている パターンを構成するそれぞれのビットを 反転させることによってできるパターンで表現します。 それでは、2の補数を使って、長さが16のビット列で~5を表現する パターンを求めてみましょう。まず、~5の絶対値は5で、それよりも 1だけ小さい整数は4ですから、まず、4を表現するビット列の パターン、つまり、 0000000000000100 というパターンを作ります。そして次に、それぞれのビットを 反転させるという操作を実行します。すると、その結果として 得られた、 1111111111111011 というパターンが、~5を2の補数で表現しているビット列の パターン、ということになります。 2の補数を使ってマイナスの整数を表現する場合、0を表現するのは、 ビット列を構成するすべてのビットが0のパターン、つまり、 0000000000000000 というパターンだけです。すべてのビットが1になっている、 1111111111111111 というパターンは、~1という整数をあらわすことになります。

Q 19.1.9___マイナスの整数をあらわすintのデータに対して Word.fromIntを適用した場合、Word.fromIntはどのようなデータを 返すのですか。

Word.fromIntをマイナスの整数に適用すると、Word.fromIntは、 その整数を2の補数で表現しているビット列のパターンを返します。 たとえば、wordの長さが16だと仮定して、Word.fromIntを~9に 適用すると、Word.fromIntは、 - Word.fromInt ~9; > val it = 0wxFFFF7 : word というように、2の補数で~9を表現しているビット列のパターンを 返します。 ちなみに、Word.toIntは、受け取った引数の左端のビットが1だった 場合は、それが2の補数でマイナスの整数を表現していると 解釈して、その結果を返します。ですから、wordの長さが16だと 仮定すると、0wxFFFF7という、左端のビットが1であるような データにWord.toIntを適用すると、Word.toIntは、 - Word.toInt 0wxFFFF7; > val it = ~9 : int というように、マイナスの整数を戻り値として返します。

Q 19.1.10___算術右シフトって何ですか。

算術右シフトというのは、左端のビットの状態だけは 移動させないで、パターンが右へ移動することによって空欄になった ビットを、左端のビットと同じ状態で埋める、という特殊な 右シフトのことです。 それでは、例として、 パターン 1101011000101110 ビット列 XXXXXXXXXXXXXXXX というビット列のパターンを、5ビットだけ算術右シフトさせる、 ということについて考えてみることにしましょう。左端のビットの 状態は移動させないわけですから、 パターン 1 101011000101110 ビット列 XXXXXXXXXXXXXXXX という移動が実行されることになります。そして、はみ出した部分は 消えてなくなり、空欄になったビットは、左端のビットの状態と 同じもので埋められるわけですから、 パターン 1111111010110001 ビット列 XXXXXXXXXXXXXXXX というパターンが、もとのビット列のパターンを5ビットだけ 算術右シフトした結果だということになります。 nビットの算術右シフトは、ビット列が整数を表現していると みなした場合に、その整数を2のn乗で除算する操作だと 考えることができます。たとえば、ビット列のパターンを 5ビットだけ算術右シフトするというのは、2の5乗で整数を除算する ということですから、2の補数で~270をあらわしている、 1111111011110010 というビット列のパターンを5ビットだけ算術右シフトして、 1111111111110111 というパターンを求めるという操作は、~270を32で除算して、 その商である~9を求めるという操作だと考えることができます。

Q 19.1.11___MLで、ビット列のパターンを算術右シフトさせるという 動作を記述したいのですが、そんなときはどうすればいいのですか。

算術右シフトをMLで記述したいときは、Word.~>>という関数を 使います。 wとnがwordのデータだとするとき、(w,n)という組にWord.~>>を 適用すると、Word.~>>は、wのパターンをnビットだけ 算術右シフトして、その結果を戻り値として返します。たとえば、 wordの長さが16だと仮定して、wが、 1101011000101110 (0wxD62E) というビット列のパターンで、nが0wx5だとするとき、(w,n)に Word.~>>を適用すると、Word.~>>は、wを5ビットだけ 算術右シフトした結果、つまり、 1111111010110001 (0wxFEB1) というビット列のパターンを戻り値として返します。

Q 19.1.12___長さがちょうど8のビット列を扱うプログラムを 書きたいのですが、そんなときはどうすればいいのですか。

長さが8のビット列を扱いたいときは、 Standard ML基本ライブラリーのWord8というストラクチャーの 中にある、型や関数の定義を使います。 Standard ML基本ライブラリーの中には、Word8という ストラクチャーがあって、その中には、長さがちょうど8の ビット列のパターンの集合という型を作り出す、Word8.wordという 型構成子の定義と、その型のデータを扱うさまざまな関数の定義が 含まれています。 Word8に含まれている関数は、扱うビット列の長さが異なるという 点を除けば、Wordに含まれている関数と、まったく同じです。 たとえば、 Word8.notb : Word8.word -> Word8.word という関数を使うことによって、長さが8のビット列のパターンを 反転させることができますし、 Word8.fromInt : int -> Word8.word という関数を使うことによって、intのデータを、長さが8の ビット列のパターンに変換することができます。

Q 19.1.13___Word8.wordという型のデータをあらわす定数は、 どのように書けばいいのですか。

Word8.wordという型のデータをあらわす定数の書き方は、wordの 定数の書き方と同じです。 0w76とか0wxCFというようなwordの定数が、Word8.wordという型の データを必要とする場所に書かれた場合、それは、Word8.wordの 定数であると解釈されます。ですから、 01011011 (0wx5B) という、長さが8のビット列のパターンを反転させた結果を 求めたいときは、 - Word8.notb 0wx5B; > val it = 0wxA4 : Word8.word というように、Word8.notbの右側に0wx5Bという定数を 書けばいいわけです。

19.2---バイナリーファイル

Q 19.2.1___バイナリーデータって何ですか。

バイナリーデータというのは、文字列以外の データのことです(「バイナリーデータ」は、「バイナリー」と 呼ばれることもあります)。 コンピュータが記憶したり読み込んだり出力したりする データというのは、すべて、ビット列のパターンという形式で ものごとを表現しています。文字列もその例外ではなくて、 それは、文字をあらわすビット列のパターンを並べることによって 作られています。しかし、文字列とそれ以外のデータとは、 取り扱う方法がかなり違っていますので、データを、文字列と それ以外のものとに分類する、というのは自然な考え方です。 「バイナリーデータ」または「バイナリー」の対義語として、 「テキストデータ」または「テキスト」という言葉が 使われることもありますが、それらは、「文字列」の同義語だと 考えていいでしょう。

Q 19.2.2___バイナリーファイルって何ですか。

バイナリーファイルというのは、バイナリーデータが格納されている ファイルのことです。

Q 19.2.3___バイナリーファイルからデータを読み込んだり、 バイナリーファイルにデータを出力したりするプログラムを 書きたいのですが、そんなときはどうすればいいのですか。

バイナリーファイルに対する読み込みや出力を実行するプログラムを 書きたいときは、Standard ML基本ライブラリーの中のBinIOという ストラクチャーの中にある関数を使います。 Standard ML基本ライブラリーの中には、BinIOという ストラクチャーがあって、その中には、バイナリーファイルに対する 読み込みや出力を実行するためのさまざまな関数の定義が 含まれています。

Q 19.2.4___バイナリーファイルをオープンしたり クローズしたりしたいときはどうすればいいのですか。

バイナリーファイルをオープンしたりクローズしたりしたいときは、 BinIOというストラクチャーの中にある、そのための関数を 使います。 Standard ML基本ライブラリーのBinIOの中には、 openIn : string -> BinIO.instream closeIn : BinIO.instream -> unit openOut : string -> BinIO.outstream openAppend : string -> BinIO.outstream closeOut : BinIO.outstream -> unit という関数の定義が含まれています。これらの関数の使い方は、 TextIOというストラクチャーに含まれている同じ名前の関数の 使い方と、まったく同じです。 BinIOに含まれている読み込みや出力の関数は、 バイナリーファイルの中のデータを、長さが8のビット列の パターンが一列に並んだものであるとみなして、読み込みや出力を 実行します。したがって、読み込みや出力が実行されると、 ファイルの現在位置は、長さが8のビット列を単位として 移動していくことになります。

Q 19.2.5___バイナリーファイルの現在位置がファイルの 終わりであるかどうかを調べたいときはどうすればいいのですか。

バイナリーファイルの現在位置がファイルの終わりであるかどうかを 調べたいときは、BinIO.endOfStreamという述語を使います。 バイナリーファイルを指示する入力ストリームに BinIO.endOfStreamを適用すると、BinIO.endOfStreamは、 そのファイルの現在位置がファイルの終わりであるならば trueを返し、そうでないならばfalseを返します。

Q 19.2.6___バイナリーファイルの現在位置にある、長さが8の ビット列のパターンを読み込みたいときは、 どうすればいいのですか。

バイナリーファイルの現在位置から、長さが8のビット列の パターンを読み込みたいときは、BinIO.input1という関数を 使います。 BinIO.input1は、BinIO.instream -> Word8.word optionという型を 持つ関数です。バイナリーファイルを指示する入力ストリームに input1を適用すると、input1は、その入力ストリームが指示する バイナリーファイルの現在位置から、長さが8のビット列の パターン(型はWord8.word)を読み込んで、ファイルの現在位置を 次の位置へ移動させて、読み込んだパターンにSOMEを適用した結果を 返します。 それでは、BinIO.input1を使って、fileToWordListという関数を 定義してみましょう。fileToWordListは、バイナリーファイルの パス名に適用されると、そのファイルに格納されているデータを すべて読み込んで、それをWord8.wordのデータのリストという形式で 返す、という動作をする関数です(型は string -> Word8.word list)。たとえば、undique.binという名前の ファイルがあって、その中に、 A0 79 4C 2D FE 03 B7 というビット列のパターンが格納されているとするとき、 "undique.bin"という文字列にfileToWordListを適用すると、 fileToWordListは、 - fileToWordList "undique.bin"; > val it = [0wxA0, 0wx79, 0wx4C, 0wx2D, 0wxFE, 0wx03, 0wxB7] : Word8.word list というように、そのファイルからデータを読み込んで、Word8.wordの データのリストという形式で、そのデータを返します。 fileToWordListを定義するfun宣言は、 fun fileToWordList filename = let open BinIO val instr = openIn filename fun fileToWordList1 () = if endOfStream instr then [] else valOf (input1 instr) :: fileToWordList1 () val wl = ref [] in wl := fileToWordList1 (); closeIn instr; !wl end というように書くことができます。

Q 19.2.7___Word8Vectorって何ですか。

Word8Vectorというのは、Standard ML基本ライブラリーの中にある ストラクチャーで、その中には、長さが8のビット列のパターンから 構成されるベクトルを扱う、さまざまな関数の定義が 含まれています。 Word8Vectorというストラクチャーの中には、tabulate、concat、 length、sub、extractというような関数が含まれていて、 それらの関数は、Vectorというストラクチャーの中にある同じ名前を 持つ関数と同じような動作をします。 Word8Vectorに含まれている関数は、長さが8のビット列の パターンから構成されるベクトルを扱います。ただし、 それらの関数が扱うベクトルの型は、 Word8.word vectorではなくて、Word8Vector.vectorという、 独自の型です。

Q 19.2.8___BinIOの中には、バイナリーファイルのすべての内容を 1回で読み込む、という動作をする関数も含まれているのですか。

はい、含まれています。 BinIOの中にあるinputAllという関数を使うことによって、 バイナリーファイルの内容を1回ですべて読み込む、ということが 可能です。 BinIO.inputAllは、BinIO.instream -> Word8Vector.vectorという 型を持つ関数です。バイナリーファイルを指示する入力ストリームに BinIO.inputAllを適用すると、BinIO.inputAllは、そのファイルの 現在位置からうしろにあるすべてのデータを読み込んで、現在位置を ファイルの終わりへ移動させて、読み込んだデータを、 Word8Vector.vectorという型のデータにして返します。 Q 19.2.4で定義を書いたfileToWordListは、BinIO.inputAllを 使うことによって、 fun fileToWordList filename = let open BinIO Word8Vector val instr = openIn filename fun vectorToList v = if length v = 0 then [] else sub (v,0) :: vectorToList (extract (v,1,NONE)) val wl = ref [] in wl := vectorToList (inputAll instr); closeIn instr; !wl end というように定義することも可能です。

Q 19.2.9___長さが8のビット列のパターンをバイナリーファイルに 出力したいときはどうすればいいのですか。

長さが8のビット列のパターンをバイナリーファイルに 出力したいときは、BinIO.output1という関数を使います。 BinIO.output1は、BinIO.outstream * Word8.word -> unitという 型を持つ関数です。bostrがバイナリーファイルを指示する 出力ストリームで、wが、長さが8のビット列の パターンだとするとき、(bostr,w)という組にBinIO.output1を 適用すると、BinIO.output1は、bostrが指示するファイルの 現在位置にwを出力して、出力したパターンの次の位置へ現在位置を 移動させます。 それでは、BinIO.output1を使って、wordListToFileという関数を 定義してみましょう。filenameがファイルのパス名で、wlistが、 長さが8のビット列のパターンから構成されるリストだとするとき、 (filename,wlist)という組にwordListToFileを適用すると、 wordListToFileは、filenameによって指定されるファイルを BinIO.openOutでオープンして、そのファイルにwlistを出力して、 そしてそのファイルをクローズします(型は string * Word8.word list -> unit)。たとえば、 - wordListToFile ("quoque.bin", [0wx63,0wx7D,0wxAB,0wxE0,0wx55,0wx0F,0wxB3,0wx9C,0wx35]); > val it = () : unit というように、パス名と、ビット列のパターンのリストから 構成される組にwordListToFileを適用したとすると、 wordListToFileは、quoque.binという名前のファイルを オープンして、そのファイルに、 63 7D AB E0 55 0F B3 9C 35 というビット列のパターンを出力して、そしてそのファイルを クローズします。 wordListToFileを定義するfun宣言は、 fun wordListToFile (filename,wlist) = let open BinIO val bostr = openOut filename fun wordListToFile1 (_,[]) = () | wordListToFile1 (bostr,w::ws) = ( output1 (bostr,w); wordListToFile1 (bostr,ws) ) in wordListToFile1 (bostr,wlist); closeOut bostr end というように書くことができます。

Q 19.2.10___Word8.word listという型のデータを Word8Vector.vectorという型に変換したいときは どうすればいいのですか。

データの型を、Word8.word listからWord8Vector.vectorへ 変換したいときは、Word8Vector.fromListという関数を使います。 Word8Vector.fromListは、 Word8.word list -> Word8Vector.vectorという型を持つ関数です。 長さが8のビット列のパターンから構成されるリスト(型は Word8.word list)にWord8Vector.fromListを適用すると、 Word8Vector.fromListは、そのリストを、Word8Vector.vectorという 型のデータに変換して、その結果を返します。

Q 19.2.11___長さが8のビット列のパターンから構成される ベクトル(型はWord8Vector.vector)をバイナリーファイルに 出力したいときはどうすればいいのですか。

Word8Vector.vectorという型を持つデータをバイナリーファイルに 出力したいときは、BinIO.outputという関数を使います。 BinIO.outputは、 BinIO.outstream * Word8Vector.vector -> unitという型を持つ 関数です。bostrがバイナリーファイルを指示する 出力ストリームで、vが、Word8Vector.vectorという型を持つ データだとするとき、(bostr,v)という組にBinIO.outputを 適用すると、BinIO.outputは、bostrが指示するファイルの 現在位置からうしろにvを出力して、出力したデータの次の位置へ 現在位置を移動させます。 Q 19.2.8で定義を書いたwordListToFileは、BinIO.outputを 使うことによって、 fun wordListToFile (filename,wlist) = let open BinIO val bostr = openOut filename in output (bostr,Word8Vector.fromList wlist); closeOut bostr end というように定義することも可能です。

---練習問題

19.1___長さが8のビット列のパターンに適用すると、それに含まれる 1をtrueに、0をfalseに変換することによってできる、長さが8の 真偽値のリストを返す、word8ToBoolListという関数(型は Word8.word -> bool list)を定義するfun宣言を書いてください。

実行例 - word8ToBoolList 0wxD7; > vai it = [true, true, false, true, false, true, true, true] : bool list

19.2___真偽値から構成される、長さが8のリストに適用すると、 それに含まれるtrueを1に、falseを0に 変換することによってできる、長さが8のビット列のパターンを 返す、boolListToWord8という関数(型は bool list -> Word8.word)を定義するfun宣言を書いてください。

なお、真偽値のリストの長さが8ではなかった場合は、 LengthIsNotEightという例外を発生させるようにしてください。 実行例 - boolListToWord8 [false,true,true,false,true,true,true,false]; > vai it = 0wx6E : Word8.word

19.3___長さが8のビット列を構成するそれぞれのビットに、左から 順番に、0、1、2、……、7、という番号を付けたとするとき、 長さが8のビット列のパターンに適用すると、そのパターンのうちで 状態が1になっているビットの番号から構成されるリストを返す、 word8ToNumberListという関数(型はWord8.word -> int list)を 定義するfun宣言を書いてください。

実行例 - word8ToNumberList 0wx96; > vai it = [0, 3, 5, 6] : int list

19.4___練習問題19.3と同じように、長さが8のビット列を構成する それぞれのビットに番号を付けたとするとき、ビットの番号から 構成されるリストに適用すると、それに含まれる番号のビットが 1で、含まれない番号のビットが0であるような、長さが8の ビット列のパターンを返す、numberListToWord8という関数(型は int list -> Word8.word)を定義するfun宣言を書いてください。

実行例 - numberListToWord8 [3, 7, 0, 6, 2]; > vai it = 0wxB3 : Word8.word

19.5___長さが8のビット列のパターンに適用すると、それを構成する ビットの順序を逆にすることによってできるパターンを返す、 reverseWord8という関数(型はWord8.word -> Word8.word)を 定義するfun宣言を書いてください。

実行例 - reverseWord8 0wx7D; > vai it = 0wxBE : Word8.word

19.6___infilenameとoutfilenameがファイルのパス名だとするとき、 (infilename,outfilename)という組に適用すると、 infilenameによって指定されるファイルに格納されているビット列の パターンを16進数に変換した結果を、outfilenameによって 指定されるファイルに出力して、ユニットを返す、 binaryToTextという関数(型はstring * string -> unit)を 定義するfun宣言を書いてください。

なお、16進数は、2桁ごとに空白で区切り、32桁ごとに改行で 区切って出力するようにしてください。 出力された16進数の例 75 40 1C B1 3A 22 05 6D 97 30 4B 59 8D E3 77 41 4F 78 39 CC 18 2E 40 F3 09 4A 02 D0 11 0D 09 15 28 D4 63 07 41 6D AD 97 03 12 B8 09 0E 75 4F 95 81 3F EA 00 DD 03 84 61 9A

19.7___infilenameとoutfilenameがファイルのパス名で、 infilenameによって指定されるファイルには16進数が格納されている とするとき、(infilename,outfilename)という組に適用すると、 infilenameによって指定されるファイルに格納されている16進数を ビット列のパターンに変換した結果を、outfilenameによって 指定されるファイルに出力して、ユニットを返す、 textToBinaryという関数(型はstring * string -> unit)を 定義するfun宣言を書いてください。

もしも、infilenameによって指定されるファイルの中に、16進数で 使われる文字ではない文字が含まれていた場合は、その文字を 無視するようにしてください。 もしも、infilenameによって指定されるファイルに格納されている 16進数の桁数が2の倍数ではなかった場合は、最後に0という桁を 追加することによって、桁数を2の倍数にしてください。 [ヒント] 文字が、16進数で使われるものかどうかを調べたいときは、 Standard ML基本ライブラリーの中にあるCharという ストラクチャーに含まれているisHexDigitという関数を 使うといいでしょう。Char.isHexDigitは、 - Char.isHexDigit #"E"; > val it = true : bool - Char.isHexDigit #"G"; > val it = false : bool というように、16進数で使われる文字に適用された場合はtrueを 返し、そうでない場合はfalseを返す述語です。

19.8___画像や音声などのバイナリーデータを電子メールで送る 場合には、メールを送る側でバイナリーデータを文字列に 変換して、メールを受け取った側で文字列をもとの バイナリーデータに戻す、という操作が必要になります。現在、 バイナリーデータを文字列に変換するための規則としては、 base64と呼ばれるものが標準として使われています[Freed,1996]。 base64という規則は、 (1) もとのデータの連続する24ビット(3バイト)を6ビットごとに 区切って、四つの部分に分ける。 (2) 四つに分けたそれぞれの部分を、そのパターンに対応する 64種類の文字のうちのいずれかに変換する。 という方法で、ビット列のパターンを文字列に変換します。 6ビットのパターンのそれぞれに対応する64種類の文字というのは、 上位3ビット 000 001 010 011 100 101 110 111 000 A I Q Y g o w 4 下 001 B J R Z h p x 5 位 010 C K S a i q y 6 3 011 D L T b j r z 7 ビ 100 E M U c k s 0 8 ッ 101 F N V d l t 1 9 ト 110 G O W e m u 2 + 111 H P X f n v 3 / というように定められています。 たとえば、 11000011 10100011 00110111 11111100 00011001 00000011 というビット列のパターンは、まず、 110000 111010 001100 110111 111111 000001 100100 000011 というように、6ビットづつに区切り直され、そして、それぞれの 部分を文字に変換することによって、 "w6M3/BkD" という文字列に変換されます。 もとのデータの長さが、3バイトの倍数ではなくて、1バイトの余分が ある場合は、すべてのビットが0である4ビットのパターンを 追加することによって6ビットのパターンを二つ作って、それを 変換することによってできた長さが2の文字列に、"=="という 文字列を連結します。たとえば、 01100111 という1バイトのパターンが余った場合は、 011001 110000 というように4ビットのパターンを追加したものを文字に 変換して、さらに"=="を連結することによって、 "Zw==" という文字列を作ります。同じように、2バイトの余分が ある場合は、両方のビットが0である2ビットのパターンを 追加することによって6ビットのパターンを三つ作って、それを 変換することによってできた長さが3の文字列に、"="という 文字列を連結します。 infilenameとoutfilenameがファイルのパス名だとするとき、 (infilename,outfilename)という組に適用すると、 infilenameによって指定されるファイルに格納されている バイナリーデータをbase64によって文字列に変換した結果を、 outfilenameによって指定されるファイルに出力して、ユニットを 返す、encodeByBase64という関数(型は string * string -> unit)を定義するfun宣言を書いてください。 なお、変換によってできた文字列は、60文字ごとに改行で 区切って出力するようにしてください。

変換前のバイナリーデータ F8 37 DA 50 AA 31 20 00 11 F0 0A 98 D6 E3 10 4B 92 4B 3B 90 7E 1C 6D FE 27 31 9E 6B F2 11 6D FC 5F E8 37 48 DF EC 03 65 A0 73 10 66 DA CE 11 50 44 C5 38 AB 03 17 CE FB 44 ED 9B 変換後の文字列 +DfaUKoxIAAR8AqY1uMQS5JLO5B+HG3+JzGea/IRbfxf6DdI3+wDZaBzEGba zhFQRMU4qwMXzvtE7Zs=

19.9___infilenameとoutfilenameがファイルのパス名で、 infilenameによって指定されるファイルには、バイナリーデータを base64によって文字列に変換したものが格納されているとするとき、 (infilename,outfilename)という組に適用すると、 infilenameによって指定されるファイルに格納されている文字列を、 base64によってもとのバイナリーデータに変換した結果を、 outfilenameによって指定されるファイルに出力して、ユニットを 返す、decodeByBase64という関数(型は string * string -> unit)を定義するfun宣言を書いてください。

なお、infilenameによって指定されるファイルに格納されている 文字列の中に、base64で使われる文字ではなく、改行でもない文字が 含まれていた場合は、その文字を例外引数として伴う、 IllegalCharという例外を発生させるようにしてください。 また、infilenameによって指定されるファイルに格納されている 文字列の長さが、4の倍数ではなかった場合は、 NotMultipleOfFourという例外を発生させるようにしてください。 [ヒント] 文字からWord8.wordへの変換は、まず文字を整数に変換して、 そののちその整数をWord8.wordに変換する、という2段階の処理で 実行します。 ASCIIという文字コードは、英字や数字を、それらの自然な順序が 反映されるようにビット列のパターンに対応させています。 ですから、cが英字の大文字だとすると、 (ord c) - (ord #"A") という式を評価することによって、#"A"を0、#"B"を1、#"C"を2、 #"D"を3、というように、文字を自然な順序で並べた場合の番号を 求めることができます。 文字を、英字の大文字、小文字、数字に分類したいときは、 Standard ML基本ライブラリーのCharに含まれている述語を 使うといいでしょう。Charには、 isUpper : char -> bool 英字の大文字 isLower : char -> bool 英字の小文字 isDigit : char -> bool 数字 という述語が含まれています。たとえば、isUpperを文字に 適用すると、isUpperは、その文字が英字の大文字ならばtrueを 返し、そうでなければfalseを返します。

第20章===オペレーティングシステム

20.1---ファイルシステム

Q 20.1.1___オペレーティングシステムって何ですか。

オペレーティングシステムというのは、コンピュータを利用する 人間やプログラムに対して、物理的な存在としてのコンピュータを そのまま見せるのではなくて、それを利用しやすくした仮想的な コンピュータを見せるためのプログラムのことです。 オペレーティングシステムは、ユーザーの管理、プログラムの実行の 制御、メモリーの管理、入出力装置の制御、データを記録するための 媒体の管理、というような機能から構成されています。

Q 20.1.2___ファイルシステムって何ですか。

ファイルシステムというのは、オペレーティングシステムが、 データを記録するための媒体の上に作り出した構造のことです。

Q 20.1.3___ディレクトリって何ですか。

ディレクトリというのは、ディレクトリまたはファイルを 格納することのできる容器のことです(ディレクトリは、 「フォルダー」と呼ばれることもあります)。 ディレクトリは、ファイルを分類して整理するために使われる 容器です。いくつかのファイルのあいだに関連性がある場合、 それらのファイルをひとつのディレクトリに格納することによって、 それらのあいだにある関連性を明示することができます。 ディレクトリの中には、ファイルだけではなくてディレクトリを 格納することもできますので、ディレクトリの中にディレクトリを 格納することによって、いくつかのディレクトリのあいだにある 関連性を明示するということも可能です。 ディレクトリとファイルによって構成されるファイルシステムは、 ディレクトリまたはファイルが節点で、「格納する」という関係が 親子関係だと考えることによって、その全体をひとつの 木(Q 11.4.3参照)だとみなすことができます。 ファイルシステムを木とみなしたとき、その根に相当する ディレクトリは、「ルートディレクトリ」と呼ばれます。 UNIXでは、ルートディレクトリにはスラッシュ(/)という文字が 名前として与えられています。同じように、DOSでは、 ルートディレクトリにはバックスラッシュ(\)という文字が 名前として与えられています。

Q 20.1.4___カレントディレクトリって何ですか。

カレントディレクトリというのは、プログラムが作業の対象として 注目しているディレクトリのことです。 たとえば、プログラムを起動するコマンドの引数として、ファイルや ディレクトリの名前を書いたとすると、そのコマンドによって 起動したプログラムは、そのファイルまたはディレクトリが カレントディレクトリにあるとみなして動作します。 カレントディレクトリを、ひとつ下の階層にあるディレクトリに 変更したいときは、シェルに対して、 cd ディレクトリ名 というコマンドを入力します。それとは逆に、 カレントディレクトリが格納されているディレクトリを カレントディレクトリにしたいときは、 cd .. というコマンドを使います。

Q 20.1.5___パス名って何ですか。

パス名というのは、特定のディレクトリまたはファイルを 指定するための文字列のことです。 パス名は、基本的には、 ● ディレクトリ名またはファイル名はパス名である。 ● ルートディレクトリの名前以外のディレクトリ名の右側に 区切り文字を連結して、その右側にパス名を連結したものは パス名である。 ● 以上の記述から導かれるもの以外はパス名ではない。 というように再帰的に定義することができます(この定義から 作ることのできる文字列のことを、仮に、「基本パス名」と 呼ぶことにします)。パス名の中で使われる区切り文字というのは、 オペレーティングシステムによって違っていて、UNIXの場合は スラッシュ(/)、DOSの場合はバックスラッシュ(\)です(つまり、 ルートディレクトリをあらわす文字と同じです)。 パス名は、相対パス名と絶対パス名に分類することができます。 相対パス名というのは、カレントディレクトリを出発点とする パス名で、絶対パス名というのはルートディレクトリを出発点とする パス名です。 基本パス名は、すべて、相対パス名だとみなされます。たとえば、 UNIXの場合、 exodus というパス名は、カレントディレクトリにあるexodusという 名前のものを指定し、 bella/dora というパス名は、カレントディレクトリにあるbellaという ディレクトリにあるdoraという名前のものを指定し、 ../benigni というパス名は、カレントディレクトリのひとつ上の ディレクトリにあるbenigniという名前のものを指定します。 ルートディレクトリの名前の右側に基本パス名を書いたものは、 絶対パス名だとみなされます。たとえば、UNIXの場合、 /aronofsky/blantner というパス名は、ルートディレクトリにあるaronofskyという ディレクトリにあるblantnerという名前のものを指定します。

Q 20.1.6___オペレーティングシステムが持っている機能を借用する プログラムを書きたいのですが、そんなときは どうすればいいのですか。

オペレーティングシステムが持っている機能を借用するプログラムを 書きたいときは、Standard ML基本ライブラリーの中のOSという ストラクチャーの中にある関数を使います。 Standard ML基本ライブラリーの中には、OSという ストラクチャーがあって、その中には、オペレーティングシステムの 機能に関連するさまざまな関数の定義が含まれています。

Q 20.1.7___SysErrって何ですか。

SysErrというのは、OSというストラクチャーの中にある関数が、 何らかの障害に遭遇したときに発生させる例外のことです。 OSというストラクチャーの中には、syserrorという型構成子の 定義と、SysErrという例外の定義が含まれています。syserrorという 型を持つデータは、関数が遭遇した障害の種類を 識別するためのものです。 SysErrという例外には、string * syserror optionという型を持つ データが例外引数として伴います。その例外引数に含まれる 文字列は、遭遇した障害について報告する短いメッセージです。

Q 20.1.8___ファイルシステムを取り扱うプログラムを 書きたいのですが、そんなときはどうすればいいのですか。

ファイルシステムを取り扱うプログラムを書きたいときは、 OSというストラクチャーの中にあるFileSysという ストラクチャーに含まれている関数を使います。 Standard ML基本ライブラリーのOSというストラクチャーの中には、 FileSysというストラクチャーが含まれていて、FileSysの中には、 ファイルシステムに関連するさまざまな関数の定義が 含まれています。

Q 20.1.9___ディレクトリの中にあるものの名前を 調べたいのですが、そんなときはどうすればいいのですか。

ディレクトリの中にあるものの名前を調べたいときは、 ディレクトリストリームというものを使います。 ディレクトリストリームというのは、ディレクトリを指示する データのことで、dirstreamという型を持ちます。 ディレクトリストリームの使い方は、ファイルからデータを 読み出すときに使う入力ストリームの使い方によく似ています。 ディレクトリストリームを作りたいときは、FileSysという ストラクチャーの中にあるopenDirという関数(型は string -> dirstream)を使います。ディレクトリを指定する パス名にopenDirを適用すると、openDirは、そのディレクトリを オープンして、そのディレクトリを指示する ディレクトリストリームを戻り値として返します。 ディレクトリをオープンした場合は、そのディレクトリに対する 操作が終わったのち、それをクローズする必要があります。 ディレクトリをクローズしたいときは、closeDirという関数を 使います。ディレクトリストリームにcloseDirを適用すると、 closeDirは、そのディレクトリストリームによって指示される ディレクトリをクローズして、ユニットを返します。 オープンされたディレクトリは、ファイルと同じように 現在位置というものを持ちます。ディレクトリがオープンされた 直後の時点では、その中の先頭に格納されているものの位置が 現在位置になっています。 OS.FileSys.readDirという関数をディレクトリストリームに 適用すると、readDirは、そのディレクトリの現在位置にあるものの 名前を読み込んだのち、現在位置を次の位置へ移動させて、 そして読み込んだ名前を戻り値として返します。ディレクトリの 中にあるものの名前をすべて読み込んだのち、つまり、 ディレクトリの末尾にあるものの次の位置が 現在位置になっているときに、そのディレクトリを指示する ディレクトリストリームにreadDirを適用した場合、readDirは、 戻り値として空文字列を返します。 それでは、openDirとreadDirとcloseDirを使って関数を 定義してみましょう。たとえば、 fun dirList path = let open OS.FileSys val dirstr = openDir path fun dirList1 () = let val s = readDir dirstr in if s="" then [] else s :: dirList1 () end val sl = ref [] in sl := dirList1 (); closeDir dirstr; !sl end というfun宣言によって定義される、dirListという関数(型は string -> string list)は、ディレクトリを指定するパス名に 適用すると、そのディレクトリの中にあるすべてのものの名前から 構成されるリストを返します。たとえば、/home/curtisという パス名がディレクトリを指定しているとするとき、そのパス名に dirListを適用すると、dirListは、 - dirList "/home/curtis"; > val it = ["pfarrer.txt", "mcneely.mid", "baldwin", "virus", "goriath", "pacula.jpg"] : string list というように、そのディレクトリに格納されているものの名前から 構成されるリストを返します。

Q 20.1.10___パス名によって指定されるものがディレクトリなのか ファイルなのかということを調べたいときは どうすればいいのですか。

パス名によって指定されるものがディレクトリなのか ファイルなのかということを調べたいときは、 OS.FileSys.isDirという関数を使います。 OS.FileSys.isDirは、string -> boolという型を持つ関数です。 パス名にisDirを適用すると、isDirは、そのパス名によって 指定されるものがディレクトリならばtrueを返し、そうでなければ falseを返します。 それでは、isDirを使う関数の例として、Q 20.1.9で定義した dirListという関数の改良版を定義してみることにしましょう。 dirListは、指定されたディレクトリの中にあるものの名前を そのままリストにして返す関数だったわけですが、その改良版である dirList2は、それらの名前を持つものがディレクトリなのか ファイルなのかということがわかるように、ディレクトリの名前の 末尾にスラッシュを追加します。つまり、dirList2というのは、 - dirList2 "/home/curtis"; > val it = ["pfarrer.txt", "mcneely.mid", "baldwin/", "virus/", "goriath/", "pacula.jpg"] : string list というような動作をする関数です。dirList2を定義するfun宣言は、 isDirを使うことによって、 fun dirList2 path = let open OS.FileSys val path2 = if path="/" then "" else path val dirstr = openDir path fun dirList21 () = let val s = readDir dirstr in if s="" then [] else if isDir (path2^"/"^s) then s^"/" :: dirList21 () else s :: dirList21 () end val sl = ref [] in sl := dirList21 (); closeDir dirstr; !sl end というように書くことができます。 もうひとつ、isDirを使う関数の例として、recursiveDirListという 関数の定義を書いてみることにします。recursiveDirListは、 string -> string listという型を持つ関数です。ディレクトリを 指定するパス名にrecursiveDirListを適用すると、 recursiveDirListは、指定されたディレクトリの下にある ファイルを、ディレクトリを再帰的にたどっていくことによって すべて調べ出して、それらの名前をリストにして返します。 たとえば、/home/stampというディレクトリの下に、 stamp | +--valorum | davies.txt | brown.txt | august.txt | +--lamb medeiros.txt dreyfus.txt finlay.txt gordon.txt というように、valorumとlambという二つのディレクトリがあって、 valorumの下には、davies.txt、brown.txt、august.txtという三つの ファイルがあり、lambの下には、medeiros.txt、dreyfus.txt、 finlay.txt、gordon.txtという四つのファイルがあるとするとき、 recursiveDirListを"/home/stamp"に適用すると、 recursiveDirListは、 - recursiveDirList "/home/stamp"; > val it = ["davies.txt", "brown.txt", "august.txt", "medeiros.txt", "dreyfus.txt", "finlay.txt", "gordon.txt"] : string list というように、指定されたディレクトリの下を再帰的に 探索することによって到達することのできるすべてのファイルの 名前から構成されるリストを返します。 recursiveDirListを定義するfun宣言は、 fun recursiveDirList path = let open OS.FileSys val path2 = if path="/" then "" else path val dirstr = openDir path fun recursiveDirList1 () = let val s = readDir dirstr val subpath = path2 ^ "/" ^ s in if s="" then [] else if isDir subpath then recursiveDirList subpath @ recursiveDirList1 () else s :: recursiveDirList1 () end val sl = ref [] in sl := recursiveDirList1 (); closeDir dirstr; !sl end というように書くことができます。

Q 20.1.11___ファイルの大きさを調べたいときは どうすればいいのですか。

ファイルの大きさを調べたいときは、OS.FileSys.fileSizeという 関数を使います。 OS.FileSys.fileSizeは、string -> intという型を持つ関数です。 ファイルを指定するパス名にfileSizeを適用すると、fileSizeは、 そのパス名によって指定されるファイルの大きさ(単位はバイト)を 調べて、その結果をあらわすintのデータを返します。 それでは、fileSizeを使う関数の例として、Q 20.1.10で定義した dirList2という関数を改造した、dirListSizeという関数を 定義してみることにしましょう。ディレクトリを指定するパス名に dirListSizeを適用すると、dirListSizeは、そのディレクトリの 中にあるもののそれぞれについて、それがディレクトリなのか ファイルなのかということを調べて、ディレクトリだった場合は、 その名前と0との組を作り、ファイルだった場合は、その名前と 大きさとの組を作って、それらの組から構成されるリストを 返します。つまり、dirListSizeは、 - dirListSize "/home/wannberg"; > val it = [("myles.mid", 5370), ("neufeld.mid", 8319), ("pope/", 0), ("edwards/", 0), ("murphy.mid", 19530), ("allen/", 0)] : (string * int) list というような動作をする関数です。dirListSizeを定義する fun宣言は、 fun dirListSize path = let open OS.FileSys val path2 = if path="/" then "" else path val dirstr = openDir path fun dirListSize1 () = let val s = readDir dirstr val subpath = path2 ^ "/" ^ s in if s="" then [] else if isDir subpath then (s^"/",0) :: dirListSize1 () else (s,fileSize subpath) :: dirListSize1 () end val sl = ref [] in sl := dirListSize1 (); closeDir dirstr; !sl end というように書くことができます。

20.2---コマンドと環境変数

Q 20.2.1___コマンドをシェルに実行させるという動作をMLの プログラムの中に書きたいときは、どうすればいいのですか。

コマンドをシェルに実行させるという動作をMLのプログラムの中に 書きたいときは、OSというストラクチャーの中のProcessという ストラクチャーの中にある、systemという関数を使います。 Standard ML基本ライブラリーの中のOSというストラクチャーの 中には、いくつかのストラクチャーが含まれていて、Processという ストラクチャーもそのひとつです。 OS.Process.systemは、string -> OS.Process.statusという型を持つ 関数です。systemを文字列に適用すると、systemは、その文字列を コマンドとしてシェルに実行させます。たとえば、 オペレーティングシステムとしてUNIXを使っているとすると、 OS.Process.system "ls -l" という式を書くことによって、"ls -l"というコマンドをシェルに 実行させることができます。 OS.Process.systemは、コマンドの実行が正常に終了した場合、 OS.Process.successというデータを戻り値として返し、正常に 終了しなかった場合はOS.Process.successとは異なるデータを 返します。ですから、 - OS.Process.system "ls /home/lapis" = OS.Process.success; aedifico.mid laetor.eps opitulor.txt recordor.gif > val it = true : bool - OS.Process.system "ls /home/herba" = OS.Process.success; ls: /home/herba: No such file or directory > val it = false : bool というように、systemが返した戻り値とOS.Process.successとを 比較することによって、コマンドの実行が正常に 終了したかどうかを調べることができます。

Q 20.2.2___環境変数って何ですか。

環境変数というのは、オペレーティングシステムによって、何らかの 文字列に結び付けられた文字列のことです。 オペレーティングシステムは、普通、実行中のプログラムが 現在の環境についての知識を得ることができるように、 「環境変数」と呼ばれる文字列に、現在の状態をあらわす文字列を 結び付けています。たとえば、UNIXの場合、 HOME ユーザーのホームディレクトリ USER ユーザーのログイン名 PATH 実行可能なファイルを検索するディレクトリの列 PWD カレントディレクトリ SHELL シェルのパス名 というような環境変数が使われています。 環境変数と文字列とを結び付けることを、環境変数を「定義する」 と言います。また、環境変数に結び付けられた文字列のことを、 その環境変数の「値」と呼びます。 シェルにコマンドを入力することによって、環境変数を定義したり、 環境変数の値を調べたりする、ということができます。そのためには どんなコマンドを入力すればいいのかというのは、シェルによって 異なりますので、それについてはシェルのマニュアルを 参照してください。

Q 20.2.3___環境変数の値を調べるという動作をMLのプログラムの 中に書きたいときは、どうすればいいのですか。

環境変数の値を調べるという動作をMLのプログラムの 中に書きたいときは、OS.Process.getEnvという関数を使います。 OS.Process.getEnvは、string -> string optionという型を持つ 関数です。文字列にgetEnvを適用すると、getEnvは、その文字列が 環境変数である場合、その値にSOMEを適用した結果を戻り値として 返します。その文字列が環境変数ではない場合は、戻り値として NONEを返します。たとえば、UNIXの場合、PATHという環境変数が、 実行可能なファイルを検索するディレクトリの列に 結び付けられていますので、 - OS.Process.getEnv "PATH"; > val it = SOME "/usr/local/bin:/bin:/usr/bin:/usr/X11/bin" : string option というように、"PATH"という文字列にgetEnvを 適用することによって、PATHの現在の値を調べることができます。

20.3---日付と時刻

Q 20.3.1___MLでは、日付と時刻は、どのような型のデータとして 表現されるのですか。

MLでは、Date.dateという型が、日付と時刻をあらわすデータの 型として使われます。 Standard ML基本ライブラリーの中には、Dateという ストラクチャーがあって、その中には、dateという、日付と時刻を あらわすデータの型の定義と、その型のデータを扱うさまざまな 関数の定義が含まれています。

Q 20.3.2___特定の日付と時刻をあらわすデータを作りたいときは、 どうすればいいのですか。

特定の日付と時刻をあらわすデータを作りたいときは、 Date.fromStringという関数を使います。 Date.fromStringは、string -> Date.date optionという型を持つ 関数です。日付と時刻をあらわす文字列にfromStringを適用すると、 fromStringは、それをDate.dateという型のデータに変換することが 可能ならば、それを変換して、その結果にSOMEを適用した結果を 戻り値として返します。変換が不可能な場合は、NONEを返します。 fromStringを使ってDate.dateという型のデータを作る場合、 "Www Mmm dd hh:mm:ss yyyy" という形式で日付と時刻をあらわす文字列を作る必要があります。 この文字列を構成するそれぞれの部分は、 Www 曜日の省略形。"Sun"、"Mon"、"Tue"、"Wed"、"Thu"など。 Mmm 月の省略形。"Jan"、"Feb"、"Mar"、"Apr"、"May"など。 dd 月内の日の番号。"01".."31"。 hh 時。"00".."23"。 mm 分。"00".."59"。 ss 秒。"00".."61"。 yyyy 年。たとえば、"1994"。 という意味を持つ文字列です("60"または"61"という秒は、 うるう秒が挿入された場合にのみ出現する数字です)。 たとえば、 "Mon Sep 22 19:08:24 1997" という文字列にfromStringを適用すると、fromStringは、 その文字列が1997年9月22日(月)の19時8分24秒という日付と 時刻をあらわしている、と解釈して、それをDate.dateという型の データに変換して、その結果にSOMEを適用した結果を返します。

Q 20.3.3___Date.dateという型のデータがあらわしている日付と 時刻を文字列に変換したいときは、どうすればいいのですか。

Date.dateという型のデータがあらわしている日付と時刻を文字列に 変換したいときは、Date.toStringまたはDate.fmtという関数を 使います。 Date.toStringは、Date.date -> stringという型を持つ関数です。 日付と時刻をあらわしているデータにtoStringを適用すると、 toStringは、そのデータがあらわしている日付と時刻を文字列に 変換して、その結果を戻り値として返します。たとえば、dという 識別子が、日付と時刻をあらわしているデータに束縛されている とするとき、Date.toStringをdに適用すると、toStringは、 - Date.toString d; > val it = "Wed Jul 14 15:54:14 1993" : string というように、そのデータを文字列に変換した結果を返します。

Q 20.3.4___Date.dateという型のデータがあらわしている日付と 時刻を、Date.toStringが返す文字列とは異なる書式の文字列に 変換したいのですが、そんなときはどうすればいいのですか。

Date.dateという型のデータがあらわしている日付と時刻を、 任意の書式の文字列に変換したいときは、Date.fmtという関数を 使います。 Date.fmtは、string -> Date.date -> stringという型を持つ 関数です。fmtstrが、日付または時刻の書式をあらわす 文字列だとするとき、Date.fmtをfmtstrに適用すると、Date.fmtは、 「Date.dateという型のデータに適用すると、そのデータを fmtstrにしたがって文字列に変換した結果を返す関数」を返します。 日付または時刻の書式をあらわす文字列は、日付と時刻を構成する 要素をあらわす、次のような文字列を組み合わせることによって 作ります。 %a 曜日の省略形。"Sun"、"Mon"、"Tue"、"Wed"、"Thu"など。 %A 曜日。"Sunday"、"Monday"、"Tuesday"、"Wednesday"など。 %b 月の省略形。"Jan"、"Feb"、"Mar"、"Apr"、"May"など。 %B 月。"January"、"February"、"March"、"April"、"May"など。 %c 日付と時刻。たとえば、"Mon Aug 18 21:33:14 1997"。 %d 月内の日の番号。"01".."31"。 %H 時。"00".."23"。 %I 時。"01".."12"。 %j 年内の日の番号。"001".."366"。 %m 月の番号。"01".."12"。 %M 分。"00".."59"。 %p 午前か午後か。"AM"または"PM"。 %S 秒。"00".."61"。 %U 年内の週の番号。"00".."53"。日曜日から土曜日までを ひとつの週とする。 %w 週内の日の番号。"0".."6"。日曜日が"0"。 %W 年内の週の番号。"00".."53"。月曜日から日曜日までを ひとつの週とする。 %x 日付。たとえば、"Mon Aug 18, 1997"。 %X 時刻。たとえば、"21:33:14"。 %y 年の下2桁。"00".."99"。 %Y 年。たとえば、"1997"。 %Z 地方標準時の名前。"EST"、"CST"、"PST"、"JST"など。 %% パーセント文字(%)。 たとえば、dという識別子が日付と時刻をあらわしているデータに 束縛されているとするとき、その中に含まれている月のデータを 文字列に変換したいならば、 - Date.fmt "%B" d; > val it = "December" : string というように、"%B"という文字列にDate.fmtを適用して、 その結果として得られた関数をdに適用すればいいわけです。 書式をあらわす文字列は、上に列挙した、日付と時刻の要素を あらわす特別な文字列だけではなく、任意の文字列を 含んでいてもかまいません。日付と時刻の要素をあらわす 文字列ではない文字列は、結果として得られる文字列の中に、 そのまま埋め込まれます。たとえば、 "Today is %A." という文字列にDate.fmtを適用すると、fmtは、日付と時刻の データを、 "Today is Friday." というような文字列に変換する関数を返すことになります。

Q 20.3.5___MLでは、時間の長さは、どのような型のデータとして 表現されるのですか。

MLでは、Time.timeという型が、時間の長さをあらわすデータの 型として使われます。 Standard ML基本ライブラリーの中には、Timeという ストラクチャーがあって、その中には、timeという、時間の長さを あらわすデータの型の定義と、その型のデータを扱うさまざまな 関数の定義が含まれています。

Q 20.3.6___秒数をあらわすintのデータをTime.timeという型の データに変換したいのですが、そんなときは どうすればいいのですか。

秒数をあらわすintのデータをTime.timeという型のデータに 変換したいときは、Time.fromSecondsという関数を使います。 Time.fromSecondsは、int -> Time.timeという型を持つ関数です。 秒数をあらわすintのデータにfromSecondsを適用すると、 fromSecondsは、そのデータを、その秒数をあらわすTime.timeの データに変換して、その結果を返します。たとえば、 Time.fromSeconds 371 という式を書くことによって、371秒という時間の長さをあらわす、 Time.timeという型のデータを求めることができます。

Q 20.3.7___Time.timeという型のデータを、秒数をあらわすintの データに変換したいのですが、そんなときは どうすればいいのですか。

Time.timeという型のデータを、秒数をあらわすintのデータに 変換したいときは、Time.toSecondsという関数を使います。 Time.toSecondsは、Time.time -> intという型を持つ関数です。 Time.timeのデータにtoSecondsを適用すると、toSecondsは、 そのデータを、秒数をあらわすintのデータに変換して、その結果を 返します。

Q 20.3.8___Time.timeという型のデータに対して加算や減算を 実行したいのですが、そんなときはどうすればいいのですか。

Time.timeという型のデータに対して加算を実行したいときは Time.+という関数を使い、減算を実行したいときはTime.-という 関数を使います。 Time.+とTime.-は、どちらも、 Time.time * Time.time -> Time.timeという型を持つ関数です。 xとyがTime.timeという型を持つデータだとするとき、 (x,y)という組にTime.+を適用すると、Time.+は、xとyを加算した 時間の長さを返します。同じように、(x,y)という組にTime.-を 適用すると、Time.-は、xからyを減算した時間の長さを返します。

Q 20.3.9___Time.timeという型の2個のデータのあいだで、長さの 比較を実行したいのですが、そんなときはどうすればいいのですか。

Time.timeという型の2個のデータのあいだで長さの比較を 実行したいときは、Time.>、Time.<、Time.>=、Time.<=という 関数を使います。 Time.>、Time.<、Time.>=、Time.<=は、どれも、 Time.time * Time.time -> boolという型を持つ関数です。xとyが Time.timeという型を持つデータだとするとき、それらの関数を (x,y)という組に適用することによって、xとyの長さを比較した 結果を求めることができます。

Q 20.3.10___現在の日付と時刻を調べたいときは どうすればいいのですか。

現在の日付と時刻を調べたいときは、Time.nowという関数を 使います。 Time.nowは、unit -> Time.timeという型を持つ関数です。nowを ユニットに適用すると、nowは、現在の日付と時刻を調べて、 その結果をTime.timeという型のデータにして返します。 Time.nowが返す戻り値は、Time.timeという型のデータですから、 何らかの時間の長さをあらわしています。それが現在の日付と時刻を あらわすことができるのは、基準となる日付と時刻というものが 環境によって定められているからです。つまり、 西暦何年何月何日何時何分という基準となる日付と時刻が 環境によって定められていて、Time.nowの戻り値は、その基準と 現在との差をあらわしているわけです。

Q 20.3.11___Time.timeという型のデータによってあらわされている 日付と時刻をDate.dateという型に変換したいときは、 どうすればいいのですか。

Time.timeからDate.dateへ、データの型を変換したいときは、 Date.fromTimeLocalという関数を使います。 Date.fromTimeLocalは、Time.time -> Date.dateという型を持つ 関数です。時間の長さをあらわしているTime.timeというデータに Date.fromTimeLocalを適用すると、fromTimeLocalは、環境によって 定められている、基準となる日付と時刻から、その長さだけ時間が 経過したのちの日付と時刻を求めて、その結果をDate.dateという 型のデータとして返します。 ですから、現在の日付と時刻を求めて、それを文字列に変換したい、 というときは、 - Date.toString (Date.fromTimeLocal (Time.now ())); > val it = "Sun Aug 22 17:39:17 1999" : string というように、Time.nowが求めた時間の長さにDate.fromTimeLocalを 適用して、その結果を、Date.toStringを使って文字列に 変換すればいい、ということになります。 なお、Date.fromTimeLocalは、Time.timeのデータを地方標準時に 変換します。Dateというストラクチャーには、fromTimeUnivという、 fromTimeLocalとほぼ同じ動作をする関数があって、 fromTimeUnivのほうは、Time.timeのデータをUTC(協定世界時)に 変換します。

Q 20.3.12___ファイルの内容が最後に更新された日付と時刻を 調べたいときはどうすればいいのですか。

ファイルの内容が最後に更新された日付と時刻を調べたいときは、 OS.FileSys.modTimeという関数を使います。 OS.FileSys.modTimeは、string -> Time.timeという型の関数です。 ファイルを指定するパス名にOS.FileSys.modTimeを適用すると、 modTimeは、そのファイルの内容が最後に更新された日付と時刻を 調べて、その結果をTime.timeという型のデータにして返します。 それでは、modTimeを使って、関数を定義してみましょう。 たとえば、 fun dirListTime path = let open OS.FileSys Date val path2 = if path="/" then "" else path val dirstr = openDir path fun dirListTime1 () = let val s = readDir dirstr val subpath = path2 ^ "/" ^ s in if s="" then [] else if isDir subpath then (s^"/","") :: dirListTime1 () else (s,toString (fromTimeLocal (modTime subpath))) :: dirListTime1 () end val sl = ref [] in sl := dirListTime1 (); closeDir dirstr; !sl end というfun宣言によって定義されるdirListTimeという関数は、 Q 20.1.10で定義したdirList2という関数をちょっと 改造したものです。ディレクトリを指定するパス名にdirListTimeを 適用すると、dirListTimeは、そのディレクトリの中にあるものの それぞれについて、それがディレクトリなのかファイルなのか ということを調べて、ディレクトリだった場合は、その名前と 空文字列との組を作り、ファイルだった場合は、その名前と、最後に 更新された日付と時刻を文字列に変換したものとの組を作って、 それらの組から構成されるリストを返します。つまり、 dirListTimeというのは、 - dirListTime "/home/ballarin"; > val it = [ ("cappato.txt", "Mon May 24 09:11:08 1999"), ("rosada.sml", "Fri Mar 19 22:18:45 1999"), ("turchi.sml", "Thu Feb 25 12:11:03 1999"), ("bravin/", ""), ("mancini.txt", "Sun Aug 29 21:38:38 1999")] : (string * string) list というような動作をする関数です。

---練習問題

20.1___pathがディレクトリを指定するパス名で、nameがファイルの 名前だとするとき、(path,name)という組に適用すると、 pathの下で、nameを名前として持つファイルを探索して、発見された すべてのファイルのパス名から構成されるリストを返す、 fileSearchという関数(型はstring * string -> string list)を 定義するfun宣言を書いてください。

実行例 - fileSearch ("/milroy","opus.txt"); > val it = ["/milroy/aycock/cucchi/opus.txt", "/milroy/johns/opus.txt", "/milroy/kiefer/borofsky/opus.txt", "/milroy/kiefer/serra/opus.txt", "/milroy/shapiro/opus.txt"] : string list

20.2___pathがディレクトリを指定するパス名だとするとき、pathに 適用すると、pathの下にあるすべてのファイルの大きさを合計した 結果を返す、dirSizeという関数(型はstring -> int)を定義する fun宣言を書いてください。

実行例 - dirSize "/regina"; > val it = 71808 : int

20.3___cmdが文字列で、pathがディレクトリを指定する パス名だとするとき、(cmd,path)という組に適用すると、pathの 下にあるすべてのファイルのそれぞれについて、cmdの右側に1個の 空白を連結して、その右側にファイルのパス名を 連結することによってできる文字列を、コマンドとしてシェルに 実行させる、recursiveSystemという関数(型は string * string -> unit)を定義するfun宣言を書いてください。

実行例 - recursiveSystem ("wc -l","/tansey"); 2571 /tansey/deacon.txt 650 /tansey/cage.txt 803 /tansey/beuys/grooms.txt 4495 /tansey/beuys/winters.txt 1302 /tansey/christo.txt > val it = () : unit

20.4___sが文字列だとするとき、sに適用すると、 カレントディレクトリの下にあるmemo.txtというファイルの中にある 文字列の末尾に、現在の日付と時刻をあらわす文字列、": "、s、 そして改行を連結する、memoという関数(型はstring -> unit)を 定義するfun宣言を書いてください。ただし、MEMOという環境変数が 定義されている場合は、その環境変数の値をパス名とする ディレクトリの下にあるmemo.txtの中にある文字列の末尾に、 それらの文字列を連結するようにしてください。

実行例 - OS.Process.getEnv "MEMO"; > val it = SOME "/planta/lignum" : string option - memo "Graeca sunt, non leguntur."; > val it = () : unit - memo "Plaudite, acta est fabula."; > val it = () : unit - OS.Process.system "cat /planta/lignum/memo.txt" = OS.Process.success; Thu Sep 23 15:33:15 1999: Graeca sunt, non leguntur. Thu Sep 23 15:33:27 1999: Plaudite, acta est fabula. > val it = true : bool

20.5___pathがディレクトリを指定するパス名で、dayが日数を あらわす整数だとするとき、(path,day)という組に適用すると、 pathの下にあるファイルのうちで、その内容が最後に更新されてから 経過した日数がday日以下であるもののパス名のリストを返す、 dirNewFileという関数(型はstring * int -> string list)を 定義するfun宣言を書いてください。

実行例 - dirNewFile ("/home/hikari/project",5); > val it = ["/home/hikari/project/martius.eps", "/home/hikari/project/aprilis.pov", "/home/hikari/project/maius.tex", "/home/hikari/project/junius.sty"] : string list

付録A===練習問題の解答例

___第0章の解答例

0.1 (a) 53+27; (b) 88-62; (c) 14*30; (d) 54- ~33; (e) 5.08+7.15; (f) 46 div 7; (g) 34.6/2.8; (h) 68 div 3+4; (i) (32-19)*15; (j) 22*45 div 81; (k) 313 div (18*7); 0.2 (a) ドットを含んでいない数と含んでいる数とが 混在しているから。 (b) ドットを含んでいる数の割り算がdivにはできないから。 (c) ドットを含んでいない数の割り算が/にはできないから。 (d) アスタリスク(*)とチルダ(~)とのあいだに空白がないから。 (e) divと15とのあいだに空白がないから。

___第1章の解答例

1.1 65536とおり 1.2 16進数 10進数 (a) E 14 (b) 5 5 (c) 36 54 (d) 3A 58 (e) 2F4 756 1.3 (a) 2671または0xa6f (b) 0x8bc3または35779 (c) ~5018または~0x139a (d) 8.10334、810334e~5、など (e) 22.107、22107e~3、など (f) 1717e5、171700000.0、など (g) 6666e~6、0.006666など (h) 0w9001または0wx2329 (i) 0wxf2ddまたは0w62173 (j) ";;;;;;;" (k) "\n\n\n" (l) "\"\"\"\"" (m) "\\\\\\\\\\" (n) #"/" 1.4 (a) int * string (b) string * int (c) int * string * real * char (d) (int * string) * char (e) int * (string * char) (f) char * (real * (bool * int) * (string * word)) * unit

___第2章の解答例

2.1 (a) 正しい。 (b) 正しくない。先頭の文字が数字だから。 (c) 正しい。 (d) 正しい。 (e) 正しくない。英数字識別子の文字と記号識別子の文字とが 混在しているから。 (f) 正しくない。セミコロンは記号識別子を作るために使える 文字ではないから。 2.2 (a) > val tact = 4081 : int (b) > val therblig = ("Frank Bunker Gilbreth", 1911) : string * int (c) > val scalogram = 37.804 : real (d) > val x = (400, 520) : int * int (e) > val resolve = "Kurt Lewin" : string val conflict = 1948 : int (f) > val x = "Barker" : string val y = "Gump" : string val z = 1964 : int (g) > val x = "Ruth Fulton Benedict" : string val y = (1887, 1948) : int * int (h) > val introversion = "Carl Gustav Jung" : string (i) > val x = "Anatol Rapaport" : string (j) > val x = 1978 : int val y = #"G" : char

___第3章の解答例

3.1 (a) > val it = 35.0 : real (b) > val it = ~6 : int (c) > val it = 10.8 : real (d) > val it = 71 : int (e) > val it = 8 : int (f) Hyades> val it = () : unit (g) Capella Menkalinan Al Maaz > val it = () : unit (h) > val it = "+" : string (i) > val it = "~5738" : string (j) > val it = "9.133" : string (k) > val it = "ProcyonGomeisa" : string (l) > val it = 100 : int (m) > val it = #"d" : char (n) 6010> val it = () : unit (o) 7> val it = () : unit (p) > val it = 8 : int (q) > val it = "NGC2158" : string (r) M35> val it = () : unit 3.2 (a) real betelgeuse (b) floor rigel (c) abs bellatrix (d) ~ mintaka (e) size alnilam (f) print alnitak (g) chr saiph (h) ord trapezium (i) str meissa (j) Int.toString algenib (k) algol ^ menkhib (l) print (Int.toString miram) (m) print (Int.toString (ord schedar)) (n) chaph ^ Int.toString cih (o) print (sirius ^ mirzam) 3.3 (a) op$ (pully,fossil) (b) op$ (sulphur,op% (leviathan,trepang)) (c) op$ (op% (mucus,griffin),monolith) (d) op% (marsupial,op** (taciturnity,exoneration)) (e) op% (op** (persimmon,shamrock),obsidian) (f) op! (op% (skepticism,providence),inscription) (g) op% (op! (valerian,saturation),heteronym) (h) op** (orangutan,op// (cephalopod,dulcimer)) (i) op// (alluvium,op** (hyacinth,lithosphere))

___第4章の解答例

4.1 fun mtoh m = m/60.0 4.2 fun hmtom (h,m) = h*60+m 4.3 fun mtohm m = (m div 60, m mod 60) 4.4 fun bunsuuToString (shi,bo) = Int.toString bo ^ "分の" ^ Int.toString shi 4.5 fun shutsu () = print "私は文字列を出力する関数です。\n" 4.6 fun printNagasa s = print (s ^ "の長さは" ^ Int.toString (size s) ^ "です。\n")

___第5章の解答例

5.1 fun hundred n = n=100 5.2 fun measure (a,b) = a mod b = 0 5.3 fun bothEven (a,b) = let fun even n = n mod 2 = 0 in even a andalso even b end 5.4 fun eitherEven (a,b) = let fun even n = n mod 2 = 0 in even a orelse even b end 5.5 fun plusOrNot n = if n>0 then "プラスです。" else "プラスではありません。" 5.6 fun passExam (culture,muscle) = if culture>=80 then if muscle>=80 then "合格です。" else "もっと体を鍛えましょう。" else if muscle>=80 then "もっと本を読みましょう。" else "教養も体力もいまいちです。" 5.7 fun jidaikubun year = if year<710 then "飛鳥時代またはそれ以前" else if year<794 then "奈良時代" else if year<1192 then "平安時代" else if year<1336 then "鎌倉時代" else if year<1392 then "南北朝時代" else if year<1573 then "室町時代" else if year<1603 then "安土桃山時代" else if year<1867 then "江戸時代" else "明治時代またはそれ以降" 5.8 fun monthToEng 1 = "January" | monthToEng 2 = "February" | monthToEng 3 = "March" | monthToEng 4 = "April" | monthToEng 5 = "May" | monthToEng 6 = "June" | monthToEng 7 = "July" | monthToEng 8 = "August" | monthToEng 9 = "September" | monthToEng 10 = "October" | monthToEng 11 = "November" | monthToEng 12 = "December" | monthToEng _ = "" 5.9 fun enclose (1,s) = "(" ^ s ^ ")" | enclose (2,s) = "[" ^ s ^ "]" | enclose (3,s) = "{" ^ s ^ "}" | enclose (_,s) = s

___第6章の解答例

6.1 fun hesaid n = if n=1 then "私は何者なのだ。" else if n>=2 then "彼は「" ^ hesaid (n-1) ^ "」と言った。" else "" 6.2 fun downup n = let val sn = Int.toString n in if n=0 then sn else if n>=1 then sn ^ " " ^ downup (n-1) ^ " " ^ sn else "" end 6.3 fun kaijou n = if n=0 then 1 else if n>=1 then n * kaijou (n-1) else 0 6.4 fun bekijou (a,b) = if b=0 then 1 else if b>=1 then a * bekijou (a,b-1) else 0 6.5 fun gcm (a,b) = if b=0 then a else if b>=1 then gcm (b,a mod b) else 0 6.6 fun fibona n = if n=0 orelse n=1 then 1 else if n>=2 then fibona (n-2) + fibona (n-1) else 0 6.7 fun binary n = let fun binary1 n = if n>=1 then binary1 (n div 2) ^ Int.toString (n mod 2) else "" in if n=0 then "0" else binary1 n end 6.8 fun soinsuu n = let fun soinsuu1 (n,m) = if n>=2 andalso m>=2 then if n mod m = 0 then Int.toString m ^ " " ^ soinsuu1 (n div m,m) else soinsuu1 (n,m+1) else "" in soinsuu1 (n,2) end 6.9 fun hanoi n = let fun hanoi1 (n,source,work,dest) = if n>=1 then hanoi1 (n-1,source,dest,work) ^ "[" ^ source ^ "->" ^ dest ^ "]" ^ hanoi1 (n-1,work,source,dest) else "" in hanoi1 (n,"A","B","C") end 6.10 fun countup n = if n>=0 then ( countup (n-1); print (Int.toString n ^ " ") ) else () 6.11 fun herspeech n = if n=1 then "わたしはいったい誰なの。" else if n>=2 then "彼は「" ^ hisspeech (n-1) ^ "」と言った。" else "" and hisspeech n = if n>=1 then "彼女は「" ^ herspeech n ^ "」と言った。" else ""

___第7章の解答例

7.1 exception Kaijou fun kaijou n = if n=0 then 1 else if n>=1 then n * kaijou (n-1) else raise Kaijou 7.2 fun safetychr n = chr n handle Chr => #"." 7.3 fun msgkaijou n = kaijou n handle Kaijou => (print "The argument is minus.\n"; 0) 7.4 exception Combi of int fun combi (n,m) = if n<0 then raise Combi 1 else if m<0 then raise Combi 2 else if m>n then raise Combi 3 else if m=0 orelse n=m then 1 else combi (n-1,m) + combi (n-1,m-1) 7.5 fun msgcombi (n,m) = combi (n,m) handle Combi 1 => (print "n is minus.\n"; 0) | Combi 2 => (print "m is minus.\n"; 0) | Combi 3 => (print "m is larger than n.\n"; 0)

___第8章の解答例

8.1 fun twice x = (x,x) 8.2 fun distribute (x,y,z) = ((x,y),(x,z)) 8.3 fun selectByEquality (w,x,y,z) = if w=x then y else z 8.4 fun testSlotMachine (x,y,z) = if x=y andalso y=z then 2 else if x=y orelse x=z orelse y=z then 1 else 0

___第9章の解答例

9.1 fun len [] = 0 | len (_::xs) = len xs + 1 9.2 fun member (_,[]) = false | member (e,x::xs) = e=x orelse member (e,xs) 9.3 fun maxElem [] = raise Empty | maxElem [x] = x | maxElem (x::xs) = let val maxtail = maxElem xs in if x > maxtail then x else maxtail end 9.4 exception Culture fun culture (n,x) = if n=0 then [] else if n>=1 then x :: culture (n-1,x) else raise Culture 9.5 fun append ([], y) = y | append (x::xs,y) = x :: append (xs,y) 9.6 fun reverse [] = [] | reverse (x::xs) = reverse xs @ [x] 別解 fun reverse x = let fun reverse1 ([], y) = y | reverse1 (x::xs,y) = reverse1 (xs,x::y) in reverse1 (x,[]) end 9.7 fun allEqual [] = true | allEqual (x::xs) = let fun allEqual1 (_,[]) = true | allEqual1 (x,y::ys) = x=y andalso allEqual1 (x,ys) in allEqual1 (x,xs) end 9.8 exception IllegalChar of char fun binaryToInt b = let fun revBinaryToInt [] = 0 | revBinaryToInt (x::xs) = let val ixs = (revBinaryToInt xs * 2) in if x = #"0" then ixs else if x = #"1" then ixs+1 else raise IllegalChar x end in revBinaryToInt (rev b) end 9.9 fun divideEvenOdd [] = ([],[]) | divideEvenOdd (x::xs) = let val (even,odd) = divideEvenOdd xs in if x mod 2 = 0 then (x::even, odd) else ( even,x::odd) end 9.10 fun swapNeighbors [] = [] | swapNeighbors (x::[]) = x::[] | swapNeighbors (x::y::xs) = y :: x :: swapNeighbors xs 9.11 fun multiHead [] = [] | multiHead ([]::xss) = multiHead xss | multiHead ((x::_)::xss) = x :: multiHead xss 9.12 fun multiTail [] = [] | multiTail ([]::xss) = multiTail xss | multiTail ((_::xs)::xss) = xs :: multiTail xss 9.13 fun transpose [] = [] | transpose x = let val mh = multiHead x val mt = multiTail x val tmt = transpose mt in if null mh then tmt else mh::tmt end 9.14 バブルソート fun sortIntList [] = [] | sortIntList [x] = [x] | sortIntList x = let fun bubble [] = [] | bubble [x] = [x] | bubble (x::xs) = let val b = bubble xs val y = hd b in if x < y then y :: x :: tl b else x :: b end val b = bubble x in hd b :: sortIntList (tl b) end クイックソート fun sortIntList [] = [] | sortIntList [x] = [x] | sortIntList x = let exception PivotUnfound fun findpivot [] = raise PivotUnfound | findpivot (x::xs) = let fun findpivot1 (e,[]) = raise PivotUnfound | findpivot1 (e,x::xs) = if e>x then e else if e=p then (x::a,b) else (a,x::b) end in let val (a,b) = partition (findpivot x,x) in sortIntList a @ sortIntList b end handle PivotUnfound => x end 9.15 fun growAllLists (_,[]) = [] | growAllLists (e,x::xs) = (e::x) :: growAllLists (e,xs) 9.16 fun intersection ([], _) = [] | intersection (x::xs,y) = if member (x,y) then x :: intersection (xs,y) else intersection (xs,y) 9.17 fun subset ([], _) = true | subset (x::xs,y) = member (x,y) andalso subset (xs,y) 9.18 fun powerSet [] = [[]] | powerSet (x::xs) = let val ps = powerSet xs in ps @ growAllLists (x,ps) end

___第10章の解答例

10.1 fun equalByFun (f,x,y) = f x = f y 10.2 exception ManyValues fun manyValues (f,n) = if n=0 then [f 0] else if n>=0 then f n :: manyValues (f,n-1) else raise ManyValues 10.3 exception Reduce fun reduce (_,[]) = raise Reduce | reduce (_,[x]) = x | reduce (f,x::xs) = f (x,reduce (f,xs)) 10.4 fun divisible n m = n mod m = 0 10.5 fun curry f x y = f (x,y) 10.6 fun uncurry f (x,y) = f x y 10.7 fun composite (f,g) x = f (g x) 10.8 fun duplicate f x = f (f x) 10.9 fun applyToAllElem f lst = foldr (fn (x,y) => f x :: y) [] lst 10.10 fun listFun _ b [] = b | listFun f b (x::xs) = f (x,listFun f b xs) 10.11 fun mapDouble ilst = map (fn x => x*2) ilst 10.12 fun mapTimes n ilst = map (fn x => x*n) ilst 10.13 fun concatBothSides lst = foldr (fn (x,y) => x^y^x) "" lst 10.14 fun concatLeftRight slst = foldr (fn (x,(y,z)) => (x^y,z^x)) ("","") slst 10.15 fun howLong lst = foldr (fn (_,y) => y+1) 0 lst 10.16 fun duplicateElem lst = foldr (fn (x,y) => x::x::y) [] lst 10.17 fun concatenate lst1 lst2 = foldr (fn (x,y) => x::y) lst2 lst1 10.18 fun deleteTrue f lst = foldr (fn (x,y) => if f x then y else x::y) [] lst 10.19 fun examine flst sample = foldr (fn (x,y) => x sample :: y) [] flst 10.20 fun divideByBool f lst = let fun selectiveGrowth (x,(yt,yf)) = if f x then (x::yt,yf) else (yt,x::yf) in foldr selectiveGrowth ([],[]) lst end 10.21 fun divideLongShort n slst = divideByBool (fn x => size x >= n) slst

___第11章の解答例

11.1 datatype operator = Add | Subtract | Multiply | Divide 11.2 datatype token = TO of operator | TI of int 11.3 datatype number = Zero | Succ of number 11.4 fun addNumber (a,Zero) = a | addNumber (a,Succ b) = Succ(addNumber (a,b)) 11.5 fun multiplyNumber (a,Zero) = Zero | multiplyNumber (a,Succ b) = addNumber (a,multiplyNumber (a,b)) 11.6 fun preorder TEmpty = [] | preorder (TNode(x,left,right)) = x :: preorder left @ preorder right fun inorder TEmpty = [] | inorder (TNode(x,left,right)) = inorder left @ (x :: inorder right) fun postorder TEmpty = [] | postorder (TNode(x,left,right)) = postorder left @ postorder right @ [x] 11.7 fun mirror TEmpty = TEmpty | mirror (TNode(x,left,right)) = TNode(x,mirror right,mirror left) 11.8 exception Minus fun numberedTree n = let fun numberedTree1 (n,m) = if n=0 then TEmpty else if n>0 then TNode(m,numberedTree1 (n-1,m*2), numberedTree1 (n-1,m*2+1)) else raise Minus in numberedTree1 (n,1) end 11.9 exception IllegalTree fun eval (TNode(TI x,TEmpty,TEmpty)) = x | eval (TNode(TO f,left,right)) = let val leftVal = eval left val rightVal = eval right in case f of Add => leftVal + rightVal | Subtract => leftVal - rightVal | Multiply => leftVal * rightVal | Divide => leftVal div rightVal end | eval _ = raise IllegalTree 11.10 fun foldTree _ basis TEmpty = basis | foldTree f basis (TNode(x,left,right)) = f (x,foldTree f basis left,foldTree f basis right) 11.11 fun mapTree f t = foldTree (fn (x,y,z) => TNode(f x,y,z)) TEmpty t 11.12 datatype 'a evenTree = ETEmpty | ETNode of 'a * 'a oddTree * 'a oddTree and 'a oddTree = OTNode of 'a * 'a evenTree * 'a evenTree

___第12章の解答例

12.1 structure Roman = struct exception NotPlus exception TooLarge exception InvalidRoman fun lettersOfFigure 1 = (#"I",#"V",#"X") | lettersOfFigure 10 = (#"X",#"L",#"C") | lettersOfFigure 100 = (#"C",#"D",#"M") | lettersOfFigure 1000 = (#"M",#" ",#" ") | lettersOfFigure _ = (#" ",#" ",#" ") fun figureIntToRoman (nfig,num) = let val (one,five,ten) = lettersOfFigure nfig in case num of 1 => [one] | 2 => [one,one] | 3 => [one,one,one] | 4 => [one,five] | 5 => [five] | 6 => [five,one] | 7 => [five,one,one] | 8 => [five,one,one,one] | 9 => [one,ten] | _ => [] end fun intToRoman1 (_,0) = [] | intToRoman1 (nfig,n) = intToRoman1 (nfig*10,n div 10) @ figureIntToRoman (nfig,n mod 10) fun intToRoman n = if n<=0 then raise NotPlus else if n>=4000 then raise TooLarge else implode (intToRoman1 (1,n)) fun figureRomanToInt (_,[]) = 0 | figureRomanToInt ((one,five,_),[a]) = if a=one then 1 else if a=five then 5 else raise InvalidRoman | figureRomanToInt ((one,five,ten),[a,b]) = if a=one andalso b=one then 2 else if a=one andalso b=five then 4 else if a=five andalso b=one then 6 else if a=one andalso b=ten then 9 else raise InvalidRoman | figureRomanToInt ((one,five,_),[a,b,c]) = if a=one andalso b=one andalso c=one then 3 else if a=five andalso b=one andalso c=one then 7 else raise InvalidRoman | figureRomanToInt ((one,five,_),[a,b,c,d]) = if a=five andalso b=one andalso c=one andalso d=one then 8 else raise InvalidRoman | figureRomanToInt _ = raise InvalidRoman fun extractFigure (_,[]) = ([],[]) | extractFigure ((one,five,ten),f::fs) = if f = #" " then extractFigure ((one,five,ten),fs) else if f=one orelse f=five orelse f=ten then let val (figure,rest) = extractFigure ((one,five,ten),fs) in (f::figure,rest) end else ([],f::fs) fun romanToInt1 (0,[]) = 0 | romanToInt1 (0,_) = raise InvalidRoman | romanToInt1 (nfig,r) = let val figletter = lettersOfFigure nfig val (figure,rest) = extractFigure (figletter,r) in figureRomanToInt (figletter,figure) * nfig + romanToInt1 (nfig div 10,rest) end fun romanToInt r = romanToInt1 (1000,explode r) end 12.2 structure Token = struct fun isOperator #"+" = true | isOperator #"-" = true | isOperator #"*" = true | isOperator #"/" = true | isOperator _ = false fun isEmptyExp [] = true | isEmptyExp (#" "::xs) = isEmptyExp xs | isEmptyExp _ = false fun isOperatorFirst [] = false | isOperatorFirst (#" "::xs) = isOperatorFirst xs | isOperatorFirst (x::_) = isOperator x fun extractOperator [] = ([],[]) | extractOperator (#" "::rest) = extractOperator rest | extractOperator (operator::rest) = ([operator],rest) fun extractToken [] = ([],[]) | extractToken (#" "::xs) = extractToken xs | extractToken (x::xs) = if isOperator x then ([],x::xs) else let val (first,rest) = extractToken xs in (x::first,rest) end fun expToTokens1 exp = if isEmptyExp exp then [] else if isOperatorFirst exp then let val (operator,rest) = extractOperator exp in implode operator :: expToTokens1 rest end else let val (first,rest) = extractToken exp in implode first :: expToTokens1 rest end fun expToTokens exp = expToTokens1 (explode exp) end 12.3 structure Eval = struct local open Roman Token in exception InvalidExpression fun operate (left,#"+",right) = left + right | operate (left,#"-",right) = left - right | operate (left,#"*",right) = left * right | operate (left,#"/",right) = left div right | operate _ = raise InvalidExpression fun eval1 [] = raise InvalidExpression | eval1 [x] = romanToInt x | eval1 [_,_] = raise InvalidExpression | eval1 (right::operator::left) = case explode operator of [] => raise InvalidExpression | (cope::_) => if isOperator cope then operate (eval1 left, cope, romanToInt right) else raise InvalidExpression fun eval exp = intToRoman (eval1 (rev (expToTokens exp))) end end

___第13章の解答例

13.1 signature ROMAN = sig exception NotPlus exception TooLarge exception InvalidRoman val intToRoman : int -> string val romanToInt : string -> int end 13.2 signature TOKEN = sig val isOperator : char -> bool val expToTokens : string -> string list end 13.3 signature EVAL = sig exception InvalidExpression val eval : string -> string end 13.4 signature QUEUE = sig type 'a queue exception EmptyQueue val create : 'a queue val enqueue : 'a queue * 'a -> 'a queue val dequeue : 'a queue -> 'a queue val front : 'a queue -> 'a val isEmpty : 'a queue -> bool end structure Queue = struct datatype 'a queue = Empty | Node of 'a * 'a queue exception EmptyQueue val create = Empty fun enqueue (Empty,y) = Node (y,Empty) | enqueue (Node (x,xs),y) = Node (x,enqueue (xs,y)) fun dequeue Empty = raise EmptyQueue | dequeue (Node (_,xs)) = xs fun front Empty = raise EmptyQueue | front (Node (x,_)) = x fun isEmpty Empty = true | isEmpty _ = false end : QUEUE 13.5 signature DICT = sig type ('a,'b) dict exception Undefined val create : ('a,'b) dict val insert : (''a,'b) dict * ''a * 'b -> (''a,'b) dict val delete : (''a,'b) dict * ''a -> (''a,'b) dict val lookup : (''a,'b) dict * ''a -> 'b end structure Dict = struct datatype ('a,'b) dict = Empty | Node of ('a * 'b) * ('a,'b) dict exception Undefined val create = Empty fun insert (Empty,key,value) = Node ((key,value),Empty) | insert (Node ((x,y),dict),key,value) = if x=key then Node ((key,value),dict) else Node ((x,y),insert (dict,key,value)) fun delete (Empty,_) = raise Undefined | delete (Node ((key,value),dict),x) = if key=x then dict else (Node ((key,value),delete (dict,x))) fun lookup (Empty,_) = raise Undefined | lookup (Node ((key,value),dict),x) = if key=x then value else lookup (dict,x) end : DICT

___第14章の解答例

14.1 signature TOKEN = sig val expToTokens : string -> string list end functor TokenFUN (Operator : OPERATOR) = struct (* この部分は、練習問題12.2で定義したTokenという ストラクチャーの内部とほとんど同じですので、 省略します。 *) end : TOKEN 14.2 signature SIMSET = sig eqtype T type 'a set val create : T set val insert : T * T set -> T set val member : T * T set -> bool val simMembers : T * T set -> T set end functor SimSetFUN (Similar : SIMILAR) = struct type T = Similar.T datatype 'a set = Empty | Node of 'a * 'a set val create = Empty fun insert (x,set) = Node (x,set) fun member (x,Empty) = false | member (x,Node (y,tail)) = if x=y then true else member (x,tail) fun simMembers (x,Empty) = Empty | simMembers (x,Node (y,tail)) = if Similar.isSim (x,y) then Node (y,simMembers (x,tail)) else simMembers (x,tail) end : SIMSET

___第15章の解答例

15.1 fun assignDummy sr = sr := "dummy" 15.2 fun assignDupString sr = sr := !sr ^ !sr 15.3 fun assignCatString (sr,tr) = sr := !sr ^ !tr 15.4 fun rotate (rx,ry,rz) = let val w = ! rx in rx := !ry; ry := !rz; rz := w end 15.5 fun ppower (nr,mr,pr) = let val ir = ref 1 in pr := 1; while !ir <= !mr do ( pr := !pr * !nr; ir := !ir + 1 ) end 15.6 fun pgcm (nr,mr,pr) = let val rx = ref (!nr) val ry = ref (!mr) val rz = ref 0 in while !ry > 0 do ( rz := !rx mod !ry; rx := !ry; ry := !rz ); pr := !rx end 15.7 fun pfibona (nr,fr) = let val rx = ref 1 val ry = ref 1 val rz = ref 0 val ir = ref 1 in while !ir < !nr do ( rz := !rx + !ry; rx := !ry; ry := !rz; ir := !ir + 1 ); fr := !ry end

___第16章の解答例

16.1 fun headOfFile filename = let open TextIO val instr = openIn filename val c = ref NONE in c := input1 instr; closeIn instr; !c end 16.2 fun countChar (filename,c) = let open TextIO val instr = openIn filename fun countChar1 () = if endOfStream instr then 0 else if (input1 instr) = SOME c then 1 + countChar1 () else countChar1 () val n = ref 0 in n := countChar1 (); closeIn instr; !n end 16.3 fun sumIntegers filename = let open TextIO val instr = openIn filename fun inputInteger () = getOpt (Int.fromString (inputLine instr),0) fun sumIntegers1 () = if endOfStream instr then 0 else inputInteger () + sumIntegers1 () val sum = ref 0 in sum := sumIntegers1 (); closeIn instr; !sum end 16.4 fun leftMargin (n,infilename,outfilename) = let fun nSpaces n = if n>=1 then " " ^ nSpaces (n-1) else "" open TextIO val instr = openIn infilename val outstr = openOut outfilename in while not (endOfStream instr) do output (outstr, nSpaces n ^ inputLine instr); closeIn instr; closeOut outstr end 16.5 fun addLineNumber (infilename,outfilename) = let open TextIO val instr = openIn infilename val outstr = openOut outfilename val lnum = ref 1 in while not (endOfStream instr) do ( output (outstr, makestring (!lnum) ^ " " ^ inputLine instr); lnum := !lnum + 1 ); closeIn instr; closeOut outstr end 16.6 fun insertSpace (infilename,outfilename) = let open TextIO val instr = openIn infilename val outstr = openOut outfilename in while not (endOfStream instr) do ( output1 (outstr,valOf (input1 instr)); output1 (outstr,#" ") ); closeIn instr; closeOut outstr end 16.7 fun encloseNthChar (n,infilename,outfilename) = let open TextIO val instr = openIn infilename val outstr = openOut outfilename val count = ref 1 in while not (endOfStream instr) do ( if !count = n then ( output1 (outstr,#"["); output1 (outstr,valOf (input1 instr)); output1 (outstr,#"]") ) else output1 (outstr,valOf (input1 instr)); count := !count + 1 ); closeIn instr; closeOut outstr end 16.8 fun parenDepth (infilename,outfilename) = let open TextIO val instr = openIn infilename val outstr = openOut outfilename val depth = ref 0 val c = ref #" " in while not (endOfStream instr) do ( c := valOf (input1 instr); if !c = #"(" then ( output1 (outstr,#"("); output (outstr,makestring (!depth)); depth := !depth + 1 ) else if !c = #")" then ( output1 (outstr,#")"); depth := !depth - 1; output (outstr,makestring (!depth)) ) else output1 (outstr,!c) ); closeIn instr; closeOut outstr end 16.9 fun stdInToFile filename = let open TextIO val outstr = openOut filename in output (outstr,inputLine stdIn); closeOut outstr end 16.10 fun fileToStdErr filename = let open TextIO val instr = openIn filename in output (stdErr,inputAll instr); closeIn instr end

___第17章の解答例

17.1 fun reverse v = let open Vector val len = length v in tabulate (len,fn n => sub (v,len-1-n)) end 17.2 exception NotSameLength fun unifyVectors f (va,vb) = let open Vector val alen = length va val blen = length vb fun rule n = f (sub (va,n),sub (vb,n)) in if alen<>blen then raise NotSameLength else tabulate (alen,rule) end 17.3 exception NotPlus fun divide (v,divisor) = if divisor<=0 then raise NotPlus else let open Vector val vlen = length v val alen = vlen div divisor fun rule n = extract (v,n*divisor,SOME divisor) val divv = tabulate (alen,rule) in if vlen mod divisor = 0 then divv else concat [divv, #[extract (v,alen*divisor,NONE)]] end 17.4 fun matrixTabulate (r,c,f) = let open Vector in tabulate (r,fn n => tabulate (c,f n)) end 17.5 fun replace (v,x,y) = let open Vector fun rule n = let val w = sub (v,n) in if w=x then y else w end in tabulate (length v,rule) end 17.6 fun count f v = let open Vector val len = length v val count = ref 0 val ir = ref 0 in while !ir100 then raise Range (!ir) else count (disa,score div 10) end; ir := !ir + 1 ); extract (disa,0,NONE) end 17.10 fun unused v = let open Array val vlen = Vector.length v val unvr = ref #[] val unua = array (10,true) val ir = ref 0 in while !ir100 then raise Range (!ir) else update (unua,x,false) end; ir := !ir + 1 ); ir := 0; while !ir<10 do ( if sub (unua,!ir) then unvr := Vector.concat [!unvr,#[!ir]] else (); ir := !ir + 1 ); !unvr end 17.11 exception LessThanTwo fun primeNumbers n = if n<2 then raise LessThanTwo else let open Array val pvr = ref #[] val sieve = array (n+1,true) val ir = ref 2 val jr = ref 0 in while !ir<=n do ( if sub (sieve,!ir) then ( jr := 2; while !ir * !jr <= n do ( update (sieve,!ir * !jr,false); jr := !jr + 1 ) ) else (); ir := !ir + 1 ); ir := 0; while !ir<=n do ( if sub (sieve,!ir) then pvr := Vector.concat [!pvr,#[!ir]] else (); ir := !ir + 1 ); Vector.extract (!pvr,2,NONE) end 17.12 exception Minus fun intToDecimal n = let open Vector val nr = ref n val vr = ref #[] val ir = ref 0 in if n<0 then raise Minus else ( while !nr>0 do ( vr := concat [!vr,#[!nr mod 10]]; nr := !nr div 10 ); !vr ) end 17.13 exception MinusDigit of int fun normalize d = let open Array val slen = Vector.length d fun rule n = Vector.sub (d,n) val da = tabulate (slen,rule) val carryr = ref 0 val ir = ref 0 in while !ir raise MinusA val ndb = normalize db handle MinusDigit _ => raise MinusB val alen = Vector.length nda val blen = Vector.length ndb val len = if alen>blen then alen else blen val adda = array (len,0) val ir = ref 0 in while !ir raise MinusA val ndb = normalize db handle MinusDigit _ => raise MinusB val alen = Vector.length nda val blen = Vector.length ndb val mula = array (alen+blen-1,0) val ir = ref 0 val jr = ref 0 in while !ir

___第18章の解答例

18.1 exception NotFound fun lookupStudent ([],_) = raise NotFound | lookupStudent ({stnum,stname}::xs,n) = if stnum=n then stname else lookupStudent (xs,n) 18.2 fun extractSubject ([],_) = [] | extractSubject ({subnum,stnum}::xs,n) = if subnum=n then stnum :: extractSubject (xs,n) else extractSubject (xs,n) 18.3 fun rollWithName (numlist,st) = let fun rollWithName1 [] = [] | rollWithName1 (n::xs) = {stnum=n,stname=lookupStudent (st,n)} :: rollWithName1 xs in rollWithName1 numlist end 18.4 fun allRolls take = let fun isMember ([],_) = false | isMember (x::xs,e) = x=e orelse isMember (xs,e) fun unique [] = [] | unique (x::xs) = if isMember (xs,x) then unique xs else x :: unique xs fun subjects [] = [] | subjects ({subnum,stnum}::xs) = subnum :: subjects xs fun allRolls1 [] = [] | allRolls1 (x::xs) = {subnum=x,stlist=extractSubject (take,x)} :: allRolls1 xs in allRolls1 (unique (subjects take)) end 18.5 fun examRanking exam = let fun extMark [] = [] | extMark ({stnum,mark}::xs) = mark :: extMark xs fun ranking1 (_,[]) = 1 | ranking1 (e,x::xs) = if e

___第19章の解答例

19.1 fun word8ToBoolList w = let fun wtobl (w,0) = [] | wtobl (w,n) = (Word8.andb (w,0wx80) = 0wx80) :: wtobl (Word8.<< (w,0wx01),n-1) in wtobl (w,8) end 19.2 exception LengthIsNotEight fun boolListToWord8 bl = let fun bltw [] = 0wx00 | bltw (x::xs) = let val w = Word8.>> (bltw xs,0wx01) in if x then Word8.orb (w,0wx80) else w end in if length bl <> 8 then raise LengthIsNotEight else bltw bl end 19.3 fun word8ToNumberList w = let fun wtonl (w,8) = [] | wtonl (w,n) = let val w1 = wtonl (Word8.<< (w,0wx01),n+1) in if Word8.andb (w,0wx80) = 0wx80 then n :: w1 else w1 end in wtonl (w,0) end 19.4 fun numberListToWord8 nl = let fun isMember (_,[]) = false | isMember (e,x::xs) = e=x orelse isMember (e,xs) val w = ref 0wx00 val i = ref 0 in while !i < 8 do ( w := Word8.<< (!w,0wx01); if isMember (!i,nl) then w := Word8.orb (!w,0wx01) else (); i := !i + 1 ); !w end 19.5 fun reverseWord8 w = let val ori = ref w val rev = ref 0wx00 val i = ref 0 in while !i < 8 do ( rev := Word8.>> (!rev,0wx01); if Word8.andb (!ori,0wx80) = 0wx80 then rev := Word8.orb (!rev,0wx80) else (); ori := Word8.<< (!ori,0wx01); i := !i + 1 ); !rev end 19.6 fun binaryToText (infilename,outfilename) = let open TextIO val instr = BinIO.openIn infilename val outstr = openOut outfilename val hex = ref "" val count = ref 0 in while not (BinIO.endOfStream instr) do ( hex := Word8.toString (valOf (BinIO.input1 instr)); if size (!hex) = 1 then output (outstr,"0" ^ !hex) else output (outstr,!hex); count := !count + 1; if !count=16 then ( output1 (outstr,#"\n"); count := 0 ) else output1 (outstr,#" ") ); if !count<>0 then output1 (outstr,#"\n") else (); BinIO.closeIn instr; closeOut outstr end 19.7 fun textToBinary (infilename,outfilename) = let open TextIO Array val instr = openIn infilename val outstr = BinIO.openOut outfilename val hex = array (2,#" ") fun outByte () = BinIO.output1 (outstr, valOf (Word8.fromString (implode [sub (hex,0),sub (hex,1)]))) val c = ref #" " val index = ref 0 in while not (endOfStream instr) do ( c := valOf (input1 instr); if Char.isHexDigit (!c) then ( update (hex,!index,!c); index := !index + 1; if !index=2 then ( outByte (); index := 0 ) else () ) else () ); if !index=1 then ( update (hex,1,#"0"); outByte () ) else (); closeIn instr; BinIO.closeOut outstr end 19.8 fun encodeByBase64 (infilename,outfilename) = let open TextIO Array val instr = BinIO.openIn infilename val outstr = openOut outfilename val bytes = array (3,0wx00) val base64chars = #[#"A",#"B",#"C",#"D",#"E",#"F",#"G",#"H", #"I",#"J",#"K",#"L",#"M",#"N",#"O",#"P", #"Q",#"R",#"S",#"T",#"U",#"V",#"W",#"X", #"Y",#"Z",#"a",#"b",#"c",#"d",#"e",#"f", #"g",#"h",#"i",#"j",#"k",#"l",#"m",#"n", #"o",#"p",#"q",#"r",#"s",#"t",#"u",#"v", #"w",#"x",#"y",#"z",#"0",#"1",#"2",#"3", #"4",#"5",#"6",#"7",#"8",#"9",#"+",#"/"] fun wtoc w = Vector.sub (base64chars,Word8.toInt w) fun first () = wtoc (Word8.>> (sub (bytes,0),0wx02)) fun second () = wtoc (Word8.orb ( Word8.<< (Word8.andb (sub (bytes,0),0wx03),0wx04), Word8.>> (sub (bytes,1),0wx04))) fun third () = wtoc (Word8.orb ( Word8.<< (Word8.andb (sub (bytes,1),0wx0f),0wx02), Word8.>> (sub (bytes,2),0wx06))) fun fourth () = wtoc (Word8.andb (sub (bytes,2),0wx3f)) fun outFourChars () = ( output1 (outstr,first ()); output1 (outstr,second ()); output1 (outstr,third ()); output1 (outstr,fourth ()) ) fun outThreeChars () = ( update (bytes,2,0wx00); output1 (outstr,first ()); output1 (outstr,second ()); output1 (outstr,third ()); output1 (outstr,#"=") ) fun outTwoChars () = ( update (bytes,1,0wx00); output1 (outstr,first ()); output1 (outstr,second ()); output (outstr,"==") ) val count = ref 0 val index = ref 0 in while not (BinIO.endOfStream instr) do ( update (bytes,!index, (valOf (BinIO.input1 instr))); index := !index + 1; if !index=3 then ( outFourChars (); index := 0; count := !count + 1; if !count=15 then ( output1 (outstr,#"\n"); count := 0 ) else () ) else () ); if !index=1 then outTwoChars () else if !index=2 then outThreeChars () else (); output1 (outstr,#"\n"); BinIO.closeIn instr; closeOut outstr end 19.9 exception IllegalChar of char exception NotMultipleOfFour fun decodeByBase64 (infilename,outfilename) = let open BinIO Array Char val instr = TextIO.openIn infilename val outstr = openOut outfilename val chars = array (4,#" ") val word0 = ref 0wx00 val word1 = ref 0wx00 val word2 = ref 0wx00 fun isBase64Char c = Char.isAlphaNum c orelse c= #"+" orelse c= #"/" orelse c= #"=" fun ctoi c = if isUpper c then (ord c) - (ord #"A") else if isLower c then (ord c) - (ord #"a") + 26 else if isDigit c then (ord c) - (ord #"0") + 52 else if c= #"+" then 62 else if c= #"/" then 63 else 0 fun ctow c = Word8.fromInt (ctoi c) fun charsToBytes () = let val char0 = ctow (sub (chars,0)) val char1 = ctow (sub (chars,1)) val char2 = ctow (sub (chars,2)) val char3 = ctow (sub (chars,3)) in word0 := Word8.orb ( Word8.<< (char0,0wx02), Word8.>> (char1,0wx04)); word1 := Word8.orb ( Word8.<< (char1,0wx04), Word8.>> (char2,0wx02)); word2 := Word8.orb ( Word8.<< (char2,0wx06), char3) end fun outBytes () = ( charsToBytes (); output1 (outstr,!word0); if sub (chars,2) = #"=" then () else ( output1 (outstr,!word1); if sub (chars,3) = #"=" then () else output1 (outstr,!word2) ) ); val c = ref #" " val index = ref 0 in while not (TextIO.endOfStream instr) do ( c := valOf (TextIO.input1 instr); if !c= #"\n" then () else ( if isBase64Char (!c) then () else ( closeOut outstr; raise IllegalChar (!c) ); update (chars,!index,!c); index := !index + 1; if !index=4 then ( outBytes (); index := 0 ) else () ) ); if !index=0 then () else raise NotMultipleOfFour; TextIO.closeIn instr; closeOut outstr end

___第20章の解答例

20.1 fun fileSearch (path,name) = let open OS.FileSys val path2 = if path="/" then "" else path val dirstr = openDir path fun fileSearch1 () = let val s = readDir dirstr val subpath = path2 ^ "/" ^ s in if s="" then [] else if isDir subpath then fileSearch (subpath,name) @ fileSearch1 () else if s=name then subpath :: fileSearch1 () else fileSearch1 () end val sl = ref [] in sl := fileSearch1 (); closeDir dirstr; !sl end 20.2 fun dirSize path = let open OS.FileSys val path2 = if path="/" then "" else path val dirstr = openDir path fun dirSize1 () = let val s = readDir dirstr val subpath = path2 ^ "/" ^ s in if s="" then 0 else if isDir subpath then dirSize subpath + dirSize1 () else fileSize subpath + dirSize1 () end val size = ref 0 in size := dirSize1 (); closeDir dirstr; !size end 20.3 fun recursiveSystem (cmd,path) = let open OS.FileSys OS.Process val path2 = if path="/" then "" else path val dirstr = openDir path fun recursiveSystem1 () = let val s = readDir dirstr val subpath = path2 ^ "/" ^ s in if s="" then () else if isDir subpath then ( recursiveSystem (cmd,subpath); recursiveSystem1 () ) else ( system (cmd ^ " " ^ subpath); recursiveSystem1 () ) end in recursiveSystem1 (); closeDir dirstr end 20.4 fun memo s = let open OS.Process TextIO Date val memodir = getOpt (getEnv "MEMO","") val path = if memodir="" then "memo.txt" else memodir ^ "/memo.txt" val mtime = toString (fromTimeLocal (Time.now ())) val outstr = openAppend path in output (outstr,mtime ^ ": " ^ s ^ "\n"); closeOut outstr end 20.5 fun dirNewFile (path,day) = let open OS.FileSys Time val border = now () - fromSeconds (day * 86400) fun dirNewFile1 path = let val path2 = if path="/" then "" else path val dirstr = openDir path fun isNew path = modTime path >= border fun dirNewFile2 () = let val s = readDir dirstr val subpath = path2 ^ "/" ^ s in if s="" then [] else if isDir subpath then dirNewFile1 subpath @ dirNewFile2 () else if isNew subpath then subpath :: dirNewFile2 () else dirNewFile2 () end val sl = ref [] in sl := dirNewFile2 (); closeDir dirstr; !sl end in dirNewFile1 path end

付録B===Standard ML基本ライブラリーの主要な関数

___この付録について

この付録は、Standard ML基本ライブラリーを構成している たくさんの関数の中から、とても重要だと思われるものを 選び出して、それらの関数について 説明したものです(少しですが、Real.Math.pi、Word.wordSize、 String.maxSizeなどのような、関数ではないものも 含まれています)。 なお、この付録で紹介されているもの以外に、 Standard ML基本ライブラリーでどのような関数が 定義されているのか、ということについて知りたい場合は、 http://www.cs.bell-labs.com/~jhr/sml/basis/pages/ sml-std-basis.html を参照するといいでしょう。

___組み込み関数

「組み込み関数」というのはMLの正式な用語ではないのですが、 この文章(初級ML講座)では、「ライブラリーに含まれている 関数のうちでトップレベル環境にあるもの」という意味でこの言葉を 使っています。 トップレベル環境には、異なるいくつかの型を持つ関数に 束縛されている識別子が存在しています。そこで、そのような関数の 型を記述するために、便宜的に、次のような型の名前を 使うことにします。 wordint wordとintの和集合 realint realとintの和集合 num wordとrealintの和集合 numtext numとstringの和集合 次の識別子は、トップレベル環境で演算子として宣言されています。 演算子 優先順位 結合規則 * / div mod 7 左 + - ^ 6 左 :: @ 5 右 = <> > >= < <= 4 左 := o 3 左 before 0 左
算術演算
~ x [動作] の符号を反転させた結果を返します。xの型がintで、 計算の結果がintの範囲を超えた場合は、Overflowという例外を 発生させます。 [型] realint -> realint [例] - ~ ~53; > val it = 53 : int abs x [動作] xの絶対値を返します。xの型がintで、計算の結果がintの 範囲を超えた場合は、Overflowという例外を発生させます。 [型] realint -> realint [例] - abs ~53; > val it = 53 : int n + m n - m n * m [動作] +はnとmを加算し、-はnからmを減算し、*はnとmを乗算し、 その結果を返します。その結果が、それぞれの型で表現できる範囲を 超えた場合は、Overflowという例外を発生させます。 [型] num * num -> num [例] > 8 * 7; - val it = 56 : int i div j [動作] iをjで除算して、その数値を上回らない最大の整数を 返します。その結果が、それぞれの型で表現できる範囲を超えた 場合は、Overflowという例外を発生させます。jが0だった場合は、 Divという例外を発生させます。 [型] wordint * wordint -> wordint [例] - ~60 div 7; > val it = ~9 : int i mod j [動作] iをjで除算したときの余りを返します。jが0だった場合は、 Divという例外を発生させます。この関数は、divと対になっていて、 (i div j) * j + (i mod j) = iという関係が常に 成り立つようになっています。 [型] wordint * wordint -> wordint [例] - ~60 mod 7; > val it = 3 : int r / s [動作] rをsで除算した結果を返します。 [型] real * real -> real [例] - 60.0 / 7.0; > val it = 8.57142857143 : real
比較演算
x > y x < y x >= y x <= y [動作] xとyとのあいだの大小関係を調べて、その結果の真偽値を 返します。文字列の大小関係というのは、文字列を辞書の順序で 並べたときに、うしろにあるものほど大きい、という 関係のことです。 [型] numtext * numtext -> bool [例] - "neutron" >= "neurosis"; > val it = true : bool - "neurosis" >= "neutron"; > val it = false : bool x = y [動作] xとyとが等しいならば真、そうでなければ偽を返します。 [型] ''a * ''a -> bool [例] - 63 = 63; > val it = true : bool - 63 = 52; > val it = false : bool x <> y [動作] xとyとが等しくないならば真、そうでなければ偽を 返します。 [型] ''a * ''a -> bool [例] - 63 <> 63; > val it = false : bool - 63 <> 52; > val it = true : bool
型の変換
real i [動作] 整数iを、同じ数値をあらわす実数に変換して、その結果を 返します。 [型] int -> real [例] - real 93; > val it = 93.0 : real trunc r [動作] 実数rの小数点の右側を切り捨てて、その結果を返します。 [型] real -> int [例] - trunc 6.2; > val it = 6 : int - trunc ~6.2; > val it = ~6 : int floor [動作] 実数rを上回らない最大の整数を求めて、その結果を 返します。 [型] real -> int [例] - floor 6.2; > val it = 6 : int - floor ~6.2; > val it = ~7 : int ceil [動作] 実数rを下回らない最大の整数を求めて、その結果を 返します。 [型] real -> int [例] - ceil 6.2; > val it = 7 : int - ceil ~6.2; > val it = ~6 : int round [動作] 実数rの0.1の桁を四捨五入して、その結果を返します。 [型] real -> int [例] - round 6.2; > val it = 6 : int - round 6.7; > val it = 7 : int chr i [動作] 整数iと同じビット列のパターンであらわされる文字を 返します。 [型] int -> char [例] - chr 63; > val it = #"?" : char ord c [動作] 文字cと同じビット列のパターンであらわされる整数を 返します。 [型] char -> int [例] - ord #"?"; > val it = 63 : int str c [動作] 文字cのみから構成される文字列を作って、その結果を 返します。 [型] char -> string [例] - str #"?"; > val it = "?" : string implode cs [動作] 文字のリストcsを構成するそれぞれの文字を同じ順序で 並べることによってできる文字列を返します。 [型] char list -> string [例] - implode [#"t",#"r",#"i",#"l",#"o",#"g",#"y"]; > val it = "trilogy" : string explode s [動作] 文字列sを構成するそれぞれの文字を同じ順序で 並べることによってできる、文字のリストを返します。 [型] string -> char list [例] - explode "trilogy"; > val it = [#"t", #"r", #"i", #"l", #"o", #"g", #"y"] : char list vector xs [動作] リストxsを構成するそれぞれの要素を同じ順序で 並べることによってできるベクトルを返します。 [型] 'a list -> 'a vector [例] - vector [71,33,44,80,67,51]; > val it = #[71, 33, 44, 80, 67, 51] : int vector
文字列
size s [動作] 文字列sの長さ(sに含まれる文字の個数)を返します。 [型] string -> int [例] - size "Harla Branno"; > val it = 12 : int s ^ t [動作] 文字列sと文字列tとを連結することによってできる文字列を 返します。文字列の長さがmaxSizeを超えた場合は、Sizeという 例外を発生させます。 [型] string * string -> string [例] - "Earth" ^ "Terminus"; > val it = "EarthTerminus" : string concat ss [動作] 文字列のリストssを構成するそれぞれの文字列を 連結することによってできる文字列を返します。文字列の長さが maxSizeを超えた場合は、Sizeという例外を発生させます。 [型] string list -> string [例] - concat ["Haven","Kalgan","Radole","Trantor"]; > val it = "HavenKalganRadoleTrantor" : string substring (s,i,n) [動作] 文字列sのi番目の文字から始まる、長さがnの文字列を 返します(先頭の文字を0番目と数えます)。そのような文字列が 存在しない場合は、Subscriptという例外を発生させます。 [型] string * int * int -> string [例] - substring ("nevernever",2,5); > val it = "verne" : string
リスト
ht xs [動作] リストxsの頭部を返します。 [型] 'a list -> 'a [例] - hd [71,33,44,80,67,51]; > val it = 71 : int tl xs [動作] リストxsの尾部を返します。 [型] 'a list -> 'a list [例] - tl [71,33,44,80,67,51]; > val it = [33, 44, 80, 67, 51] : int list x :: xs [動作] リストxsの左側にxを連結して、その結果を返します。 [型] 'a * 'a list -> 'a list [例] - 71 :: [33,44,80,67,51]; > val it = [71, 33, 44, 80, 67, 51] : int list xs @ ys [動作] リストxsとリストysとを連結して、その結果を返します。 [型] 'a list * 'a list -> 'a list [例] - [14,29,50,31] @ [22,17,64]; > val it = [14, 29, 50, 31, 22, 17, 64] : int list null xs [動作] リストxsが空リストならば真を返し、そうでなければ偽を 返します。 [型] 'a list -> bool [例] - null [71,33,44,80,67,51]; > val it = false : bool - null []; > val it = true : bool length xs [動作] リストxsの長さ(xsに含まれる要素の個数)を返します。 [型] 'a list -> int [例] - length [71,33,44,80,67,51]; > val it = 6 : int rev xs [動作] リストxsを構成するそれぞれの要素を逆の順序に 並べ替えることによってできるリストを返します。 [型] 'a list -> 'a list [例] - rev [71,33,44,80,67,51]; > val it = [51, 67, 80, 44, 33, 71] : int list app f xs [動作] リストxsを構成するそれぞれの要素に対して、左から右へ 順番に関数fを適用して、 ユニットを返します。 [型] ('a -> unit) -> 'a list -> unit [例] - app (fn x => (print x; print "\n")) ["Start of the Search","Conspirator", "Interlude in Space"]; Start of the Search Conspirator Interlude in Space > val it = () : unit map f xs [動作] リストxsを構成するそれぞれの要素に対して、左から右へ 順番に関数fを適用して、fが返したデータから構成されるリストを 返します。 [型] ('a -> 'b) -> 'a list -> 'b list [例] - map ord [#"+",#"-",#"*",#"/",#">",#"<",#"=",#"^"]; > val it = [43, 45, 42, 47, 62, 60, 61, 94] : int list foldr f b xs [動作] xsが[x1, x2, ..., x(n-1), xn]だとするとき、 f (x1, f (x2, ..., f (x(n-1), f (xn, b))...)) を評価することによって得られた値を返します。xsが空リストだった 場合は、bを返します。 [型] ('a * 'b -> 'b) -> 'b -> 'a list -> 'b [例] - foldr (fn (s,t) => s^"{"^t^"}") "" ["sphere","cube","cylinder","cone"]; > val it = "sphere{cube{cylinder{cone{}}}}" : string foldl f b xs [動作] xsが[x1, x2, ..., x(n-1), xn]だとするとき、 f (xn, f(x(n-1), ..., f (x2, f (x1, b))...)) を評価することによって得られた値を返します。xsが空リストだった 場合は、bを返します。 [型] ('a * 'b -> 'b) -> 'b -> 'a list -> 'b [例] - foldl (fn (s,t) => s^"{"^t^"}") "" ["sphere","cube","cylinder","cone"]; > val it = "cone{cylinder{cube{sphere{}}}}" : string
参照
ref x [動作] xの型のデータを記憶することのできる場所を作って、 xをその場所に記憶させて、その場所を指示する参照を返します。 [型] 'a -> 'a ref [例] - val r = ref 43; > val r = ref 43 : int ref - !r; > val it = 43 : int ! xr [動作] 参照xrによって指示される場所に記憶されているデータを 返します。 [型] 'a ref -> 'a [例] refの例を参照してください。 xr := x [動作] 参照xrによって指示される場所にxを代入して、ユニットを 返します。 [型] 'a ref * 'a -> unix [例] - !r; > val it = 43 : int - r := 77; > val it = () : unit - !r; > val it = 77 : int
オプション型のデータ
getOpt (xo,y) [動作] オプション型のデータxoがSOME xならばxを返し、xoが NONEならばyを返します。 [型] 'a option * 'a -> 'a [例] - getOpt (SOME 88,100); > val it = 88 : int - getOpt (NONE,100); > val it = 100 : int isSome xo [動作] オプション型のデータxoがSOME xならば真を返し、 そうでなければ偽を返します。 [型] 'a option -> bool [例] - isSome (SOME 88); > val it = true : bool - isSome NONE; > val it = false : bool valOf xo [動作] オプション型のデータxoがSOME xならばxを返し、 そうでなければOptionという例外を発生させます。 [型] 'a option -> 'a [例] - valOf (SOME 88); > val it = 88 : int
例外
exnName ex [動作] 例外exの名前を返します。 [型] exn -> string [例] - exception Unknown; > exn Unknown = Unknown : exn - exnName Unknown; > val it = "Unknown" : string exnMessage ex [動作] 例外exに対応するメッセージを返します。 [型] exn -> string [例] - exception Unknown; > exn Unknown = Unknown : exn - exnMessage Unknown; > val it = "Unknown" : string
その他
x before () [動作] xを返します。 [型] 'a * unit -> 'a [例] - 40 before (print "Mandell Gruber\n"); Mandell Gruber > val it = 40 : int ignore x [動作] ユニットを返します。 [型] 'a -> unit [例] - fun tee s = (print s; print "\n"; s); > val tee = fn : string -> string - app (ignore o tee) ["Convert","Death of a Psychologist","End of Search"]; Convert Death of a Psychologist End of Search > val it = () : unit f o g [動作] 「引数に対して関数gを適用して、その結果に対して関数fを 適用する」という動作をする関数を作って、その結果を返します。 [型] ('b -> 'c) * ('a -> 'b) -> 'a -> 'c [例] - (implode o rev o explode) "dodecahedron"; > val it = "nordehacedod" : string not b [動作] 真偽値bが真ならば偽を返し、そうでなければ真を返します。 [型] bool -> bool [例] - not true; > val it = false : bool - not false; > val it = true : bool print s [動作] 文字列sを標準出力に出力して、ユニットを返します。 [型] string -> bool [例] - print "Bayta Darell\n"; Bayta Darell > val it = () : unit use s [動作] パス名sによって指定されるファイルから、MLで書かれた プログラムを読み込んで、ユニットを返します。 [型] string -> unit

___Int

quot (i,j) [動作] iをjで除算して、その結果の小数点の右側を 切り捨てることによってできる整数を返します。その結果がintの 範囲を超えた場合は、Overflowという例外を発生させます。jが 0だった場合は、Divという例外を発生させます。 [型] int * int -> int [例] - Int.quot (~60,7); > val it = ~8 : int rem (i,j) [動作] iをjで除算したときの余りを返します。jが0だった場合は、 Divという例外を発生させます。この関数は、 quotと対になっていて、quot (i,j) * j + rem (i,j) = iという 関係が常に成り立つようになっています。 [型] int * int -> int [例] - Int.rem (~60,7); > val it = ~4 : int min (i,j) [動作] iとjのうちで、小さいほうを返します。 [型] int * int -> int [例] - Int.min (3,7); > val it = 3 : int max (i,j) [動作] iとjのうちで、大きいほうを返します。 [型] int * int -> int [例] - Int.max (3,7); > val it = 7 : int toString i [動作] iを10進数であらわす文字列を返します。 [型] int -> string [例] - Int.toString ~89; > val it = "~89" : string fromString s [動作] 文字列sが10進数であらわされた整数ならば、それをintに 変換した結果にSOMEを適用した結果を返し、そうでなければNONEを 返します。sが10進数であらわされた整数であるけれども、 その結果がintの範囲を超えてしまった場合は、Overflowという 例外を発生させます。 [型] string -> int option [例] - Int.fromString "~89"; > val it = SOME ~89 : int option fmt radix i [動作] radixを基数としてiをあらわす文字列を作って、その結果を 返します。radixは、 2 StringCvt.BIN 8 StringCvt.OCT 10 StringCvt.DEC 16 StringCvt.HEX のいずれかです。 [型] StringCvt.radix -> int -> string [例] - Int.fmt StringCvt.BIN 283; > val it = "100011011" : string

___Word

wordSize [動作] wordのビット列の長さです。 [型] int [例] - Word.wordSize; > val it = 31 : int andb (w1,w2) [動作] ビット列のパターンw1とw2のANDを返します。 [型] word * word -> word [例] - Word.andb (0wxC,0wx5); > val it = 0wx4 : word orb (w1,w2) [動作] ビット列のパターンw1とw2のORを返します。 [型] word * word -> word [例] - Word.orb (0wxC,0wx5); > val it = 0wxD : word xorb (w1,w2) [動作] ビット列のパターンw1とw2のXORを返します。 [型] word * word -> word [例] - Word.xorb (0wxC,0wx5); > val it = 0wx9 : word notb w [動作] ビット列のパターンwのNOTを返します。 [型] word -> word [例] - Word.notb 0wx8; > val it = 0wx7FFFFFF7 : word >> (w1,w2) [動作] ビット列のパターンw1を右へw2ビットだけシフトした結果を 返します。 [型] word * word -> word [例] - Word.>> (0wxF00,0wx6); > val it = 0wx3C : word << (w1,w2) [動作] ビット列のパターンw1を左へw2ビットだけシフトした結果を 返します。 [型] word * word -> word [例] - Word.<< (0wxF00,0wx6); > val it = 0wx3C000 : word ~>> (w1,w2) [動作] ビット列のパターンw1を右へw2ビットだけ算術シフトした 結果を返します。 [型] word * word -> word [例] - Word.~>> (0wx7FFFF0FF,0wx6); > val it = 0wx7FFFFFC3 : word fromInt i [動作] 整数iを表現するビット列のパターンを返します。 [型] int -> word [例] - Word.fromInt 14; > val it = 0wxE : word toInt w [動作] ビット列のパターンwが表現している整数を返します。 [型] word -> int [例] - Word.toInt 0wxE; > val it = 14 : int fromString s [動作] 文字列sが16進数ならば、その16進数をビット列のパターンに 変換した結果にSOMEを適用した結果を返します。文字列sが 16進数ではないならば、NONEを返します。 [型] string -> word option [例] - Word.fromString "C7D3"; > val it = SOME 0wxC7D3 : word option toString w [動作] ビット列のパターンwを16進数に変換した結果を返します。 [型] word -> string [例] - Word.toString 0wxC7D3; > val it = "C7D3" : string

___Real

toString r [動作] rを10進数であらわす文字列を返します。 [型] real -> string [例] - Real.toString ~3507.4 > val it = "~3507.4" : string fromString s [動作] 文字列sが10進数であらわされた数値ならば、それをrealに 変換した結果にSOMEを適用した結果を返し、そうでなければNONEを 返します。 [型] string -> real option [例] - Real.fromString "~3507.4"; > val it = SOME ~3507.4 : real option

___Real.Math

pi [値] 円周率です。 [型] real [例] - Real.Math.pi; > val it = 3.14159265359 : real e [値] 自然対数の底です。 [型] real [例] - Real.Math.e; > val it = 2.71828182846 : real sqrt x [動作] xの平方根を返します。 [型] real -> real [例] - Real.Math.sqrt 3.0; > val it = 1.73205080757 : real sin x cos x tan x [動作] それぞれ、xのサイン、コサイン、タンジェントを 返します。角度の単位はラジアンです。 [型] real -> real [例] - Real.Math.cos (Real.Math.pi / 3.0); > val it = 0.5 : real asin x acos x atan x [動作] それぞれ、xのアークサイン、アークコサイン、 アークタンジェントを返します。角度の単位はラジアンです。 戻り値の範囲は、asinが閉区間[~pi/2.0,pi/2.0]、acosが 閉区間[0.0,pi]、atanが開区間(~pi/2.0,pi/2.0)です。 [型] real -> real [例] - Real.Math.atan 2.0; > val it = 1.10714871779 : real atan2 (y,x) [動作] y/xのアークタンジェントを返します。角度の象限は、 (y,x)という点の位置によって決定されます。atanの戻り値の範囲が 開区間(~pi/2.0,pi/2.0)であるのに対して、atan2の戻り値の範囲は 閉区間[~pi,pi]になります。 [型] real * real -> real [例] - Real.Math.atan2 (0.0,~1.0); >val it = 3.14159265359 : real exp x [動作] eのx乗を返します。 [型] real -> real [例] - Real.Math.exp 2.0 > val it = 7.38905609893 : real ln x [動作] xの自然対数を返します。 [型] real -> real [例] - Real.Math.ln 7.38905609893; > val it = 2.0 : real log10 [動作] 10を底とするxの対数を返します。 [型] real -> real [例] - Real.Math.log10 1000000.0; > val it = 6.0 : real pow (x,y) [動作] xのy乗を返します。 [型] real * real -> real [例] - Real.Math.pow (3.0,4.0); > val it = 81.0 : real sinh x cosh x tanh x [動作] 双曲線関数の値を返します。 [型] real -> real [例] - Real.Math.sinh 2.0; > val it = 3.62686040785 : real

___Char

toLower c [動作] cが英字の大文字ならばそれを小文字に変換した結果を返し、 そうでなければcをそのまま返します。 [型] char -> char [例] - Char.toLower #"Q"; > val it = #"q" : char toUpper c [動作] cが英字の大文字ならばそれを小文字に変換した結果を返し、 そうでなければcをそのまま返します。 [型] char -> char [例] - Char.toUpper #"q"; > val it = #"Q" : char 文字の種類を判定する述語 isAlpha 英字ならば真。 isLower 英字の小文字ならば真。 isUpper 英字の大文字ならば真。 isDigit 10進数の数字(0-9)ならば真。 isAlphaNum 英字または10進数の数字ならば真。 isHexDigit 16進数の数字(0-9,a-f,A-F)ならば真。 isSpace ホワイトスペース(空白、タブ、改行、 ページ送り)ならば真。 isCntrl 制御文字(警告音、バックスペースなど)ならば真。 isPrint 印字可能な(制御文字ではない)文字ならば真。 isGraph ホワイトスペース以外の印字可能な文字ならば真。 isPunct 英字でも数字でもホワイトスペースでもない 印字可能な文字ならば真。 isAscii 対応する整数が0以上かつ127以下ならば真。 [型] char -> bool [例] - Char.isAlpha #"M"; > val it = true : bool - Char.isAlpha #"7"; > val it = false : bool contains s c [動作] 文字列sの中に文字cが含まれているならば真、 そうでなければ偽を返します。 [型] string -> char -> bool [例] - Char.contains "Ebling Mis" #"g"; > val it = true : bool - Char.contains "Ebling Mis" #"h"; > val it = false : bool

___String

maxSize [値] 現在の環境で扱うことのできるもっとも長い文字列の 長さです。 [型] int [例] - String.maxSize; > val it = 16777211 : int sub (s,i) [動作] 文字列sのi番目の文字を返します(先頭の文字を0番目と 数えます)。i番目の文字が存在しない場合は、Subscriptという 例外を発生させます。 [型] (string * int) -> char [例] String.sub ("Dagobert IX",4); val it = #"b" : char extract (s,i,SOME n) [動作] 文字列sのi番目の文字から始まる、長さがnの部分文字列を 返します(先頭の文字を0番目と数えます)。SOME nの代わりに NONEを使うと、i番目の文字から始まる、最後の文字までの 部分文字列を返します。そのような文字列が存在しない場合は、 Subscriptという例外を発生させます。 [型] string * int * int option -> string [例] - String.extract ("Magnifico Giganticus",10,SOME 5); > val it = "Gigan" : string - String.extract ("Magnifico Giganticus",10,NONE); > val it = "Giganticus" : string translate f s [動作] 文字列sを構成するそれぞれの文字に対して、左から右へ 順番に関数fを適用して、その結果を連結することによってできる 文字列を返します。 [型] (char -> string) -> string -> string [例] - String.translate (fn c => str c ^ str c) "Lee Senter"; > val it = "LLeeee SSeenntteerr" : string tokens f s [動作] 関数fによって示される区切り文字で文字列sをいくつかの 部分文字列に分解して、その結果から構成されるリストを 返します。sの中に、連続する区切り文字から構成される 部分文字列があった場合は、その全体を1個の区切り文字として 扱います。 [型] (char -> bool) -> string -> string list [例] - String.tokens Char.isPunct "Fran/Mangin//Randu"; > val it = ["Fran", "Mangin", "Randu"] : string list fields f s [動作] 関数fによって示される区切り文字で、文字列sをいくつかの 部分文字列に分解して、その結果から構成されるリストを 返します。sの中に、連続する区切り文字から構成される 部分文字列があった場合は、それらの区切り文字のあいだにある 空文字列を、結果のリストの中に含めます。 [型] (char -> bool) -> string -> string list [例] - String.fields Char.isPunct "Fran/Mangin//Randu"; > val it = ["Fran", "Mangin", "", "Randu"] : string list

___Substring

Substringというストラクチャーは、substringという 型構成子の定義と、substringという型のデータを扱う関数の 定義から構成されています。substringという型は、文字列の 一部分として存在するデータを要素とする集合です。substring型の データは、モニターの画面などに表示することはできませんが、 コンピュータの内部では、母体となる文字列、先頭の文字の位置、 長さ、という3個のデータの組によって表現されている と考えることができます。 substring型のデータは基本的には文字列ですので、普通の 文字列と同じように扱うことができます。また、Stringという ストラクチャーの中にある大部分の関数については、それに 対応するsubstring版がSubstringの中にあります。 extract (s,i,SOME n) [動作] 文字列sのi番目の文字から始まる、長さがnの、 substring型のデータを返します(先頭の文字を0番目と数えます)。 SOME nの代わりにNONEを使うと、i番目の文字から始まる、最後の 文字までのsubstring型のデータを返します。i番目の文字が 存在しない場合は、Subscriptという例外を発生させます。 [型] string * int * int option -> substring [例] - Substring.extract ("Magnifico Giganticus",10,SOME 5); > val it = - : substring string ss [動作] substring型のデータssを文字列に変換した結果を返します。 [型] substring -> string [例] - Substring.string (Substring.extract ("Magnifico Giganticus",10,SOME 5)); > val it = "Gigan" : string base ss [動作] substring型のデータssを表現している、母体となっている 文字列s、先頭の文字の位置i、長さn、という三つのデータから 構成される組(s,i,n)を返します。 [型] substring -> (string * int * int) [例] - Substring.base (Substring.extract ("Magnifico Giganticus",10,SOME 5)); > val it = ("Magnifico Giganticus", 10, 5) : string * int * int getc ss [動作] substring型のデータssの先頭の文字cと、ssからcを 取り除いた結果rst、という二つのデータから構成される 組(c,rst)にSOMEを適用した結果を返します。ssが空文字列だった 場合はNONEを返します。 [型] substring -> (char * substring) option [例] - Substring.getc (Substring.extract ("quadrillion",2,SOME 7)); > val it = SOME(#"a", -) : (char * substring) option

___List

last xs [動作] リストxsの末尾の要素を返します。xsが空リストだった 場合は、Emptyという例外を発生させます。 [型] 'a list -> 'a [例] - List.last [36,87,43,99,14,71]; > val it = 71 : int nth (xs,i) [動作] リストxsのi番目の要素を返します(先頭の要素を0番目と 数えます)。i番目の要素が存在しない場合は、Subscriptという 例外を発生させます。 [型] 'a list * int -> 'a [例] - List.nth ([49,50,21,89,17,36,69],3); > val it = 89 : int take (xs,n) [動作] リストxsの先頭からn個の要素を取り出して、 それらの要素から構成されるリストを返します。nがリストの 長さよりも大きい場合は、Subscriptという例外を発生させます。 [型] 'a list * int -> 'a list [例] - List.take ([93,41,27,18,33,64,52,19,77],4); > val it = [93, 41, 27, 18] : int list drop (xs,n) [動作] リストxsの先頭からn個の要素を取り除くことによってできる リストを返します。nがリストの長さよりも大きい場合は、 Subscriptという例外を発生させます。 [型] 'a list * int -> 'a list [例] - List.drop ([93,41,27,18,33,64,52,19,77],4); > val it = [33, 64, 52, 19, 77] : int list concat xss [動作] リストのリストxssを構成するすべてのリストを 連結することによってできるリストを返します。 [型] 'a list list -> 'a list [例] - List.concat [[38,27,61],[19,84],[53],[71,93]]; > val it = [38, 27, 61, 19, 84, 53, 71, 93] : int list find f xs [動作] リストxsを構成するそれぞれの要素に対して、先頭から 順番に関数fを適用していって、fの戻り値が真になる要素があれば、 その要素にSOMEを適用した結果を返します。fの戻り値が真になる 要素が存在しなかった場合はNONEを返します。 [型] ('a -> bool) -> 'a list -> 'a option [例] - List.find (fn n => n mod 7 = 0) [53,64,27,92,18,42,12,35,88,20,14,66]; > val it = SOME 42 : int option filter f xs [動作] リストxsを構成するすべての要素に対して関数fを適用して、 fの戻り値が真になる要素だけから構成されるリストを返します。 [型] ('a -> bool) -> 'a list -> 'a list [例] - List.filter (fn n => n mod 7 = 0) [53,64,27,92,18,42,12,35,88,20,14,66]; > val it = [42, 35, 14] : int list partition f xs [動作] リストxsを構成するすべての要素に対して関数fを適用して、 fの戻り値が真になる要素だけから構成されるリストと、fの戻り値が 偽になる要素だけから構成されるリスト、という二つのリストから 構成される組を返します。 [型] ('a -> bool) -> 'a list -> 'a list * 'a list [例] - List.partition (fn n => n mod 7 = 0) [53,64,27,92,18,42,12,35,88,20,14,66]; > val it = ([42, 35, 14], [53, 64, 27, 92, 18, 12, 88, 20, 66]) : int list * int list exists f xs [動作] リストxsを構成する要素の中に、関数fを適用した結果が 真になるものが存在するならば真を返し、存在しないならば偽を 返します。 [型] ('a -> bool) -> 'a list -> bool [例] - List.exists (fn n => n >= 100) [53,64,27,88,15,44,103,41,24,78,11,39]; > val it = true : bool - List.exists (fn n => n >= 100) [66,23,84,92,17,33,69,43,70,22,18,45]; > val it = false : bool all f xs [動作] リストxsを構成するそれぞれの要素に対して関数fを適用した 結果がすべて真ならば真を返し、そうでないならば偽を返します。 [型] ('a -> bool) -> 'a list -> bool [例] - List.all (fn n=> n < 100) [66,23,84,92,17,33,69,43,70,22,18,45]; > val it = true : bool - List.all (fn n=> n < 100) [53,64,27,88,15,44,103,41,24,78,11,39]; > val it = false : bool tabulate (n,f) [動作] 0からn-1までのそれぞれの整数に対して関数fを適用した 結果から構成されるリストを返します。nが0よりも小さい場合は、 Sizeという例外を発生させます。 [型] int * (int -> 'a) -> 'a list [例] - List.tabulate (8,fn n => n * n); > val it = [0, 1, 4, 9, 16, 25, 36, 49] : int list

___Vector

maxLen [動作] 現在の環境で扱うことのできるもっとも長いベクトルの 長さです。 [型] int [例] - Vector.maxLen; > val it = 4194303 : int tabulate (n,f) [動作] 関数fを、0、1、2、3、……、n-1に適用したときの それぞれの戻り値から構成されるベクトルを返します。nが0よりも 小さいか、またはmaxLenよりも大きい場合は、Sizeという例外を 発生させます。 [型] int * (int -> 'a) -> 'a vector [例] - Vector.tabulate (5,fn n => chr (ord #"A" + n)); > val it = #[#"A", #"B", #"C", #"D", #"E"] : char vector length v [動作] ベクトルvの長さ(含まれる要素の個数)を返します。 [型] 'a vector -> int [例] - Vector.length #[#"B",#"r",#"a",#"n",#"n",#"o"]; > val it = 6 : int sub (v,i) [動作] ベクトルvのi番目の要素を返します(先頭の要素を0番目と 数えます)。i番目の要素が存在しない場合は、Subscriptという 例外を発生させます。 [型] 'a vector * int -> 'a [例] - Vector.sub (#[#"P",#"e",#"l",#"o",#"r",#"a",#"t"],3); > val it = #"o" : char extract (v,i,SOME n) [動作] ベクトルvのi番目の要素から右へ順番にn個の要素を 取り出して、それらの要素から構成されるベクトルを 返します(先頭の要素を0番目と数えます)。SOME nの代わりに NONEを使った場合は、i番目の要素から最後の要素までを 取り出します。i番目の要素が存在しないか、またはn個の要素を 取り出すことができない場合は、Subscriptという例外を 発生させます。 [型] 'a vector * int * int option -> 'a vector [例] - Vector.extract (#[#"P",#"e",#"l",#"o",#"r",#"a",#"t"],2,SOME 4); > val it = #[#"l", #"o", #"r", #"a"] : char vector concat vs [動作] ベクトルのリストvsを構成するそれぞれのベクトルを、 連結することによってできるベクトルを返します。ベクトルの長さが maxLenを超えた場合は、Sizeという例外を発生させます。 [型] 'a vector list -> 'a vector [例] - Vector.concat [#[38,27,63],#[72,88],#[25,16,99]]; > val it = #[38, 27, 63, 72, 88, 25, 16, 99] : int vector

___Array

maxLen [動作] 現在の環境で扱うことのできるもっとも長い配列の 長さです。 [型] int [例] - Array.maxLen; > val it = 4194303 : int array (n,x) [動作] 長さがnの配列を作って、そのそれぞれの記憶場所にxを 格納して、その配列を指示するデータを返します。nが0よりも 小さいか、またはmaxLenよりも大きい場合は、Sizeという例外を 発生させます。 [型] int * 'a -> 'a array [例] - val a = Array.array (5,#"M"); > val a = - : char array - Array.extract (a,0,NONE); > val it = #[#"M", #"M", #"M", #"M", #"M"] : char vector fromList xs [動作] リストxsと同じ長さの配列を作って、そのそれぞれの 記憶場所にxsの要素を格納して、その配列を指示するデータを 返します。xsの長さがmaxLenよりも大きい場合は、Sizeという例外を 発生させます。 [型] 'a list -> 'a array [例] - val a = Array.fromList [#"L",#"u",#"x",#"o",#"r"]; > val a = - : char array - Array.extract (a,0,NONE); > val it = #[#"L", #"u", #"x", #"o", #"r"] : char vector tabulate (n,f) [動作] 長さがnの配列を作って、関数fを、0、1、2、3、……、n-1に 適用したときのそれぞれの戻り値を、その配列のそれぞれの 記憶場所に格納して、その配列を指示するデータを返します。 nが0よりも小さいか、またはmaxLenよりも大きい場合は、Sizeという 例外を発生させます。 [型] int * (int -> 'a) -> 'a array [例] - val a = Array.tabulate (5,fn n => chr (ord #"a" + n)); > val a = - : char array - Array.extract (a,0,NONE); > val it = #[#"a", #"b", #"c", #"d", #"e"] : char vector length a [動作] 配列aの長さ(含まれる記憶場所の個数)を返します。 [型] 'a array -> int [例] - val a = Array.array (70,#" "); > val a = - : char array - Array.length a; > val it = 70 : int sub (a,i) [動作] 配列aのi番目の記憶場所に格納されているデータを 返します(先頭の記憶場所を0番目と数えます)。i番目の記憶場所が 存在しない場合は、Subscriptという例外を発生させます。 [型] 'a array * int -> 'a [例] - val a = Array.tabulate (26,fn n => chr (ord #"a" + n)); > val a = - : char array - Array.sub (a,10); > val it = #"k" : char update (a,i,x) [動作] 配列aのi番目の記憶場所にxを代入して、ユニットを 返します(先頭の記憶場所を0番目と数えます)。i番目の記憶場所が 存在しない場合は、Subscriptという例外を発生させます。 [型] 'a array * int * 'a -> unit [例] - val a = Array.array (5,#" "); > val a = - : char array - Array.update (a,3,#"D"); > val it = () : unit - Array.extract (a,0,NONE); > val it = #[#" ", #" ", #" ", #"D", #" "] : char vector extract (a,i,SOME n) [動作] 配列aのi番目の記憶場所を左端とするn個の連続する 記憶場所からデータを取り出して、それらのデータから構成される ベクトルを返します(先頭の要素を0番目と数えます)。 SOME nの代わりにNONEを使った場合は、i番目から最後までの 記憶場所からデータを取り出します。i番目を左端とするn個の 連続する記憶場所が存在しない場合は、Subscriptという例外を 発生させます。 [型] 'a array * int * int option -> 'a vector [例] - val a = Array.tabulate (26,fn n => chr (ord #"a" + n)); > val a = - : char array - Array.extract (a,15,SOME 5); > val it = #[#"p", #"q", #"r", #"s", #"t"] : char vector

___TextIO

openIn s [動作] パス名sによって指定されるファイルを読み込みのために オープンして、そのファイルを指示する入力ストリームを返します。 sによって指定されるファイルが存在しないか、またはファイルを オープンすることができなかった場合は、Ioという例外を 発生させます。 [型] string -> instream openOut s [動作] パス名sによって指定されるファイルを出力のために オープンして、そのファイルを指示する出力ストリームを返します。 sによって指定されるファイルが存在しない場合は、新しい ファイルを作ります。sによって指定されるファイルが存在する 場合、そのファイルの内容は消去されます。ファイルを オープンすることができなかった場合は、Ioという例外を 発生させます。 [型] string -> outstream openAppend s [動作] パス名sによって指定されるファイルを出力のために オープンして、そのファイルを指示する出力ストリームを返します。 sによって指定されるファイルが存在しない場合は、新しい ファイルを作ります。sによって指定されるファイルが存在する 場合は、そのファイルの終わりを現在位置にします。ファイルを オープンすることができなかった場合は、Ioという例外を 発生させます。 [型] string -> outstream closeIn istr [動作] 入力ストリームistrによって指示されるファイルを クローズします。 [型] instream -> unit closeOut ostr [動作] 出力ストリームistrによって指示されるファイルを クローズします。 [型] outstream -> unit input1 istr [動作] 入力ストリームistrによって指示されるファイルの 現在位置にある文字を読み込んで、現在位置を次の文字の位置に 移動させて、読み込んだ文字にSOMEを適用した結果を返します。 istrによって指示されるファイルの現在位置がファイルの 終わりにある場合、またはそのファイルがクローズされている 場合は、NONEを返します。 [型] instream -> char option inputAll istr [動作] 入力ストリームistrによって指示されるファイルの 現在位置にある文字から末尾の文字までを読み込んで、現在位置を ファイルの終わりに移動させて、読み込んだ文字から構成される 文字列を返します。文字列の長さがmaxSizeを超えた場合は、 Sizeという例外を発生させます。 [型] instream -> string inputLine istr [動作] 入力ストリームistrによって指示されるファイルの 現在位置にある文字から次の改行までを読み込んで、現在位置を その改行の次の位置に移動させて、読み込んだ文字から構成される 文字列(改行も含みます)を返します。現在位置よりもうしろに 改行が存在しない場合は、ファイルの末尾にある文字までを 読み込んで、現在位置をファイルの終わりに移動させます。文字列の 長さがmaxSizeを超えた場合は、Sizeという例外を 発生させます。 [型] instream -> string endOfStream istr [動作] 入力ストリームistrによって指示されるファイルの 現在位置がファイルの終わりならば真を返し、そうでないならば偽を 返します。 [型] instream -> bool lookahead istr [動作] 入力ストリームistrによって指示されるファイルの 現在位置にある文字を読み込んで、その文字にSOMEを適用した結果を 返します。現在位置は、移動させません。istrによって指示される ファイルの現在位置がファイルの終わりにある場合、 またはそのファイルがクローズされている場合は、NONEを返します。 [型] instream -> char option output1 (ostr,c) [動作] 出力ストリームostrによって指示されるファイルの 現在位置に文字cを出力して、現在位置を、出力した文字の次の 位置に移動させて、ユニットを返します。 [型] outstream * char -> unit output (ostr,s) [動作] 出力ストリームostrによって指示されるファイルの 現在位置に文字列sを出力して、現在位置を、出力した文字列の次の 位置に移動させて、ユニットを返します。 [型] outstream * string -> unit flushOut ostr [動作] 出力ストリームostrによって指示されるファイルへの 出力のために使われているバッファーをフラッシュします。 [型] outstream -> unit stdIn [動作] 標準入力を指示する入力ストリームです。 [型] instream stdOut [動作] 標準出力を指示する出力ストリームです。 [型] outstream stdErr [動作] 標準エラーを指示する出力ストリームです。 [型] outstream

___OS.FileSys

openDir s [動作] パス名sによって指定されるディレクトリをオープンして、 そのディレクトリを指示するディレクトリストリームを返します。 sによって指定されるディレクトリが存在しないか、 またはディレクトリをオープンすることができなかった場合は、 OS.SysErrという例外を発生させます。 [型] string -> dirstream closeDir dstr [動作] ディレクトリストリームdstrによって指示される ディレクトリをクローズします。 [型] dirstream -> unit readDir dstr [動作] ディレクトリストリームdstrによって指示される ディレクトリの現在位置にあるものの名前を読み込んで、現在位置を 次の位置へ移動させて、読み込んだ名前を返します。 [型] dirstream -> string rewindDir dstr [動作] ディレクトリストリームdstrによって指示される ディレクトリの現在位置を、ディレクトリの先頭に戻します。 [型] dirstream -> unit getDir () [動作] カレントディレクトリのパス名を返します。 [型] unit -> string [例] - OS.FileSys.getDir (); > val it = "/home/umberto/pendolo" : string chDir s [動作] パス名sによって指定されるディレクトリを、 カレントディレクトリにします。カレントディレクトリを 変更することができなかった場合は、OS.SysErrという例外を 発生させます。 [型] string -> unit [例] - OS.FileSys.getDir (); > val it = "/home/umberto/pendolo" : string - OS.FileSys.chDir "../tesi"; > val it = () : unit - OS.FileSys.getDir (); > val it = "/home/umberto/tesi" : string mkDir s [動作] パス名sによって指定されるディレクトリを作成します。 ディレクトリを作成することができなかった場合は、 OS.SysErrという例外を発生させます。 [型] string -> unit [例] - OS.FileSys.mkDir "lingua"; > val it = () : unit rmDir s [動作] パス名sによって指定されるディレクトリを削除します。 ディレクトリは、空でないと削除できません。ディレクトリを 削除することができなかった場合は、OS.SysErrという例外を 発生させます。 [型] string -> unit [例] - OS.FileSys.rmDir "smedof"; > val it = () : unit remove s [動作] パス名sによって指定されるファイルを削除します。 ファイルを削除することができなかった場合は、OS.SysErrという 例外を発生させます。 [型] string -> unit [例] - OS.FileSys.remove "nevlamo.txt"; > val it = () : unit rename {old=so,new=sn} [動作] パス名soによって指定されるファイルの名前をsnに 変更します。ファイル名の変更ができなかった場合は、 OS.SysErrという例外を発生させます。 [型] {old : string, new : string} -> unit [例] - OS.rename {old="mezonte.txt",new="gusfun.txt"}; > val it = () : unit fullPath s [動作] パス名sによって指定されるものをあらわす絶対パス名を 返します。sに含まれる名前を持つディレクトリまたは ファイルのうちに、存在しないかまたは アクセスできないものがあった場合は、OS.SysErrという例外を 発生させます。 [型] string -> string [例] - OS.FileSys.fullPath "../amori"; > val it = "/home/italo/amori" : isDir s [動作] パス名sによって指定されるものがディレクトリならば真を 返し、そうでないならば偽を返します。 [型] string -> bool [例] - OS.FileSys.isDir "/"; > val it = true : bool - OS.FileSys.isDir "/etc/hosts"; > val it = false : bool fileSize s [動作] パス名sによって指定されるファイルの大きさ(その中に 格納されているデータの大きさ。単位はバイト)を返します。 [型] string -> int [例] - OS.FileSys.fileSize "speaker.txt"; > val it = 7685 : int modTime s [動作] パス名sによって指定されるファイルの内容が最後に 更新された日付と時刻を返します。 [型] string -> Time.time [例] - Date.toString (Date.fromTimeLocal (OS.FileSys.modTime "speaker.txt")); > val it = "Sat Nov 13 16:52:18 1999" : string setTime (s,SOME t) [動作] パス名sによって指定されるファイルが持っている、 その内容が最後に変更された時刻のデータを、時刻tに変更します。 SOME tの代わりにNONEを使うと、現在の時刻が使われます。 時刻の変更ができなかった場合は、OS.SysErrという例外を 発生させます。 [型] string * Time.time option -> unit [例] - Date.toString (Date.fromTime (OS.FileSys.modTime "mampa.txt")); > val it = "Tue Aug 19 17:16:26 1997" : string - OS.FileSys.setTime("mampa.txt",NONE); > val it = () : unit - Date.toString (Date.fromTime (OS.FileSys.modTime "mampa.txt")); > val it = "Sun Oct 17 16:19:04 1999" : string

___OS.Process

system s [動作] コマンドsをシェルに実行させて、そのコマンドが正常に 終了したかどうかを意味する、OS.Process.statusという型の データ(正常に終了した場合はOS.Process.success)を返します。 [型] string -> OS.Process.status [例] - OS.Process.system "grep delarmi /etc/passwd" = OS.Process.success; delarmi:fR21aQ8x7bEvc:1023:100::/home/delarmi:/bin/bash > val it = true : bool getEnv s [動作] 環境変数sの値にSOMEを適用した結果を返します。sが 環境変数として定義されていなかった場合は、NONEを返します。 [型] string -> string option [例] - OS.Process.getEnv "TZ"; > val it = SOME "Pacific/Fiji" : string option

___OS.Path

OS.Pathは、パス名を扱う関数などから構成される ストラクチャーです。このストラクチャーに含まれる関数は、 あくまでパス名を文字列として扱うだけですので、 ファイルシステムにアクセスすることはありません。 なお、OS.Pathに含まれる関数の説明の中で、「アーク」という 言葉がしばしば使われていますが、これは、「パス名を構成する 個々のディレクトリやファイルの名前」という意味の言葉です。 isRelative s [動作] パス名sが相対パス名ならば真を返し、そうでなければ偽を 返します。 [型] string -> bool [例] - OS.Path.isRelative "../../astrum/somnium.txt"; > val it = true : bool - OS.Path.isRelative "/liber/astrum/somnium.txt"; > val it = false : bool isAbsolute s [動作] パス名sが絶対パス名ならば真を返し、そうでなければ偽を 返します。 [型] string -> bool [例] - OS.Path.isAbsolute "/liber/astrum/somnium.txt"; > val it = true : bool - OS.Path.isAbsolute "../../astrum/somnium.txt"; > val it = false : bool mkRelative (s1,s2) [動作] 絶対パス名s1を、絶対パス名s2を起点とする相対パス名に 変換して、その結果を返します。s2が絶対パス名ではないならば、 Pathという例外を発生させます。 [型] string * string -> string [例] - OS.Path.mkRelative ("/nomen/urbs/pagus.txt","/nomen/imago/aestas"); > val it = "../../urbs/pagus.txt" : string mkAbsolute (s1,s2) [動作] 絶対パス名s2を起点とする相対パス名s1を、絶対パス名に 変換して、その結果を返します。s2が絶対パス名ではないならば、 Pathという例外を発生させます。 [型] string * string -> string [例] - OS.Path.mkAbsolute ("../../urbs/pagus.txt","/nomen/imago/aestas"); > val it = "/nomen/urbs/pagus.txt" : string fromString s [動作] パス名sが絶対パス名かどうかを示す真偽値、sに含まれる ボリューム名、sを構成するアークのリスト、という三つの要素から 構成されるレコードを返します。 [型] string -> {isAbs : bool, arcs : string list, vol : string} [例] - OS.Path.fromString "../../cultura/speculum.txt"; > val it = {isAbs = false, vol = "", arcs = ["..", "..", "cultura", "speculum.txt"]} : {isAbs : bool, vol : string, arcs : string list} toString {isAbs=b,vol=sv,arcs=ss} [動作] ボリューム名svとアークのリストssから構成される パス名(真偽値bが真ならば絶対パス名、そうでなければ 相対パス名)を返します。 [型] {arcs : string list, isAbs : bool, vol : string} -> string [例] - OS.Path.toString {isAbs=false,vol="", arcs=["..","..","cultura","speculum.txt"]}; > val it = "../../cultura/speculum.txt" : string splitDirFile s [動作] パス名sを、もっとも右側にあるアークとそれ以外の部分とに 分割して、それらから構成されるレコードを返します。 [型] string -> {dir : string, file : string} [例] - OS.Path.splitDirFile "../../semen/lapis"; > val it = {dir = "../../semen", file = "lapis"} : {dir : string, file : string} joinDirFile {dir:sd,file:sf} [動作] パス名sdの右側にアークsfを追加することによってできる パス名を返します。 [型] {dir : string, file : string} -> string [例] - OS.Path.joinDirFile {dir="../../semen",file="lapis"}; > val it = "../../semen/lapis" : string splitBaseExt s [動作] パス名sからもっとも右側にあるアークの拡張子を 取り出して、それにSOMEを適用した結果と、その拡張子をsから 取り除いてできる文字列、という二つの要素から構成される レコードを返します。もっとも右側にあるアークに 拡張子がなかった場合は、sとNONEから構成されるレコードを 返します。 [型] string -> {base : string, ext : string option} [例] - OS.Path.splitBaseExt "../../mensis/dictum.txt"; > val it = {base = "../../mensis/dictum", ext = SOME "txt"} : {base : string, ext : string option} joinBaseExt {base=sb,ext=SOME se} [動作] パス名sbの末尾にドットと拡張子seを連結した結果を 返します。SOME seではなくNONEを使った場合は、sbをそのまま 返します。 [型] {base : string, ext : string option} -> string [例] - OS.Path.joinBaseExt {base="../../mensis/dictum", ext=SOME "txt"}; > val it = "../../mensis/dictum.txt" : string

___Date

fromString s [動作] 文字列sを日付と時刻のデータに変換して、その結果に SOMEを適用した結果を返します。変換が不可能な場合は、NONEを 返します。文字列sは、 "Www Mmm dd hh:mm:ss yyyy" Www 曜日の省略形。"Sun"、"Mon"、"Tue"、"Wed"、"Thu"など。 Mmm 月の省略形。"Jan"、"Feb"、"Mar"、"Apr"、"May"など。 dd 月内の日の番号。"01".."31"。 hh 時。"00".."23"。 mm 分。"00".."59"。 ss 秒。"00".."61"。 yyyy 年。たとえば、"1994"。 という形式で日付と時刻をあらわしていないといけません。 [型] string -> Date.date option [例] - Date.fmt "%A, %d %B %Y" (valOf (Date.fromString "Tue Nov 23 16:22:17 1999")); > val it = "Tuesday, 23 November 1999" : string fromTimeLocal t fromTimeUniv t [動作] 時間のデータtを日付と時刻のデータに変換した結果を 返します。fromTimeLocalが返すのは地方標準時の日付と時刻で、 fromTimeUnivが返すのはUTC(協定世界時)の日付と時刻です。 [型] Time.time -> Date.date [例] - Date.toString (Date.fromTimeLocal (Time.now ())); > val it = "Sat Mar 30 19:21:33 1998" : string toString d [動作] 日付と時刻のデータdを文字列に変換した結果を返します。 [型] Date.date -> string [例] - Date.toString (valOf (Date.fromString "Tue Nov 23 16:22:17 1999")); > val it = "Tue Nov 23 16:22:17 1999" : string fmt s d [動作] 日付と時刻のデータdを、文字列sによってあらわされる 書式にしたがって文字列に変換した結果を返します。書式は、 %a 曜日の省略形。"Sun"、"Mon"、"Tue"、"Wed"、"Thu"など。 %A 曜日。"Sunday"、"Monday"、"Tuesday"、"Wednesday"など。 %b 月の省略形。"Jan"、"Feb"、"Mar"、"Apr"、"May"など。 %B 月。"January"、"February"、"March"、"April"、"May"など。 %c 日付と時刻。たとえば、"Mon Aug 18 21:33:14 1997"。 %d 月内の日の番号。"01".."31"。 %H 時。"00".."23"。 %I 時。"01".."12"。 %j 年内の日の番号。"001".."366"。 %m 月の番号。"01".."12"。 %M 分。"00".."59"。 %p 午前か午後か。"AM"または"PM"。 %S 秒。"00".."61"。 %U 年内の週の番号。"00".."53"。日曜日から土曜日までを ひとつの週とする。 %w 週内の日の番号。"0".."6"。日曜日が"0"。 %W 年内の週の番号。"00".."53"。月曜日から日曜日までを ひとつの週とする。 %x 日付。たとえば、"Mon Aug 18, 1997"。 %X 時刻。たとえば、"21:33:14"。 %y 年の下2桁。"00".."99"。 %Y 年。たとえば、"1997"。 %Z 地方標準時の名前。"EST"、"CST"、"PST"、"JST"など。 %% パーセント文字(%)。 という文字列を組み合わせることによって記述します。 [型] string -> Date.date -> string [例] - Date.fmt "%Y/%m/%d %I:%M %p" (valOf (Date.fromString "Tue Nov 23 16:22:17 1999")); > val it = "1999/11/23 04:22 PM" : string toTime d [動作] 日付と時刻のデータdを時間のデータに変換した結果を 返します。dがあらわしている日付と時刻が、Time.time型では 表現できないものだった場合は、Dateという例外を発生させます。 [型] Date.date -> Time.time [例] - Time.toSeconds (Date.toTime (valOf (Date.fromString "Sat Mar 28 15:18:48 1998"))); > val it = 891116328 : int

___Time

fromSeconds i fromMilliseconds i fromMicroseconds i [動作] 整数iによってあらわされる時間を、時間のデータに変換した 結果を返します。iの単位を、fromSecondsは秒だとみなし、 fromMillisecondsはミリセカンドだとみなし、fromMicrosecondsは マイクロセカンドだとみなします。 [型] int -> Time.time [例] - Time.toReal (Time.fromMilliseconds 34); > val it = 0.034 : real fromReal r [動作] 実数rによってあらわされる時間(単位は秒)を、時間の データに変換した結果を返します。 [型] real -> Time.time [例] - Time.toMilliseconds (Time.fromReal 2.58); > val it = 2580 : int toSeconds t toMilliseconds t toMicroseconds t [動作] 時間のデータtを整数に変換した結果を返します。 toSecondsはtを秒に変換し、toMillisecondsはtをミリセカンドに 変換し、toMicrosecondsはtをマイクロセカンドに変換します。 [型] Time.time -> int [例] - Time.toMilliseconds (Time.fromReal 0.034); > val it = 34 : int toReal t [動作] 時間のデータtを実数(単位は秒)に変換した結果を 返します。 [型] Time.time -> real [例] - Time.toReal (Time.fromMilliseconds 2580); > val it = 2.58 : real + (t1,t2) [動作] 時間のデータt1とt2を加算した結果を返します。 その結果が、Time.time型で表現できる範囲を超えた場合は、 Overflowという例外を発生させます。 [型] Time.time * Time.time -> Time.time [例] - Time.toSeconds (Time.+ (Time.fromSeconds 14,Time.fromSeconds 8)); > val it = 22 : int - (t1,t2) [動作] 時間のデータt1からt2を減算した結果を返します。 t1よりもt2のほうが長い時間だった場合は、Timeという例外を 発生させます。 [型] Time.time * Time.time -> Time.time [例] - Time.toSeconds (Time.- (Time.fromSeconds 14,Time.fromSeconds 8)); > val it = 6 : int t1 < t2 t1 <= t2 t1 > t2 t1 >= t2 [動作] 時間のデータt1とt2を比較した結果を返します。 [型] Time.time * Time.time -> bool [例] - Time.> (Time.fromSeconds 14,Time.fromSeconds 8); > val it = true : bool - Time.< (Time.fromSeconds 14,Time.fromSeconds 8); > val it = false : bool now () [動作] 現在の日付と時刻をあらわす時間のデータを返します。 [型] unit -> Time.time [例] - Date.toString (Date.fromTimeLocal (Time.now ())); > val it = "Tue Mar 31 18:13:14 1998" : string

付録C===プログラミング用語英和辞典

___この付録について

この付録は、英語で書かれたプログラミングに関連する文献で 使われている専門的な単語や熟語に、日本語の単語や熟語を 対応させた辞典です(「初級ML講座」の中で言及されていない 単語や熟語も含まれています)。 単語の品詞は、次の略語であらわされています。 n. 名詞 vi. 自動詞 vt. 他動詞 a. 形容詞 adv. 副詞

___数字、記号

1's complement, n., 1の補数. 15-puzzle, n., 15パズル. 2-3 tree, n., 2-3木. 2's complement, n., 2の補数. \alpha -\beta\ method, n., \alpha -\beta 法. \Omega -notation, n., \Omega 記法. \exists -introduction, n., 特殊化規則.

___A

A$^{*}$ algorithm, n., A$^{*}$アルゴリズム. absolute pathname, n., 絶対パス名. abstract data type, n., 抽象データ型. abstract machine, n., 抽象機械. accept, vt., (記号列を)受理する. ACM, n., Association for Computing Machineryの略称. ACM Turing Award, n., ACMチューリング賞. acyclic, a., 閉路のない. AI, n., artificial intelligenceの略称. algebraic semantics, n., 代数的意味論. algebraic system, n., 代数系. algorithm, n., アルゴリズム. Association for Computing Machinery, n., 計算機学会. ancestor, n., (木の節点の)先祖. AND, n., conjunctionの同義語, 論理積. AND/OR graph, n., AND/ORグラフ. anonymous variable, n., 匿名変数. antisymmetric, a., 反対称的な. antisymmetric law, a., 反対称律. AO$^{*}$ algorithm, n., AO$^{*}$アルゴリズム. application, n., 適用. apply, vt., (関数をデータに)適用する. arc, n., (1) edgeの同義語, (グラフの)弧, (2) (パス名の)アーク. argument, n., 引数. arithmetic operation, n., 算術演算. arithmetic operator, n., 算術演算子. arithmetic shift, n., 算術シフト. array, n., 配列. artificial intelligence, n., 人工知能. ascending order, n., 昇順. assemble, vt., (プログラムを)アセンブルする. assembler, n., アセンブラ. assembly language, n., アセンブラ言語. assign, vt., (データを変数に)代入する. assignment, n., 代入. assignment operation, n., 代入演算. assignment operator, n., 代入演算子. associative, a., 結合的な. associative law, n., 結合律. associativity, n., (演算子の)結合規則, 結合性. automata, n., automatonの複数形. automaton, n., オートマトン. AVL tree, n., AVL木. axiom, n., 公理. axiomatic semantics, n., 公理的意味論. axiomatic system, n., 公理系.

___B

backtrack, vi., バックトラックする. backtracking, n., バックトラック. Backus-Naur form, n., バッカス・ナウル記法. Backus normal form, n., Backus-Naur formの同義語, バッカス標準記法. balanced tree, n., 平衡木. basis, n., (再帰の)基底. binary, a., 2進法の. binary data, n., バイナリーデータ. binary file, n., バイナリーファイル. binary notation, n., 二進法. binary number, n., 二進数. binary operator, n., 二項演算子. binary relation, n., 二項関係. binary search, n., 二分探索. binary search tree, n., 二分探索木. binary tree, n., 二分木. bind, vt., (識別子をデータなどに)束縛する. binding, n., 束縛. bit, n., ビット. bit sequence, n., ビット列. bitwise operation, n., ビット演算. bitwise operator, n., ビット演算子. BNF, n., Backus-Naur formまたはBackus normal formの略称. Boolean value, n., 真偽値. bound, vt., bindの過去・過去分詞. breadth-first search, n., 幅優先探索. breakpoint, n., ブレークポイント. brother, n., (木の頂点の)兄弟. B tree, n., B木. bubble sort, n., バブルソート. buffer, n., バッファー. bug, n., バグ. byte, n., バイト.

___C

car, n., headの同義語, (リストの)頭部. catch, vt., (例外を)捕獲する, 受け取る. cdr, n., tailの同義語, (リストの)尾部. cell, n., (データ構造の)セル. character, n., 文字. character code, n., 文字コード. child, n., (木の頂点の)子供. Chomsky hierarchy, n., チョムスキー階層. clause, n., (プログラムの)節. close, vt., (ファイルなどを)クローズする. code, n., コード. column, n., (行列の)列. combination, n., 組み合わせ. command, n., コマンド. comment, n., 注釈. comment out, vt. (プログラムの一部分を)コメントアウトする. common divisor, n., 公約数. common multiple, 公倍数. commutative, a., 可換的な. commutative law, a., 交換律. comparison operation, n., relational operationの同義語, 比較演算. comparison operator, n., relational operatorの同義語, 比較演算子. compile, vt., (プログラムを)コンパイルする. compilation, n., コンパイル. compiler, n., コンパイラ. compose, vt., (関数を)合成する. composition, n., 合成. complement, n., 補数, 補集合. complete, a., 完全である. complete graph, n., 完全グラフ. completeness, n., 完全性. completeness theorem, n., 完全性定理. computability, n., 計算可能性. computable, a., 計算可能な. computation, n., 計算. computational complexity, n., 計算量. computer, n., コンピュータ. concatenate, vt., (文字列、リストなどを)連結する. concatenation, n., 連結. condition, n., 条件. conditional operation, n., 条件演算. conditional operator, n., 条件演算子. conjunction, n., 論理積. connected graph, n., 連結グラフ. connotation, n., 内包. cons, vt., (リストの左側にデータを)連結する. consistency, n., 無矛盾性. consistent, a., 無矛盾である. constant, n., 定数. constraint logic programming, n., 制約論理プログラミング. constraint programming, n., 制約プログラミング. constructor, n., 構成子. container, n., コンテナ. context free grammar, n., 文脈自由文法. context free language, n., 文脈自由言語. context sensitive grammar, n., 文脈依存文法. context sensitive language, n., 文脈依存言語. continuation, n., 継続計算. control character, n., 制御文字. control structure, n., 制御構造. cracker, n., クラッカー. current directory, n., カレントディレクトリ. Curried form, n., カリー化形式. Curry, vt., カリー化する. Currying, n., カリー化. cut, n., (バックトラックの)カット. cyberspace, n., 電脳空間, サイバースペース. cycle, n. 閉路.

___D

dag, n., directed acyclic graph.の略称. data, n., データ. data constructor, n., データ構成子. data structure, n., データ構造. deadlock, n., デッドロック. decimal, a., 10進法の. decimal notation, n., 10進法. decimal number, n., 10進数. declaration, n., 宣言. declare, vt., (識別子の束縛などを)宣言する. decrement, vt., (変数を)デクリメントする. decrementing, n., デクリメント. debug, vt., (プログラムを)デバッグする. debugging, n., デバッグ. deductive inference, n., 演繹的推論. define, vt., (関数などを)定義する. definition, n., 定義. degree, n., (グラフの頂点の)次数. DeMorgan's theorem, n., ド・モルガンの定理. denotation, n., 外延. denotational semantics, n., 表示的意味論. depth-first search, n., 深さ優先探索. deque, n., double-ended queueの略称. descendant, n., (木の頂点の)子孫. descending order, n., 降順. determinant, n., 行列式. dictionary, n., 辞書. difference list, n., 差分リスト. digraph, n., directed graphの略称. directed acyclic graph, n., 閉路のない有向グラフ. directed graph, n., 有向グラフ. directed tree, n., 有向木. directory, n., ディレクトリ. disjunction, n., 論理和. distance, n., (グラフの頂点と頂点とのあいだの)距離. divide and conquer, n., 分割統治法. divisor, n., 約数. domain, n., 定義域. double-ended queue, n., 両頭待ち行列. doubly-linked list, n., 双方向リスト.

___E

edge, n., (グラフの)辺. editor, n., エディター. eight queens problem, n., エイトクイーン問題. element, n., (集合、リスト、ベクトル、配列などの)要素. empty list, n., 空リスト. empty set, n., 空集合. empty string, n., null stringの同義語, 空文字列. emulation, n., エミュレーション. emulator, n., エミュレーター. encapsulation, n., カプセル化. end-of-file, n., ファイルの終わり. environment, n., 環境. EOF, n., end-of-fileの略称. EOR, n., exclusive ORの略称. equality, n., 相等性. equality type, n., 等値型. equivalence, n., (1) (論理式の)同値, (2) (プログラムの)同値性. equivalence relation, n., 同値関係. Eratosthenes' sieve, n., エラトステネスのふるい. error, n., エラー. error message, n., エラーメッセージ. escape sequence, n., エスケープシーケンス. Euclidean algorithm, n., ユークリッドの互除法. Euler cycle, n., オイラー閉路. Euler's thorem, n., オイラーの定理. Eulerian graph, n., オイラーグラフ. evaluate, vt., (式を)評価する. evaluation, n., 評価. evaluation function, n., (ゲームの局面の)評価関数. event, n., イベント. event-driven programming, n., イベント駆動型プログラミング. exception, n., 例外. exception constructor, n., 例外構成子. exclusive disjunction, n., 排他的論理和. exclusive OR, n., exclusive disjunctionの同義語, 排他的論理和. existential quantifier, n., 存在限量子. execute, vt., (プログラムなどを)実行する. executable program, n., 実行可能プログラム. expression, n., 式. extension, n., (ファイル名の)拡張子.

___F

fact, n., 事実. false, n., (真偽値の)偽. Fibonacci sequence, n., フィボナッチ数列. field, n., (1) 体, (2) (レコードの)フィールド. file, n., ファイル. file system, n., ファイルシステム. filter, n., フィルター. finite automaton, n., 有限オートマトン. finite state machine, n., 有限状態機械. first-order predicate logic, n., 1階述語論理. five dining philosophers' problem, n., 五人の哲学者の食事問題. flag, n., フラグ. forest, n., 森. formal language, n., 形式言語. formal semantics, n., 形式的意味論. formal system, n., 形式的体系. formula, n., 論理式. four-color problem, n., 四色問題. function, n., 関数. function application, n., 関数適用. functional computation model, n., 関数型計算モデル. functional language, n., 関数型言語. functional programming, n., 関数プログラミング. function type, n., 関数型. functor, n., ファンクター. fundamental type, n., 基本型.

___G

game, n., ゲーム. game tree, n., ゲームの木. garbage collection, n., ガーベジコレクション, ごみ集め. garden path sentence, n., ガーデンパス文. Gaussian elimination, n., ガウスの消去法. generative grammar, n., 生成文法. gloval, a., グローバルな, 大域的な. gloval variable, n., グローバル変数, 大域変数. goal, n., ゴール. goal clause, n., ゴール節. G\"{o}del number, n., ゲーデル数. grammar, n., 文法. graph, n., グラフ. greatest common divisor, n., 最大公約数. group, n., 群.

___H

hacker, n., ハッカー. halting problem of Turing machine, n., チューリングマシンの停止問題. Hamilton cycle, n., ハミルトン閉路. Hamiltonian graph, n., ハミルトングラフ. has-a relationship, n., has-a関係. hash function, n., ハッシュ関数. hash table, n., ハッシュ表. hashing, n., ハッシュ法. head, n., (リストの)頭部. heap, n., ヒープ. heap area, n., ヒープ領域. heapsort, n., ヒープソート. heuristic, a., ヒューリスティックな. heuristics, n., ヒューリスティックス. heuristic search, n., ヒューリスティック探索. hexadecimal, a., 16進法の. hexadecimal notation, n., 16進法. hexadecimal number, n., 16進数. higher-order, a., 高階の. higher-order function, n., 高階関数. higher-order predicate logic, n., 高階述語論理. Hoare's logic, n., ホーア論理. home directory, n., ホームディレクトリ. homomorphism, n., 準同形写像. homomorphism theorem, n., 準同形定理. Horn clause, n., ホーン節. Huffman code, n., ハフマン符号. hypertext, n., ハイパーテキスト.

___I

ideal, n., イデアル. identifier, n., 識別子. identity element, n., 単位元. identity function, n., 恒等関数. image, n., (写像の)像. imperative computation model, n., procedural computation modelの同義語, 命令型計算モデル. imperative language, n., procedural languageの同義語, 命令型言語. imperative programming, n., procedural programmingの同義語, 命令プログラミング. implement, vt., (機能を)実装する. implementation, n., 実装. implication, n., 含意. inclusion relation, n., (集合の)包含関係. increment, vt., (変数を)インクリメントする. incrementing, n., インクリメント. indent, vt., インデントする. indentation, n., インデント. indent style, n., インデントスタイル. inductive inference, n., 帰納的推論. inference, n., 推論. infix notation, n., 中置記法. infix operator, n., 中置演算子. information hiding, n., 情報隠蔽. inherit, vt., (クラスのメンバーを)継承する. inheritance, n., 継承. initial value, n., 初期値. initialize, vt., (変数などを)初期化する. inorder traversal, n., (木の)間順走査. input, vt., (データを)入力する. input, n., 入力. instance, n., インスタンス. integer, n., 整数. intensional logic, n., 内包論理. interactive, a., 対話的な. interface, n., インターフェース. interpreter, n., インタプリタ. intersection, n., (集合の)共通部分. inverse, n., 逆元. inverse matrix, n., 逆行列. is-a relationship, n., is-a関係. isomorphism, n., 同形写像. iteration, n., 繰り返し, 反復. iterator, n., イテレーター, 反復子.

___K

Kleene closure, n., クリーネ閉包. Kleene's theorem, n., クリーネの定理. knapsack problem, n., ナップザック問題. knowledge, n., 知識. knowledge representation, n., 知識表現. knowledge-base, n., 知識ベース.

___L

lambda abstraction, n., ラムダ抽象. lambda calculus, n., ラムダ計算. language, n., 言語. language processor, n., 処理系, 言語処理系. lattice, n., 束. lazy evaluation, n., 遅延評価. learning, n., 学習. least common multiple, 最小公倍数. left-associativity, n., 左結合性. lemma, n., 補題. length, n., (文字列、リスト、ベクトルなどの)長さ. lexical analysis, n., 字句解析. lexicographic order, n., 辞書式順序. library, n., ライブラリー. linear grammar, n., 線形文法. linear mapping, n., 線形写像. linear search, n., 線形探索. link, vt., (プログラムのいくつかの部分を)リンクする. linkage, n., リンケージ. linked list, n., 連結リスト. linker, n., リンカー. list, n., リスト. LL parsing method, n., LL構文解析法. LL(k) grammar, n., LL(k)文法. local, a., ローカルな, 局所的な. local variable, n., ローカル変数, 局所変数. logic, n., 論理. logic computation model, n., 論理型計算モデル. logic language, n., 論理型言語. logic programming, n., 論理プログラミング. logical operation, n., 論理演算. logical operator, n., 論理演算子. loop, n., iterationの同義語, 繰り返し, ループ. LR parsing method, n., LR構文解析法. LR(k) grammar, n., LR(k)文法. lvalue, n., 左辺値.

___M

M-expression, n., M式. make, vt., (プログラムを)メイクする. makefile, n., メイクファイル. mapping, n., 写像. machine language, n., 機械語. macro, n., マクロ. manual, n., マニュアル. match, vi., (パターンとデータとが)一致する. matching, n., マッチング. matrix, n., 行列, マトリックス. member, n., (クラスの)メンバー. merge, vt., (データの列とデータの列とを)マージする. merge sort, n., マージソート. message, n., メッセージ. message expression, n., メッセージ式. message passing, n., メッセージパッシング. metainference, n., メタ推論. metaknowledge, n., メタ知識. metapredicate, n., メタ述語. method, n., メソッド. minimax method, n., ミニマックス法. minus, a., マイナスの. modal, n., 様相. modal logic, n., 様相論理. module, n., モジュール. modus ponens, n., モードゥスポーネンス. Montague grammar, n., モンタギュー文法. Monte Carlo method, n., モンテカルロ法. multiple, 倍数. multiple inheritance, n., 多重継承. mutual recursion, n., 相互再帰.

___N

n-ary relation, n., n項関係. n-queen problem, n., nクイーン問題. n-square matrix, n., n次の正方行列. natural deduction, n., 自然演繹法. natural language, n., 自然言語. natural number, n., 自然数. negation, n., 否定. nest, vt., 入れ子にする. nesting, n., 入れ子. newline, n., 改行. nil, n., emply listの同義語, 空リスト. node, n., (木の)節点. non-terminal symbol, n., 非終端記号. normal form, n., 正規形, 標準形. NOT, n., negationの同義語, 否定. NP-complete, a., NP完全な. NP-complete problem, n., NP完全問題. NP-completeness, n., NP完全性. NP-hard, a., NP困難な. null string, n., 空文字列.

___O

O-notation, n., O記法. object, n., オブジェクト. object-oriented, a., オブジェクト指向の. object-oriented computation model, n., オブジェクト指向計算モデル. object-oriented language, n., オブジェクト指向言語. object-oriented programming, n., オブジェクト指向プログラミング. octal, a., 8進法の. octal notation, n., 8進法. octal number, n., 8進数. one-liner, n., 一行野郎. OO, a., object-orientedの略称. open, vt, (ファイルを)オープンする. operand, n., (演算子の)オペランド. operating system, n., オペレーティングシステム. operation, n., 演算. operational semantics, n., 操作的意味論. operator, n., 演算子. optimize, vt., (プログラムを)最適化する. optimization, n., 最適化. OR, n., disjunctionの同義語, 論理和. ordered pair, n., 順序対. OS, n., operating systemの略称. output, vt., (データを)出力する. output, n., 出力. overflow, n., オーバーフロー. overloading, n., 多重定義.

___P

paradox, n., パラドックス, 逆理, 二律背反. parsing, n., 構文解析. partial computation, n., 部分計算. partial order relation, n., 半順序関係. Pascal's triangle, n., パスカルの三角形. path, n., (グラフの)路. pathname, n., パス名. pattern, n., パターン. pattern matching, n., パターンマッチング. pattern recognition, n., パターン認識. perfect number, n., 完全数. permutation, n., 順列. Petri net, n., ペトリネット. phrase structure grammar, n., 句構造文法. pipe, n., パイプ. pivot, n., 枢軸. pointer, n., ポインター. Polish notation, n., prefix notationの同義語, ポーランド記法. polymorphic, a., 多相の, 多態の. polymorphic function, n., 多相型関数. polymorphic type., n., 多相型. polymorphism, n., ポリモーフィズム, 多相性, 多態性. polynomial, n., 多項式. polynomial time, n., 多項式時間. pop, vt., (スタックからデータを)ポップする. positional notation, n., 位取り記数法. postfix notation, n., 後置記法. postfix operator, n., 後置演算子. postorder traversal, n., (木の)後順走査. power, n., べき乗. power set, n., べき集合. precedence, n., (演算子の)優先順位, 優先度. predicate, n., 述語. predicate logic, n., 述語論理. prefix notation, n., 前置記法. prefix operator, n., 前置演算子. preorder traversal, n., (木の)前順走査. preprocessor, n., プリプロセッサー, 前処理系. prime number, n., 素数. procedural computation model, n., 手続き型計算モデル. procedural language, n., 手続き型言語. procedural programming, n., 手続き型プログラミング. process, n., プロセス. product, n., 直積. production, n., プロダクション, 生成規則. program, n., プログラム. programmer, n., プログラマー. programming, n., プログラミング. programming language, n., プログラミング言語. programming paradigm, n., プログラミングパラダイム. prompt, n., プロンプト. proof, n., 証明. proper subset, n., 真部分集合. proposition, n., 命題. propositional logic, n., 命題論理. prove, vt., (論理式を)証明する. prune, n., (木の枝を)刈る. pruning, n., 枝刈り. push, vt., (スタックにデータを)プッシュする. pushdown automaton, n., プッシュダウンオートマトン.

___Q

quantifier, n., 限量子. queue, n., 待ち行列. quicksort, n., クイックソート.

___R

radix, n., (位取り記数法の)基数. raise, vt., (例外を)発生させる. random numbers, n., 乱数. range, n., 値域. read, vt., (データを)読み込む. real number, n., 実数. reasoning, n., inferenceの同義語, 推論. record, n., レコード. recursion, n., 再帰. recursive, a., 再帰的な. reductio ad absurdum, n., 背理法. reduction, n., 簡約. reduction operator, n., 簡約演算子. reference, n., 参照. reflexive, a., 反射的な. reflexive law, a., 反射律. regular expression, n., 正規表現. regular grammar, n., 正規文法. regular language, n., 正規言語. relation, n., 関係. relational operation, n., 関係演算. relational operator, n., 関係演算子. relative pathname, n., 相対パス名. remainder, n., (除算の)余り, 剰余. repetition, n., iterationの同義語, 繰り返し, 反復. reserved word, n., 予約語. resource, n., リソース, 資源. reverse Polish notation, n., postfix notationの同義語, 逆ポーランド記法. right-associativity, n., 右結合性. ring, n., 環. root, n., (木の)根. root directory, n., ルートディレクトリ. row, n., (行列の)行. rule, n., 規則. rule of generalization, n., 一般化規則. rule of inference, n., 推論規則. rvalue, n., 右辺値.

___S

S-expression, n., S式. scalar, n., スカラー. scope, n., スコープ. script, n., スクリプト. scripting language, n., スクリプト言語. search, vt., (解やデータを)探索する. search, n., 探索. second-order predicate logic, n., 2階述語論理. segmentation fault, n., セグメンテーションフォールト. selection, n., 選択. semantic network, n., 意味ネットワーク. semantics, n., 意味規則, 意味論. semigroup, n., 半群. sentinel, n., 番兵, 番人. sequence, n., 列. sequential machine, n., 順序機械. set, n., 集合. set operation, n., 集合演算. set operator, n., 集合演算子. shell, n., シェル. shell script, n., シェルスクリプト. shift, vt., (ビット列を)シフトする. shift, n., シフト. side-effect, n., 副作用. sign, n., (プラスとマイナスの)符号. signature, n., シグネチャー. sort, vt., ソートする. sorting, n., ソート. source, n., ソース. sparse, a., 疎な. sparse matrix, n., 疎行列. specification, n., 仕様. square matrix, n., 正方行列. stack, n., スタック. standard error, n., 標準エラー. standard input, n., 標準入力. standard output, n., 標準出力. start symbol, n., 開始記号. state space, n., 状態空間. state diagram, n., 状態遷移図. state transition, n., 状態遷移. statement, n., 文. stream, n., ストリーム. string, n., 文字列, 記号列. structure, n., ストラクチャー. subclass, n., サブクラス. subgraph, n., 部分グラフ. subscript, n., 添字. subset, n., 部分集合. substring, n., 部分文字列. subtree, n., 部分木. superclass, n., スーパークラス. symbol, n., シンボル. symmetric, a., 対称的な. symmetric law, a., 対称律. syntax, n., 構文. syntax analysis, n., parsingの同義語, 構文解析. syntax sugar, n., 構文上の糖衣. syntax tree, n., 構文木.

___T

tail, n., (リストの)尾部. tail recursion, n., 末端再帰. tautology, n., トートロジー, 恒真論理式. term, n., 項. terminal symbol, n., 終端記号. ternary operator, n., 三項演算子. text, n., テキスト. text data, n., テキストデータ. text file, n., テキストファイル. theorem, n., 定理. theorem proving, n., 定理証明. throw, vt., (例外を)投げる, 送出する. token, n., トークン. top-level environment, n., トップレベル環境. tower of Hanoi, n., ハノイの塔. Trojan horse, n., トロイの木馬. transitive, a., 推移的な. transitive closure, n., 推移的閉包. transitive law, n., 推移律. translator, n., トランスレーター, 変換系. traveling salesman problem, n., 巡回セールスマン問題. tree, n., 木. tree traversal, n., 木の走査. true, n., (真偽値の)真. truth value, n., Boolean valueの同義語, 真理値. tuple, n., 組. Turing machine, n., チューリングマシン. Turing test, n., チューリングテスト. type, n., 型. type checking, n., 型検査. type inference, n., 型推論. type expression, n., 型式. type variable, n., 型変数. typed, a., 型付けされた.

___U

unary operator, n., 単項演算子. undefined, a., 未定義の. underflow, n., アンダーフロー. undirected graph, n., 無向グラフ. unification, n., 単一化, ユニフィケーション. unify, vt., (変数と項とを)単一化する. union, n., 和集合. unit, n., ユニット. unit matrix, n., 単位行列. universal quantifier, n., 全称限量子. universal set, n., 普遍集合.

___V

value, n., (式の)値. variable, n., 変数. vector, n., ベクトル. Venn diagram, n., ベン図. version, n., バージョン. vertex, n., (グラフの)頂点. virtual function, n., 仮想関数. virtual machine, n., 仮想機械, 仮想計算機. virtual reality, n., 仮想現実. virus, n., ウイルス. von Neumann computer, n., ノイマン型コンピュータ. Voronoi diagram, n., ボロノイ図式.

___W

warning message, n., 警告メッセージ. well-formed formula, n., 整合論理式. wff, n., well-formed formulaの略称. white space, n., ホワイトスペース. wildcard, n., ワイルドカード. worm, n., ワーム.

___X

XOR, n., exclusive ORの略称.

___Z

zero matrix, n., 零行列. zero vector, n., 零ベクトル. zeroth, a., ゼロ番目の.

===参考文献

[Aho,1983] Alfred V. Aho, John E. Hopcroft and Jeffrey D. Ullman, Data Structures and Algorithms, Addison-Wesley, 1983, ISBN 0-201-00023-7. 邦訳(大野義夫)、『データ構造とアルゴリズム』、培風館、 1987、ISBN 4-563-00791-9。 [Appel,1997] Andrew W. Appel, Modern Compiler Implementation in ML: Basic Techniques, Cambridge University Press, 1997, ISBN 0-521-58775-1. [Bird,1988] Richard Bird and Philip Wadler, Introduction to Functional Programming, Prentice-Hall, 1988, ISBN 0-13-484197-2. 邦訳(武市正人)、『関数プログラミング』、近代科学社、1991、 ISBN 4-7649-0181-1。 [FAQforFunc,1998] Frequently Asked Questions for comp.lang.functional, edited by Graham Hutton, 1998. [FAQforML,1998] COMP.LANG.ML Frequently Asked Questions and Answers, compiled by Dave Berry and Greg Morrisett, maintained by Rowan Davies, 1998. [Felleisen,1998] Matthias Felleisen and Daniel P. Friedman, The Little MLer, MIT Press, 1998, ISBN 0-262-56114-X. [Freed,1996] Ned Freed and Nathaniel S. Borenstein, Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies, RFC 2045, 1996. [Hansen,1999] Michael R. Hansen and Hans Rischel, Introduction to Programming using SML, Addison-Wesley, 1999, ISBN 0-201-39820-6. [Harper,1993] Robert Harper, Introduction to Standard ML, 1993. [Johnsonbaugh,1997] Richard Johnsonbaugh, Discrete Mathematics, Fourth Edition, Prentice-Hall, 1997, ISBN 0-13-518242-5. [Knuth,1997] Donald E. Knuth, The Art of Computer Programming, Volume 1: Fundamental Algorithms, Third Edition, Addison-Wesley, 1997, ISBN 0-201-89683-4. [Milner,1991] Robin Milner and Mads Tofte, Commentary on Standard ML, MIT Press, 1991, ISBN 0-262-63137-7. [Milner,1997] Robin Milner, Mads Tofte, Robert Harper and Dave MacQueen, The Definition of Standard ML (Revised), MIT Press, 1997, ISBN 0-262-63181-4. [Paulson,1991] Lawrence C. Paulson, ML for the Working Programmer, Cambridge University Press, 1991, ISBN 0-521-42225-6. [Reade,1989] Chris Reade, Elements of Functional Programming, Addison-Wesley, 1989, ISBN 0-201-12915-9. [Romanenko,1997] Sergei Romanenko and Peter Sestoft, Moscow ML Language Overview, 1997. [Ullman,1993] Jeffrey D. Ullman, Elements of ML Programming, Prentice-Hall, 1993, ISBN 0-13-184854-2. 邦訳(神林靖)、『プログラミング言語ML』、アスキー出版局、 1996、ISBN 4-7561-1641-8。 [Ullman,1997] Jeffrey D. Ullman, Elements of ML Programming, ML97 Edition, Prentice-Hall, 1997, ISBN 0-13-080391-X. [石畑,1989] 石畑清、『アルゴリズムとデータ構造』、 岩波講座ソフトウェア科学、第3巻、岩波書店、1989、 ISBN 4-00-010343-1。 [大堀,1995] 大堀淳、「MLプログラミング」、 『コンピュータソフトウェア』、第12巻、第1-4号、岩波書店、 1995。 [大堀,1997] 大堀淳、『プログラミング言語の基礎理論』、 情報数学講座、第9巻、共立出版、1997、ISBN 4-320-02659-4。 [大堀,1999] 大堀淳、ジャック・ガリグ、西村進、 『コンピュータサイエンス入門/アルゴリズムと プログラミング言語』、岩波書店、1999、ISBN 4-00-005006-0。 [木村,1982] 木村泉、米澤明憲、『算法表現論』、 岩波講座情報科学、第12巻、岩波書店、1982。 [土井,1987] 土井範久、筧捷彦、『プログラミングの考えかた』、 コンピュータ入門、第1巻、岩波書店、1987、ISBN 4-00-007751-1。 [中島,1992] 中島秀之、上田和紀、 『楽しいプログラミングII(記号の世界)』、コンピュータ入門、 第5巻、岩波書店、1992、ISBN 4-00-007755-4。 [新出,1993] 新出尚之、「文字コードの国際規格について」、1993。 [萩谷,1998] 萩谷昌己、『関数プログラミング』、日本評論社、 1998、ISBN 4-535-60817-2。 [米澤,1992] 米澤明憲、柴山悦哉、『モデルと表現』、 岩波講座ソフトウェア科学、第17巻、岩波書店、1992、 ISBN 4-00-010357-1。