2回目のテーマ月、Mechanical Marchへようこそ。今月は、マシンコードにコンパイルされる言語、つまりシステム言語に焦点を当てます。
この記事は、Mechanical Marchの動画を一部記事として、一部文字起こしとしてまとめたものです。 まず私が今月の概要を簡単に説明し、そのあとでシステム言語について少し見ていきます。歴史の中でどのように進化してきたのか、マシンコードにコンパイルすることの長所と短所、そして今月の注目言語をそれぞれ簡単に紹介します。 今回もErikが参加してくれて、後半はほとんどErikが話してくれます。まずは私から、いくつかの実用的な情報をお伝えします。
まずは今月の注目言語です。C、C++、D、Go、Nim、Rust、V、Zigです。Mechanical Marchのバッジを獲得するには、これらの言語のいずれかで演習を5つ完了する必要があります。GoトラックのシラバスはExercismの中でも特に優れたものの一つなので、ぜひ試してみることをおすすめします。ExercismではNimのファンも多く、比較的シンプルで始めやすく、書くのもとても簡単なので、こちらもぜひ試してみてください。
試してほしい注目の演習も5つあります:
- Linked-listまたはsimple-linked-list(言語によって異なります):メモリの確保と解放、ポインター
- secret-handshake:ビット演算
- pangram:
forループ、文字列と文字 - sieve:配列、
forループ - binary-search:配列、ループ
数日前のアップデート動画で新しいバッジを発表しました。テーマ言語で注目の演習5つを完了すると獲得できるバッジです。このバッジを獲得するには、今年中にいずれかのシステム言語でこれらの演習をすべて完了する必要があります。
ほかにも楽しい企画を続々と準備しています。Goコアチームの数人へのインタビュー、うまくいけばRustやそのほかの言語の方にもお話を聞きたいと思っています。今月はライブ配信もたくさん予定しています。Mechanical Marchのグッズもまもなく登場します!
それでは、技術的な面をもう少し掘り下げていきましょう。
これらの言語は何に使われているのでしょうか?
Exercismのあちこちで使っています。CLIはGoで書かれていて、トラックを管理する社内ツールのconfigletはNim、解答のコード行数を数える主要なライブラリはRustで書かれています。Erik、なぜこれらのツールにこれらの言語を選んだのでしょうか?
CLI:
- Goを選んだのは、Katrinaが一番慣れていた言語だったからだと思います。
- Goはこうした小さめのコマンドラインツールに最適です。
- Goのコードは比較的わかりやすく、コントリビュートしやすくなります。
- Goのバイナリはランタイムを必要としないため、簡単にデプロイできます。
- Goはクロスコンパイルが得意です。
Nim
- NimにはGoとほぼ同じ利点があります。
- CLIをメンテナンスできるほどGoに詳しい人があまりいなかったので、Nimに切り替えました。
これらの言語はほかにどのような場所で使われていると思いますか?
- パフォーマンスが重要なあらゆる場所(ドライバー、ゲーム、オペレーティングシステム、ビルドシステム・コンパイラー)
- リソースが限られている場所(例:組み込みソフトウェア)
- 高い移植性が求められるもの、つまり多くの異なるプラットフォームで動作する必要があるもの
マシンコードとは何でしょうか?
先ほども言いましたが、Mechanical Marchで取り上げるこれらの言語は、マシンコードにコンパイルされるという点で特徴づけられます。マシンコードとは何か、そして対照的にバイトコードとは何かを少し説明してもらえますか?
- マシンコードは、マシン上で直接実行できるコードです。
- 対照的にバイトコードは、バイトコードを解釈したりマシンコードにコンパイルしたりする別のコードを必要とします。そのため、バイトコードを実行するには中間ステップが必要です。
バイトコードに対するマシンコードの長所と短所は何でしょうか?
長所:
- 起動が速い(JITコンパイルのステップがない)
- メモリ使用量が少ない(ランタイムを読み込む必要がなく、メモリ上にバイトコードもないので、組み込みシステムに最適)
- 実行するマシンにランタイムをインストールする必要がない(Dockerコンテナーを小さく保つのに重要)
短所:
- 移植性がない。バイトコードは移植性がありますが、コンパイルされたマシンコードはプラットフォーム固有です。
- プロファイル誘導最適化のような高度な最適化ができない(しばらく実行したあとに、(バイト)コードをコンパイルする最適な方法を判断すること)
補足:ハイブリッドなアプローチも可能です。言語がバイトコードにコンパイルし、そのバイトコードを別のツールでマシンコードにコンパイルする方式です。
システムプログラミングの進化
では、今月取り上げる言語のいくつかについて、その進化を少し見ていきましょう。まずはCから始めて、そこからC++がどう進化したかを見ていきます。これらの言語について少し話してもらえますか?
Cは非常に低レベルな言語です。マシンコードのほんの少し上にいるような感覚です。そのため非常に強力で最適化しやすい一方、バグも入りやすい面があります(ヌルポインター例外やバッファオーバーフローなど)。 メモリ管理はすべて手動で、プログラマーの責任になるため、バグやメモリリークにつながる可能性があります。 C++はCにオブジェクト指向プログラミングのサポートを加えたような言語です。今でもかなり低レベルで、メモリ管理を手動で行う必要があります。CとC++はどちらもインラインアセンブリ(ASM)を書くことができます!
新しいシステム言語はどうでしょうか? どのように進化してきたのでしょうか?
現代のシステムプログラミング言語はどれも、参照カウント、ガベージコレクター、その他の仕組みによって自動メモリ管理をサポートしています。
初期のシステムプログラミング言語はどれもヌルポインターをサポートしています(Tony Hoareが自らの「10億ドルの過ち」と呼んだものです)。ヌルポインターは実行時エラーや脆弱性を引き起こすことで悪名高いものです。 現代の言語の多くはnullを廃止しているか、少なくとも使うには手間がかかるようにしています。
もう一つの変化は、デフォルトが可変の値から、デフォルトで不変の値へと移行したことです。たとえばRustとVlangはどちらもデフォルトで値が不変で、可変にしたい場合は明示的に指定する必要があります。
新しい言語はどれもC(またはC++)との相互運用をサポートしています。これらの言語で書かれたコードが非常に多いからです。
もう一つ興味深いのは、新しい言語のいくつかは直接マシンコードにコンパイルするのではなく、別のツールを使ってコンパイルすることです。たとえばRustとZigはLLVMを使い、Nimはさまざまなコンパイラーを使えます。これはトランスパイルと呼ばれます。
マクロやメタプログラミングなどはどうでしょうか?
マクロやメタプログラミングについては、興味深い対照があります。C/C++のマクロは強力ですが、扱いにくいというあまり良くない評判があります。Rust、Nim、Dはどれも強力なメタプログラミングを提供していますが、ずっと扱いやすい形で実現しています。反対に、VLangとZigはマクロがないことを言語の特徴として明確に挙げており、Goはgo generateという別のアプローチを取っています。
システム言語はかなり低レベルだという評判があります。それは今でも当てはまるのでしょうか?
新しい言語はどれも、C/C++よりも高い抽象度で扱えます。 たとえばRust、D、Nimは、非常に機能的な書き方もできます。 NimとDには「純粋」な関数という概念まであります。
今月の言語の紹介
それぞれの言語を順に見ていきましょう。どの言語にも共通点があります。すべて強い静的型付けです。ただ、どこが違うかも見ていきましょう。まずはCから始めましょうか。
C
- Dennis Ritchieが開発
- 最も古い言語の一つで、おそらく世界で最も使われている言語
- UnixやLinuxなど、膨大なソフトウェアがCで書かれている
- 非常に影響力が大きい(Cライクな言語という概念が生まれたことからもわかります)
- 手動のメモリ管理
- 非常に高性能(ハードウェアに近い)
- どこでも動く
- 組み込みシステムに最適
- 言語仕様がかなり小さい
C++
- Bjarne Stroustrupが開発
- Cの後継だが、オブジェクト指向を追加(クラス付きのC)
- オブジェクト指向プログラミングの普及に貢献
- Cよりも高レベルな言語機能が多い
- テンプレートによるジェネリックプログラミングをサポート
- 名前空間によるモジュールサポートを追加
- 多くのゲーム(エンジン)やWindowsの大部分がC++で書かれている
- 手動のメモリ管理
- 今も進化を続け、新しい機能が定期的に追加されている(巨大な仕様)
D
- Walter Brightが開発し、後にAndrei Alexandrescuが加わった
- 当初はC++を作り直したもの(C++の「過ち」から学ぶもの)と考えられていたが、多くの言語から着想を得ている
- マルチパラダイムで、手続き型・オブジェクト指向・機能型プログラミングをサポート
- C/C++との相互運用が簡単
- Uniform Function Call Syntax
- コンパイル時関数評価(例:コンパイル時に正規表現の状態機械を生成)
- 機能型プログラミングと「純粋」な関数をサポート
- 多くの安全性機能
-
@safeによるメモリ安全性 - コントラクト(事前条件・事後条件、不変条件)
- 純粋関数
- 単体テストを重視し、テストは対象のソースコードの隣に置く(Exercismのサイトでは例外扱いが必要でした :))
- Unicode
Rust
- Mozilla Researchの社員Graydon Hoareが開発し、その後Mozillaが正式に採用、現在はRust Foundationの一部
- マルチパラダイムで、オブジェクト指向(ただし考え方が強く、継承がないなど)、手続き型、機能型プログラミングをサポート(Option/Result型、パターンマッチング)
- 多くの新しいツールがRustで書かれている(SWC、Gleam、Linuxカーネルで2番目にサポートされた言語など。Exercismでもコード行数のカウントにRustを使っています)
- 信頼性とパフォーマンスを重視
- LLVM上に構築
- StackOverflowの調査で7年連続で最も愛されている言語
- 高速。コアと標準ライブラリが最小限であることも一因
- 安全。所有権とライフタイムによるメモリ安全性とスレッド安全性を備え、デフォルトで不変
- 強力な型システムで、コンパイル時に多くのバグ(特にメモリ関連)を捕捉。コンパイラーは非常に役立つエラーを出力
- 必要なものがすべて揃っている:コンパイラー、ビルドツール、フォーマッター、パッケージマネージャー、IDE連携
- すばらしいドキュメント(コンパイラーの仕組みについての膨大な資料まであります)
- 移植性が高い:単一の静的バイナリにコンパイルされ、ランタイムのインストールが不要
- Cのコードとの相互運用が簡単
- ゼロコスト抽象化
- 恐れることのない並行処理
- マクロ
Nim
- Andreas Rumpfが開発(当初の名前はNimrod)
- Pythonに着想を得た構文
- マルチパラダイム
- Exercismのconfigletで使用
- 優れたパフォーマンス:オーバーヘッドのないイテレーター、値ベースの型をスタックに割り当てることを優先
- 現代的で表現力豊かな型システム:型推論、タプル、ジェネリクス、直和型、async/await
- ガベージコレクションを備えつつ、決定的なメモリ管理もサポート(複数のメモリ管理方式から選べる)
- コンパイル時のコード実行
- Uniform call syntax
- マクロ:言語を簡単に拡張できる
- エフェクトシステム:副作用を型システムで表現
Go
- GoogleのRobert Griesemer、Rob Pike、Ken Thompsonが開発
- 主に手続き型で、オブジェクト指向に似たアプローチもサポート(ただし継承はなし)
- DockerやKubernetesのような大規模プロジェクトで使用。バックエンドやCLIにも最適(esbuildなど)
- 頭の中に入るほどシンプルであることを目指す(構文が少ない)
- ガベージコレクターによるメモリ安全性
- 高速:コンパイルが速く、テストも実行も速い。ベンチマークを書くためのサポートが組み込まれている!
- 考え方が強い:Goのコードのスタイルに影響を与えるための取り組みが多い。構文が少なく、
go fmtでコードを整形し、慣用的な使い方をチェックするツールがあり、他の言語では警告で済むもの(未使用の変数など)もエラーにし、使うべきGoのイディオムをまとめたドキュメントもある - Goをとても柔軟にする軽量な型システム(型推論をサポート)
- 移植性が高い:単一の静的バイナリにコンパイルされ、ランタイムのインストールが不要。クロスコンパイルも簡単。ゴルーチン(軽量)による並行処理と、チャネルによる通信
- interfaceによる構造的型付け(ダックタイピングに似ているが、静的にチェックされる)
- エラー処理:エラーを確認して対処することを言語が促す
VLang
- Alexander MedvednikovとDelyan Angelovが開発
- Goから着想:
- 「やり方は一つだけ」という同じ方針
- 「構文は最小限」という同じ方針
- コルーチン
- Goとの違い:
- nil/nullがなく、result型を使う
- デフォルトで不変
- 直和型(機能型)
- 文字列補間
- ランタイムやバイナリが小さい
- パターンマッチング
- GCをオプションにする取り組み(autofree)
- ゼロコストのC相互運用
- コードからドキュメントを生成
- メモリ使用量が少なく高速なコンパイラー
Zig
- Andrew Kelleyが開発
- 構文がかなり少ない(500行のPEG文法ファイル)
- コンパイル時のコード実行とリフレクション
- 「明快」であることを目指し、隠れた制御フローやアロケーション、マクロ・メタプログラミングがない
- 手動のメモリ確保
- さまざまなアロケーターをサポート
- メモリを確保する標準ライブラリの関数は、アロケーターを引数に取る
- テストフレームワークがメモリリークを検出できる
- 安全性:
- エラーは値であり、必ず処理しなければならない
- nullがなく、optional型を使う
- テストはソースと同じファイルに書ける(Dと同じ)
- バックエンドとしてLLVMを使用
- C/C++のコードをコンパイルできる
- Cとの相互運用が簡単
- クロスコンパイルが簡単
まとめ
ここで終わりにしましょう。この時点で、みなさんの頭はかなりいっぱいだと思いますので。
今月の注目言語について、役に立ち、楽しい紹介になっていればうれしいです。これらの言語を存分に楽しみながら探求してください。Erikも私も、どの言語を選んだのか、使ってみてどうだったのかをぜひ聞きたいので、コメントやフォーラムに投稿してください!
ご視聴ありがとうございました!