25

これは私がしばらく疑問に思っていた質問です。if ステートメントは、ほとんどのプログラミング言語 (少なくとも私が使用したプログラミング言語) の定番ですが、Haskell ではかなり嫌われているようです。複雑な状況では、Haskell のパターン マッチングは一連の if よりもはるかにクリーンであることは理解していますが、実際の違いはありますか?

簡単な例として、自家製バージョンの sum を取り上げます (そうです、単に である可能性があることはわかっていますfoldr (+) 0)。

sum :: [Int] -> Int
-- separate all the cases out
sum [] = 0
sum (x:xs) = x + sum xs

-- guards
sum xs
    | null xs = 0
    | otherwise = (head xs) + sum (tail xs)

-- case
sum xs = case xs of
    [] -> 0
    _ -> (head xs) + sum (tail xs)

-- if statement
sum xs = if null xs then 0 else (head xs) + sum (tail xs)

2 番目の質問として、これらのオプションのどれが「ベスト プラクティス」と見なされ、その理由は? 私の教授は、可能な限り常に最初の方法を使用していましたが、それが彼の個人的な好みなのか、それとも背後に何かがあるのか​​ 疑問に思っています。

4

6 に答える 6

47

あなたの例の問題は式ではなく、andifのような部分関数の使用です。これらのいずれかを空のリストで呼び出そうとすると、例外がスローされます。headtail

> head []
*** Exception: Prelude.head: empty list
> tail []
*** Exception: Prelude.tail: empty list

これらの関数を使用してコードを記述する際にミスを犯した場合、エラーは実行時まで検出されません。パターン マッチングを間違えると、プログラムはコンパイルされません。

たとえば、関数の と の部分を誤って切り替えたthenとします。else

-- Compiles, throws error at run time.
sum xs = if null xs then (head xs) + sum (tail xs) else 0

-- Doesn't compile. Also stands out more visually.
sum [] = x + sum xs
sum (x:xs) = 0

ガードを使用した例にも同じ問題があることに注意してください。

于 2013-05-15T18:36:06.187 に答える
20

Boolean Blindnessの記事は、この質問に非常によく答えていると思います。問題は、ブール値を構築するとすぐに、その意味的な意味がすべて失われることです。これにより、バグの大きな原因となり、コードが理解しにくくなります。

于 2013-05-15T19:16:01.203 に答える
16

教授が好む最初のバージョンには、他のバージョンと比較して次のような利点があります。

  1. の言及なしnull
  2. リスト コンポーネントはパターンで名前が付けられているため、headおよびについては言及されていませんtail

これは「ベストプラクティス」と見なされていると思います。

大したことは何ですか?head特にとを避けたいのはなぜtailですか? まあ、これらの機能が完全ではないことは誰もが知っているので、自動的にすべてのケースがカバーされていることを確認しようとします. [] でのパターン マッチは よりも目立つだけでなくnull xs、一連のパターン マッチが完全かどうかコンパイラによってチェックされます。したがって、完全なパターン マッチを使用する慣用的なバージョンは、(訓練された Haskell リーダーにとって) 把握しやすく、コンパイラーによって徹底的に証明されます。

2 番目のバージョンは、すべてのケースが処理されていることを一度に確認できるため、最後のバージョンよりもわずかに優れています。それでも、一般的なケースでは、2 番目の方程式の RHS はより長くなる可能性があり、いくつかの定義を持つ where 句が存在する可能性があり、それらの最後のものは次のようになります。

where
   ... many definitions here ...
   head xs = ... alternative redefnition of head ...

RHS の機能を確実に理解するには、共通名が再定義されていないことを確認する必要があります。

3 番目のバージョンは最悪のバージョンです: a) 2 番目の一致はリストの分解に失敗し、まだ頭と尾を使用しています。b)ケースは、2 つの方程式を使用した同等の表記法よりも少し冗長です。

于 2013-05-15T18:48:56.300 に答える
8

多くのプログラミング言語では、if ステートメントは基本的なプリミティブであり、switch ブロックのようなものは、深くネストされた if ステートメントを簡単に記述できるようにするための単なる構文糖衣です。

Haskell はその逆を行います。パターン マッチングは基本的なプリミティブであり、if 式は文字通り単なるパターン マッチングの構文糖衣です。同様に、nullやのような構成head要素は単なるユーザー定義関数であり、最終的にはすべてパターン マッチングを使用して実装されます。したがって、パターン マッチングはすべての根底にあるものです。(したがって、ユーザー定義関数を呼び出すよりも効率的である可能性があります。)

上記のような多くの場合、それは単にスタイルの問題です。コンパイラーはほぼ確実に、すべてのバージョンのパフォーマンスがほぼ同等になるように最適化できます。しかし、一般的には[常にではありません!] パターン マッチングにより、何を達成しようとしているのかが明確になります。

(2 つの選択肢を間違った方法で取得する if 式を書くのは面倒なほど簡単です。めったにない間違いだと思うかもしれませんが、驚くほど一般的です。パターン マッチでは、その特定の間違いを犯す可能性はほとんどありません。 、まだ台無しにすることがたくさんありますが。)

于 2013-05-15T19:54:07.983 に答える
6

nullheadおよびへの各呼び出しにtailは、パターン マッチが伴います。ただし、回答の最初のバージョンはパターン マッチを 1 つだけ実行し、パターンの名前付きコンポーネントを介してその結果を再利用します。

そのためだけに、その方が良いです。しかし、視覚的にもわかりやすく、読みやすくなっています。

于 2013-05-15T18:58:53.790 に答える
5

パターン マッチングは、(少なくとも) 次の理由から、一連の if-then-else ステートメントよりも優れています。

  1. それはより宣言的です
  2. 合計型とうまく相互作用します

パターン マッチングは、コード内の「偶発的な複雑さ」の量を減らすのに役立ちます。つまり、プログラムの本質的なロジックではなく、実装の詳細に関するコードです。

他のほとんどの言語では、コンパイラ/ランタイムが if-then-else ステートメントの文字列を検出すると、プログラマが指定した順序で条件をテストする以外に選択肢はありません。しかし、パターン マッチングにより、プログラマーは、何が起こるべきかを説明することよりも、物事がどのように実行されるべきかを説明することに集中するようになります。Haskell の値の純度と不変性により、コンパイラはパターンのコレクションを全体として考慮し、それらを実装する最善の方法を決定できます。

類推は、C のswitchステートメントです。さまざまな switch ステートメントのアセンブリ コードをダンプすると、コンパイラが比較のチェーン/ツリーを生成する場合と、ジャンプ テーブルを生成する場合があることがわかります。プログラマーはどちらの場合も同じ構文を使用します。コンパイラーは比較値に基づいて実装を選択します。それらが値の連続したブロックを形成する場合、ジャンプ テーブル メソッドが使用されます。それ以外の場合は、比較ツリーが使用されます。そして、この関心の分離により、比較値の中に他のパターンが検出された場合、コンパイラーは将来さらに多くの戦略を実装できます。

于 2013-05-15T19:38:47.553 に答える