139
111

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 2Advent Calendar 2020

Day 1

Rust、こんなときはこう書こう

Last updated at Posted at 2020-11-30

こんにちは、Rust2 Advent Calendar 2020の1日目を担当させていただくyasuo-ozuです。

今回は特にまとまったネタを思いつかなかったのでtips集としてお送りします。有名なものばかりで恐縮ですが、何かありましたらご指摘いただければ幸いです。

Enum型の要素を賢く使う

イテレータを Option でマップしたいとき、通常では以下のようにすると思います。

let v = vec![1usize, 2, 3];
let v = v.into_iter().map(|v| Some(v)).collect::<Vec<_>>();

実は、これは以下のように省略することが可能です。

let v = vec![1usize, 2, 3];
let v = v.into_iter().map(Some).collect::<Vec<_>>();

ここで、Some(v)Option<usize>型のEnum値ですが、ただSomeとだけ書くとfn(usize) -> Option<usize> という関数になります。よって、Iterator::map()の中で直接用いることができます。

もちろん、Optionだけでなく自分で定義したEnumでも使用できます。

enum MyError {
	IoError(std::io::Error),
	MyError(String),
	OtherError(Box<dyn std::error::Error>),
}
std::fs::read_to_string("myfile").map_err(MyError::IoError);

複数の条件を組み合わせる

プログラムから環境変数を使いたいと思った場合、以下のように書くと思います。

if let Ok(s) = std::env::var("MYENV") {
    println!("{:?}", s);
}

しかし、複数の環境変数を扱う場合に、すべてに対応する処理を書くのは少し面倒です。

if let Ok(s1) = std::env::var("MYENV1") {
    if let Ok(s2) = std::env::var("MYENV2") {
        println!("{:?} {:?}", s1, s2);
    }
}

これは、以下のようにかけます。

if let (Ok(s1), Ok(s2)) = (std::env::var("MYENV1"), std::env::var("MYENV2")) {
    println!("{:?} {:?}", s1, s2);
}

また、環境変数の候補が複数あり、どちらかセットされている方を選ぶという場合は以下のようにかけます。

match (std::env::var("MYENV1"), std::env::var("MYENV2")) {
    (Ok(s), Err(_)) | (_, Ok(s)) => println!("{:?}", s);
}

matches! マクロを使おう

RustではEqトレイトを実装していない型同士を比較することができません。実際、Eqを実装しないEnumを用いた以下の例は動きません。

enum TripleChoice {
    A,
    B,
    C
}
fn is_a(choice: TripleChoice) -> bool {
    choice == TripleChoice::A
}

ここで、matches!マクロを使って以下のようにすれば、TripleChoiceEqを実装していなくても、is_a関数を定義できます。

fn is_a(choice: TripleChoice) -> bool {
    matches!(choice, TripleChoice::A)
}

すばらしい数

別の変な例です。以下のようにすれば、「3以上12以下の偶数」を判定する関数を定義できます。

fn is_good_number(n: usize) -> bool {
    matches!(n, n @ 3..=12 if n % 2 == 0)
}

そうです、実はmatches!マクロの中に書けるのは、match式の条件に書けるものと同じです。

matches!マクロの定義も見てください。

スライスの代入を使う

CLIのツールを作るとき、コマンドライン引数を使用することが多いと思いますが、間違った使い方を防ぐためにチェックをする必要があります。通常これは以下のように行うと思います。

let args = std::env::args().collect::<Vec<String>>();
if args.len() != 2 {
    eprintln!("bad args: {:?}", args);
}
let subcmd = args[1];
println!("{:?}", subcmd);

これは、スライスの代入を用いることでもっとスマートにかけます。

let args = std::env::args().collect::<Vec<String>>();
if let [_, subcmd] = args.as_slice() {
    println!("{:?}", subcmd);
} else {
    eprintln!("bad args: {:?}", args);
}

固定された要素数のIterator生成

Iteratorを使う局面で多いのは、扱う要素の数が可変である時です。しかしながら、場合によっては要素の固定された配列をIteratorとして返したいときもあります。その時、以下のように書くことができます。

fn get_channels() -> impl Iterator<Item=usize> {
    vec![1, 3, 4, 6, 8, 10].into_iter()
}

もしくは、sliceを用いて以下のようにもかけます。

fn get_channels() -> impl Iterator<Item=usize> {
    [1, 3, 4, 6, 8, 10].iter().cloned()
}

しかしながら、場合によっては要素数0または1のIteratorを生成したい場合もあります。その場合は以下のように様々な方法があります。

Some(1).into_iter()
std::iter::once(1)
None.into_iter()
std::iter::empty()

重複を取り除く

Dedup を使う方法

@kobae964さんのご指摘を踏まえて追加しました。

let mut v = vec![1, 3, 4, 3, 7, 6, 4, 7];
v.sort();
v.dedup();
println!("{:?}", &v);

HashSetを使う方法

let v = vec![1, 3, 7, 6, 3, 7];
let v = v.into_iter().collect::<HashSet<_>>().into_iter().collect::<Vec<_>>();

ただし、取り出すときに要素の順番が不定になります(順序が保存されない)。数値の大小の順番で取り出したいときは代わりにBTreeSetを使います。

let v = v.into_iter().collect::<BTreeSet<_>>().into_iter().collect::<Vec<_>>();

Dedupを自分で実装する場合

let mut v = vec![1, 3, 4, 3, 7, 6, 4, 7];
v.sort();
let v = v
    .into_iter()
    .scan(None as Option<usize>, |stat, n| {
        if let Some(stat) = stat {
            if stat == &n {
                return Some(None);
            }
        }
        *stat = Some(n);
        Some(*stat)
    })
    .flatten()
    .collect::<Vec<usize>>();
