🦀

自身の経験とRustへの思い

に公開

はじめに

こんにちは!aq2rです。
突然ですが皆さんRustは書いてますか?いい言語ですよね!最高の言語ですよね!(異論は認めます)
この記事では私が「なぜRustにのめりこむことになったか」や、「なぜRustを気に入っているか」、また全体として大好きなんだけどこういう部分残念だよねーみたいな話を書いておきたいと思います

内容はだいたいこんなかんじ。
だらだらと思ったことを書いてる感じなので興味ある所だけ目次から飛んで読んじゃってください。

  1. プログラミングを初めて触ってからRustにたどり着くまでのお話
  2. Rustの"良い / 気になる"点
  3. これからRustを触ろうと思っている人へ

対象読者

  • Rustに興味があるけどなかなか手を出せていない人 (特に昔の私のようなスクリプト言語しか触ったことない人)
  • Rustの良い点を聞いて後方腕組みしたい人
  • Rustの悪い点・あるあるを共有して共感したい人
  • 暇だからだらだらと暇つぶしに文章読んでたい人

1. 私がRustにたどり着くまで

プログラミングを始める

私がプログラミングを始めたきっかけはDiscordのBotを作りたいというものでした。
当時(あいまいな記憶では6年ほど前)、私は当然プログラミングの知識などはなく、Python(discord.py)とJavaScript(discord.js)の違いすら分かりません。
(discord.pyとdiscord.jsをごちゃごちゃにして参考にしていました。)

そんな状況でPythonの基礎を学ばずに、いきなりdiscordのbotを動かすという応用に突っ込んで、
なぜか動かないと苦戦しながらなんとなくこう書けばこう動くというのを学習していき、
だいたい3~4年ほどでdiscord.pyのAPIリファレンスを読んで解決できるほどの知識が付きました。

余談 discordのAPIリファレンスについて:

discordpyのAPIリファレンス(というかライブラリのリファレンス)ってclassとか関数とかを理解してないと読めないですよね...
当時「リファレンス?説明書あるんだ!」と思ってページを見に行ったらナニコレ?ってなった記憶。

さてそこである程度プログラミングができるようになり、思いました。

「ゆるゆるすぎてボンミスする!」

ボンミスの例:
1.
インスタンス作成時にif文で分岐してインスタンス変数を初期化していたので、
場合によってはインスタンス変数が初期化されておらず "AttributeError!"

AttributeErrorになる例:
class A:
    def __init__(self, *, is_flag: bool) -> None:
        if is_flag:
            self.value = 1


if __name__ == "__main__":
    a = A(is_flag=False)
    print(a.value)
Traceback (most recent call last):
  File "main.py", line 9, in <module>
    print(a.value)
          ^^^^^^^
AttributeError: 'A' object has no attribute 'value'

これは簡単な例なのでこうなることは明白なのですが、当時書いていたコードはある程度複雑だったので、実行してからこのエラーが発生し面倒なことになったのでした。
もちろん「設計が悪い!」というのはそうなのですが...

柔軟、悪く言えば「汚い書き方もできる」ので、あとから見返してみて「きったな...」となる

当時書いていた謎コードの再現:
import library_a
import library_b

if True:
    import my_module_a
    import my_module_b

...

🤔

説明しておくと、当時の書き方ではimportされた瞬間に実行されるようなコードを書いていたので、フォーマッタにimport文の順序を変えられないように このような書き方になっていたのでした。
もちろん「設計が悪い!」というのはそうなのですが...

当時の私: 「もうPython書きたくない!」

余談:今はPythonは割と気に入っているというお話

今はPythonは結果的に気に入ってます!上のボンミスはpythonが悪いというよりは汚い書き方をした私が悪いものですし...
今はuvがあり簡単に仮想環境を扱えるし、ruffもあるのでPythonの汚い書き方もできてしまうというのも割と目立たないんじゃないかしら。

私の「いいかんじのげんご」探しが始まりました。
注: 以下でこの言語微妙かもみたいな書き方をしていますがその言語を批判する意図はないです、あくまで当時の私に合わなかっただけで...

「いいかんじのげんご」探し

  1. C

まず触ってみたのがC言語。Pythonはプログラミング言語の中では遅い方です。かなり遅い方です...
実際軽く触っていたtkinterで画像の表示などで遅さを実感していました(もちろん書き方が悪かった可能性もあり)

速さは正義!ということでC言語を触った結果は...
「で、何ができるんだろう...」

