Rust: the book

Computer

Rust が面白そうだなーと思って色々読んでみました。なんとなく概要を掴めた気がするので、自分への防備用でまとめていきます。

あくまでも個人的な観点で、「こんな概念あるんだ」て思った部分のみです。網羅的ではありません。が、公式のドキュメントだとなんだかとっつきにくいなって人の役に立ってくれれば幸いです。

いわゆる所の “the book” ってやつです。https://doc.rust-lang.org/book/

Hello World!

とりあえず、どの言語でもやるので、一通り、”Hello World!” しておきます。

実際は Rust のコンパイラのみを利用して実行ファイル作成できますが、プロジェクトの管理全般やってくれる仕組みとして “Cargo” というものがデフォルトで付随しているのでこちらを使います。

基本的には、Cargo を利用する方が一般的、みたいですね。

まずはプロジェクトの作成から。

// 新規プロジェクト作成 hello_worldって名前のディレクトリができます。
$ cargo new hello_world
// カレントディレクトリの移動  
$ cd hello_world

こんなプロジェクトが作成されます。

hello_world/
├── Cargo.toml
└── src
       └── main.rs

.toml ファイルとかありますけど、Node.js とか Golang で見かけるプロジェクトの設定ファイルみたいなものです。今は細かいこと気にせず進めます。機会があればこの辺のオフィシャルの情報とかもまとめてみます。

Cargo でプロジェクトを作成すると以下のコードが最初から入ってるので、

Filename: src/main.rs

fn main() {
    println!("Hello, world!");
}

そのままプロジェクトから実行ファイルをビルドします。

$ cargo build

大量に色んなファイル生成してくれるので、一部抜粋で。

hello_world/
├── Cargo.lock
├── Cargo.toml
├── src
│      └── main.rs
└── target
       ├── CACHEDIR.TAG
       └── debug
              ├── build
              ├── deps
              ├── hello_world  // こいつがビルドされたプログラムです。
              ├── hello_world.ds
              ︙              

Cargo からプロジェクトの実行可能ファイルを走らせることができるので、こんな感じに。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `./hello_world`
Hello, world!

まずは簡単に、基本的な流れはざっくり上みたいな感じです。

Cargo がかなり便利な役回りをしているみたいで、コードチェックやテストからドキュメントの作成までいろいろな用途で使えます。

Ownership: 所有権

さて、続いて Rust の最も特徴的な機能と言われている Ownership: 所有権 について、についてメモ書きしておきます。

まずは、オフィシャルから引用。

Ownership is Rust’s most unique feature and has deep implications for the rest of the language. It enables Rust to make memory safety guarantees without needing a garbage collector, so it’s important to understand how ownership works.

The Rust Programming Language 4.Understanding Ownership

なんとなく訳してみると、所有権は Rust の最もユニークな特徴であり、この言語の他の部分にも深みを持たせています。この機能は、メモリの安全性に対する保証をガベージコレクタなしで実現できます。そして、この所有権の働きについて理解することは重要なことです。

と、まぁこんなとこでしょうか。若干日本版の説明と違いますね。 “implications” が訳しにくいです。個人的には「この言語の他の部分にも」いろいろと影響しているってとこが重要かなと思います。 Rust でコーディングする以上は常に念頭に入れておく概念かと。

メモリ領域のヒープやらスタックやらの説明がありますが、その辺はいったん置いておいて、所有権の基本的なルールが以下です。

  1. Each value in Rust has an owner. -> それぞれの値には所有者が存在する
  2. There can only be one owner at a time. -> 1度に1つのみの所有者が存在する
  3. When the owner goes out of scope, the value will be dropped. -> 所有者がスコープを外れた時、その値は放棄される

スコープはその他の言語と大体似たようなものです。丸括弧(curly brackets)のブロックの中、みたいなイメージで概ね問題ないかと。

そして、値が “drop” されるとあるところが意外とポイントかなと思います。

Rust では変数がスコープを抜けると、その変数型に定義されている drop という関数が呼び出され、値が放棄されてメモリが解放されるようになっています。

細かい内容はこの辺に書いてあります。 15.3. Running Code on Cleanup with the Drop Trait 気が向けばこの辺りの応用的な内容もまとめてみようかなと思います。

では、気を取り直して、オフィシャルにあるサンプルコードを見てみましょう。

// String型の "hello" を変数 s1 に束縛(代入)。
let s1 = String::from("hello");
// s1 を s2 に束縛(代入)。
let s2 = s1;

// s1 に束縛されている "hello" と ", world!" をくっつけて表示しようとするけど、、、
println!("{}, world!", s1);

こんなエラーが表示されます。

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error

とりあえず大事なとこだけ、補足します。

7行目: “– move occurs because `s1` has type `String`, which does not implement the `Copy` trait” -> 変数 s1 は Copy トレイトを実装していないString型であるため、move(所有権の移動) が起きます。

上記のルール2「 1度に1つのみの所有者が存在する」とあります。つまり、変数に値を束縛したりすると、その変数がその値の所有者になりますよってことですね。

9行目: “– value moved here” -> ここで、値は(所有権が)移動しています。

そして、上記の例で s2 = s1 とした場合に、もし変数 s1 の参照が s2 にも同様にコピーされてしまった場合、s1 と s2 が同じ値を参照することになり、「所有者が2つに」なってしまいますよね。これは Rust の言語使用上は禁止されてる行為となります。