println!("{:?}", &v);

feature flags とモジュール

Rustでは、コンパイル時にプログラムの一部を有効化もしくは無効化するときにfeature flagsというものを使用できます。例えば、Cargo.toml

Cargo.toml
[features]
my-feature = []

と書いた上で、

#[cfg(feature = "my-feature")]
pub fn my_func() { /* ... */ }

とやれば、my_func()my-featureが有効な時のみコンパイルされることになります。

しかし、このように制御したい定義が1個だけではなく、多数に上る場合、モジュールを切り分けた方が楽です。

fn outer_func() { /* ... */ }
#[cfg(feature = "my-feature")]
pub mod my_feature_mod {
    pub fn my_func1() {
        super::outer_func()
    }
}

ただし、この場合外部からmy_func1()を呼び出す場合、my_feature_mod::my_func1()のようにする必要がありますし、モジュール内から外側の定義を用いる場合もsuper::をつけて参照する必要があります。
これを省略するには、以下のようにします。

#[cfg(feature = "my-feature")]
pub use my_feature_mod::*;

#[cfg(feature = "my-feature")]
mod my_feature_mod {
    use super::*
    pub fn my_func1() {
        outer_func()
    }
}

HashSetじゃなくてBTreeSet

Rust言語で数値の集合を取り扱いたい場合が時々あります。たとえば、選んだ数値の組み合わせによって点数が決まるゲームを考えます。ただし、同じ数値は1回しか選べず、選び方は順不同だとします。

このとき、次のような関数を定義することを考えます。

fn get_score(set: HashSet<usize>) -> usize {
    /* ... */
}

この関数内で、setの内容に応じた分岐を行ってもよいのですが、折角なので集合とそれに対応する得点を別に宣言することにしましょう。

let mut map: HashMap<HashSet<usize>, usize> = HashMap::new();
map.insert(vec![1, 3].into_iter().collect(), 10);
map.insert(vec![1, 3, 7].into_iter().collect(), 20);

fn get_score(set: HashSet<usize>, map: &HashMap<HashSet<usize>, usize>) -> usize {
    map.get(&set).cloned().unwrap_or(0)
}

しかし、これでは動作しません。なぜなら、HashSet自体がtrait Hashを実装していないため、HashMapのKeyとして用いることができないからです。

これは、HashSetの代わりにBTreeSetを使えば解決します。

let mut map: HashMap<BTreeSet<usize>, usize> = HashMap::new();
map.insert(vec![1, 3].into_iter().collect(), 10);
map.insert(vec![1, 3, 7].into_iter().collect(), 20);

fn get_score(set: BTreeSet<usize>, map: &HashMap<BTreeSet<usize>, usize>) -> usize {
    map.get(&set).cloned().unwrap_or(0)
}

ただ、BTreeSetを使うためには要素がtrait Ordを実装していなければならないので、注意してください。
詳しくはこの記事を参照

結合型としてBox を使う

例えば、Rustでイテレータを返す関数を実装する場合、以下のようにします。

fn iter_number() -> impl Iterator<Item=usize> {
    0usize..
}

for n in iter_number().take(10) {
    println!("{}", n);
}

ここで、0usize..は0から始まって永遠に加算されていくイテレータを生成します。また、impl Iteratorはイテレータの型を省略する記法で、ここではコンパイル時にRangeFromという型に変換されます。

では、このiter_number()に平方数を出力する機能を実装してみましょう。

fn iter_number(squared: bool) -> impl Iterator<Item=usize> {
    let it = 0usize..;
    if squared {
	it.map(|n| n * n)
    } else {
	it
    }
}

for n in iter_number(false).take(10) {
    println!("{}", n);
}

これは、一見良さそうに見えますが、実は動きません。これは、Rustの「関数は一つの型しか返せない」という制約によるものです。
ここでitは先程説明したようにRangeFromという型の変数になっていますが、これをmap()によって変換すると、Mapという型に変換されます。この関数は、RangeFromMapの両方を返す可能性があり、impl Iteratorに対応する具体的な型が決まらないためエラーとなるのです。

ここで、Rustの手動結合型機能であるBoxを用います。これによって、RangeFrom<_>Map<_>はどちらもBox<dyn Iterator<Item=_>>という型にキャストできます。また、Box<T> where T: Iterator<Item=_>Iterator<Item=_>を実装しているため、impl Iteratorとしてreturn することができます。

fn iter_number(squared: bool) -> impl Iterator<Item=usize> {
    let it = 0usize..;
    if squared {
        Box::new(it.map(|n| n * n)) as Box<dyn Iterator<Item=_>>
    } else {
        Box::new(it) as Box<dyn Iterator<Item=_>>
    }
}

for n in iter_number(true).take(10) {
    println!("{}", n);
}

traitを用いる例

同じことはtraitを使う場合にも言えます。というのも、trait内で関数を宣言する場合、implは使用できないからです。この場合もやはりimpl Iteratorの代わりに、以下のようにBox<dyn Iterator<Item=usize>>を用いることができます。

trait IterMaker {
    fn iter(self) -> Box<dyn Iterator<Item = usize>>;
}
struct NumberIterMaker();
impl IterMaker for NumberIterMaker {
    fn iter(self) -> Box<dyn Iterator<Item = usize>> {
	Box::new(0usize..) as Box<dyn Iterator<Item = _>>
    }
}

fn print_iter(it: impl IterMaker) {
    for v in it.iter().take(10) {
	println!("{}", v);
    }
}

let im = NumberIterMaker();
print_iter(im);
139
111
2

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
139
111

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?