はじめに
この記事は下記イベントでの私の発表を、スライドだと、コードやコンパイラのエラーメッセージ、リンクのテキストが取得しづらかったり、当日、時間が足りなくて省略した部分も色々とあったため、別途、文字に書き起こしたものになります。
「コンセプトから理解するRust」という本
初期のLearning Curveがキツめと言われているRust、私も七転八倒しながら、あれこれとキャッチアップをしている中で、この本は、めちゃくちゃ見事な構成と言葉で説明してくれているな〜と、感銘を受けながら読みましたし、表紙の「エラーメッセージをよく読み、所有権の感覚をつかみ、豊富な型に精通し、トレイトの実体を捉えられれば、Rustはもう怖くない」は至言ではないかと感じます。
ここでは、「第5章 Rustの抽象化プログラミング」のサンプルコードを元に、"エラーメッセージをよく読む"、"型に精通する"、"トレイトの実体を捉える"といった辺りのイメージの紹介を出来ればと思います。
導入
この本、ほんと素敵だな〜と感動しながら、本を読み進めていった、「第5章 Rustの抽象化プログラミング」。
絶対値を求める関数として、このようなサンプルコードが提示されていました。
trait IAbs {
type Output;
fn iabs(self) -> <Self as IAbs2>::Output;
}
impl IAbs for i32 {
type Output = u32;
fn iabs(self) -> <Self as IAbs>::Output {
if self >= 0 {
self as <Self as IAbs>::Output
} else {
-self as <Self as IAbs>::Output
}
}
}
そして、以下のような説明があり、
さて、IAbsトレイトをほかの整数型であるi8、i16、i64型に実装するときには、i32型に対して作成した実装をそれぞれについて作ることになります。関連型Outputが異なるだけで、どの型もほぼ同じ見た目のコードを複数回繰り返して書くことになります。(中略) コードの見た目がどの型でも同じだからといって、i32型のiabs()の実装を、そのままトレイトのiabs()のデフォルト実装にしてもコンパイルはできません。コンパイルできるようにしたの修正したものが次のものになります。
修正版として、このようなサンプルコードが提示されていました。
trait IAbs {
type Output;
fn iabs(self) -> <Self as IAbs>::Output
where
Self: Sized + PartialOrd + Neg + From<i8> + TryInto<<Self as IAbs>::Output>,
<Self as IAbs>::Output: TryFrom<<Self as Neg>::Output>,
<Self as TryInto<<Self as IAbs>::Output>>::Error: Debug,
<<Self as IAbs>::Output as TryFrom<<Self as Neg>::Output>>::Error: Debug,
{
if self >= 0_i8.into() {
self.try_into().unwrap()
} else {
(-self).try_into().unwrap()
}
}
}
impl IAbs for i32 {
type Output = u32;
}
後続の文章で、各トレイト境界の説明はされているし、これらのトレイト境界の定義、個別には何となく見たことある気がするけど、全部を自分では書ける気しないな〜、、、
where
Self: Sized + PartialOrd + Neg + From<i8> + TryInto<<Self as IAbs>::Output>,
<Self as IAbs>::Output: TryFrom<<Self as Neg>::Output>,
<Self as TryInto<<Self as IAbs>::Output>>::Error: Debug,
<<Self as IAbs>::Output as TryFrom<<Self as Neg>::Output>>::Error: Debug,
Rustを分かりやすく説明するのはこれが限度なのか、、とちょっとモヤっていたら、
以下の文章がありました。
筆者も一発でこのコードを書いたわけではありません。VS Codeにインラインで出力されるエラー情報、コンパイルをした時のエラー情報、公式ページのそれぞれの型やトレイとの情報を見ながら、必要なトレイト境界を追加し、このコードを作りました。
本当かどうか、やってみよう!!ということで、やってみた結果がこのLTの発表のネタとなっています。
やってみる
まずは、Traitのデフォルト実装にそのまま持ってきてみます。
trait IAbs {
type Output;
fn iabs(self) -> <Self as IAbs>::Output {
if self >= 0 {
self as <Self as IAbs>::Output
} else {
-self as <Self as IAbs>::Output
}
}
}
Sized Trait
こんなコンパイルエラーが
error[E0277]: the size for values of type `Self` cannot be known at compilation time
--> src/main.rs:6:13
|
6 | fn iabs(self) -> <Self as IAbs>::Output {
| ^^^^ doesn't have a size known at compile-time
|
help: consider further restricting `Self`
|
6 | fn iabs(self) -> <Self as IAbs>::Output where Self: Sized {
| +++++++++++++++++
help: function arguments must have a statically known size, borrowed types always have a known size
|
6 | fn iabs(&self) -> <Self as IAbs>::Output {
スタック領域に置かれるものはコンパイルのタイミングでメモリサイズが決定できていないといけない、、ってのは何だか聞いたことあるな。
ドキュメント見てみると、通常はimplicitだけど、trait objectは例外だ、、みたいなことが書いてあるな。
とりあえず、コンパイラの言う通り、where Self: Sized
をつけてみよう。
trait IAbs {
type Output;
fn iabs(self) -> <Self as IAbs>::Output
where
Self: Sized,
{
if self >= 0 {
self as <Self as IAbs>::Output
} else {
-self as <Self as IAbs>::Output
}
}
}
PartialOrd Trait
コンパイルエラーが変わった。進んでるっぽい。
error[E0369]: binary operation `>=` cannot be applied to type `Self`
--> src/main.rs:10:17
|
4 | / trait IAbs {
5 | | type Output;
6 | | fn iabs(self) -> <Self as IAbs>::Output
7 | | where
... |
10 | | if self >= 0 {
| | ---- ^^ - {integer}
| | |
| | Self
... |
15 | | }
16 | | }
| |_- `Self` might need a bound for `std::cmp::PartialOrd`
大小比較するには、Ord、PartialOrd Trait実装が必要ってのは聞いたことあるな。
今回、Selfに入ってくるのは、i8, i32等の数値型なので、そこの実装はされてる前提で良さそう。コンパイラの言う通りに足しておこう。
trait IAbs {
type Output;
fn iabs(self) -> <Self as IAbs>::Output
where
Self: Sized + PartialOrd,
{
if self >= 0 {
self as <Self as IAbs>::Output
} else {
-self as <Self as IAbs>::Output
}
}
}
intoで型を合わせる
コンパイルエラーが変わった。進んでるっぽい。
error[E0308]: mismatched types
--> src/main.rs:10:20
|
4 | / trait IAbs {
5 | | type Output;
6 | | fn iabs(self) -> <Self as IAbs>::Output
7 | | where
... |
10 | | if self >= 0 {
| | ^ expected type parameter `Self`, found integer
... |
15 | | }
16 | | }
| |_- this type parameter
|
= note: expected type parameter `Self`
found type `{integer}`
"0"(i32型)とSelfの大小比較してるけど、それはダメだよと言われている。
0をSelfの型に変換してやる必要がある。Selfには、i8, i16, i32, i64が入る想定なので、一番桁数の少ないi8型の0から変換してやれば良さそう。
でも、i8から変換できない、想定してない型でこのTraitをimplされた場合、どうなるかな、、? 何か書き方あった気がするけど、一旦保留。
trait IAbs {
type Output;
fn iabs(self) -> <Self as IAbs>::Output
where
Self: Sized + PartialOrd,
{
if self >= 0_i8.into() {
self as <Self as IAbs>::Output
} else {
-self as <Self as IAbs>::Output
}
}
}
Neg Trait
コンパイルエラーが変わった。進んでるっぽい。
error[E0600]: cannot apply unary operator `-` to type `Self`
--> src/main.rs:13:13
|
13 | -self as <Self as IAbs>::Output
| ^^^^^ cannot apply unary operator `-`
-self
って書いてるけど、この演算子は今の状態だと使えないよってことのようだ。
これは、対応方法が全然分からず、エラーメッセージにも出てないし、エラーメッセージでググっても有益な情報にヒットせず、書籍をカンニング、、💧
unary operator -
を使えるようになるには、NegというTrait境界を持つ必要があると学んだ。
Neg Traitがimplされているstd内の型の一覧にも行き着いた。i8, i16, …で、確かに実装されている。
※3/13 追記
コメントにて、最新verionのコンパイラだと、エラーMSGが強化されていると教えていただきました。
https://qiita.com/seikoudoku2000/items/28c5c6b09dcaea1744dc#comment-d9576cf72dfca6d56243
trait IAbs {
type Output;
fn iabs(self) -> <Self as IAbs>::Output
where
Self: Sized + PartialOrd + Neg,
{
if self >= 0_i8.into() {
self as <Self as IAbs>::Output
} else {
-self as <Self as IAbs>::Output
}
}
}
From Trait
コンパイルエラーが変わった。進んでるっぽい。
error[E0277]: the trait bound `Self: From<i8>` is not satisfied
--> src/main.rs:10:25
|
10 | if self >= 0_i8.into() {
| ^^^^ the trait `From<i8>` is not implemented for `Self`
|
= note: required because of the requirements on the impl of `Into<Self>` for `i8`
help: consider further restricting `Self`
|
8 | Self: Sized + PartialOrd + Neg, Self: From<i8>
| ~~~~~~~~~~~~~~~~
さっき、よく分からんからと飛ばした、i8から変換できないパティーンもあるよねってことで怒られてる。From / Into聞いたことあるし、結構使ってる。
ここでは、From<i8>
ってのを足しなさいとコンパイラが教えてくれているので足しておこう。
trait IAbs {
type Output;
fn iabs(self) -> <Self as IAbs>::Output
where
Self: Sized + PartialOrd + Neg + From<i8>,
{
if self >= 0_i8.into() {
self as <Self as IAbs>::Output
} else {
-self as <Self as IAbs>::Output
}
}
}
TryInto Trait
コンパイルエラーが変わった。進んでるっぽい。
error[E0605]: non-primitive cast: `Self` as `<Self as IAbs>::Output`
--> src/main.rs:11:13
|
11 | self as <Self as IAbs>::Output
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ an `as` expression can only be used to convert between primitive types or to coerce to a specific trait object
絶対値の返却の時には、Outputで指定した型での返却で、signedからunsignedにしている(e.g. i32 -> u32)けど、そこの型変換をas
で行なっているのがダメだと言われている。
さっき使った、Fromでも良さそうだけど、i32のマイナスの値は変換でこけそうな予感。
ドキュメントを漁ってみたら、TryIntoで変換されている。Selfを<<Self as IAbs>::Output>
に変換するので、TryInto<<Self as IAbs>::Output>
を追加して、try_into
で値を取得。
また、try_into
の結果はResultで返ってくるけど、ここでは事前に変換可能な状態であることを担保しているから、雑にunwrapしとこう。
trait IAbs {
type Output;
fn iabs(self) -> <Self as IAbs>::Output
where
Self: Sized + PartialOrd + Neg + From<i8> + TryInto<<Self as IAbs>::Output>,
{
if self >= 0_i8.into() {
self.try_into().unwrap()
} else {
(-self).try_into().unwrap()
}
}
}
Debug Trait
コンパイルエラーが変わった。進んでるっぽい。
error[E0599]: the method `unwrap` exists for enum `Result<<Self as IAbs>::Output, <Self as TryInto<<Self as IAbs>::Output>>::Error>`, but its trait bounds were not satisfied
--> src/main.rs:11:29
|
11 | self.try_into().unwrap()
| ^^^^^^ method cannot be called on `Result<<Self as IAbs>::Output, <Self as TryInto<<Self as IAbs>::Output>>::Error>` due to unsatisfied trait bounds
|
= note: the following trait bounds were not satisfied:
`<Self as TryInto<<Self as IAbs>::Output>>::Error: Debug`
Result
で返ってくるエラー型の方が何か怒られている。。
これは解読に時間がかかりましたが、try_intoの結果は、Result<<Self as IAbs>::Output, <Self as TryInto<<Self as IAbs>::Output>>::Error>
という型で、これのエラー型である <Self as TryInto<<Self as IAbs>::Output>>::Error
が必要なトレイト境界を満たしていないためにエラーになっています。
ドキュメントを色々と漁った結果、エラーで返される型には、DebugとDisplayというTraitが必要らしい。
コンパイラが最後の行のエラーMSGで教えてくれてるように、<Self as TryInto<<Self as IAbs>::Output>>::Error: Debug
を満たしてないってのを足してみよう。 (色々調べて、結局、コンパイラの言う通りのものをコピペするというのは結構あるあるな気がする。)
※なお、Displayに関しては最後まで怒られないままコンパイルは通った。どこかで暗黙的に付与されているのだろうか? (未調査)
trait IAbs {
type Output;
fn iabs(self) -> <Self as IAbs>::Output
where
Self: Sized + PartialOrd + Neg + From<i8> + TryInto<<Self as IAbs>::Output>,
<Self as TryInto<<Self as IAbs>::Output>>::Error: Debug,
{
if self >= 0_i8.into() {
self.try_into().unwrap()
} else {
(-self).try_into().unwrap()
}
}
}
TryFrom Trait その2
コンパイルエラーが変わった。進んでるっぽい。
error[E0277]: the trait bound `<Self as IAbs>::Output: From<<Self as Neg>::Output>` is not satisfied
--> src/main.rs:14:21
|
14 | (-self).try_into().unwrap()
| ^^^^^^^^ the trait `From<<Self as Neg>::Output>` is not implemented for `<Self as IAbs>::Output`
|
= note: required because of the requirements on the impl of `Into<<Self as IAbs>::Output>` for `<Self as Neg>::Output`
= note: required because of the requirements on the impl of `TryFrom<<Self as Neg>::Output>` for `<Self as IAbs>::Output`
= note: required because of the requirements on the impl of `TryInto<<Self as IAbs>::Output>` for `<Self as Neg>::Output`
help: consider further restricting the associated type
|
9 | <Self as TryInto<<Self as IAbs>::Output>>::Error: Debug, <Self as IAbs>::Output: From<<Self as Neg>::Output>
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-self
から、Output型への変換で、トレイト境界の定義が足りないと怒られている。コンパイラが、最後に、それっぽい<Self as IAbs>::Output: From<<Self as Neg>::Output>
とそれっぽい定義を提案してくれている。けど、i32 to u32の変換は、TryFromで行われるってのはさっき調べたので、TryFromで定義する。
trait IAbs {
type Output;
fn iabs(self) -> <Self as IAbs>::Output
where
Self: Sized + PartialOrd + Neg + From<i8> + TryInto<<Self as IAbs>::Output>,
<Self as TryInto<<Self as IAbs>::Output>>::Error: Debug,
<Self as IAbs>::Output: TryFrom<<Self as Neg>::Output>,
{
if self >= 0_i8.into() {
self.try_into().unwrap()
} else {
(-self).try_into().unwrap()
}
}
}
Debug Traitその2
コンパイルエラーが変わった。進んでるっぽい。
error[E0277]: `<<Self as IAbs>::Output as TryFrom<<Self as Neg>::Output>>::Error` doesn't implement `Debug`
--> src/main.rs:15:32
|
15 | (-self).try_into().unwrap()
| ^^^^^^ `<<Self as IAbs>::Output as TryFrom<<Self as Neg>::Output>>::Error` cannot be formatted using `{:?}` because it doesn't implement `Debug`
|
= help: the trait `Debug` is not implemented for `<<Self as IAbs>::Output as TryFrom<<Self as Neg>::Output>>::Error`
note: required by a bound in `Result::<T, E>::unwrap`
--> /Users/yosuke/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/result.rs:1232:12
|
1232 | impl<T, E: fmt::Debug> Result<T, E> {
| ^^^^^^^^^^ required by this bound in `Result::<T, E>::unwrap`
help: consider further restricting the associated type
|
10 | <Self as IAbs>::Output: TryFrom<<Self as Neg>::Output>, <<Self as IAbs>::Output as TryFrom<<Self as Neg>::Output>>::Error: Debug
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
(-self).try_into().unwrap()
でDebug Traitがないぞって怒られている。
さっき、<Self as TryInto<<Self as IAbs>::Output>>::Error: Debug
ってのを足したけど、(-self)
は型が違うので、定義が足りてないということになっているっぽい。
先ほどと同様、コンパイラがそれっぽい定義の提案をしてくれているので、足してみよう。<<Self as IAbs>::Output as TryFrom<<Self as Neg>::Output>>::Error: Debug
型がえらいことになってて、読みづらいけど、、さっき足した行と比較しながら読み比べてみたら、意味が分かる(気がする)。
trait IAbs {
type Output;
fn iabs(self) -> <Self as IAbs>::Output
where
Self: Sized + PartialOrd + Neg + From<i8> + TryInto<<Self as IAbs>::Output>,
<Self as TryInto<<Self as IAbs>::Output>>::Error: Debug,
<Self as IAbs>::Output: TryFrom<<Self as Neg>::Output>,
<<Self as IAbs>::Output as TryFrom<<Self as Neg>::Output>>::Error: Debug,
{
if self >= 0_i8.into() {
self.try_into().unwrap()
} else {
(-self).try_into().unwrap()
}
}
}
コンパイルエラー解消。そして、動いた!!
最終形
use std::fmt::Debug;
use std::ops::Neg;
trait IAbs {
type Output;
fn iabs(self) -> <Self as IAbs>::Output
where
Self: Sized + PartialOrd + Neg + From<i8> + TryInto<<Self as IAbs>::Output>,
<Self as TryInto<<Self as IAbs>::Output>>::Error: Debug,
<Self as IAbs>::Output: TryFrom<<Self as Neg>::Output>,
<<Self as IAbs>::Output as TryFrom<<Self as Neg>::Output>>::Error: Debug,
{
if self >= 0_i8.into() {
self.try_into().unwrap()
} else {
(-self).try_into().unwrap()
}
}
}
impl IAbs for i32 {
type Output = u32;
}
impl IAbs for i8 {
type Output = u8;
}
impl IAbs for i64 {
type Output = u64;
}
fn main() {
println!("Hello, world!");
println!("{}", 1.iabs());
println!("{}", (-1).iabs());
println!("{}", 2_i8.iabs());
println!("{}", (-2_i8).iabs());
println!("{}", 3_i64.iabs());
println!("{}", (-3_i64).iabs());
}
まとめ
今回は、解答がある安心感があって(&ちょっとカンニングもしたり💧)本番開発とはちょっと違ったものの、このフローを経験したことで、様々なトレイトの意味を腹落ち感高めで復習したり、多少なりともトレイト境界の意味を掴めた気がするし、Rustコンパイラの言い分(?)が聞きやすくなった気がします。また、私は実際に業務でRustを書くこともありますが、ここでやったことは、業務中にRust分からん!ってなった時に、詳しい人にペアで教えてもらう時にやってることと同じだな〜と感じたりもして、結構実践的な練習できな〜と感じることができました。
このトレイト境界の変換に自信がない方は、一回やってみると良いかもです。また、他にこういう学習法もオススメだよ!っていうのがあったら是非教えてください!!
また、繰り返しになりますが、「コンセプトから理解するRust」、とても良い本だと思いますので、めちゃオススメです。
Rustの勉強を始める最初の一冊にも良いのかもですが、私の印象としては、公式のドキュメントやらを一通りさらって、ちょっと触ってみたけど、何かしっくり来ないな〜とモヤモヤしてるくらいの時に読む方が、効果が実感できそうな気がします。
Rust はもう怖くない ちょっと怖くなくなった!