参照でライフタイム指定が必要な場合(@Rust)

Beginning Rustは言語仕様を単に羅列するのではなく、なぜそのような仕様にする必要があるかを丁寧に説明していますが、ライフサイクル指定でもそれは同じことが言えます。

そもそも関数が返すことができる参照(リファレンス)とは、以下の二つのうちのどちらかだと、

① 静的オブジェクトの借用:以下のstr関数

② 引数の借用:以下のf関数

それ以外の関数の引数オブジェクト、関数内のローカル変数オブジェクト、関数内の一時的オブジェクトは関数から戻る時に破棄されるから参照にはなり得ない、したがってライフタイム指定が必要なケース(本来は関数の戻り時点で破棄されるオブジェクトを、戻り値の参照が破棄されるまでライフタイムを延命と同義)もある程度限定的ということになる。コードとしてライフタイム表記はスッキリ感がないし、

/* It prints:
Static object
13 12*/

fn main() {
    // borrowing of the static object
    fn str<'a>() -> &'a str {
        "Static object"
    }
    println!("{}", str());

    // borrowing of the augments
    // in this case x and y need to have different life time
    fn f<'a, 'b>(x: &'a i32, y: &'b i32) -> (&'a i32, bool, &'b i32) {
        (x, true, y)
    }
    let i1 = 12;
    let i2;
    {
        let j1 = 13;
        let j2;
        let r = f(&i1, &j1);
        i2 = r.0;
        j2 = r.2;
        print!("{} ", *j2);
    }
    print!("{}", *i2);
}

関数fで複数のライフタイムが必要な理由は、関数fを展開してみれば理由がわかる、つまり{}の外側にあるi1/i2と内側にあるj1/j2では本質的なライフタイムに差があるのだから、それを一律なライフタイムで規定しようとしても不可能だということを言っているだけ。

 

admin

 

 

Unityで2DのシンプルゲームをVS(Visual Scripting)で作ってみる

Visual Scriptingは旧Boltと言われたものを、正式にUnityに取り込んだもののようですが、ゲームエンジンでこのようなスクリプティングを使うのはUnityだけではないですが、おそらく最もポピュラーなのがUnityのVSではないかと思うので、Scratchで最初に作ってみるような2Dゲームを作ってみます。

全体の画面はこんな感じで、右からTreeが移動してきてPlayerはジャンプしてそれを避ける、プレイ開始からの時間表示を行い、PlayerがFloorから落下してしまったらタイマー停止というもの。猫スプライトとサウンド(Coin)はScratchからインポートしています。

オブジェクトを動かすまでは、

https://qiita.com/K3nsuke/items/a1946fe4dac3513f012b

をそのまま使っています。

実際の動作は、

以下から、オブジェクトごとに割り付けられたスクリプト他を記載します。

<Treeの移動を操作するScript machine>

Treeがあるところまで移動(左側の見えなくなる位置)したら、乱数でスタート位置を右側に設定してやります。

 

<Playerの状態チェンジ>

state machineでFloorに設置状態とJumping状態を識別して遷移条件を記述します。

 

<遷移条件 -> Jumping, -> Grounded>

Spaceキー押されたらジャンプ状態に移行

Floorに接地したらGrounded 状態に移行

 

<Jumpingに対応するスクリプト>

Jumpingステートに入ったら、上方向に力を加えてジャンプ、同時に音を出します。

 

<時間表示のためのcanvas設定>

ElapsedTimeCtlにはElapsed変数を追加して、時間更新と停止のスクリプトをアタッチします、Elapsedは単なるラベルです。

 

 

<ElapsedTimeCtlへのScript machine>

Elapsedタイムの更新と停止、停止はPlayerがFloorから押し出されたことをカスタムイベントとして受け取って対処。

 

<scene変数の追加>

sceneの中の共通変数(GameEnd)を定義します。

 

<ゲーム終了判定するScript machine>

Playerにアタッチされて、自分自身がFloorの外に出たことを判定してカスタムイベントを発行します。

この例からもわかるように、裏側ではC#のスクリプトが動いているので、C#から独立したスクリプトブロックにはなっていなくて、多分にC#のコードブロックを意識したものとなっています。したがってVSに慣れればC#でコード書くのも実はそれほど障壁があるものではないと思います。

 

admin

 

 

構造体や列挙体などオブジェクト要素にCopy/Cloneが実装されてない型の複製(@Rust)

コピーや複製はRustでは、オブジェクトの型に依存してデフォルトで実装されているもの(プリミティブ型)もありますが、性能を重視しているので基本は移動です。

代入のセマンティクには以下の三種類がありますが、

