RustにはCargoという優れたパッケージマネージャーがあるので細かい多数の依存パッケージが問題になることはあまりありません。というよりパッケージを細かく分割することが推奨されています。ちょっとしたボイラープレートを取り除くような小さなライブラリも大量にあります。これらは積極的に使うべきです。
問題があるとすれば悪意のあるようなパッケージの存在ですが、これらに対処するcargo-auditやcargo-crevというツールもあります。
本記事では
- 誰かが紹介するか誰かが使っているのを見る、あるいは何かのtrendingに載っているのを見るなどしない限り出会わない
- 日本語の情報があまり無い
- 用途がニッチすぎない
- (synとかはしょうがないとして)あまりbloatを引き起こさない
- deprecatedとかWIPとか書かれたやつは含まない
- 総DL数が100k以上
のような便利なクレートを50個紹介したいと思います。もちろんこのようなクレートは他にも沢山あります。暇があればcrates.ioを眺めてみるのもいいでしょう。
執筆当時のRustのバージョンは1.40.0です。
対象読者
-
Rustに触れたことが無い
「Rustではこういうことができる」ということに触れてもらえれば。
-
チュートリアル and/or 書籍を読んだばかり
頭の片隅にでも置いておくと後で少しだけ楽になるかもしれません。
-
Rustに慣れた人
知らないクレートがあるかもしれません。
Changelog
- 2019-12-27
- 冒頭の文面を変更
- "対象読者"を追加
- せっかくなのでref-cast, ordered-float, im, crossbeamを追加してちょうど50個に
- 2019-12-26
- リンクを修正
- typoを修正(編集リクエスト)
- 冒頭の文面を変更
- defmac, structopt, getset, ascii, stable_deref_trait, owning_refの説明と例を変更
ボイラープレート削減
itertools
Pythonのmore-itertoolsのようなものです。Iterator
を拡張します。良く使われているのはjoin
とformat
ですがその他のメソッドも大変便利です。一度Itertools
のメソッドを眺めるのも良いでしょう。
+use itertools::Itertools as _;
+
static VALUES: &[u32] = &[1, 2, 3];
-let joined = VALUES
- .iter()
- .map(ToString::to_string)
- .collect::<Vec<_>>()
- .join(", ");
+let joined = VALUES.iter().join(", ");
assert_eq!(joined, "1, 2, 3");
またiterate
, assert_equal
といった関数やizip!
, iproduct!
といったマクロも便利です。
static ACTUAL: &str = " foo bar baz ";
static EXPECTED: &str = "foo bar baz";
-assert_eq!(
- ACTUAL.split_whitespace().collect::<Vec<_>>(),
- EXPECTED.split_whitespace().collect::<Vec<_>>(),
-);
+itertools::assert_equal(ACTUAL.split_whitespace(), EXPECTED.split_whitespace());
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>()?;
- // https://github.com/rust-lang/rust/issues/64260
- 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("foo").min_depth(1).max_depth(3) { println!("{}", entry?.path().display()); }
anyhow
(thiserrorと合わせて)post failureの最大手です。
2019-10-05ZにInitial commitされた後2日で1.0.0がリリースされたみたいです。その後先発のsnafuを抜いて最近ではPlaygroundでも使えるようになりました。
See:
-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;
defmac
式を生成するマクロを生成するマクロを提供します。
Rustでは&mut _
を巻き込むクロージャはその対象を専有する必要がありますがmacro_rules!
なら名前さえ見えていれば良いので、代用としてよく使われます(例)。Cell
やRefCell
を使うという手もありますが。
ただしmacro_rules!
での定義はrustfmt下で最低5行を要します。defmac!
なら1+行に収まるので見た目がスッキリします。
またパターンで引数を与えることもできます。defmac!
はこの『引数』をmatch (..) { (..) => .. }
と展開するマクロに展開します。
+use defmac::defmac;
+
let mut st = 0;
-macro_rules! incr {
- ($n:expr $(,)?) => {
- st += $n;
- };
-}
+defmac!(incr n => st += n);
incr!(1);
assert_eq!(st, 1);
matches
let p = match x {
$pat => true,
_ => false,
};
のようなものを
let p = matches!(x, $pat);
のようにできます。
実装は非常に簡素です。27(= 9 + 9 + 11)行しかありません。
また現在core::matches!
(i.e. std::matches!
)というのが提案されていてnightlyのRustで使うことができます。こちらはif
ガートがサポートされています。(&&
と違いマッチした値が使えます)
-let p = 'a' <= c && 'z' <= c || '0' <= c && c <= '9' || c == '-' || c == '_';
+use matches::matches;
+
+let p = matches!(c, 'a'..='z' | '0'..='9' | '-' | '_');
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では中がフォーマットできないことです。
maplit
std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}
に対してvec!
のようなマクロを提供します。
また各key, valueを指定の関数に通すconvert_args!
マクロも便利です。
ちなみに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:
-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 {
+ #[structopt(long, help("Ignores robots.txt"))]
+ norobots: bool,
+ #[structopt(
+ long,
+ value_name("PATH"),
+ default_value("./config.yml"),
+ help("Path to the config")
+ )]
+ config: PathBuf,
+ #[structopt(help("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の代替品を提供します。これらのマクロは型境界等を色々とカスタマイズできます。
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が用意されていないものが対象です。
-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を提供します。
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集です。このクレートはnumには含まれていません。
#[macro_use] extern crate num_derive; #[derive(FromPrimitive, ToPrimitive)] enum Color { Red, Blue, Green, }
strum
enum
の文字列への/からの変換に特化したderive macro集です。
少し前までstrumとstrum-macroの2つのクレートを使う形でしたが0.16.0からfeature-gatedの形でderive macroをre-exportするようになりました。ドキュメントはそのままのようですが..
[dependencies]
strum = { version = "0.17.1", features = ["derive"] }
serdeと同様の問題を抱えていますが(前はメソッドを直に生やしてたのがトレイトを経由するようになり、それらがマクロ名と衝突する)皆がfeatureを有効化すれば良い話なので使いましょう。
See: serdeの `derive` feature と名前空間の困った関係
-use std::convert::Infallible;
-use std::str::FromStr;
+use structopt::StructOpt;
+use strum::{EnumString, EnumVariantNames, VariantNames as _};
#[derive(StructOpt)]
struct Opt {
#[structopt(
long,
value_name("FORMAT"),
default_value("human"),
possible_values(Format::VARIANTS),
help("Output format")
)]
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です。
derivativeやderive_moreにもコンストラクタを生やすderive macroがあるのですがそれらより高性能です。
-use derive_new::new;
+#[derive(new)]
struct Item {
param: u64,
}
-impl Item {
- fn new(param: u64) -> Self {
- Self { param }
- }
-}
-
let item = Item::new(42);
getset
getterとsetterを生やすderive macroです。
難点は個々のgetter/setterにdocが設定できない(フィールドのdocが使われる)点と&Deref::Target
(e.g. &String
→ &str
)を返すgetterを作れない点です。
+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()?;
型を強化する
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 {
// https://rust-lang.github.io/rust-clippy/current/index.html#transmute_ptr_to_ptr
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氏)があるみたいです。
+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 {
- // https://rust-lang.github.io/rust-clippy/current/index.html#transmute_ptr_to_ptr
- 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]
とstr
の両方の長所を持ちます。
use ascii::{AsciiChar, AsciiStr};
let mut s = AsciiStr::from_ascii("abcde\n").unwrap().to_owned();
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 a = UniCase::new("Maße"); let b = UniCase::new("MASSE"); let c = UniCase::new("mase"); assert_eq!(a, b); assert!(b != c);
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ユーザーがソート等にどうやって浮動小数点数を使うのかというと..ソートに関しては雑にこのようなことができます。この場合、NaN
が混じっていると.unwrap()
の部分でpanicします。
let mut xs = vec![2.0, 1.0, 0.0];
xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
これがBTreeMap
やBinaryHeap
等のデータ構造に使うとなるとこうはいきません。f32
/f64
に対するnewtypeが必要になります。そこでこのクレートの出番です。
NotNan<_>
は名の通りです。四則演算の結果NaN
になったのなら整数型と同様にpanicします。ただしこちらはrelease buildでもチェックされます。OrderedFloat<_>
はNaN
を許容しますがそれを「最大の値であり、自身と等しい」としています。
im
std::collections::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, vec::Vec}
のimmutable data版を提供します。
OCamlやHaskell等を使っている方は馴染み深いのではないでしょうか。それぞれの中身はこんな感じのようです。
imとim-rcに分けられていますがその違いは前者はArc
、後者はRc
を使っていることです。
im-rcのデータ型はSync
でもSend
でもなく、複数のスレッドから参照するのはもちろん、他スレッドにmoveすることもできません。
ちなみにArc
をRc
にすることで得られる恩恵は"20-25% increase in general performance"だそうです。
-let mut map = maplit::hashmap!();
-map.insert("foo", 42);
+let map = im::hashmap!();
+let map = map.update("foo", 42);
indexmap
IndexMap
, IndexSet
とそれらを生成するmaplit風のコンストラクタ用マクロindexmap!
, indexset!
を提供します。
JavaのLinkedHashMap
, C#のOrderedDictionary
, PythonのOrderedDict
のようなものです。
「Vec<(K, V)>
にしたいけどK
の重複が無いことを示したい」というとき等に便利です。またserde(特に"ser"の方)と相性がいいです。
-use itertools::Itertools as _;
-use serde::{Deserialize, Deserializer, Serialize};
-
-use std::hash::Hash;
-use std::iter::FromIterator;
-
-#[derive(Serialize)]
-#[serde(transparent)]
-pub(crate) struct UniqueVec<T>(Vec<T>);
-
-impl<T: Clone + Eq + Hash> FromIterator<T> for UniqueVec<T> {
- fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
- Self(iter.into_iter().unique().collect())
- }
-}
-
-impl<'de, T: Clone + Eq + Hash + Deserialize<'de>> Deserialize<'de> for UniqueVec<T> {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de>,
- {
- Vec::<_>::deserialize(deserializer).map(FromIterator::from_iter)
- }
-}
+use indexmap::IndexSet;
今の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:
- 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があるのでそれを回避する目的で使えます。
デフォルトでエラーが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`"))?;
テストに役立つ
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
に使えます。
tempdir
名の通りです。
TempDir
はdrop時に対象の一時ディレクトリを削除します。(self) -> io::Result<()>
として削除することもできます。bin
クレートのテストにも使うことができます。
use tempdir::TempDir;
let tempdir = TempDir::new("appliction-name")?;
let tempdir_path = tempdir.path();
パフォーマンスの向上
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: 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);