Rust HashMap::entry ユースケースと書き方まとめ

# Rust の HashMap の entry とは

「Rust entry」などで検索すると HashMap の entry を説明してくださっている記事は多く出てくるので、ここでは簡単な説明にとどめます。
参考: [RustのHashMapはentryが便利](https://keens.github.io/blog/2020/05/23/rustnohashmaphaentrygabenri/)
参考: [Rustでdefaultdict](http://segafreder.hatenablog.com/entry/2018/08/13/162716)

マップや辞書などのデータ構造を扱う際、「あるキーの値が、あればなんらかの処理、なければ新たにデータを追加してから処理」という操作をしたいことは多いと思います。

たとえば、「キーの出現回数をマップに保存する」というケースを考えます。愚直に実装をすると、条件分岐を用いて、「マップにキーがあるか判定し、あれば処理、なければ追加」という操作を記述するかと思います。

```rust
let mut map = std::collections::HashMap::new();
let key = "one";
// map.insert(key, 3);  // コメントアウトを外すと出力が{"one": 4}になる
if let Some(one) = map.get_mut(key) {
    *one += 1;
} else {
    map.insert(key, 1);
}
println!("{:?}", map);  // {"one": 1}
```

しかし、この書き方はマップアクセスを2度記述しているので、冗長です。
そこで、[HashMap::entry](https://doc.rust-lang.org/beta/std/collections/struct.HashMap.html#method.entry) を用いることで、同等の処理を1行で容易に記述できます。

```rust
let mut map = std::collections::HashMap::new();
let key = "one";
// map.insert(key, 3);  // コメントアウトを外すと出力が{"one": 4}になる
*map.entry(key).or_insert(0) += 1;
println!("{:?}", map);  // {"one": 1}
```

# ユースケース別書き方
こちらが今回のメインです。ユースケースから [hash_map::Entry](https://doc.rust-lang.org/beta/std/collections/hash_map/enum.Entry.html) 以下に生えている各メソッドをまとめていきたいと思います。
といいつつユースケースが2つほどしか思いついていません…

## 数値のマップで出現回数を数える場合
初めに例として挙げたものです。key ごとに何回出現したかなどを記録することができます。
`or_insert()` を用いて次のように書くことができます。

```rust
let mut map = std::collections::HashMap::new();
for c in "abcabc".chars() {
    *map.entry(c).or_insert(0) += 1;
}
println!("{:?}", map);  // {'c': 2, 'a': 2, 'b': 2}
```

また、型が決まっていれば、 `or_default()`を用いることもできます。

```rust
let mut map = std::collections::HashMap::<_, usize>::new();
for c in "abcabc".chars() {
    *map.entry(c).or_default() += 1;
}
println!("{:?}", map);  // {'c': 2, 'a': 2, 'b': 2}
```

あるいは、`and_modify()` を用いるのもいいのかもしれません。

```rust
let mut map = std::collections::HashMap::new();
for c in "abcabc".chars() {
    map.entry(c).and_modify(|e| *e += 1).or_insert(1);
}
println!("{:?}", map);  // {'c': 2, 'a': 2, 'b': 2}
```

## リストのマップで保存していく場合
これも出現回数を数える場合とほぼ同じです。例えば、文字ごとの出現位置の記録などで使うことができます。
`or_insert()` を使うと、

```rust
let mut map = std::collections::HashMap::new();
for (i, c) in "abcabc".chars().enumerate() {
    map.entry(c).or_insert(Vec::new()).push(i);
}
println!("{:?}", map);  // {'b': [1, 4], 'c': [2, 5], 'a': [0, 3]}
```

また、型推論ができていれば、`or_default()` でもできます。

```rust
let mut map = std::collections::HashMap::<_, Vec<_>>::new();
for (i, c) in "abcabc".chars().enumerate() {
    map.entry(c).or_default().push(i);
}
println!("{:?}", map);  // {'b': [1, 4], 'c': [2, 5], 'a': [0, 3]}
```

そして、`and_modify()` でも記述できます。この場合でもなぜか型を明示する必要があるようです。(or_insert で Vec を与えているのに型推論はしてくれない(？))

```rust
let mut map = std::collections::HashMap::<_, Vec<_>>::new();
for (i, c) in "abcabc".chars().enumerate() {
    map.entry(c).and_modify(|e| e.push(i)).or_insert(vec![i]);
}
println!("{:?}", map);  // {'b': [1, 4], 'c': [2, 5], 'a': [0, 3]}
```

# 副作用のある処理をデフォルトで行いたい場合
当然なのかもしれませんが、`entry().or_insert()`の行が実行されるたび`or_insert()`の引数に与えている部分が評価されるようです。そのため、副作用のある処理を`or_insert()`の中に書いてしまうと、挿入が発生しなくても初期化処理の部分が実行されて、期待している動作をしないことがあります。

そのような場合は、`or_insert_with()`が使えます。[or_insert_with](https://doc.rust-lang.org/beta/std/collections/hash_map/enum.Entry.html#method.or_insert_with)
これは引数にデフォルト値を返す関数を与えるため、挿入が発生して初めて実行されます。

# あとがき
自分は Rust にあまり詳しいわけではないので、間違いや抜けがあった場合はぜひコメントなどで指摘をお願い致します。