Rustでもっとも微妙な構文
(zkrising.com)Rustのletとconst
letは新しい変数を宣言するために使われるlet PAT = EXPR;という形で、見た目以上に強力- パターンマッチと組み合わせることで便利な機能を提供する
let (a, b) = (5, 10);let maybe_string: Option<String> = ..;let Some(value) = maybe_string else { panic!("die horribly")};
constはコンパイル時に計算され、コンパイル済みコードに直接埋め込まれる定数const MY_VAR: &str = "heyyyyyyyy man";const SECRET: i32 = 0x1234;const IDENT: TYPE = EXPR;という形で、型を明示する必要があり、パターンは使えない
ややこしい点
constは宣言順に関係なく使える(ホイスティング)
// XがYの後に定義されていてもコンパイルされる
const Y: i32 = X + X;
const X: i32 = 5;
- 関数の中でも宣言でき、その場合でもホイスティングされる
fn oh_boy() -> i32 {
return X;
const X: i32 = 5;
// ^ コンパイルされて動作する。警告もない!
}
- JavaScript出身でRustを学び始めたばかりのプログラマーと一緒に作業しているなら、この機能は彼らを戸惑わせるにはうってつけ
- これは優れた機能の無害な結果だが、次は有害な結果を書いてみよう
Rustのmatch
// let PAT = EXPR;
let x = 5;
// この場合、`x`はパターン。`5`を`x`に入れられるかを確認している
// このパターンは常にマッチする -- 5は常に`x`という変数に入れられる
// すべてのパターンが必ずマッチする必要はない。例えば:
let (5, x) = (a, b);
// ここでは式は a == 5 の場合にだけパターンに「マッチ」する
//
// これを「反駁可能(refutable)」なパターンと呼ぶ
//
// `let`宣言では、反駁可能なパターンは「拒否された(refused)」場合を処理しなければならない:
let (5, x) = (a, b) else { panic!() };
//
// ...そうしないと「条件付きでしか存在しない(conditionally existing)」変数を持つことになり、これはよくない
- では
matchを見てみよう。matchとは何か?
// matchは、パターンにマッチしたときに実行する処理の一覧
//
// match EXPR {
// PAT => EXPR
// PAT => EXPR
// ..
// }
match (a, b) {
(5, x) => {
// もし(a,b)が(5,x)にマッチしたら、このブロックが実行される
},
(x, 5) => {
// 同様に: もし(a,b)が(x, 5)にマッチしたら..
},
(x, y) => {
// そしてこれは「何でも受け止める」パターンで、let (x,y) = (a,b) と同じ動作
}
}
苦痛を与えてみよう
- 人を混乱させるのも楽しいが、完全な不幸と実際のバグを引き起こすのはどうだろう?
- 私から見ると、これがRustのもっとも微妙な構文だ:
- この記事でいちばん面白い一文: Rustでもっとも微妙な構文は、定数そのものがパターンであること
- この構文はマッチ周りにいくつかの優れた ergonomic を加える:
let input: i32 = ..;
const GOOD: i32 = 1;
const BAD: i32 = 2;
match input {
// これは input == GOOD かを確認する。GOODは定数だから
GOOD => println!("input was 1"),
// これは input == BAD かを確認する。BADは定数だから。
BAD => println!("input was 2"),
// これは otherwise = input を定義し、常にマッチする...
otherwise => println!("input was {otherwise}"),
}
しかし、定数を大文字で書くのは単なる慣例にすぎない。そうしない場合はコンパイラが警告するだけだ。
const good: i32 = 1;
const bad: i32 = 2;
match input {
// うーん...
good => {},
bad => {},
otherwise => {},
}
これで、見た目は同じ3つの分岐が並ぶが、それぞれが何をするかはその名前の定数が存在するかどうかに依存してしまう!
さらに悪くしてみよう。下では何が起きるだろう?
const GOOD: i32 = 1;
match input {
// タイポ...
GOD => println!("input was 1"),
otherwise => println!("input was not 1")
}
ここではコンパイラ警告は出るが、このコードは常にinput was 1を出力する
あるいは、もう少し現実的には:
// しまった、うっかりこのimportをコメントアウトまたは削除してしまった
// use crate::{SOME_GL_CONSTANT, OTHER_THING}
// おっと!
match value {
SOME_GL_CONSTANT => ..,
OTHER_THING => ..,
_ => ..,
}
これは人を混乱させる。特にenumでしゃれたことをしようとすると、なおさらだ。
enum MyEnum {
A, B, C
}
// 普通はこう書く
match value {
MyEnum::A => ..,
MyEnum::B => ..,
MyEnum::C => ..,
}
// でもこう書くこともできる
use MyEnum::*;
match value {
A => {},
B => {},
C => {}
}
// そして、その後 MyEnum を変更すると...
enum MyEnum { A, B, D, E };
use MyEnum::*;
// それでもこれはコンパイルされる!
match value {
A => {},
B => {},
C => {},
}
// `C`はいまや「何でも受け止める」パターンになる。`C`のようなものがスコープ内に存在しないからだ。
// あなたがやっているのは let C = value であり、これは常にマッチする!!!
Clippyには、こういうことをするなと警告するルールがたくさんある。なぜなら、これはいつも人を混乱させるからだ。
しかし、これはさらに混乱を深めることもできる:
// xを5に irrefutably バインドする...
let x = 5;
// ...ちょっと待って...
const x: i32 = 4;
このコードはコンパイルされない。なぜならconst xはパターンであり、定数はホイスティングされるので、このコードは次のように評価されるからだ:
let 4 = 5;
// error[E0005]: refutable pattern in local binding
// --> src/main.rs:3:5
// |
// 3 | let x = 5;
// | ^
// | |
// | パターン `i32::MIN..=3_i32` と `5_i32..=i32::MAX` がカバーされていない
// | `x` が新しい変数ではなく定数パターンとして解釈されるため、不足しているパターンはカバーされない
// | ヘルプ: 代わりに変数を導入してください: `x_var`
// |
// = note: `let` バインディングには「irrefutable pattern」が必要。たとえば `struct` や1つのvariantしか持たない `enum` のようなもの
「exprが4に等しい」は反駁不可能なマッチではなく、それ以外の場合を処理していない
周囲全員をイラつかせる
// `maybe` が Option<&str> だとする。何らかのテキストかもしれないし、Noneかもしれない。
let maybe_username: Option<&str> = ..;
// これは1行マッチでのRustの一般的なパターン。これが Some(..) とマッチすれば、その文字列で何かできる。
if let Some(username) = maybe_username {
// だからこのコードは username が存在すれば実行される...
return username.to_uppercase();
}
// ところが... いまやこのコードは 'username' が Some("hey") とマッチするときにだけ実行される
const username: &str = "hey";
定数のホイスティングと、定数がパターンであるという事実の組み合わせによって、謎めいたRustコードを書くことができてしまう
これは実際の問題ではない
- 現実的には、これが混乱を招きうる唯一の理由は、
let UPPERCASEとconst lowercaseが書けてしまうこと - もし大文字で始まる変数を作ることが lint エラーだったなら、混乱は起きないだろう
- enum variantや定数にマッチさせようとして、誤って何かをバインドしてしまうことはなくなるはずだから
- ただしはっきり言っておくと、これは単に言語の面白い癖にすぎない
macro_rules! f {
($cond: expr) => {
if let Some(x) = $cond {
println!("i am some == {x}!");
} else {
println!("i am none");
}
}
}
fn main() {
f!(Some(100));
{
f!(Some(100));
return;
const x: i32 = 5;
}
}
3件のコメント
実際のところ大きな問題ではなくて、たいていの開発環境には言語サーバーがあり、
そこで全部推論して表示してくれるからですね。
RustRover の言語サーバーの基盤である rust-analyzer は、かなり強力なツールなんですよ
どの言語にもあるダークパターンをただ集めてきて、
これは混乱を招く可能性がある!
そんな感じの文章なんですよね
うーん……という感じですね。Rust はこれをどうするつもりなのでしょうか?