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

Rust 100 Ex 🏃【11/37】 Sized トレイト・From トレイト・関連型 ~おもしろトレイトと関連型~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

100 Exercise To Learn Rust 演習第11回になります!今回は演習中心に軽めに済ませたかったのですが、まぁそうも言ってられなさそうですね。無理せず行きます!

今回の関連ページ

[04_traits/08_sized] Sized トレイト

問題はこちらです。今回はたまにある「コンパイルエラーが出ることを確かめたら次のエクササイズに進んでいいよ」問題のようです。

lib.rs
pub fn example() {
    // Trying to get the size of a str (or any other DST)
    // via `std::mem::size_of` will result in a compile-time error.
    //
    // TODO: Comment out the following line and move on to the next exercise.
    std::mem::size_of::<str>();
}

解説

&str&[T] は見るのに str[T] を直接扱っているものは見ないな?」という疑問に関するアンサーが今回のBookの内容です。

Rustでは、「コンパイル時にメモリ上のサイズが決まるもの」に Sized という マーカートレイト を、逆に「コンパイル時にメモリ上のサイズ不定なもの(Dynamically Sized Type, DST)」に !Sized というマーカートレイトを付けます。

「マーカートレイト」というのは、特定のメソッド等は持たないものの、 対象の型の性質を表すのに使うトレイト です。この他にも CopySyncSend 等々色々あります。

DSTな、つまり !Sized な型には str[T] の他に、 トレイトオブジェクト dyn Trait (第18回等で取り扱い)があります。彼らはコンパイル時サイズ不定なので、 size_of 関数のジェネリクスに渡すと(func::<T>()みたいにturbofish ::<> を使って渡しています。)エラーになってしまいます。ちなみに size_of に限らず全ての関数はデフォルトでジェネリクス型に Sized を要求してきます。 サイズ不定なやつを渡したい場合 ?Sized をトレイト境界に書く必要があります。

問題に戻りましょう。せっかくなのでコンパイルが通る例を記載しておきます。

lib.rs
pub fn example() {
    // std::mem::size_of::<str>();
    // std::mem::size_of::<[usize]>();

    // 次はコンパイルが通る
    std::mem::size_of::<&str>();
    std::mem::size_of::<&[usize]>();
    std::mem::size_of::<[usize; 3]>();
}

DST自体は ?Sized でも、 DSTの参照は Sized で、その大きさは普通の参照なら usize と同様(64bit OSなら8バイト)になっています。なので上記コードはコンパイルが通ります。

[04_traits/09_from] From トレイト

問題はこちらです。第9回のOrphan ruleにてコンパイルを通すために使用したニュータイプパターン(ラッパー)が本問題でも使われていますね。

lib.rs
// TODO: Implement the `From` trait for the `WrappingU32` type to make `example` compile.

pub struct WrappingU32 {
    value: u32,
}

fn example() {
    let wrapping: WrappingU32 = 42.into();
    let wrapping = WrappingU32::from(42);
}

u32 -> WrappingU32 というキャストを手軽に行えるようにするために、 impl From<u32> for WrappingU32 してほしいということらしいです。

Rustではキャストもトレイトで実現されている1わけです!

解説

実装すればよいのです。

lib.rs
impl From<u32> for WrappingU32 {
    fn from(value: u32) -> Self {
        Self { value }
    }
}

ちなみに impl From<T> for U の方を実装すると impl Into<U> for T も自動実装されます。こういう関係にあるものをDual Trait (双子トレイト...?) というらしい、便利ですね。

Bookに書いてあるように、Dual Traitの実装自体は難しくないので、試しに何か書いてみると面白いかもしれません。

Rust
trait MyFrom<T> {
    fn my_from(value: T) -> Self;
}

trait MyInto<U> {
    fn my_into(self) -> U;
}

// Dual Traitたらしめている部分
impl<T, U> MyInto<U> for T
where
    U: MyFrom<T>
{
    fn my_into(self) -> U {
        U::my_from(self)
    }
}

// 意味としては、「型Tに対して、MyFrom<T>を実装しているUについてMyInto<U>を実装する」

