JavaScript開発者のためのRust
JavaScriptやTypeScriptを書いているなら、あなたは思っているよりもRustを知っています。Rustには、アロー関数とよく似たクロージャ、async/await、map、filter、find といったおなじみの名前のイテレータメソッド、分割代入があります。そして、npm と同じ仕事をこなすコマンドラインツール(Cargo)もあります。TypeScriptを使っているなら、コードを実行する前にミスを見つけてくれるコンパイラにも慣れていますよね。Rustのコンパイラも同じことをします。ただ、もっと厳しいだけです。
では、本当に新しいのは何でしょうか。3つあります。Rustにはガベージコレクタがありません。その代わりに、すべての値には所有者がひとりだけいて、コンパイラがプログラムの実行前に所有権(ownership)と借用(borrowing)をチェックします。Rustには undefined も null もありません。値がないかもしれないものはOption型になります。そして、Rustには例外がありません。失敗する可能性のある関数はResultを返します。学習時間のほとんどは、この3つの考え方に使うことになるでしょう。
JavaScript開発者がRustを学ぶのはなぜでしょうか。たいていは、次のどれかが理由です。ファイルひとつで配布できる速いWebバックエンドやコマンドラインツールを作りたい。ブラウザの重い処理をWebAssemblyで速くしたい。Node.jsのネイティブアドオンを書きたい。あるいは、ふだん使っているツールに興味がある。SWC、Biome、Oxc、Rolldown、Rspack、Turbopack、Lightning CSS、Denoは、どれもRustで書かれています。
このページでは、JavaScriptとTypeScriptで知っていることをRustに対応づけていきます。そうすれば、本当に新しい部分に時間をかけられます。C++、Go、Pythonとの全般的な比較が知りたい場合は、Rust vs C++・Go・Pythonを見てください。
言語の概念:JavaScriptとTypeScriptからRustへ
Section titled “言語の概念:JavaScriptとTypeScriptからRustへ”この表は、旅行の会話帳のように使ってください。いちばん右の列は、Rust側を説明しているこのサイトのページへのリンクです。
| JavaScript / TypeScript | Rust | 詳しくは |
|---|---|---|
let と const |
let(変更できない)と let mut(変更できる)。JSの const のオブジェクトは中身を変更できるが、Rustの let の値は変更できない |
変数 |
number |
サイズを自分で選ぶ:i32、i64、u32、usize、f64、…。整数と浮動小数点数は別の型 |
データ型 |
bigint |
i128 / u128、または任意の大きさを扱える num-bigint クレート |
データ型 |
string |
String(所有していて、伸ばせる)と &str(テキストを借用したビュー) |
文字列 |
boolean |
bool |
真偽値 |
| テンプレートリテラル | format!("Hi {name}") と println!("Hi {name}") |
出力 |
any、unknown |
any はない。ジェネリクス、enum、または形のわからないJSONなら serde_json::Value を使う |
ジェネリクス、RustでJSONを扱う |
オブジェクトの形を表す interface や type |
struct |
構造体 |
メソッドを表す interface |
trait |
トレイト |
| 構造的型付け(同じ形なら同じ型) | 公称型付け:同じフィールドを持つ2つの構造体でも、別の型 | 構造体 |
| ユニオン型と判別可能なユニオン(discriminated union) | 各バリアントが独自のデータを持てる enum |
列挙型 |
"small" や "large" のような文字列リテラル型 |
データを持たないバリアントだけの enum:enum Size { Small, Large } |
列挙型 |
undefined、null、オプショナルプロパティ x?: T |
Option<T>:Some(value) か None のどちらか |
Option |
?. と ?? |
?(Option を返す関数の中で)、.map(...)、.and_then(...)、.unwrap_or(...) |
Option |
非nullアサーション x! |
.unwrap() または .expect("理由")。値が None ならプログラムを止める |
panic |
throw、try/catch、rejectされたPromise |
Result<T, E> + ? 演算子 |
Result |
class MyError extends Error |
独自のエラー用enum(thiserror を使うことが多い)。アプリケーションでは anyhow |
独自のエラー型、anyhowとthiserror |
フィールドとメソッドを持つ class |
データ用の struct + メソッド用の impl ブロック |
構造体 |
extends(継承) |
継承はない。コンポジション(構造体の中に構造体を入れる)+ デフォルトメソッド付きのトレイトを使う | トレイト、デザインパターン |
class Dog implements Animal |
impl Animal for Dog |
トレイト |
| 同じインターフェースを持つ異なるクラスを入れた配列 | Vec<Box<dyn Animal>>(トレイトオブジェクト) |
トレイトオブジェクト |
T extends HasId のようなジェネリクス |
トレイト境界付きのジェネリクス:T: HasId |
ジェネリクス |
| アロー関数とコールバック | クロージャ。Fn、FnMut、FnOnce というトレイトで表される |
クロージャ、Fn・FnMut・FnOnce |
this |
明示的な self、&self、&mut self パラメータ。this が何を指すのか迷うことはない |
構造体 |
分割代入 const { x, y } = point |
パターン:let Point { x, y } = point;(match や関数の引数でも使える) |
パターンマッチング |
スプレッド構文 { ...user, admin: true } |
構造体更新記法:User { admin: true, ..user } |
構造体 |
switch、または typeof / kind で分岐する if の連続 |
match(すべてのケースを網羅したかコンパイラがチェックする) |
match |
=== |
==。値を比較する(PartialEq トレイトを通して)。暗黙の型変換は一切ない |
よく使うトレイト |
for...of |
for item in &items |
forループ |
map、filter、reduce、find、some、every |
イテレータのメソッド:map、filter、fold、find、any、all、最後に collect |
イテレータ |
ジェネレータ(function*)と Symbol.iterator |
Iterator トレイトを実装する |
独自のイテレータ |
Promise<T> |
Output = T の Future。Promiseと違って、awaitされるまで何もしない |
async/await |
async / await |
async fn と .await。実行にはtokioのようなランタイムが必要 |
async/await、asyncをもっと深く |
Promise.all |
tokio::join!、または futures クレートの join_all |
asyncをもっと深く |
setTimeout |
asyncのコードなら tokio::time::sleep、それ以外なら std::thread::sleep |
async/await |
Web Workers、worker_threads |
本物のスレッド:std::thread::spawn、または並列ループなら rayon |
並行処理 |
ESモジュール:import、export |
モジュール:mod、use、pub |
モジュール |
JSON.parse / JSON.stringify |
#[derive(Serialize, Deserialize)] を付けて serde_json を使う |
RustでJSONを扱う |
console.log |
println!。手早くデバッグするなら dbg!。Debug をderiveした型は {:?} で表示する |
出力 |
readonly、as const、Object.freeze |
必要ない。mut と書かない限り、すべて読み取り専用 |
変数 |
| デコレータ | #[derive(Debug)] のような属性と、マクロ |
マクロ |
Number("abc") は NaN になる |
"abc".parse::<i32>() は、処理が必要な Err を返す |
型変換、型変換(From・Into) |
| ガベージコレクタ | 所有権と借用。コンパイル時にチェックされる | 所有権、借用 |
コレクション
Section titled “コレクション”| JavaScript | Rust | 詳しくは |
|---|---|---|
Array |
Vec<T>(すべての要素が同じ型) |
ベクタ |
長さ固定の配列、またはTSのタプル [string, number] |
長さが決まっているなら [T; N]、型が混ざるならタプル (String, i32) |
配列、タプル |
Map、または辞書として使うオブジェクト(Record<string, T>) |
HashMap<K, V> |
HashMap |
| キーが並んだマップ | BTreeMap<K, V> |
HashMap |
Set |
HashSet<T>(並べたままにしたいなら BTreeSet<T>) |
HashSetとBTreeMap |
キューとして使う配列(push + shift) |
VecDeque<T>(push_back / pop_front。どちらも速い) |
VecDequeとBinaryHeap |
スタックとして使う配列(push + pop) |
push / pop で使う Vec<T> |
ベクタ |
Uint8Array、Buffer |
Vec<u8>、またはバイトスライス &[u8] |
スライス |
WeakMap、WeakRef |
Weak<T>(Rc や Arc から作る) |
WeakとCow |
ひとつ注意があります。JavaScriptの Map、Set、普通のオブジェクトは、挿入した順番を覚えています。Rustの HashMap と HashSet は覚えていません。並べた順にしたいなら BTreeMap を、挿入順を保ちたいなら indexmap クレートを使いましょう。
ツール:npmからcargoへ
Section titled “ツール:npmからcargoへ”Cargoには、すぐになじめるはずです。パッケージマネージャ、ビルドツール、テストランナー、スクリプトランナーがひとつになっていて、どのRustプロジェクトも同じ使い方をします。
| JavaScript | Rust | メモ |
|---|---|---|
node + npm(またはpnpm、yarn) |
cargo |
ビルド、実行、テスト、パッケージの追加、公開。Cargoとクレートを参照 |
package.json |
Cargo.toml |
|
package-lock.json |
Cargo.lock |
自動で作られる |
node_modules/ |
プロジェクトごとのフォルダはない。ダウンロードしたものは ~/.cargo に一度だけキャッシュされ、ビルドの成果物は target/ に置かれる |
|
| npmjs.com | crates.io | 公開されたすべてのクレートのAPIドキュメントが docs.rs にある |
npm install zod |
cargo add serde |
|
npm init |
cargo new my_app(ライブラリなら cargo new --lib) |
|
npm run dev / build / test |
cargo run / cargo build / cargo test |
最適化したビルドには --release を付ける。デバッグビルドはかなり遅い |
package.json の "scripts" |
scriptsのセクションはない。.cargo/config.toml のcargoエイリアスか、just のようなタスクランナーを使う |
|
npx some-tool |
cargo install some-tool でインストールしてから実行する |
cargo binstall を使うと、コンパイルせずにビルド済みのバイナリをダウンロードできる |
nvm と .nvmrc |
rustup と rust-toolchain.toml ファイル |
Rustのツールと環境構築を参照 |
| npm / pnpmのワークスペース(モノレポ) | Cargoワークスペース | 最上位の Cargo.toml ひとつにメンバーのクレートを並べる |
tsc |
rustc。cargoが代わりに呼んでくれる |
|
tsc --noEmit |
cargo check |
ビルドせずに型だけをチェックする。ずっと速い |
| ESLint | cargo clippy |
役に立つリントが何百もある公式ツール |
| Prettier | cargo fmt |
全員にひとつの標準スタイル |
nodemon、tsx watch |
cargo watch または bacon |
cargo install でインストールする |
| Jest / Vitest / Mocha | 組み込みの #[test] と cargo test(より速いランナーとして cargo nextest が人気) |
Rustのテストを参照 |
| Jestのスナップショット | insta |
テストをもっと活用するを参照 |
jest.mock() |
mockall、またはトレイトを実装した手書きのフェイク |
|
| fast-check | proptest |
プロパティベーステスト |
| c8 / Istanbul(カバレッジ) | cargo llvm-cov |
|
| JSDoc / TypeDoc | Markdownで書く /// ドキュメントコメント + cargo doc |
ドキュメント内のコード例はテストとして実行される。Rustのドキュメントを参照 |
| VS Code + TypeScriptサーバー | VS Code + rust-analyzer、またはJetBrains RustRover | rust-analyzerで補完、定義へのジャンプ、その場でのエラー表示が使える |
npm publish |
cargo publish |
package.json と比べられるように、小さな Cargo.toml を載せておきます。
[package]name = "orders-api"version = "0.1.0"edition = "2021"
[dependencies]axum = "0.8"serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }"1" のようなバージョン指定は、package.json の "^1" と同じ意味です。互換性のある1.xのリリースなら何でもかまいません。フィーチャー(features)は、クレートのうち自分で有効にするオプションの部分です。Cargoのフィーチャーとcfgを見てください。
ライブラリとフレームワーク:「代わりに何を使えばいいの?」
Section titled “ライブラリとフレームワーク:「代わりに何を使えばいいの?」”これは、どのJavaScript開発者も最初にする質問です。「うちはExpressとPrismaを使っているけど、Rust版は何?」というものですね。ここでは、いちばん近いものを紹介します。npmと同じく、crates.ioにもたいていの用途に多くの選択肢があります。ここに挙げたのは、人気があってよくメンテナンスされているものです。
Webとネットワーク
Section titled “Webとネットワーク”| JavaScript | Rust | メモ |
|---|---|---|
| Express、Fastify、Koa | axum(RustとAxumで作るWeb APIで実際に作ります)、actix-web、Rocket | axumとactix-webは本番環境で広く使われている |
| Expressのミドルウェア | tower のレイヤーと tower-http クレート(CORS、圧縮、トレース、タイムアウト) |
axumはtowerを直接使っている |
fetch、axios |
reqwest |
標準で非同期。ブロッキング版のクライアントもオプションである。Rustのネットワークを参照 |
ws、Socket.IO |
axumのWebSocket(ws フィーチャー)、tokio-tungstenite、または socketioxide(Socket.IOサーバー) |
|
| Apollo Server(GraphQL) | async-graphql |
|
gRPC(@grpc/grpc-js) |
tonic(protobufには prost) |
|
| Swagger / OpenAPIのジェネレータ | utoipa |
|
jsonwebtoken |
jsonwebtoken |
名前も考え方も同じ |
bcrypt |
argon2 または bcrypt |
データアクセス
Section titled “データアクセス”| JavaScript | Rust | メモ |
|---|---|---|
| Prisma、TypeORM | SeaORM(非同期、エンティティ、マイグレーション)または Diesel(クエリはコンパイル時に型チェックされる) | PrismaやTypeORMにいちばん近い感覚なのはSeaORM |
Knex、pg、postgres.js |
sqlx:普通のSQLを書くと、query! マクロがコンパイル時に実際のデータベースと照らし合わせてチェックする |
非同期で、とても人気。ORMではない |
better-sqlite3 |
rusqlite、またはSQLiteで使うsqlx |
|
| Mongoose、MongoDBドライバ | mongodb(公式ドライバ) |
Mongooseのようなスキーマはない。serdeの構造体を使う |
| ioredis | redis |
Rustのデータベースでは、sqlx、Diesel、SeaORMを動くコード付きで紹介しています。
日常的な部品
Section titled “日常的な部品”| JavaScript | Rust | メモ |
|---|---|---|
JSON.parse / JSON.stringify |
serde + serde_json |
定番。TOML、YAMLなどにも対応。RustでJSONを扱うを参照 |
| Zod、Yup | 仕事のほとんどは構造体の型とserdeがこなす。「8文字以上」のようなルールには validator か garde |
JSONが構造体と合わなければ、パースがエラーで失敗する |
| Lodash | ほとんどは標準ライブラリのイテレータで足りる。unique、sorted_by、chunk_by などの追加機能は itertools |
|
| date-fns、Day.js、Luxon | chrono または jiff |
Rustの日付と時刻を参照 |
| dotenv | dotenvy |
環境変数とプロセスを参照 |
| Commander、yargs | clap |
Rustとclapで作るコマンドラインツールを参照 |
| chalk | colored または owo-colors |
|
| winston、pino | tracing(+ tracing-subscriber)、または log + env_logger |
ログ出力を参照 |
uuid |
uuid |
|
EventEmitter |
チャネル(std::sync::mpsc、受け手が多いなら tokio::sync::broadcast) |
チャネルを参照 |
| BullMQ(ジョブキュー) | プロセス内のタスクなら tokio::spawn、永続的なジョブキューなら apalis |
|
| 正規表現 | regex |
設計上、先読みや後方参照はない。Rustの正規表現を参照 |
フロントエンド、デスクトップ、Node.js
Section titled “フロントエンド、デスクトップ、Node.js”| JavaScript | Rust | メモ |
|---|---|---|
| ブラウザの重いコードを速くする | wasm-bindgen と wasm-pack でRustをWebAssemblyにコンパイルする |
RustとWebAssemblyと下のセクションを参照 |
| React、Vue、Svelte | Leptos、Yew、Dioxus(どれもWebAssemblyにコンパイルされる) |
どれもバージョン1.0前で、APIはまだ変わる |
| Electron | Tauri:Webのフロントエンド + Rustのバックエンドで、アプリがずっと小さくなる |
Tauri 2は安定版。RustのGUIアプリ開発を参照 |
| Node.jsのネイティブアドオン(node-gypを使うC++) | napi + napi-derive(napi-rs)、または neon |
ビルド済みのバイナリをnpmで配布できる。C++は不要 |
| Phaser、Three.jsのゲーム | Bevy、macroquad |
Bevyは1.0前で、リリースのたびにAPIが変わる。Rustのゲーム開発を参照 |
並べて比較:インターフェースは構造体になる
Section titled “並べて比較:インターフェースは構造体になる”インターフェースと、メソッドのような関数、スプレッド構文でのコピーを使ったTypeScriptのオブジェクトです。
interface User { name: string; age: number; admin: boolean;}
const greeting = (u: User) => `Hi, I'm ${u.name} (${u.age})`;
const ada: User = { name: "Ada", age: 36, admin: false };const promoted = { ...ada, admin: true };
console.log(greeting(ada));console.log(promoted);Rustでは、オブジェクトの形は構造体になり、関数は impl ブロックの中のメソッドになります。
#[derive(Debug, Clone)]struct User { name: String, age: u32, admin: bool,}
impl User { // `&self` は「このメソッドはユーザーを読むだけで、変更しない」という意味です。 fn greeting(&self) -> String { format!("こんにちは、{}です({}歳)", self.name, self.age) }}
fn main() { let ada = User { name: "Ada".to_string(), age: 36, admin: false };
// JavaScriptの { ...ada, admin: true } と同じです。 let promoted = User { admin: true, ..ada.clone() };
println!("{}", ada.greeting()); println!("{:?}", promoted);}実行結果:
こんにちは、Adaです(36歳)User { name: "Ada", age: 36, admin: true }いくつか違いがあります。どのフィールドにも正確な型があります。age は汎用の number ではなく、u32(負にならない整数)です。#[derive(Debug)] を付けると、console.log(obj) のように構造体全体を {:?} で表示できるようになります。では、なぜ .clone() が必要なのでしょうか。スプレッドにあたる ..ada は、ada から name の文字列をムーブしてしまい、そのあと ada が使えなくなるからです。先にクローンしておけば、ada はそのまま残ります。これが所有権の働きで、これから何度も目にすることになります。
並べて比較:判別可能なユニオンは enum になる
Section titled “並べて比較:判別可能なユニオンは enum になる”TypeScript開発者は、判別可能なユニオンが大好きですよね。kind フィールドでどの形なのかがわかり、switch で型が絞り込まれます。
type Shape = | { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number };
function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.radius ** 2; case "rect": return s.width * s.height; }}
const shapes: Shape[] = [ { kind: "circle", radius: 1 }, { kind: "rect", width: 2, height: 3 },];for (const s of shapes) console.log(area(s).toFixed(2));Rustにはこれが最初から組み込まれていて、言語の中でも特に優れた機能のひとつです。列挙型(enum)の各バリアントが独自のデータを持ち、matchがそれを取り出します。
enum Shape { Circle { radius: f64 }, Rect { width: f64, height: f64 },}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rect { width, height } => width * height, }}
fn main() { let shapes = vec![ Shape::Circle { radius: 1.0 }, Shape::Rect { width: 2.0, height: 3.0 }, ];
for shape in &shapes { println!("{:.2}", area(shape)); }}実行結果:
3.146.00kind の文字列はいりません。バリアントそのものがタグの役割をします。そして match はすべてのバリアントを網羅しなければなりません。あとから Triangle を追加すると、それを忘れている match はすべてコンパイルエラーになります。TypeScriptの never を使った網羅性チェックのテクニックと似ていますが、こちらはいつでも有効です。詳しくはパターンマッチングを見てください。
並べて比較:undefined と null は Option になる
Section titled “並べて比較:undefined と null は Option になる”TypeScriptでは、値がないかもしれないものは undefined、null、オプショナルプロパティとして表れ、?. と ?? で扱います。
interface User { name: string; email?: string;}
function findUser(id: number): User | undefined { if (id === 2) return { name: "Grace" }; return undefined;}
for (const id of [1, 2, 3]) { console.log(`${id}: ${findUser(id)?.email?.toUpperCase() ?? "no email"}`);}Rustでは、値がないかもしれないものは Option<T> 型になります。Some(value) か None のどちらかで、None の場合を処理するまでは、普通の値として使うことができません。
struct User { name: String, email: Option<String>,}
fn find_user(id: u32) -> Option<User> { match id { 2 => Some(User { name: "Grace".to_string(), email: None }), _ => None, }}
// Option を返す関数の中では、`?` が `?.` のように働きます。// 値が None なら、ここで止めて None を返します。fn email_domain(id: u32) -> Option<String> { let email = find_user(id)?.email?; let at = email.find('@')?; Some(email[at + 1..].to_string())}
fn main() { for id in [1, 2, 3] { // findUser(id)?.email?.toUpperCase() ?? "no email" と同じです。 let email = find_user(id) .and_then(|user| user.email) .map(|email| email.to_uppercase()) .unwrap_or("メールなし".to_string()); println!("{id}: {email}"); }
println!("{:?}", email_domain(1)); println!("{:?}", email_domain(2));
if let Some(user) = find_user(2) { println!("{}が見つかりました", user.name); }}実行結果:
2: メールなし3: メールなしSome("example.com")NoneGraceが見つかりましたRustには「cannot read properties of undefined」がありません。読み取る元の undefined が、そもそも存在しないからです。コンパイラは、Option<String> に対して .to_uppercase() を呼ぶことを許しません。そして、null と undefined の違いを覚える必要もありません。あるのは None だけです。便利なヘルパーメソッドはOptionのページで、短い書き方はif let / while letで学べます。
並べて比較:try/catch は Result と ? になる
Section titled “並べて比較:try/catch は Result と ? になる”JavaScriptでは、関数が例外を投げ、そのエラーは呼び出し元のどこかでキャッチします。関数のシグネチャを見ても、例外を投げるかどうかはわかりません。
function parseAge(input) { const age = Number(input); if (!Number.isInteger(age)) throw new Error(`'${input}' is not a whole number`); if (age > 150) throw new Error(`${age} is too old`); return age;}
function totalAge(a, b) { return parseAge(a) + parseAge(b);}
for (const [a, b] of [["36", "41"], ["36", "abc"], ["36", "200"]]) { try { console.log(`Total: ${totalAge(a, b)}`); } catch (e) { console.log(`Error: ${e.message}`); }}Rustでは、失敗する可能性のある関数は Result を返します。Ok(value) か Err(error) のどちらかです。? 演算子は「失敗していたら、今すぐそのエラーを呼び出し元に返す」という意味です。例外を上に伝えていくのと同じ役割ですが、それがコードの上で目に見えます。
fn parse_age(input: &str) -> Result<u32, String> { let age: u32 = input .parse() .map_err(|_| format!("'{input}' は整数ではありません"))?;
if age > 150 { return Err(format!("{age}歳は大きすぎます")); } Ok(age)}
fn total_age(a: &str, b: &str) -> Result<u32, String> { // それぞれの `?` が、例外と同じようにエラーを呼び出し元に渡します。 let first = parse_age(a)?; let second = parse_age(b)?; Ok(first + second)}
fn main() { for (a, b) in [("36", "41"), ("36", "abc"), ("36", "200")] { match total_age(a, b) { Ok(total) => println!("合計: {total}"), Err(e) => println!("エラー: {e}"), } }}実行結果:
合計: 77エラー: 'abc' は整数ではありませんエラー: 200歳は大きすぎます-> Result<u32, String> というシグネチャは、すべての呼び出し元に「これは失敗することがある」と伝えます。そしてコンパイラは、呼び出し元がそれを無視することを許しません。asyncのコードでも同じです。JavaScriptならrejectされたPromiseになるところで、Rustのasync関数は Result を返し、.await? と書きます。独自のエラー型については独自のエラー型を見てください。アプリケーションでは、anyhow クレートを使うと「どんなエラーでもキャッチする」に近いことができます(anyhowとthiserrorを参照)。
並べて比較:配列メソッドはイテレータになる
Section titled “並べて比較:配列メソッドはイテレータになる”毎日使っている配列メソッドには、Rustにそのまま対応するものがあります。
const orders = [ { customer: "Ada", total: 120, paid: true }, { customer: "Grace", total: 80, paid: false }, { customer: "Linus", total: 200, paid: true },];
const paidNames = orders.filter((o) => o.paid).map((o) => o.customer);const revenue = orders.filter((o) => o.paid).reduce((sum, o) => sum + o.total, 0);const firstBig = orders.find((o) => o.total > 150);
console.log(paidNames.join(", "));console.log(`Revenue: ${revenue}`);console.log(`First big order: ${firstBig?.customer ?? "none"}`);console.log(`Anything unpaid? ${orders.some((o) => !o.paid)}`);Rust版では、イテレータとクロージャを使います。クロージャの書き方は、(o) => o.paid ではなく |o| o.paid です。
struct Order { customer: String, total: u32, paid: bool,}
fn main() { let orders = vec![ Order { customer: "Ada".to_string(), total: 120, paid: true }, Order { customer: "Grace".to_string(), total: 80, paid: false }, Order { customer: "Linus".to_string(), total: 200, paid: true }, ];
// filter + map のあと、collect() で結果を Vec にします。 let paid_names: Vec<&str> = orders .iter() .filter(|o| o.paid) .map(|o| o.customer.as_str()) .collect();
// 初期値を指定する reduce は、fold と呼ばれます。 let revenue = orders .iter() .filter(|o| o.paid) .fold(0, |sum, o| sum + o.total);
// 見つからないこともあるので、find は Option を返します。 let first_big = orders.iter().find(|o| o.total > 150);
println!("{}", paid_names.join(", ")); println!("売上: {revenue}"); match first_big { Some(o) => println!("最初の大口注文: {}", o.customer), None => println!("最初の大口注文: なし"), } // some() は any() と呼ばれます。 println!("未払いの注文はある? {}", orders.iter().any(|o| !o.paid));}実行結果:
Ada, Linus売上: 320最初の大口注文: Linus未払いの注文はある? trueJavaScriptとの大きな違いがひとつあります。JSの配列メソッドは、呼ぶたびにすぐ新しい配列をまるごと作ります。Rustのイテレータは遅延評価(lazy)です。filter や map はそれだけでは何もせず、collect()、sum()、fold()、for ループなどを呼んだときに、1回の走査でまとめて処理されます。途中で一時的な配列は作られません。合計だけなら、.map(|o| o.total).sum::<u32>() と書くこともできます。並べ替えはイテレータではなく Vec のメソッド(sort、sort_by)です。並べ替えと検索を見てください。
並べて比較:Promise は Future + tokio になる
Section titled “並べて比較:Promise は Future + tokio になる”asyncを使ったJavaScriptのコードです。"fetching 1" が表示されるタイミングに注目してください。
async function fetchUser(id) { console.log(`fetching ${id}`); await new Promise((resolve) => setTimeout(resolve, 100)); return `user ${id}`;}
const promise = fetchUser(1); // すぐに "fetching 1" と表示されるconsole.log("promise created");console.log(await promise);
const [a, b] = await Promise.all([fetchUser(2), fetchUser(3)]);console.log(`${a}, ${b}`);JavaScriptでは、async関数を呼ぶとすぐに実行が始まります。Rustは違います。async fn を呼んでも、作られるのはFuture(フューチャー)、つまり「これからする仕事の説明書」だけです。.await するまでは何も実行されません。また、Rustには組み込みのイベントループがないので、ランタイムを追加します。いちばん人気があるのはtokio(英語)です。
use std::time::Duration;
async fn fetch_user(id: u32) -> String { println!("ユーザー{id}を取得中"); tokio::time::sleep(Duration::from_millis(100)).await; format!("ユーザー{id}")}
#[tokio::main]async fn main() { // Future を作っても、実行は始まりません。まだ何も表示されません。 let future = fetch_user(1); println!("Futureを作成しました"); println!("{}", future.await);
// Promise.all と同じく、両方を同時に実行して、両方を待ちます。 let (a, b) = tokio::join!(fetch_user(2), fetch_user(3)); println!("{a}, {b}");}実行結果:
Futureを作成しましたユーザー1を取得中ユーザー1ユーザー2を取得中ユーザー3を取得中ユーザー2, ユーザー3順番に気づきましたか? JavaScriptでは、「fetching 1」は「promise created」より前に表示されます。Rustでは、「ユーザー1を取得中」は「Futureを作成しました」のあとに表示されます。Futureは .await されたときに初めて動き出すからです。#[tokio::main] はランタイム(あなたにとってのイベントループ)を用意して、その上で main を実行します。tokio::join! は Promise.all のように2つのFutureをまとめて待ちます。tokio::spawn はタスクをバックグラウンドで開始するもので、async関数をawaitせずに呼ぶJavaScriptの書き方にいちばん近いものです。詳しくはasync/awaitとasyncをもっと深くのレッスンで説明しています。
JavaScriptのプロジェクトでRustを使う:WebAssemblyとNodeアドオン
Section titled “JavaScriptのプロジェクトでRustを使う:WebAssemblyとNodeアドオン”Rustを使い始めるために、何かを書き直す必要はありません。JavaScriptのプロジェクトにRustを混ぜる、よくある方法を2つ紹介します。
ブラウザでWebAssembly。 Rustの関数に #[wasm_bindgen] を付けて wasm-pack でビルドすると、その結果をほかのESモジュールと同じように import できます。wasm-pack は .d.ts ファイルまで書いてくれるので、TypeScriptも関数の型を知ることができます。
use wasm_bindgen::prelude::*;
#[wasm_bindgen]pub fn slugify(title: &str) -> String { title .to_lowercase() .split_whitespace() .collect::<Vec<_>>() .join("-")}import init, { slugify } from "./pkg/my_lib.js";
await init(); // .wasm ファイルをダウンロードして起動するconsole.log(slugify("Hello Rust World")); // "hello-rust-world"RustとWebAssemblyのレッスンでは、セットアップの全手順と、どんな値なら境界を安く渡せるのかを説明しています。コツをひとつ。JSとWasmの間の呼び出しには毎回少しコストがかかるので、Wasmが効果を発揮するのは重い計算(画像処理、パース、シミュレーション)です。何千回も呼ぶ小さな関数には向いていません。
Node.jsのネイティブアドオン。 napi-rs(英語。napi と napi-derive クレート)を使うと、Rustの関数に #[napi] を付けるだけで、Nodeから require や import できる関数になります。人気のnpmパッケージの多くは、この方法でビルド済みのRustバイナリを配布しているので、ユーザーはRustのコンパイラを用意する必要がありません。
JavaScript開発者がつまずきやすいところ
Section titled “JavaScript開発者がつまずきやすいところ”JavaScriptから来た人は、みんな同じところでつまずきます。前もって知っておくだけで、ずいぶん楽になります。
値を関数に渡すと、ムーブされます。 JavaScriptでは、配列を関数に渡すと参照が渡され、そのあとも配列を使い続けられます。Rustでは、Vec や String を値として渡すと、所有権が関数にムーブされ、元の変数はもう使えなくなります。
fn print_all(names: Vec<String>) { for name in names { println!("{name}"); }}
fn main() { let names = vec![String::from("Ada"), String::from("Grace")]; print_all(names); println!("{} names", names.len());}error[E0382]: borrow of moved value: `names` --> src/main.rs:10:26 | 8 | let names = vec![String::from("Ada"), String::from("Grace")]; | ----- move occurs because `names` has type `Vec<String>`, which does not implement the `Copy` trait 9 | print_all(names); | ----- value moved here10 | println!("{} names", names.len()); | ^^^^^ value borrowed here after moveこのエラーは「ムーブ済みの値 names を借用しようとしている」という意味です。よくある直し方は、代わりに借用することです。関数が names: &[String] を受け取るようにして、print_all(&names) と呼びます。まず所有権と借用を読んでください。このルールを説明していますし、毎日のように出会うことになります。
数値には型があり、混ぜることはできません。 JavaScriptの数値は number ひとつです。Rustでは、3 は整数で 2.5 は浮動小数点数なので、変換しないと組み合わせられません。
fn main() { let items = 3; let price = 2.5; let total = items * price; println!("{total}");}error[E0277]: cannot multiply `{integer}` by `{float}` --> src/main.rs:4:23 |4 | let total = items * price; | ^ no implementation for `{integer} * {float}`このエラーは「整数({integer})に浮動小数点数({float})を掛けることはできない」という意味です。items as f64 * price と書くか、items を最初から浮動小数点数(3.0)にしましょう。また、整数の割り算では余りが切り捨てられることにも注意してください。7 / 2 は 3.5 ではなく 3 です。Rustのデータ型と型変換を見てください。
ほかにも、こんなところでつまずきます。
- Futureは遅延評価で、ランタイムが必要です。 上で見たとおり、
.awaitするまで何も起きません。そもそもasyncのコードを動かすには、tokio(または別のランタイム)が必要です。.awaitを忘れると、コンパイラの警告が出るだけで、タスクは実行されません。 - 文字列にインデックスでアクセスできません。
name[0]はコンパイルできません。RustのStringはUTF-8で、1文字が数バイトになることがあるからです。name.chars().next()や.chars().nth(i)を使いましょう。また、Stringと&strの違いは早めに覚えておきましょう。関数はたいてい&strを受け取り、構造体はたいていStringを持ちます。Rustの文字列を見てください。 - 構造的型付けも
anyもありません。 TypeScriptでは、必要なフィールドを持つオブジェクトなら何でもインターフェースに当てはまります。Rustでは、Userを受け取る関数はUserしか受け取りません。振る舞いを共有するにはトレイトを使います。形のわからないJSONにはserde_json::Valueを使います。 - 共有されて変更されるオブジェクトのグラフは難しいです。 JavaScriptでは、ひとつのオブジェクトをあちこちに渡してみんなで変更したり、オブジェクトが親を指し返したりするのは普通のことです。借用チェッカーが許すのは、同時に「読み手がいくつでも」または「書き手がひとりだけ」で、両方は許しません。そうしたパターンにはスマートポインタ(
Rc<RefCell<T>>、Arc<Mutex<T>>)が必要になります。もっと良いのは、設計を変えることです。最初の数週間はイライラするかもしれませんが、そのあとはずっと楽になります。 HashMapには順序がありません。 JavaScriptのMapやオブジェクトは挿入順を保つので、気づかないうちにそれに頼ったコードを書きがちです。RustのHashMapは順序を保ちません。表示する前に並べ替えるか、BTreeMapやindexmapクレートを使いましょう。- 例外がありません。 エラーは値なので、
?をたくさん書くことになります。学習中は.unwrap()を使ってもかまいませんが、これはTypeScriptの!のRust版、つまり「信じて、ちゃんとあるから」という意味です。もし間違っていたら、プログラムはパニックします。 - コンパイル時間。
node app.jsのような即時のリロードはありません。多くのクレートを使う最初のビルドには時間がかかりますが、2回目以降はインクリメンタルビルドなので速くなります。編集中は、コード生成を省略するcargo checkを使いましょう。
JavaScript開発者のための学習ルート
Section titled “JavaScript開発者のための学習ルート”このサイトを順番どおりに読む必要はありません。JavaScriptやTypeScriptの開発者なら、次のルートで進むと、すでに知っていることは飛ばして、Rustならではの部分に時間をかけられます。
- Rustを始めるとRustのツールと環境構築:Rustをインストールし、VS Codeにrust-analyzerを設定して、
cargo newを実行します。 - Rustの変数とRustのデータ型:しっかり読みましょう。数値の型は、あなたにとって新しいものです。
- 所有権、次に借用、そしてスライス:あなたにとっていちばん大事なページです。じっくり時間をかけてください。
- Rustの文字列:
Stringと&strの違い。 - 構造体と列挙型、次にmatchとパターンマッチング:これがあなたのインターフェースと判別可能なユニオンです。
- OptionとResult、次に独自のエラー型。
- クロージャとイテレータ:これがあなたの配列メソッドです。
- ベクタ、HashMap、HashSetとBTreeMap。
- トレイト、ジェネリクス、よく使うトレイト。
- モジュール、Cargoとクレート、テスト。
- ライフタイムとスマートポインタ。
- async/awaitとasyncをもっと深く。
- RustでJSONを扱う。そのあとWeb APIのプロジェクトを作るか、WebAssemblyを試してみましょう。
どちらの問題も、JavaScriptかTypeScriptのコード片をRustに移植(ポーティング)します。
1. Mapで単語を数える
Section titled “1. Mapで単語を数える”このJavaScriptのコードは、それぞれの単語が何回出てくるかを数え、多いものから順に表示します(同じ数ならアルファベット順)。これをRustに移植してください。ヒント: HashMap<&str, u32> と *counts.entry(word).or_insert(0) += 1 を使います。並べ替えるには、マップを (単語, 回数) のペアの Vec に集めて、sort_by を使いましょう。
const text = "the cat and the hat and the bat";
const counts = new Map();for (const word of text.split(" ")) { counts.set(word, (counts.get(word) ?? 0) + 1);}
const sorted = [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));for (const [word, n] of sorted) console.log(`${word}: ${n}`);解答を見る
use std::collections::HashMap;
fn main() { let text = "the cat and the hat and the bat";
let mut counts: HashMap<&str, u32> = HashMap::new(); for word in text.split(' ') { *counts.entry(word).or_insert(0) += 1; }
// [...counts] と同じく、マップを (単語, 回数) のペアのリストにします。 let mut sorted: Vec<(&str, u32)> = counts.into_iter().collect(); // 多いものから順に。同じ数ならアルファベット順。 sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (word, n) in sorted { println!("{word}: {n}"); }}実行結果:
the: 3and: 2bat: 1cat: 1hat: 1entry(word).or_insert(0) は、counts.get(word) ?? 0 の部分と set を一度にこなします。回数への可変参照を返し、まだなければ 0 で作ってくれるのです。ここでの並べ替えは「あったら便利」ではなく必須です。並べ替えないと、HashMap は実行するたびに違う順番で表示されます。この例では、アルファベット順に並べるので、文章は英語のままにしています。
2. デフォルト値付きのオプション設定を移植する
Section titled “2. デフォルト値付きのオプション設定を移植する”このTypeScriptの関数は、すべてのフィールドが省略可能なオプションオブジェクトを受け取り、?? でデフォルト値を補います。これをRustに移植してください。ヒント: Option のフィールドを持つ構造体を作り、#[derive(Default)] を付けます。すると ServerOptions { port: Some(3000), ..Default::default() } が、{ port: 3000 } を渡すのと同じように働きます。bool のデフォルト値は false です。
interface ServerOptions { host?: string; port?: number; verbose?: boolean;}
function describe(opts: ServerOptions): string { const host = opts.host ?? "localhost"; const port = opts.port ?? 8080; const mode = opts.verbose ? "verbose" : "quiet"; return `${host}:${port} (${mode})`;}
console.log(describe({}));console.log(describe({ port: 3000 }));console.log(describe({ host: "example.com", verbose: true }));解答を見る
#[derive(Default)]struct ServerOptions { host: Option<String>, port: Option<u16>, verbose: bool,}
fn describe(opts: &ServerOptions) -> String { let host = opts.host.as_deref().unwrap_or("localhost"); let port = opts.port.unwrap_or(8080); let mode = if opts.verbose { "verbose" } else { "quiet" }; format!("{host}:{port} ({mode})")}
fn main() { println!("{}", describe(&ServerOptions::default())); println!("{}", describe(&ServerOptions { port: Some(3000), ..Default::default() })); println!( "{}", describe(&ServerOptions { host: Some("example.com".to_string()), verbose: true, ..Default::default() }) );}実行結果:
localhost:8080 (quiet)localhost:3000 (quiet)example.com:8080 (verbose)unwrap_or があなたの ?? です。as_deref() は Option<String> を Option<&str> に変えるので、何もコピーせずに "localhost" という普通のテキストにフォールバックできます。そして ..Default::default() は「ここで書かなかったフィールドは、すべてデフォルト値にする」というRustの書き方で、JavaScriptでプロパティを省略するのとまったく同じ意味です。ポートを u16 にしておけば、70000 のようなポート番号をうっかり格納することもできません。
- JavaScriptとTypeScriptの多くはそのまま活かせます。クロージャ、async/await、分割代入、そして
map、filter、findのような配列風のメソッドには、どれもRustによく似たものがあります。 - 大きな違いは、ガベージコレクタの代わりに所有権、
undefinedとnullの代わりにOption、throwとtry/catchの代わりにResult+?を使うことです。 - TypeScriptのインターフェースは構造体(データ用)かトレイト(振る舞い用)になり、判別可能なユニオンは
matchと一緒に使うenumになります。 - RustのFutureは遅延評価で、tokioのようなランタイムが必要です。
Promise.allの役割はtokio::join!が担います。 - Cargoがnpm、
package.json、スクリプトの代わりになり、clippyとrustfmtがESLintとPrettierの代わりになります。テストは最初から組み込まれています。 - Expressならaxumかactix-web、
fetchならreqwest、PrismaならSeaORMかDiesel、ElectronならTauriを見てみましょう。WebAssemblyやnapi-rsを使って、JSのプロジェクトにRustを取り入れることもできます。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. Rustでは undefined と null の代わりに何を使いますか?
2. JavaScriptでは、async関数を呼ぶとすぐに実行が始まります。Rustで async fn を .await せずに呼ぶと、どうなりますか?
3. あなたのTypeScriptのコードは、kind フィールドを持つ判別可能なユニオンを使っています。いちばん近いRustの機能はどれですか?
4. あなたのチームのバックエンドはExpressを使っています。いちばん近いRustのクレートはどれですか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”- The Rust Programming Language 日本語版:公式の本の日本語訳です。JavaScript開発者にとって特に大事なのは、4章(所有権)、6章(enumとパターンマッチング)、9章(エラー処理)です。
- Tokio tutorial(英語):tokioを使った非同期Rustの公式ガイドです。最初のタスクから、チャネルやI/Oまで学べます。
- The wasm-bindgen Guide(英語):WebAssemblyでRustとJavaScriptがやり取りする方法を、たくさんの例とともに説明しています。
- Rust から WebAssembly にコンパイル(MDN):MDN Web Docsのステップバイステップのチュートリアルです。npmへの公開まで扱っています。
- napi-rs(英語):RustでNode.jsのアドオンを書くためのドキュメントです。