393
303

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rustその3Advent Calendar 2019

Day 25

Rustの便利クレート

Last updated at Posted at 2019-12-25

RustにはCargoという優れたパッケージマネージャーがあるので細かい多数の依存パッケージが問題になることはあまりありません。というより同一のアプリケーションでもパッケージを細かく分割することが推奨されています。ちょっとしたボイラープレートを取り除くような小さなライブラリも大量にあります。これらは積極的に使うべきです。

問題があるとすれば悪意のあるようなパッケージの存在ですが、これらに対処するcargo-auditcargo-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

ボイラープレート削減

itertools

crates.io docs.rs license downloads

Iteratorを拡張したItertoolsとイテレータを生成するいくつかの関数とマクロを提供します。

Pythonのmore-itertoolsのようなものです。 ただし単純に対応するものではなく、互いに存在しないものや対応していても名前が違う場合があります。

良く使われているのはjoinformatですがその他のメソッドも大変便利です。一度Itertoolsのメソッドを眺めるのも良いでしょう。

例を挙げると、

  • Itertools::all_equal

    -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!();
     }
    
  • Itertools::tuple_combinations

    -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>();
    
  • iproduct!

    -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`
    

そのほか

等の様々なユーティリティを提供します。 是非一度網羅的に調べてみるのはどうでしょうか。メソッドチェーン一発で解決できる問題が増えます。

極めて軽量かつ使いどころも多いのでstructoptanyhow等と共にcargo new直後に入れておくのもいいかもしれません。

See also: itertoolsの紹介 | κeenのHappy Hacκing Blogその1の6日目)

itertools-num

crates.io docs.rs license downloads

itertoolsと同じくIteratorを拡張しますが提供するのはcumsumlinspace、これだけです。

@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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

ディレクトリを捜索するときに使えるユーティリティです。

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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

扱いやすくしたstd::process::Commandです。

std::process::Commandと比べて

  1. 短いです。一行に収まることが多いです。
  2. ステータスチェック & 失敗時のエラーメッセージの設定が不要です。
  3. .stderr(Stdio::inherit())が不要です。
  4. stdoutstr::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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

thiserrorと合わせれば)error-chainfailure →と続くエラーハンドリングライブラリ3代目の最有力候補です。

2019-10-05ZにInitial commitされた後2日で1.0.0がリリースされ、爆発的に使われ始めました。2ヶ月経った頃には先発のsnafuを総DL数で抜いてPlaygroundでも使えるようにもなりました。 (2020-05-25現在は外れています)

anyhow::ErrorBox<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::backtracenightlyコンパイラ上でのみ使っているためです。

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

crates.io docs.rs license downloads

Either<_, _>try_left!, try_right!を提供します。

即席の構造体としてタプルが使えるようにEitherは即席の(バリアント2つの)直和型として使えます。

またEitherには使いやすくするためのメソッドがいくつか付いていて、またstdの各トレイトについてimpl<L: Trait, R: Trait> Trait for Either<L, R>という定義がなされています。

実装が非常に小さいのもありitertoolsrayonをはじめとした多くのクレートに使われ、またre-exportされています。

ちなみに即席直和型を専用構文付きで入れようという議論がpre 1.0時代からあります

-enum Either<L, R> {
-    Left(L),
-    Right(L),
-}
+use either::Either;

paste

crates.io docs.rs license downloads

現在unstableであるstd::concat_ident!の代用となるマクロです。

またはアイテム単位で識別子をconcatします。

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

crates.io docs.rs license downloads

ifif 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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

clap(コマンドラインパーサーのデファクトスタンダード)のラッパーです。

v0.3でAPIが大きく変わりました。attributeは一部の名前を除きclap::Argのメソッドとして扱われます。

またclapのv0.3でstructoptと同じAPIのderive macroが追加される予定です

How does clap compare to structopt?

Simple! clap is stuctopt. With the 3.0 release, clap imported the structopt code into it's own codebase as the clap_derive crate. Since structopt already used clap 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 the clap_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

crates.io docs.rs license downloads

ドキュメント(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

crates.io docs.rs license downloads

derivativeと同じくstdのトレイトに対するderive macroを提供しますが、こちらはFrom, Into, Deref, FromStr, Displaystdで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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

num-traitsの各トレイトを実装するderive macro集です。

  1. filedが1つのtuple struct (e.g. struct Foo(i32))
  2. variantがすべてunitのenum (e.g. enum Bar { A, B })

が対象です。 このクレートはnumには含まれていません。

注意: rust-num/num-derive#34

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

crates.io docs.rs license downloads

enum文字列への/からの変換に特化したderive macro集です。

FromStrDisplayの実装はもちろん、&'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

crates.io docs.rs license downloads

コンストラクタを"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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

ジェネリックな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

crates.io docs.rs license downloads

StringPathBufの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

crates.io docs.rs license downloads

物理量を表現できます。

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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

case-insensitiveに2つの文字列を等値判定する関数と、それをPartialEqに使うnewtypeを提供します。

use unicase::UniCase;

let s1 = UniCase::new("aaa");
let s2 = UniCase::new("AAA");

assert_eq!(s1, s2);

ordered-float

crates.io docs.rs license downloads

Ord (: Eq)を実装するNotNan<_: Float>OrderedFloat<_: Float>を提供します。

Rustの比較演算子用のインターフェイスにはEq, Ordの他にPartialEq, PartialOrdがあります。具体的な要請はリンク先を参照してください。比較演算子にはPartialEqPartialOrdが使われます。

何故このような区分があるのかというと一つは浮動小数点数のためです。f32, f64PartialEq, 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());

maxminも同様にIterator::max_byを使うかあるいはf64::maxfoldすれば良いです。

use std::f64;

static XS: &[f64] = &[0.0, 1.0, 2.0];

let max = XS.iter().copied().fold(f64::NEG_INFINITY, f64::max);

これがBTreeMapBinaryHeap等のデータ構造に使うとなるとこうはいきません。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

crates.io docs.rs license downloads

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);

imim-rcに分けられていますがその違いは前者はArc、後者はRcを使っていることです。
im-rcのデータ型はSyncでもSendでもなく、複数のスレッドから参照するのはもちろん、他スレッドにmoveすることもできません。

ちなみにArcRcにすることで得られる恩恵は"20-25% increase in general performance"だそうです。

indexmap

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

「型パラメータとしての整数」を表現します。

4桁以下なら定数が揃っていますがそれより大きい値は2つの既存の値を+-*/したりビットで表現することで生み出します。

generic-array

crates.io docs.rs license downloads

[T; _]のように振る舞う、長さをtypenum::UIntで表現するGenericArray<T, _>を提供します。

ちなみにGenericArray<T, _>: IntoIterator<Item = T>です。(これを書いているときのRust 1.40の時点では[T; _]: IntoIteratorではありません。)

stdの挙動を改善する

crossbeam

crates.io docs.rs license downloads

並行処理を行なう低レベルのライブラリです。

rayon(並列処理用のライブラリ), tokio(非同期処理用のライブラリ)に使われています。crossbeamが提供する機能はそれらより低レベルで、std::syncの改善に焦点をあてています。std::sync + std::threadで苦労したのなら使うと幸せになれるかもしれません。

など提供するものは多岐にわたります。

実際に使うときはbloatを避けるため、個別のsubcrate (crossbeam-*)を使うと良いでしょう。

See also:

-let (tx, rx) = std::sync::mpsc::channel();
+let (tx, rx) = crossbeam_channel::unbounded();
 tx.send(42)?;
 assert_eq!(rx.recv()?, 42);

libm

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

実行ファイルを探します。

Windowsではrust-lang/rust#37519があるのでそれを回避する目的で使えます。

注意するべき点として、デフォルトでエラーがfailure::Failです。default-features = falsestd::error::Errorになりますが依存クレートのアップデートで壊れる可能性が付き纏います。 使うときはこのようにすれば良いでしょう。

use anyhow::anyhow;

let rustup = which::which("rustup")
    .map_err(|e| anyhow!("{}", e.kind()).context("failed to find `rustup`"))?;

dunce

crates.io docs.rs license downloads

file pathを正規化するためのAPIとして、stdにはstd::fs::canonicalizeとそのショートカットのPath::canonicalizeがあります。

ただしWindows上では、このcanonicalize\\?\C:\foo\barのような"UNC path"を返します。 そしてマイクロソフト製のものを含め、多くのソフトウェアはUNC pathを扱うことができません。 Rustのstd::fsも同様です。

std::fs::canonicalize returns UNC paths on Windows, and a lot of software doesn't support UNC paths · Issue #42869 · rust-lang/rust

このクレートは問題の無いときに限りUNC pathを通常のものに変換するsimplifiedと、それを用いたcanonicalizeの2つの関数を提供します。 Windows上でなければsimplifiedは何も行ないません。

-let path = path.canonicalize()?;
+let path = dunce::canonicalize(path)?;

テストに役立つ

pretty_assertions

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

コンパイル時の"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

crates.io docs.rs license downloads

f32f64の等値判定が楽にできます。

ところでこれを使ってPartialEqを実装したくなりますが実はPartialEqも推移律は要求されるので良くありません。approxはPartialEqの代わりに絶対誤差(+ 相対誤差)の指定の元で等値判定するAbsDiffEqRelativeEqというトレイトを提供しています。こちらを実装しましょう。

use approx::{abs_diff_eq, relative_eq};

abs_diff_eq!(1.0, 1.0);
relative_eq!(1.0, 1.0);

version-sync

crates.io docs.rs license downloads

READMEを含むドキュメントに書かれている現在のパッケージ($CARGO_PKG_NAME)を指しているバージョンのうち、現在のバージョン($CARGO_PKG_VERSION)と異なるものを検出してくれます。

assert_cmd

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

名の通りです。 一時ファイルと一時ディレクトリを生成できます。

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

crates.io docs.rs license downloads

ある関数をテストする際、このように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

crates.io docs.rs license downloads

ユニットテストを別プロセスで走らせることができるマクロです。

プロセスが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

crates.io docs.rs license downloads

コンパイルエラーのメッセージをassertするためのクレートです。

マクロ以外にも複雑な型境界を持つ普通の関数にも使えます。

use trybuild::TestCases;

#[test]
fn ui() {
    TestCases::new().compile_fail("tests/ui/**/*.rs");
}

使い方はREADMEの画像を見た方がわかりやすいでしょう。

trybuildのREADMEの画像

テスト対象の.rsファイルをディレクトリに入れ、走らせることでファイルパスがマスクされた.stderrファイルが生成されます。 これを.rsファイルと同じ場所に放り込めばテストが出来上がります。

trybuildのREADMEの画像

TRYBUILD=overwriteと環境変数をセットした場合、.stderrファイルは最初から同じディレクトリに生成されます。

insta

crates.io docs.rs license downloads

Debug表記やJSON/YAML/RONの文字列でassert_eq!を行います。

trybuildと同じようにテストが書かれた.rsファイルと同ディレクトリにsnapshots/{name}___snapshots.snapというファイルが生成され、それが比較対象になります。

操作をインタラクティブに行うためにcargo-instaというものが用意されているのでそれを使うのが良いでしょう。

tests/test.rs
use insta::assert_debug_snapshot;

#[test]
fn test_snapshots() {
    let value = vec![1, 2, 3];
    assert_debug_snapshot!(value);
}
$ cargo insta test --review

cargo-insta.png

パフォーマンスの向上

take_mut

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

std::io::{BufReader, BufWriter, LineWriter}と入れ替えるだけで速くなるみたいです。

-use std::io::{BufReader, BufWriter, LineWriter};
+use buf_redux::{BufReader, BufWriter, LineWriter};

stable_deref_trait

crates.io docs.rs license downloads

このような構造体TextWordを考えてみます。

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を持つとアウトです。

またCloneStableDerefStableDerefのサブセットで、cloneしても問題ないもの(&_, Rc<_>, Arc<_>)に対して実装されています。

rental

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

arrayvecVecDeque版のようなものです。

容量が無いときにエラーにするか反対側の要素を削除(排出)するかを選べます

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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

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

crates.io docs.rs license downloads

コンパイル時に生成できる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

crates.io docs.rs license downloads

インデントされた文字列リテラルを文字列リテラルのまま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というクレートで提供されています。

393
303
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
393
303

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?