プリミティブ型にメソッドの追加(@Rust)

プリミティブ型に、他の型にもあるだろうけれども、メソッドの追加定義は単純なimpl str/i32 ・・・のようなやり方ではできませんが、traitを介在したメソッド追加はできます。

以下Beginning Rustのコードからですが、traitを介在させることで、追加メソッドlenght()を定義しています。

fn main() {
    // "imple str" doesn't work as intended
    // primitive type doesn't allow adding some specific methods to it
    // to use tarit to adopt specific method lenght() for type "str"
    trait HasLength {
        fn length(&self) -> usize;
    }
    impl HasLength for str {
        fn length(&self) -> usize {
            self.chars().count()
        }
    }
    let s = "ۏe";
    print!("{} {}", s.len(), s.length());
}

trait経由でプリミティブ型にメソッド追加の一般的なやり方ですが、直接追加(今はできない仕様ですが、できたとしたら)と比較して可読性とか保守性が直接追加に比較して優位であるというところが理由なのだろう。

 

admin

スライスの長さをメソッド記法とドット記法で求めるときに(@Rust)

Beginning Rustの第18章の最初の方に出てくるコードですが、

fn main() {
    print!("{} {}",
        [1, 2, 3].len(),
        <[i32]>::len(&[1, 2, 3]));
}

ここで最後の2行はドット記法あるいはメソッド記法の違いですが、メソッド記法では<[i32]>::の表現が出てきます。ここで、<[i32]>の意味はスライスのジェネリック型でi32型の要素を持つこと、::はlen()の引数の型指定です。なぜこのような記法が必要かというと、len()というメソッドは他にもあって型を明示的に指定しないとどのトレイトに属するメソッドかを判断できないかから。

またlen()の引数は参照で無いとコンパイルできません。スライスのメソッドlen()の解説は以下の通り、

https://doc.rust-lang.org/std/primitive.slice.html#method.len

ここでa.len()を最後の行のように書き直してもコンパイルできます、スライスの要素はi8でもカバーされるので、

    let a = [1, 2, 3];
    assert_eq!(a.len(), 3);
    assert_eq!(<[i8]>::len(&a), 3);

ドット記法は実はこのような変換は暗黙で実行されています。それゆえ先のソースコードで、

&[1, 2, 3].len()のような冗長な記法でも問題ありません。ドット記法はRustの背後に存在するオブジェクト指向的な特性故に簡略な記法で使うことができます。

 

admin

Result型を扱う演算子?(@Rust)

Beginning Rustに以下のようなコードがありますが、

fn main() {
    fn f1(x: i32) -> Result<i32, String> {
        if x == 1 {
            Err(format!("Err. 1"))
        } else {
            Ok(x)
        }
    }
    fn f2(x: i32) -> Result<i32, String> {
        if x == 2 {
            Err(format!("Err. 2"))
        } else {
            Ok(x)
        }
    }
    fn f3(x: i32) -> Result<i32, String> {
        if x == 3 {
            Err(format!("Err. 3"))
        } else {
            Ok(x)
        }
    }
    fn f4(x: i32) -> Result<i32, String> {
        if x == 4 {
            Err(format!("Err. 4"))
        } else {
            Ok(x)
        }
    }

    fn f(x: i32) -> Result<i32, String> {
        let result1 = f1(x);
        // -----------------------------------
        // let result2 = f2(result1?); equivalent to 
        //
        // if result1.is_err() { return result1; }
        // let result2 = f2(result1.unwrap());
        // -----------------------------------
        // if result1.is_err() { return result1; }
        let result2 = f2(result1?);
        //if result2.is_err() { return result2; }
        let result3 = f3(result2?);
        //if result3.is_err() { return result3; }
        f4(result3?)
    }
    // in short
    //    fn f(x: i32) -> Result<i32, String> {
    //      f4(f3(f2(f1(x)?)?)?)
    //    }
    //
    match f(2) {
        Ok(y) => println!("{}", y),
        Err(e) => println!("Error: {}", e),
    }
    match f(4) {
        Ok(y) => println!("{}", y),
        Err(e) => println!("Error: {}", e),
    }
    match f(5) {
        Ok(y) => println!("{}", y),
        Err(e) => println!("Error: {}", e),
    }
}

