rustを学んで[rustlings②]

·
#CLI#Rust#Terminal#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のモジュールシステムと比較すると、概念は同じです!

JavaScriptRust
exportpub
importuse
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);
}

i32Copyトレイトを持っているので、所有権の移動ではなくコピーが起きています。

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: Stringx: &mut Stringx: &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) -> Stringfn 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を学習中の実際の躓きと気づきを記録したものです。同じように学んでいる方の参考になれば幸いです。