コンテンツにスキップ

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)
ガベージコレクタ 所有権と借用。コンパイル時にチェックされる 所有権、借用
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 クレートを使いましょう。

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にもたいていの用途に多くの選択肢があります。ここに挙げたのは、人気があってよくメンテナンスされているものです。

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
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を動くコード付きで紹介しています。

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.14
6.00
▶ 実際に試してみよう

kind の文字列はいりません。バリアントそのものがタグの役割をします。そして 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 === 1) return { name: "Ada", email: "[email protected]" };
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 {
1 => Some(User { name: "Ada".to_string(), email: Some("[email protected]".to_string()) }),
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")
None
Graceが見つかりました
▶ 実際に試してみよう

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
未払いの注文はある? true
▶ 実際に試してみよう

JavaScriptとの大きな違いがひとつあります。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 here
10 | 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ならではの部分に時間をかけられます。

  1. Rustを始めるとRustのツールと環境構築:Rustをインストールし、VS Codeにrust-analyzerを設定して、cargo new を実行します。
  2. Rustの変数とRustのデータ型:しっかり読みましょう。数値の型は、あなたにとって新しいものです。
  3. 所有権、次に借用、そしてスライス:あなたにとっていちばん大事なページです。じっくり時間をかけてください。
  4. Rustの文字列:String と &str の違い。
  5. 構造体と列挙型、次にmatchとパターンマッチング:これがあなたのインターフェースと判別可能なユニオンです。
  6. OptionとResult、次に独自のエラー型。
  7. クロージャとイテレータ:これがあなたの配列メソッドです。
  8. ベクタ、HashMap、HashSetとBTreeMap。
  9. トレイト、ジェネリクス、よく使うトレイト。
  10. モジュール、Cargoとクレート、テスト。
  11. ライフタイムとスマートポインタ。
  12. async/awaitとasyncをもっと深く。
  13. RustでJSONを扱う。そのあとWeb APIのプロジェクトを作るか、WebAssemblyを試してみましょう。

どちらの問題も、JavaScriptかTypeScriptのコード片をRustに移植(ポーティング)します。

この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: 3
and: 2
bat: 1
cat: 1
hat: 1
▶ 実際に試してみよう

entry(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を取り入れることもできます。

理解度チェック

1. Rustでは undefined と null の代わりに何を使いますか?

2. JavaScriptでは、async関数を呼ぶとすぐに実行が始まります。Rustで async fn を .await せずに呼ぶと、どうなりますか?

3. あなたのTypeScriptのコードは、kind フィールドを持つ判別可能なユニオンを使っています。いちばん近いRustの機能はどれですか?

4. あなたのチームのバックエンドはExpressを使っています。いちばん近いRustのクレートはどれですか?

スコア: 0 / 4

  • 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のアドオンを書くためのドキュメントです。