"簡略化された構文ほど、設計者の姿勢が露呈する。"
Rustにおける ?
演算子は、一見すると些細な構文糖衣だ。
Result<T, E>
を扱う関数の中で、match
を書く代わりに ?
を使えば、コードがすっきりする。
let content = std::fs::read_to_string("config.toml")?;
しかしこの ?
こそ、Rustという言語の思想が凝縮された構文であり、
同時に開発者に対して「本当にこの関数はエラーを“上に伝える”べきなのか?」という問いを突きつける。
この章では、?
演算子に込められた設計の責任と構文的誠実さを深掘りする。
?
の本質は“委譲”である
?
を使うとはどういうことか?
それは、「ここで発生したエラーを、自分では処理せず、そのまま呼び出し元へ返す」という意味だ。
fn load_config() -> Result<String, std::io::Error> {
let content = std::fs::read_to_string("config.toml")?;
Ok(content)
}
このコードは、**明示的に「この関数は、読み取り失敗を自分では処理しない」**ことを表明している。
つまり ?
は構文的な簡略化であると同時に、責任の転送を行う記号である。
「責任を返す」ことの設計的意味
関数から Result<T, E>
を返すということは、呼び出し側にエラー処理の責務を課すということだ。
そして ?
は、それを構文的に簡素に記述できるが、その設計上の意味は重い。
使うということは:
- 呼び出し側に設計の判断を委ねる
- この関数の目的は「処理を継続すること」ではなく「責任を引き継ぐこと」である
つまり、?
を使うということは、自分の関数が「判断しない関数である」と明言することに他ならない。
?
は便利な構文ではなく、“設計の姿勢”を明文化する手段
たとえば ?
を多用して構文をスリム化したコードは、たしかに読みやすい。
fn process(path: &str) -> Result<String, std::io::Error> {
let content = std::fs::read_to_string(path)?;
let parsed = parse_config(&content)?;
Ok(parsed)
}
だがこのコードが本当に設計として正しいかは、別問題だ。
-
parse_config
のエラーはユーザーに返すべきか? -
read_to_string
の失敗を握りつぶさず、上層へ伝えるのは意図的か?
この問いを無視して ?
を並べると、コードは「見やすいが無責任な設計」になる。
構文の簡略化は、設計の透明化とイコールではない。
?が使える場所:構文の安全圏と設計の意識
?
が使えるのは、戻り値が Result<T, E>
または Option<T>
の関数内に限られる。
これは Rust の型システムによる構文レベルでの設計制御である。
fn try_parse(path: &str) -> Option<Config> {
let content = std::fs::read_to_string(path).ok()?;
Some(parse(&content)?)
}
ここでの ?
は None
の連鎖を発生させる。
「Noneが出たら終了する」という短絡的制御を、構文で表現している。
エラーを「処理する」のか「流す」のか、その判断が設計を分ける
設計者は、あるエラーに対して2つの判断が必要となる:
- この層で握りつぶすべきか(match等で明示的に処理)
- この層で処理せず、上位へ伝えるべきか(?で委譲)
これらをすべて ?
にするということは、「判断をすべて他者に委ねる」ことであり、
場合によっては設計上の怠惰と捉えられかねない。
?
を使うということは、“委譲していい”という設計的正当性があることが前提なのである。
expect vs ?:設計のポリシーの差異
let config = std::fs::read_to_string("config.toml").expect("必須ファイル");
この場合は、「エラーを許さない」という強い意思表明。
対して ?
は、「失敗するかもしれないが、その責任は自分では持たない」という柔らかい委譲。
この2つの違いは、構文上は小さくても、設計思想としてはまったく異なる。
「失敗しても止まらない設計」を実現するか、「失敗したら止める設計」を選ぶか
?
を繋げて書くことは、データフローを連鎖させる表現である。
一つでも失敗すれば処理は止まる。これは “失敗は止める”という思想に基づいた構文である。
let config = read_file(path)?
.lines()
.map(parse_line)
.collect::<Result<Vec<_>, _>>()?;
このような連鎖は、「失敗したら戻す」という設計方針があるからこそ意味を持つ。
結語:?は簡易な記号であり、重たい設計判断の表出である
Rustの ?
演算子は、単なる糖衣ではない。
それは「この関数は判断しない」「責任は上に渡す」という設計ポリシーを表明する構文である。
よって、?
の使用には常に問いが付きまとう。
- これは委譲すべき責任か?
- 上位で処理されることは保証されているか?
- 設計上、処理の継続か停止か、どちらが正当か?
この問いに誠実に答えたときだけ、?
は設計の美を宿す。
"構文が簡素であればあるほど、設計者の倫理が問われる。?とはその象徴である。"