1 ポイント 投稿者 GN⁺ 2024-09-27 | 1件のコメント | WhatsAppで共有
  • 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 ループ本体の外へ出る breakcontinuegoto も、適切なループ外の位置へ goto する形で動作する
  • 標準 C がこのような機能を統合しようとする可能性は低い
  • C++20 は、コンパイル時のコルーチン変換に基づく非常に柔軟で複雑な コルーチン 機能を提供する
    • これを使ってジェネレーターを実装することはできそうである
    • ただし、その結果はローカル関数とこのように直感的には組み合わさらないだろう

1件のコメント

 
GN⁺ 2024-09-27
Hacker News のコメント
  • 2011年に イテレータ駆動の for についてまとめたことがある。すでにかなり前に忘れられていた機能の一つで、当時 C++ 標準に入るとしたらどのような形になるかも併せて扱っていた
    運よく英語版の High C/C++ Language Reference を1冊持っている
    http://jdebp.uk./FGA/metaware-iterator-driven-for.html
    http://jdebp.uk./Proposals/metaware-iterator-driven-for.html
    • breakreturn がどのようにコンパイルされるのか気になる。yield 関数が ステータスコード を返すように変換され、呼び出し側で検査される方式だったのだろうか?
    • あれは逆さの笑顔を意図したものなのか?
  • D には、Das BetterC にも、こうした機能がある:数値リテラルのアンダースコア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... にある
    • この記事を読んでいる間ずっと D が頭に浮かんだ。コメント欄に Walter Bright が現れそうだと思った
    • D の ガベージコレクタ も本当に良い機能だと思う。低レベルコードでは手動メモリ管理が必要な場合もあるが、実際にはあまり問題にならず、ガベージコレクタが作業をずっと楽にしてくれる部分も多い
      例えばインメモリキャッシュサービスを作るなら、キャッシュ項目自体はガベージコレクタに追跡させない方がよい。ガベージコレクタは実際のアクセスパターンを知らないことが多く、邪魔になるからだ。しかし、そのサービスの他の構成要素の大半は、ガベージコレクタがある方がより適している
    • 質問がある。なぜ人々は C における ネスト関数 という概念を嫌うのか知っているか?
      名前付き引数はなぜ test(a:4, b:3) という形で、test(.a=4, b.=3); ではないのか?
      C でファーストクラスの型をどう扱えるのかも気になる
  • 関連して、lcc-win C コンパイラは 演算子オーバーロード、デフォルト関数引数、関数オーバーロードを追加していた。ドキュメントでは “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 のコピーはあるだろうか?
    • CLU は1970年代半ばから後半にはすでにイテレータ、つまりジェネレータと yield を備えた for ループ を持っていた [0]。同時期の Icon 言語にも似たジェネレータ機能があり [1]、yieldsuspend と呼んでいた。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
    • Bitsavers に HC 1.2 リファレンスマニュアル(1985) のコピーがある
      数値内のアンダースコア、case 範囲、名前付きパラメータ、ネスト関数、完全な関数変数まで説明している
      https://bitsavers.org/pdf/metaware/…
      ファイル末尾から50ページほど手前の Appendix A を見ればよい
    • MetaWare は80〜90年代に Santa Cruz にあった、多くの製品を出していたコンパイラ会社だった。彼らの作ったものが好きだったし、文化もかなり興味深かった
      昔コードを学んで書いていた頃には、少し怪しげなサイトを通じて知った
    • それほど驚くことではない。FORTRAN、Lisp、ALGOL、COBOL 以後の 高水準プログラミング言語 のアーカイブを掘ると、こうした言語アイデアはたくさん見つかる
      システムプログラミング言語の豊かな歴史も見えてくる。C と Go の設計が、他のエコシステムで進んでいたことや過去の経験を無視したという点でどれほど似ているかも分かる
    • こうした機能が、ほとんどのプログラミング言語が提供する標準機能一覧の一部ではなく、新機能 のように見えてしまうのは残念だ