そう、私がやっていたのはdiscordのbot作成な、どちらかというと高レイヤーなもの。
高速な計算を行いたいわけでもなかったのでc言語は私の用途には向かなかったのでした。
ただポインタの考え方などPythonでは扱うことのなかったメモリ周りの知識を学習できたのはうれしかったです。

余談 ポインタについて:

C言語でよくめちゃくちゃ難しいものとして語られるポインタ。
これって「するするっと理解できる人」と「全く理解できない人」に極端に分かれるような...

私自身はC言語を触ってみてポインタ周りの理解で苦労することは全くなく、
むしろ理解した後も、苦労しなかったため以下のように疑っていました。
「これよく難しいと言われていて、私は難しいと思っていないということはまだ私は理解できてないのでは!?」

私と私の友人(C言語使い)は「よく難しいと言われているけど何が難しいのか理解できない」という立場なので、この辺個人差があるんですかね...
「簡単に理解できる人」と「理解が難しい人」のどこが違うのかを解明できれば理解が難しい人向けにわかりやすい説明をしたりできないかしら。

Rustの所有権の理解周りに関しても似たような状況があるような。

  1. Java

次に触ったのはJava。discordのbotの中にはPythonやjsではなくJavaで作られているものもあるみたい。(Vortexとかそうだったかしら。)

Javaってなんか重要なシステムとかに使われてるイメージあるし!いい感じに書けるのでは!?
結果は...
「冗長すぎませんかね...」

理想はPythonの型ヒントみたいに、本当に必要な部分だけ型を書くこと。
変数宣言するときに毎回型を書きたくない(わがまま)
注: 多分今のJavaは型推論でいい感じに書けるんじゃないかしら。その時は知らなかったけど...

