🧊

100 Exercises To Learn Rust をやった

Welcome - 100 Exercises To Learn Rust https://rust-exercises.com/100-exercises/

ここまでRustをなんとなく書いてきたけど、今がそういう時期かなと思ったので。

まあ全部ではなく、気になるところやあまり自信のなかったところを中心にやってみた。

overflow-check

Overflow and underflow - 100 Exercises To Learn Rust https://rust-exercises.com/100-exercises/02_basic_calculator/08_overflow.html#overflow-check

前にハマったやつなので覚えてた。

Rustの整数オーバーフローは、デフォルトではdebugビルドでのみパニックする | Memory ice cubes https://leaysgur.github.io/posts/2024/08/28/133918/

Operator traits

Operator overloading - 100 Exercises To Learn Rust https://rust-exercises.com/100-exercises/04_traits/03_operator_overloading.html

(Partial)Eq(Partial)Ordstd::ops::Addとかstd::opts::Subとかあのあたり。

それぞれ==<などのオペレーターに対応してて、実装してあれば使える。というか本当にどこもかしこもtraitで成り立ってるな。

何気なく使ってたassert_eq!()も、内部的にはleft == rightしてるだけで、そのためにはEqが必要というのも納得。

あとDerive macroの便利さを改めて実感した。フィールドの多いstructでいちいち実装してらんないもんね。

Trait bounds

Trait bounds - 100 Exercises To Learn Rust https://rust-exercises.com/100-exercises/04_traits/05_trait_bounds.html#trait-bounds-1

個人的な鬼門だったやつ。

この2つが同義ってことにも最初は馴染めなかったけど、やっとわかってきた気がする。

fn print_if_even<T>(n: T) where T: IsEven + Debug {}

fn print_if_even<T: IsEven + Debug>(n: T) {}

もう常にwhereで書いてくれ・・・!って気持ちになってた。

FromInto

From trait - 100 Exercises To Learn Rust https://rust-exercises.com/100-exercises/04_traits/09_from.html#into

Fromがあればinto()できる!

asを書く前にまずinto()してみる!

まあそれでも、純粋に型変換をやる関数を用意するべきか、Fromでやるべきかは永遠にわからない。

ちなみに、TryFromTryIntoも、どっちかがあればどっちもできるようになる、Dual traitってやつらしい。

Generic trait

Associated vs generic types - 100 Exercises To Learn Rust https://rust-exercises.com/100-exercises/04_traits/10_assoc_vs_generic.html#output

これも学びだった。

impl Add<&u32> for &u32 {
    type Output = u32; // 👀

    fn add(self, rhs: &u32) -> Self::Output { // 👀
    }
}

なぜこの実装で、-> Selfではなく、type Outputを経由するのか。

このtrait-> Selfで定義しちゃうと、結果も&u32になってしまい、&1 + &2の結果まで参照の&u32になってしまう。それでいい場合もあるかもしれないが、実体のu32のほうが取り回しやすいのは間違いない。

あとは、このクッションがあるおかげで、まったく関係ない異なる型を返したいときにも使える、と。

ちなみに、こう書いてもコンパイラに怒られないのは、元々のtraitではSelf::Outputを返すように定義されていて、たまたま直接書いてる-> u32と型が一致してるから。 もちろん-> 他の型にしちゃうとエラーになる。

impl Add<&u32> for &u32 {
    type Output = u32;

    fn add(self, rhs: &u32) -> u32 { // 👀
    }
}

なるほどな。

Drop trait

Drop trait - 100 Exercises To Learn Rust https://rust-exercises.com/100-exercises/04_traits/13_drop.html

JS/TSでいう[Symbol.dispose]ね!

そしてDropCopyは両立できない、と。

Lifetime elision

Lifetimes - 100 Exercises To Learn Rust https://rust-exercises.com/100-exercises/06_ticket_management/06_lifetimes#lifetime-elision

'_でライフタイムの明示を省略できるやつ。

今まではコンパイラに「そこは明示しなくていいよ」って言われたときだけ、'_を書いてた。

なので、どういう時に書ける?とかはあまり考えてこなかった。

impl<'_> IntoIterator for &'_ TicketStore { // ただ型を書くところなので、場所としてはOK(中で使えないから結局NGだが)
    // ここは型それ自体の定義なのでNG
    type Item = &'_ Ticket;
    type IntoIter = std::slice::Iter<'_, Ticket>;
}

型定義時には使えなくて、型を利用する時は使える。 こんな初歩的なことがわかっていなかったとは・・・ってなった。

RPIT

impl Trait - 100 Exercises To Learn Rust https://rust-exercises.com/100-exercises/06_ticket_management/08_impl_trait#rpit

-> impl Traitのこと、“Return Position Impl Trait”っていうらしい。

RPIT lifetime capture rules - The Rust Edition Guide https://doc.rust-lang.org/edition-guide/rust-2024/rpit-lifetime-capture.html

そして引数版の、“Argument Position Impl Trait”ってのもあるらしい。

そして引数の場合は、APITよりも、ジェネリックな型パラメーターを使う方がおすすめとのこと。

// これよりも
pub fn add_ticket(&mut self, ticket: impl Into<Ticket>) { }

// こっちがおすすめ
pub fn add_ticket<T>(&mut self, ticket: T) where T: Into<Ticket> { }

これは、呼び出し側がadd_ticket::<>()で好きな型を指定できるかららしい。

この::<>という書き方は、魚っぽいからTurbofish syntaxというらしい。想像力があるな!

impl<K, V> HashMap<K, V> where K: Eq + Hash

HashMap - 100 Exercises To Learn Rust https://rust-exercises.com/100-exercises/06_ticket_management/15_hashmap.html#key-requirements

何気なく使ってたHashMapも、traitで成り立ってるんだなということを改めて実感した。

言われてみれば実装としても、キーがHashEq(そしてPartialEq)できないと比較できないもんね。

やはりtraitはRustの核なのだなあ。

Threads ‘static

‘static lifetime - 100 Exercises To Learn Rust https://rust-exercises.com/100-exercises/07_threads/02_static#static-lifetime

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T + Send + 'static,
    T: Send + 'static
{ }
  • Sendじゃないとスレッドをまたがって共有できない
  • 'staticじゃないものは、いつ終わるかわからないスレッドとしては認めるわけにはいかない
    • moveするか、ローカルで定義するなど

traitを改めて理解した今なら、この定義がスッと読める!これは嬉しい。

Futures

Runtime - 100 Exercises To Learn Rust https://rust-exercises.com/100-exercises/08_futures/03_runtime.html

この章も学びは多かったけど、結局tokioの使い方という話になるのだろうか。

Rustとしてasync / .awaitのシンタックスは存在するけど、そのランタイムは実装されてないのは驚きだった。 用途に応じてタスクをどう捌くか?を委ねられるというのは、いろんなシーンで使われる言語としては妥当なのか・・・な?

Futureになった非同期関数は、.awaitするかそれ相当の処理でpollされるまで実行されないってのは、JSのawaitとは違って慣れるのが大変そう。

まだこのあたりは仕様もまだ完全ではないらしいので、またいずれおさらいする予定で。