構造体で遊ぶ
この記事を書いた理由って必要ですか。
特に構造体推しでもないし、ネタ切れではあるけど、少しまとめとこと思っただけで理由って必要ですか。
(オリ曲のサビから抽出)
本日の一本
曲ストックが切れたので、次は動画でも共有しようかと思った。
これ本当草
(twitter の引用 RT かよ)
まず普通に定義する
classDiagram class Point { - usize x - usize y }
この UML の通りに構造体を定義してみます
struct Point { x: usize, y: usize, } fn main() { let p = Point { x: 3, y: 6 }; println!("{}", p); }
実行すりゃ破るけど、これだと普通にエラーになるわけで。
こいつは表示機能を持ってないからね。もしくは、println で表示する免許を持ってない。
Point 構造体に(とりあえず)Debug
トレイトを実装しよう。
note
これを書いている頃の日記さんは、trait を免許とたとえてもいいんじゃないかという困ったどうでもいい考えを持ってます。
tip
実行すれば、コンパイルエラーと表示されるはずです。 (error: could not compile...
)
コンパイルエラーは実行ファイルが作られないで発生するエラーです。 よって、コンパイルエラーが含まれているコードからはプログラムが発生しない。
コンパイルエラーは安全なのです。 一方、実行時に発生するエラーは安全ではないエラーです。
rust の安全性の一つは、コンパイルエラーが豊富なことだと感じます。(あくまで感想)
エラーが含まれるコードは、コンパイルの時点で弾いてくれるのでね。
Debug を定義する
classDiagram direction TB class Point { -usize x -usize y } class Debug { - std::fmt::Result fmt() } <<Trait>> Debug Point --|> Debug
多分こんな感じ
#[derive(Debug)] struct Point { x: usize, y: usize, } fn main() { let p = Point { x: 3, y: 6 }; println!("{:?}", p); }
derive マクロによってほぼ自動的に実装してもらいました。
println!
の中身で、{}
が{:?}
に変わっていることに注意が必要です。
tip
#[]
は手続きマクロと呼ばれるものですね。
この記事が詳しいと思います。
一方て、println!()
と関数名の後ろに!
がついているのは宣言マクロです。
可変長引数に対応する関数が作れます。
note
せっかくなので出力例を、おっと。
この mdbook というやつはコードブロックそのまま実行可能だったんだ。 ▶️ ボタンで実行可能。
文字列変換に対応させ、好きなフォーマットで出力できるようにする
classDiagram direction TB class Point { -usize x -usize y } class Debug { - std::fmt::Result fmt() } <<Trait>> Debug class Display { - std::fmt::Result fmt() } <<Trait>> Display Point --|> Debug Point --|> Display
use std::fmt; #[derive(Debug)] struct Point { x: usize, y: usize, } impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}, {}", self.x, self.y) } } fn main() { let p = Point { x: 3, y: 6 }; println!("{}", p); assert_eq!(p.to_string(), String::from("3, 6")); }
Debug と異なり、Display はフォーマットを自分で作りたいので手動です。
Display はto_string()
も間接的に実装してくれます ToString トレイトもありますが、Display を使っておけば両方に対応するのです。
write!
マクロは println と使い心地が似てますが、先頭に Formatter を指定する必要があります。
ユーザーに自由な値を提供する
usize
以外にも、u8
でメモリを節約する、i32
でプラマイに対応する、f64
で小数点に対応させる、&str
で文字列を扱うようにするなど、さまざまなユースケースが考えられます。
classDiagram direction TB class Point["Point < T >"] { -T x -T y } class Debug { - std::fmt::Result fmt() } <<Trait>> Debug class Display { - std::fmt::Result fmt() } <<Trait>> Display Point --|> Debug Point --|> Display
use std::fmt; #[derive(Debug)] struct Point<T> { x: T, y: T, } impl<T> fmt::Display for Point<T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}, {}", self.x, self.y) } } fn main() { let p = Point { x: 3, y: 6 }; println!("{}", p); assert_eq!(p.to_string(), String::from("3, 6")); }
残念ながらこれでは、コンパイルエラーになります。
問題点は「Display」です。 T が Display(もしくは ToString)を実装しているという確証がないのです。
T が特定のトレイトを実装している時のみ、メソッドの利用を許容する
rust のトレイトは、面白い機能があります。 メンバー変数が実装しているトレイトによって、使用できるメソッドを変化させられるのです!
use std::fmt; #[derive(Debug)] struct Point<T> { x: T, y: T, } impl<T> fmt::Display for Point<T> where T: fmt::Display, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}, {}", self.x, self.y) } } fn main() { let p = Point { x: 3, y: 6 }; println!("{}", p); assert_eq!(p.to_string(), String::from("3, 6")); }
構造体Point
は、型に制限なく作ることができます。 Display トレイトを実装してなくても、実装できます。
一方で、Point を Display に対応させるためには、型 T に Display トレイトを実装している必要があります。
足し算引き算できるようにする。
数学的なのはおいといて、x と y それぞれ足し引きできるようにします。 Add
トレイト、およびSub
トレイトです。
use std::{ fmt::{self, Pointer}, ops::{Add, Sub}, }; #[derive(Debug, PartialEq, Clone, Copy)] struct Point<T> { x: T, y: T, } impl<T> fmt::Display for Point<T> where T: fmt::Display, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}, {}", self.x, self.y) } } impl<T> Add for Point<T> where T: Add<Output = T>, { type Output = Point<T>; fn add(self, rhs: Self) -> Self::Output { let x = self.x + rhs.x; let y = self.y + rhs.y; Point { x, y } } } impl<T> Sub for Point<T> where T: Sub<Output = T>, { type Output = Point<T>; fn sub(self, rhs: Self) -> Self::Output { let x = self.x - rhs.x; let y = self.y - rhs.y; Point { x, y } } } fn main() { let p = Point { x: 3, y: 6 }; println!("{}", p); assert_eq!(p.to_string(), String::from("3, 6")); assert_eq!(p + p, Point { x: 6, y: 12 }) }
type は型に別名を与えるという役割を持ってます。型に名前をつけると、コメント以上に可読性が上がります。
オリジナルな型のように見えます。 構造体、enum の仲間みたいな。見えるだけでなく、可視性もそんな感じになったはず。
/// 戻り値は、kg単位で返却されます
fn latest_weight(id: usize) -> i32;
/// 戻り値は、cm単位で返却されます
fn latest_length(id: usize) -> i32;
type Kg = i32;
type Cm = i32;
fn latest_weight(id: usize) -> Kg;
fn latest_length(id: usize) -> Cm;
もう一つは、トレイト定義の自由度を高める使い方があります。
Add
、Sub
などの計算系トレイトの戻り値は、Output(出力)の方を自由に調整できます。
impl<T> Sub for Point<T>
where
T: Sub<Output = T> + Copy,
{
type Output = Point<T>;
}
型 T 自体にも、T を Output とする Sub トレイトを実装している必要があり、
自身もPoint<T>
を Output とする Sub トレイトを実装しています。
note
ついでに、Clone, Copy, PartialEq を実装しております
- Clone
構造体の複製を行う clone メソッドを追加する - Copy
デフォルトが move なのを、clone に置き換える
Copy まで実装しておくと楽です。
参照ではない時の値が move から copy に変わるため、所有権が奪われる心配が減ります。
一方で、インスタンスが増えてメモリを圧迫する、同じインスタンスだと思ったら別物だったなんてトラブルもつきます。
数値i32
などでは、Copy が実装されています。
- PartialEq
値の比較ができるようになる。
参照で計算できるように工夫する
計算する度にコピーよりか、参照の方がいいと思った。でも中身で結局コピーしちゃうんだよね。
use std::{ fmt::{self}, ops::{Add, Sub}, }; #[derive(Debug, PartialEq, Clone)] struct Point<T> { x: T, y: T, } impl<T> fmt::Display for Point<T> where T: fmt::Display, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}, {}", self.x, self.y) } } impl<T> Add for &Point<T> where T: Add<Output = T> + Clone, { type Output = Point<T>; fn add(self, rhs: Self) -> Self::Output { let x = self.x.clone() + rhs.x.clone(); let y = self.y.clone() + rhs.y.clone(); Point { x, y } } } impl<T> Sub for &Point<T> where T: Sub<Output = T> + Clone, { type Output = Point<T>; fn sub(self, rhs: Self) -> Self::Output { let x = self.x.clone() - rhs.x.clone(); let y = self.y.clone() - rhs.y.clone(); Point { x, y } } } fn main() { let p = Point { x: 3, y: 6 }; println!("{}", p); assert_eq!(p.to_string(), String::from("3, 6")); assert_eq!(&p + &p, Point { x: 6, y: 12 }) }
Copy の方が楽ですね。
変換に対応させる
タプルから変換できると楽そうですよね。
use std::{ fmt::{self}, ops::{Add, Sub}, }; #[derive(Debug, PartialEq, Clone, Copy)] struct Point<T> { x: T, y: T, } impl<T> fmt::Display for Point<T> where T: fmt::Display, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}, {}", self.x, self.y) } } impl<T> Add for Point<T> where T: Add<Output = T>, { type Output = Point<T>; fn add(self, rhs: Self) -> Self::Output { let x = self.x + rhs.x; let y = self.y + rhs.y; Point { x, y } } } impl<T> Sub for Point<T> where T: Sub<Output = T>, { type Output = Point<T>; fn sub(self, rhs: Self) -> Self::Output { let x = self.x - rhs.x; let y = self.y - rhs.y; Point { x, y } } } impl<T> From<(T, T)> for Point<T> { fn from(value: (T, T)) -> Self { Point { x: value.0, y: value.1, } } } impl<T> From<Point<T>> for (T, T) { fn from(value: Point<T>) -> Self { (value.x, value.y) } } fn main() { let x: (i32, i32) = (2, 3); let x: Point<i32> = Point::from(x); let (x, y): (i32, i32) = x.into(); }
Fromトレイトを実装すると、変換が可能になります。 今回はタプルから Point へ、Point からタプルへ変換できるようにしました。
main 関数では、相互変換を実装しています。
2 行目は From の使い方です。 from はいわゆる静的メソッドであり、new メソッドのように新たなインスタンスを作ることを目的としています。
つまり、変換先のインスタンスは別物です。 そりゃそう。
一方で、3 行目は into メソッドによってタプルに変換されています。インスタンスについているメソッドですね。
From トレイトを実装すると、自動的に実装されます。
note
Into は From の逆だと聞いて、相互変換と考えてしまった時期がありました。
実際には、静的メソッドかただのメソットか、その差です。
相互変換に対応させるために、お互いに From トレイトを適用しています。
これによって相互変換ができるようになるのです。
into 使う時は、型注釈が必要です。
まとめ
オリ曲の存在を証明するには、私の頭を解剖するしかない。