HN 日本語サマリー

← 一覧へ戻る
プログラミング

網羅的な分割代入を称賛する

In Praise of Exhaustive Destructuring (antoine.vandecreme.net)

24 pointsby avandecreme5 コメント

要約

この記事は、プログラミング言語における構造体(struct)の分割代入(destructuring)において、全てのフィールドを明示的に指定する「網羅的な分割代入」の利点を説いています。当初は冗長に感じられたこの手法が、構造体にフィールドが追加された際にコンパイラがエラーを検知してくれるため、コードの保守性を高め、バグを防ぐ上で非常に有効であることをRustの例を挙げて解説しています。

全文翻訳

Rustを学んでいた頃、構造体を分割代入する際に全てのフィールドをリストアップするか、`..`構文を使用しなければならないことに不満を感じていました。これを説明するために、そして猛暑の最中にこの記事を書いているので、次のような構造体があると仮定しましょう。 struct WeatherReading { station_id: String, recorded_at: DateTime<Utc>, temperature: f64, humidity: f64, pressure: f64, } 注: `recorded_at`以外は全てnewtypeであるべきですが、簡潔さのために省略しています。 ここで、`station_id`と`recorded_at`だけを取得したいのに、なぜ次のように書く必要があるのでしょうか。 let WeatherReading { station_id, recorded_at, .. } = weather_reading; TypeScriptでは、次のように簡単にできます。 const { station_id, recorded_at } = weather_reading; Haskellでは: -- NamedFieldPuns拡張を使用 let WeatherReading { station_id, recorded_at, } = weather_reading -- または RecordWildCards を使用 let WeatherReading {..} = weather_reading それほど大きな手間ではありませんが、これにより `weather_reading.station_id`/`weather_reading.recorded_at` の構文を好むようになりました。しかし、過去数ヶ月で、フィールドに`.`構文でアクセスするよりも構造体の分割代入を大いに好むようになりました。なぜなら、それがソフトウェアの保守をより安全にしてくれるからです。例えば、危険な天候を検出したいとしましょう。 fn is_dangerous(weather_reading: &WeatherReading) -> bool { if weather_reading.temperature > 40.0 { //摂氏と仮定、華氏で104°F、ケルビンで313.15K return true; } if weather_reading.humidity < 5.0 { //パーセンテージと仮定 return true; } if weather_reading.pressure < 960.0 { // hPaと仮定、inHgで28.35 return true; } return false; } その関数は危険です!その理由を見るために、いくつかの観測局が風速計を備え、風速を記録できるようになり、`WeatherReading`構造体に新しい`wind_speed`フィールドを追加したとしましょう。 struct WeatherReading { station_id: String, recorded_at: DateTime<Utc>, temperature: f64, humidity: f64, pressure: f64, wind_speed: Option<f64>, // これもnewtypeであるべき } 全て順調!出荷しましょう! おっと。`is_dangerous`関数を更新することを覚えていますか?確かに強い風は危険として報告されるべきです。残念ながら、Rustコンパイラは私たちを警告してくれませんでした。良いニュースは、関数を次のように書いていれば警告を得られたことです。 fn is_dangerous( WeatherReading { station_id: _, recorded_at: _, temperature, humidity, pressure, }: &WeatherReading ) -> bool { if *temperature > 40.0 { //摂氏と仮定、華氏で104°F、ケルビンで313.15K return true; } if *humidity < 5.0 { //パーセンテージと仮定 return true; } if *pressure < 960.0 { // hPaと仮定、inHgで28.35 return true; } return false; } これでエラー[E0027]: pattern does not mention field 'wind_speed' が発生します。このトリックが機能するためには、未使用のフィールドについて明示的に言及する必要があり、`..`パターンを使用する誘惑に抵抗する必要があることに注意してください。Rustでは、`self`を引数リストから分割代入できないため、`is_dangerous`がメソッドとして書かれていた場合、少し手間がかかります。 impl WeatherReading { fn is_dangerous(&self) -> bool { let WeatherReading { station_id: _, recorded_at: _, temperature, humidity, pressure, } = self; if *temperature > 40.0 { //摂氏と仮定、華氏で104°F、ケルビンで313.15K return true; } if *humidity < 5.0 { //パーセンテージと仮定 return true; } if *pressure < 960.0 { // hPaと仮定、inHgで28.35 return true; } return false; } } このトリックは、CRUD Webサービスの異なるレイヤー(データアクセス、ビジネスロジック、API)間の`From`実装を書く際に特に役立つと思います。あるレイヤーにフィールドを追加すると、コンパイラはそのフィールドを他のレイヤーに伝播させるかどうかを決定するように強制します。もう一つの利点は、他のフィールドが無視されている間に、同じフィールドの束が常に一緒に分割代入されているのを見た場合、それらのフィールドを独自の構造体に抽出するべきかもしれないという良い兆候となりうることです。TypeScriptを使用している場合、`Required`型を使用したトリックがあります。Haskellには、驚くべきことに現時点では解決策がありませんが、提案があります。