元のソースでは、f(x: i32)は以下のようになっていますが、上のソースにコメント付きで変更しています。オリジナルのコードは説明のために冗長になってますが。

    fn f(x: i32) -> Result<i32, String> {
        let result1 = f1(x);
        if result1.is_err() { return result1; }
        let result2 = f2(result1.unwrap());
        if result2.is_err() { return result2; }
        let result3 = f3(result2.unwrap());
        if result3.is_err() { return result3; }
        f4(result3.unwrap())
    }

比較すると、f2(result1?)の?はResult値の処理を行うsyntax sugarで以下のif文でのエラー処理(エラーの伝搬)とエラーがなけれが値を取り出すunwrap()処理を合わせて実行するのだと、

// -----------------------------------
// let result2 = f2(result1?); equivalent to 
// 
// if result1.is_err() { return result1; } 
// let result2 = f2(result1.unwrap()); 
// -----------------------------------

テキスト中にも書かれていますが、

let ex = func();

match ex {

Ok(v) => v,

Err(_) => return ex,

}

と等価なのだと。実は?はResultのみならずOptionの戻り値処理でも使えると、当然OkはSomeでErrはNoneになりますが。

さらにmain()関数も普通の関数のようにResultを返すことができて、以下のようなコードが出てきます。

Okからば空のタプルを返却し、エラー(この場合は負の値を引数)ならばErrを返却するコードです。以下のコードではincremented(-3)でmain()が終了してしまうので、最後の2行は実行されませんが、戻り値としてのOk(())は無いとコンパイルできません。実行時にエラーが起こるかどうかはコンパイラは事前に知ることはできないので。

ここでも?演算子はErrはそのまま返していることがわかります。

/* It prints:
5
Error: "negative argument"
*/
fn main() -> Result<(), String> {
    fn incremented(n: i32) -> Result<i32, String> {
        if n < 0 { Err(format!("negative argument")) }
        else { Ok(n + 1) }
    }
    println!("{} ", incremented(4)?);
    println!("{} ", incremented(-3)?);
    println!("{} ", incremented(7)?);
    Ok(())
}

 

admin

イテレーターチェインと遅延評価(@Rust)

Beginning Rustのコードから、

以下のようなイテレーターを返す関数(イテレーターアダプタとイテレータコンシューマ)をチェインして、最後にcollectコンシューマというのは、Rustではイテレーターチェインと呼ばれる構造になりますが、同時に遅延評価(lazy evaluation)もなされます。遅延評価とはコンシューマが要求しない限りは結果を出さない、つまり使われない結果を出すというのは無駄なことだろうから。

fn main() {
    let v = [66, -8, 43, 19, 0, -31]
        .into_iter()
        .filter(|x| { print!("F{} ", x); *x > 0 })
        .map(|x| { print!("M{} ", x); x * 2 })
        .collect::<Vec<_>>();
    print!("{:?}", v);
}

実行結果は、

F66 M66 F-8 F43 M43 F19 M19 F0 F-31 [132, 86, 38]

実質ループ処理になっていて、collectコンシューマがループ処理を受け持っていることになります、このコードをfor loopで書き直すと、

fn main() {
    let mut v = vec![];
    for item in [66, -8, 43, 19, 0, -31]
        .into_iter()
        .filter(|x| { print!("F{} ", x); *x > 0 })
        .map(|x| { print!("M{} ", x); x * 2 }) {
        v.push(item);
    }
    print!("{:?}", v);
}

このコードでは、vは変更されるのでmutになってますが、先のコードでvはmutである必要はありません。なぜなら作成されるだけで、書き換えはしないから。

 

admin

Rustのedition(@Rust)

Rustもそこそこ年代を重ねてきているので、editionによる互換性問題は出てきています。Beginning Rustの以下のコードはedition=2021と指定しないとコンパイルが通りません。