① 共有:本体は共有してアクセスポインタは個別に持つやり方、GCを持つ言語(Java, C#, Golang)では標準のようです

② コピー:C++のデフォルト実装が該当

③ 移動:Rustの標準、ただしプリミティブ型(オブジェクトの長さが変化しないのでスタックを使用する型)は例外

Rustでプリミティブ型以外のヒープ領域に保存されるオブジェクトは標準は移動ですが、コピー(コピーすることに意味があれば、DBのコネクションとかコピーは無意味だし害があるだろう)はcloneを実装すれば良いのですが、例えば構造体などはデフォルトでcloneを適用しようとしても、対象オブジェクトのメンバーは対象に含まれないからコンパイルは通りません。

以下のコードはコンパイルエラーになりますが、理由はderiveは構造体という型に適用されるだけで、その要素であるVec型には適用されないから。

回避方法は、以下のソースでコメントアウトしているCloneのカスタマイズが必要になります。

  |     #[derive(Copy, Clone)]
  |              ^^^^
  |     struct S {
  |         x: Vec,
  |         ----------- this field does not implement `Copy`
fn main() {
    #[derive(Copy, Clone)]
    struct S {
        x: Vec<i32>,
    }
    /*impl Clone for S {
        fn clone(&self) -> Self {
            S { x: self.x.clone() }
        }
    }*/
    let mut s1 = S { x: vec![12] };
    let s2 = s1.clone();
    s1.x[0] += 1;
    print!("{} {}", s1.x[0], s2.x[0]);

    //
    // same as above(this field(x: Vec) does not implement `Copy`)
    // Object members are out of target, Copy/Clone is applied only to struct S/S1
    //

    struct S1 {
        x: Vec<i32>,
    }
    impl Copy for S1 {}
    impl Clone for S1 {
        fn clone(&self) -> Self {
            *self
        }
    }
}

この辺りについては、

https://doc.rust-lang.org/std/marker/trait.Copy.html

の解説を読むのが良いと思います。

 

admin

コンパイルのoptimizeオプション(@Rust)

Beginning Rustに出てくるコードですが、rustcに -Oというコンパイルオプションがありますが、以下のコードで-Oを指定しないと何回か実行するとその都度時間は変わってきますが、この程度の処理時間、

実行環境はM1 Mac 16GB/512GB、rustc 1.79.0です

5.14425ms 1.031666ms

ここでrustic -Oでコンパイルすると、

584.666µs 84ns

この数字はwhileループを丸々パス、全く処理しないでvdをクリアしているだけに見えます。

fn main() {
    use std::time::Instant;
    const SIZE: usize = 40_000;
    let start_time = Instant::now();
    let mut vd = std::collections::VecDeque::<usize>::new();
    for i in 0..SIZE {
        vd.push_back(i);
        vd.push_back(SIZE + i);
        vd.pop_front();
        vd.push_back(SIZE * 2 + i);
        vd.pop_front();
    }
    let t1 = start_time.elapsed();
    while vd.len() > 0 {
        vd.pop_front();
    }
    let t2 = start_time.elapsed();
    print!("{:?} {:?}", t1, t2 - t1);
}

で、コンパイラが手抜きをさせないように、毎回結果を使うように書き換えると、

    while vd.len() > 0 {
        sum += vd.pop_front().unwrap();
    }
    let t2 = start_time.elapsed();
    print!("{:?} {:?} {}", t1, t2 - t1, sum);
386.083µs 81.209µs 2933353333

こちらはまともそうに見えます。コンパイルオプションで処理時間が著しく異なるときは実はコードが冗長というケースもありそうだよねということです、

 

admin

FromとInto変換できる浮動小数と整数の精度条件(@Rust)

まあどちらもトレイトで定義されているわけで、対応する型が定義されてなければ変換できないわけですが、Begginig Rustのコードで浮動小数点に整数から変換するコードが出てきますが、テキストではi32をfloat変換するには相手先はf64でなければできないとあります。

https://doc.rust-lang.org/std/convert/trait.From.html

で該当部分を抜き出すと、

impl From<i16> for f32
impl From<i16> for f64
~
impl From<i32> for f64

i32からfloatに変換するときにはf32ではダメで、f64になるよということで無理に変換しようとするとエラーメッセージは、

the trait `From<i32>` is not implemented for `f32`

なので、そういうことです。i32の精度はf32では保持できないから変換に意味がないから用意されてないということです。

 

admin

プリミティブ型にメソッドの追加(@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

$ echo $?コマンド

Macでecho $?と打ち込むと、直前に実行したコマンドの終了コードを取得できる。

例えば、

% date
2024年 7月19日 金曜日 21時28分47秒 JST
% echo $?
0

というふうになります、ただ履歴を遡ることはできず、あくまで直前に実行したコマンドの終了コードです。

 

admin