パターンマッチング


私は過去数日にわたって光栄に思っていたコードの断片を共有したいです.
文脈のために、私はウェブカメラから写真を撮るためのコマンドラインインターフェースに取り組んでいます.私は、Macベースの交換のドロップとして互換性を目指していますimagesnap ツール、そして私はクロスプラットフォームのサポートを有効にすることを期待して錆でそれを書いている.しかし、今のところ、私はMacの実装に焦点を当てており、MacOSのObjective - Cライブラリの1つとインタフェースをとっていますAVFoundation フレームワーク.
しかし、それはこのポストに重要ではありません.何が重要なのは、私は人間からの入力を受け入れるツールを構築し、それらをフードの下で非自明な行動のシリーズに変換することです.この種のシェルコマンドの仕事に精通している人は、任意の数の組み合わせで与えられる一連の引数を受け入れて解析することを意味します.
$ imagesnap -q -w 2 -d "Microsoft LifeCam HD-3000" 2020-01-05-test-snapshot-1.jpg
Capturing image from device "Microsoft LifeCam HD-3000"..................2020-01-05-test-snapshot-1.jpg
これは引数を受け付けないことを意味します:
$ imagesnap
Capturing image from device "FaceTime HD Camera (Built-in)"..................snapshot.jpg
そしてもちろん、不正な入力もちゃんと扱います.
$ imagesnap -xzfv
Error: Unrecognized option: 'x'
この種の人間-マシン・インタフェースパターンが実質的に無限の入力順列を考慮に入れるという事実は、それをとても強力で永続的にするものです.しかし、それはまた、古い挑戦を提示する:我々は設計しなければならないmain これらの入力を感知し、条件を調整する方法.
そして、私がConditionalsについて言うことができる1つのものがあるならば、それはあなたが持っているそれらのより多くであるということです、より微妙なバグはあります、そして、より難しく、それは縁ケースについて理由になります.
これが一般的に必要とするものを見てみましょう.

条件付き引数処理の実際
錆では、ほとんどの言語のように、ユーザーによって供給された入力のリストを得ることは超簡単です:
let args: Vec<String> = std::env::args().collect();
簡潔さのために、我々はそれを使うことができますgetopts 我々のプログラムのオプションを定義するライブラリ(そして、認識されていない引数がエラー状態になるようにする)
let mut opts = getopts::Options::new();
opts.optopt("w", "warmup", "Warm up camera for x seconds [0-10]", "x.x");
opts.optflag("l", "list", "List available capture devices");
opts.optopt("d", "device", "Use specific capture device", "NAME");
opts.optflag("h", "help", "This help message");

let inputs = opts.parse(&args[1..]).unwrap(); // might panic!
これにより、引数の処理ロジックをすぐに繰り返すことができます.あなたが私のようであるならば、あなたの脳は、入力を通って歩く一連の手続き的なステップを持ち出し始めたいです.
let help = inputs.opt_present("h");
let list_devices = inputs.opt_present("l");

let filename = if !inputs.free.is_empty? {
    inputs.free[0].clone()
} else {
    "snapshot.jpg".to_string()
}

let warmup_secs = if inputs.opt_present("w") {
    inputs.opt_str("w").parse().unwrap()
} else {
    0.5 // default based on rough manual testing
}

let device = if inputs.opt_present("d") {
    inputs.opt_str("d")
} else {
    get_default_device()
}
これは完全に賢明なアプローチであり、Cスタイルの言語の多くにわたって非常に似ていますgetopts これは単純にすべての入力を通してループを避けるのを助けます.
あなたはこの可能性があります画像を開始することができますif/else コンディショナルズ、すべての可能な行動の最終的なシリーズで
if help {
    print_usage();
} else if list_devices {
    list_devices();
} else {
    snap_image(filename, device, warmup_secs);
}
これまで続く?良い.
土曜日のように、これは私のコードがどのように見えたか(そして、より少ない)、そして、それはプロトタイピングのためによく働きました.もちろん、私はそれをきれいに呼び出すことができますunwrap() しかし、単純性のために、私は上記の例からすべてのエラー処理論理を除外しました.
そして、確かに、私がこれを実行した方法の束があります、しかし、あなたはおそらく上記のほとんどが比較的に不愉快であり、すべての可能性において、おもしろくないことに同意します.だからもっと面白いパーツに移動しましょう.

