世界を動かす技術を、日本語で。

C3を学ぶ

概要

  • C3言語 の学習体験記録
  • C3の特徴 とCとの比較
  • 主要な構文 (Hello World、ループ、enum、deferなど)の紹介
  • インストールから簡単なプロジェクト作成 までの流れ
  • リアルタイムでの学習記録 による率直な感想と気付き

C3言語学習体験記

  • C3 は、既存の C言語 を基盤としつつ、より 高い生産性新機能 を提供するプログラミング言語
  • 低レベルシステム言語 に親しんできた筆者が、 新しい言語への好奇心 でC3を学習
  • リアルタイム執筆 のため、他記事では省略されがちな 細かな気付き課題点 を記録
  • 言語ごとに異なる パラダイムアイデア表現方法 の違いを体験
  • C3でどんな プロジェクト が向いているかを模索

C3とは

  • C3公式サイト によれば、C3は「Cの上にCで構築」し、 エルゴノミクス最適化新機能 を提供
  • 主な特徴
    • モジュールシステム
    • 演算子オーバーロード
    • ジェネリクス
    • コンパイル時実行
    • セマンティックマクロ
    • 組み込みビルドシステム
    • エラー処理
    • defer、値メソッド、enumの拡張
    • サブタイプ、コントラクト、スライス、foreach、動的型 など

言語の概要

  • C3リファレンス を見ながら、主要な機能や構文をピックアップ
  • C言語 に似ている部分も多いが、 新しい表現力使いやすさ が強化

Hello World

  • サンプルコード
    import std::io;
    fn void main() {
      io::printn("Hello, World!");
    }
    
  • C言語 に近いが、関数定義に fnキーワード を明示的に使用
  • モジュールのimport はサブモジュールまで再帰的にインポート
  • 名前衝突abc::Context のように 名前空間 で解決
  • print関数 は多くの型を自動表示、 デバッグ に便利
  • フォーマット指定子 はCとほぼ同じだが、enumも文字列化される

foreachループ

  • サンプル
    fn void example_foreach(float[] values) {
      foreach (index, value : values) {
        io::printfn("%d: %f", index, value);
      }
    }
    
  • foreach構文 を標準搭載、参照渡しも可能(&value
  • break/continue も期待通り動作
  • forより高レベル な意図表現、ロジックエラー減少

whileループ

  • サンプル
    int a = 10;
    while (a > 0) {
      a--;
    }
    while (Point* p = getPoint()) {
      // ...
    }
    
  • C99以降 のforループのように、while条件内で変数宣言が可能

enum型とswitch文

  • サンプル
    enum Height : uint { LOW, MEDIUM, HIGH, }
    fn void demo_enum(Height h) {
      switch (h) {
        case LOW:
        case MEDIUM:
          io::printn("Not high");
        case HIGH:
          io::printn("High");
      }
    }
    
  • 暗黙的なbreak が特徴、nextcaseで明示的フォールスルー
  • 全パターン網羅時はdefault不要、コンパイラが警告
  • Duff’s Device のようなジャンプテーブル的な使い方も可能
  • @jump属性で 最適化コンパイラ によるジャンプテーブル化を強制

deferキーワード

  • サンプル
    fn void test(int x) {
      defer io::printn();
      defer io::print("A");
      if (x == 1) return;
      {
        defer io::print("B");
        if (x == 0) return;
      }
      io::print("!");
    }
    
  • スコープ終了時 に逆順でdefer文が実行
  • リソース解放クリーンアップ に非常に便利
  • Cのgoto cleanup よりも直感的・安全
  • defer catchdefer tryエラー時の自動処理 も可能

struct型

  • サンプル
    struct MyData {
      char* name;
      Callback open;
      Callback close;
      Status status;
      struct other { int value; int status; }
      struct { int value; int status; }
      union { Person* person; Company* company; }
      union either { int this; bool or; char* that; }
    }
    
  • サブ構造体匿名/名前付きunion を柔軟に宣言
  • タグ付きユニオン もenumとanonymous unionで簡単実装
  • Cでも可能 だが、C3はより明示的・安全な表現

エラー処理

  • サンプル
    int? a = 1; // オプショナル型
    int? b = io::FILE_NOT_FOUND?;
    faultdef OOPS, LOTS_OF_OOPS, USER_ERROR;
    fn int? get_value();
    if (catch excuse = get_value()) // エラー時処理
    int foo = maybe_function()!; // エラーなら即リターン
    
  • オプショナル型?)と Excuse型 でエラーを明示管理
  • catch構文 でエラー検知とハンドリング
  • !演算子 で例外発生時の即リターン
  • 表現力の高いエラー処理、Cよりも安全で直感的