[04_traits/10_assoc_vs_generic]

問題はこちらです。なんかBookにめっちゃ良いこと書いてるのにそっちをあんまり意識してない問題...?!

lib.rs
// TODO: Define a new trait, `Power`, that has a method `power` that raises `self`
//  to the power of `n`.
//  The trait definition and its implementations should be enough to get
//  the tests to compile and pass.
//
// Recommendation: you may be tempted to write a generic implementation to handle
// all cases at once. However, this is fairly complicated and requires the use of
// additional crates (i.e. `num-traits`).
// Even then, it might be preferable to use a simple macro instead to avoid
// the complexity of a highly generic implementation. Check out the
// "Little book of Rust macros" (https://veykril.github.io/tlborm/) if you're
// interested in learning more about it.
// You don't have to though: it's perfectly okay to write three separate
// implementations manually. Venture further only if you're curious.

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

    #[test]
    fn test_power_u16() {
        let x: u32 = 2_u32.power(3u16);
        assert_eq!(x, 8);
    }

    #[test]
    fn test_power_u32() {
        let x: u32 = 2_u32.power(3u32);
        assert_eq!(x, 8);
    }

    #[test]
    fn test_power_ref_u32() {
        let x: u32 = 2_u32.power(&3u32);
        assert_eq!(x, 8);
    }
}

累乗のトレイト Power 💪 を実装してほしいという問題ですね。 num-traits やマクロを使ったほうが良さそうというアドバイス付きです。

解説

というわけで num::Num トレイトとマクロ両方を使って実装してみました。

lib.rs
use num::Num;
use std::collections::HashMap;
use std::hash::Hash;

fn power_inner<N>(n: N, e: u32) -> N
where
    N: Num + Hash + Eq + Clone + Copy + std::fmt::Debug,
{
    if e == 0 {
        return N::one();
    }

    if e == 1 {
        return n;
    }

    let vv = power_inner(n, e / 2);

    let v = vv * vv * if e % 2 == 0 { N::one() } else { n };

    v
}

trait Power<RHS = Self>: Num + Hash + Eq + Clone + Copy {
    fn power(self, rhs: RHS) -> Self;
}

macro_rules! power_impl {
    ( $(($t:ty, $r:ty))* ) => {$(
        impl Power<$r> for $t {
            fn power(self, rhs: $r) -> Self {
                power_inner(self, rhs.clone().into())
            }
        }
    )*}
}

power_impl!((u32, u32)(u32, u16)(u32, &u32)(u128, u32));

マクロを使う場合トレイト側のトレイト境界は不要ですが、試行錯誤の結果残していました。まぁあるとマクロのシグネチャを見た人に親切かなと思い残しています。

演習問題とは関係なく、Bookの方では「ジェネリクス」と「関連型」どちらを使うべき?という話題が繰り広げられています。本記事でもその指針に触れたら今回を終わりたいと思います。

Conclusion
To recap:

  • Use an associated type when the type must be uniquely determined for a given trait implementation.
  • Use a generic parameter when you want to allow multiple implementations of the trait for the same type, with different input types.

とあります。筆者は以下のように捉えています。

  • トレイトの自身の型以外の型指定は引数のような身分: ジェネリクスを使うべきシーン。 impl Power<U> for T は、 T × U -> 実装 という写像と捉えられる。
  • トレイトの自身の型以外の型は、トレイトの実装により決まる、つまり返り値のような身分: 関連型を使うべきシーン。 impl Deref for T とした時、 型 Target = U が一意に定まるべき

ジェネリクスと関連型は別に混在していてもよくて、その場合は上記の指針で考えれば良いんじゃないかなと思っています。

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

次の記事: 【12】 Clone・Copy・Dropトレイト ~覚えるべき主要トレイトたち~

  1. もちろんですが、as を使用したプリミティブ型のキャストのことではなく、このキャストにはトレイトが関わっていません(確か)。しかし使用頻度は from/into または try_from/try_into の方が多いんじゃないかなと思います。

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