Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@amutake
Last active November 20, 2017 07:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amutake/c72c634ebeee1bbe69203824bbef14d3 to your computer and use it in GitHub Desktop.
Save amutake/c72c634ebeee1bbe69203824bbef14d3 to your computer and use it in GitHub Desktop.
2005-pattern-binding-modes-ja

概要

参照に関するパターンマッチをよりわかりやすくします。

現在、参照に関するパターンマッチは ref& を使いわけなければいけません。

let x: &Option<_> = &Some(0);

match x {
    &Some(ref y) => { ... },
    &None => { ... },
}

// or using `*`:

match *x {
    Some(ref x) => { ... },
    None => { ... },
}

この RFC の導入後は、上のコードは変わらず動きますが、以下のようによりシンプルに記述することができるようになります。

let x: &Option<_> = &Some(0);

match x {
    Some(y) => { ... }, // `y` is a reference to `0`
    None => { ... },
}

これは、自動的な参照外しと、デフォルトの束縛モードの導入を通して達成されます。

モチベーション

Rust は通常、値と参照の区別、特に borrow されているデータと所有権を持っているデータの区別は厳格に行います。 しかし、明示的に記述することと人間にわかりやすくすることはしばしばトレードオフの関係にあります。 また、Rust はいくつかの注意深く選ばれた場所では人間工学的な側面について間違っています (! 人間にとってのわかりやすさよりも明示的に記述することを採用しているということ?)。 メソッド呼び出しやフィールドへのアクセスをするためにドット演算子を使うとき、クロージャを定義するとき、などです。

マッチ式はとても一般的な式で、おそらく Rust で最も重要な制御構造です。 借用されたデータは Rust で最も一般的な形でしょう。しかし、借用されたデータとマッチ式を同時に使うことはフラストレーション(挫折感)を起こさせます。 型チェッカと借用チェッカを満たすために *&ref を正しく組み合わせなければいけないということはよくある問題で、 Rust の初心者が早い段階でぶち当たるものです。コンパイラはどんなものが必要なのか推論できるのに、ヘルプではなくただのエラーメッセージしか出さないのが特にフラストレーションを起こさせます。

例えば、以下の問題を考えます:

enum E { Foo(...), Bar }

fn f(e: &E) {
    match e { ... }
}

ここでは何がしたいかは明確です。e が参照しているものがどのバリアントか (Foo なのか Bar なのか) チェックしたいというものです。 面倒なことに、正しい書き方が2つあります。

match e {
    &E::Foo(...) => { ... }
    &E::Bar => { ... }
}

と、

match *e {
    E::Foo(...) => { ... }
    E::Bar => { ... }
}

です。

前者はより明らかです。が、よりノイジーなシンタックス (節ごとの &) が必要です。 後者は初心者には少し不思議かもしれません。型チェッカは *e を値として扱いますが、借用チェッカはそのデータをマッチ式の間借用されているものとして扱います。 また、ネストした型については動きません。例えば match (*e,) ...  というコードは許されません (! 試してみたら cannot move out of borrowed content というエラーになった)。

どちらのケースも、変数をさらに束縛する場合、データをムーブしないようにする必要があります。例えば、

match *e {
    E::Foo(x) => { ... }
    E::Bar => { ... }
}

x の型が Copy というトレイトを実装していなかったら、これは借用チェックがエラーになります。参照を取得するために、E::Foo(ref x) (もしくは *e ではなく  e に対してパターンマッチしている場合は &E::Foo(ref x)) というように ref キーワードを使わなければいけません。