これだったらPythonでいいや...(´・ω・`)
何というかコンパイル言語だから厳格なのはわかるけど、正直discord.pyで実行速度に困ったことはないから書きやすさを犠牲にしてまでJavaを書かなくてもいいかなぁとその時は思ったのでした。

  1. Lisp

次に触ったのはLisp。かっこがたくさんのげんご。なんかすごいらしいというのをきいて!
結果は...
「面白い!めっちゃ面白い!けど実用的じゃ無さ過ぎませんかね...」

ライブラリの充実度やエディタ上での補完といったそういう面で微妙だと感じ、触るのをやめてしまったのでした...

  1. Rust

そして最後に触ったのはRust。なんか速いし最近の言語と同じように書けるというのを聞いて!
結果は...
「え、ほしいもの全部そろってない?」

速度。プログラミング言語中トップクラス。
冗長さ。ほぼPythonの型ヒントみたいな感じで書ける。
フォーマッタ、リンター。標準装備。rust-analyzerで補完も完ぺき。
ライブラリ。Pythonほどとは言わないけど割と充実してきてる。Cargoがあるからpipと同じ感覚で使える。

結果私はRustにハマっていったのでした!

2. Rustの"良い / 気になる"点

良いと思う所

  • 実行速度

Rustは速い!とよく言われるのでここはとても良い点だと思います...が。
私は実行速度が速いというのはオマケだと考えています。
いや、Rustが素晴らしい点の1つであることは間違いないんですが、そんなことよりもいかにRustが書きやすいかを褒めたいし広まってほしい。

  • 意外と柔軟

Rustは厳格な言語です。コンパイラのチェックも厳しいです。けど意外に柔軟な書き方ができるんですよね~
例えばファイルパスを受け入れる時にimpl AsRef<Path>という風に引数を書くことで、
&str, String, Path, PathBufなどの様々な型を同じように受け入れるとか。

use std::path::Path;
use std::path::PathBuf;

fn with_filepath(_path: impl AsRef<Path>) {
    /* do_something */
}

fn main() {
    // &str
    with_filepath("hello.txt");
    
    // String
    let s = String::from("hello.txt");
    with_filepath(s);
    
    let path = PathBuf::from("hello.txt");
    
    // &Path
    with_filepath(&path);
    
    // PathBuf
    with_filepath(path);
}
  • チェックが厳しいのは後々楽

Rustのコンパイラ、厳しいですよね。特にスクリプト言語しか触ってこなかったらそう思います。
なんだかんだRustはこれで2年ほど書いていますが、それでもあまり出ないパターンだと所有権周りで引っかかったりします。(例: ループや再帰で所有権に引っかかる)
この性質のおかげで読みづらく書くことが逆に難しいような気がするんですよ。
もちろんマクロを多用すれば多少読みづらくなったりはしますが、きれいなコードを書きやすいというか、そんな気がします。

  • ブロックの最後に置いた値が戻り値になる
let x = 2;
let y = 3;
let z = 5;

let tmp = x + y;
let answer = tmp + z;
println!("{answer}"); // 10

これは以下のように書くこともできます。

let x = 2;
let y = 3;
let z = 5;

let answer = {
    let tmp = x + y;
    tmp + z
};
println!("{answer}"); // 10

このブロックの最後に置いた値が戻り値になるという性質、あまり語られないと思うのですがこの変数はここからここまでしか使わないということを明示でき、めちゃくちゃ便利だと思いませんか...?

  • マルチスレッドでもバグらない(競合が起きない)

これもかなり強いところだと思っています!他の言語だと"バグってから気づく(c, c++)/バグらない代わりに制限がある(PythonのGIL)"といった感じですが、
Rustだとバグるようなコードはそもそも実行できないというのはありがたいんじゃないでしょうか?安心して書くことができますね!

気になる点

ここからはRustの気になる点を書いていきたいと思います。
Rustが最高の言語であることは周知の事実なので良い点よりも気になる点を重点的に書いていきたいと思います。

  1. 重い。

Rustは重いです。え?「実行速度は速いんじゃないのか」って?あ、実行時じゃなくて開発時の話なんです...

まずコンパイル。長い、長すぎる... 例えばserenityを使ってdiscord botを作ろうとすると、初回コンパイルに数分待たされます。まあ初回コンパイルさえ済ませてしまえば次回からはそこまで待つ必要はないんですが、そうするとtargetフォルダが意外と容量を食います。
軽くC#でdiscord botを作って見ようと触った時にはコンパイルの速さに感動しました。

そしてrust-analyzer。結構重いです。tauriでアプリを作っているとたまに保存してから結果が表示されるまでちょっと待たされたり、プロジェクトによると思いますがタスクマネージャーを見ると意外とメモリを食ってたりします。

  1. 標準ライブラリが薄い、そして依存関係が積みあがる

Rustの標準ライブラリは貧弱です。何と正規表現や乱数すら標準ライブラリに搭載されていません。これは私の推測ですが標準ライブラリに含めてしまうと互換性を失うようなことができなくなるので、ライブラリとして切り出してるのではないかと思っています。
どうせcargo addで簡単にライブラリ追加できますし。

そしてその結果ライブラリを追加すると1つのライブラリがx個のライブラリに依存していて、その依存されているライブラリがy個のライブラリに依存していて...ということが起こり、1つのライブラリを使いたいだけなのに数百個のライブラリをビルドすることになったりします。

正直私は依存関係は少なければ少ないほどいいと思っているのでこれは気になってしまいます。

  1. 非同期Rustは正直使いやすいとは言えない...

まず非同期を扱おうとするとライブラリの追加が(ほぼ)必須です。(tokio)
これはPythonならasyncioが標準装備なのになーと思ってしまう所...

そして非同期RustはRustの中でもトップクラスに難しい(と思います)。
というのもRustでの非同期は基本マルチスレッド。
(デファクトスタンダードである tokio ランタイムのデフォルトがマルチスレッドになっています)
つまり、
マルチスレッド x 非同期 + 所有権もろもろ
という高難易度が襲い掛かります。
例えば私は同期Mutexのロックがawaitをまたぐようなコードを書いてしまいなぜかコンパイルが通らなくなってしまったことがありました。

またうまく言語化できませんが、そもそも非同期Rustは全体的にうまくできていないような印象を受けます...

  1. フロントエンドRustは発展途上、だしマクロの補完が効かないのはきつい。

Rustaceanの皆さんは思うことでしょう。どうせならフロントエンドも全部Rustでやりたいと。
Rustにはそれを叶えるライブラリがあります!Yew, Leptos, Dioxus...などなど他にもいろいろ。
それぞれ触った感想は...
「補完効かないのきつすぎない?」
「毎回コンパイル入って再起動もきつくない?」

正直これならTypeScript(React)使った方が楽だしライブラリも多いし...(´・ω・`)
と、Tauriアプリの開発時にフロントエンド側にRustを使うことはあきらめたのでした。

と書きましたが、希望はあります。それはDioxus。
何と最近のv0.7(2026年3月時点)でRustコードのホットリロードが実装されたらしい!(<- !?)
試してみたところ本当にRustのコードを書き換えても全体を再起動することなく変更が反映されました!
私はこのライブラリの今後に期待してます!

  1. const周りの計算も発展途上

