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); }
変数x
に5
を入れた後、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_str
やspaces_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.
できなかったよ。
ブール値
false
とtrue
。
文字型
文字列型ではない。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); }
タプルは異なる型を含めることができる。
アンパックもできる。
これを実行すると未使用変数x
とz
がある、と警告が出る。
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); }
$ 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
ループにはloop
、while
、for
の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を参照。 簡単な定義なのに一般項を求めると黄金比と呼ばれる無理数が出てくる面白い数である。 なお、
書いてみた
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数列のプログラムで初めて書いた。 数値の取り扱いが面倒だなという感想であるが、きちんと調べればもっと楽な方法があるかもしれない(数値計算向けには向いていない可能性もある)。