RustにはCargoという優れたパッケージマネージャーがあるので細かい多数の依存パッケージが問題になることはあまりありません。というより同一のアプリケーションでもパッケージを細かく分割することが推奨されています。ちょっとしたボイラープレートを取り除くような小さなライブラリも大量にあります。これらは積極的に使うべきです。
問題があるとすれば悪意のあるようなパッケージの存在ですが、これらに対処するcargo-auditやcargo-crevというツールもあります。
本記事では
- 誰かがTwitterやブログで紹介するか誰かが使っているのを見る、あるいは何かのtrendingに載っているのを見るなどしない限り出会わない
- 日本語の情報があまり無い
- 用途がニッチすぎない
- (synとかはしょうがないとして)あまりbloatを引き起こさない
- deprecatedとかWIPとか書かれたやつは含まない
-
総DL数が100k以上なるべく有名なもの
のような便利なクレートを60個紹介したいと思います。もちろんこのようなクレートは他にも沢山あります。暇があればcrates.ioを眺めてみるのもいいでしょう。
執筆当時のRustのバージョンは1.40.0 1.44.1です。
対象読者
-
Rustに触れたことが無い
「Rustではこういうことができる」というのを感じてもらえれば。
-
チュートリアル and/or 書籍を読んだばかり
頭の片隅にでも置いておくと後で少しだけ楽になるかもしれません。
-
Rustに慣れた人
知らないクレートがあるかもしれません。
Changelog
Changelog
- 2021-04-28
- itertoolsのコード例を修正
- 2020-07-11
- itertools, fallible-iterator, walkdir, anyhow, num-derive, ref-cast, ascii, unicase, ordered-float, im, indexmap, generic-array, crossbeam, difference, assert_cmdの説明と例を更新
- defmac, matches, tempdirの3つを削除
- ignore, duct, duct_sh, paste, easy-ext, ghost, dunce, tempfile, test-case, rusty-fork, trybuild, insta, indocの13個を追加
- 2019-12-30
- 2019-12-28
- itertools, anyhow, if_chain, maplit, derivative, derive_more, strum, derive-new, getset, ref-castの説明と例を変更
- 2019-12-27
- 2019-12-26
- リンクを修正
- typoを修正(編集リクエスト)
- 冒頭の文面を変更
- defmac, structopt, getset, ascii, stable_deref_trait, owning_refの説明と例を変更
ボイラープレート削減
itertools
Iterator
を拡張したItertools
とイテレータを生成するいくつかの関数とマクロを提供します。
Pythonのmore-itertoolsのようなものです。 ただし単純に対応するものではなく、互いに存在しないものや対応していても名前が違う場合があります。
良く使われているのはjoin
とformat
ですがその他のメソッドも大変便利です。一度Itertools
のメソッドを眺めるのも良いでしょう。
例を挙げると、
-
-if xs.iter().all(|x| x % 2 == 0) || xs.iter().all(|x| x % 2 == 1) { +if xs.iter().map(|x| x % 2).all_equal() { todo!(); }
-
-for i in 0..xs.len() { - for j in i + 1..xs.len() { - todo!(); - } +for (i, j) in (0..xs.len()).tuple_combinations() { + todo!(); }
let sum = (0..xs.len()) - .flat_map(|i| (i + 1..xs.len()).map(move |j| (i, j))) + .tuple_combinations() .map(|(i, j)| f(&xs[i..=j])) .sum::<i32>();
-
-for i in 0..xs.len() { - for j in 0..ys.len() { - todo!(); - } +for (i, j) in iproduct!(0..xs.len(), 0..ys.len()) { + todo!(); }
-let sum = (0..xs.len()) - .flat_map(|i| (0..ys.len()).map(move |j| (i, j))) +let sum = iproduct!(0..xs.len(), 0..ys.len()) .map(|(i, j)| f(i, j)) .sum::<i32>();
-
Itertools::{chunks, join, tuple_windows, ..}
let max = xs .into_iter() .map(f) - .collect::<Vec<_>>() - .windows(2) - .map(|w| g(w[0], w[1])) + .tuple_windows() + .map(|(a, b)| g(a, b)) .max() .unwrap();
let s = [1u32, 2, 3, 4, 5] .iter() .filter(|&x| x % 2 == 0) - .map(|x| (10 * x).to_string()) - .collect::<Vec<_>>() + .map(|x| 10 * x) .join(" "); // `<[_]>::join` → `Itertools::join`
そのほか
-
izip!
(3個以上をzip
) -
iterate
(successors
のSome
固定版) -
assert_equal
(イテレータをassert_eq!
) -
Itertools::sorted
(.collect::<Vec<_>>()
してソート) -
Itertools::merge
(ソート済みの2つをマージ) -
Itertools::permutations
(順列) -
Itertools::combinations
(組み合わせ) -
Itertools::zip_longest
(要素を切り捨てないzip
) -
Itertools::intersperse
(ある要素を挟んでいく) -
Itertools::{interleave, interleave_shortest}
(交互) -
Itertools::coalesce
(柔軟に部分的にfold) -
Itertools::set_from
(Item = &mut T
のに対し、&mut
の参照先をItem = T
ので埋める)
等の様々なユーティリティを提供します。 是非一度網羅的に調べてみるのはどうでしょうか。メソッドチェーン一発で解決できる問題が増えます。
極めて軽量かつ使いどころも多いのでstructoptやanyhow等と共にcargo new
直後に入れておくのもいいかもしれません。
See also: itertoolsの紹介 | κeenのHappy Hacκing Blog(その1の6日目)
itertools-num
itertoolsと同じくIterator
を拡張しますが提供するのはcumsum
とlinspace
、これだけです。
@bluss氏本人によるクレートですがitertoolsから分離されている理由はよくわかりません。num-traitsへの依存を嫌ったのでしょうか..?
+use itertools_num::ItertoolsNum as _;
+
static XS: &[u32] = &[1, 2, 3, 4, 5];
-let cumsum = XS
- .iter()
- .scan(0, |sum, x| {
- *sum += x;
- Some(*sum)
- })
- .collect::<Vec<_>>();
+let cumsum = XS.iter().cumsum().collect::<Vec<u32>>();
assert_eq!(cumsum, &[1, 3, 6, 10, 15]);
const START: f64 = 2.0;
const STOP: f64 = 3.0;
const NUM: usize = 5;
-let linspace = (0..NUM)
- .map(|i| START + (STOP - START) * (i as f64) / ((NUM - 1) as f64))
- .collect::<Vec<_>>();
+let linspace = itertools_num::linspace(START, STOP, NUM).collect::<Vec<_>>();
assert_eq!(linspace, &[2.0, 2.25, 2.5, 2.75, 3.0]);
fallible-iterator
Iterator<Item = Result<_, _>>
を変換して扱いやすくします。
+use fallible_iterator::FallibleIterator as _;
+
use std::io::{BufRead as _, Cursor};
let stdin = Cursor::new("1\n2\n3\n101\n");
-let xs = stdin
- .lines()
- .map(|line| {
- let x = line?.parse::<u32>()?;
- Ok(if x <= 100 { Some(x) } else { None })
- })
- .flat_map(Result::transpose)
- .collect::<anyhow::Result<Vec<_>>>()?;
+let xs = fallible_iterator::convert(stdin.lines())
+ .map_err(anyhow::Error::from)
+ .map(|line| line.parse::<u32>().map_err(Into::into))
+ .filter(|&x| Ok(x <= 100))
+ .collect::<Vec<_>>()?;
assert_eq!(xs, &[1, 2, 3]);
walkdir
ディレクトリを捜索するときに使えるユーティリティです。
use walkdir::WalkDir;
for entry in WalkDir::new("./src").follow_links(true).max_depth(10) {
let path = entry?.into_path();
if path.extension() == Some("rs".as_ref()) {
println!("{}", path.display());
}
}
ignore
hidden fileやgitignore
されているものを除外して探索できます。
ripgrepの一部です。
use ignore::WalkBuilder;
let text_file_paths = WalkBuilder::new(".")
.require_git(true)
.follow_links(true)
.max_depth(Some(32))
.build()
.map(|entry| {
let path = entry?.into_path();
Ok(if path.extension() == Some("txt".as_ref()) {
Some(path)
} else {
None
})
})
.flat_map(Result::transpose)
.collect::<Result<Vec<_>, ignore::Error>>()?;
ripgrepのファイルパス関連のオプションがそのまま使えます。 例えば-g, --glob <GLOB>...
のフィルターが使えます。
use ignore::{overrides::OverrideBuilder, WalkBuilder};
use structopt::StructOpt;
#[derive(StructOpt)]
struct Opt {
/// Include or exclude files. For more detail, see the help of ripgrep
#[structopt(short, long, value_name("GLOB"))]
glob: Vec<String>,
}
let Opt { glob } = Opt::from_args();
let mut overrides = OverrideBuilder::new(".");
for glob in glob {
overrides.add(&glob)?;
}
let overrides = overrides.build()?;
for entry in WalkBuilder::new(".").overrides(overrides).build() {
todo!();
}
-g, --glob <GLOB>... Include or exclude files and directories for searching that match the given glob. This always overrides any other ignore logic. Multiple glob flags may be used. Globbing rules match .gitignore globs. Precede a glob with a ! to exclude it. If multiple globs match a file or directory, the glob given later in the command line takes precedence. When this flag is set, every file and directory is applied to it to test for a match. So for example, if you only want to search in a particular directory 'foo', then *-g foo* is incorrect because 'foo/bar' does not match the glob 'foo'. Instead, you should use *-g +++'foo/**'+++*. --glob-case-insensitive Process glob patterns given with the -g/--glob flag case insensitively. This effectively treats --glob as --iglob. This flag can be disabled with the --no-glob-case-insensitive flag.
duct
扱いやすくしたstd::process::Command
です。
std::process::Command
と比べて
- 短いです。一行に収まることが多いです。
- ステータスチェック & 失敗時のエラーメッセージの設定が不要です。
-
.stderr(Stdio::inherit())
が不要です。 -
stdout
のstr::from_utf8
と.trim_end()
が不要です。
use duct::cmd;
use std::path::Path;
let path = Path::new("/etc/os-release");
let os_release = cmd!("cat", path).read()?; // マクロ版だと`.as_ref()`とかで`args`の型を揃えなくてもよい
if os_release.starts_with("NAME=\"Arch Linux\"") {
println!("Arch Linux!");
}
またpipe
でパイプ処理が可能です。
use duct::cmd;
use duct_sh::sh; // ↓で紹介
let output = sh("yes | head -5").pipe(cmd!("xargs", "echo")).read()?;
assert_eq!(output, "y y y y y");
その関係でいくつかのdependencyがくっついてきますが、軽量な上にどの道他のdependencyにくっついてくるようなポピュラーなものばかりなので問題はあまり無いでしょう。
$ pwd # `git clone`したリポジトリ上
/home/ryo/src/github.com/oconnor663/duct.rs
$ cargo metadata --format-version 1 | jq -r '.workspace_members[]'
duct 0.13.4 (path+file:///home/ryo/src/github.com/oconnor663/duct.rs)
$ cargo metadata --format-version 1 | jq -r '. as $r | .packages[] | select(. as $p | $r.workspace_members | contains([$p.id]) | not) | .id' | sort
fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)
libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)
once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)
os_pipe 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)
rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)
rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)
rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)
rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)
remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)
shared_child 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)
tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)
winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)
winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)
winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)
duct_sh
cfg(unix)
の場合/bin/sh -c ...
、cfg(windows)
の場合%COMSPEC% /C ...
を呼びだすduct::{cmd, cmd!}
のショートカットです。
use duct_sh::sh;
let output = sh("echo Hello!").read()?;
assert_eq!(output, "Hello!");
anyhow
(thiserrorと合わせれば)error-chain → failure →と続くエラーハンドリングライブラリ3代目の最有力候補です。
2019-10-05ZにInitial commitされた後2日で1.0.0がリリースされ、爆発的に使われ始めました。2ヶ月経った頃には先発のsnafuを総DL数で抜いてPlaygroundでも使えるようにもなりました。 (2020-05-25現在は外れています)
anyhow::Error
はBox<std::error::Error>
やfailure::Error
のようにFrom<_: std::error::Error + Send + Sync + 'static>
を実装しているのでエラーを雑にまとめることができます。
あとDebug
表示がhuman-readableになっているのでコードを書き始めるとき、あるいはサンプルコードを書くときにmain
の戻り値をとりあえずanyhow::Result<()>
とするといいでしょう。流石に"Error: "
や"Cuased by:"
の部分をカラーにはしてくれませんが。
use anyhow::Context as _;
use std::fs;
fn main() -> anyhow::Result<()> {
fs::read_to_string("./nonexisting.txt").with_context(|| "Could not read ./nonexisting.txt")?;
Ok(())
}
Error: Could not read ./nonexisting.txt
Caused by:
No such file or directory (os error 2)
failureはdeprecatedになったので新規にコードを書くときはanyhowを使うべきでしょう。 現在はcargoもanyhowを使っています。 ただしanyhowはstable上でバックトレースを扱えません。 backtraceクレートではなく現在安定化していないstd::backtrace
をnightlyコンパイラ上でのみ使っているためです。
See Also:
-use std::fmt;
-use thiserror::Error;
-type Result<T> = std::result::Result<T, Box<dyn std::error::Error + 'static>>;
-trait ResultExt {
- fn with_context<F: FnOnce() -> E, E: fmt::Display>(self, f: F) -> Self;
-}
-impl<T> ResultExt for Result<T> {
- fn with_context<F: FnOnce() -> E, E: fmt::Display>(self, f: F) -> Self {
- #[derive(Debug, Error)]
- #[error("{}", .0)]
- struct WithContext(String, #[source] Box<dyn std::error::Error + 'static>);
- self.map_err(|e| WithContext(f().to_string(), e).into())
- }
-}
+use anyhow::Context as _;
-fn main() -> Result<()> {
+fn main() -> anyhow::Result<()> {
todo!();
}
either
Either<_, _>
とtry_left!
, try_right!
を提供します。
即席の構造体としてタプルが使えるようにEither
は即席の(バリアント2つの)直和型として使えます。
またEither
には使いやすくするためのメソッドがいくつか付いていて、またstd
の各トレイトについてimpl<L: Trait, R: Trait> Trait for Either<L, R>
という定義がなされています。
実装が非常に小さいのもありitertoolsやrayonをはじめとした多くのクレートに使われ、またre-exportされています。
ちなみに即席直和型を専用構文付きで入れようという議論がpre 1.0時代からあります。
-enum Either<L, R> {
- Left(L),
- Right(L),
-}
+use either::Either;
paste
現在unstableであるstd::concat_ident!
の代用となるマクロです。
proc-macroを書かなくても、このクレートがあればいくつかのケースではmacro_rules!
だけでどうにかすることができます。
use std::path::{Path, PathBuf};
const _: fn(&Struct) -> &str = Struct::name;
const _: fn(&Struct) -> &Path = Struct::path;
const _: fn(&mut Struct) -> &mut String = Struct::name_mut;
const _: fn(&mut Struct) -> &mut PathBuf = Struct::path_mut;
const _: fn(&mut Struct, String) = Struct::set_name;
const _: fn(&mut Struct, PathBuf) = Struct::set_path;
struct Struct {
name: String,
path: PathBuf,
}
macro_rules! impl_accessor {
($struct:ident { $($tt:tt)* }) => {
impl $struct {
impl_accessor_inner!(@rest($($tt)*));
}
};
}
macro_rules! impl_accessor_inner {
(@rest()) => {};
(@rest($field:ident { get<& $get:ty>, get_mut<&mut $get_mut:ty>, set<$set:ty> } $($rest:tt)*)) => {
fn $field(&self) -> &$get {
&self.$field
}
paste::item! {
fn [<$field _mut>] (&mut self) -> &mut $get_mut {
&mut self.$field
}
}
paste::item! {
fn [<set_ $field>] (&mut self, $field:$set) {
self.$field = $field;
}
}
impl_accessor_inner!(@rest($($rest)*));
};
}
impl_accessor! {
Struct {
name { get<&str>, get_mut<&mut String>, set<String> }
path { get<&Path>, get_mut<&mut PathBuf>, set<PathBuf> }
}
}
if_chain
if
とif let
を『まとめる』マクロif_chain!
を提供します。
let x: Option<i32> = todo!();
if let Some(x) = x {
if p(x) {
f(x);
} else {
g();
}
} else {
g();
}
を
use if_chain::if_chain;
let x: Option<i32> = todo!();
if_chain! {
if let Some(x) = x;
if p(x);
then {
f(x);
} else {
g();
}
}
のようにできます。else
節が必要なくてもインデントを2段までに抑える効果があります。
難点はRustの構文としては不正なため現在のrustfmtでは中がフォーマットできないことです。私はrustfmtっぽく手動フォーマットするのが面倒なので適当に書いちゃうことが多いです。
maplit
std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}
に対してvec!
のようなマクロを提供します。また各key, valueを指定の関数に通すconvert_args!
マクロも便利です。
結構歴史が長くポピュラーなクレートで、mapのようなデータ型を提供するライブラリは大抵これに似た形式のマクロを提供しています。
ちなみにpre 1.0時代からこんな提案があります。もしかしたら標準ライブラリに似たようなのが入るかもしれません。
-use std::collections::HashMap;
+use maplit::hashmap;
-let mut map = HashMap::new();
-map.insert("foo", 10);
-map.insert("bar", 20);
-map.insert("baz", 30);
+let map = hashmap!(
+ "foo" => 10,
+ "bar" => 20,
+ "baz" => 30,
+);
structopt
clap(コマンドラインパーサーのデファクトスタンダード)のラッパーです。
v0.3でAPIが大きく変わりました。attributeは一部の名前を除き、clap::Arg
のメソッドとして扱われます。
またclapのv0.3でstructoptと同じAPIのderive macroが追加される予定です。
How does
clap
compare to structopt?Simple!
clap
isstuctopt
. With the 3.0 release,clap
imported thestructopt
code into it's own codebase as theclap_derive
crate. Sincestructopt
already usedclap
under the hood, the transition was nearly painless, and is 100% feature compatible.If you were using
structopt
before, the only thing you should have to do is change the attributes from#[structopt(...)]
to#[clap(...)]
.Also the derive statements changed from
#[derive(Structopt)]
to#[derive(Clap)]
. There is also some additional functionality that's been added to theclap_derive
crate. See the documentation for that crate, for more details.
とのことなので安心して使いましょう。
数行で用意できるので引数を取らないアプリケーションで--help
のためだけに使うのも良いでしょう。
use structopt::StructOpt;
#[derive(StructOpt)]
struct Opt {}
fn main() -> anyhow::Result<()> {
Opt::from_args();
Ok(())
}
ちなみにコマンドラインパーサーにはclapの他に以下のものがあります。
See also:
-use clap::{App, Arg};
+use structopt::StructOpt;
use url::Url;
use std::ffi::OsStr;
use std::path::PathBuf;
static ARGS: &[&str] = &[
"",
"--norobots",
"--config",
"../config.yml",
"https://example.com",
];
-let matches = App::new(env!("CARGO_PKG_NAME"))
- .version(env!("CARGO_PKG_VERSION"))
- .author(env!("CARGO_PKG_AUTHORS"))
- .about(env!("CARGO_PKG_DESCRIPTION"))
- .arg(
- Arg::with_name("norobots")
- .long("norobots")
- .help("Ignores robots.txt"),
- )
- .arg(
- Arg::with_name("config")
- .long("config")
- .value_name("PATH")
- .default_value("./config.yml")
- .help("Path to the config"),
- )
- .arg(
- Arg::with_name("url")
- .validator(|s| s.parse::<Url>().map(drop).map_err(|e| e.to_string()))
- .help("URL"),
- )
- .get_matches_from_safe(ARGS)?;
-let norobots = matches.is_present("norobots");
-let config = PathBuf::from(matches.value_of_os("config").unwrap());
-let url = matches.value_of("url").unwrap().parse::<Url>().unwrap();
+#[derive(StructOpt)]
+#[structopt(author, about)]
+struct Opt {
+ /// Ignores robots.txt
+ #[structopt(long)]
+ norobots: bool,
+ /// Path to the config
+ #[structopt(long, value_name("PATH"), default_value("./config.yml"))]
+ config: PathBuf,
+ /// URL
+ url: Url,
+}
+let Opt {
+ norobots,
+ config,
+ url,
+} = Opt::from_iter_safe(ARGS)?;
assert_eq!(norobots, true);
assert_eq!(config, OsStr::new("../config.yml"));
assert_eq!(url, "https://example.com".parse().unwrap());
derivative
ドキュメント(docs.rsには何も書かれていない)
std
のderive macroの代替品を提供します。これらのマクロは型境界等を色々とカスタマイズできます。Default
にはDefault::default
と同じ値を返すメソッドnew() -> Self
を生やすオプションもあります。
See also: Rustのderiveはあまり頭がよくない
use std::fmt;
struct Foo {
name: String,
content: ExternalNonDebuggableItem,
}
impl fmt::Debug for Foo {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.debug_struct("Foo")
.field("name", &self.name)
.field("content", &format_args!("_"))
.finish()
}
}
↓
use derivative::Derivative;
use std::fmt;
#[derive(Derivative)]
#[derivative(Debug)]
struct Foo {
name: String,
#[derivative(Debug(format_with = "fmt_underscore"))]
content: ExternalNonDebuggableItem,
}
fn fmt_underscore(_: impl Sized, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "_")
}
derive_more
derivativeと同じくstd
のトレイトに対するderive macroを提供しますが、こちらはFrom
, Into
, Deref
, FromStr
, Display
等std
でderive macroが用意されていないものが対象です。
またnew
というメソッドを生やすderive macroがありますが、カスタマイズができないのでしたい場合は(ドキュメントにも書かれてますが)derive-newを使いましょう。
-use std::fmt;
+use derive_more::Display;
+#[derive(Display)]
+#[display(fmt = "({}, {})", _0, _1)]
struct Point2(f64, f64);
-
-impl fmt::Display for Point2 {
- fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
- write!(fmt, "({}, {})", self.0, self.1)
- }
-}
thiserror
std::error::Error
を対象とするderive macroを提供します。source
にはanyhowを使うといいでしょう。
use std::path::PathBuf;
use std::{fmt, io};
#[derive(Debug)]
enum Error {
ReadFile(PathBuf, io::Error),
Command(PathBuf, io::Error),
Reqwest(reqwest::Error),
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::ReadFile(path, _) => write!(fmt, "Failed to read {}", path.display()),
Self::Command(path, _) => write!(fmt, "Failed to execute {}", path.display()),
Self::Reqwest(err) => write!(fmt, "{}", err),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::ReadFile(_, source) | Self::Command(_, source) => Some(source),
Self::Reqwest(err) => err.source(),
}
}
}
↓
use thiserror::Error;
use std::io;
use std::path::PathBuf;
#[derive(Debug, Error)]
enum Error {
#[error("Failed to read {}", .0.display())]
ReadFile(PathBuf, #[source] io::Error),
#[error("Failed to execute {}", .0.display())]
Command(PathBuf, #[source] io::Error),
#[error("{}", .0)]
Reqwest(#[from] reqwest::Error),
}
num-derive
num-traitsの各トレイトを実装するderive macro集です。
- filedが1つのtuple struct (e.g.
struct Foo(i32)
) - variantがすべてunitのenum (e.g.
enum Bar { A, B }
)
が対象です。 このクレートはnumには含まれていません。
use num_derive::{FromPrimitive, Num, NumCast, NumOps, One, ToPrimitive, Zero};
#[derive(Zero, One, FromPrimitive, ToPrimitive, NumCast, NumOps, PartialEq, Num)]
struct Newtype(i64);
#[derive(FromPrimitive, ToPrimitive)]
enum Enum {
A,
B,
C,
}
strum
enum
の文字列への/からの変換に特化したderive macro集です。
FromStr
やDisplay
の実装はもちろん、&'static str
へ変換したりclap::arg_enum!
のように対応する文字列を&[&str]
で得たり個数を定数で得たり各バリアントからフィールドを抜いたenum
を生成したりすることができます。
少し前までstrumとstrum-macroの2つのクレートを使う形でしたが0.16.0からfeature-gatedの形でderive macroをre-exportするようになりました。ドキュメントはそのままのようですが..
[dependencies]
strum = { version = "0.17.1", features = ["derive"] }
serdeと同様の問題を抱えていますが(前はメソッドを直に生やしてたのがトレイトを経由するようになり、それらがマクロ名と衝突する)皆がfeatureを有効化すれば良い話なので使いましょう。
See also: serdeの `derive` feature と名前空間の困った関係
-use std::convert::Infallible;
-use std::str::FromStr;
+use structopt::StructOpt;
+use strum::{EnumString, EnumVariantNames, VariantNames as _};
#[derive(StructOpt)]
struct Opt {
/// Output format
#[structopt(
long,
value_name("FORMAT"),
default_value("human"),
possible_values(Format::VARIANTS)
)]
format: Format,
}
+#[derive(EnumString, EnumVariantNames)]
+#[strum(serialize_all = "kebab-case")]
enum Format {
Human,
Json,
}
-
-impl Format {
- const VARIANTS: &'static [&'static str] = &["human", "json"];
-}
-
-impl FromStr for Format {
- type Err = Infallible;
-
- fn from_str(s: &str) -> Result<Self, Infallible> {
- match s {
- "human" => Ok(Self::Human),
- "json" => Ok(Self::Json),
- _ => todo!(),
- }
- }
-}
derive-new
コンストラクタを"new"
という名のメソッドとして生やすderive macroです。
derive_more::Constructor
とは違い、fieldの値を設定することで引数を減らすことができます。
引数を取らず、Default::default
と同じ値を返すのならderivativeの#[derivative(Default(new = "true"))]
を使うといいでしょう。
+use derive_new::new;
+
+#[derive(new)]
struct Item {
x: i32,
+ #[new(value = "42")]
y: i32,
}
-
-impl Item {
- fn new(x: i32) -> Self {
- Self { x, y: 42 }
- }
-}
getset
getterとsetterを生やすderive macroです。
難点は個々のgetter/setterにdocが設定できない(フィールドのdocが使われる)点と&Deref::Target
(e.g. &String
→ &str
)を返すgetterを作れない点です。ただpub(crate)
までなら問題にはならないでしょう。
+use getset::{CopyGetters, MutGetters, Setters};
+
+#[derive(CopyGetters, MutGetters, Setters)]
pub(crate) struct Item {
+ #[get_copy = "pub(crate)"]
+ #[get_mut = "pub(crate)"]
+ #[set = "pub(crate)"]
param: u64,
}
-
-impl Item {
- #[inline]
- pub(crate) fn param(&self) -> u64 {
- self.param
- }
-
- #[inline]
- pub(crate) fn param_mut(&mut self) -> &mut u64 {
- &mut self.param
- }
-
- #[inline]
- pub(crate) fn set_param(&mut self, param: u64) {
- self.param = param;
- }
-}
derive_builder
builder patternのbuilderを生成するマクロです。
.build()
の返り値はResult<_, _>
のみのようです。
use derive_builder::Builder;
#[derive(Builder)]
struct Foo {
required_param: String,
#[builder(default)]
optional_param: Option<String>,
}
let item = FooBuilder::default()
.required_param("foo".to_owned())
.optional_param(Some("bar".to_owned()))
.build()?;
easy-ext
extension trait patternのためのproc-macroです。
+use easy_ext::ext;
use termcolor::{BufferedStandardStream, ColorChoice};
-trait BufferedStandardStreamExt {
- fn stderr_with_atty_filter(choice: ColorChoice) -> Self;
-}
-
-impl BufferedStandardStreamExt for BufferedStandardStream {
+#[ext]
+impl BufferedStandardStream {
fn stderr_with_atty_filter(choice: ColorChoice) -> Self {
let choice = if choice == ColorChoice::Always || atty::is(atty::Stream::Stderr) {
choice
} else {
ColorChoice::Never
};
Self::stderr(choice)
}
}
let _: fn(_) -> _ = BufferedStandardStream::stderr_with_atty_filter;
ふと試したところimpl <T: Trait> _Dummy for T { .. }
とすることでトレイトに対して実装できるようです。
use easy_ext::ext;
use std::{
fmt,
io::{self, Sink},
};
use termcolor::{NoColor, WriteColor};
#[ext]
impl<W: WriteColor> __ for W {
/// [`cargo::core::shell::Shell::warn`]のようなやつ
///
/// [`cargo::core::shell::Shell::warn`]: https://docs.rs/cargo/0.44.1/cargo/core/shell/struct.Shell.html#method.warn
fn warn<T: fmt::Display>(&mut self, message: T) -> io::Result<()> {
todo!();
}
}
let _: fn(_, _) -> _ = NoColor::<Sink>::warn::<&str>;
注意すべき点として、生成されるトレイトの名前は指定できますが今現在そのvisibilityを設定する方法はありません。
Consider adding another way to specify visibility #13
型を強化する
ghost
ジェネリックなPhantomData
を生成します。
use ghost::phantom;
#[phantom]
pub struct MyPhantom<T: ?Sized>;
このMyPhantom
は
pub enum MyPhantom<T: ?Sized> {
__Phantom(self::__void_MyPhantom::MyPhantom<T>),
MyPhantom,
}
のように展開されます。
何が嬉しいのかと言うと自分で書く型のため、自由にトレイトを実装することができます。 READMEにある例のようにIntoIterator
を実装する等といった使い方があります。
use ghost::phantom;
use std::vec;
fn main() {
for item in Registry::<Flag> {
let _: &'static str = item;
}
}
#[phantom]
struct Registry<T: ?Sized>;
enum Flag {}
impl IntoIterator for Registry<Flag> {
type Item = &'static str;
type IntoIter = vec::IntoIter<&'static str>;
fn into_iter(self) -> vec::IntoIter<&'static str> {
vec!["foo", "bar", "baz"].into_iter()
}
}
ref-cast
String
やPathBuf
のnewtypeを作りたくなったとします。ここでは必ず絶対パスを示すAbsPathBuf
を作りたいとします。
use std::ffi::OsString;
use std::path::Path;
/// An owned, mutable absolute path.
pub(crate) struct AbsPathBuf(OsString);
impl AbsPathBuf {
#[inline]
fn unchecked(s: OsString) -> Self {
Self(s)
}
#[inline]
fn new(s: OsString) -> Option<Self> {
if Path::new(&s).is_absolute() {
Some(Self::unchecked(s))
} else {
None
}
}
}
AbsPathBuf
と来たらAbsPath
を作りたくなります。これもいいですが
pub(crate) struct AbsPath<'a>(&'a OsStr);
せっかくなのでこうしたいです。
pub(crate) struct AbsPath(OsStr);
そうすると少し困ったことになります。&OsStr
や&AbsPathBuf
からどうやって&AbsPath
を得るのでしょうか?
impl AbsPath {
fn unchecked(s: &OsStr) -> &Self {
todo!("what to do?")
}
}
impl AbsPathBuf {
fn as_abs_path(&self) -> &AbsPath {
todo!("what to do?")
}
}
実はunsafeな手段しかありません。
#[repr(transparent)]
pub(crate) struct AbsPath(OsStr);
impl AbsPath {
fn unchecked(s: &OsStr) -> &Self {
unsafe { &*(s as *const OsStr as *const AbsPath) }
}
}
impl AbsPathBuf {
fn as_abs_path(&self) -> &AbsPath {
AbsPath::unchecked(&self.0)
}
}
ここで注意しなければならないこととして、安全性を確保するために#[repr(C)]
か#[repr(transparent)]
を付ける必要があります。さて、何とかできましたがunsafe
が出てしまいました。少し収まりが悪いです。
ここでこのクレートの出番です。ref-castはこのunsafe操作を肩代りしてくれます。何が嬉しいのかと言うとコードの見た目からunsafe
が消えます。実は#[forbid(unsafe_code)]
にも引っ掛らなくなるのでsafety-danceにも多分参加できます。
また"validation"も含めて簡易に実装できるようにする、validated-sliceというクレート(作者は@lo48576氏)があるみたいです。
ちなみにこの例、本気でPath
& PathBuf
のように振る舞うnewtypeを作ろうと思うと山のようなメソッドとトレイトを実装しなければなりません。しかも不変条件を加味して調節しなければならないので辛いです。私もすこし前Haskellのpathのようなtyped_pathというクレートを作ろうとしたのですが1コミットもしないままローカルに眠っています((str
| OsStr
) × (絶対パス | 制限無し) × (file_name
が存在 | 制限無し) × (セパレータをネイティブに | /
に | 制限無し)という制約を考えてました)。 やるんだったらurl
のようにBuf
版だけ用意するのを検討することをおすすめします。
今のところPath
(Buf
)のnewtypeを提供するクレートは以下のものがあります。
+use ref_cast::RefCast
use std::ffi::OsStr;
/// An absolute path.
+#[derive(RefCast)]
#[repr(transparent)]
pub(crate) struct AbsPath(OsStr);
impl AbsPath {
fn unchecked(s: &OsStr) -> &Self {
- unsafe { &*(s as *const OsStr as *const AbsPath) }
+ Self::ref_cast(s)
}
}
dimensioned
物理量を表現できます。
use dimensioned::si;
use static_assertions::{assert_impl_all, assert_not_impl_any};
use std::ops::{Add, Div};
assert_eq!((3.0 * si::M / (2.0 * si::S)).to_string(), "1.5 m*s^-1");
assert_impl_all!(si::Meter<f64>: Div<si::Second<f64>, Output = si::MeterPerSecond<f64>>);
assert_not_impl_any!(si::Meter<f64>: Add<si::Second<f64>>);
ascii
ASCII文字列を表現するAsciiChar
, AsciiStr
, AsciiString
を提供します。
それぞれの表現はu8
, [u8]
, Vec<u8>
でチェック以外のオーバーヘッドはありません。 これらはASCII文字列を扱うにあたって(u8
, [u8]
, Vec<u8>
)と(char
, str
, String
)の両方の長所を持ちます。
use ascii::{AsciiChar, AsciiString};
let mut s = "abcde\n".parse::<AsciiString>()?;
s[2] = AsciiChar::C;
assert_eq!(s, "abCde\n");
assert_eq!(s.trim_end(), "abCde");
unicase
case-insensitiveに2つの文字列を等値判定する関数と、それをPartialEq
に使うnewtypeを提供します。
use unicase::UniCase;
let s1 = UniCase::new("aaa");
let s2 = UniCase::new("AAA");
assert_eq!(s1, s2);
ordered-float
Ord
(: Eq
)を実装するNotNan<_: Float>
とOrderedFloat<_: Float>
を提供します。
Rustの比較演算子用のインターフェイスにはEq
, Ord
の他にPartialEq
, PartialOrd
があります。具体的な要請はリンク先を参照してください。比較演算子にはPartialEq
とPartialOrd
が使われます。
何故このような区分があるのかというと一つは浮動小数点数のためです。f32
, f64
はPartialEq
, PartialOrd
を実装していますがEq
, Ord
は実装していません。つまりソート等が行なえません。というのもIEEE 754の浮動小数点数においては==
の反射律すら成り立たないからです。他の言語においても配列のソートや順序を使うデータ構造の構築にNaN
のような値を混ぜるとその結果は保証されません。C++に至っては鼻から悪魔が出てきます。Rustではこのような関数やデータ構造に要素がtotal orderであることを要求することで『浮動小数点数をソートしたら何か変』という事態を防いでいます。
さて、我々Rustユーザーがソート等にどうやって浮動小数点数を使うのかというと... このうち関数については.foo()
に対して大抵.foo_by(impl FnMut(..) -> std::cmp::Ordering)
のようなものが共に用意されているのでこれを使えばどうにかすることができます。例えばソートに関しては雑にこのようなことができます。この場合、NaN
が混じっていると.unwrap()
の部分でpanicします。
let mut xs = vec![2.0, 1.0, 0.0];
xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
max
やmin
も同様にIterator::max_by
を使うかあるいはf64::max
でfold
すれば良いです。
use std::f64;
static XS: &[f64] = &[0.0, 1.0, 2.0];
let max = XS.iter().copied().fold(f64::NEG_INFINITY, f64::max);
これがBTreeMap
やBinaryHeap
等のデータ構造に使うとなるとこうはいきません。f32
/f64
に対するnewtypeが必要になります。そこでこのクレートの出番です。
use std::collections::BinaryHeap;
let mut queue = BinaryHeap::<f64>::new();
// error[E0599]: no function or associated item named `new` found for type `std::collections::BinaryHeap<f64>` in the current scope
// --> src/lib.rs:6:36
// |
// 5 | let mut queue = BinaryHeap::<f64>::new();
// | ^^^ function or associated item not found in `std::collections::BinaryHeap<f64>`
// |
// = note: the method `new` exists but the following trait bounds were not satisfied:
// `f64 : std::cmp::Ord`
NotNan<_>
は名の通りです。四則演算の結果NaN
になったのなら整数型と同様にpanicします。ただしこちらはrelease buildでもチェックされます。
実際に使う際ですが、f64
からNotNan<f64>
に変換するにはNotNan::new(x).unwrap()
とする必要があります。 これは少々冗長です。 ただNotNan<f64>
はFromStr
を実装していて、また四則演算の右辺には中身の型が許されているのでNatNan::new
を呼ぶ回数は少ないと思われます。
use ordered_float::NotNan;
let x = "42.0".parse::<NotNan<_>>().unwrap();
let half: NotNan<_> = x / 2.0;
let plus_one: NotNan<_> = x + 1.0;
リテラルが必要になる場合にはマクロを一行用意すると良いでしょう。
macro_rules! notnan(($lit:literal $(,)?) => (NotNan::new($lit).unwrap())); // NaNのリテラルは無い
let y = if p(x) { x } else { notnan!(1.0) };
OrderedFloat<_>
はNaN
を許容しますがそれを「最大の値であり、自身と等しい」としています。
こちらはf64
からの変換は容易ですがNotNan
と違い四則演算自体ができません。std::cmp::Reverse
のようなものだと思いましょう。
im
std::collections::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, vec::Vec}
のimmutable data版を提供します。
-let mut map = maplit::hashmap!();
-map.insert("foo", 42);
+let map = im::hashmap!();
+let map = map.update("foo", 42);
OCamlやHaskell等を使っている方は馴染み深いのではないでしょうか。それぞれの中身はこんな感じのようです。
ただしAPIはstd
のものに寄せられており、たびたび&mut self
が要求されます。
let mut xs = im::vector![1, 2, 3];
xs.push_back(4);
imとim-rcに分けられていますがその違いは前者はArc
、後者はRc
を使っていることです。
im-rcのデータ型はSync
でもSend
でもなく、複数のスレッドから参照するのはもちろん、他スレッドにmoveすることもできません。
ちなみにArc
をRc
にすることで得られる恩恵は"20-25% increase in general performance"だそうです。
indexmap
IndexMap
, IndexSet
とそれらを生成するmaplit風のコンストラクタ用マクロindexmap!
, indexset!
を提供します。
JavaのLinkedHashMap
, C#のOrderedDictionary
, PythonのOrderedDict
のようなものです。
「Vec<(K, V)>
にしたいけどK
の重複が無いことを示したい」というとき等に便利です。またserde(特に"ser"の方)と相性がいいです。
use indexmap::IndexMap;
static JSON: &str = r#"{"c":3,"b":2,"a":1}"#;
let map = serde_json::from_str::<IndexMap<String, u32>>(JSON)?;
assert_eq!(serde_json::to_string(&map)?, JSON);
今のRustでは不可能な抽象化を擬似的に表現する
typenum
「型パラメータとしての整数」を表現します。
4桁以下なら定数が揃っていますがそれより大きい値は2つの既存の値を+-*/
したりビットで表現することで生み出します。
generic-array
[T; _]
のように振る舞う、長さをtypenum::UInt
で表現するGenericArray<T, _>
を提供します。
ちなみにGenericArray<T, _>: IntoIterator<Item = T>
です。(これを書いているときのRust 1.40の時点では[T; _]: IntoIterator
ではありません。)
std
の挙動を改善する
crossbeam
並行処理を行なう低レベルのライブラリです。
rayon(並列処理用のライブラリ), tokio(非同期処理用のライブラリ)に使われています。crossbeamが提供する機能はそれらより低レベルで、std::sync
の改善に焦点をあてています。std::sync
+ std::thread
で苦労したのなら使うと幸せになれるかもしれません。
- MPSC (Multi-Producer Single-Consumer) channelの欠点を解消し、パフォーマンスも向上したMP M C (Multi-Producer Multi -Consumer) channle
- MPMC queue
- work-stealing deque
- GC
- 複数のスレッドをspawnしてその場でそれらの終了を待つ、
'static
を要求しない関数
など提供するものは多岐にわたります。
実際に使うときはbloatを避けるため、個別のsubcrate (crossbeam-*)を使うと良いでしょう。
See also:
- MultiProducer-MultiConsumer (MPMC) channel using crossbeam-channel - Qiita
- Lock-free Rust: Crossbeam in 2019 - stjepang.github.io
-let (tx, rx) = std::sync::mpsc::channel();
+let (tx, rx) = crossbeam_channel::unbounded();
tx.send(42)?;
assert_eq!(rx.recv()?, 42);
libm
libmのRust実装です。
最近になってからコンパイラに使われるようにようになったみたいです。(丸め方法等を除いて)プラットフォームに依らない挙動をしていて、数値を扱うクレートに結構使われています。
-let val = 10f64.powf(-9.0);
+let val = libm::pow(10.0, -9.0);
assert_eq!(error.to_string(), "0.000000001");
remove_dir_all
Windows用に改善したstd::fs::remove_dir_all
を提供します。Windows以外にはstd::fs::remove_dir_all
をそのままre-exportしてます。
-std::fs::remove_dir_all("./dir")?;
+remove_dir_all::remove_dir_all("./dir")?;
which
実行ファイルを探します。
Windowsではrust-lang/rust#37519があるのでそれを回避する目的で使えます。
注意するべき点として、デフォルトでエラーがfailure::Fail
です。default-features = false
でstd::error::Error
になりますが依存クレートのアップデートで壊れる可能性が付き纏います。 使うときはこのようにすれば良いでしょう。
use anyhow::anyhow;
let rustup = which::which("rustup")
.map_err(|e| anyhow!("{}", e.kind()).context("failed to find `rustup`"))?;
dunce
file pathを正規化するためのAPIとして、std
にはstd::fs::canonicalize
とそのショートカットのPath::canonicalize
があります。
ただしWindows上では、このcanonicalize
は\\?\C:\foo\bar
のような"UNC path"を返します。 そしてマイクロソフト製のものを含め、多くのソフトウェアはUNC pathを扱うことができません。 Rustのstd::fs
も同様です。
このクレートは問題の無いときに限りUNC pathを通常のものに変換するsimplified
と、それを用いたcanonicalize
の2つの関数を提供します。 Windows上でなければsimplified
は何も行ないません。
-let path = path.canonicalize()?;
+let path = dunce::canonicalize(path)?;
テストに役立つ
pretty_assertions
pretty_assertions::{assert_eq!, assert_ne!}
を提供します。
これらが失敗したとき、メッセージがDebug
のpretty-printed表示のカラフルなdiffで表示されます。
str
に対しては下のdifferenceを使うことをおすすめします。
-assert_eq!(4, 2 + 2);
+pretty_assertions::assert_eq!(4, 2 + 2);
difference
str
のdiffが取れる関数のほか、それを使ったマクロ assert_diff!
を提供します。
+use difference::assert_diff;
+
static EXPECTED: &str = "foo\nbar\n";
static ACTUAL: &str = "foo\nbar\n";
-assert_eq!(EXPECTED, ACTUAL);
+assert_diff!(EXPECTED, ACTUAL, "\n", 0);
static_assertions
コンパイル時の"assertion"を行なうマクロを多数提供します。
use static_assertions::{assert_cfg, assert_eq_align, assert_eq_size, const_assert_eq};
assert_cfg!(
any(unix, windows),
"There is only support for Unix or Windows",
);
assert_eq_size!(i64, u64);
assert_eq_align!([i32; 4], i32);
const_assert_eq!(1, 1);
approx
f32
やf64
の等値判定が楽にできます。
ところでこれを使ってPartialEq
を実装したくなりますが実はPartialEq
も推移律は要求されるので良くありません。approxはPartialEq
の代わりに絶対誤差(+ 相対誤差)の指定の元で等値判定するAbsDiffEq
とRelativeEq
というトレイトを提供しています。こちらを実装しましょう。
use approx::{abs_diff_eq, relative_eq};
abs_diff_eq!(1.0, 1.0);
relative_eq!(1.0, 1.0);
version-sync
READMEを含むドキュメントに書かれている現在のパッケージ($CARGO_PKG_NAME
)を指しているバージョンのうち、現在のバージョン($CARGO_PKG_VERSION
)と異なるものを検出してくれます。
assert_cmd
bin
クレートの入力→出力を確かめるintegration-test
に使えます。
use predicates::Predicate;
use std::{str, time::Duration};
#[test]
fn test() -> std::result::Result<(), assert_cmd::cargo::CargoError> {
assert_cmd::Command::cargo_bin("myapp")?
.args(&["--noconfirm", "--config", "./config.toml"])
.timeout(Duration::from_secs(10))
.write_stdin(&b"input\n"[..])
.assert()
.success()
.stdout(str_predicate(|s| s.starts_with("output\n")))
.stderr("");
Ok(())
}
fn str_predicate(p: impl Fn(&str) -> bool) -> impl Predicate<[u8]> {
predicates::function::function(move |s| matches!(str::from_utf8(s), Ok(s) if p(s)))
}
tempfile
名の通りです。 一時ファイルと一時ディレクトリを生成できます。
assert_cmdと併用してbin
クレートのテストにも使うことができます。
#[cfg(test)]
mod tests {
use std::{
fs::File,
io::{self, Write as _},
path::Path,
};
use tempfile::{NamedTempFile, TempDir, TempPath};
#[test]
fn tempfile() -> io::Result<()> {
// 一時ファイル。
let mut tempfile: File = tempfile::tempfile()?; // should be automatically removed by the OS
writeln!(tempfile, "aaaaa")
}
#[test]
fn named_tempfile() -> anyhow::Result<()> {
// 一時ファイルとそのパス。 `NamedTempFile`と`TempPath`からなる。
let mut named_tempfile = NamedTempFile::new()?;
writeln!(named_tempfile, "bbbbb")?;
let (_, temp_path): (File, TempPath) = named_tempfile.into_parts();
let _: &Path = temp_path.as_ref();
if false {
temp_path.persist("./result.txt")?;
}
Ok(())
}
#[test]
fn tempdir() -> io::Result<()> {
// 一時ディレクトリ。 一応`Drop`時に対象のディレクトリの消去を試みるが、Windows等のために明示的に`close`する方が良いように思える。
let tempdir: TempDir = tempfile::tempdir()?;
let tempdir_path: &Path = tempdir.path();
let tempdir_path = tempdir_path.to_owned();
assert!(tempdir_path.is_dir());
tempdir.close()?;
assert!(!tempdir_path.exists());
Ok(())
}
#[test]
fn spooled_tempfile() -> io::Result<()> {
// ある大きさまでファイルシステムにアクセスせずにin memoryに保存する、一時ファイルのように振る舞うオブジェクト。
let mut spooled_tempfile = tempfile::spooled_tempfile(10);
write!(spooled_tempfile, "0123456789")?;
assert!(!spooled_tempfile.is_rolled());
write!(spooled_tempfile, "a")?;
assert!(spooled_tempfile.is_rolled());
Ok(())
}
}
test-case
ある関数をテストする際、このようにassert_eq!
を並べることがあると思います。
fn equiv_mod(a: u32, b: u32, m: u32) -> bool {
(a % m) == (b % m)
}
#[cfg(test)]
mod tests {
use super::equiv_mod;
#[test]
fn test_equiv_mod_10() {
assert_eq!(equiv_mod(0, 0, 10), true);
assert_eq!(equiv_mod(0, 10, 10), true);
assert_eq!(equiv_mod(1, 2, 10), false);
}
}
そしてそれを関数やマクロでまとめることもあると思います。
#[cfg(test)]
mod tests {
use super::equiv_mod;
#[test]
fn test_equiv_mod_10() {
macro_rules! test(($($a:expr, $b:expr => $expected:expr,)*) => {
$(
{
let (a, b) = ($a, $b);
assert_eq!(equiv_mod(a, b, 10), $expected, "`equiv_mod({}, {}, 10)`", a, b);
}
)*
});
test! {
0, 0 => true,
0, 10 => true,
1, 2 => false,
}
}
}
test-case
はそのような単純なassertを簡潔に書けるようにするマクロです。
#[cfg(test)]
mod tests {
use super::equiv_mod;
use test_case::test_case;
#[test_case(0, 0 => true; "0_equiv_0_mod_10")]
#[test_case(1, 11 => true; "1_equiv_11_mod_10")]
#[test_case(1, 2 => false; "1_not_equiv_2_mod_10")]
fn test_equiv_mod_10(a: u32, b: u32) -> bool {
equiv_mod(a, b, 10)
}
}
test_case::test_case!
は記述したテストケースをこのように個別のテストに展開します。
running 3 tests
test tests::test_equiv_mod_10::_0_equiv_0_mod_10 ... ok
test tests::test_equiv_mod_10::_1_equiv_11_mod_10 ... ok
test tests::test_equiv_mod_10::_1_not_equiv_2_mod_10 ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
rusty-fork
ユニットテストを別プロセスで走らせることができるマクロです。
プロセスがabort等で終了しても他のテストが走り続けます。 またテストに対してタイムアウトを設定することができます。
#[cfg(test)]
mod tests {
use rusty_fork::rusty_fork_test;
use std::{env, fs};
rusty_fork_test! {
#![rusty_fork(timeout_ms = 1000)]
#[test]
fn test1() {
test_set_current_dir("./dir-a")
}
#[test]
fn test2() {
test_set_current_dir("./dir-b")
}
#[test]
fn test3() {
test_set_current_dir("./dir-c")
}
}
fn test_set_current_dir(dir_file_name: &str) {
let dir = env::current_dir().unwrap().join(dir_file_name);
fs::create_dir_all(&dir).unwrap();
env::set_current_dir(&dir).unwrap();
assert_eq!(env::current_dir().unwrap(), dir);
}
}
trybuild
コンパイルエラーのメッセージをassertするためのクレートです。
マクロ以外にも複雑な型境界を持つ普通の関数にも使えます。
use trybuild::TestCases;
#[test]
fn ui() {
TestCases::new().compile_fail("tests/ui/**/*.rs");
}
使い方はREADMEの画像を見た方がわかりやすいでしょう。
テスト対象の.rs
ファイルをディレクトリに入れ、走らせることでファイルパスがマスクされた.stderr
ファイルが生成されます。 これを.rs
ファイルと同じ場所に放り込めばテストが出来上がります。
TRYBUILD=overwrite
と環境変数をセットした場合、.stderr
ファイルは最初から同じディレクトリに生成されます。
insta
Debug
表記やJSON/YAML/RONの文字列でassert_eq!
を行います。
trybuildと同じようにテストが書かれた.rs
ファイルと同ディレクトリにsnapshots/{name}___snapshots.snap
というファイルが生成され、それが比較対象になります。
操作をインタラクティブに行うためにcargo-insta
というものが用意されているのでそれを使うのが良いでしょう。
use insta::assert_debug_snapshot;
#[test]
fn test_snapshots() {
let value = vec![1, 2, 3];
assert_debug_snapshot!(value);
}
$ cargo insta test --review
パフォーマンスの向上
take_mut
Rustでは&mut T
の値にはそのまま(T) -> T
の関数を適応することができません。Copy
でもない限り何らかの壊れていない値を代わりに詰めておく必要があります。良く使われる方法がstd::mem::replace
(またはRust 1.40で安定化されたstd::mem::take
)でダミーの値を詰めることですがそのような生成に副作用を伴わず軽量な値が常に用意できるとは限りません。
take_mut::take
ならダミーの値を用意しなくても(T) -> T
の関数を適応できます。返せなくなったとき、すなわちpanicした場合プログラムをabortさせます。
あとパニックしたときのリカバリを設定できるtake_or_resover
やスコープ内でRefCell
のようにmove操作を実行時に判定するscope
という関数もあります。
See also: Rustのパニック機構
use either::Either;
let mut data: Either<LargeData, LargeData> = todo!();
if let Either::Left(data) = &mut data {
- *data = convert(data.clone());
+ take_mut::take(data, convert);
}
fn convert(_: LargeData) -> LargeData {
todo!()
}
buf_redux
std::io::{BufReader, BufWriter, LineWriter}
と入れ替えるだけで速くなるみたいです。
-use std::io::{BufReader, BufWriter, LineWriter};
+use buf_redux::{BufReader, BufWriter, LineWriter};
stable_deref_trait
このような構造体Text
とWord
を考えてみます。
use std::str::FromStr;
struct Text(Vec<Word>);
enum Word {
Plain(String),
Number(String),
Whitespace(String),
Lf,
}
impl Text {
fn diff(&self, other: &Self) -> Result<(), Diff> {
todo!()
}
}
impl FromStr for Text {
type Err = anyhow::Error;
fn from_str(_: &str) -> anyhow::Result<Self> {
todo!("parse with `nom` or `combine` or something")
}
}
ここでString
を沢山作るのは良くないと考えてWord
をこのようにするとします。
enum Word<'a> {
Plain(&'a str),
Number(&'a str),
Whitespace(&'a str),
Lf,
}
Box::leak
でリークするのも良くないと考えてWord
にライフタイムパラメータを持たせるとします。そうするとText
はこうなります。
struct Text<'a>(Vec<Word<'a>);
そうするとテキスト全体を表わすString
の置き場所が困るので以下のようにしたいです。
struct Text(String, Vec<Word<'this>);
ここで自己参照することで問題を解決します。ここからはunsafeな操作になります。自己参照というとpinがありますが今回はこれを使わなくても安全性を確保できます。まず細心の注意を払いstd::mem::transmute
でライフタイムパラメータを強制的に'static
にします。ここでmutableな操作と自身のライフタイムを伴わない形での&str
の持ち出しを防ぐためmod
を切って『安全ではない』範囲を最小にします。
use self::inner::TextInner;
struct Text(TextInner);
enum Word<'a> {
Plain(&'a str),
Number(&'a str),
Whitespace(&'a str),
Lf,
}
mod inner {
use super::Word;
use std::mem;
pub(super) struct TextInner {
string: String,
words: Box<[Word<'static>]>, // 実際は`'static`ではない!
}
impl TextInner {
// `impl FnOnce(&str) -> Box<[Word]>`は`impl for<'a> FnOnce(&'a str) -> Box<[Word<'a>]>`の略。
// `&str`や`Word`を外に持ち出すことはできない。自己参照でなくても良く使われる。
pub(super) fn new(string: String, words: impl FnOnce(&str) -> Box<[Word]>) -> Self {
// - `TextInner`はimmutable
// - 各`&str`は`&'static str`として流出することはない。
unsafe {
Self {
words: mem::transmute(words(&string)),
string,
}
}
}
pub(super) fn string(&self) -> &str {
&self.string
}
pub(super) fn words<'a>(&'a self) -> &[Word<'a>] {
&self.words
}
}
}
あとの問題はTextInner
がmoveしたときにstring: String
からの&str
が壊れないかです。ここでこのクレートとstatic_assertionsの出番です。これらを使って安心を得ます。
+use stable_deref_trait::StableDeref;
+use static_assertions::assert_impl_all;
+
+assert_impl_all!(String: StableDeref<Target = str>);
StableDeref
は(unsafeな操作でライフタイムを誤魔化しながら)自身がmoveしても&Deref::Target
が壊れないものだけに実装されているunsafe traitです。String
は無理矢理moveしても&<String as Deref>::Target
(= &str
)は壊れないのでString: StableDeref
です。ここで&String
は壊れることに注意してください。上の例で&str
の代わりに&String
を持つとアウトです。
またCloneStableDeref
はStableDeref
のサブセットで、clone
しても問題ないもの(&_
, Rc<_>
, Arc<_>
)に対して実装されています。
rental
stable_deref_traitで紹介した方法を自動でやるマクロです。mod
ごと生成します。またコンストラクタのフィールドの順番も問題無いように並びかえてくれます。
#[macro_use]
extern crate rental;
rental! {
mod inner {
use super::Word;
#[rental]
pub(super) struct TextInner {
string: String,
words: Box<[Word<'string>]>,
}
}
}
owning_ref
Object: StableDeref
と&PartOfObjectDeref
の組をOwningRef<Object, PartOfObjectDeref>
またはOwningRefMut<〃, 〃>
として持てます。ただし"ref"の側は&_
/&mut _
である必要があり、上のText
, Word<'_>
の例等では使えません。
use anyhow::anyhow;
use once_cell::sync::Lazy;
use owning_ref::OwningRef;
use regex::Regex;
let pair = OwningRef::new(" foo ".to_owned()).try_map(|s| {
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("([a-z]+)").unwrap());
REGEX
.captures(&s)
.map(|caps| caps.get(1).unwrap().as_str())
.ok_or_else(|| anyhow!("not matched"))
})?;
assert_eq!(&*pair, "foo");
assert_eq!(pair.as_owner(), " foo ");
smallvec
C++のboost::container::small_vector
のようなものです。
ある長さまで要素を『直に』持ち、残りをVec
のようにヒープアロケートします。
「大体は要素数1だけど複数ある場合も考えなくてはならない」といった場合にフィットします。
+use smallvec::SmallVec;
+
use std::iter;
let probably_single_values = (0..100)
.map(|i| generate_values(i).collect())
- .collect::<Vec<Vec<_>>>();
+ .collect::<Vec<SmallVec<[_; 1]>>>();
arrayvec
C++のboost::container::static_vector
のようなものです。
こちらはヒープアロケーションを行なわず一定数までしか要素を持てません。
no-std環境の他「本当に高々<コンパイル時定数>個の要素しか持たない」といった場合にも使えます。
-let mut at_most_100 = vec![];
+use arrayvec::ArrayVec;
+
+let mut at_most_100 = ArrayVec::<[_; 100]>::new();
at_most_100.push(42);
類似クレート:
arraydeque
容量が無いときにエラーにするか反対側の要素を削除(排出)するかを選べます。
use arraydeque::ArrayDeque;
let mut deque = ArrayDeque::<[_; 10]>::new();
deque.push_front(1)?;
deque.push_back(1)?;
let mut deque = ArrayDeque::<[_; 10], arraydeque::Wrapping>::new();
assert!(deque.push_front(1).is_none());
assert!(deque.push_back(1).is_none());
enum-map
unit variantのみからなるenum
をキーとする、中身が[_; $num_variants]
のマップを作れます。
-use maplit::btreemap;
+use enum_map::{enum_map, Enum};
-#[derive(PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Enum)]
enum Key {
A,
B,
C,
}
-let map = btreemap! {
+let map = enum_map! {
Key::A => 1,
Key::B => 2,
Key::C => 3,
};
enumset
enum-mapのset版のようなものです。(作者は別です)
中身はu8
, u16
, u32
, u64
, u128
のうち全variantが収まる最小のもので表現されるビット列です。
-use maplit::btreeset;
+use enumset::EnumSetType;
-#[derive(PartialEq, Eq, PartialOrd, Ord)]
+#[derive(EnumSetType)]
enum Value {
A,
B,
C,
}
-let set = btreeset!(Value::A, Value::B);
-assert!(set.contains(&Value::A));
-assert!(set.contains(&Value::B));
-assert!(!set.contains(&Value::C));
+let set = Value::A | Value::B;
+assert!(set.contains(Value::A));
+assert!(set.contains(Value::B));
+assert!(!set.contains(Value::C));
phf
コンパイル時に生成できるhash mapとhash setです。
-use maplit::hashmap;
-use once_cell::sync::Lazy;
-use std::collections::HashMap;
+use phf::phf_map;
-static MAP: Lazy<HashMap<&str, i32>> = Lazy::new(|| {
- hashmap!(
- "foo" => 1,
- "bar" => 2,
- )
-});
+static MAP: phf::Map<&str, i32> = phf_map!(
+ "foo" => 1,
+ "bar" => 2,
+);
assert_eq!(MAP["foo"], 1);
assert_eq!(MAP["bar"], 2);
コードの見た目を美しくする
indoc
インデントされた文字列リテラルを文字列リテラルのままunindentするマクロです。
+use indoc::indoc;
-static S: &str = r"---
-foo: 42
-bar: {}
-";
+static S: &str = indoc!(
+ "
+ ---
+ foo: 42
+ bar: {}
+ "
+);
raw string literalの書き方が気に入らない場合に綺麗な書き方ができます。
またbyte string literalにも対応しています。
static S: &[u8] = indoc!(b"foo");
unindentの処理((&str) -> String
)自体はunindentというクレートで提供されています。