ref キーワードは Rust の初心者にとってはつらいもので、それ以外の人にとっては大して気にもとめないようなものです (! a bit of a wart ちょっとしたイボ??)。 ref キーワードはパターンマッチ宣言のルールに違反するもので (! 違反してるの?)、パターン以外では使われず、よく & と混同されます (たとえば https://github.com/rust-lang/rust-by-example/issues/390)。

マッチ式はプログラマがしばしば遊ぶ「型テトリス」の一種です。つまり、問題の理解なしにコンパイラが黙るまで適当に演算子を追加するようなことです (! 思考停止して適当にやる、みたいなニュアンスだと思う)。 この RFC では、マッチ式を、安全性と可読性を犠牲にせずにより人間にわかりやすいものにできます。

マッチ式の人間工学は2017年の改善の一つとして取り上げられてきました。

詳細なデザイン

この RFC はマッチ式人間工学 RFC を洗練させたものです。 (オリジナルの RFC の) 自動的な参照外しと自動的な参照の取得を用いるよりも、この RFC では、参照値が参照ではないパターンにマッチしたときの デフォルトの束縛モード というものを導入します。

言い換えると、パターンマッチ中で自動的な参照外しを許します。自動的な参照外しが起こると、コンパイラは自動的に束縛を ref または ref mut の束縛として扱います。

例:

let x = Some(3);
let y: &Option<i32> = &x;
match y {
  Some(a) => {
    // `y` は参照が外され, `a` は `ref a` として束縛される.
  }
  None => {}
}

この RFC はマッチ式だけではなくすべてのパターンマッチに適用されます。

struct Foo(i32);

let foo = Foo(6);
let foo_ref = &foo;
// `foo_ref` は参照が外され、 `x` は `ref x` として束縛される.
let Foo(x) = foo_ref;

定義

参照パターンは、強制 (coercion) を除いた、参照にマッチし得る任意のパターンです。 参照パターンは、束縛、ワイルドカード (_)、参照型の const& または &mut で始まるパターンを含みます。 それ以外のすべてのパターンは非参照パターンです。

デフォルト束縛モード: moverefref mut のこのモードは、新規のパターン変数をどのように束縛するかを決定するために使われます。 コンパイラは、明示的に ref ref mut mut とつけられていない変数束縛を発見したとき、デフォルト束縛モードを、どのように束縛すべきかの判断に使います。 現在、デフォルト束縛モードは常に move です。この RFC の下では、非参照パターンでの参照のマッチは、デフォルト束縛モードが ref または ref mut となります。

束縛モードのルール

デフォルト束縛モードは move から出発します。あるパターンにマッチするか見るとき、コンパイラはパターンの外側からはじめて内側に行きます (! ネストしたパターンは外側からやるという意味)。 参照が非参照パターンを使ってマッチされるときは、自動的に参照が外され、デフォルト束縛モードが更新されます:

  1. その参照が &val だった場合、デフォルト束縛モードが ref になる
  2. その参照が &mut val だった場合: 現在のデフォルト束縛モードが ref だったとき、ref のままとする。それ以外は、デフォルト束縛モードを ref mut にする。

自動的に参照が外された値が参照だったときは、さらに参照が外されてこの一連のプロセスが繰り返されます。

                        Start                                
                          |                                  
                          v                                  
                +-----------------------+                     
                | Default Binding Mode: |                     
                |        move           |                     
                +-----------------------+                     
               /                        \                     
Encountered   /                          \  Encountered       
  &mut val   /                            \     &val
            v                              v                  
+-----------------------+        +-----------------------+    
| Default Binding Mode: |        | Default Binding Mode: |    
|        ref mut        |        |        ref            |    
+-----------------------+        +-----------------------+    
                          ----->                              
                        Encountered                           
                            &val

束縛モードが ref であるとき、このモードから出る方法はありません。これは & の 中の &mut は共有された参照であり、実際の値をミュータブルな値として使うことはできないからです。

また、明示的な ref または ref mut の束縛を使ったときは束縛モードは遷移しません。デフォルト束縛モードは非参照パターンを使った参照のマッチングによってのみ変わります。

上記のルールとこれ以降の例は、@nikomatsakis の コメント で書かれました.

今の振る舞い:

match &Some(3) {
    p => {
        // `p` はただの変数束縛です. 従って、これはデフォルトが `ref` のマッチではありません。
        // `p` はムーブセマンティクスで束縛されます (そして `&Option<i32>` を持ちます)。
    },
}

一つの節における新しい振る舞い:

match &Some(3) {
    Some(p) => {
        // このパターンは `const` 参照、`_`、`&` パターンではありません。
        // よってこれは非参照パターンです。
        // `&` 参照を外し、デフォルト束縛モードを `ref` に変更します。
        // `p` は `ref p` とされ、型は `&i32` になります。
    },
    x => {
        // この節では、デフォルトの `move` モードです。よって `x` の型は `&Option<i32>` です。
    },
}

// 脱糖後:
match &Some(3) {
  &Some(ref p) => {
    ...
  },
  x => {
    ...
  },
}

"or" (|) パターンとともに使ったマッチ式:

let x = &Some((3, 3));
match x {
  // ここでは、各パターンは非依存なものとして扱われます。
  Some((x, 3)) | &Some((ref x, 5)) => { ... }
  _ => { ... }
}

// 脱糖後:
let x = &Some((3, 3));
match x {
  &Some((ref x, 3)) | &Some((ref x, 5)) => { ... }
  None => { ... }
}

新しい振る舞いと古い振る舞いでの複数のネストされたパターンは、それぞれ、

match (&Some(5), &Some(6)) {
    (Some(a), &Some(mut b)) => {
        // ここでは、`a` は `&i32` になります。タプルの1要素目で非参照パターンを使い、`ref` モードになっているからです。
        //
        // 2要素目は非参照パターンではないので、`b` は `i32` (`move` モードで束縛) になります。さらに、`b` はミュータブルです。
    },
    _ => { ... }
}

// 脱糖後:
match (&Some(5), &Some(6)) {
  (&Some(ref a), &Some(mut b)) => {
    ...
  },
  _  => { ... },
}

複数回参照外しをする例:

let x = (1, &Some(5));
let y = &Some(x);
match y {
  Some((a, Some(b))) => { ... }
  _ => { ... }
}

// 脱糖後:
let x = (1, &Some(5));
let y = &Some(x);
match y {
  &Some((ref a, &Some(ref b))) => { ... }
  _ => { ... }
}

ネストした参照の例:

let x = &Some(5);
let y = &x;
match y {
    Some(z) => { ... }
    _ => { ... }
}

// 脱糖後:
let x = &Some(5);
let y = &x;
match y {
    &&Some(ref z) => { ... }
    _ => { ... }
}

新しいミュータブルな参照を作る振る舞いの例:

match &mut x {
    Some(y) => {
        // ここでは `y` は `&mut` 参照です。`ref mut` を使うのと同じです。
    },
    None => { ... },
}

// 脱糖後:
match &mut x {
  &mut Some(ref mut y) => {
    ...
  },
  &mut None => { ... },
}

let の例:

struct Foo(i32);

// これらのルールは `match` や `let` に関わらず任意のパターンマッチに適用されます。
// 例えば、ここでの `x` は `ref` での束縛です。
let Foo(x) = &Foo(3);

// 脱糖後:
let &Foo(ref x) = &Foo(3);

後方互換性

後方互換性を保つために、この提案は参照が非参照パターンによってマッチされた場合 (これは現在ではエラー) のみを対象とします。

この推論は、コンパイラが、「マッチされた型が参照であるかどうかを知っている」ということを必要とします。 マッチした型が参照かもしれないしかそうでないかもしれない場合、かつ、それが非参照パターンによってマッチされたものである場合、コンパイラはそれが参照ではないとし、束縛モードが move になり、現在の Rust と全く同じふるまいをします。

例:

let x = vec![];

match x[0] { // panic になりますが、この例ではあまり関係ありません。

    // ここでマッチするとき、`x[0]` が `Option<_>` なのか `&Option<_>` なのかはわかりません。
    // `Some(y)` は非参照パターンであるため、`x[0]` は参照ではないと仮定します。
    Some(y) => {
        
        // `Vec::contains` は `&T` を取るので、`x` は `Vec<Option<usize>>` でなければいけません。
        // しかし、マッチの本体 (`x[0]`) を分析する前にこのことはわかりません。
        if x.contains(&Some(5)) {
            ...
        }
    }
    None => {}
}

どう教えるか

この RFC は参照に関するマッチングを簡単かつエラーを起こしにくいものにします。 参照に関するマッチングに関するドキュメントはこの RFC を踏まえたものに更新される必要があります。 ドキュメントとエラーメッセージは新しくシンプルな構文に合わせて refref mut の記述を徐々になくしていくべきです。

欠点

この提案の主要な欠点はパターンマッチのロジックが複雑になることです。 しかし、これはよくあるケース (! 参照に非参照パターンを (間違って) 使っているケースだと思われる) を「動く」ものにし、初心者にとってはより直接的で、マニュアルの参照を少なくするものになるでしょう。

今後の拡張

今後、この RFC に対して、 DerefDerefMut トレイトを使ったカスタムスマートポインタの自動的な参照外しのサポートという拡張がなされるでしょう。

let x: Box<Option<i32>> = Box::new(Some(0));
match &x {
    Some(y) => { ... }, // y: &i32
    None => { ... },
}

この機能はこの RFC からは省かれました。DerefMove トレイトなどを考えたときなどの詳細部分がまだいくつか定まっていないためです。

まだ定まってはいなくはあるのですが、以降の RFC で、カスタム自動参照外しが可能な型のサポートは、後方互換性を保ったまま追加されるべきです。

他の案

  1. ref しか推論しないで、ref mutmut は明示的に指定してもらうようにする案。 これはミュータビリティは明示的に保つという利点を持ちます。 残念ながら、これは非直感的な結果にもなります。実際 ref mut はミュータブルな束縛を提供しません。これはミュータブル参照のイミュータブルな束縛を提供します。
// いまの振る舞い:
let mut x = Some(5);
let mut z = 6;
if let Some(ref mut y) = *(&mut x) {
    // ここでは `y` はイミュータブルな束縛。`y` は `x` の値を変更するときに使われますが、
    // `y` 自身は新しい参照に束縛できません。
    y = &mut z; //~ ERROR: re-assignment of immutable variable `y`
}

// この RFC が入った後:
let mut x = Some(5);
let mut z = 6;
if let Some(y) = &mut x {
    // 上と同じエラーになります。`y` はイミュータブルな束縛です。
    y = &mut z; //~ ERROR: re-assignment of immutable variable `y`
}

// 明示的な `mut` アノテーションを必要にするならこうなります:
let mut x = Some(5);
let mut z = 6;
if let Some(mut y) = &mut x {
    // エラーメッセージは同じですが、ひどく混乱させるものになっています。
    // `y` は `mut` がついていますが、変更不可能です。
    y = &mut z; //~ ERROR: re-assignment of immutable variable `y`
}

さらに、イミュータブルな参照の束縛を宣言するときにも mut は現在必要ありません。

// いまの振る舞い:
let mut x = Some(5);
// ここで `y` は、`x` を変更するものとして使われているにも関わらず、`mut` としては宣言されていません。
let y = &mut x;
*y = None;

参照の束縛で mut の明示をユーザに強制させることは、Rust の現在のセマンティクスと一貫性を持ちません。その結果エラーがよくわからないものになります。

  1. オリジナルの RFC で提案された、auto-ref / deref をサポートする案。 このアプローチは後方互換性を壊す可能性があります。そして、借用された値なのかそれともムーブされた値なのかの (人間の) 推論をより難しくします。

  2. パターンに move の記述を許す案。 これがなければ、move は、refref mut と違って、常に暗黙的なものになり、 refref mut のデフォルト束縛モードを上書きしたり参照の裏から値をムーブする方法がなくなります。 しかし、共有参照もしくはミュータブルな参照の裏にある値をムーブすることは Copy のみ可能なので、特に便利というわけではなく、むしろ不必要な複雑さを Rust に持ち込んでしまいます。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment