こんにちは、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!
マクロを使って以下のようにすれば、TripleChoice
がEq
を実装していなくても、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
に
[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
という型に変換されます。この関数は、RangeFrom
とMap
の両方を返す可能性があり、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);