コンパイラのマニュアルへのリンクは https://winworldpc.com/product/metaware-high-c-cpp/33x にある
C マニュアル PDF には 2007 年の著作権表記がある

  • 以前の投稿とコメントはこちら: https://news.ycombinator.com/item?id=38938402
    • 今日 Joe Groff が FediVerse でこれを再び取り上げたので、ここにもまた上がってきたようだ
      https://f.duriansoftware.com/@joe/113195961485703110
    • 著者が昨日、同じ内容を別の URL に再投稿したようだ。妙な感じがする
  • 図中の例の文字列リテラルが \n ではなく ¥n で終わっている理由が気になるなら、これらのコード例は Shift-JIS で書かれているようだ。Shift-JIS では ASCII の \ の位置に ¥ が入る
    • もともとは 1969 年の日本語 ASCII 変種である JIS Roman [0] だった。Shift-JIS はずっと後になって、ダブルバイト文字集合のサポートを追加したもの
      [0] https://en.wikipedia.org/wiki/JIS_X_0201
    • 問題は、Shift-JIS ではバックスラッシュの ASCII コードが 2 バイト文字の 2 バイト目としても使われる点だ。そのため C では日本語の文字列リテラルが正しく動作しないことがある
      この用途には EUC-JP のほうがよい。そうした問題がないからだ。Pascal で (* *) コメントを使い、{ } コメントを使わないなら、Shift-JIS を使ってもこの問題は起きない
    • 著者はこの本がいつ出たのか情報を示しておらず、探しても情報がない。しかし本が出た時点では Shift-JIS 標準はまだ存在していなかったように思える
      代わりに、Shift-JIS の基になった JIS X 0201(https://en.m.wikipedia.org.org/wiki/JIS_X_0201)が使われていた可能性が高い
    • 同様に、日本語 DOS のプロンプトは C:\ ではなく C:¥ だった
  • これらの拡張は Ada の機能だ。Ada には Call (Param_A => 1, Param_B => "Foo"); という形のラベル、任意基数の数値におけるアンダースコア(X : Integer := 1_000;)、ネストしたサブプログラム、範囲ベースの検査がある
    • 記事でも述べているように、Pascal は Ada より前からこうした機能を持っていたし、エントリポイントを持つタスク型は実質的に ジェネレータ と見なせる
      C が当時の他の多くの言語と比べて信じられないほど原始的だったことは、よく忘れられている気がする
  • 内容とは別に、この本の タイポグラフィ が興味深い。美しくもあり、ひどくもある感じが同時にある
    日本語表記法やカーニングの規則を十分に知っているわけではないが、漢字とラテン文字の両方を含むプロポーショナルフォントを持ってきて、等幅のマス目に無理やり押し込んだように見える
    いずれにせよ、コード例が手元の多くの本のような 8pt フォントではないのはよい
  • ジェネレータを見ると、Rust の 内部/外部イテレーション 問題と try_fold() を思い出す(https://scribe.rip/@veedrac/rust-is-slow-and-i-am-the-cure-3...
  • 特にジェネレータを見ると、時代をかなり先取りしていたように思える。Fujitsu が長い 標準化手続き を気にしなかったから、単に実装できたのかもしれない
    しかしまさにその理由で、こうした拡張は比較的知られず、数十年後に現代の C/C++ で再発見され、再発明されなければならなかったようだ
    • Fujitsu ではなく MetaWare だった。MetaWare はコンパイラの経験がかなりある会社で、同じ時期にかなりよく知られた Pascal コンパイラも持っていた。Pascal にはすでに ネストした関数 があった
    • C は、2 の補数でさえ標準に入れるべきではないと固執した人々に支配されていなければ、はるかによい言語になれたはずだ
    • コルーチンとジェネレータは当時すでによく理解されていた概念だった。Icon を見ればよい。だから本当に主な理由は、標準化の負担 を心配しなくてよかった点に近いように思える