2012年3月5日月曜日

Pascalが私の好きな言語でない理由 | Taro-nishinoの日記 | スラッシュドット・ジャパン

Pascalが私の好きな言語でない理由 | taro-nishinoの日記 | スラッシュドット・ジャパン

Pascalが私の好きな言語でない理由
1981年4月2日 Brian W. Kernighan

要約
プログラミング言語Pascalは、コンピュータサイエンス教育における授業の主要な言語となっている。また、その後の言語開発に強い影響を与えている(特に、Ada)。
Pascalは元々主として教育用言語として意図されたが、シリアスなプログラミング(例えば、システムプログラミング作業やオペレーティングシステムさえも)のためにも益々推奨されている。

Pascalは、少なくともその標準の形は平凡であり、シリアスなプログラミングに向いていない。この論文は、その理由の幾つかの私個人の発見を議論する。

1. 起源
この論文は2つのイベントに起源を持つ。すなわち、CとPascalの比較についての論文(1, 2, 3, 4)が次々に発表されていることと、'Software Tools'(5)をPascalで書き直す私個人の試みだ。

CとPascalの比較は、殆どLearjet(訳注:ビジネスジェット機)とPiper Cub(訳注:軽量プロペラ機)の比較に似ている。一方は何かをさせるため、他方は学習のため。従って、そのような比較は幾分非現実な趣がある。だが、'Software Tools'の改訂版は更に的を得た比較だと思われる。その中で、プログラムは元々Ratfor(プリプロセッサで実装された、Fortranの「構造的」方言)で書かれた。Ratforは実は変装したFortranだから、Pascalがもたらす重要なもの、つまり文字処理にもっと適したデータタイプ、一般の人のデータ編成をうまく定義するためのデータ構造化能力、データについて真実を語る強い型付け、を持っていない。

Pascalでプログラムを書き直すつもりよりも、もっと困難であることが判明した。この論文は、プログラミングのためのPascalの適合(プログラミングのための学習と区別して)について経験した教訓を引出す試みである。それは、Pascalと、C又はRatforの比較ではない。

プログラムは最初、バークレイのカリフォルニア大学で開発されたPascalイン� �ープリタpiによりサポートされたPascal方言で書かれた。その言語は、JensenとWirthの名目上の標準(6)に近く、良い原因診断と注意深いランタイムチェックが付いている。それ以降、プログラムは4つの他のシステムでも走り続け、プリミティブな新しいライブラリを除いて変更は無い。4つの他のシステムとは、アムステルダム自由大学(以下、Vrije Universiteitを意味する、VUとして引用する)からのインタープリタ、バークレイシステムのVAX版(本当のコンパイラ)、Whitesmiths, Ltd.から提供されているコンパイラ、Z80上のUCSD Pascalである。最後を除いて、これらのPascalシステムはすべてCで書かれている。

Pascalは議論の多い言語である。最近の参考文献表(7)は"議論、解析、討論"の表題の許に175項目をリストしている。最も屡々引用される論文(非常に読む価値がある)は、Habermann(8)による強い批判、LecarmeとDesjardinss(9) による同じく強い返答だ。BoomとDeJong(10)による論文も良い読み物だ。Wirth自身のPascalの評価は[11]で見つかる。私は文献を総括する意欲も能力も無い。この論文は私個人の観察を記録し、その大部分は必ず他の人の見解と重なる。私は、以下の問題点周辺に題材を編成する。
・型とスコープ
・コントロールフロー
・環境
・コスメティック
及び、多少重要度が下がるエリアについて。

始めから私の結論を述べる。初心者にプログラムの方法を教えるためにはPascalは立派な言語かも知れない。それについて私は直接の経験を持っていない。1968年では相当な業績だった。最近の言語設計に確かに影響を与え、その内でもAdaが最も重要だろう。しかし、その標準形式(現行と提案済みの両方)の中で、実際のプログラムを書く� �めにはPascalは適当ではない。環境とトリビアルなやり取りをし、他の誰かによって書かれたプログラムを使用しない、小さな自給自足プログラムだけのために適している。

2. 型とスコープ
Pascalは(ほぼ)強い型付け言語だ。大雑把に言えば、プログラムの各オブジェクトは、オブジェクトの正当な値とオブジェクト上のオペレーションを暗黙に定義する、上手に定義された型を持つことを意味する。コンパイルとランタイムチェックの或る混合によって、言語は不当な値とオペレーションを禁じることを保証する。勿論、コンパイラは言語定義の中で暗黙に示されたチェックの全てを実際にはしないかも知れない。更に、強い型付けは次元解析で混乱しない。人が以下のように'apple'と'orange'を定義するならば、

type
     apple = integer;
     orange = integer;

appleとorangeを伴なう任意の勝手な算術式は完全に正当である。

強い型付けはいろいろな方法で現れる。例えば、関数とプロシージャへの引数は正式な型マップのためにチェックされる。整数を期待するサブルーチンに浮動小数点数を渡す、Fortranの自由は消えている。これは、確実にエラーを引き起す指示を警告するのだから、Pascalの望ましい特質であると私は考える。