どこで故障するか
上記のアプローチが全く私を助けてくれないことは、コンディショナーの中で隠れる多くの角のケースです.
The getopts ライブラリは既にErr 変形した引数が渡された場合にはvariantを返しますが、例えば、以下のようにします.
$ imagesnap -l -w 1
Error: Invalid combination of arguments supplied!
この作業を条件にするには、両方を指定する必要があります.
if inputs.opt_present("l") && inputs.opt_present("w") {
    eprintln!("Error: Invalid combination of arguments supplied!");
    return;
}
しかし、すべての入力の間でこれを行うことは、全く厄介です!私は連鎖を開始するかもしれないopt_present 一緒に呼び出す&&/|| 演算子、または一連の条件を破ることで興味深い置換をチェックしますが、条件付き振る舞いのこの拡張はエラーになりがちであり、一般的にmain 方法は、ガード節(しばしばユーザーに通知されたバグに応じて)で散らばっています.
あるいは、それぞれの行動の厳しい状況に焦点を合わせることもできました.
if inputs.opt_present("l") {
    if no_incompatible_options_present {
        list_devices();
    } else {
        eprintln!("Error: Invalid combination of arguments supplied!");
    }
}
しかし、もう一度、「非互換性のないオプション」条件のための解決は、理由について悩ましていて、新しいプログラム引数を加えるたびに更新を必要とするかもしれません.
上記の難問に対する一般の解決は、単にそれを解決しないで、有効な入力を供給するためにユーザーに依存する.そして、それらが未定義の動作に遭遇しない.そのような傾向は、それが間違って取得するの低コストに相対的にそれを得るの高いコストの結果です.これはマイナーなIRCは、簡単に回避ので、なぜ面倒か?私は、そのようなトレードオフをするために、誰も非難しません.
しかし、我々がそれをより簡単にすることができるならば、どうですか?可能な入力の全体の行列を可視化し、コード内でその可視化を表現する低コストの方法があった場合は?

パターンとマッチング
はい、これはパターンマッチングが来るところです.
Rust ' s Match構文がどのように動作するかの簡単な例として、そのファイル名を前に確認してみましょう.
let filename = if !inputs.free.is_empty? {
    inputs.free[0].clone()
} else {
    "snapshot.jpg".to_string()
}
これは代わりにパターンマッチとして表現できます:
let filename = match inputs.free.get(0) {
    Some(name) => name.clone(),
    None => "snapshot.jpg".to_string(),
}
機能言語のファンはこのパターンを認識するでしょう.(パターンマッチングの最初の経験はHaskellと一緒でした).しかし、この例では、私たちはただ一つの値にマッチしていますget(0) 呼び出しを返すOption 全体的なアプローチは、if/else 文.
パターンマッチングが本当に輝いているところは、複数の値と型を作ることができます.点のケース:それからの「非互換性のないオプション」問題.複数入力のパターンのマッチングによってそれを解決するか?
match (
    inputs.opt_present("l"),
    inputs.opt_str("w"),
    inputs.opt_str("d"),
) {
    (true, None, None) => list_devices(),
    (true, _, _) => eprintln!("--list/-l is incompatible with other args!"),
    (_, _, _) => continue_execution(),
}
今、我々は話している!前と同様に、我々は依存しているopt_str 返すOption 種類他の言語では、これはヌルチェックであるかもしれません、しかし、さびにおいて、我々はタイプ変異体にパターンマッチをすることができて、我々がこれらの3つの入力のすべての可能な順列をカバーしたと保証します.
はい、それは保証です.我々がすべての可能性を考慮しない限り、さびはコードをコンパイルしませんmatch 枝.これは、ワイルドカードに依存して/キャッチすべての武器のようにcontinue_execution() 上記のアーム.

