前の記事
- 【0】 準備 ← 初回
- ...
- 【3】 可変・ループ・オーバーフロー ← 前回
- 【4】 キャスト・構造体 (たまにUFCS) ← 今回
全記事一覧
- 【0】 準備
- 【1】 構文・整数・変数
- 【2】 if・パニック・演習
- 【3】 可変・ループ・オーバーフロー
- 【4】 キャスト・構造体 (たまにUFCS)
- 【5】 バリデーション・モジュールの公開範囲 ~ → カプセル化!~
- 【6】 カプセル化の続きと所有権とセッター ~そして不変参照と可変参照!~
- 【7】 スタック・ヒープと参照のサイズ ~メモリの話~
- 【8】 デストラクタ(変数の終わり)・トレイト ~終わりと始まり~
- 【9】 Orphan rule (孤児ルール)・演算子オーバーロード・derive ~Empowerment 💪 ~
- 【10】 トレイト境界・文字列・Derefトレイト ~トレイトのアレコレ~
- 【11】 Sized トレイト・From トレイト・関連型 ~おもしろトレイトと関連型~
- 【12】 Clone・Copy・Dropトレイト ~覚えるべき主要トレイトたち~
- 【13】 トレイトまとめ・列挙型・match式 ~最強のトレイトの次は、最強の列挙型~
- 【14】 フィールド付き列挙型とOption型 ~チョクワガタ~
- 【15】 Result型 ~Rust流エラーハンドリング術~
- 【16】 Errorトレイトと外部クレート ~依存はCargo.tomlに全部お任せ!~
- 【17】 thiserror・TryFrom ~トレイトもResultも自由自在!~
- 【18】 Errorのネスト・慣例的な書き方 ~Rustらしさの目醒め~
- 【19】 配列・動的配列 ~スタックが使われる配列と、ヒープに保存できる動的配列~
- 【20】 動的配列のリサイズ・イテレータ ~またまたトレイト登場!~
- 【21】 イテレータ・ライフタイム ~ライフタイム注釈ようやく登場!~
- 【22】 コンビネータ・RPIT ~ 「
Iterator
トレイトを実装してるやつ」~ - 【23】
impl Trait
・スライス ~配列の欠片~ - 【24】 可変スライス・下書き構造体 ~構造体で状態表現~
- 【25】 インデックス・可変インデックス ~インデックスもトレイト!~
- 【26】 HashMap・順序・BTreeMap ~Rustの辞書型~
- 【27】 スレッド・'staticライフタイム ~並列処理に見るRustの恩恵~
- 【28】 リーク・スコープ付きスレッド ~ライフタイムに技あり!~
- 【29】 チャネル・参照の内部可変性 ~Rustの虎の子、mpscと
Rc<RefCell<T>>
~ - 【30】 双方向通信・リファクタリング ~返信用封筒を入れよう!~
- 【31】 上限付きチャネル・PATCH機能 ~パンクしないように制御!~
- 【32】
Send
・排他的ロック(Mutex
)・非対称排他的ロック(RwLock
) ~真打Arc<Mutex<T>>
登場~ - 【33】 チャネルなしで実装・Syncの話 ~考察回です~
- 【34】
async fn
・非同期タスク生成 ~Rustの非同期入門~ - 【35】 非同期ランタイム・Futureトレイト ~非同期のお作法~
- 【36】 ブロッキング・非同期用の実装・キャンセル ~ラストスパート!~
- 【37】 Axumでクラサバ! ~最終回~
- 【おまけ1】 Rustで勘違いしていたこと3選 🏄🌴 【100 Exercises To Learn Rust 🦀 完走記事 🏃】
- 【おまけ2】 【🙇 懺悔 🙇】Qiitanグッズ欲しさに1日に33記事投稿した話またはQiita CLIとcargo scriptを布教する的な何か
100 Exercise To Learn Rust 演習第4回になります!前半の2問はなんというか前回フライングしているというか...という感じですが、最後の1問は新章に入り構造体という大きめの話題になっています!
今回の関連ページ
では行きましょう!
[02_basic_calculator/09_saturating] 飽和演算
問題はこちらです。
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);
}
// ...省略...
}
省略なし
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);
}
}
うーん... 前回最後の問題にてフライングしちゃってたみたいですね...
本当は本問題で u32::wrapping_mul
を紹介するべきだったみたい...ただ、問題自体はこちらではなくて u32::saturating_mul
の方で実装してほしいようです。 wrapping
は一周したらまた増えていく・減っていく演算(最後の繰り上がりを無視する)になっていますが、 saturating
の方は最大値・最小値になったらそこで止まる演算(飽和演算)になっています。 本編 ではCargo.tomlへの overflow-checks
では指定できない挙動という理由で面白いとしているみたいですね。
解説
前回最後の別解の u32::wrapping_mul
を u32::saturating_mul
に置き換えるだけです。
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] キャスト
問題はこちらです。
// 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!()
を置き換えてね! - コンパイラは賢いので
255
がi8
に収まらないことを知っています。そのためエラーを出すでしょう- 意図的にオフにしてこの悪いキャストを行うことは可能です
- これをコンパイルエラーとして検出できるのはここに書かれている数字はリテラルだからです。変数の場合は動的エラーになるでしょう
- 上記の方法を使わず、
i8
での本物の値 を使って解いてね!
解説
最初の方はシンプルに置き換えるだけですね。
#[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$ ...
#[test]
fn u8_to_i8() {
#[allow(overflowing_literals)]
let x = { 255 as i8 };
let y: i8 = -1;
assert_eq!(x, y);
}
最後は真偽値から u8
へのキャスト... 🤮
#[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 のようなチケットシステムを模倣するのが題材らしく、構造体や、いよいよ本章で 所有権 の話が登場するようです...!楽しくなりそう...
さっそく最初の問題はこちらです。
// 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
を返します
解説
struct Order {
price: u32,
quantity: u32,
}
impl Order {
fn is_available(&self) -> bool {
self.quantity > 0
}
}
うーん、数行のコードに色々な要素が詰まっている...struct Order {...}
の部分で Order
という構造体を用いることができ、 impl Order {}
ブロックの中にて関数を定義することで、作成した構造体に対して is_available
というメソッドを呼び出せるようになります。
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 (少し改変)
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文法を提供しています!
struct Order {
price: u32,
quantity: u32,
}
impl Order {
fn is_available(&self) -> bool {
self.quantity > 0
}
}
前提として、 is_available
はOOPでいうメソッドである以前に構造体 Order
の 関連関数 と呼ばれるもので、 Order::is_available
というパスでアクセスできる ただの 関数になっています。よって以下のようにして呼び出すことができます(まだここまでの演習で出てきていない不変参照 &
が登場していますが、とりあえず無視してください)。
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でも単に「メソッド」と呼ばれます!
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
レシーバが潜んでいるのを発見できます!
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コードに近づけた例
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
ブロックは本章にとどまらずこの後も トレイト 等いろんな場所で登場すると思われますが、そのたびに「良くできた書き方だなぁ」と思うこと請け合いです!
では次の問題に行きましょう!
次の記事: 【5】 バリデーション・モジュールの公開範囲 ~ → カプセル化!~
-
明らかに分かりそうでも型推論が働かない時は意外とありますが、こういう場合は人間にとってもちゃんと注釈しておいた方が良かったりしますね。筆者的には程よいバランスだと思います。(慣れているだけ説はある) ↩
-
テキトー表記で厳密な数式表現じゃありません...伝われ ↩
-
一応補足しておくとUFCSの初出はNimではありません。先に挙げたリンク先を読む限りD言語でいいのかな...?こういう歴史系の話はマサカリが怖い... ↩
-
C言語なら構造体のフィールドに関数ポインタを設けることで擬似的に、JSでも構造体のフィールドに関数をもたせることで擬似的に、ぞれぞれメソッドのような書き方が可能ですが、どちらにせよクラスや構造体の方で定義する必要があり、後から拡張することは叶わないんだろうなと思っていた感じです。 ↩
-
それ以外のメリットといえばカプセル化とかでしょうか?この辺言及すると怖い人達がたくさんマサカリを投げてくるのでクラスベースOOPは正直苦手です... ↩
-
RustのものはUFCSと呼ばれたり呼ばれなかったりしています。UFCSのUは統一(Uniform/Unified)のUですが、記事執筆時点ではプログラミング言語間ではUFCSに近い概念に対して統一された名前はないように感じます...いっそのことドット記法とでも呼べばと言っていた人がいるのですが、自分もそう思います。 ↩
-
ただこの場合は直接ではなく何かしらのトレイトを
impl
する形のほうが良さそうですね。 ↩