整数型変数は正当な値の範囲を持つと宣言されるかも知れない。コンパイラとランタイムサポートは、小さなものしか保持しない変数に大きな整数を置かないことを保証する。勿論ランタイムチェックはかなりな科料を払うけれども、これもまたサービスのようであるらしい。

型とスコープの幾つかの問題に移ろう。

2.1. 配列のサイズはその型の一部である
人が以下のように宣言するなら、

var arr10 : array [1..10] of integer;
    arr20 : array [1..20] of integer;

arr10とarr20は各々10と20の整数の配列だ。整数配列をソートするためのプロシージャ'sort'を書きたいとしよう。arr10とarr20は違う型を持っているので、両方をソートする単一のプロシージャを書くことは可能ではない。

これが特に'Software Tools'、そして一般的にプログラムに影響すると私が思う処は、ソーティングのような共通的、一般的な目的のオペレーションをするためのルーティンのライブラリを作るのを実に困難にしていることだ。

最も常に影響を受ける特別なデータタイプは'文字の配列'で、Pascalでは文字列は文字の配列だからである。文字列aの中で文字cが最初に現れる位置又は無ければ0を返す関数'index(s,c)'を書くことしよう。'index'の文字列引数を処理する方法が問題だ。文字列の長さが違うので、'index('hello',c)'と'index('goodbye',c)'の呼出し両方を正当には出来ない(出来ないのだから、'hello'のような固定文字列の終りをどうやって感知するかの疑問は省く)。次に試みることは、

var temp : array [1..10] of char;
temp := 'hello';
 
n := index(temp,c);

しかし、'hello'と'temp'は違う長さなので、'temp'への代入は不当だ。

この無限の後退から抜け出す唯一つの手段は、各可能な文字列の長さのためのメンバーを持つルーティンのファミリーを定義することか又は、文字列すべて('define'のような固定文字列を含む)同じ長さにすることである。

後者のアプローチは、2つの大きな邪悪のうち、ましな方だ。'Software Tools'では、'string'と呼ばれるデータタイプは、

type string = array [1..MAXSTR] of char;

として宣言され、ここで定数'MAXSTR'は"十分に大きく"、そして、全てのプログラムの全ての文字列は正確にこのサイズだ。プログラムを走らせることが可能だけれども、これは理想からは程遠い。便利なルーティンの本当のライブラリを作る問題の解決にならない。

固定サイズの配列表現が簡単には受入れられない幾つかの状況がある。例えば、テキストの行をソートするための'Software Tools'プログラムは、収容出来るだけ多くの行を持つメモリを満杯にすることによって作動する。その動作時間は、どのようにメモリを目一杯にパックするかに依存する。

従って、'sort'のためには別の表現が使われる。文字の長い配列と、この配列へのインデクスのセットである。

type charbuf = array [1..MAXBUF] of char;
   charindex = array [1..MAXINDEX] of 0..MAXBUF;


漁業は、南アフリカに重要な方法です。

しかし、固定長表現を処理するために書かれたプロシージャと関数は可変長形式と一緒には使えない。全く新しいルーティンのセットは、この表現の中で文字列のコピーと比較をすることが必要とされる。Fortran又はCでは、同じ関数が両方に使われるだろう。

上で示したように、固定文字列は、

として書かれ、'packed array [1..n] of char'の型を持つ。ここでnは長さである。従って、異なる長さの各文字列リテラルは異なる型を持つ。メッセージをプリントし、クリーンアップするルーティンを書く唯一つの方法は、全てのメッセージを同じ最大長へ膨らませることだ。つまり、

error('short message                    ');
error('this is a somewhat longer message');

多くの商用Pascalコンパイラは、問題を明示的に避ける'string'データタイプを与える。'string'は長さと無関係に同じ型に取られる。これは、この単一のデータタイプの問題を解決するが、他はそうではない。固定文字列の長さを計算するような二次的な問題を解くことも失敗する。別のビルトイン関数が通常の解法だ。

Pascalの熱狂ファンは屡々、配列サイズ問題にうまく対処するためには、幾つかのライブラリルーティンをコピーし、プログラムのパラメータに手で記入しなければならないだけだと主張するが、その弁明は良く言っても弱い(12)。

"配列の境界はその型の一部(又はもっと正確に、そのインデクスの型)であるので、異なる境界を持つ配列に適用するプロシージャ又は関数を定義することは不可能だ。この制約は深刻なものに見えるかも知れないけれども、私達がPascalで持っている経験は、それは殆ど発生しないことを示すようだ。[...]しかし、パラメトリック配列のサイズをバインドする必要は、プログラムライブラリの使用に関して深刻な欠陥である。"

この言い損ないがPascalについて最も大きな一つの問題だ。フィクスされるのであれば、その言語はとてつもなく便利になるであろうと私は信じる。提案されたPascalのISO標準(13)はそのようなフィクス("順応な配列スキーマ")を与えているが、標準のこの部分の承認は明らかに依然としてどうなるか分か� ��ない。

2.2. 静的変数と初期化が無い
'静的'変数(よくAlgolが盛んな処では'それ自身の'変数と呼ばれる)は、あるルーティンにプライベートで、ルーティンの一つの呼出しから次の呼出しまで、その値を保持するものだ。事実上Fortranの変数は、COMMONを除いて内部的に静的だし、Cでは、ローカル変数に適用出来る'static'宣言がある(厳密に言えば、Fortran 77では、静的属性を強制するにはSAVEを使用しなければならない)。

Pascalはそんなストレージクラスを持っていない。Pascal関数又はプロシージャが一つの呼出しから別の呼出しまで値を記憶するつもりならば、関数又はプロシージャに対して、使用される変数は外部でなければならないことを意味する。従って、他のプロシージャに可視でなければならず、更に大きなスコープでは、その名前は一意でなければならない。問題の簡単な実例はランダムナンバージェネレータだ。現行の出力に使用された変数は次のものを計算するため保存されなければならない。だから、ランダムナンバージェネレータの呼出し全てを含むライフタイムの変数に格納されなければならない。実際には、これは典型的にプログラムの最も外側のブロックだ。 従って、そのような変数の宣言は、それが実際に使われている場所からは非常に離れている。

一つの実例は'Software Tools'の第7章に書かれたテキストフォーマッタから来る。変数'dir'は、進路理由付けの間、交互に左右を獲得するため、超過ブランクが挿入された処からの方向をコントロールする。Pascalでは、そのコードは次のようである。

program formatter (...);
 
var
  dir : 0..1;     { direction to add extra spaces }
  .
  .
  .
procedure justify (...);
begin
  dir := 1 - dir; { opposite direction from last time }
  ...
end;
 
  ...
 
begin { main routine of formatter }
  dir := 0;
  ...
end;

変数'dir'の宣言、初期化、使用がプログラム全体に散らばっていて、文字通り何百行と離れている。C又はFortranでは、'dir'は、それを知る必要のあるルーティンのみへプライベート化出来る。

...
main()
{
  ...
}
 
...
 
justify()
{
  static int dir = 0;
 
  dir = 1 - dir;
  ...
}

勿論、もっと大規模で同じ問題の他の実例も多くある。バッファされたI/Oの関数、ストレージ管理、シンボルテーブル、すべてが心に浮かぶ。

少なくとも2つ関連する問題がある。Pascalは静的に変数を初期化する方法(すなわち、コンパイル時に)を持たない。FortranのDATAステートメント又は以下のようなCでの初期化子

に類似なものはない。
これは、Pascalプログラムが変数初期化のために明示的な代入ステートメントを含まなければならないことを意味する(以下のように)。

このコードはプログラムソーステキストを大きくさせ、ランタイム時プログラム自体を大きくさせる。

更に、静的ストレージクラスの欠如により引き起こされる大き過ぎるスコープの問題を、初期化子の欠如はより悪化させる。初期化すべき時は開始時であり、メインルーティン自体が多くの初期化コードで始まるか又は、初期化するための一つ又はそれより多くのルーティンを呼ぶ。どちらの場合でも、初期化されるべき変数は可視でなければならず、それは階層の高いレベルで有効である。その結果は、初期化されるべき任意の変数はグローバルスコープを持つということだ。

3番目の困難は、最小共通先祖又はそれより上の先祖で宣言されていなければ、2つのルーティンが変数を共有する方法が無いことである� �FortranのCOMMONとCの外的且つ静的ストレージクラスの両方が、先祖の情報の共有無しに、2つのルーティンがプライベートに協調する方法を与えている。

新しい標準は静的変数、初期化、又は非階層型コミュニケーションを提供していない。

2.3. 関連プログラムコンポーネントは分離されなければならない
元々のPascalはワンパスコンパイラで実装されていたので、言語は使用前の宣言に信頼を置く。特に、プロシージャと関数は、それらが使用される前に宣言されていなければならない(本体とすべて)。その結果は、典型的なPascalプログラムはボトムアップから読む。つまり、全プロシージャと関数が、任意のコードがそれらを呼出す前に、全てのレベルで表示される。これは、関数が設計され使用される順序と本質的に反対だ。

CやRatforの#include機能のようなメカニズムにより、ある程度これは緩和される。すなわち、プログラムをごった返しすること無しに必要な処でソースファイルはインクルードされる。UCB、VU、Whitesmithsのコンパイラ全てが#includeを与えているけれ ども、標準Pascalの一部ではない。

Pascalでは本体から関数又はプロシージャのヘッダーの宣言を分離することを許す'先行'宣言もあり、それは相互再帰プロシージャを定義するために意図されている。本体が後で宣言される時、その宣言上のヘッダーは関数の名前だけを含んでもいいが、最初のインスタンスからの情報を繰り返してはならない。

関連する問題は、Pascalが宣言を喜んで受入れる厳密な順序を持つことだ。各プロシージャ又は関数は

label ラベル宣言(あれば)
const 定数宣言(あれば)
type  型宣言(あれば)
var   変数宣言(あれば)
 
procedureとfunction宣言(あれば)
begin
関数又はプロシージャ本体
end


トップ10のゴルフドライバー

から成る。
これは、プログラムを分り易くするため論理的に関連あるものを纏めたいとプログラマが思っている時でも、コンパイラの便宜のために、一つの種類(例えば、型)の全宣言は共にグループ化されなくてはならないことを意味する。プログラムはコンパイラに全て一度に送られなければならないから、宣言、初期化、型の使用、変数を近くに纏めることは滅多に可能ではない。最も熱心なPascal支持者の一部の人でさえ賛成している(14)。

"構造化された大きなプログラム内で、そのようなグループ化を出来ないことは、Pascalの最も不満な制約の一つである"

ファイルのインクルージョン機能は、ここでは少ししか役に立たない。

新しい標準は、宣言の順序についての必要条件を緩めていない。

2.4. 分割コンパイルがない
"公式"Pascal言語は分割コンパイルを与えていない。なので、各実装は何をすべきか自身で決定している。一部は(例えば、バークレイインタープリタ)は完全に許していない。これは言語の精神に最も近く、文字通り正確にマッチする。他の多くは、関数の本体が外部的に定義されていることを指定する宣言を与えている。いずれにせよ、そのようなメカニズム全てが非標準であり、異なるシステムは異なることをする。

理論的には分割コンパイルは必要でない。誰かのコンパイラが素晴らしく高速なら(且つ、全てのルーティンのソースが入手可能で、そのコンパイラがファイルインクルージョン機能を持ち、その結果ソースの複数コピーが必要でないなら)、すべての再コンパイルは同等である。勿論� ��際には、コンパイラはそんなに速くなく、ソースは屡々隠れ、ファイルインクルージョンは言語の一部でないので、変更は時間を喰う。

一部のシステムは分割コンパイルを許すが、境界を跨って型の一貫性を確認しない。これは強い型付けに大きな穴を作る(他の大部分の言語もクロスコンパイルチェックしないので、この点でPascalは劣っていない)。私は少なくとも一つの論文(幸いにも未発表だが)を見たことがある。その論文は、nページにて、分割コンパイル境界を跨ぐ型チェックにCが失敗することを厳しく非難し、一方n+1ページにて、Pascalとうまく付き合う方法は、型チェックを避けるためプロシージャを分割してコンパイルすることだと示唆している。

新しい標準は分割コンパイルを提供していない。

2 .5. 型とスコープのその他の問題
以下の諸細目は小さないらいらだが、どこかでそれらに触れなければならない。

プロシージャのリテラルフォーマルパラメータとして非ベーシック型の名前付けは正当でない。つまり、以下が許されていない。

procedure add10 (var a : array [1..10] of integer);

それどころか、型名を案出して型宣言をし、その型のインスタンスであるフォーマルパラメータを宣言しなければならない。

type a10 = array [1..10] of integer;
...
procedure add10 (var a : a10);

当然、その型宣言は、それを使うプロシージャからは物理的に離れている。型名を案出する躾は、よく使用される型にはいいことだが、一度限りなら邪魔だ。

関数とプロシージャのフォーマルパラメータに対して宣言'var'を持つことはいいことである。プロシージャは、引数を変更するつもりであるとはっきり述べているからだ。しかし、呼ぶプログラムは、変数は変更されると宣言する方法を持たない。情報は一ヶ所のみにあるが、2ヶ所ならなお良い(しかし、パン半分でも無いよりはましだ。Fortranはユーザに、誰が変数に対して何をするのか、何も語らない)。

デフォルトでは配列は値渡しであることも少し面倒だ。その実質的結果は、全ての配列パラメータはプログラマにより大概考え無しで'var'と宣言される� ��いうこである。もし'var'がうっかり省略されるなら、出力されるバグは難解だ。

Pascalの'集合'概念は、便利な記法と何らかの自由な型チェックを与えるので、いい考えだ。例えば、

if (c = blank) or (c = tab) or (c = newline) then ...

のようなテストの集合は、

if c in [blank, tab, newline] then ...

として、もっと明快に、おそらくもっと効果的に書ける。しかし、集合型は強く実装依存(多分、オリジナルのCDC実装が59ビットだったから)なので、実際には集合型はこれより以上には役立たない。例えば、関数'isalphanum(c)'(``is c alphanumeric?'')を、

{ isalphanum(c) -- true if c is letter or digit }
function isalphanum (c : char) : boolean;
begin
    isalphanum := c in ['a'..'z', 'A'..'Z', '0'..'9']
end;

と書くのは自然である。だが、多くのPascal実装(オリジナルを含めて)では、集合が単に小さ過ぎるというだけで、このコードは失敗する。従って、ポータブルなプログラムを書く積りなら、集合を一般的に使用しないのが最もいい(この明快なルーティンも、範囲テスト又は配列参照を使うよりも、集合を使う方が断然に遅く走る)。

2.6. 脱出がない
必要な時、型メカニズムをオーバーライドする方法、Cにおける"cast"メカニズムのようなものが無い。これは、ストレージアロケータ又はI/OシステムのようなプログラムをPascalで書くことは可能ではないことを意味する。返すオブジェクトの型を話す方法が無く、そのようなオブジェクトを他の使用のために任意の型に強制する方法が無いからである(厳密に言えば、可変レコード付近の型チェックに大きな穴があり、それ以外は不正型チェックメカニズムは得られる)。

3. コントロールフロー
Pascalのコントロールフローの欠乏は小さいが、多い。致命点への一撃よりも、何百の傷の死亡である。

論理的演算子'and'と'or'の評価順序に保証が無い。つまり、Cでの&&と||のようなものは無い。他の大部分の言語でもそうだが、この欠点はループコントロールに殆ど打撃を与える。

while (i <= XMAX) and (x[i] > 0) do ...

は、x[i]の前にiがテストされる保証が無いのだから、極端に賢くないPascalの使用である。

ところで、このコードでの括弧は必須だ。言語は関係演算子を最下位に4つのレベルの演算子優先順位しか持たない。

ループを抜けるのに、'break'ステートメントがない。これは、構造化プログラミングの支持者によって採用された哲学、一つのエントリ-一つの出口に忠実であるが、特に論理式が評価される順序のコントロール不能が連結している時、気色悪い遠まわしな又は重複したコードに導く。この同じ状況を、C又はRatforで記述を考えよう。

while (getnext(...)) {
    f (something)
        break
    rest of loop
}

'break'ステートメントが無い場合、Pascalでの最初の試みは、

done := false;
while (not done) and (getnext(...)) do
    if something then
        done := true
    else begin
        rest of loop
    end

だが、'getnext'の次の呼出しの前に、"not done"を評価を強制させる方法が無いのだから、これは動かない。幾つかの失敗の後、以下に帰着する。

done := false;
while not done do begin
    done := getnext(...);
    if something then
        done := true
    else if not done then begin
        rest of loop
    end
end

勿論、常習犯はループを抜け出すのに'goto'とラベル(数字だけで、予め宣言しなければならない)を使える。そうでなければ、早期抜け出しは苦痛であり、ほぼいつもブーリアン変数とある量のずるを要求される。Ratforで配列内のブランクでない最後を見つける探索と比較せよ。

for (i = max; i > 0; i = i - 1)
     if (arr(i) != ' ')
         break

Pascalを用いると

done := false;
i := max;
while (i > 0) and (not done) do
    if arr[i] = ' ' then
         i := i - 1
    else
        done := true;


ここでライブを認めるのですか?

'for'ループのインデクスはループの外側で定義されていないから、最後まで行ったのかそうでないのか知ることは可能ではない。小さな制約として、'for'ループのインクリメントは+1又は-1のみ出来る。

再びワンイン-ワンアウトの理由のため、'return'ステートメントが無い。擬似バリューに値をセットすることにより関数返り値は返されれ(Fortranでするように)、関数の最後に落ちる。これは時には、固有の値を持つ関数の終りへ全パスが行き着くことを保証するための歪みをもたらす。多くの実装が緊急終了を引き起す'halt'を与えているけれども、最も外側のブロックへ行き着くことを除いて、実行を終了させる標準の方法も無い。

'case'ステートメントはCよりも良く設計されている。但し、'default'句が無く、入力式が どのケースにもマッチしなければ、その振舞いは未定義である。このきわめて重大な省略は'case'概念をほぼ無意味にしている。'Software Tools in Pascal'の6000行を超える中で、'default'があったならば、'case'は少なくとも一ダースの場所で役に立ったであろうに、私は4回しか使わなかった。

新しい標準はこれらの点に関して何の救済も無い。

4. 環境
Pascalのランタイム環境は比較的希薄で、おそらく"公式"言語内のソースレベルのライブラリを除いて、拡張メカニズムは無い。

PascalのビルトインI/Oは受けて当り前の悪い評判を持つ。レコード指向の入力及び出力に強く価値を置いている。インタラクティブ環境の真当な実装を困難にする先読み様式も持っている。基本的に、I/Oシステムは処理されるレコードの一つ先のレコードを読まなければならないと信じていることが問題である。これは、インタラクティブシステムにおいて、プログラムが走る時、その最初のオペレーションは、プログラム自身がやることの前に、入力の最初の行のためにターミナルを読もうとすることを意味する。だが、プログラム内では

write('Please enter your name: ');
read(name);
...

先読みは、入力のためのプロンプトのプリントの前に入力を待っているので、プログラムはハングする。

もっと注意深い実装によって、このI/O設計の酷い結果の大部分から抜け出すことは可能であるが、全てのPascalシステムがそうとは限らないし、何れにせよコストがかかる。

I/O設計は、Pascalが設計されたオペレーティングシステムを反映している。その欠点ではないとしているけれども、Wirthでさえ偏りを認めている(15)。テキストファイルはレコード、すなわちテキストの行から成ると仮定されている。行の最後の文字が読まれる時、ビルトイン関数'eoln'は真となる。その時点で、新しい行の読込みを始め、'eoln'をリセットするために'readln'を呼ばなくてはならない。同様に、ファイルの最後の文字が読まれる時、� �ルトイン関数'eof'は真となる。どちらの場合も、'eoln'と'eof'は、後ではなく、'read'の前にテストされなければならない。

これを考えれば、賢い入力をシミュレーションすることで相当の苦痛が取り除かれるはずである。この'getc'の実装は、バークレイとVUのI/Oシステムで動くが、他の全てで必ずしも動くとはならないかも知れない。

{ getc -- read character from standard input }
function getc (var c : character) : character;
var
    ch : char;
begin
    if eof then
        c := ENDFILE
    else if eoln then begin
        readln;
        c := NEWLINE
    end
 
    else begin
        read(ch);
        c := ord(ch)
    end;
    getc := c
end;

ENDFILEとおそらくNEWLINEは'char'変数に対して不当な値ではないので、型'character'は'char'と同じではない。

各プログラムを始める'program'ステートメントの中の(有効な)論理的ユニット番号により名前付けられた定義済みファイルを除いて、ファイルシステムへのアクセスの考えは全く無い。これは、Pascalが元々開発されたCDCバッチシステムを明らかに反映している。ファイル変数

は、非常に特別な種類のオブジェクトだ。割当ても出来なければ、'eof'、'eoln'、'read'、'write'、'reset'、'rewrite'('reset'はファイルを巻き戻し、再読み込みを容易にする。'rewrite'は書くためのファイルを用意する)のようなビルトイン関数への呼出しを除いて、使用も出来ない。

Pascalの多くの実装が、外部環境からの名前によってファイルへのアクセスを許す回避手段を与えている。だが、便利でも標準でもない。例えば、多くのシステムが、'reset'と'rewrite'の呼出しの中でファイル名引数を許す。

しかし、'reset'と'rewrite'はプロシージャであって、関数ではない。ステータスの返りは無いし、ある理由のためアクセスが失敗すればコントロールの回復方法が無い(UCSDシステムは通常アボートを無力にするコンパイルフラグを用意している)。そして、

reset(fv, filename);
if fv = failure then ...

のような式にfvが出現出来ないのだから、その方針でも脱出が無い。この拘束は、ファイル名のミススペル等から回復するプログラムを書くことを本質的に不可能にしている。'Software Tools'の改訂版で、私は十分に解決しなかった。

再度、おそらくPascalのバッチシステムの起源を反映しているので、コマンドライン引数にアクセスする考えは無い。環境に非標準のプロシージャを加えることによって、ローカルルーティンはアクセス出来る。

Pascalでは多目的ストレージアロケータを書くことが可能ではないので(そのような関数が返す型について語る術を持たない)、言語はヒープ上にスペースを割当てる'new'と呼ばれるビルトインプロシージャを持つ。定義された型のみが割当てられるので、例えば、文字列を保持するために任意サイズの配列を割当てることは可能でない。'new'によって返されるポインタは周辺に渡されてもいいが、処理されない。つまり、ポインタ算術が無い。ストレージが尽きると、コント� ��ールを回復する方法が無い。

新しい標準はこれらの分野に何の変更も無い。

5. コスメティック
経験あるプログラマにとって、これらの問題の大部分がうんざりで、おそらく初心者にとっても迷惑だろう。だが、共有しよう。

他のAlgolに啓発された言語も共通に、Pascalはセミコロンをターミネータ(PL/IやCではターミネータ)と言うよりもむしろセパレータとして使う。結果として、ステートメントが厳密にセミコロンを置くことに人は道理にかなう洗練された考えを持つに違いない。おそらくもっと重要なことは、適切な処にセミコロンを置くことについて人が真剣であれば、相当量の迷惑な編集作業が必要である。プログラムで最初の場面を考えよう。

しかし、bの前に何かを挿入しなければならないなら、今やbは'end'に先行するので、セミコロンはもはや必要でない。

if a then begin
    b0;
    b
end;
c;

今、'else'を追加するなら、'end'のセミコロンを削除しなければならない。

if a then begin
    b0;
    b
end
else
     d;
c;

そして、等々。プログラムの発展に伴って、セミコロンのさざ波が上下する。

プログラマの心理に一般的に受入れられている実験結果は、セパレータとしてのセミコロンはターミネータとしてのセミコロンよりも10倍エラーしがちであることだ(Pascalに基づく最も重要な言語Ada(17) では、セミコロンはターミネータである)(16)。幸い、Pascalでは、人はほぼいつも目を瞑り、ターミネータとしてのセミコロンを隠し通せる。例外は宣言のような場所にあり、そこでセパレータvs.ターミネータ問題は少なくても真剣ではない。そして'else'の直前だが、憶えているのは容易だ。

CとRatforプログラマは、{と}に比べて'begin'と'end'を嵩張っていると感じる。


関数名それ自体で、その関数の呼出しだ。関数の名前を知っている場合を除いて、そのような関数呼出しと簡単な変数を区別する方法が無い。Pascalは、関数名に関数内の変数かのように振舞わせるFortranのトリックを使う。但し、Fortranでは関数名は実のところ変数であって、式の中に出現出来るが、Pascalでは式の中の出現は再帰呼出しである。fが引数無し関数なら、'f:=f+1'はfの再帰呼出しだ。

演算子は少ししか無い(おそらく、優先順位レベルの少なさに関係する)。特に、ビット操作演算子(AND、OR、XOR、等)が無い。私は、次のようなトリビアルな暗号化関数をPascalで書こうとしたが、諦めた。

i := 1;
while getc(c) <> ENDFILE do begin
    putc(xor(c, key[i]));
    i := i mod keylen + 1
end

賢い'xor'を書けなかったからだ。集合型は少しは役立つ(言ってみれば)が、十分ではない。Pascalがシステムプログラミング言語と主張する人は一般的にこの点を見逃している。例えば、[18, p. 685]

"Pascalは現時点[1977]でシステムプログラミングとソフトウェア実装のためには公的分野で最も優れた言語である"

は少し世間知らずだと思われる。

おそらくPascalは文字列の中に埋め込まれたクオートを指し示すためにダブルクォートを使うので、ヌル文字列は無い。

文字列の中に非グラフィックシンボルを置く方法が無い。もっとはっきり言えば、標準言語のどの部分もそれらに言及していないから、非グラフィック文字は強い意味で非人間的だ。改行、タブ等のような概念は各システム上"特殊"な規則(普通は文字集合、例えばASCIIの改行は10進数値10を知っていること)で処理される。

マクロプロセッサが無い。明白な定数を定義するための'const'メカニズムは、Cでの簡単な#defineステートメントの使用の約95%を担当するが、もっと込み入ったものは絶望的だ。Pascalコンパイラにマクロプリプロセッサを載せることは確かに可能である。これは、

#define error(s)begin writeln(s); halt end

のような賢い'error'プロシージャをシミュレーションすることを可能にする('halt'は結局のところ、一番外側のブロックへの分岐として定義されるかも知れない)。それから、

error('little string');
error('much bigger string');

のような呼出しは、'writeln'(標準Pascal環境の一部として)は任意のサイズの文字列を取扱えるから、動く。この便利なものを可能にしてルーティンにする方法が一般的に無いことは不幸だ。

言語は宣言での式を禁止しているので、

const   SIZE = 10;
type    arr = array [1..SIZE+1] of integer;

のようなことを、又はもっと簡単なものでさえ、

const   SIZE = 10;
SIZE1 = SIZE + 1;

書くことは可能でない。

6. 展望
'Software Tools'の中のプログラムを書き直す苦心は1980年3月に始まり、やったりやらなかったりして、1981年1月まで続いた。最終製品(19)は1981年6月に刊行された。その間、私は徐々にPascal(コスメティック、コントロールフローの不完全)の表面に順応し、重要なもの(配列サイズ、ランタイム環境)に対して不完全な解法を開発した。

本の中のプログラムは、トリビアルでない仕事をする完全な、うまく工作されたプログラムになっているはずである。だが、それらは十分である必要は無く、複雑にオペレーティングシステムと相互作用もしていない。だから、私は非常に急場しのぎの解法(実際のプログラムのためには働かない)で通り抜けられた。

PascalがCより優れていると私が感じた重要な方法は無いが、Ratforより明らかな改善がある 処が多くある。最もはっきりとしているのは再帰だ。多くのプログラムが、再帰的書かれている時(取り分けパターン検索、クイックソート、式評価)にずっと明快である。

列挙データ型はいい考えだ。それらは、正当な値の範囲の境界を定め、ドキュメント化する。レコードは関連する変数のグループ化に役立つ。ポインタは比較的殆ど使用しないことが分かった。

ブール型変数はブール条件のための整数よりもいい。Fortranの論理変数は駄目な設計なので、元のRatforプログラムは幾つか不自然な考えを含んでいた。

プログラムを書いている最中に手の間違いをたまにPascalの型チェックは警告した。値のランタイムチェックも随時、エラー特に指標範囲違反を報告した。

ネガティブな面に振り返ると、ソースの単一行� �変えるため最初から大きなプログラムを再コンパイルすることは非常に骨が折れる。型チェック有り又は無しの分割コンパイルは大きなプログラムには必須だ。

Pascalの文字列と非グラフィックの扱いは非常に不適当なので、文字はPascalの一部でありFortranの一部でないという事実からの恩恵を受けなかった。両方の言語では、キーワード、エラーメッセージ、その他類似のテーブルのためのリテラル文字列を初期化することは、甚だしく下手くそである。

仕上がったプログラムは概してRatfor同等とほぼ同じ行数である。私の予断ではPascalは口先が多くて式が少ない言語だったから、最初これは私を驚かせた。ループ制限や指標のような処にPascalは任意の式を許可し、Fortran(すなわちポータブルFortran 66)はしていないのが本当の理由らしい。だから、幾つかの不要な代入は削除出来る。更に、Ratforプログラムは関数を宣言し、Pascalはしない。

終わるに当たって、Pascalに不利な場合の主なポイントを纏めさせて欲しい。

1. 配列のサイズがその型の一部であるので、多目的なプログラム(すなわち、異なるサイズの配列を処理すること)を書くことは可能でない。特に文字列処理は非常に難しい。
2. 静的変数、初期化、プログラム(変数は本来よりもずっと大きいスコープを要求する)の"地域"を殺すための非階層的組合のコミュニケーション方法の欠如。
3. 言語のワンパス性質は、プロシージャと関数が不自然な順序で送られることを強制する。様々な宣言の強制分離は、論理的に同じ所属であるプログラムコンポーネントをあちこちに散らかす。
4. 分割コンパイルの欠如は、大きなプログラムの開発の邪魔をし、ライブラリの使用を不可能にする。
5. 論理式の評価の順序をコントロール出来ない。それは、入組んだコードと異質な変数を導く。
6. 'case'ステートメントが、default句が無い故に骨抜きにされている。
7. 標準I/Oの欠陥。標準言語の一分として、ファイル又はプログラム引数を取扱う賢い用意が無く、拡張メカニズムが無い。
8. 大きなプログラムを編成するのに必要なツールの大部分(特にファイルインクルージョン)を言語は欠如している。
9. 脱出が無い。

最後のポイントがおそらく最も重要だ。言語は不十分だが、制限される。その制約を脱出する方法が無いからである。必要な時に型チェックを無力にするキャストが無い。"標準プロシージャ"を定義するコンパイラをコントロールしなければ、欠陥ランタイム環境を賢いもので置換える方法が無い。言語は閉鎖的である。

真剣なプログラミングのためにPascalを使う人は致命的な罠にはまる。

言語はとても重要なので、拡張されなければならない。しかし、各グループは各々の方向で、彼等が本当に欲しい言語なら何でも似るようにPascalを拡張している。分割コンパイルのための拡張、FortranライクなCOMMON、文字列データ型、内部静的変数、初期化、8進数、ビット操作等すべてが一つグループのための言語のユ ーティリティに追加している。しかし、他者へのポータビリティを殺している。

本来のターゲットを遥かに超えて何にでもPascalを使うのは間違っていると私は感じる。純粋な形式では、Pascalは玩具言語で、教育に適しているが、本当のプログラミング用ではない。



These are our most popular posts:

データセット (IBMメインフレーム) - Wikipedia

... される。データセットという言葉は OS/360 で使い始められ、MVS、OS/390、z/OS に 至るまで使い続けられている。 ... DSORG ( data set organization ) や RECFM ( record format ) などとして構造化されたブロックとして定義されたものである。 ... この ことは、Unix や Windows や Mac OS などの小さなコンピュータのシステムに見られる 構造化されないバイトの流れ ( stream of bytes ) とは対照をなすものである。 ... この タイプのデータセットは、実行形式のプログラム、ロードモジュール を保持したりするのに 使われる。 read more

GHCのこと

GHCではIntやDoubleなどの型はプリミティブではなく、内部に非ボックス化型(ja)を 持つ代数的データ型として定義されている。これらの非ボックス化 ... 例として以下に、 小さなモジュールと、それをghc-7.0.1 -ddump-prep(最適化なし)でコンパイルした結果 を表示しておく。 module M ..... 構築子フィールドの型指定にUNPACKプラグマ(ja)を 付けると、その型の中身を直接(たとえばIntの代わりにInt#を)保持することができる。 UNPACK ... read more

Pascalが私の好きな言語でない理由

2010年1月27日 ... Pascalは元々主として教育用言語として意図されたが、シリアスなプログラミング( 例えば、システムプログラミング作業やオペレーティング ... すなわち、CとPascalの比較 についての論文(1, 2, 3, 4)が次々に発表されていることと、Software Tools(5)を Pascalで書き直す私個人の試みだ。 ... 勿論、コンパイラは言語定義の中で暗黙に示 されたチェックの全てを実際にはしないかも知れない。 ... コンパイラとランタイムサポート は、小さなものしか保持しない変数に大きな整数を置かないことを保証する。 read more

列挙型 - Wikipedia

言語によっては、真偽値の論理型は、あらかじめ宣言された二値の列挙型とされている 。 .... 型の定数として CLUBS 、 DIAMONDS 、 HEARTS 、 SPADES を定義している が、これらがその型の変数に保存されるときは、(暗黙裡 ... また、列挙型は利用者定義 型の一種であるということから、列挙型に対しての演算子多重定義も可能となっている。 ... C#プログラミング言語の列挙型はCのenumの意味する多くのを保持 する。 read more

0 件のコメント:

コメントを投稿