すべて行く
では、なぜすべての方法を行くと、すべての入力のすべてを含めるmatch ? いくつかはそれをクレイジーと呼ぶかもしれないが、私はパターンのマッチングで信じられないほどのユーティリティを見つけた.
確かに、ARGSを解析して扱う方法で論理的なバグを防ぐことはありませんが、少なくともユーザ入力のすべての可能なpermuttionschenを表現することを保証します.これは緩やかに関連条件の以前の山よりもはるかに良い出発地を提供します.
確信?または、少なくとも興味をそそら?グレート.最初の構造は、すべてのユーザ入力を計算します.
match (
    inputs.free.get(0), // optional filename
    inputs.opt_present("l"),
    inputs.opt_present("h"),
    inputs.opt_str("d"),
    inputs.opt_str("w"),
) {
    (file, false, false, device, warmup) => {
        snap_image(file, device, warmup) // todo: handle Option types
    }
    (None, true, false, None, None) => list_devices(),
    (None, false, true, None, None) => print_usage(),
    (_, _, _, _, _) => eprintln!("Invalid combination of arguments."),
}
合わせてNonelist_devices() and print_usage() ケースでは、これらの引数は分離でのみ使用できることを確認します.そうでなければマッチは失敗します.もちろん、snap_image() 我々は、いくつかを逃しているOption -しかし、そのメソッドが受け入れることができると仮定しましょうNone 型と既定値の適用.
私たちが残っているのは、ロジックの入力からアクション/結果に流れる方法の信じられないほど簡潔な表現です.各アームは、特定の動作につながる互換性のある入力のセットをまとめます.我々は2つの武器を同じ行動に導くのを避けなければならなくて、離れてそれらの詳細のいくつかを抽象化するために、我々のタイプシステム/変種に頼ります.

追加武器
我々はいくつかのコーナーケースが不足している可能性がありますが、それらのための会計は単に新しいパターンアームを追加することです!
私にすぐに飛び出す1つの事は、私は余分な議論を扱うのを忘れたということです.誰かが1つの代わりに2つ以上の非オプション引数を供給するならば、私はエラーを返したいです.これは2番目の引数に追加マッチを追加することで実現できますNone すべてのケースでキャッチします.
match (
    inputs.free.get(0), // optional filename
    inputs.free.get(1), // 2nd free arg should always be `None`
    // etc...
) {
    (file, None, ...) => snap_image(...),
    // etc...
    (_, _, ...) => eprintln!("Invalid combination of arguments."),
}
私が作りたいもう一つの改善は、「warmup」入力を解析して、検証することです.もう一度、私は2つの余分なマッチアームでスロットを缶詰にすることができますif パターンマッチの条件!
match (
    // Transpose lets us match on the `Result` of the parse,
    // but preserve the `Option` type. `Ok(None)` is a valid input!
    inputs.opt_str("w").map(|s| s.parse()).transpose(),
    // etc...
) {
    (..., Ok(Some(warmup))) if w < 0.0 || w > 10.0 => {
        eprintln!("Warmup must be between 0 and 10 seconds")
    }
    (..., Ok(warmup)) => snap_image(...),
    (..., Err(_)) => eprintln!("Failed to parse warmup!"),
    // etc...
}
いいね.

ラッピング
パターンマッチングの喜びは、少し離れて取得することが容易になりますので、もちろん私はこの特定の複雑さに留意する必要がありますmatch . 数値検証のようなものは、常に腕のうちの1つに隠されることができます.しかし、いくつかの引数を持つ単純なプログラムでは、フロントに有効な入力のフルレンジを表現することができることは非常に満足しており、コンパイラ強制保証が付属しています!
そして、これはCスタイルif/else アプローチは本質的に悪い!私は、多くのコーダーが上記を見ると想像しますmatch 構造とドライヒーティングを開始し、それは大丈夫です!宣言的なプログラミングは、いくつかの使用を取得し、誰もがその美学を好むわけではない.しかし、人々のために、うまくいけば、このポストは、若干のインスピレーションを引き起こしました!