次のステップ:C3の環境構築と簡単なプロジェクト

  • C3のインストール 方法紹介
  • 新規プロジェクト作成 手順
  • C3で電卓アプリ を作成する流れ
    • 必要な機能の整理
    • ユーザー入力取得
    • トークナイザー作成
    • パーサー作成
  • C3を使った開発の実際的な流れ を体験

まとめ

  • C3言語 はCに親しんだ開発者にとって 移行しやすく、かつ 現代的な機能 を多数提供
  • リアルタイム学習 だからこその率直な感想や細かな気付き
  • 新しい言語パラダイム の発見と、それが 自分のプロジェクト にどう生きるかの模索
  • C3の特徴 を活かした開発事例や、 他言語との比較 も今後の課題

Hackerたちの意見

C3は期待できそうだけど、nullをサポートする言語にはnull制限型が必要だよね。あの契約コメントみたいなのじゃなくて。もし全てにnullチェックをしなきゃいけないなら、Javaを書けばいいし…実際、Javaもこれを修正しようとしてるみたいだよね。https://openjdk.org/jeps/8303099

こういう関数契約についてはちょっと迷ってる。別の言語で10年見てきたけど、実際には使ったことがないから、どう思うかは言えないな。でも、コメントの中にあるのはちょっと変だよね。

面白い問題だね。最初は``と&の構文を両方試してみたんだ。int&が参照(非null)で、intがポインタって感じ。そこで気づくのは2つのことだね。1. ほとんどのポインタパラメータは非nullにしたい。2. 非null変数をコンストラクタなしで言語に組み込むのはすごく難しい。ZIIのようにコンストラクタやデストラクタを避けるアプローチは、参照値ともうまくいかないし。結局、値が準有効な状態になる期間ができちゃうんだ。非null型は割り当てが必要で、最初に割り当てられる前は壊れた状態だからね。C3で「型安全」な非null型を作ることは確かに可能だけど、それは言語に組み込まれてはいないんだ。

Nimは、2つの明示的で制限されたnullable型、ポインタと参照だけを持つことでこの問題を解決してる。ポインタは手動で管理され、参照は自動で管理される。どちらも最初はnilから始まり、参照されるオブジェクトは手動でインスタンス化しなきゃいけない。言語全体は、スタック値とスタック管理された隠れたユニークポインタを使った値渡しで構築されてる。CやC++ライブラリにインターフェースを作る以外では、refやポインタを使う必要はほとんどない。40,000行のプロダクションアプリを、参照やポインタ型なしで書いたことがある。必要なケースは、可変値として複合型や動的コンテナを渡すことでほぼカバーされる。ポインタや参照のセマンティクスを行うことは不可能だから、ライフタイムはすでに管理されてるし、セマンティクス的にはただの値なんだ。

なんで問題を解決する方法が一つしかないの?

C3に関する他の興味深いリンクもいくつかあるよ:インタビュー: - https://www.youtube.com/watch?v=UC8VDRJqXfc - https://www.youtube.com/watch?v=9rS8MVZH-vA いろんなタスクをC3でやってるシリーズ: - https://ebn.codeberg.page/programming/c3/c3-file-io/ プロジェクト: - ゲームボーイエミュレーター https://github.com/OdnetninI/Gameboy-Emulator/ - RISCV Bare metal Hello World: https://www.youtube.com/watch?v=0iAJxx6Ok4E - "Depths of Daemonheim"ローグライク https://github.com/TechnicalFowl/7DRL-2025 TsodingのC3の「初印象」ストリーム: - https://www.youtube.com/watch?v=Qzw1m7PweXs

