LoginSignup
3
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-02-25
1 / 20

この記事はRust LT会の資料になります


自己紹介(Public)

名前: @higumachan725
会社: KICONIA WORKS
仕事: AI系のエンジニア
趣味: APEX, 漫画


自己紹介(Rust)

  • 仕事
    • 数理解析
    • 画像処理
    • ロボットアームの制御
  • 趣味
    • 言語処理系(インタプリタ)
    • pyflowへのコントリビュート
    • 自作コンテナ型crateを作る

はじめに


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

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

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

形質の例

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


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


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

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

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

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

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が実はコンテナ型になってる

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

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

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

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

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>がこれに当たる。

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は値を使うときに必ず存在しているかどうかのチェックが行われていることが保証される。(unwrapもチェック自身は行われているので)


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

typed-sign

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

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


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って感じがします。

3
3
0

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