LoginSignup
2
0
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

🦀100 Exercises To Learn Rustに挑戦【4】 キャスト・構造体 (たまにUFCS)

Posted at

前の記事

100 Exercise To Learn Rust 演習第4回になります!前半の2問はなんというか前回フライングしているというか...という感じですが、最後の1問は新章に入り構造体という大きめの話題になっています!

今回の関連ページ

では行きましょう!

[02_basic_calculator/09_saturating] 飽和演算

問題はこちらです。

lib.rs
pub fn factorial(n: u32) -> u32 {
    let mut result = 1;
    for i in 1..=n {
        // Use saturating multiplication to stop at the maximum value of u32
        // rather than overflowing and wrapping around
        result *= i;
    }
    result
}

#[cfg(test)]
mod tests {
    use crate::factorial;

    #[test]
    fn twentieth() {
        assert_eq!(factorial(20), u32::MAX);
    }

    // ...省略...
}
省略なし
lib.rs
pub fn factorial(n: u32) -> u32 {
    let mut result = 1;
    for i in 1..=n {
        // Use saturating multiplication to stop at the maximum value of u32
        // rather than overflowing and wrapping around
        result = result.saturating_mul(i);
    }
    result
}

#[cfg(test)]
mod tests {
    use crate::factorial;

    #[test]
    fn twentieth() {
        assert_eq!(factorial(20), u32::MAX);
    }

    #[test]
    fn first() {
        assert_eq!(factorial(0), 1);
    }

    #[test]
    fn second() {
        assert_eq!(factorial(1), 1);
    }

    #[test]
    fn third() {
        assert_eq!(factorial(2), 2);
    }

    #[test]
    fn fifth() {
        assert_eq!(factorial(5), 120);
    }
}

うーん... 前回最後の問題にてフライングしちゃってたみたいですね... :sweat_smile:

本当は本問題で u32::wrapping_mul を紹介するべきだったみたい...ただ、問題自体はこちらではなくて u32::saturating_mul の方で実装してほしいようです。 wrapping は一周したらまた増えていく・減っていく演算(最後の繰り上がりを無視する)になっていますが、 saturating の方は最大値・最小値になったらそこで止まる演算(飽和演算)になっています。 本編 ではCargo.tomlへの overflow-checks では指定できない挙動という理由で面白いとしているみたいですね。

解説

前回最後の別解の u32::wrapping_mulu32::saturating_mul に置き換えるだけです。

lib.rs
pub fn factorial(n: u32) -> u32 {
    let mut result = 1u32;
    for i in 1..=n {
        result = result.saturating_mul(i);
    }
    result
}

前回別解紹介時には言っていなかった注意点としては、型推論の都合からか let mut result の段階で result の型が確定していないといけないようです1。模範解答では let mut result: u32 = 1; としていますが筆者は 1 側を 1u32 として u32 型であることを表しています。どっちでも良いので好みが出る部分でしょう。

[02_basic_calculator/10_as_casting] キャスト

問題はこちらです。

lib.rs
// TODO: based on what you learned in this section, replace `todo!()` with
//  the correct value after the conversion.

#[cfg(test)]
mod tests {

    #[test]
    fn u16_to_u32() {
        let v: u32 = todo!();
        assert_eq!(47u16 as u32, v);
    }

    #[test]
    fn u8_to_i8() {
        // The compiler is smart enough to know that the value 255 cannot fit
        // inside an i8, so it'll emit a hard error. We intentionally disable
        // this guardrail to make this (bad) conversion possible.
        // The compiler is only able to pick on this because the value is a
        // literal. If we were to use a variable, the compiler wouldn't be able to
        // catch this at compile time.
        #[allow(overflowing_literals)]
        let x = { 255 as i8 };

        // You could solve this by using exactly the same expression as above,
        // but that would defeat the purpose of the exercise. Instead, use a genuine
        // `i8` value that is equivalent to `255` when converted from `u8`.
        let y: i8 = todo!();

        assert_eq!(x, y);
    }

    #[test]
    fn bool_to_u8() {
        let v: u8 = todo!();
        assert_eq!(true as u8, v);
    }
}