C3とHareの両方を試した人いる?どうだった?二つの間にはかなりの重複があるみたいだね。[1] https://harelang.org/

Hareの問題は、(少なくとも最後にチェックしたときは)Linux/Unix専用で、そういう設計になってることだね。これだと多くの人にとっては致命的だよね。

Cの代替としてZigもあるよ。https://ziglang.org/

面白いことにC2もあるよ:http://c2lang.org

C4もあるけど、あれは爆発物かソフトウェアアーキテクチャをモデル化するための表記言語だよ。

そうそう、C3はC2のバリアントとして始まったんだよね。

ちょっとした指摘なんだけど、ページの下の方にケース構文についてのことが書いてあるね。「空のブレークはダメ」っていうのはいい選択だけど、同じことをする2つのケースがあると、ケース X: case Y: みたいな構文は危険な罠になりそう。C3の著者には、ケースをこういう風にスタックさせることを強く勧めたいな:case X, Y:

case 3,4: は3または4の値のため、case (3,4): は値が[3,4]の配列のためっていうのを区別するのは微妙だね。

「case X, Y」は3〜4個の値には使えるけど、もっと長くなると問題が増えてくるね。例えば、case SOME_BAD_THING, SOME_OTHER_CONDITION, HERE_IS_NUMBER_THREE: foo(); int y = baz(); みたいに次の行に置くのは読みづらい。case SOME_BAD_THING, SOME_OTHER_CONDITION, HERE_IS_NUMBER_THREE, AND_NUMBER_FOUR, AND_NUMBER_FIVE, AND_THE_LAST_ONE: foo(); int y = baz(); みたいに10個以上のフォールスルーがあるリストになることもよくある。特にenumのデフォルトより完全なスイッチを好むから。case SOME_BAD_THING: case SOME_OTHER_CONDITION: case HERE_IS_NUMBER_THREE: case AND_NUMBER_FOUR: case AND_NUMBER_FIVE: case AND_THE_LAST_ONE: foo(); int y = baz(); こういう感じ。確かに「case X, Y:」を使いたい気持ちはわかるし、長い間考えたけど、可読性が欠けてて無理だと思った。妥協案としては、case SOME_BAD_THING, case SOME_OTHER_CONDITION, case HERE_IS_NUMBER_THREE, case AND_NUMBER_FOUR, case AND_NUMBER_FIVE, case AND_THE_LAST_ONE: foo(); int y = baz(); みたいにする手もあったけど、一貫性がないにしてもCの構文に従った方がわかりやすいと感じた。

うーん、微妙な感じ。deferは、言語やフレームワークに適切な機能がなかったら急いでコードで作り上げるようなものだけど、Pythonのwith文やJavaの自動リソース管理の方がいいと思う。あと、OptionalやEitherとか、そういうのはもういい加減にした方がいいと思う。ちゃんと使うのはすごく手間がかかるから。私のCの初体験は1985年頃で、Byte誌からCP/M用のターミナルエミュレーターをTRS-80 Color ComputerのOS-9に移植してたんだけど、幸せなパスでの10行のコードが、エラーハンドリングが絡んで50行に膨れ上がるのを見てかなりトラウマになった。95年にJavaを見たときは、デフォルトの不幸なパスがcatch {}で修正できて、finally {}で強化できるのを見てすごく嬉しかった。例外がクールじゃないって考えるのはいいけど、その唯一の理由はデバッグのためにスタックトレースを埋めるのが面倒だからだと思う。90年代には、例外はC++の仕様の中で実際には機能しなかった多くのものの一つだったしね。エラーハンドリングには難しい問題があるけど、エラーはカプセル化のアイデアを無視するし、そういうのは言語やフレームワークでもあまり取り上げられないんだよね。?やOptional、Eitherを入れるのは、タイタニックのデッキチェアを動かしてるだけだと思う。[1] 私は変わってるのは自覚してる。物事が整然としてるときは嬉しくなるけど、Dockerがlibcの5バージョン、Javaの7バージョン、あるライブラリの15バージョンを動かせるって見ると、もっと多くの人が嬉しがってるみたい。[2] 「現実の砂漠」が「物事があるべき姿」に侵入する場所だね。

