AtCoder で Option も Result も ? で .unwrap() する

競技プログラミングでの Rust のつらさのひとつに .unwrap() と9文字も書くのがだるいというのがあります。エラー伝搬は ? 1文字なのでエラー無視も楽に書きたいところですが、この記事にあるように stable Rust が入っている競プロサイトでは無理そうに見えます。ところが、実はできます。1

fn main() {
    main1().ok().unwrap()
}
fn main1() -> Result<(), Never> {
    // ここにメインの実装
    dbg!("foo 42 bar".split_ascii_whitespace().nth(1)?.parse::<i32>()?);
    Ok(())
}
enum Never {}
impl<E: std::fmt::Debug> From<E> for Never {
    fn from(e: E) -> Self {
        panic!("unwrap ?: {:?}", e)
    }
}

解説

現在の AtCoder 上の Rust はコンパイラのバージョンが 1.42.0 なので ? の仕様は (v2 になる前の) try_trait です。ここで Option? して Result を early-return するための型 std::option::NoneError#[feature(try_trait)] がないと名指しできないのですが、 Debug トレイトを持つすべての型からの変換なら stable の範囲で書けるため、 From<NoneError> が実装できてしまいます。標準的に使われるエラーは std::error::Error トレイトなので Debug + Display 以上と分かっているし fn binary_search(&self, x: &T) -> Result<usize, usize>2 のように「エラー」ではない Result もだいたい Debug です。

ちなみに try_trait_v2 では専用の FromResidual トレイトを実装する必要がある3のでこういう抜け穴はありません。

main 関数に書きたい場合

fn main() -> Result<(), Never> {
    // ここにメインの実装
    Ok(())
}
#[derive(Debug)]
enum Never {}
impl<E: std::fmt::Debug + Clone> From<E> for Never {
    fn from(e: E) -> Self {
        panic!("unwrap ?: {:?}", e)
    }
}

Never: Debug にするとそのまま main にできるのですが、 T: From<T> との重複実装が許されないため、変換できるエラー型を絞る必要があり、たとえば Debug + Clone を課すと良さそうです。 std::marker::PhantomPinned を用いて Never: Debug + !Unpin にして E: Debug + Unpin にできれば使い勝手良くなりそうですが、なぜかできませんでした (T: From<T> との重複判定が消えないため)。


  1. .unwrap() を直接書く場合と比べるとエラー時に表示されるソースコード位置が変わってしまう問題はあります。 RUST_BACKTRACE=1 で1つ下を見ましょう。

  2. partition_point が 1.52.0 からなので AtCoder では仕方なく使ったりします。

  3. try_trait_v2 については以前の日記にも書きました。 https://toslunar.hatenablog.com/entry/2021/08/03/230856