型変換の問題です。本編では暗黙キャストをしてくれないRustにてどうやって明示キャストさせるかを書いています。テスト内に直書きさせるのか...

コメントが多いので要約しておきます。

  • 習った内容を使って todo!() を置き換えてね!
  • コンパイラは賢いので 255i8 に収まらないことを知っています。そのためエラーを出すでしょう
    • 意図的にオフにしてこの悪いキャストを行うことは可能です
    • これをコンパイルエラーとして検出できるのはここに書かれている数字はリテラルだからです。変数の場合は動的エラーになるでしょう
  • 上記の方法を使わず、 i8 での本物の値 を使って解いてね!

解説

最初の方はシンプルに置き換えるだけですね。

lib.rs
#[test]
fn u16_to_u32() {
    let v: u32 = 47;
    assert_eq!(47u16 as u32, v);
}

2つ目は i8 のフルビットで2の補数表現が使われ、

そして $01 + \mathrm{FF} = 01 \ 00 \equiv 0$ より $\mathrm{FF} \equiv -1$ 2 であるから $255 = \mathrm{FF} \equiv -1$ ...

lib.rs
#[test]
fn u8_to_i8() {
    #[allow(overflowing_literals)]
    let x = { 255 as i8 };

    let y: i8 = -1;

    assert_eq!(x, y);
}

最後は真偽値から u8 へのキャスト... 🤮

lib.rs
#[test]
fn bool_to_u8() {
    let v: u8 = 1;
    assert_eq!(true as u8, v);
}

なんで吐いてしまったかというと 真偽値型から整数型へのキャストをしたい場面というのはない ためです。低レイヤーでバイナリを扱いたいならRustの真偽値ではなくもっと適切な構造体なりなんなりがありそうですし、そうではなく一般用途でキャストが要るのでしたら、そもそも真偽値で表していること自体が設計誤りな気がします。

as によるキャストは使うには使いますが、数値型間でオーバーフローの心配がない場面ぐらいでしか使わないです。構造体間でキャストしたい場合は...本編で後ほど登場するらしいのでここでは触れないでおきます。が、ともかく as はあんまり型変換には使用しないです。(それよりは名前空間で use する時にエイリアスつける時の方が使われてそう)

[03_ticket_v1/01_struct] 構造体

3章では JIRA のようなチケットシステムを模倣するのが題材らしく、構造体や、いよいよ本章で 所有権 の話が登場するようです...!楽しくなりそう...

さっそく最初の問題はこちらです。

lib.rs
// Define a struct named `Order` with the following fields:
// - `price`, an unsigned integer
// - `quantity`, an unsigned integer
//
// It should also have a method named `is_available` that returns a `true` if the quantity is
// greater than 0, otherwise `false`.

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_order_is_available() {
        let order = Order {
            price: 100,
            quantity: 10,
        };
        assert!(order.is_available());
    }

    #[test]
    fn test_order_is_not_available() {
        let order = Order {
            price: 100,
            quantity: 0,
        };
        assert!(!order.is_available());
    }
}

長いですが、テスト部分も参考になるため折りたたまず全て載せました。

コメントを要約すると次のような感じです。

  • Order という名前の構造体を定義してください
    • 符号なし整数 price フィールドを持ちます
    • 符号なし整数 quantity フィールドを持ちます
  • また Order 構造体は is_available というメソッドを持ち、 quantity が0より大きければ true を、そうでなければ false を返します

解説

lib.rs
struct Order {
    price: u32,
    quantity: u32,
}

impl Order {
    fn is_available(&self) -> bool {
        self.quantity > 0
    }
}

うーん、数行のコードに色々な要素が詰まっている...struct Order {...} の部分で Order という構造体を用いることができ、 impl Order {} ブロックの中にて関数を定義することで、作成した構造体に対して is_available というメソッドを呼び出せるようになります。

Rust
let order = Order {
    price: 100,
    quantity: 10,
};

if order.is_available() {
    println!("Available order.");
}

後ほど登場する所有権によってリソース所在が明確化されるなど、筆者がRust好きな理由は色々ありますが、特にこの impl キーワード内での構造体へのメソッド定義方法も好きな理由だったりします。

いい機会ですのでちょっとオタク語りしちゃいますね...