C3のエラーハンドリングはかなり新しい試みだよ。コンポーザビリティ、明示性、Cとの互換性の間の絶妙なバランスを探してる。try-catchはいいコンポーザビリティを持ってる:try { int x = foo_may_fail(); int y = bar_may_fail(x); } catch (... ) { ... } 通常のResult型はこれにflatmapを使う必要があるし、エラーコードや複数の戻り値もこれに苦しむことが多い。C3では:int? x = foo_may_fail(); int? y = bar_may_fail(x); if (catch err = y) { ... return; } // ここでyは暗黙的に「int」にアンラップされる。これがあなたを満足させるとは言わないけど、OptionalやEitherを超えた新しいアプローチで、try-catchと多くの共通点があることを示したかっただけだよ。

この比較に基づくと:https://c3-lang.org/faq/compare-languages/ C/C++の代替または進化した言語として最適なのはDだと言えるね。Dには独自のクロスプラットフォームGUIライブラリとIDEもあるし、Dが広く採用されていない理由が気になる。

自分の意見しか言えないけど:1. すごく大きい。2. まだGCに大きく依存してる(実際にはそれほど重要じゃないけど)。機能を追加し続けてるけど、機能を追加することが言語を使う価値に繋がるわけじゃない。実際、C++の一番魅力がない部分でもある。だから私の推測は:1. GCに賭けてC++と競争しようとしたのが間違いだった。2. tractionを得られず、機能を追加し続けた – まるでその機能が言語のキラーフィーチャーになるかのように。3. 追加された機能が実際には魅力を減らしていることを理解していなかった。4. C++はその後GCトラックから完全に離れ、より低レベルな代替品になった。その時点でDは変な位置に置かれた:高レベルな代替品としては十分高くなく、C++と競争するには十分低くもない。5. 最後に、長い間存在していて全然伸びなかったことが、逆に「過去のもの」と見なされるようになって、さらに立ち上がるのが難しくなってる。もしかしたら、ウォルター・ブライトはDのベスト機能だけを集めたキュレーション版を作るべきかも。でも、言語と成熟したstdlibを作るのにどれだけ時間がかかるかを考えると、それは言うは易し行うは難しだね。

C3にはシンプルなRAII/オブジェクト/クラスが組み込まれてたらいいな(継承不要、ポリモーフィズムもいらない、Cの構造体よりはマシなカプセル化だけで十分)。そうすれば、もっとパワフルなCになって、C++よりもずっとシンプルになる。両者の中間にある理想的な言語で、C/C++の90%のユースケースに対応できると思う。

構文が変わって、関数や型の定義を検索/grepしやすくなればいいなと思う。Odinはそれがすごく良くて、「::」で検索できるんだ。戻り値の型を閉じカッコの後に移動させるだけで十分かも? あと2つの願いがある:名前付きパラメータと構造化された並行性を追加してくれたら、すごくクールな言語になると思う。

Cからの最小限の変更だったよ。型を正規表現で抽出するのは簡単だから、Odinほど良くはないけど、わかりやすいはず。名前付きパラメータはすでに言語に含まれてるし、並行性については特定のモデルを選びたくない。ユーザーランドの追加用にどんなフックが作れるか見てみるけど、言語自体は並行性については意見を持たないつもり。

C4を作ったら全てが爆発するのかな?

ユーモアでアップボートしたよ。