前の記事
- 【0】 準備 ← 初回
- ...
- 【17】 thiserror・TryFrom ~トレイトもResultも自由自在!~ ← 前回
- 【18】 Errorのネスト・慣例的な書き方 ~Rustらしさの目醒め~ ← 今回
全記事一覧
- 【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 演習第18回になります!
今回の関連ページ
[05_ticket_v2/14_source] Errorのネスト
問題はこちらです。今回はソースファイルが複数に分割している点に注意が必要です。
use crate::status::Status;
// We've seen how to declare modules in one of the earliest exercises, but
// we haven't seen how to extract them into separate files.
// Let's fix that now!
//
// In the simplest case, when the extracted module is a single file, it is enough to
// create a new file with the same name as the module and move the module content there.
// The module file should be placed in the same directory as the file that declares the module.
// In this case, `src/lib.rs`, thus `status.rs` should be placed in the `src` directory.
mod status;
// TODO: Add a new error variant to `TicketNewError` for when the status string is invalid.
// When calling `source` on an error of that variant, it should return a `ParseStatusError` rather than `None`.
#[derive(Debug, thiserror::Error)]
pub enum TicketNewError {
#[error("Title cannot be empty")]
TitleCannotBeEmpty,
#[error("Title cannot be longer than 50 bytes")]
TitleTooLong,
#[error("Description cannot be empty")]
DescriptionCannotBeEmpty,
#[error("Description cannot be longer than 500 bytes")]
DescriptionTooLong,
}
#[derive(Debug, PartialEq, Clone)]
pub struct Ticket {
title: String,
description: String,
status: Status,
}
impl Ticket {
pub fn new(title: String, description: String, status: String) -> Result<Self, TicketNewError> {
if title.is_empty() {
return Err(TicketNewError::TitleCannotBeEmpty);
}
if title.len() > 50 {
return Err(TicketNewError::TitleTooLong);
}
if description.is_empty() {
return Err(TicketNewError::DescriptionCannotBeEmpty);
}
if description.len() > 500 {
return Err(TicketNewError::DescriptionTooLong);
}
// TODO: Parse the status string into a `Status` enum.
Ok(Ticket {
title,
description,
status,
})
}
}
#[cfg(test)]
mod tests {
use common::{valid_description, valid_title};
use std::error::Error;
use super::*;
#[test]
fn invalid_status() {
let err = Ticket::new(valid_title(), valid_description(), "invalid".into()).unwrap_err();
assert_eq!(
err.to_string(),
"`invalid` is not a valid status. Use one of: ToDo, InProgress, Done"
);
assert!(err.source().is_some());
}
}
status.rs
#[derive(Debug, PartialEq, Clone)]
pub enum Status {
ToDo,
InProgress,
Done,
}
impl TryFrom<String> for Status {
type Error = ParseStatusError;
fn try_from(value: String) -> Result<Self, Self::Error> {
let value = value.to_lowercase();
match value.as_str() {
"todo" => Ok(Status::ToDo),
"inprogress" => Ok(Status::InProgress),
"done" => Ok(Status::Done),
_ => Err(ParseStatusError {
invalid_status: value,
}),
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("`{invalid_status}` is not a valid status. Use one of: ToDo, InProgress, Done")]
pub struct ParseStatusError {
invalid_status: String,
}
#[cfg(test)]
mod tests {
use super::*;
use std::convert::TryFrom;
#[test]
fn test_try_from_string() {
let status = Status::try_from("ToDO".to_string()).unwrap();
assert_eq!(status, Status::ToDo);
let status = Status::try_from("inproGress".to_string()).unwrap();
assert_eq!(status, Status::InProgress);
let status = Status::try_from("Done".to_string()).unwrap();
assert_eq!(status, Status::Done);
}
}
source
メソッドを呼んだ時に、 status.rs
にて定義されている ParseStatusError
を返すような Status
関連エラーのバリアントを列挙体 TicketNewError
に追加してほしい、という問題です。
解説
// ...省略...
#[derive(Debug, thiserror::Error)]
pub enum TicketNewError {
#[error("Title cannot be empty")]
TitleCannotBeEmpty,
#[error("Title cannot be longer than 50 bytes")]
TitleTooLong,
#[error("Description cannot be empty")]
DescriptionCannotBeEmpty,
#[error("Description cannot be longer than 500 bytes")]
DescriptionTooLong,
+ #[error("{0}")]
+ InvalidStatusString(#[from] status::ParseStatusError),
}
// ...省略...
impl Ticket {
pub fn new(title: String, description: String, status: String) -> Result<Self, TicketNewError> {
// ...省略...
// TODO: Parse the status string into a `Status` enum.
+ let status = status.try_into()?;
Ok(Ticket {
title,
description,
status,
})
}
}
// ...省略...
列挙型の方には、 status::ParseStatusError
を source
として返し、かつこのエラーより変換して生成することができるようにした InvalidStatusError
を定義します。 thiserror
クレートめっちゃ便利...
そして呼び出し側でバリデーションを追加しています。 status.try_into()
は Result<Status, ParseStatusError>
を返すようになっていますが、ここで便利なのが ?
演算子です!
?
演算子は try
マクロ と呼ばれるもので、一種の糖衣構文みたいなもので、 status.try_into()?
と書くと大体以下のような match
式に展開されます(実際に出力されるコードとは異なるかも...)。
match status.try_into() {
Ok(s) => s,
Err(e) => return Err(TicketNewError::from(e)),
}
-
Ok
の時は持っている値を返す -
Err
の時は返り値のError
型にfrom/into
でキャストし、return
する
頻出構文である match
を短く書くための演算子というわけです!正体不明な処理をするわけではないのでお手軽です。トレイトにしろ列挙体にしろこういう糖衣構文にしろ、 Rustの文法は型システムを絡めて比較的簡単に説明できる/理解しやすい なと思います。筆者がRustを好む理由の一つです。
トレイトオブジェクト
今回 source
メソッドの返り値型としてサラッとトレイトオブジェクト( &(dyn Error + 'static)
)が登場していますが、本エクササイズのBook以外で登場していなかったため軽く言及しておきたいと思います(Bookでも後述すると書いておきながらその後詳細は未登場...?)。
まず前提として、Rustでは 基本的にコンパイル時に全ての型が解決します。言い換えると、 "リフレクション" と呼ばれるような、 「ランタイム時に型を決定する」機能に関してはかなり貧弱 です。
具体的には(マクロのようにRustコード自体を入出力とするプログラムを書くことはありますが)、いわゆる eval
関数 のような「ランタイム時にソースコードを受け取り、そのソースコード内で生成された構造体を事前に用意したコード側で扱う」みたいなことはできないような作りになっています。むしろそこがいい...
コンパイル時に型が解決するというのは、 ジェネリクスも そうです。故に、例えば以下のようなコードはコンパイルエラーになります(フライングして動的配列を使用していますがご容赦ください)。
trait Hoge {}
#[derive(Clone, Copy)]
struct Fuga;
impl Hoge for Fuga {}
#[derive(Clone, Copy)]
struct Bar;
impl Hoge for Bar {}
fn create_vec<T: Hoge + Clone + Copy>(v: T) -> Vec<T> {
vec![v; 5]
}
fn main() {
let mut t = create_vec(Fuga);
// t は Vec<impl Hoge> であることを期待して
// 以下のように書いても、
t.push(Bar);
// expected `Fuga`, found `Bar`
// すでに動的配列の型は Vec<Fuga> で固定されておりコンパイルエラー!
}
「じゃあ Fuga
と Bar
が混在した動的配列は作れないの...?」というと、これだけは例外的に可能なようにできており、それが「トレイトオブジェクト(動的ディスパッチ)」という機能です!
次のように書き直すことで Fuga
と Bar
が混在している動的配列を記述可能です。
trait Hoge {}
#[derive(Clone, Copy)]
struct Fuga;
impl Hoge for Fuga {}
#[derive(Clone, Copy)]
struct Bar;
impl Hoge for Bar {}
fn create_vec<T: Hoge + Clone + Copy + 'static>(v: T) -> Vec<Box<dyn Hoge>> {
(0..5).map(|_| -> Box<dyn Hoge> { Box::new(v) }).collect()
}
fn main() {
let mut t = create_vec(Fuga);
// t は Vec<Box<dyn Hoge>> であるため
// Box<Bar> は要件を満たし追加可能
t.push(Box::new(Bar));
}
Box<dyn T>
という型が出てきました。 Box
は「コンパイル時サイズ不定でも、包むことでサイズを確定したことにする」ために使用しています。中身の dyn T
は、「あるトレイトを実装したある型(ただしコンパイル時未確定)」を表しています!
Box<U>
の U
は ?Sized
、つまりコンパイル時サイズ不明でも指定できます。これは U
型の値をヒープ上に置き、代わりにポインタ(コンパイル時サイズ確定!)を値として持つことで実現しています。
なぜ「コンパイル時サイズ不定」となってしまうかというと、(今回の例だったらソースコード全体を読めばわかりそうな気もしますが)一般的に「あるトレイトを実装したある型」という記述だけではコンパイル時にはサイズがわからないためです!
Rustの一機能でしかないようなトレイトオブジェクトをちょっと回りくどく説明していたり、これをメインテーマとしたエクササイズがない理由ですが、トレイトオブジェクトは できれば使用を避けたい 機能だからです。コンパイル時に型が確定しないとそれなりのオーバーヘッドがあったり、またそもそもトレイトオブジェクトとして扱うにはvtableが作れなければならない、みたいな制約が多かったりと1、使いにくい機能になっており滅多に出てきません。
「記述場所が異なると違う型扱いされるクロージャを、同時に複数種類扱いたい」とか、「面倒な型パズルを回避したい」等、どうしても必要な場合というのはなくはないのですが、例えば列挙型を使うことでトレイトオブジェクトを使用せずに目的を達成できるなら、無理に使うべき機能ではないという感じです。
[05_ticket_v2/15_outro] 慣例的なコードへとリファクタ!
問題のソースコードは複数ファイルに分割されており、各ファイルにTODOがあります。
// TODO: you have something to do in each of the modules in this crate!
mod description;
mod status;
mod title;
// A common pattern in Rust is to split code into multiple (private) modules
// and then re-export the public parts of those modules at the root of the crate.
//
// This hides the internal structure of the crate from your users, while still
// allowing you to organize your code however you like.
pub use description::TicketDescription;
pub use status::Status;
pub use title::TicketTitle;
#[derive(Debug, PartialEq, Clone)]
// We no longer need to make the fields private!
// Since each field encapsulates its own validation logic, there is no risk of
// a user of `Ticket` modifying the fields in a way that would break the
// invariants of the struct.
//
// Careful though: if you had any invariants that spanned multiple fields, you
// would need to ensure that those invariants are still maintained and go back
// to making the fields private.
pub struct Ticket {
pub title: TicketTitle,
pub description: TicketDescription,
pub status: Status,
}
Ticket
構造体の各フィールドが専用の型になっており、そちらで正規の値を作れる上、カプセル化により不正な値を作れなくなっています。そして lib.rs
がシンプルになっています!
description.rs
// TODO: Implement `TryFrom<String>` and `TryFrom<&str>` for the `TicketDescription` type,
// enforcing that the description is not empty and is not longer than 500 bytes.
// Implement the traits required to make the tests pass too.
pub struct TicketDescription(String);
#[cfg(test)]
mod tests {
use super::*;
use std::convert::TryFrom;
#[test]
fn test_try_from_string() {
let description = TicketDescription::try_from("A description".to_string()).unwrap();
assert_eq!(description.0, "A description");
}
#[test]
fn test_try_from_empty_string() {
let err = TicketDescription::try_from("".to_string()).unwrap_err();
assert_eq!(err.to_string(), "The description cannot be empty");
}
#[test]
fn test_try_from_long_string() {
let description = "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.".to_string();
let err = TicketDescription::try_from(description).unwrap_err();
assert_eq!(
err.to_string(),
"The description cannot be longer than 500 bytes"
);
}
#[test]
fn test_try_from_str() {
let description = TicketDescription::try_from("A description").unwrap();
assert_eq!(description.0, "A description");
}
}
status.rs
// TODO: Implement `TryFrom<String>` and `TryFrom<&str>` for the `Status` enum.
// The parsing should be case-insensitive.
pub enum Status {
ToDo,
InProgress,
Done,
}
#[cfg(test)]
mod tests {
use super::*;
use std::convert::TryFrom;
#[test]
fn test_try_from_string() {
let status = Status::try_from("ToDO".to_string()).unwrap();
assert_eq!(status, Status::ToDo);
let status = Status::try_from("inproGress".to_string()).unwrap();
assert_eq!(status, Status::InProgress);
let status = Status::try_from("Done".to_string()).unwrap();
assert_eq!(status, Status::Done);
}
#[test]
fn test_try_from_str() {
let status = Status::try_from("ToDO").unwrap();
assert_eq!(status, Status::ToDo);
let status = Status::try_from("inproGress").unwrap();
assert_eq!(status, Status::InProgress);
let status = Status::try_from("Done").unwrap();
assert_eq!(status, Status::Done);
}
#[test]
fn test_try_from_invalid() {
let status = Status::try_from("Invalid");
assert!(status.is_err());
}
}
title.rs
// TODO: Implement `TryFrom<String>` and `TryFrom<&str>` for the `TicketTitle` type,
// enforcing that the title is not empty and is not longer than 50 characters.
// Implement the traits required to make the tests pass too.
pub struct TicketTitle(String);
#[cfg(test)]
mod tests {
use super::*;
use std::convert::TryFrom;
#[test]
fn test_try_from_string() {
let title = TicketTitle::try_from("A title".to_string()).unwrap();
assert_eq!(title.0, "A title");
}
#[test]
fn test_try_from_empty_string() {
let err = TicketTitle::try_from("".to_string()).unwrap_err();
assert_eq!(err.to_string(), "The title cannot be empty");
}
#[test]
fn test_try_from_long_string() {
let title =
"A title that's definitely longer than what should be allowed in a development ticket"
.to_string();
let err = TicketTitle::try_from(title).unwrap_err();
assert_eq!(err.to_string(), "The title cannot be longer than 50 bytes");
}
#[test]
fn test_try_from_str() {
let title = TicketTitle::try_from("A title").unwrap();
assert_eq!(title.0, "A title");
}
}
解説
thiserror
クレートを導入して演習開始です!
cargo add thiserror
lib.rs の改変は必要ありません。
各フィールドについて、関心がファイルごとに分かれています!ファイルごとにTODOを満たしていきます。
description.rs
では、文字列型から構造体 TicketDescription
に変換する TryFrom
を実装します。専用のエラー型も用意します!
#[derive(Debug, PartialEq, Clone)]
pub struct TicketDescription(String);
#[derive(thiserror::Error, Debug)]
pub enum InvalidDescriptionError {
#[error("The description cannot be empty")]
Empty,
#[error("The description cannot be longer than 500 bytes")]
TooLong,
}
use InvalidDescriptionError::*;
fn str2description(value: &str) -> Result<TicketDescription, InvalidDescriptionError> {
if value.is_empty() {
return Err(Empty);
}
if value.len() > 500 {
return Err(TooLong);
}
Ok(TicketDescription(value.to_string()))
}
impl TryFrom<String> for TicketDescription {
type Error = InvalidDescriptionError;
fn try_from(value: String) -> Result<Self, Self::Error> {
str2description(&value)
}
}
impl TryFrom<&str> for TicketDescription {
type Error = InvalidDescriptionError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
str2description(value)
}
}
同じく、 status.rs
では文字列型から Status
型へ変換する TryFrom
を実装します。
Todo
や toDO
も受け入れられるよう、 to_lowercase
を使用して小文字での比較をしています。 &str
は match
にてパターンマッチ分岐できるので便利ですね。
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Status {
ToDo,
InProgress,
Done,
}
use Status::*;
#[derive(thiserror::Error, Debug)]
#[error("{0}")]
pub struct InvalidStatusError(String);
fn str2status(value: &str) -> Result<Status, InvalidStatusError> {
match value.to_lowercase().as_str() {
"todo" => Ok(ToDo),
"inprogress" => Ok(InProgress),
"done" => Ok(Done),
s => Err(InvalidStatusError(s.to_string())),
}
}
impl TryFrom<String> for Status {
type Error = InvalidStatusError;
fn try_from(value: String) -> Result<Self, Self::Error> {
str2status(&value)
}
}
impl TryFrom<&str> for Status {
type Error = InvalidStatusError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
str2status(value)
}
}
最後に title.rs
にも同様の TryFrom
とエラー型を用意すれば完成です!
#[derive(Debug, PartialEq, Clone)]
pub struct TicketTitle(String);
#[derive(thiserror::Error, Debug)]
pub enum InvalidTitleError {
#[error("The title cannot be empty")]
Empty,
#[error("The title cannot be longer than 50 bytes")]
TooLong,
}
use InvalidTitleError::*;
fn str2title(value: &str) -> Result<TicketTitle, InvalidTitleError> {
if value.is_empty() {
return Err(Empty);
}
if value.len() > 50 {
return Err(TooLong);
}
Ok(TicketTitle(value.to_string()))
}
impl TryFrom<String> for TicketTitle {
type Error = InvalidTitleError;
fn try_from(value: String) -> Result<Self, Self::Error> {
str2title(&value)
}
}
impl TryFrom<&str> for TicketTitle {
type Error = InvalidTitleError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
str2title(value)
}
}
第5回 の時と見比べると、かなりRustの慣例に従ったプログラムになっていて、読みやすくなっています!従い過ぎなぐらい...
ここまででRustの基本的な道具であるトレイトと列挙型について見てきました。Rustの言語慣例に従うことで読みやすいコードになおしていこうという方針でした。
次章からは配列や並列処理・非同期処理と進んでいきます。新しい機能の紹介、というエクササイズが増えて風向きが変わっていくようです。配列が後半に来ているのってなかなかファンキー...?
では次の問題に行きましょう!
次の記事: 【19】 配列・動的配列 ~スタックが使われる配列と、ヒープに保存できる動的配列~
-
今回例として出したコードをコンパイルするまでに筆者もn敗しました ↩