UFCS

あれは遡ること6年前ぐらい... Nim という言語が少し流行り、 Qiita上で見かけた のをきっかけに筆者も触ってみたのですが、その時に出会ったのが UFCS (Uniform Function Call Syntax) という文法でした3

Nim basics ufcs.nim (少し改変)

Nim
proc plus(x, y: int): int =
  echo "plus(", x, ", ", y, ")"
  return x + y

proc multi(x, y: int): int =
  echo "multi(", x, ", ", y, ")"
  return x * y

echo multi(plus(1, 2), 3)

# ...?!
echo 1.plus(2).multi(3)

let
  a = 2
  b = 3
  c = 4

echo a.plus(b) == plus(a, b)
echo c.multi(a) == multi(c, a)


echo a.plus(b).multi(c)
echo c.multi(b).plus(a)

それまでメソッドの概念をクラスベースオブジェクト指向言語 (JavaやC#など。以降クラスベースOOPと呼ぶ) でしか知らなかった筆者は衝撃を受けました。

「組み込み型へ自分でメソッドを生やせるのか?!」

と。

実際はそういうわけではなく、ただの関数を定義していて、それをメソッドに呼び出しています。

書き方が異なるだけで全く同じ意味(糖衣構文)
multi(plus(1, 2), 3) == 1.plus(2).multi(3)

「クラスとして定義しないとドットによるメソッド呼び出しという(高尚な)記法は使えないんだろうな」という固定観念に囚われていたために4目から鱗をこぼさずにはいられませんでした。

メソッド記法が使えると以下のようなメリットがあります!

  • メソッドチェーンを用いて流れるように処理を書ける
  • IDEで変数に対して補完を効かせることができる

そしてUFCSのお陰で、オブジェクト指向パラダイムで最も有能な要素である5メソッド記法が、上記に示したような ただの糖衣構文である という、とてもわかりやすい形で取り入れられています。

UFCSの世界には関数定義しかなく、クラスベースOOPにあるようなインスタンスメソッド、クラスメソッド、 this キーワード、オーバーライドやスーパークラスみたいな複雑な概念と戦わなくても、メソッド記法のメリットが享受できるのです!

RustとUFCS

ここで先程のメソッド定義に戻ってみましょう。Rustも、UFCSに近い6文法を提供しています!

lib.rs
struct Order {
    price: u32,
    quantity: u32,
}

impl Order {
    fn is_available(&self) -> bool {
        self.quantity > 0
    }
}

前提として、 is_available はOOPでいうメソッドである以前に構造体 Order関連関数 と呼ばれるもので、 Order::is_available というパスでアクセスできる ただの 関数になっています。よって以下のようにして呼び出すことができます(まだここまでの演習で出てきていない不変参照 & が登場していますが、とりあえず無視してください)。

Rust
let order = Order {
    price: 100,
    quantity: 10,
};

let res = Order::is_available(&order);

fn is_available(&self) {...} の第一引数になっている &self はレシーバと呼ばれ、 self: &Order の糖衣構文になっています。また特別な意味も持っており、 self を用いて定義された関連関数こそが メソッド風に呼び出せる ようになっていて(UFCS要素)、 よってこの場合 is_available はRustでも単に「メソッド」と呼ばれます!

Rustのメソッド呼び出し
let res = order.is_available(); // Order::is_available(&order) と同じ意味!

Rustにおいて、 impl ブロックで関連関数及びメソッドを定義できる方式になっているのは、以下のような特徴・メリットがあると筆者は考えています!

  • impl ブロックとして構造体定義とは独立した部分で宣言されている
    • 構造体は構造体にすぎない (クラスではない)と認識でき、シンプルに考えられる
    • 大規模になった場合、メソッド部分だけ別な場所やファイルに切り出せる(柔軟性がある)7
  • 第一引数 self の存在
    • メソッドは 関連関数の第一引数がその構造体であるだけの関数にすぎない と認識でき、シンプルに考えられる
    • self をクラスベースOOPにおける this 変数のような複雑な存在ではなく、 その構造体型の、ただの値に過ぎないもの と認識できる
    • レシーバ self の部分は構造体本体だったり後ほど登場する不変参照だったり可変参照だったりするが、すなわち身分を明記できる
      • 逆に言えば self を使ったUFCSのような記法はRustのその他のパラダイムとも相性がいい

まとめると implブロックでメソッド定義を行うこの決まりは「 認知負荷が少ない 」記法だと言えるわけです。このシンプルさが筆者がRustを好む理由の一つになっています!

UFCSっぽい言語たち

Rustだけではなく先に挙げたNimを含め、UFCS、あるいはUFCSモドキにメソッドを定義できる言語は実は結構居たりします。筆者が認識しているものだと以下です。

  • D言語
  • Rust
  • Nim
  • Go
  • Python

そう、クラスベースOOPではありますが実はPythonもこの点で似ているんですよね...UFCSを知ってからPythonのメソッド定義を見直してみると、Rustと同様に self レシーバが潜んでいるのを発見できます!

Python
class Order:
    price: int
    quantity: int

    def __init__(self, price: int, quantity: int):
        self.price = price
        self.quantity = quantity

    def is_available(self) -> bool:
        return self.quantity > 0

order = Order(100, 10)
print(order.is_available())

# 実はPythonも以下のようにメソッドを関連関数ライクに呼べる
print(Order.is_available(order))

というかRustとほぼ同じ構造であることに気づきます...!

Rust側をさらにこのPythonコードに近づけた例
Rust
struct Order {
    price: u32,
    quantity: u32,
}

impl Order {
    fn new(price: u32, quantity: u32) -> Self {
        Self {
            price,
            quantity,
        }
    }

    fn is_available(&self) -> bool {
        self.quantity > 0
    }
}

fn main() {
    let order = Order::new(100, 10);
    
    println!("{}", order.is_available());
    
    println!("{}", Order::is_available(&order));
}

new はただの関連関数です!Rustには構造体のコンストラクタにあたる文法はなく、それにあたる関数には慣例的に new という名前をつけるという文化があります!これもシンプルさに寄与している推しポイントでしたが、盛り込むと長くなるためここに書きました。(もとい、次回以降取り扱いがありそう)

こんな感じで、メソッドを、「レシーバを明記しているただの関数である」という扱いにしている言語、最近流行ってきていると思います...!UFCS以外に適切な呼称がない一方、厳密にはUFCSではないと言われているため、筆者はこの流行りにもっと適切な名称がないかと日々モニョっております...


以上、めちゃくちゃ語ってしまいましたが、それだけこの impl ブロックに面白さがあるわけです。 impl ブロックは本章にとどまらずこの後も トレイト 等いろんな場所で登場すると思われますが、そのたびに「良くできた書き方だなぁ」と思うこと請け合いです!

では次の問題に行きましょう!

次の記事: (Comming soon...)

  1. 明らかに分かりそうでも型推論が働かない時は意外とありますが、こういう場合は人間にとってもちゃんと注釈しておいた方が良かったりしますね。筆者的には程よいバランスだと思います。(慣れているだけ説はある)

  2. テキトー表記で厳密な数式表現じゃありません...伝われ

  3. 一応補足しておくとUFCSの初出はNimではありません。先に挙げたリンク先を読む限りD言語でいいのかな...?こういう歴史系の話はマサカリが怖い...

  4. C言語なら構造体のフィールドに関数ポインタを設けることで擬似的に、JSでも構造体のフィールドに関数をもたせることで擬似的に、ぞれぞれメソッドのような書き方が可能ですが、どちらにせよクラスや構造体の方で定義する必要があり、後から拡張することは叶わないんだろうなと思っていた感じです。

  5. それ以外のメリットといえばカプセル化とかでしょうか?この辺言及すると怖い人達がたくさんマサカリを投げてくるのでクラスベースOOPは正直苦手です...

  6. RustのものはUFCSと呼ばれたり呼ばれなかったりしています。UFCSのUは統一(Uniform/Unified)のUですが、記事執筆時点ではプログラミング言語間ではUFCSに近い概念に対して統一された名前はないように感じます...いっそのことドット記法とでも呼べばと言っていた人がいるのですが、自分もそう思います。

  7. ただこの場合は直接ではなく何かしらのトレイトを impl する形のほうが良さそうですね。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0