$ rustc –edition=2021 main.rs (=ではなくスペースでも同じ)

Cargoを使っていれば、cargo.tomlにエディション指定(デフォルトは2021)しておけば問題ないですが

/* It prints:
11 21 31 */
fn main() {
    let array_iterator: std::array::IntoIter<i32, 3_usize>
        = [10, 20, 30].into_iter();
    for item in array_iterator {
        let j: i32 = item;
        print!("{} ", j + 1);
    }
}

ここでarray_iteratorの型は、https://doc.rust-lang.org/std/array/struct.IntoIter.html

の通りですが、ここでは説明用に型を指定しているだけで、省略してもコンパイラが解釈できます。ここで3_usizeという見慣れない表記が出てきますが、ここで’_’は見やすさのためのスペーサーで3usizeの記法でも構いません。

ここで配列がイテレーターを返せる訳は、イテレーターを返すinto_iter()関数を実装しているからです。

この辺りの仕様はeditionにより変わるようで、以下に記載があります。

https://doc.rust-jp.rs/edition-guide/rust-2021/IntoIterator-for-arrays.html

該当部分は”

  • Rust 2021 から、array.into_iter()IntoIterator::into_iter を意味するように変更されます。

従って、2021以前のeditionでコンパイルするためには以下のようなコードになります。

fn main() {
    let array_iterator: std::array::IntoIter<i32, 3_usize>
        = IntoIterator::into_iter([10, 20, 30]);
    for item in array_iterator {
        let j: i32 = item;
        print!("{} ", j + 1);
    }
}

 

admin

Unicodeのcode point(@Rust)

別にRust限定ではないですが、Rustのサンプルコードで出てきたので書き止め。

Beginning Rustに以下のようなコードがあり、

fn main() {
    fn print_codes(s: &str) {
        let mut iter = s.chars();
        loop {
            match iter.next() {
                // c as u32 generates 'code point', not UTF-8
                Some(c) => { println!("{}: {}", c, c as u32); },    
                None => { break; },
            }
        }
    }
    print_codes("ۏe");
}

結果は、

€: 8364
è: 232
e: 101

となりますが、eはUTF-8でもそのままですが、€とèは何なのかと思ったら、c as u32というのはUnicode上のコードポイントを返すだけで、UTF-8コードではないから。

https://qiita.com/omiita/items/50814037af2fd8b2b21e

このリンクを参照すると理屈はよくわかります。

 

admin

スライシングでの表現(@Rust)

Beginning Rustからのコードですが、変数というのはある型の表現のaliasというのはRustでも言えることですが、そこから以下のrange = 2..5のような定義もできます。2..5のように表現される形式の型は、

std::ops::Range

になります。

https://doc.rust-lang.org/std/ops/struct.Range.html

さらに発展系とも言えますが、&[23, 17, 12, 16, 15, 2][2..5]のような表現も順序立てて見てみると有効になることがわかります。

最後の1行はその直前の4行と等価ですが、なぜ最後の1行のような表現ができるかがわかるプロセスそのものになっています。

fn main() {
    fn min(arr: &[i32]) -> i32 {
        // Let's assume 'arr' is not empty.
        let mut minimum = arr[0];
        for i in 1..arr.len() {
            if arr[i] < minimum { minimum = arr[i]; }
        }
        minimum
    }

    let arr = [23, 17, 12, 16, 15, 2];
    let range = 2..5;                       // a variable can accepts like "2..5"
    let slice_ref = &arr[range];
    print!("{}\n", min(slice_ref));

    // &[23, 17, 12, 16, 15, 2] is as &arr
    // &[23, 17, 12, 16, 15, 2][2..5] is as &arr[range]
    print!("same as above : {}", min(&[23, 17, 12, 16, 15, 2][2..5])); 
}

蛇足ながら、なぜスライス(可変長)を引数とするときには参照でなければならないかというと、コンパイル時に長さが決まっていないといけないから、他の型でも全てそうですが。本質的に引数や戻り値はスタック上で固定長で管理されるので、可変(任意)長では管理できないからという理屈です。