※ String型はメモリ上のヒープ領域に文字列の実態データを保持し、s1 や s2 といった変数にはその保管場所(いわゆるポインタ)が入ってます。

12行目: “^^ value borrowed here after move” -> 所有権の移動の後で、値が借用されています。

「借用」がうんちゃか、とか書いてありますが、これも Rust の特徴的な言葉の使い回しです。

(データの)所有権を借りてくる = ポインタの指示先を参照する

って解釈でとりあえずはいいんでないかなと思います。また「借用」についても後ほどまとめます。

15行目: “help: consider cloning the value if the performance cost is acceptable” -> ヘルプ:もし、パフォーマンスコストが許容できるなら、値のクローン作成を検討してみてください。

そして、17行目に clone メソッドを使った解決案が提示されています。

let s2 = s1.clone();

乱暴な言い方をするのであれば、 ”let s2 = s1;” が、「String型の “hello” というデータを所有している変数 s1 から 所有権を変数 s2 に移す」という処理なのに対して、”let s2 = s1.clone();” は、「String型の “hello” というデータ自体を変数 s1 から複製して変数 s2 に束縛する」って意味になります。

本格的に理解しようと思うと、ヒープ領域やら、スタック領域やらのそれぞれの役割とどのようなデータを保持しているのか?等々 その他の周辺知識が必要になるので、とりあえずこの記事では簡単に。

オフィシャルのリンクがここです。 4.1. What is Ownership?

References and Borrowing: 参照と借用

 それでは、次は 「参照」と「借用」についてです。「借用」は上の所有権のまとめでもちらっと出てきてますね。

オフィシャルの引用の通り、「所有権」はメモリ安全性を保証するために有効な機能ですが、反面困ったこともあります。

というのが、「関数に引数として変数を渡す際にも所有権が移動してしまう」のです。つまり、変数を関数に引き渡した後にその変数を利用することができなくなる(所有権を失ったため)ってことです。

具体的には、

Filename: src/main.rs

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(s1);

    println!("The length of '{}' is {}.", s1, len);
  // *** ここで、すでに s1 の所有権が失われているため、エラー ***
}

fn calculate_length(s: String) -> usize {
    s.len()
}

上のコードは、次のエラーを再度引き起こします。

“^^ value borrowed here after move” -> 所有権の移動の後で、値が借用されています。

そして、参照というのは、またこれも乱暴に言うと「所有権を移動させずにある変数(所有者)の値を得ること」です。

そして、オフィシャルより、

We call the action of creating a reference borrowing. As in real life, if a person owns something, you can borrow it from them. When you’re done, you have to give it back. You don’t own it.

The Rust Programming Language 4.2. References and Borrowing

なんとなく訳すと、我々は参照を作成する行為を「借用」と呼びます。実生活のように、ある人が何かを所有している場合に、あなたはそれを借りることができますが、使い終わった際にはそれを返却しなければなりません。あなたがそれを所有するわけではなく。

荒っぽくまとめます。

  • 参照:所有権の移動なしに、変数(所有者)の有する値を得ること
  • 借用:参照を作成すること

さて、ここまでの参照は、実際は「不変な」参照として扱われます。つまり、参照先のデータに変更を加えることは許可されていないってことですね。

「可変な」参照についてもさらっとみておきます。

まずは、不変な参照先のデータに変更を加えようとするとどうなるか、です。

Filename: src/main.rs

fn main() {
    let s = String::from("hello");

    change(&s);
    // 不変な参照 &s を借用
}

fn change(some_string: &String) {
    some_string.push_str(", world");
    // push_str()関数で "hello" と ", world" をくっつけたい、、、
}

こんな感じのエラーが発生。

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- help: consider changing this to be a mutable reference: `&mut String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` due to previous error

とりあえず重要なとこだけ、

3行目:”error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference” -> エラーコード[E0596] 変数 *some_string が「&(不変)」参照の裏側にある場合は、それを可変なものとして借用できません。

”*some_string” の 「*」はデリファレンスってやつですね。参照先のデータを実際にいじくるときに使われます。push_str()関数の裏側で使われてる?

要は、不変な参照である “&” の参照ではデータの変更できませんよってことですね。

