Rustにおける左辺値選好と可変性調停

概要: Rustのミュータビリティー推論に使われる左辺値選好と可変性調停について説明する。

左辺値選好

Rustの型推論では、期待型のほかに、左辺値選好 (lvalue preference) という状態もトップダウンに渡される。

LvaluePreferenceは以下のどちらかの値をとる。

  • PreferMutLvalue … 可変な左辺値を優先する。
  • NoPreference … 選好はない。不変な左辺値や、右辺値などでよい。

左辺値選好はほぼ構文的に決定される。具体的には以下のように決定・伝搬される。

  • &mut の内側の式は PreferMutLvalue
  • 代入の左辺は PreferMutLvalue
  • メソッド呼び出しとフィールド参照のレシーバーは左辺値選好を継承する。
  • x[i] の左辺は左辺値選好を継承する。
  • 参照外し演算子は左辺値選好を継承する。

左辺値選好は次の用途に用いられる

  • x[i] は原則として Index に脱糖されるが、PreferMutLvalue ならば先に IndexMut を試す。
  • *x は原則として Deref に脱糖されるが、 PreferMutLvalue ならば先に DerefMut を試す。
    • メソッド以外の自動参照外しに対しても同じ処理が行われる。
    • メソッドのレシーバの自動参照外しは NoPreference が仮定される。 (後述)

左辺値選好が誤推論するケース

さて、この左辺値選好は多くの場合に正しく可変性を推論するが、誤った推論をするケースも考えられる。以下がこの左辺値選好の仮定である。

  • x[i] の左辺は可変性を継承する→正しい。 (Index/IndexMut の型がそうなっているので)
  • *x は可変性を継承する→正しい。 (Deref/DerefMut の型がそうなっている。また、 &T/&mut T/Box<T> の参照外しの動作も同様である。)
  • フィールド参照は可変性を継承する→正しい。
  • メソッド呼び出しは可変性を継承する→正しくない。 &mut self を受け取り &T を返したり、 &self を受け取り &mut T を返したりする可能性がある。

これにより、メソッド呼び出しのレシーバの位置に DerefIndex が来ると、可変性の推論で誤推論を起こす可能性がある。これを実験すると以下のようになる。

use std::fmt::Debug;
use std::ops::{Deref, DerefMut};


// deref/deref_mut 時にメッセージを出すラッパー
#[derive(Copy, Clone, Debug)]
pub struct Wrap<X: Debug + ?Sized>(X);

impl<X: Debug + ?Sized> Deref for Wrap<X> {
    type Target = X;
    fn deref(&self) -> &Self::Target {
        println!("deref({:?})", self);
        &self.0
    }
}

impl<X: Debug + ?Sized> DerefMut for Wrap<X> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        println!("deref_mut({:?})", self);
        &mut self.0
    }
}


// 左辺値選好による推論が失敗する例
#[derive(Copy, Clone, Debug)]
pub struct A;

impl A {
    pub fn f<'a, 'b>(&'a self, p: &'b mut i32) -> &'b mut i32 {
        p
    }
    pub fn g<'a, 'b>(&'a mut self, p: &'b i32) -> &'b i32 {
        p
    }
}

fn main() {
    let mut x = Wrap(A);
    let mut y : i32 = 0;
    
    // レシーバが&Tでかまわないが、構造上&mut Tが推論される例
    &mut *(*x).f(&mut y); // deref_mut(Wrap(A));
    &mut *x.f(&mut y); // deref(Wrap(A));
    // レシーバが&mut Tを必要とするが、構造上&Tが推論される例
    &*x.g(&y); // deref_mut(Wrap(A));
}

これを見ると、一番最初の例だけ、 Deref::deref でよいはずの位置で DerefMut::deref_mut が呼ばれているが、他の例では問題なく動作している。

これは次のような理由による。

  • この例には書いていないが、&T で十分だが &mut T が推論され、しかし実際には DerefMut が実装されていない場合には、 Deref にフォールバックされるため問題ない。
  • メソッドのレシーバの自動参照外しでははじめ DerefMut ではなく Deref が仮定される。
  • メソッド解決後に発生する可変性調停のため、推論と異なり &mut T が必要な場合は、そのように修正される。

可変性調停

可変性調停 (reconciliation) はメソッドの解決後に行われる処理で、レシーバが &mut self だった場合に Deref/Index を再解決する処理である。

これは次のような手順を踏む。

  • レシーバを指定している式を調べ、影響を受ける Deref/Index 呼び出しを列挙する。 (例えば x[i][j].some_method()some_method のレシーバが &mut self の場合、 x[i]x[i][j] を補正する必要がある。
  • それぞれの Deref/IndexPreferMutLvalue で再推論する。これによりこの部分が DerefMut/IndexMut に昇格する。

これにより、&mut T が要求される部分で &T を誤推論したことにより発生しえたエラーを回避している。

まとめ

  • *xx[i] などではイミュータブルな Deref/Index とミュータブルな DerefMut/IndexMut のいずれかが選択される。
  • これらは色々な仕組みによりだいたいいい感じに推論される。それには以下のような動作が関わってくる。
    • 構文的に決まる「左辺値選好」をトップダウンに伝搬する。
    • DerefMut が駄目なら Deref にフォールバックする。
    • メソッド解決でははじめイミュータブルを仮定し、 &mut self が要求されていたらミュータブルとして再推論する。