- 1989年の FM TOWNS 向け High C Compiler は、DOS 環境への対応を超えて、当時の C コンパイラとしては珍しいユーザー志向の言語機能を数多く備えていた
- Phar Lap の DOS extender と組み合わされ、16ビット MS-DOS 環境で 32ビット 80386 を活用する開発の流れの中で、FM TOWNS の 1st-party C コンパイラとなった
- 数値リテラルのアンダースコア、ラベル付き引数、case 範囲、ネスト関数、ジェネレーターなどは、C/C++ 標準にずっと後になって取り入れられたか、今なお標準に存在しない機能である
- ネスト関数は、関数ポインタとコンテキストポインタを一緒に渡す 非エスケープクロージャ 形式の「full function value」を提供し、通常の C の関数ポインタより表現力が高かった
- ジェネレーターはネスト関数の上に載る糖衣構文として実装されており、呼び出し側の
for ループ本体をネスト関数に変換して yield 引数として渡す単純な構造で動作する
FM TOWNS と High C の位置づけ
- FM TOWNS 関連の本の山から出てきた 1980 年代の C コンパイラのマニュアルには、予想以上に豊富な言語拡張が含まれていた
- C とその系統の言語を実運用環境で使うには、長い間 ベンダー拡張 が必要だった
- 今日の GCC、Clang、MSVC 中心の環境では、拡張はプラットフォーム固有の処理や低レベルの細かな制御に集中する傾向がある
- 1980 年代には、より小規模で数多くの企業が採用競争を繰り広げていたため、拡張機能ももっと多様だった
- Phar Lap は、16ビット MS-DOS 環境で 32ビット 80386 プロセッサを活用できるようにする初期の DOS extender の 1 つを作った
- MetaWare は Phar Lap の依頼で、High C Compiler を Phar Lap の DOS extender SDK に移植した
- Fujitsu は 803386 ベースの FM TOWNS プラットフォーム OS に Phar Lap の DOS extender を統合し、High C はこのプラットフォームの 1st-party C コンパイラとなった
- FM TOWNS は、最初の ANSI C 標準である C89 が批准される直前の 1989 年に発売された
標準より先を行っていた小さな利便機能
-
数値リテラルのアンダースコア区切り
- 長い数値リテラルを読みやすくするため、数値の中に アンダースコア区切り を入れられた
- C++ は C++14 で
1'000'000 のようなシングルクォート区切りを導入した
- C は C23 になってようやく類似機能を導入した
-
ラベル付き引数
- 引数が多かったり、
bool のように呼び出し箇所で意味が伝わりにくい型を多用する関数で、引数名を付けられる
- High C のラベル付き引数は Python の人気機能に似た形で動作する
- 引数ラベルは任意である
- ラベルがある場合、
argumentName => value 構文で引数を任意の順序で指定できる
- ラベルなしの引数とラベル付きの引数を混在させられるが、関数のすべての仮引数には対応する実引数が必要である
- 標準 C と C++ には、今もこの機能はない
-
case 範囲
- Pascal の
case low..high のように、値の範囲を一度にマッチさせる機能を提供する
- 標準 C と C++ はこの機能を採用していない
ネスト関数と full function value
- High C では、Pascal のように関数の中に ネスト関数 を宣言できる
- 実装方式は、標準 Pascal や GCC のネスト関数拡張よりも、より完全な形に近い
- High C はネスト関数の宣言だけでなく、full function value 型も宣言できる
- 伝統的な C の関数ポインタと異なり、関数ポインタにコンテキストポインタを一緒に持たせる
- これにより、ネスト関数がキャプチャしたコンテキストを再び参照できる
- 外側の関数が返った後まで寿命が続くわけではない、非エスケープクロージャである
- GCC のネスト関数拡張は、ネスト関数を通常の関数ポインタとして参照させようとして、呼び出しスタック上に実行可能コードを書き込み、コンテキストポインタを thunk していた
- この方式はセキュリティ上の大きなリスクにつながり、多くのプラットフォームでこの機能が完全に無効化されることになった
- High C のローカル関数参照は 1級の値のように使えるが、外側の関数が返った後まで寿命が延長されることはない
- ネスト関数は親関数へ
goto することもできる
- Smalltalk のブロックのように、ネスト関数の外へ抜け出す 非局所脱出 が可能である
- これを利用して、制御フローのように振る舞う関数を作れる
- Objective-C は 2009 年に escaping closure として使える blocks を得て、C++ は 2011 年に lambdas を導入した
- どちらの機能も非局所脱出の能力は持っていなかった
- 標準 C には、いまだに正式なネスト関数機能はない
ジェネレーターコルーチン
- MetaWare は 1 章まるごと割くほど ジェネレーター 機能を強調している
- High C は 1989 年に plain C で Python スタイルのジェネレーターコルーチンをサポートしていた
- ジェネレーター関数は
void foo(Arg arguments) -> (Yield yields) 構文で宣言する
- 関数内部で魔法の関数
yield(values...) を複数回呼び出し、値のシーケンスを生成できる
- 呼び出し側は
for variable... <- foo(arguments...) do { ... } という新しい for ループ構文で、生成された値を順に反復処理する
- この実装はネスト関数と複雑に組み合わせられる
- ジェネレーター内のネスト関数は、外側のジェネレーターの
yield 動作をキャプチャできる
- ネスト関数が自分自身を再帰呼び出しして、木構造や再帰的データ構造を巡回しながら各段階で
yield できる
- このような形は、Python や多くの主流ジェネレーターコルーチン言語では実装しにくい方式に見える
ジェネレーターの実装方式と標準言語との違い
- High C のジェネレーターは、高度なランタイムなしに ネスト関数上の糖衣構文 として動作する
void foo(Arg arguments) -> (Yield yields) という形のジェネレーター宣言は、通常の関数宣言 void foo(void yield(Yield yields)!, Arg arguments) と等価である
yield は「full function value」型の暗黙の仮引数である
- ジェネレーター本体内の
yield(values) 呼び出しは、この暗黙の関数仮引数を呼び出す通常の関数呼び出しである
- 呼び出し側の
for ループ本体はネスト関数に変換される
- このネスト関数がジェネレーターの
yield 引数として渡される
- 構造は単純だが効果的である
- ネスト関数が非局所脱出をサポートするため、
for ループ本体の外へ出る break、continue、goto も、適切なループ外の位置へ goto する形で動作する
- 標準 C がこのような機能を統合しようとする可能性は低い
- C++20 は、コンパイル時のコルーチン変換に基づく非常に柔軟で複雑な コルーチン 機能を提供する
- これを使ってジェネレーターを実装することはできそうである
- ただし、その結果はローカル関数とこのように直感的には組み合わさらないだろう
1件のコメント
Hacker News のコメント
運よく英語版の High C/C++ Language Reference を1冊持っている
http://jdebp.uk./FGA/metaware-iterator-driven-for.html
http://jdebp.uk./Proposals/metaware-iterator-driven-for.html
breakやreturnがどのようにコンパイルされるのか気になる。yield関数が ステータスコード を返すように変換され、呼び出し側で検査される方式だったのだろうか?case範囲、名前付き引数、ネスト関数、静的ネスト関数、ジェネレータに似た機能例えば
int a = 1_234_567;、case 5 .. case 6:、test(b:3, a:4);のような形が可能静的ネスト関数は外側の関数のフレーム変数にアクセスできないため、
Error: static function test.foo.plus cannot access variable i in frame of function test.fooのようなエラーになるジェネレータに似た機能は https://dlang.org/spec/statement.html#foreach_over_struct_an... にある
例えばインメモリキャッシュサービスを作るなら、キャッシュ項目自体はガベージコレクタに追跡させない方がよい。ガベージコレクタは実際のアクセスパターンを知らないことが多く、邪魔になるからだ。しかし、そのサービスの他の構成要素の大半は、ガベージコレクタがある方がより適している
名前付き引数はなぜ
test(a:4, b:3)という形で、test(.a=4, b.=3);ではないのか?C でファーストクラスの型をどう扱えるのかも気になる
lcc-winC コンパイラは 演算子オーバーロード、デフォルト関数引数、関数オーバーロードを追加していた。ドキュメントでは “generic functions” を見るとよい [1]Plan 9 C コンパイラも複数の言語拡張を導入し、そのうち匿名構造体/共用体のような一部は後に C 標準に入った。現在の GCC は
-fplan9-extensionsフラグを受け付けており [2]、関数呼び出しや代入で構造体ポインタを匿名フィールドへ自動変換するなど、かなり有用な機能を有効にできる[1] https://lcc-win32.services.net/C-Tutorial.pdf
[2] https://gcc.gnu.org/onlinedocs/gcc/Unnamed-Fields.html
世の中に広く普及して言語標準に影響を与えられなかったのが惜しい。そんな昔にこうした機能があったことに驚く
Hacker News でも以前取り上げられていた: https://news.ycombinator.com/item?id=38938402
どこかに PDF のコピーはあるだろうか?
yieldを備えた for ループ を持っていた [0]。同時期の Icon 言語にも似たジェネレータ機能があり [1]、yieldをsuspendと呼んでいた。Ada(1983)にもこうした機能があったと理解しているこうした言語機能が完全に知られていなかったわけではない
[0] https://publications.csail.mit.edu/lcs/pubs/pdf/MIT-LCS-TR-2...
[1] https://dl.acm.org/doi/pdf/10.1145/800055.802034
数値内のアンダースコア、
case範囲、名前付きパラメータ、ネスト関数、完全な関数変数まで説明しているhttps://bitsavers.org/pdf/metaware/…
ファイル末尾から50ページほど手前の Appendix A を見ればよい
昔コードを学んで書いていた頃には、少し怪しげなサイトを通じて知った
システムプログラミング言語の豊かな歴史も見えてくる。C と Go の設計が、他のエコシステムで進んでいたことや過去の経験を無視したという点でどれほど似ているかも分かる
コンパイラのマニュアルへのリンクは https://winworldpc.com/product/metaware-high-c-cpp/33x にある
C マニュアル PDF には 2007 年の著作権表記がある
https://f.duriansoftware.com/@joe/113195961485703110
\nではなく¥nで終わっている理由が気になるなら、これらのコード例は Shift-JIS で書かれているようだ。Shift-JIS では ASCII の\の位置に¥が入る[0] https://en.wikipedia.org/wiki/JIS_X_0201
この用途には EUC-JP のほうがよい。そうした問題がないからだ。Pascal で
(* *)コメントを使い、{ }コメントを使わないなら、Shift-JIS を使ってもこの問題は起きない代わりに、Shift-JIS の基になった JIS X 0201(https://en.m.wikipedia.org.org/wiki/JIS_X_0201)が使われていた可能性が高い
C:\ではなくC:¥だったCall (Param_A => 1, Param_B => "Foo");という形のラベル、任意基数の数値におけるアンダースコア(X : Integer := 1_000;)、ネストしたサブプログラム、範囲ベースの検査があるC が当時の他の多くの言語と比べて信じられないほど原始的だったことは、よく忘れられている気がする
日本語表記法やカーニングの規則を十分に知っているわけではないが、漢字とラテン文字の両方を含むプロポーショナルフォントを持ってきて、等幅のマス目に無理やり押し込んだように見える
いずれにせよ、コード例が手元の多くの本のような 8pt フォントではないのはよい
try_fold()を思い出す(https://scribe.rip/@veedrac/rust-is-slow-and-i-am-the-cure-3...)しかしまさにその理由で、こうした拡張は比較的知られず、数十年後に現代の C/C++ で再発見され、再発明されなければならなかったようだ