上記のコード例ではarrはサイズが可変長なので参照を使い、minimumはサイズが既知で固定なので戻り値として使用できることになります。

 

admin

 

&strはポインタと長さのペアの情報を持つ(@Rust)

&strというのは単なる参照かと思ってましたが、参照ポインタと長さ情報を持つんですね。

Beginning Rustのサンプルコードを一部変更したものが以下で、M1 MacBook Airで実行した結果がコメントの数字になりますが、

fn main() {
    // all str consist of UTF-8
    use std::mem::*;
    let a: &str = "";
    let b: &str = "0123456789";
    let c: &str = "abcdè";
    print!("{} {} {}; ",
        size_of_val(&a),    // -> 16
        size_of_val(b),     // -> 10
        (c).len());         // -> 6
    print!("{} {} {}",
        size_of_val(&&a),
        size_of_val(&&b),
        size_of_val(&&c));
}

size_of_val(&a)の結果は16ということは、ポインタで8バイト、長さ情報情報で8バイト持つことになります。次のsize_of_val(b)は参照ではなく文字列そのものの長さなので10、(c).len())はsize_of_val(c)でも同じですが、6になります。

なぜなら、Rustでは文字の内部コードはUTF-8を使っていますがアクサン・グラーヴ(è)は2バイトなので、5ではなく6になります。

&strを使ってて普段意識することはそれほどないはずですが、知ってて損はない情報のように思います。

 

admin

vec!のpop()戻りはOption(@Rust)

以下のコードがBeginning Rustに出てきますが、: Option<i32>はOption<T>の説明上出て来るだけで、コンパイラが型を推測できるからなくても良い。

Option<T>の説明だからあえて冗長に指定しているだけで、実際のコードで記述することはないだろう。

fn main() {

    let mut v = vec![11, 22, 33];
    for _ in 0..5 {
        let item: Option<i32> = v.pop();
        match item {
            Some(number) => print!("{}, ", number),
            None => print!("#, "),
        }
    }
}

vec!のpop()戻りは、以下のドキュメントにある通りOption<T>になります。

https://doc.rust-lang.org/std/vec/struct.Vec.html#method.pop

i32は明示的な指定ですが、ここはベクターの要素を満足できる型ならばu32でもu8でも良い。コンパイラは最適化をするだろうから、内部的にはu8で扱っているはず、と思ったチェックしてみたら実はi32でした。64bitマシン(MacBook M1 Air)であればu8よりもi32の方が効率的なのかも知れないね。

fn main() {
    let mut v = vec![11, 22, 33];
    for _ in 0..5 {
        let item = v.pop();
        p_typename(item);
        match item {
            Some(number) => print!("{}, ", number),
            None => print!("#, "),
        }
    }

    fn p_typename<T>(_: T) {
        println!("{}", std::any::type_name::<T>());
    }
    
}

で実行すると、

core::option::Option<i32>

と返ってきたから、

 

admin

char::from_u32(@Rust)

Beginning Rustの「基本データ」の第6章に0~255までのu8形式の整数を文字表示できる整数の範囲から表示するのに、以下のようなコードが出てきますが、

fn main() {
    for n in 32..127 {
        println!("{}: [{}]", n, n as u8 as char);
    }
    for n in 160..256 {
        println!("{}: [{}]", n, n as u8 as char);
    }
}

ここで、

n as u8 as char

の代わりに、

どの数(u8以外)でも一文字に変換したいときには、char::from_u32()を使えとあって、それを使うと以下のようなコードになります。

fn main() {
    for n in 32..127 {
        println!("{}: [{}]", n, char::from_u32(n).unwrap());
    }
    for n in 160..256 {
        println!("{}: [{}]", n, n as u8 as char);
    }
}

char::from_u32()関数の解説は、

https://doc.rust-lang.org/core/primitive.char.html#method.from_u32

にあるように戻りの形はSome()なので、中身を取り出すためにunwrap()を使っています。オリジナルよりもこの方が見やすいように思います、処理速度は遅くなりそうですが。

 

admin