私は最近、とにかくパフォーマンスを追求したコードを書こうとしていたのですが、
そこで気になったのがconst generics。残念ながらconst genericsの値同士の計算ができません。
どういうことかというと以下のようなコードです。

fn func<const N: usize, const M: usize>() -> [bool; N * M] {
    [true; N * M]
}

C++のtemplateはめちゃくちゃ強力そうなので、C++を勉強してみたくなりました。
Rustacean向けのC++入門みたいな記事ないかしら。逆ならたくさんある気がするんだけど...

だいたいRustで気になっている部分はこんな感じでしょうか。
いろいろ言いましたがRustは本当に大好きです。今後の進化を楽しみにしてます!

3. これからRustを触ろうと思っている人へ

(私はまだRust歴2年ですが)ここからRustを触ってみたい!という人向けにいろいろ書いておきたいと思います。
(Rustaceanの皆さんへ: わかりやすさを重視した結果正確ではない表現になっている場所があったら指摘してもらえると助かります!)

難易度は高め

Rustはプログラミング言語の中でも、「とっつきやすさ」という点ではかなり難しめの言語だと思います。
例えば、CやC++を除く大体の言語ではメモリをたくさん使う変数は暗黙的に共有されます。
Pythonで例を出しますが以下のような経験はないでしょうか?

num_list = [1, 2, 3]
tmp = num_list

tmp[0] = 0

print(num_list) # [0, 2, 3]
print(tmp) # [0, 2, 3]

「あれっ、tmp変数を変更しただけなのにnum_list変数も変わってしまっている!?」

これはlistのような「データがたくさんあるのでメモリをたくさん食うもの」は毎回コピーしているとメモリの容量を圧迫してしまうので「共有」することでメモリを圧迫しないようにしているのです。

ではRustではどうか。

fn main() {
    let num_list = vec![1, 2, 3];
    let mut tmp = num_list;

    tmp[0] = 0;

    println!("{num_list:?}"); // borrow of moved value: num_list
    println!("{tmp:?}");
}

Rustではnum_list変数の内容がtmp変数に「移動」します。
するとlet mut tmp = num_list; とした時点でnum_list変数は「消滅」するのでもう使えませんよ!ということです。

ただし軽いデータ(正確に言うと`Copyトレイト`が実装されたもの)は別です

ただしこの性質はVecのような「たくさん容量を食うやつ」に限られます。
例えば以下のコードは問題なく動きます。

fn main() {
    let num = 0;
    let mut tmp = num;

    tmp += 1;

    println!("{num}"); // 0
    println!("{tmp:?}"); // 1
}

「え?なんで数値は問題なく動くの?全部"移動"させたほうがわかりやすくない?」
と思うかもしれませんが、このような「軽いデータ」、正確に言うと「メモリのスタックに乗るデータ」は高速にコピーができ、かつ容量もあまり食わないのでコピーしてしまった方がプログラミングしやすくなるためこうなっています。

じゃあ「どういうものはコピーされて、どういうものは移動するの?」
というのを理解するにはメモリの仕組み(ヒープとスタック)を理解する必要があります。

※ この「移動」は効率的なプログラムを書くだけでなく、メモリの二重解放などのバグも防いでくれます

これ、暗黙的に共有されちゃった結果、
「こっちの変数いじったらなんかあっちの変数も変わってたんだけど!?」
という状況を生みにくくなり非常にわかりやすいと思いませんか?

ただしこれは、「変数がどこにあるか、どこに貸し出されているか」をしっかり把握しておく必要があります。

例えばPythonと同じく一時的にtmpにも共有して書き換えたい場合は以下のようにします。

fn main() {
    let mut num_list = vec![1, 2, 3];

    let tmp = &mut num_list;
    tmp[0] = 0;

    println!("{num_list:?}"); // [0, 2, 3]
}

「こっちの方がtmpに共有していることが明示されていてわかりやすいじゃん!」
と思うかもですが、以下の例ではエラーになります。

fn main() {
    let mut num_list = vec![1, 2, 3];
    let tmp = &mut num_list; // ここで貸し出し中

    tmp[0] = 0;

    // ↓ tmpに書き換え権限を貸し出している最中に、num_listを使おうとしたためエラー!
    println!("{num_list:?}"); // cannot borrow num_list as immutable because it is also borrowed as mutable
    tmp[1] = 0; // <- これを消すと動く

    // Rustのコンパイラは優秀なので最後の行を消せば
    // `tmp`はもう使われていないと判断し、num_list変数を使わせてくれます。
}

