python使用者が読むプログラミングRust 第2章 前半

38711 ワード

概要

Rustを勉強したくプログラミングRustを買ったので、コードを写経しつつ、自分が慣れているpythonでの実行結果と比較しつつ理解していく。

コードはここ

Toyプログラム

2つの整数の最大公約数を計算するプログラムを書いてみる。

fn gcd(mut n: u64, mut m: u64) -> u64{
    assert!(n !=0 && m !=0);
    while m !=0 {
        if m < n{
            let t = m;
            m = n;
            n = t;
        }
        m = m % n;
    }
    n
}

mutで可変であることを示す。数値の型は以下の通り。

型名 意味
i32 符号あり32ビット整数
u64 符号なし64ビット整数
u8 符号なし8ビット整数
f32 単精度浮動小数点
f64 倍精度浮動小数点

「!」が付くものはマクロ呼び出しであることを示す。assert!は引数が真であることを確認する。真でなければメッセージを出してプログラムを突然終了させる。このような終了はパニックと呼ばれる。
while文において、条件式を()で囲む必要はない。制御対象文は{}で囲われる。

pythonで書いたらこんな感じか

def gcd(n: int, m: int) -> int:
    assert (n != 0) & (m != 0), "n and m must be non-zero"
    while m != 0: 
        if m < n :
            t = m
            m = n
            n = t
        m = m % n
    return n

ユニットテスト

Rustではテスト機構が言語に組み込まれていて、コードに同梱する形で書くことで実現する。

#[test]
fn test_gcd() {
    assert_eq!(gcd(14, 15), 1);
    assert_eq!(gcd(2 * 3 * 5 * 11 * 17, 3 * 7 * 11 * 13 * 19), 3 * 11);
    assert_ne!(gcd(10,5,4))
}

これを先ほどのプログラムと同じファイルの末尾に書けば良い。
Cargo testを実行すれば以下のような結果が返ってくる。

cargo test
    Finished test [unoptimized + debuginfo] target(s) in 0.04s
     Running unittests (target/debug/deps/rust-67ad38ae0a14e5b1)

running 1 test
test test_gcd ... ok

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

pythonでやるなら組み込みのunittestを使うか、もしくはpytestを使うか。
自分はpytestを基本的に使うので、pytestでテストを書いてみる。

from python.main import gcd
import pytest


@pytest.mark.parametrize(
    "x, y, answer", [
        (10, 5, 5),
        (36, 4, 4)
    ]
)
def test_gcd(x, y, answer):
    assert gcd(x, y) == answer, f"gcd(10,5) should be {answer}"

@pytest.mark.parametrize(
    "x, y, answer", [
        (10, 5, 4),
        (36, 4, 9)
    ]
)
def test_gcd_error(x, y, answer):
    with pytest.raises(AssertionError):
        assert gcd(x, y) == answer, f"gcd(10,5) should be {answer}"

コマンドライン引数

コマンドライン引数をプログラムに与える方法について、下記のプログラムを実装してみる。

use std::str::FromStr;
use std::env;

fn main(){
    let mut numbers = Vec::new();
    for arg in env::args().skip(1) {
        numbers.push(u64::from_str(&arg)
            .expect("error parsing argument"));
    }
    if numbers.len() == 0 {
        println!("Usage : gcd NUMBER ...");
        std::process::exit(1);
    }

    let mut d = numbers[0];
    for m in &numbers[1..] {
        d = gcd(d, *m);
    }
    println!("The greatest common divisor of {:?} is {}", numbers, d);
}

main.rsをこう書き換える。
use宣言で標準ライブラリのトレイトFromStrをスコープに取り込む。トレイトとは型が実装できるメソッドの集合のことを指す。FromStrトレイトを実装した型は全てfrom_strというメソッドを持つことになり、これは文字列を解析してその型の値に変換しようとするメソッドである。FromStr自体はこのプログラムには出てこないが、このトレイトのメソッドであるfrom_strを使うために必要である。
2つ目のuse宣言ではstd::envモジュールを取り込んでいる。このプログラムではenvモジュールが持つargs関数を用いてコマンドライン引数を取得する。

let mut numbers = Vec::new();

で可変なローカル変数numberを宣言し、空のベクタで初期化している。Vecはサイズ可変のベクタ型で、pythonでいうlistに相当する。ただし、mutを付けなければ動的に変化させることはできない。
実際にはnumbersの型はu64つまりVec<u64>だが、後段でu64をpushしているのでそこから型推論できるので、ここでは明示的に書く必要はない。

