この記事は NTTコムウェア AdventCalendar 2023 5日目の記事です。
自己紹介&動機
高鶴と申します。NTTコムウェア コーポレート革新本部で、プログラム設計~コーディング~ユニットテストにかかわる技術の社内標準化をやっております。
プログラムの静的な解析で早期にバグを発見・修正することで、後工程でのバグ対処コスト削減(ウォーターフォール開発の場合)や、技術的負債の早期解消(アジャイル開発の場合)を目指す、というのが私のチームの仕事の大きな一部となっています。
静的な解析で早期にバグを発見するツールには、オープンソースでも商用でも様々なものがあります。しかし、ソフトウェアの品質をより抜本的に良くしていこうと思うと、「プログラミング言語を何とかする」というところを考えたくなってきます。
Rustであれば、そのような期待に応えてくれるのではないかと期待し、調査・検証を始めました。
Rustとは
Rustとは、「効率的で信頼できるソフトウェアを誰もがつくれる言語」を目指すプログラミング言語です。
2006年にMozillaのグレイドン・ホアレによって発案され、2015年に最初の安定版(1.0)がリリースされた、比較的新しい言語です。
特徴としては、次のようなものが挙げられます。
- 高い性能
- 高速かつメモリ効率よく動作する。
- ガベージコレクタ無しで安全にメモリを管理できるため、
ランタイムのメモリ管理に起因する処理の一時停止の心配がない。
- 相互運用性
- 動作する実行環境の種類が多い。
- Rust自体に言語処理のためのランタイムが必要ないため、他の言語の一部分としてよく調和する。
- 信頼性
- 代数的データ構造を利用できる型システムを持ち、厳格に型チェックが行われる。 データの定義と処理が食い違った場合にコンパイルエラーになってくれるため、整合性を取りながら変更を加えて成長をさせるようなソフトウェアに向いている。
- 所有権モデルにより、リソースを解放する責任の所在が不明瞭なことによって起こるバグを、コンパイル時に排除できる。
- データがスレッド間で共有可能/同期的更新可能かどうかを示す印を型情報の一部として持っており、スレッド安全性にかかわるバグをコンパイル時に排除できる。
- 生産性
- コンパイラのエラーメッセージがわかりやすく、解決案まで示してくれる場合も多い。
- 信頼性の高いライブラリとドキュメントが充実しており、ソフトウェア開発で必要となるものの多くがコミュニティですぐに手に入る。
- 優れたオープンソースソフトウェアに感謝
- 静的な情報(構文木や型)が豊富に使えるため、標準装備されているチェックツール(Clippy)やコード整形ツール(rustfmt)等が優秀である。
Rustの使いどころ
Rustは汎用プログラミング言語なので、「何に使えるのか?」と聞かれると、「他の一般的な言語ができることは何でも」という答えになってしまいます。C言語が入り込めるような場所には、ほぼRustが適用可能です。
- サーバサイド
- オンラインアプリケーション
- バッチアプリケーション
- コマンドラインツール
- クライアントサイド
- ブラウザ (WebAssembly)
- スマホやゲーム機
- デスクトップ
- これまでCやC++が中心だった場所
- 他言語のプログラムの一部として動作するネイティブライブラリ
- OSカーネルに組み込むモジュール
- ファイヤウォールやロードバランサ等のネットワーク機器のプラグイン
- リソースが小さな組み込み機器
その中で、次のようなユースケースがRustにとって有用ではないかと考えています。
- 他の言語に組み込む高性能・高品質なライブラリ
- Python (PyO3), JavaScript (WASM), ...
- リソーススケーリングで頻繁にプロセスを起動・停止するようなサービス
- FaaSのアプリケーション (AWS Lambda, Azure Functionsなど)
- 数秒間の停止がユーザに影響するようなサービス
- 動画や音声の配信、物理的な機器制御など
- Web、スマホ、デスクトップのアプリを高速・低リソースで動かすためにRustに移植する
Rustの難所は学習コスト
Rustの普及における最も大きな壁は、初期の学習コストが高いことです。初心者にとっては、プログラムを動かす以前にコンパイルを通すことすら難しいです。
ただ、思ったようにプログラムを書いてコンパイルを通せるくらいまで学習できれば、そこから先は、あまり考えなくても品質や保守性の高いプログラムを作ることが可能だと感じています。初級者から中・上級者になるハードルはむしろ低いのではないかと思います。
勉強会で見えたつまずきポイント
弊社では、Rustを知らなかったメンバに、Rustlingsという教材を使って学習してもらっています。
もともとC言語でプログラミングをやっていた50代の方、Pythonならよく知っている30代、プログラミングの経験があまりない20代等、数名のメンバで勉強会を行いました。
勉強会メンバには毎週所感を書いてもらっています。どんなところに違和感を感じたり、感動したりしているか、雰囲気が伝わる内容を抜粋してみました。
(間違って理解していると思われる箇所もありますが、あくまで学習中の感想ため、ご容赦を)
- 静的型
- 何でRustの定数は型宣言必須なんでしょうね。
- Rust君も少しくらいアバウトな型宣言に対応してくれませんかね...
- 所有権・借用
- 所有権非常に難しい。
- 借用と参照どっちの呼称がわかりやすいんでしょうね。
- 「どこに所有権があるかプログラマなら当然把握してるよね?」の声がRustコンパイラから聞こえてくる気がします...
- ライフタイム
- lifetime指定の必要/不要のケースで今一つ腑に落ちない点があるので調べてみる
- 構造体のライフタイムと同じになるのはイメージ通りだが、関数のライフタイムと同じにして関数から出てもメモリが維持されるのはよくわからない。
- 参照のlifetimeを一致させることの重要性は理解できたが、それなら
'a
で指定しなくてもデフォルトで一致させてくれれば良いと思った。代入、演算で型をすべて一致させる必要があるのと同じような感覚で指定するのか?
- スマートポインタ
- Rcは確かにガーベジコレクションとそんなに変わらない気がします。
- Cowの用途がいまいちわからなかった。
- 関数型言語の特徴
- return文がいらない書き方に困惑しています...
- returnを含むif構文は式か文か難しいです。でも「?」(三項演算子)と同じ挙動なんですかね。
- mapも関数型言語っぽいですね。そちらも勉強した方がいいかもですが敷居が高いです。
- 文法・標準ライブラリ
- 配列をスライスする際のインデックス指定をもっとわかりやすくして欲しい。
- into関数は暗黙的に、戻り値と引数の型が逆のfrom関数を(実装されていれば)呼び出すそうです。
- ツール
- rust-analyzerは型の自動表示やコード補完の他にもいろいろ機能があり、コーディングに役立ちそう。
- Rustfmt(フォーマットツール)できれいに整形してくれるので、プログラマは余計な気を使わずにコード作成に集中できると思った。
- Rustfixで自動修正される例をもっと調べたい。
- Clippyはコンパイラ以上の有用な指摘をするので驚いています。
- その他
- ELFファイルの.rodataと.dataセクションを勉強できました。
上記の所感を踏まえると、Rust技術者の育成にあたっては次のような課題が感じられました。
- 所有権は、CやC++のプログラマには納得感があるが、GCに慣れた人たちには謎。
- 教えるときには、メモリ以外のリソース管理に使える例を強調したほうが良い?
- 関数型言語になじみがないと、意味が掴みにくい文法がある。
- 文法以外に、スタックやヒープなどのメモリ管理のモデルを最低限教える必要がある。
- 型推論で型指定を省略できる場所とできない場所の違いで混乱する。
- 最初は、省略しない書き方(ライフタイム含む)をテキスト上に併記したほうが良い?
Rustに対する感想
最後に自身でRustを使ってみた感想を述べてみます。
開発速度は低下する?向上する?
Rustの学習は難しいものの、一度理解してしまうとPythonよりも速くモノを作れると感じています。その理由をいくつか挙げてみたいと思います。
Rustに限った話ではないものも混ざっていますが、弊社で利用実績の多い他の言語(JavaやPython)との比較という文脈で見ていただければと思います。
定量的な比較は利用実績が溜まらないと難しいため、定性的な話になる点はご容赦ください。
- セルフレビューの手間を、コンパイラが助けてくれる。
- データ型の定義とそれを使う処理部分が整合しているかのチェック
- メモリ以外のリソースの解放忘れがないかのチェック (リソース: ファイル、TCP接続など)
- 「ブロックから出る≠リソース解放」である場合の管理を所有権が担ってくれる
- ユニットテストを一部減らすことができる。
- 型が全く異なる引数が渡って来る場合のテスト
- 不正な値の組合せになっていないか(不変条件)のテスト
- 不正な値を表現できないように型を定義する (make illegal state unrepresentable)
- 他人のコードのレビューが楽になる。
- 面倒なもの・危険なものは、実装コストが高く、目立ちやすくなっている。
- 面倒なもの: 可変の変数(mut)、スレッド間の共有/同期、など
- 危険なもの: 生のポインタ、型チェックの回避、循環参照(Rc+RefCell)、など
- 面倒なもの・危険なものは、実装コストが高く、目立ちやすくなっている。
- ライブラリのニッチなバグに出くわす確率が下がった気がする。
- ユースケースから少し外れた使い方をした場合に、型チェックが厳格な言語の方がバグに出くわす頻度が低い。
(客観的な評価をしていないし、言語以外の要因も考えられる)
- ユースケースから少し外れた使い方をした場合に、型チェックが厳格な言語の方がバグに出くわす頻度が低い。
従来、コードレビューやユニットテストでチェックしなければならなかったものを、どれだけRustコンパイラに任せられるか・・・を考えるのが、Rustを上手く使いこなすポイントかもしれません。
レビューやテストでは、ヒューマンエラーやスキル不足がネックになるため、その一部を型チェックで確実・網羅的にカバーできると大変助かります。
「難しい」ことの利点
Rustコンパイラが指摘することの多くは、他の言語でも本来なら考えなければならないことであるように思います。「Rustは難しい」という文句が出るのは、従来ならコードレビューで教えなければならなかったことの多くをRustコンパイラが代わりに教育してくれているからだ、と捉えることも可能です。
Rustをチームで利用できるようになれば、組織全体のスキルも上がるのではないでしょうか?
おわりに
今、力を入れて検証していることもあり、Rust激推しの記事となりました。ただ、プログラミング言語の採否が、それ単体の良し悪しだけで決まることはめったにないので、どこまでいけるかは未知数です。
とはいえ、未来を見据えて技術を深く追究するのは非常に楽しいことなので、今後も頑張っていきたいと思います。
記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。