🧠

RustでAtCoderをやってみた / Rustの良さを語る

2023/12/17に公開

この記事は 東京高専プロコンゼミ1年 Advent Calendar 2023 17日目の記事です。
RustでAtCoderをやってみたら想像以上に快適だったので書いてみる。

まずは環境構築

WSL2とVSCodeでやった。
https://qiita.com/evid/items/f81534518b30847a24d2
この辺を参考にしてやった気がする。
Cargo CompeteというRustで競技プログラミングをやるための神ツールがあるので導入した。

cargo compete init atcoder .

で作業ディレクトリを登録して

cargo compete new abcXXX

でテストケースなどを取り込んだディレクトリを作成する。

するとこんな感じになる
作業ディレクトリ
├── abcXXX
│  ├── Cargo.lock
│  ├── Cargo.toml
│  ├── src
│  │  └── bin
│  │     ︙
│  └── testcases
│     ︙
├── abcXXX
│  ├── Cargo.lock
│  ├── Cargo.toml
│  ├── src
│  │  └── bin
│  │     ︙
│  └── testcases
│     ︙
├── abcXXX
│  ├── Cargo.lock
│  ├── Cargo.toml
│  ├── src
│  │  └── bin
│  │     ︙
│  └── testcases
│     ︙
︙
└── target
│   ︙
└── compete.toml
└── template-cargo-lock.toml
cargo compete test a

でテストしたり

cargo compete submit a

でAtCoderに即座にSubmitしたりできる。
導入の際、cargo compete init atcoder .で生成されるディレクトリの下にある、「compete.toml」でlanguage_id = "5054"と書き換えないとSubmitが失敗する点でやや躓いた。

自分はより速くタイプできるように、~/.bashrcに

alias cc='cargo compete'
alias cct='cargo compete test'
alias ccs='cargo compete submit'
ccn () {
    cd ~/rust
    cargo compete new $1
    cd ~/rust/$1
    code .
}

を追記していい感じにしてみた。参考にする場合はcd ~/rust, code .の部分を環境に応じて適宜変更していただきたい。

Rustのここがすごい!

イテレータ系メソッドがすごい!

ABC329: C - Count xxx

use itertools::Itertools;
use proconio::input;
use proconio::marker::Chars;

fn main() {
    input! {
        n: usize,
        s: Chars,
    };
    let mut l: Vec<(char, usize)> = Vec::new();
    for i in 0..n {
        if i == 0 || s[i] != s[i - 1] {
            l.push((s[i], 1));
        } else {
            l.last_mut().unwrap().1 += 1;
        }
    }
    let ans = l
        .into_iter()
        .sorted_by_key(|v| v.1)
        .rev()
        .unique_by(|v| v.0)
        .fold(0, |acc, cur| acc + cur.1);
    println!("{}", ans);
}

C++で同じようなことを書くとこうなると思います。

#include <bits/stdc++.h>
using namespace std;

int main() {
    size_t n;
    string s;
    cin >> n;
    cin >> s;

    vector<pair<char, int>> l;
    for (size_t i = 0; i < n; i++) {
        if (i == 0 || s[i] != s[i - 1]) {
            l.push_back(make_pair(s[i], 1));
        } else {
            l.back().second++;
        }
    }

    sort(l.begin(), l.end(), [](pair<char, int> a, pair<char, int> b) {
        if (a.first == b.first) {
            return a.second > b.second;
        }
        return a.first < b.first;
    });
    l.erase(unique(l.begin(), l.end(), [](pair<char, int> a, pair<char, int> b) {
        return a.first == b.first;
    }), l.end());
    int ans = accumulate(l.begin(), l.end(), 0, [](int acc, pair<char, int> p) {
        return acc + p.second;
    });
    cout << ans << endl;
}

Rustの方が、メソッドが非破壊的であるためチェーンすることができ、「ペアの2コ目の値でソートして、反転して、1コ目の値でuniqueして、2コ目の値の和を取る」という操作が簡潔に記述できていると思います。

コンパイラが優しい

型変換エラーの例
これはRustの型変換エラーの例です。「i32を期待したがusizeだった」「.try_into().unwrap()を使って変換することができる」と親切に教えてくれます。嬉しい。

Result型やOption型がイケてる

Rustでは、失敗する可能性のあるメソッドはResult型(Ok()Err())を返すようになっていて、仮にそのメソッドが失敗したとしても「エラーであったことを示すモノ」が返ってくるだけです。例えば、

// "abc"をi32(32ビット整数)としてパースしろ、という意味、当然失敗する
let hoge = "abc".parse::<i32>();

と書いても、ここですぐpanicはしません。

let hoge = "abc".parse::<i32>().unwrap();

.unwrap()によって値を取り出そうとして始めてpanicを起こします。
.unwrap()でpanicした例

この仕様によって多様にエラーハンドリングをすることができます。

// エラーの場合にpanic
let hoge = "abc".parse::<i32>().unwrap();

// エラーの場合に0を返す
let hoge = "abc".parse::<i32>().unwrap_or(0);

// 成功ならtrue、失敗ならfalse
let hoge = "abc".parse::<i32>().is_ok();

// matchでよしなに
let result = "abc".parse::<i32>();
let hoge = match result {
    Ok(num) => format!("Success: {}", num),
    Err(err) => format!("Failure: {}", err),
};

// if letでよしなに
let result = "abc".parse::<i32>();
if let Ok(num) = result {
    Ok(num) => println!("Success: {}", num),
}

一方Option型は、値が無い可能性のある場合の戻り値として使用されます。

// 「空の配列から最後の値を読み出す」という意味、無いものは無い
let empty: Vec<i32> = Vec::new();
let hoge = empty.last();

これも.unwrap().unwrap_or()などで値を取り出して扱うことができます。
良い

おしまい

Rustはいいぞ

Discussion