for arg in env::args().skip(1) {

ここでコマンドライン引数を処理している。env::argsはイテレータを返すので、forループで一つ一つの引数を処理することができる。イテレータのskipメソッドは与えられた数値までのインデックスを省いたイテレータを生成する。従って、ここでは1つ目の値を省いたイテレータを生成している。

numbers.push(u64::from_str(&arg)
            .expect("error parsing argument"));

ここでu64::from_strを呼び出すことで、argに格納されたコマンドライン引数を64ビット整数にパースしている。from_strが返すものはu64そのものではなく、パースが成功したかどうかを示すResult値である。Result値は次の2つの値を取る。

  • Ok(v):パースが成功したことを示し、vは生成した値。
  • Err(e):パースが失敗したことを示し、eはその理由を説明するエラー値。

失敗する可能性のあることを行う関数は全てResult型を返すようになっている。Rustは例外処理を持たないため、エラーはResultまたはパニックで処理される。

パースが成功したかどうかのチェックにはResultのexpectメソッドを用いる。Err(e)ならばexpectはeの説明をするメッセージを出力し、プログラムの実行を中止する。Ok(v)ならばvを返す。

if numbers.len() == 0 {
        println!("Usage : gcd NUMBER ...");
        std::process::exit(1);
    }

ベクタには最低1つの要素が必要なので、そのチェックを行っている。eprint!マクロで標準エラー出力にエラーメッセージを書き出す。

let mut d = numbers[0];
    for m in &numbers[1..] {
        d = gcd(d, *m);
    }

ここで注目すべきは&numbersの&演算子と、*mの*演算子である。
これまでのコードでは整数のようなメモリ上の固定超ブロックに収まるような単純な値しか操作してこなかったが、ベクタのサイズは任意であり、とても大きなサイズになる可能性がある。
このような値を操作するには、ベクタの所有権はnumbersが持っているが、ループではその要素を借用しているだけだということをRustに教える必要がある。&numbers[1..] ではベクタの2番目以降の要素の参照を借用していることを示す。*mはmを参照解決する演算子で、参照される値を返す。

println!("The greatest common divisor of {:?} is {}", numbers, d);

最後に、結果を標準出力に書き出す。

実際に実行してみると、

cargo run 46 23
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/rust 46 23`
The greatest common divisor of [46, 23] is 23

上で借用の話が出たので、試しに借用せずにベクタを操作してみる。
つまり&numbers[1..]の&を消して実行してみる。

cargo run 46 23
   Compiling rust v0.1.0 (/Users/ippei/workspace/programming_rust/chapter2/2.4/rust)
error[E0277]: the size for values of type `[u64]` cannot be known at compilation time
  --> src/main.rs:16:14
   |
16 |     for m in numbers[1..] {
   |              ^^^^^^^^^^^^ expected an implementor of trait `IntoIterator`
   |
   = note: the trait bound `[u64]: IntoIterator` is not satisfied
   = note: required because of the requirements on the impl of `IntoIterator` for `[u64]`
help: consider borrowing here
   |
16 |     for m in &numbers[1..] {
   |              +
16 |     for m in &mut numbers[1..] {
   |              ++++

error[E0277]: `[u64]` is not an iterator
  --> src/main.rs:16:14
   |
16 |     for m in numbers[1..] {
   |              ^^^^^^^^^^^^ expected an implementor of trait `IntoIterator`
   |
   = note: the trait bound `[u64]: IntoIterator` is not satisfied
   = note: required because of the requirements on the impl of `IntoIterator` for `[u64]`
help: consider borrowing here
   |
16 |     for m in &numbers[1..] {
   |              +
16 |     for m in &mut numbers[1..] {
   |              ++++

For more information about this error, try `rustc --explain E0277`.
error: could not compile `rust` due to 2 previous errors

結果はこうなる。確かに、ベクターが不定長であることから、コンパイル時間をがわからないという旨のエラーが発生した。合わせて、[u64] is not an iteratorというエラーが発生している。
u64がイテレータではないというのはその通りだと思うが、借用しなかった場合にはVecがu64として参照されるのだろうか。4章以降で借用については説明があるようなので、後に譲る。
また、もう一つ面白いのは、&をつけて借用することも考えたら?というコンパイラからのヘルプが出ているところである。これはコードを書く側からすると非常にありがたい。
続いて、*mの*を消して、参照解決せずに実行してみる。

cargo run 46 23
   Compiling rust v0.1.0 (/Users/ippei/workspace/programming_rust/chapter2/2.4/rust)
error[E0308]: mismatched types
  --> src/main.rs:17:20
   |
17 |         d = gcd(d, m);
   |                    ^ expected `u64`, found `&u64`
   |
help: consider dereferencing the borrow
   |
17 |         d = gcd(d, *m);
   |                    +

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

この場合は参照解決していないので参照自体が渡されてしまい、gcd関数に渡す値の型としては不適としてエラーを吐いている。
ここでも参照解決するようなヘルプが出ている。

ここまでをpythonで再現すると以下の通り。

import sys

def gcd(n: int, m: int) -> int:
    assert (n != 0) & (m != 0), "n and m must be non-zero"
    while m != 0: 
        if m < n :
            t = m
            m = n
            n = t
        m = m % n
    return n

def main():
    numbers = sys.argv
    if len(numbers) == 1:
        raise Exception("No arguments")
    d = numbers[1]
    for m in numbers[2:]:
        d = gcd(int(d), int(m))
    print(f"The greatest common divisor of {numbers[1:]} is {d}")

if __name__ == '__main__':
    main()

個人的にはpythonでコマンドライン引数を与えるようなスクリプトを作るならtyperfireなどを使うだろう。