これはtmpに貸し出している状態でnum_listを読み取ろうとしたためエラーになります。
このように変数の「所有者」はだれか、それを「どこでどの変数に貸し出しているか」というのをしっかり把握しておく必要があります。

Rustコンパイラくんの「Rustaceanならメモリのどこに変数があってそれをどこで貸し出しているか当然把握しているよね?」という声が聞こえてきそうですね。

簡単な例ではどこでエラーになっているかわかりやすいですが、ある程度複雑な状況になってくると、「なぜかエラーになるんだけど!」という状態になったりすることもあると思います。

これがRustがとっつきにくい理由の一つになっていると思います。
ただし一度書きなれてしまえばこの仕組みのおかげで速度と安全性を両立でき、コンパイラの厳しさは安心感に変わります!

以下のRust入門の動画はメモリの解説から行われているのですごく参考になると思います!
https://www.youtube.com/watch?v=lG7YbM2AfU8

RustはあくまでC++などの代替であることを意識する

Rustはあくまで「プログラミング言語トップクラスの速度を保ちながら、安全に書きたい!」というワガママを叶えてくれる言語です。

トップクラスの速度じゃなくても「スクリプト言語よりは速度が出てくれればいいな」というのであればGo言語やC#などを選んだほうが、学習コストや開発スピードの面で幸せになれるかもしれません。

個人的にC++やRustはプログラミング言語の中でもそれぞれ違った方向でトップクラスに難しいと思います。

余談: C++を学んでみたい話

私はRust信者でC++を触ったことはないのですが最近C++のコードを読む機会があり、
「Rustで言うUnsafeコードを利用した極端な最適化」をしたり、
「強力なtemplateを活かして速度とコードのきれいさを両立する」コードだったりと、
感動する点が多かったのでC++を学んでみたいなと思っています。

ただ、C++は歴史が深く、古い情報から新しい情報までネット上に混在しており、どこから手をつけていいのかなかなか難しいなーと感じています。
特に私は最新のC++(C++20〜23あたり)のモダンな書き方を学んでみたいのですが、情報が少なくて悩んでいます。

もしこの記事を読んでいる方の中に「C++熟練者」の方がいらっしゃったら、おすすめの学習方法などをぜひコメント欄で教えていただけませんか...?

Rustを「なんか最近のモダンな言語と同じ感覚でサクッと書けて、しかもめちゃくちゃ速いらしい!」と、他の言語と同じような期待値で挑むと、コンパイラの厳格なルールで大やけどするかもしれません。

「これまでPythonやTypeScriptをメインで触っていたけど、Rustもやってみたい!」という方は、パラッコリーさんの『Rustが嫌いです。』という記事を読むと体験談が書かれていて参考になるかも!
https://zenn.dev/miguel/articles/f052de93fc9980

非同期Rustは...最初は避けたほうがいいかも?

上でも書いたのですが、非同期Rustは難易度が高いと思います。
higumachanさんの「Rust入門者は非同期Rustをやらないでください」という記事で詳しく説明されているので参考になると思います。
https://zenn.dev/higumachan/articles/b2d7b5f1e77c50

おわりに

Rustは最初のハードルは高いですがとてもいい言語だと思うのでもっと広まってくれると嬉しいです!
ここまで読んでいただきありがとうございました!

宣伝

ここからは私が作っている beatrice-client というアプリの宣伝です!
https://github.com/aq2r/beatrice-client

これは @prj_beatrice さんが作っているボイスチェンジャー、beatriceを気軽に使えるようにアプリ化したものです。
※ 誤解のないように説明すると私はボイスチェンジャー本体には関わっておらず、ボイスチェンジャーを気軽に使えるようにTauriでアプリ化したという感じです。

もともとDAW上で使用するボイスチェンジャーなのですが気軽に使えると嬉しいなと思い作成しました!

以下は@prj_beatriceさんの記事やbeatriceの遅延を計測されている記事です。

https://zenn.dev/prj_beatrice/articles/40b122b8de6472
https://zenn.dev/gokrack/articles/2f50ef6cb9531f

低遅延で負荷も少なく、きれいに変換されるのでボイスチェンジャーを触ったことない人、またRVCなどを使ったことがある人はぜひ使ってみてほしいです!

Discussion