rustを学んで[rustlings②]
Rustの学習記録:モジュールと所有権の深掘り
はじめに
前回の続き。Rustを学んでいて、モジュールシステムと所有権の概念に躓いた記録です。他の言語と違って?(js、pythonをそっとくらいしかありませんが)、Rustのfundamentalsは「なぜそうなっているのか」を考えさせてくれて、とても面白いと今のところは感じています。
表現方法の一部としてRustを使いたいと考えているので、深く理解したいと思っています。
Part 1: モジュールシステムとの格闘
最初のエラー:プライベート関数
error[E0603]: function `make_sausage` is private
Rustでは、モジュール内の関数はデフォルトでプライベートです。外部から使うにはpubキーワードが必要との事。
mod sausage_factory {
pub fn make_sausage() { // pub を追加
get_secret_recipe();
println!("sausage!");
}
}
モジュールは「箱の中の箱」
最初、モジュールの階層構造が直感的ではありませんでした。でも、こう考えるとわかりやすいです。
mod 大きな箱 {
mod 小さな箱 {
pub fn 関数() {}
}
}
これは以下のような構造:
大きな箱/
└── 小さな箱/
└── 関数
:: は「の中の」という意味で、大きな箱::小さな箱::関数 = 「大きな箱の中の、小さな箱の中の、関数」です。
JavaScriptやPythonの . と同じ役割ですが、Rustでは . はメソッド呼び出しに使うので、:: を使います。
モジュール ≠ オブジェクト
重要な気づき:モジュールはオブジェクトではありません。
JavaScript(オブジェクト):
const snacks = { // これは実行時に存在するデータ
fruit: "Pear",
veggie: "Cucumber"
};
Rust(モジュール):
mod snacks { // これはコンパイル時の整理
pub const FRUIT: &str = "Pear";
pub const VEGGIE: &str = "Cucumber";
}
| 概念 | 存在するタイミング | 役割 |
|---|---|---|
| オブジェクト | 実行時(メモリ上) | データを保持する |
| モジュール | コンパイル時 | コードを整理する |
モジュールはJavaScriptのモジュールと同じ
でも、ES6のモジュールシステムと比較すると、概念は同じです!
| JavaScript | Rust |
|---|---|
export | pub |
import | use |
import { x as y } | use x as y |
export { x as y } | pub use x as y |
JavaScript:
// fruits.js
export const PEAR = "Pear";
// main.js
import { PEAR as fruit } from './fruits.js';
Rust:
mod fruits {
pub const PEAR: &str = "Pear";
}
use fruits::PEAR as fruit;
useキーワードの正体
useは、長いパスを短く書けるようにするためのキーワードです。インポートではなく、エイリアス(別名)を作る機能です。
// useなし
delicious_snacks::fruits::PEAR
// useあり
use delicious_snacks::fruits::PEAR as fruit;
// これで fruit でアクセスできる
useはファイルの先頭である必要はなく、どこにでも書けます。スコープ内で有効になります。
// ファイルの先頭
use std::collections::HashMap;
mod my_module {
// モジュールの中
use std::fmt::Display;
fn my_function() {
// 関数の中
use std::io::Write;
}
}
pub useで再エクスポート
モジュール内でuseしたものを、そのモジュールの外からも使えるようにします。
mod delicious_snacks {
// この2行を追加
pub use self::fruits::PEAR as fruit;
pub use self::veggies::CUCUMBER as veggie;
mod fruits {
pub const PEAR: &str = "Pear";
pub const APPLE: &str = "Apple";
}
mod veggies {
pub const CUCUMBER: &str = "Cucumber";
pub const CARROT: &str = "Carrot";
}
}
fn main() {
println!(
"favorite snacks: {} and {}",
delicious_snacks::fruit, // これが使えるようになる
delicious_snacks::veggie, // これも
);
}
Part 2: 所有権システムの実験
所有権リレーの実験
最初はi32のような単純な型で実験しました。
fn plus_one(x: i32) -> i32 {
x + 1
}
fn main() {
let x = plus_one(6);
let y = plus_two(x);
println!("the value y is : {}", y);
}
i32はCopyトレイトを持っているので、所有権の移動ではなくコピーが起きています。
Stringで所有権を体感する
次にStringで実験してみました。
fn taking_string_chain(x: String) -> String {
x
}
fn main() {
let x = String::from("hello");
let y = taking_string_chain(x);
let z = taking_string_chain(y);
println!("{}", z);
}
これが「所有権リレー」。各関数が所有権を受け取って、次に渡していきます。
所有権変更リレーへの挑戦
「所有権をそのままに変更地獄を作ろう」と思って試したコード:
fn taking_string_chain(x: &mut String) -> &mut String {
x
}
fn main() {
let mut x = String::from("hello");
let y = taking_string_chain(&mut (x + " me")); // エラー!
}
問題点: x + " me" は一時的な値なので、可変参照を取れません。
正しい所有権変更リレー
fn add_me(mut x: String) -> String {
x.push_str(" me");
x // 所有権を返す
}
fn add_you(mut x: String) -> String {
x.push_str(" you");
x
}
fn main() {
let x = String::from("hello");
let y = add_me(x); // xの所有権がyに移動
let z = add_you(y); // yの所有権がzに移動
println!("{}", z); // "hello me you"
}
可変参照のチェーン
所有権を移動せずに変更するパターン:
fn add_me(x: &mut String) {
x.push_str(" me");
}
fn add_you(x: &mut String) {
x.push_str(" you");
}
fn main() {
let mut x = String::from("hello");
add_me(&mut x);
add_you(&mut x);
println!("{}", x); // "hello me you"
}
返り値の有無による違い
返り値なし(その場で変更):
fn add_me(x: &mut String) {
x.push_str(" me");
}
fn main() {
let mut x = String::from("hello");
add_me(&mut x);
println!("{}", x); // "hello me"
}
返り値あり(チェーン可能):
fn add_me(x: &mut String) -> &mut String {
x.push_str(" me");
x
}
fn add_you(x: &mut String) -> &mut String {
x.push_str(" you");
x
}
fn main() {
let mut x = String::from("hello");
add_you(add_me(&mut x)); // チェーンできる!
println!("{}", x); // "hello me you"
}
Rustの所有権・参照・可変性の完全マップ
基本パターン一覧
| パターン | 所有権 | 変更可能 | 返す必要 | 用途 |
|---|---|---|---|---|
x: String | 移動 | ❌ | ⚠️ 返さないと消える | 値を受け取って消費 |
mut x: String | 移動 | ✅ | ⚠️ 返さないと消える | 値を受け取って変更して返す |
x: &String | 借用 | ❌ | 不要 | 読み取り専用 |
x: &mut String | 借用 | ✅ | 不要(返すとチェーン可) | その場で変更 |
詳細な比較表
| 所有権パターン | 可変参照パターン | 不変参照パターン | |
|---|---|---|---|
| 構文 | mut x: String | x: &mut String | x: &String |
| 所有権 | 関数が所有 | 元の変数が所有 | 元の変数が所有 |
| 変更 | ✅ できる | ✅ できる | ❌ できない |
| 返す必要 | ✅ 必須 | ❌ 任意 | ❌ 任意 |
| 返さないと | 値が消える | 何も起きない | 何も起きない |
| 返すメリット | 値を使い続けられる | チェーン可能 | チェーン可能 |
| メモリ | ヒープでもスタックでも同じ | ヒープでもスタックでも同じ | ヒープでもスタックでも同じ |
使い分けフローチャート
値を変更したい?
├─ NO → 不変参照 `x: &String`
└─ YES
└─ 関数の後も元の変数を使いたい?
├─ YES → 可変参照 `x: &mut String`
└─ NO → 所有権 `mut x: String`
よくある間違いと正解
| ❌ 間違い | ✅ 正解 | 理由 |
|---|---|---|
fn f(x: String) { x.push_str("a"); } | fn f(mut x: String) -> String | 所有権パターンは返す必要あり |
fn f(mut x: &String) | fn f(x: &mut String) | mutの位置が違う |
fn f(x: &mut String) -> String | fn f(x: &mut String) -> &mut String | 参照を返すなら型も参照 |
レイヤーで考える
┌─────────────────────────────────┐
│ 値を返すかどうか(戦術的な選択) │
│ - 返す: チェーン可能 │
│ - 返さない: シンプル │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ 所有権 vs 参照(根本的な違い) │
│ - 所有権: 値を持つ │
│ - 参照: 値を借りる │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ 可変 vs 不変(変更できるか) │
│ - mut / &mut: 変更できる │
│ - &: 読むだけ │
└─────────────────────────────────┘
学びと気づき
なぜRustのfundamentalsは面白いのか
僕にとって、Rustは「なぜそうなっているのか」を考えさせてくれます。
他の言語:
- 「こう書くんだ、はい次」
- ルールを覚えるだけ
Rust:
- 「なんでこのエラーが出るの?」
- 「所有権って何?」
- 「あ、これはメモリ安全性のためか!」
- 「モジュールの仕組み、深く理解したい!」
→ fundamentalsが楽しいパズルになる
表現方法としてのRust
多くの人にとってプログラミングは「手段」ですが、私にとってRustは「表現方法の一部」になるもんじゃないかと思っています。
楽器を演奏する人が、楽器の仕組みや音楽理論を深く学びたいのと同じように、Rustという言語を通して何かを表現したいから、深く知りたいと思っています。
Rustの設計思想—メモリ安全性を型システムで表現し、所有権を言語の文法として表現している—は、技術的な制約を美しく言語化しています。だからfundamentalsが面白いのだと思います。
学習のペース配分
一度に全部やろうとすると混乱するので、一つずつ確実にが大事と思っています。
- 70%は進める、30%は深掘りを意識したい
- 「完璧に理解してから次」じゃなく、「ある程度わかったら次に進んで、後で戻る」
- 実際にコードを書きながら学ぶと、理論と実践が繋がる
今日は返さないパターンで実験してみます。チェーンとかメソッドとかはまた今度。
まとめ
- モジュール = コードを整理するための名前空間(JavaScriptのES6モジュールと同じ概念)
- 所有権 = 値を持つ。返さないと消える
- 可変参照 = 値を借りて変更。返さなくてもOK、返すとチェーン可能
- 不変参照 = 値を借りて読むだけ
Rustのfundamentalsは深いからこそ、焦らず一歩ずつ。実験しながら進めていきます。
この記事は、Rustを学習中の実際の躓きと気づきを記録したものです。同じように学んでいる方の参考になれば幸いです。