9行目:”^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ some_string is a `& reference, so the data it refers to cannot be borrowed as mutable” -> 変数 「some_string」は 「&(不変)参照」なので、この変数が参照するデータは可変なものとして借用できません。

上とほぼ同じこと言ってますが、実際のエラーはここで示される行のコードで起こってます。

その他、オフィシャルでは 「dangling reference: 宙に浮いた参照」なんてのも紹介されてますが、こちらはコンパイラがうまいこと阻止してくれるので、とりあえず今回の概要では触れずにおきます。

Enum型

続いて、Enum型についてです。

とりあえず英単語的には “Enumaration: 列挙、一覧表” とかって意味です。

その名の通り、いろんなものを列挙してしまおうってやつです。オフィシャルでもStruct型との対比で概念の説明がなされています。

Where structs give you a way of grouping together related fields and data, like a Rectangle with its width and height, enums give you a way of saying a value is one of a possible set of values.

The Rust Programming Language 6.1. Defining an Enum

こちらもなんとなく、Struct型は関連するフィールドやデータをまとめる役割を提供します。例えば、幅と高さを持つ長方形ですね。そして、Enum型は、可能性のあるもののうちの一つをその値とする方法を提供します。

文頭の “Where” がとっても訳しにくいです。”whereas” みたいな意味合いで使われてますね。「〜であるのに対して」って意味です。

本筋に戻ります。Enum型ですが、個人的に Python やら JavaScript やら Golang やら触ったことありますが、初めて見ました。

ただし、どうやら Java には Enum型:列挙型があるみたいです。

適当に訳しているので、だいぶわかりづらいですが、「可能性のあるもののうちの一つをその値とする」って言うのは乱暴に言って「列挙された要素のうちのどれか1つになる」ってことです。

構造体である Struct型が必ずそれぞれの要素を持つ:Struct型の「ボールペン」であれば、例えばその要素として「色」、「重さ」、「長さ」の要素は確実に持ってますね。

対して、列挙型である Enum型は定義された要素のどれかになる:Enum型の「筆記用具」であれば、一例として、「ボールペン」、「鉛筆」、「シャーペン」なんて要素を持ってそうです。

続いて、コード上での Enum型の定義の仕方は以下です。

enum Coin {  // Enum型の Coin を定義する
    Penny,   // 1セント硬貨
    Nickel,  // 5セント硬貨
    Dime,    // 10セント硬貨
    Quarter, // 25セント硬貨
}

そして、Enum型は以下の match や、または let if 文とともに使うのが一般的です。

// 戻り値を符号なし8ビット整数、引数を Coin型 で value_in_cents()関数を定義:「値をセント表示する」関数
fn value_in_cents(coin: Coin) -> u8 {  
    match coin {              // match文 変数 coin が
        Coin::Penny => 1,     // アーム1: ペニーなら 1
        Coin::Nickel => 5,    // アーム2:ニッケルなら 5 
        Coin::Dime => 10,     // アーム3:ダイムなら 10
        Coin::Quarter => 25,  // アーム4:クォーターなら 25 をそれぞれ返す。
    }
}

match文はその他の言語の case文みたいな雰囲気があります。ただ Rust のパターンマッチングはかなり強力みたいですね。ついでに、それぞれの条件で分岐する部分は Arm と呼ばれています。

では、続いて、Rust のコードを読んでるとよく見かける Option型 なる Enum型の利用例についてメモっておきます。

This section explores a case study of Option, which is another enum defined by the standard library. The Option type encodes the very common scenario in which a value could be something or it could be nothing.

The Rust Programming Language 6.1. Defining an Enum

先の通り適当に、このセクションでは、標準ライブラリで定義されたもう1つの列挙型であるOptionのケーススタディを深掘りしていきます。 Option型は、何かしらの値がある可能性を有する場合と、何もない場合、といった非常によくあるシナリオをコードとして落とし込みます。

ここでいう何もない場合のシナリオの具体例は、他の言語でよく見る Null値とかのことです。

ただし、Rust では単純に Null値の概念を取り込むことをよしとしていません。オフィシャルドキュメントには以下の記述があります。

The problem with null values is that if you try to use a null value as a not-null value, you’ll get an error of some kind. Because this null or not-null property is pervasive, it’s extremely easy to make this kind of error.

However, the concept that null is trying to express is still a useful one: a null is a value that is currently invalid or absent for some reason.

The problem isn’t really with the concept but with the particular implementation. As such, Rust does not have nulls, but it does have an enum that can encode the concept of a value being present or absent. This enum is Option, and it is defined by the standard library as follows:

The Rust Programming Language 6.1. Defining an Enum – The Option Enum and Its Advantages Over Null Values

null値の問題は、null値を非null値として使用しようとすると、何らかのエラーが発生するということです。この null または非null の性質が広く普及しているため、この種のエラーをいとも簡単に生み出してしまいます。

ただし、nullが表現しようとしている次のような概念は依然として有用です。「nullは現在無効または何らかの理由で存在しない値である。」

問題は実際の概念ではなく、特定の実装にあります。したがって、Rust には null はありませんが、値が存在するか存在しないかの概念をコードで表現できる enum があります。この enum は Option として、標準ライブラリによって以下のように定義されています:

個人的に思った乱暴な解釈で言うなれば、Rust では「通常の型の値は当然有効であるものして扱われ、Option型の値についてのみそれが無効である可能性を含ませる。ついでにコンパイラでこの辺を管理させてる。」って感じかなと、勝手に思ってます。

Option型はエラーのハンドリングをする場合(無効な値もあるかもしらん、みたいなケース)ではよく見かけるような気がします。 この辺りはまたエラーハンドリングとか見かけるタイミングがあればつらつらと書いていきます。

さて、気を取り直して、Option型の標準ライブラリ上での定義です。

enum Option<T> {
    None,
    Some(T),
}

先に見てきた enum型の定義ですね。ただ、ここで “<T>” という部分がありますが、これはジェネリック型の引数ってものを表します。大まかには、Someバリアント( Enum型の中に定義されてる要素)が任意の方のデータを保持できますよって意味です。 詳細はまた別途。

こんな感じで、 i32型と char型で、

    let some_number = Some(5);
    let some_char = Some('e');

    let absent_number: Option<i32> = None;

ここで、変数 some_number と some_char はそれぞれ、 Option<i32> と Option<char>型となることがコンパイラによって推定されます。余計なタイプアノテーションはなくてもOK。もちろんあってもOK。

要注意なのが、Enumバリアント None に束縛される変数 absent_number です。こちらはどのような型なのかが自然には推定されないため、 Option<i32> のようにきちんとアノテーションしてあげる必要ありです。

さて、ここで問題となるのが Option<i32> は通常の i32型とは違って加減乗除などのオペレーションがそのままではできません。

この辺りはパターンマッチの処理や、Option型に付随するメソッドによって処理することになります。

Trait

続いて trait について見ていきます。英単語としては知ってましたが、プログラミングの文脈では初めて見ました。

Cambridge Dictionary の定義より

a particular characteristic that can produce a particular type of behaviour:

https://dictionary.cambridge.org/dictionary/english/trait

特定のタイプの振る舞いを生み出す特定の性質・特質 みたいな感じですかね。「固有の特性」みたいなイメージです。

オフィシャルの説明を見ていきます。

trait defines functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way. We can use trait bounds to specify that a generic type can be any type that has certain behavior.

Note: Traits are similar to a feature often called interfaces in other languages, although with some differences.

The Rust Programming Language 10.2. Traits: Defining Shared Behavior

トレイトは、特定の型が保持できたり、他の型と共有できる機能を定義します。トレイトを使用して、共有される振る舞いを抽象的な方法で定義できます。ジェネリック型が特定の振る舞いを持つ任意の型であることを指定するために、トレイト境界(トレイトのフィルタみたいなもの)を使用できます。

注意: トレイトは他の言語でよく「インタフェース」と呼ばれる機能に似ていますが、いくらかの違いがあります。

結局、Trait は「振る舞い」的なものを定義するってことです。Struct が「構造」的なものを定義するのと対照的ですね。

つまり、何かしらのデータを持つ物体やコンセプトそのものを Struct で定義して(言語で言うとこの名詞っぽい)、そしてデータに対する動作や変化を Trait で定義する。(同じく動詞)って感じかなと思います。

I had breakfast. って文があるとすると、文型的にはSVOですが、これを Rust で表現しようとすると S1TS2 みたいな感じになりそうですね。 Struct1+ Trait + Struct2 です。

では、上記を踏まえてオフィシャルのサンプルコードを見ていきます。

こちらのトレイトの目的は、「XやFacebookのポストなり、ニュースの記事なりのデータをまとめる振る舞いを定義する」ことです。なので、後々このトレイトで定義した振る舞いをそれぞれのデータ(要約される対象)に紐づけてあげることになります。

pub trait Summary {
    fn summarize(&self) -> String;
}

英単語的には、summary: 要約・概要、summarize: 要約する って意味ですね。

まずは1行目、トレイトの定義の構文から、pub: 他のモジュールに次のものを公開する、trait: トレイトの定義、Summary: トレイトの名前 って意味です。

続いて2行目、メソッドのシグネチャです。シグネチャとは、関数やメソッドの入力と出力について定義してあげるものです。Java とかだとインターフェイスって呼ばれてたと思います。具体的には、パラメータとしてどのような型を受け取り、戻り値としてどのような型を返すのか?といったことを定義してあげます。

それでは、トレイトの実装についてです。

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

1〜6行目までと、14〜19行目まで:
それぞれ NewsArticle と Tweet という Struct を定義しています。要約の対象となるデータです。

8行目:
Struct型 である NewsArticle のための Summaryトレイトを定義しています。ここがいわゆるトレイトの実装と言われる部分です。上で Trait は「動作や振る舞い」と書きましたが、実際のところ最低限必要なのは「入出力をどのようにするのか?」の定義のみで、「その入出力は別途メソッドで実装してあげる」ことになります。

それ以下、構造体 Tweet に対しても同様に(NewsAtricle用のとは中身は違う)Summary トレイトを実装してます。

そして、上で実装したトレイトは次のように呼び出されます。

use aggregator::{Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

1行目:useキーワードで、main関数のあるスコープに Summaryトレイトと Tweet構造体を持ち込みます。

3〜11行目:Tweet構造体のインスタンス化。初期化とデータ作成。

13行目:プレースホルダ “{}” の部分に、tweet.summarize()メソッドの戻り値を挿入して標準出力に。

また、トレイトの実装には「トレイトまたはそれを実装する型がローカルである必要がある。」というさ制約があります。

結局、自分が定義した型に外部のトレイトを実装するか、または、外部の型に自分が定義したトレイトを実装するか、しておかないとコントロールができなくなるからですね。この制約は「coherence: 一貫性」やもっと具体的には「orphan rule: 孤児ルール」って呼ばれてます。

例えば、Displayトレイトを Vec<T>型に実装することを考えると、トレイトと型のどちらも標準ライブラリに定義されていて、どうなるかわかんない、というか外部で定義されたものを別の外部の定義で上書きするかも知らんので、コントロールしようがないってことですね。乱暴な解釈をすると。

従って、トレイトまたは型のいずれかはローカルなスコープに定義してあげて、自分でコントロール可能な状態にしてあげる必要があります。

その他、メソッドの引数や戻り値の型に特定のトレイトが実装されていることを指定するトレイト境界なんて構文もあったりしますが、こちらは今回は省略します。

Lifetime

それでは、続いてライフタイムについてです。こちらは参照と密接な関係がありますが、言語の機能としてはある種のジェネリックとして扱われます。

Lifetimes are another kind of generic that we’ve already been using. Rather than ensuring that a type has the behavior we want, lifetimes ensure that references are valid as long as we need them to be.

The Rust Programming Language 10.3. Validating References with Lifetimes: Validating References with Lifetimes

ライフタイムは、私たちが既に使用している別の種類のジェネリックです。私たちが望む振る舞いを型が有していることを保証するのではなく(これはトレイト境界のことですね)、ライフタイムは私たちが必要な期間は参照が有効であることを確保します。

Rust において、すべての参照はライフタイムを持ちます。そして、ライフタイムとはその参照が有効な範囲、換言するとスコープのことです。

ただし、基本的には Rust の処理側でライフタイムは明示的には示されずコンパイラによって自動的に推定されます。もちろん、明示的に指定することも可能です。

Rust 以外のほとんどの言語では、ライフタイムという概念は使用されないため、多くのプログラマにとって不慣れなものなんですね。なるほど確かに聞いたこともなかったです。

それでは、オフィシャルよりライフタイムのサンプルです。

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

ライフタイムのアノテーションは 「シングルクォテーション + ライフタイム名」と定義します。 ‘a とか ‘b とか、’static とかです。

まず注目すべきは、変数r のライフタイム ‘a は2行目から10行目までとなっています。そして、変数x のライフタイム ‘b は5行目から7行目までです。

次に、5行目では「i32型のデータに束縛されている変数x の参照が」変数r に束縛されていますが、変数x のスコープは7行目で終わってしまい、変数r の 「変数xが元々示していたメモリ上への参照」だけが残ってしまいます。いわゆるダングリング(宙に浮いた)参照ってやつです。

乱暴にまとめます、参照とは実態データがあって初めて成り立つもので、「そもそもデータがない場所、または、データが無効・削除された場所」への参照はなんの意味もないですね。

つまり、実態データに関するスコープよりも参照へのスコープの方が小さくなくては(その中に含まれていなければ)いけないわけです。

以下で、ライフタイムの問題が解決された例も見ておきます。

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          //   |       |
}                         // --+-------+

(実体データの変数x のライフタイム ‘b)>(その参照へのライフタイム ‘a)

となっていますね。

では、続いて、ライフタイムのアノテーションが必要となるケースについてみていきます。下のコードはlongest()関数を呼び出して2つの文字列スライスのうちより長いものを result変数に束縛して、最終的に標準出力へ受け渡してます。

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

こちらをコンパイルしようとすると以下のエラーが発生。

   Compiling playground v0.0.1 (/playground)
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `playground` (bin "playground") due to previous error

6行目:
“^ expected named lifetime parameter” -> 名前付きライフタイムパラメータが期待される場所です。

要するに、 「’a」 とか 「’b」とかで名前付きのライフタイムアノテーションしてね、ってことですね。

11行目:
“9 | fn longest<‘a>(x: &‘a str, y: &‘a str) -> &‘a str {“

ここでこうしたらどう?って提案が示されてます。 「’a」 が追加されてますね。

あまりライフタイムについて正確に理解しているとは言い難いのでこれは主観的な考えですが。

ライフタイムアノテーションを行っていない以下の関数において、文字列スライスの参照をパラメータとして受け取り、文字列スライスの参照を戻り値として返すわけですが、受け取った参照と返す参照が同一のものであるとコンパイラレベルでは判断できないのかな? と思います。

仮に、関数内で “z” のような変数を宣言して、そこに束縛した値への参照 &z を返すような関数の構成になっていた場合に、ダングリング参照が発生するが、参照ルールと借用チェッカーではそこまで管理できないので人が明示してあげる? ってことかなと。

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

完全に理解したとは言い難いので、また実験的なスニペットとか走らせてみて細かい理解詰めていきたいです。とりあえずは簡単に、ライフタイムについてはここまで。

Rust Tests

In his 1972 essay “The Humble Programmer,” Edsger W. Dijkstra said that “Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.” That doesn’t mean we shouldn’t try to test as much as we can!

The Rust Programming Language 11. Writing Automated Tests

また大雑把に訳すと、

1972年のエッセイ「The Humble Programmer」で、Edsger W. Dijkstra(計算機科学者)は「プログラムのテストは、バグの存在を示す非常に効果的な方法であるかもしれないが、それらの不存在を示すには全くもって不十分です。」と述べました。これは、私たちができる限りのテストを行う必要はない、ということを意味するものではありません。

ぐらいの意味ですね。いきなりなかなの含蓄のある金言です。確かにそうだなぁ、と。

そして、基本的な自動テストの処理は以下の通りです。

  1. Set up any needed data or state. :
    必要なデータや状態を初期設定
  2. Run the code you want to test. :
    テストしたいコードを走らせる
  3. Assert the results are what you expect. :
    結果を期待する値と突き合わせる

Rust におけるテストとは端的に言って「testアトリビュートでアノテーションされた関数」のことです。
※ここでアトリビュートは関数に付与できるメタデータのことです。具体的には “#[test]” となります。

何はともあれ、とりあえず具体的なコードをみていきます。

まずは、ライブラリとしてプロジェクトを新規作成。

$ cargo new adder --lib
     Created library `adder` project
$ cd adder

とりあえず、プロジェクトルートから src/lib.rs を編集。デフォルトで色々コードが入ってるので、

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

とする。今回はライブラリプロジェクトなので main() 関数は不要。

日本語訳しておきます。

1行目:”#[cfg(test)]” はコンパイラ用のフラグです。説明略

2行目: テスト用のモジュールを宣言

3行目: ”#[test]” でテスト時に実行する関数としてアノテーション

4〜7行目: テスト時に実行される関数

6行目: “assert_eq!(result, 4);” で result と 整数の4 が同じであることをチェック
assert は英単語だと「断定する・主張する」 とかそんな意味ですね。 eq は “equal” です。

$ cargo test

とすれば、 “#[test]” でアノテーションされた関数、今回は it_works() がテスト関数として走ります。

結果として、以下のようなものが標準出力されます。

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.57s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

細かい説明は省略します。 “passed” と “failed” のとこの数字だけ最初は見ておけば大丈夫です。

なお、テストの結果は2つのパートに分かれています。

  • 6〜9行目のコードテスト
  • 11〜15行目のドキュメントテスト

一応、テストが “failed” となる場合も確認。以下のコードでテストを実行します。

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}

cargo からテストを走らせると、

exploration() については 2 + 2 = 4 なので当然 “passed”

another() については panic! が使われているので “failed” となります。

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.72s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok

failures:

---- tests::another stdout ----
thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::another

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

テストで利用できるアサーションのマクロには、assert_eq!()の他にも、
assert_ne!(): Not Equal のことです。
assert!(): Boolean値をとります。

基本的には assert!() マクロは引数の真偽値について評価をするのみです。 引数の値を表示してくれるため、assert_eq!() か assert_ne!() を使った方が便利です。

また、上記のマクロはそれぞれ、オプションでfailed時の表示文字列を引数として取ることができます。

pub fn greeting(_name: &str) -> String {
    format!("Hello !")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(
            result.contains("Carol"),
            "Greeting did not contain name, value was {}",
            // あいさつ文には名前が含まれておらず、その値はうんちゃか(resultの値)です。
            result
        );
    }
}

required な引数(ここでは第1引数のresult.contains(“Caorl”))以外のものは format!()マクロに引き渡されます。 丸括弧 “{}” をプレースホルダーとするやつですね。

こんな感じの出力になります。

$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished test [unoptimized + debuginfo] target(s) in 0.93s
     Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
Greeting did not contain name, value was `Hello !`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

13行目が引数として指定してあげた文字列となっています。コード上の変数 result の値はこの場合 “Hello !” ですね。

その他のテスト手法として、#[should_panic] アトリビュートでテスト関数をアノテーションしてあげる方法や、Result<T, E> ジェネリクスを使う方法もあります。

さらには、特定のテストのみ走らせたり無視したり、ユニットテストや統合テストを組成したり等々あります。

が、長々となってしまうので、概要はこの辺までにしておきます。後ほどこの辺りも詳しくまとめたいです。

Packages, Crates, and Modules

続いて、Rust のコードをまとめていく機構についてです。 Rust にも当然、コードの分類と階層を織りなす機能が存在します。

  • Packages: A Cargo feature that lets you build, test, and share crates
    -> ビルドしたりテストしたり、クレートを共有するような Cargo の機能?特徴?
  • Crates: A tree of modules that produces a library or executable
    -> ライブラリや実行可能ファイルを成すモジュールの階層構造
  • Modules and use: Let you control the organization, scope, and privacy of paths
    -> 全体の構成やスコープ、パスの非公開性をコントロールする
  • Paths: A way of naming an item, such as a struct, function, or module
    -> 構造体、関数やモジュールなどの要素を(スコープ内で)名前付けする方法

なかなかに訳しにくいです。今のところ、 Packages > Crates > Modules って大小関係なイメージです。そして、特定の要素を利用する際に use キーワードを使って Paths を指定してあげる。

Crate の説明は以下のようになっています。

crate is the smallest amount of code that the Rust compiler considers at a time. 
-> クレートとは、Rust コンパイラが1度に解釈するコードの最小量のこと。

Crates can contain modules, and the modules may be defined in other files that get compiled with the crate, 
-> クレートはモジュールを含むことができ、モジュールは、クレートと一緒にコンパイルされる別のファイル内にも定義することができます。

とりあえずクレートは「まずこのファイルはコンパイラに認識されて処理が始まるよ」ってファイルを指すと思っておけば良いかと思います。 cargo new で新しく作られるプロジェクトにあるファイルとかですね。

A crate can come in one of two forms: a binary crate or a library crate. 
-> クレートは、二つのうちどちらかの形式となります。バイナリクレートまたはライブラリクレートです。

バイナリクレートはいわゆる実行可能なプログラム(のソースコード)ですね。Webサーバやコマンドラインツールなんかもこれに含まれます。

対してライブラリクレートはその名の通りライブラリクレートで、実行可能なプログラムのコードを書くときに利用されたり、Rust クレートのエコシステムである crates.io で共有されたりします。他の言語で言うとこのライブラリって思ってもほぼ同義です。

The crate root is a source file that the Rust compiler starts from and makes up the root module of your crate
-> クレートルートは、コンパイラの処理の起点となり、ルートモジュールを構成する元となるようなソースファイルのこと。

個人的なイメージですが、クレートはコンパイラから見た時のソースコードの構成単位(ファイルの構成寄り)であって、モジュールはプログラマから見た時のソースコードの構成単位(コード内の構成寄り)なのかなと思ってます。

package is a bundle of one or more crates that provides a set of functionality. A package contains a Cargo.toml file that describes how to build those crates.
-> パッケージは、1式の機能を提供する一つまたはそれ以上のクレートのかたまりです。パッケージは1つの Cargo.toml ファイルを含み、そこにはどのようにクレートをビルドするのかといった情報が記述されます。

A package can contain as many binary crates as you like, but at most only one library crate. A package must contain at least one crate, whether that’s a library or binary crate.
-> パッケージには必要なだけの数のバイナリクレートを含むことができますが、ライブラリクレートは最大でも1つ飲みです。パッケージには最低でも一つのクレートが必要で、これはライブラリクレートかバイナリクレートのいずれでも良いです。

では、実際に Cargo を使って、どのようにパッケージ、クレート、モジュールが構成されるのか見てみます。

まずは適当なディレクトリで cargo new で新しいプロジェクトを作成します。

$ cargo new my-project
     Created binary (application) `my-project` package

こんな感じでファイルが生成されます。

my-project/
├── Cargo.toml
└── src
       └── main.rs

オフィシャルにはこのように説明があります。

Here, we have a package that only contains src/main.rs, meaning it only contains a binary crate named my-project. If a package contains src/main.rs and src/lib.rs, it has two crates: a binary and a library, both with the same name as the package. A package can have multiple binary crates by placing files in the src/bin directory: each file will be a separate binary crate.
-> このように、src/main.rs のみを含むパッケージとなります。これは、my-project と言う名前のバイナリクレートを一つだけ含むという意味合いです。もし仮に、パッケージが src/main.rs 及び src/lib.rc を含む場合は二つのクレートを含むことになります。(共にパッケージと同じ名前を有するバイナリクレートとライブラリクレート)また、ファイルを src/bin ディレクトリに含めることで、複数のバイナリクレートを含むこともできます。(それらのファイルが別個のバイナリクレートということです)

とまぁ、色々と書いてありますが、とりあえずはパッケージの名前とクレート(ルート)の名前が一致する との解釈で問題ないです。

処理の流れやキーワードの概要は以下、

  • コンパイラがクレートルートから処理を開始する。(だいたい、 src/main.rs か src/lib.rs )
  • クレートルート内にモジュールが宣言されていればそのモジュールを探す。たとえば、”mod garden;”
    • クレートルート内で丸括弧の中にインラインで定義されてるとこ、mod garden{ … } のような感じ。
    • ファイル src/garden.rs の中
    • ファイル src/garden/mod.rs の中
  • モジュール内にサブモジュールが宣言されていればそのサブモジュールを探す。たとえば、”mod vegetables;”
    • モジュール内で丸括弧の中にインラインで定義されてるとこ、mod vegetables{ … } のような感じ。
    • ファイル src/garden/vegetables.rs の中
    • ファイル src/garden/vegetables/mod.rs の中
  • サブモジュール以下、続く
  • モジュール内の要素は基本的にはプライベート(外部に公開されていない)ため、公開したい要素、たとえばモジュールであれば “pub mod vegetables;” のように宣言する。
  • “use” キーワードを使うことによって、そのスコープ内に要素へのショートカットを宣言できる。”use crate::garden::vegetables::Asparagus;” とすればそれ以降は “Asparagus” とすればフルパスを使う必要なし。

それでは、実際にクレートとモジュールを用いた構成を見ておきます。

$ cargo new backyard
$ mkdir backyard/src/garden
$ touch backyard/src/garden.rs backyard/src/garden/vegetables.rs

として、中に色々とファイルを作ります。こんな感じの構成になります。

backyard/
├── Cargo.toml
└── src
       ├── garden
       │       └── vegetables.rs
       ├── garden.rs
       └── main.rs

コンパイルの際に、まずコンパイラはクレートルートとなる src/main.rs から処理を開始します。

Filename: src/main.rs

use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {
    let plant = Asparagus {};
    println!("I'm growing {:?}!", plant);
}

1行目で “use” キーワードを使うことによって、6行目で “”Asparagus” をショートカットとして使うことができます。

そして、3行目の “pub mod garden;” によって、コンパイラは、garden モジュールを探しに行きます。この場合はインラインで定義されていないので、クレート内の所定のファイルですね。具体的には、 src/garden.rs のことです。これに以下のようなコードが含まれているとします。

Filename: src/garden.rs

pub mod vegetables;

上記同様に、今度は vegetables サブモジュールを探します。具体的には、 src/garden/vegetables.rs です。ファイルの中身が以下だとして、

Filename: src/garden/vegetables.rs

#[derive(Debug)]
pub struct Asparagus {}

cargo run すると以下が出力されます。

$ cargo run
I'm growing Asparagus!

きちんと Asparagus 構造体が利用できてますね。

ちなみに、#[derive(Debug)] は derive アトリビュートといって、よく使われるトレイトを簡単に derive: 派生させる ことができるものです。 Debug トレイトを派生させて、デバッグ用の振る舞い Asparagus 構造体に追加しています。

あとはうまいことパッケージ、クレート、モジュールの仕組みを組み合わせて、ソースコードを機能ごとに分類したり階層構造を持たせたりすることによって保守性や可読性を向上させることができます。この辺は Rust  以外のプログラミング言語でも普通に行われてることですね。

オフィシャルのドキュメントには他にも便利な使い方の説明がありますが、今回の概要はこの辺までで。

Smart Pointers

続いて、スマートポインタについてです。Python とか JavaScript、あとは Golang にもなかった機能なので、個人的にまだあんまり理解できてない感があります。

調べてみたところ C++ にはあったりするみたいですね。では。

ポインターはその名の通り、アドレスによってメモリを “point at:指し示す” 機能です。Rust において、最も一般的なものは “&” による「参照」です。そして、ポインタは、アドレス上のデータを示す以上の機能を特に何も持ってないです。と、ここまでは 納得です。

一方で、スマートポインタは、アドレスの参照以外にも「メタデータや特別な機能を有する」とあります。

特に Rust では、所有権や借用といった概念を有するため、特別な差異がスマートポインタと借用との間に存在します。 多くの場合、参照はデータを「借用」し、スマートポインタはデータを「所有」します。

たとえば、Reference counting スマートポインタは、所有者(実際は参照)をカウントすることで、複数の所有者によるデータのアクセスを管理できます。

String や Vec<T> も実はスマートポインタで、メモリを確保し、そのデータを操作することを可能にしてます。同時に、メタデータや特別な機能を有してます。

たとえば、String では capacity というメタデータや、「データが常にUTF-8であることを保証する」機能を持ちます。

スマートポインタの根底にあるものは構造体ですが、通常の構造体とは違い、スマートポインタは Deref や Drop といった特別なトレイトを実装しています。

Deref: デリファレンスは、リファレンス(参照)の逆の操作で、参照アドレスではなく実際のデータにアクセスすることです。Deref トレイトを実装することで、そのスマートポインタ構造体は参照のような振る舞いを得ることができます。

Drop は、英単語的には「落とす」という意味ですが、「省略」や「取り除く」という意味でも使われます。スマートポインタの文脈では、後者です。これは、スマートポインタのインスタンスがスコープから外れた際に実行される振る舞いを定義します。

一番わかりやすい、というより一番よく見かけるスマートポインタは Box<T> という型です。これは、データ本体をスタック領域ではなく、ヒープ領域に保持させます。そして、そのデータを有するヒープ領域のアドレスをスタックに積む、といった塩梅です。

主な用途は以下、

  • When you have a type whose size can’t be known at compile time and you want to use a value of that type in a context that requires an exact size
    -> コンパイル時にサイズのわからない型の値を、正確なサイズが必要となる文脈で使用したい場合。
  • When you have a large amount of data and you want to transfer ownership but ensure the data won’t be copied when you do so
    -> 巨大な量のデータを扱う際、所有権は移動させたいが、そのデータ自体がコピーされないようにしたいとき。
  • When you want to own a value and you care only that it’s a type that implements a particular trait rather than being of a specific type
    -> ある値を扱いたいとき、その値が特定のトレイトを実装していることのみを配慮したいとき。(特定の型であること、ではなく)

1つ目が再起的なデータ構造ですね。2つ目は単純にデータの読み書きのオーバーヘッドを削減したいとき、3つ目が、トレイトオブジェクトと呼ばれるものです。(これは別途まとめるかもです)

とりあえずサンプルコードを見ておきます。

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

さて、ここで、変数 b は、ヒープ領域に保持された “5” というデータへの Box(スマートポインタ)に束縛されます。なお、スマートポインタ自体はスタック領域に保持されます。

そして、変数 b がスコープを4行目で外れた際に、Drop トレイトのメソッドが実行され、スタック上のスマートポインタと、ヒープ上の実体データへのメモリの割り当てが解除されます。

次に、再帰型と呼ばれるデータ構造を見て行きます。

ざっくりいうと、「ある型の中にその型があってその中にさらにその型があって…」って感じの構造です。ここで、Rust の有する型の一種 Enum型でこれを実装することを考えます。

この場合、「ある型」を “List” 、その列挙子(バリアント)に再起データとして”Con(i32, List)”、終端データとして “Nil” としたとき以下のような形になります。

enum List {
    Cons(i32, List),
    Nil,
}

が、これは以下のようなコンパイルエラーとなります。

error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

For more information about this error, try `rustc --explain E0072`.
error: could not compile `cons-list` due to previous error

なぜか? エラーメッセージを日本語訳して行きます。

1行目:recursive type `List` has infinite size
-> 再帰型の List は無限のサイズです。

7行目:—- recursive without indirection
-> 間接的な型を持たない再帰です

9行目:insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
-> 間接的な型を使って、循環的な再帰を防止してみてください

一番重要なのは、1行目です。要するにコンパイルの段階で型のサイズが不明、というよりもいくらでも、なんなら無限に大きく解釈してしまうためです。

上で述べた、「ある型の中にその型があってその中にさらにその型があって…」ってやつが無限に続きます。

エラーメッセージにある処置をすることにより、

enum List {
    Cons(i32, Box<List>),
    Nil,
}

上記のように、再起的な型のサイズ把握が破られます。

Cons(i32, Box<List>) 型は、「i32型と、次のリストへのアドレス(usize型)」を有するだけです。何度も何度も List 自身が同じ型(同じメモリの領域)に入れ子になっていないですね。

と、概要はこんな感じです。実際には、Box<T> の他にも Rc<T> や RefCell<T> などがありますが、こちらはもっと掘り下げた内容で個別にまとめます。

最後に

ざーっとまとめてみましたが、なかなか、書ききれてない部分が多いです。

オブジェクトトレイトとか並列処理とか Cargo の使い方の細かい部分とか、まだまだ重要な要素はありますが、どう考えても一つの記事に入り切るような分量ではないのと、今回は個人的に気になったトピックをかいつまんでまとめただけなので。

ともあれ、これから Rust を触ってみようと思ってる人の参考になれば幸いです。

それでは次の記事で。

コメント

タイトルとURLをコピーしました