Rustでコンテナ型を便利に使うお話



**この記事は[Rust LT会](https://uniquevision.connpass.com/event/202944/)の資料になります**

---

# 自己紹介(Public)

名前: [@higumachan725](https://twitter.com/higumachan725)
会社: [KICONIA WORKS](https://www.kiconiaworks.com/)
仕事: AI系のエンジニア
趣味: APEX, 漫画

---

# 自己紹介(Rust)

- 仕事
    - 数理解析
    - 画像処理
    - ロボットアームの制御
- 趣味
    - 言語処理系(インタプリタ)
    - [pyflow](https://github.com/David-OConnor/pyflow)へのコントリビュート
    - 自作コンテナ型crateを作る



---

## はじめに

---

### コンテナ型のこのスライドの中での定義

```rust
let v: Vec<i64> = vec![1, 2, 3];
let o: Option<i64> = Some(1);
```

以上のような型引数を取って、その方に何かしらの形質を与えるような型のことを指す。

**形質の例**

`Vec<T>`: 複数の値を持てる。
`Option<T>`: 値を持つ場合と持たない場合がある。

---

### 実はこのコンテナ型とRustの型の考え方を組み合わせるといろいろな形質を与えて、便利に書けたり静的な検証を行うことが出来ます。

---

## 1. 一部の演算子の振る舞いを変える

例えば以下のような例を考えます。

```rust
fn main() {
    let mut v: Vec<i64> = vec![2, 1, 3];
    v.sort();
    println!("{:?}", v); // [1, 2, 3]
}
```

このsortの昇順・降順を入れ替えるとします。
このときに

```rust
use std::cmp::Reverse;

fn main() {
    let mut v: Vec<i64> = vec![2, 1, 3];
    v.sort_by_key(|x| Reverse(*x));
    println!("{:?}", v); // [1, 2, 3]
}
```

というふうに書くことが出来ます。

--- 

## 何がコンテナ型なの？

---

## [Reverse](https://doc.rust-lang.org/std/cmp/struct.Reverse.html)が実はコンテナ型になってる

つまり、`Reverse<T>`という型は`T`という型の比較演算子の結果をひっくり返すようになっている。

```rust
fn main() {
    let (a, b): i64 = (1, 2);
    let (ra, rb): (Reverse(i64), Reverse(i64)) = (Reverse(1), Reverse(2));

    assert_eq!(a < b, true);
    assert_eq!(ra < rb, false);
}
```

---

### `Reverse<T>`: Tの比較演算子をひっくり返す。

--- 

## 自作してみました

[unwrap-ord](https://github.com/higumachan/unwrap-ord)

解決したい問題: Rustではfloat型を始めとしてPartialOrdのみが定義されておりOrdが定義されていない型が多くある。その場合にsortを行う場合何かしらの対策を取らなければならない。

解決方法: `PartialOrd`が実装されている場合に`partial_ord`の結果をunwrap()するようなOrdを実装した型にする。

```rust
fn main() {
    let mut v: Vec<f64> = vec![1.0, 3.0, 2.0];
    // v.sort(); // compile error
    v.sort_by((a, b) => a.partial_cmp(b).unwrap()) // よくある解決方法
    v.sort_by_key(|a| UnwrapOrd(a)) // このcrateを使って出来ること
}
```

---

## この方法論の何が良いのか

- 新しい型や関数を定義することなく、型の振る舞いを変えることで静的に安全な状況で演算子の挙動を変えることが出来る。
- `BinaryHeap<T>`などのデータ構造は演算子の型変換を行わなければ昇順・降順の入れ替えが出来ない。

---

## 2. 静的検証可能なものを静的に検証する

--- 

### `Option<T>`がこれに当たる。

```rust
fn main() {
   let a: Option<i64> = Some(1);
   let b: i64 = 1;

   // assert_eq!(a + b, 2); // compile error Option<i64>とi64の間に足し算が定義されていない
   assert_eq!(a.map(|x| x + b), Some(2));
   assert_eq!(a.unwrap() + b, 2);
}
```

このように、Option<T>は値を使うときに必ず存在しているかどうかのチェックが行われていることが保証される。(`unwrap`もチェック自身は行われているので)

--- 

### これも自作してみた(実装途中)

[typed-sign](https://github.com/higumachan/typed-sign)

解決したい課題: float型などの型は正値、負値などの符号が型レベルでサポートされていない。


解決方法: `Positive<f64>`, `Negative<f64>`, `Zero` などの符号を表したような型を用意して符号を型レベルでサポートしてその間の演算を定義する。

---

```rust
fn main() {
    let a = Positive::from_value(100.0).unwrap();
    let b = Positive::from_value(10.0).unwrap();
    let c = Negative::from_value(-20.0).unwrap();
    let d = Negative::from_value(-110.0).unwrap();

    assert_eq!(a + b, Positive(110.0));
    assert_eq!(a + c/* Result<Positive, NonPositive> */, Ok(Positive(80.0)));
    assert_eq!(a + d/* Result<Positive, NonPositive> */, Err(NonPositive::Negative(Negative(-10.0))));
}
```

--- 

#### 想定される利用シーン

- GameのHP(0以下になった場合に必ず検証をしたい)
- 会計のソフトウェア

--- 

## まとめ

---

1. Rustはコンテナ型を使って形質を与えることが出来る。
2. Rustはコンテナ型を使って静的な検証(型安全)を増やすことが出来る。
3. コンテナ型は比較的簡単に実装が出来る。
    - `unwrap-ord`: 50行(含テスト)
    - `typed-sign`: 300行(くらいになりそう)

## 補足

1のパターンはdecoratorとかadaptorとか言ったほうがわかりやすいといただきました。確かに型レベルでのdecoratorって感じがします。