何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

『The Rust Programming Language 2nd edition』読書記録 その3

Common Programming Concepts

『The Rust Programming Language 2nd edition』の続き。

3章はよくあるプログラミングの概念、変数、基本タイプ、関数、コメント、および制御フローについて扱う。

Variables and Mutability

Rustの変数はデフォルトでイミュータブルである。 Rustで安全かつ簡単に並列処理を書くことを奨励するRustの仕組みの一つ。 なぜRustはイミュータブルをデフォルトにしてミュータブルをオプトアウトにしているのか?

変数がイミュータブルであるとき、一度値が名前に束縛されたらその値を変更することができない。 実験してみる。

$ cargo new --bin variables

main.rsを以下の通りにする。

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

変数x5を入れた後、6を入れようとしている。

cargo runを実行すると、

$ cargo run
   Compiling variables v0.1.0 (file:///variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
3 |     println!("The value of x is: {}", x);
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

error: aborting due to previous error

error: Could not compile `variables`.

コンパイラがきちんとエラーを通知してくれるのがうれしい。 エラーが出たからといって自分が良いプログラマーではないという訳ではない! 経験豊かなRustプログラマーコンパイラでエラーを出す。 僕はPythonインタプリタでエラーを出すのであった...。

不変である値を変更しようとする状況がバグにつながる可能性があるため、不変として指定した値を変更しようとするとコンパイル時エラーが発生することが重要とRustは考えている。 あるコードでは不変な値が変更されないけれども、別のコードがその値を変更する場合、意図しない動作が生じる可能性がある。 この手のバグは追跡するのが難しい。 特に、たまに変更される場合は難しい。

Rustコンパイラは、不変な値が変更されないことを保証する。 どこで変更されるのかを把握する必要がなくなり、動作が推論しやすくなる。

とはいえ、変更できたらそれはそれで便利なので、mutキーワードでミュータブルにできる。

fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

実行する。

$ cargo run
   Compiling variables v0.1.0 (file:///variables)
    Finished dev [unoptimized + debuginfo] target(s) in 1.35 secs
     Running `target/debug/variables`
The value of x is: 5
The value of x is: 6

バグ防止だけでなく、明快さとパフォーマンスなどのトレードオフを考慮する必要がある。

Differences Between Variables and Constants

変更できない変数というと定数が思い浮かぶが、( Rustでは)いくつか異なる点がある。 定数はmutを使うことができず、常に変更不可である。 定数を定義する際はletの代わりにconstを使い、値の注釈を必ずつける必要がある。 定数はグローバルスコープを含む任意のスコープで宣言できる。 最後に、定数は関数呼び出しの結果や実行時にのみ計算できる他の値ではなく、定数式を設定できる。

fn main() {
    const MAX_POINTS: u32 = 100_000;
}

Shadowing

以前の変数と同じ名前の新しい変数を宣言することができ、新しい変数 は前の変数をシャドウする(と呼ぶ)。 同じ変数の名前を使用し、letキーワードを繰り返すことによって、変数をシャドウできる。

fn main() {
    let x = 5;
    let x = x + 1;
    let x = x * 2;

    println!("The value of x is: {}", x);
}
$ cargo run
   Compiling variables v0.1.0 (file:///variables)
    Finished dev [unoptimized + debuginfo] target(s) in 1.14 secs
     Running `target/debug/variables`
The value of x is: 12

ミュータブルな変数に代入することとは異なる。 letを使わない限り同じ変数に再び代入しようとするとエラーが生じる。 値を変換したのちに不変にすることができる。

mutシャドウイングの違いは、letキーワードをするときに新しい変数を作成するため、値のタイプを変更しつつ同じ名前を再利用できること。

fn main() {
    let spaces = "   ";
    let spaces = spaces.len();

    println!("Length of spaces: {}", spaces);
}

このようにspaces_strspaces_numといった名前を考える必要がなくspacesを再利用できる。

$ cargo run
   Compiling variables v0.1.0 (file:///variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.40 secs
     Running `target/debug/variables`
Length of spaces: 3

letの代わりにmutを使うとエラーとなる。

fn main() {
    let mut spaces = "   ";
    spaces = spaces.len();

    println!("Length of spaces: {}", spaces);
}
$ cargo run
   Compiling variables v0.1.0 (file:///variables)
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected &str, found usize
  |
  = note: expected type `&str`
             found type `usize`

error: aborting due to previous error

error: Could not compile `variables`.

To learn more, run the command again with --verbose.

では、ミュータブルだった変数を処理してイミュータブルにすると?

fn main() {
    let mut spaces = "   ";
    let spaces = spaces.len();

    println!("Length of spaces: {}", spaces);
}
cargo run
   Compiling variables v0.1.0 (file:///variables)
warning: variable does not need to be mutable
 --> src/main.rs:2:9
  |
2 |     let mut spaces = "   ";
  |         ---^^^^^^^
  |         |
  |         help: remove this `mut`
  |
  = note: #[warn(unused_mut)] on by default

    Finished dev [unoptimized + debuginfo] target(s) in 0.88 secs
     Running `target/debug/variables`
Length of spaces: 3

mutいらないよね、と警告してくれる。 Rustのコンパイラはかしこい。

Data Types

Rustは静的型付き言語であり、コンパイル時にすべての型を知る必要がある。 parseなど、複数の型が予想できる場合は型の注釈をつける必要がある。

スカラー

Rustには、整数、浮動小数点数、ブール値、および文字の4つの主なスカラ型が存在する。

整数型

8bit、16bit、32bit、64bitでそれぞれ符号つき、符号なしが存在する。 Rustのデフォルトの整数型は符号つき32bit整数i32であり、64bit環境でも速い(と主張している)。

浮動小数点数

32bitと64bitでデフォルトは64bit。 スピードはほぼ同じであるが64bitの方が精度が高いので、とある。

数値演算

よくある演算ができる。

fn main() {
    // addition
    let sum = 5 + 10;
    println!("sum: {}", sum);

    // subtraction
    let difference = 95.5 - 4.3;
    println!("diff: {}", difference);

    // multiplication
    let product = 4 * 30;
    println!("product: {}", product);

    // division
    let quotient = 56.7 / 32.2;
    println!("quotient: {}", quotient);

    // remainder
    let remainder = 43 % 5;
    println!("remainder: {}", remainder);

    // cast???
    // let cast = 4 / 2.0;
    // println!("cast: {}", cast);
}

実行結果は以下の通り。

$ cargo run
   Compiling variables v0.1.0 (file:///variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.90 secs
     Running `target/debug/variables`
sum: 15
diff: 91.2
product: 120
quotient: 1.7608695652173911
remainder: 3

では、整数型と浮動小数点数は演算ができるのか?

$ cargo run
   Compiling variables v0.1.0 (file:///variables)
error[E0277]: the trait bound `{integer}: std::ops::Div<{float}>` is not satisfied
  --> src/main.rs:23:18
   |
23 |     let cast = 4 / 2.0;
   |                  ^ no implementation for `{integer} / {float}`
   |
   = help: the trait `std::ops::Div<{float}>` is not implemented for `{integer}`

error: aborting due to previous error

error: Could not compile `variables`.

To learn more, run the command again with --verbose.

できなかったよ。

ブール値

falsetrue

文字型

文字列型ではない。8章で文字列について取り扱う。 うーん、文字列を扱うのが大変そうだ...。

複合型

タプルと配列の2つの基本的な複合型が存在する。

タプル
fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
    
    let (x, y, z) = tup;

    println!("The value of y is: {}", y);
}

タプルは異なる型を含めることができる。 アンパックもできる。 これを実行すると未使用変数xzがある、と警告が出る。

cargo run
   Compiling variables v0.1.0 (file:///variables)
warning: unused variable: `x`
 --> src/main.rs:4:10
  |
4 |     let (x, y, z) = tup;
  |          ^
  |
  = note: #[warn(unused_variables)] on by default
  = note: to avoid this warning, consider using `_x` instead

warning: unused variable: `z`
 --> src/main.rs:4:16
  |
4 |     let (x, y, z) = tup;
  |                ^
  |
  = note: to avoid this warning, consider using `_z` instead

    Finished dev [unoptimized + debuginfo] target(s) in 1.16 secs
     Running `target/debug/variables`
The value of y is: 6.4

Rustでは接尾辞に_をつけることで未使用であることをコンパイラに伝えるらしい。

タプルに位置でアクセスするにはx.0のようにする。

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

インデックスは0から始まる。最近は1から始まる言語はなさそうである。

配列

タプルとは異なり、配列の要素はすべて同じ型である必要がある。 配列の長さは固定長で、一度宣言すると、それ以降配列のサイズが拡大または縮小できない。 配列は、ヒープではなくスタックにデータを割り当てたい場合に便利らしい。スタックとヒープについては第4章。 可変長の配列みたいなものが欲しい場合は標準ライブラリのVector型を使う。詳しくは8章。

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

配列のアクセスはa[0]とする。タプルとだいぶ違うなあ。

存在しない要素へのアクセス

Pythonだと例外が送出されるが。

fn main() {
    let a = [1, 2, 3, 4, 5];
    let index = 10;

    let element = a[index];

    println!("The value of element is: {}", element);
}

コンパイル時はエラーが生じないが、実行時にエラーが生じる。 Rustでは、実際にアクセスする前にインデックスの配列とインデックスの値をチェックして存在しない要素にアクセスしようとする場合はpanic状態となる。 他の言語(C言語?)では実際にアクセスしてしまうが、Rustではアクセスすることなく止まる。 Rustの安全に対する原則の最初の例である。

How Functions Work

函数はRustのコードに広がっている(函数が広がっていないプログラミング言語ってあるのだろうか)。 函数名はsnake_caseを使う。 函数定義はfn name(){}のようにfnキーワードを使う。

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

    another_function();
}

fn another_function() {
    println!("Another function.");
}

函数の定義の順序は関係なく、定義されていればよい(同一ファイルならそうなんだろう。ファイルをまたぐとどうなるんだろう)。

Function Parameters

fn main() {
    another_function(5, 6);
}

fn another_function(x: i32, y: i32) {
    println!("The value of x is: {}", x);
    println!("The value of y is: {}", y);
}

引数はname: typeと書く。型注釈は必須。これはRustのデザインで意図されている(そうである)。 型がきっちりしている言語で触ったことがあるのがC言語Javaなので型が先にある言語に慣れているが、GoやRustは型を後に書く。 Pythonアノテーションで型を後に書く。 流行りなのだろうか。

Function Bodies

函数は複数の式で構成される。 Rustは式ベースの言語である。 文(statement)は値を返さない、式(expression)は値を返す、え、こんな雑な分け方でいいのだろうか…。

fn main() {
    let x = (let y = 6);
}

let y =6 は何も値を返さないのでコンパイルエラーとなる。

fn main() {
    let x = 5;

    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {}", y);
}

式として評価されるのは5+6函数の呼び出し、マクロ呼び出し、新しいスコープを作る{}で囲まれたブロック(!)。 上記を実行すると、

$ cargo run
   Compiling functions v0.1.0 (file:///functions)
warning: unused variable: `x`
 --> src\main.rs:2:9
  |
2 |     let x = 5;
  |         ^
  |
  = note: #[warn(unused_variables)] on by default
  = note: to avoid this warning, consider using `_x` instead

    Finished dev [unoptimized + debuginfo] target(s) in 0.75 secs
     Running `target\debug\functions.exe`
The value of y is: 4

警告は出たものの、{}ブロック内で最後の式(文ではない!)がyに代入される。 {}ブロック内の最後の行にセミコロン;がないのがポイントで、これにセミコロンをつけると文となり、

$ cargo run
   Compiling functions v0.1.0 (file:///functions)
error[E0277]: the trait bound `(): std::fmt::Display` is not satisfied
 --> src\main.rs:9:39
  |
9 |     println!("The value of y is: {}", y);
  |                                       ^ `()` cannot be formatted with the default formatter; try using `:?` instead if you are using a format string
  |
  = help: the trait `std::fmt::Display` is not implemented for `()`
  = note: required by `std::fmt::Display::fmt`

error: aborting due to previous error

error: Could not compile `functions`.

To learn more, run the command again with --verbose.

println!するために必要な何かstd::fmt::Displayを満たしていなくてエラーとなる。

ここまで式と文に気を留めたのは久しぶりである。

Functions with Return Values

函数から値を返すことができる(値を返さない函数とは。それは手続きと呼ぶ とPascalを習った僕は思うのである)。 fn name() -> i32のように-> typeで返り値の型の注釈をつける。 函数の返り値は本文のブロックの最後にある式と同じであり、returnで明示的に返すこともできるがほとんどの函数は暗黙的に最後の式を返す。 これはRubyのようだ...ちょっとすぐには慣れずにreturnと書いてしまうかもしれない。

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {}", x);
}

のように、函数five()の本文を上記のようにしても、

$ cargo run
   Compiling functions v0.1.0 (file:///functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.79 secs
     Running `target\debug\functions.exe`
The value of x is: 5

と正しくコンパイルされ、期待通り(僕はそう思っていなかった!)の結果となる。 なお、明示的にreturnをつけても問題ないが、これはRustの文化なので積極的に逆らうのはよろしくないであろう。

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {}", x);
}

fn plus_one(x: i32) -> i32 {
    x + 1;
}

のように函数の最後の式(だったもの)にセミコロンをつけて文にするとエラーとなる。

$ cargo run
   Compiling functions v0.1.0 (file:///functions)
error[E0308]: mismatched types
 --> src\main.rs:7:28
  |
7 |   fn plus_one(x: i32) -> i32 {
  |  ____________________________^
8 | |     x + 1;
  | |          - help: consider removing this semicolon
9 | | }
  | |_^ expected i32, found ()
  |
  = note: expected type `i32`
             found type `()`

error: aborting due to previous error

error: Could not compile `functions`.

To learn more, run the command again with --verbose.

内容はi32を期待していたのにタプルが返された、というエラー。 それにしても、Rustはコンパイラが丁寧にエラーを教えてくれるのでありがたい。 昔、学部で扱ったPascalは実行時エラー(無効なメモリへのアクセスなど)をしたらAbortだけでて何も役に立たなかった思い出がある。

Comments

//でコメント、以上。別の方法やドキュメンテーションコメントは14章で取り扱う。 14章はCargoに関する章なので、cargo docで自動生成されるといった類のことができるのだろう(推測)(ドキュメント読もう)

Control Flow

制御構造として条件分岐とループがある。 これはよくある構造である。

if Expressions

if式であり、if文ではないことに注意。

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

ifの使い方は想像の通り。 Rustは整数型や文字型をBool型に変換しないので、明示的にBool型にする必要がある。 Pythonの癖で、

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

とすると、

cargo run
   Compiling functions v0.1.0 (file:///functions)
error[E0308]: mismatched types
 --> src\main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected bool, found integral variable
  |
  = note: expected type `bool`
             found type `{integer}`

error: aborting due to previous error

error: Could not compile `functions`.

To learn more, run the command again with --verbose.

と型が違うと怒られる。

if式なので、

fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };

    println!("The value of number is: {}", number);
}

のようにif式の値を代入することができる。 ただし、返り値の型は同一にする必要がある。

Repetition with Loops

ループにはloopwhileforの3つが存在する。

Repeating Code with loop

loopはいわゆる無限ループでCtrl+Cで止めたり、break文で止める。

Conditional Loops with while

whileは条件つきループ、Bool式を指定してその条件がtrueの場合のみ繰り返す。

Looping Through a Collection with for

forはRustで一番使われるループである。

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a.iter() {
        println!("the value is: {}", element);
    }
}

恐らくイテレータに相当するものを渡すことで安全に(配列の外にアクセスするようなことなしに)ループを回すことができる。

fn main() {
    for number in (1..4).rev() {
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}

Summary

この章で学んだことを練習するには以下のプログラムを作ってね、という唐突な演習が入る。

このうち、数学っぽいFibonacci数に取り組んでみよう。

n番目のFibonacci数

定義などはWikipediaを参照。 簡単な定義なのに一般項を求めると黄金比と呼ばれる無理数が出てくる面白い数である。 なお、  F_{1} = 0, F_{2} = 1

書いてみた

fn fibonacci(n: u32) -> u32 {
    let mut f1: u32 = 0;
    let mut f2: u32 = 1;
    let mut sum: u32 = 0;

    if n < 2 {
        return n;
    }
    for _ in 0..n {
        sum = f1 + f2;
        f1 = f2;
        f2 = sum;
    }
    f1
}

あまりきれいに書けなかった。

ところで、Rustも再帰、できるよね?

fn fibonacci_recursive(n: u32) -> u32 {
    if n < 2 {
        return n;
    } else {
        return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2);
    }
}

おお、再帰函数もかけたぞ。 やはり(パフォーマンスを気にしなければ)再帰函数の方がきれいに、自然にかける。 数列が漸化的に定義されているからね、当然である。

さて、Fibonacci数列は一般項を計算できるので、わざわざFor文や再帰で書く必要はない。 数学の力を駆使して一発で計算しようではないか。

use std::f64;


fn fibonacci_math(n: u32) -> f64 {
    let exp = n as i32; 
    let coff: f64 = 1.0_f64 / 5.0_f64.sqrt();
    let a: f64 = (1.0_f64 + 5.0_f64.sqrt()) / 2.0_f64;
    let b: f64 = (1.0_f64 - 5.0_f64.sqrt()) / 2.0_f64;
    coff * (a.powi(exp) - b.powi(exp))
}

ここまで書いて本来の趣旨である「型や制御構造を使って書こう」というのを忘れていた。

use std::f64;
use std::io;
use std::io::Write;


fn main() {
    let mut n = String::new();
    print!("Please input positive number: ");
    io::stdout().flush().unwrap();

    io::stdin().read_line(&mut n)
        .expect("Failed to read line");

    let n: u32 = n.trim().parse()
        .expect("Failed to read line");

    let fib = fibonacci(n);
    let fib_recursive = fibonacci_recursive(n);
    let fib_math = fibonacci_math(n);
    println!("{}-th Fibonatti number: {}", n, fib);
    println!("{}-th Recursive Fibonatti number: {}", n, fib_recursive);
    println!("{}-th Mathematical Fibonatti number: {}", n, fib_math);
    
    
}

fn fibonacci(n: u32) -> u32 {
    let mut f1: u32 = 0;
    let mut f2: u32 = 1;
    let mut sum: u32 = 0;

    if n < 2 {
        return n;
    }
    for _ in 0..n {
        sum = f1 + f2;
        f1 = f2;
        f2 = sum;
    }
    f1
}

fn fibonacci_recursive(n: u32) -> u32 {
    if n < 2 {
        return n;
    } else {
        return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2);
    }
}

fn fibonacci_math(n: u32) -> f64 {
    let exp = n as i32; 
    let coff: f64 = 1.0_f64 / 5.0_f64.sqrt();
    let a: f64 = (1.0_f64 + 5.0_f64.sqrt()) / 2.0_f64;
    let b: f64 = (1.0_f64 - 5.0_f64.sqrt()) / 2.0_f64;
    coff * (a.powi(exp) - b.powi(exp))
}
$ cargo build --release
   Compiling fibonacci v0.1.0 (file:///fibonacci)
    Finished release [optimized] target(s) in 1.16 secs

$ cargo run --release
    Finished release [optimized] target(s) in 0.0 secs
     Running `target\release\fibonacci.exe`
Please input positive number: 10
10-th Fibonatti number: 55
10-th Recursive Fibonatti number: 55
10-th Mathematical Fibonatti number: 55

今までは写経に近い行為で自分で考えて書いてこなかったが、Fibonacci数列のプログラムで初めて書いた。 数値の取り扱いが面倒だなという感想であるが、きちんと調べればもっと楽な方法があるかもしれない(数値計算向けには向いていない可能性もある)。