web-dev-qa-db-ja.com

Haskellの `head`が空のリストでクラッシュするのはなぜですか(または空のリストが返されないのはなぜですか*)。 (言語哲学)

他の潜在的な貢献者への注記:抽象的または数学的表記法を使用してあなたの主張をすることをためらわないでください。不明な点がございましたら、ご説明をさせていただきますが、それ以外の場合は、快適な表現でお気軽にどうぞ。

明確にするために、私は「安全」なheadではなく探していません。また、headの選択は特に例外的に意味がありません。質問の要点は、コンテキストを提供するheadおよびhead'の説明に続きます。

Haskellを使って数か月間ハッキングを続けてきましたが(それが私の主要言語になった時点で)、確かに、より高度な概念や言語の哲学の詳細については詳しくありません(ただし、私は学びたい以上のものです)。私の質問は哲学の1つであるため、それほど技術的な質問ではありません(実際のところ、私が理解していない限り)。

この例では、headについて話しています。

ご想像のとおり、

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

これはhead :: [a] -> aから続きます。けっこうだ。明らかに、(手を振って)タイプのない要素を返すことはできません。しかし、同時に、定義するのは簡単ではありません

head' :: [a] -> Maybe a
head' []     = Nothing
head' (x:xs) = Just x

特定のステートメントのコメントセクションで、この here について少し議論したことがあります。特に、一人のアレックス・スタングルは言います

「すべてを「安全」にしないこと、および前提条件に違反したときに例外をスローすることには十分な理由があります。」

私は必ずしもこの主張を疑う必要はありませんが、これらの「正当な理由」が何であるかについて知りたいです。

さらに、ポール・ジョンソンは言います、

'たとえば、「safeHead :: [a]->多分a」を定義できますが、空のリストを処理したり、それが発生しないことを証明したりする代わりに、「なし」を処理するか、発生しないことを証明する必要があります」

そのコメントから私が読んだ口調は、これが難易度/複雑さ/何かの顕著な増加であることを示唆していますが、私が彼が何を出しているのかを私は理解していません。

Steven Pruzina氏は次のように述べています(2011年にはそれ以上)、

「たとえば、「ヘッド」がクラッシュ防止できない理由はより深いものがあります。ポリモーフィックでありながら空のリストを処理するには、「ヘッド」は常に特定の空のリストにないタイプの変数を返す必要があります。ハスケルがそれをすることができればデルフィック...」。

空のリスト処理を許可することにより、ポリモーフィズムは失われますか?もしそうなら、どのように、そしてなぜですか?これを明らかにする特定のケースはありますか? このセクションは、@ Russell O'Connorによって十分に回答されています。もちろん、これ以上の考えは高く評価されます。

これは、明確さと提案に応じて編集します。あなたが提供することができるどんな考え、書類などでも、最も高く評価されます。

73
Jack Henahan

空のリスト処理を許可することにより、ポリモーフィズムは失われますか?もしそうなら、どのように、そしてなぜですか?これを明らかにする特定のケースはありますか?

headの自由定理は、

f . head = head . $map f

この定理を[]に適用すると、

f (head []) = head (map f []) = head []

この定理はfごとに成立する必要があるため、特にconst Trueおよびconst Falseについて成立する必要があります。これは意味します

True = const True (head []) = head [] = const False (head []) = False

したがって、headが適切に多態性であり、head []が合計値である場合、TrueFalseと等しくなります。

PS。あなたのリストが空ではないという前提条件がある場合、リストを使用する代わりに関数のシグネチャで空でないリストタイプを使用してそれを強制するべきであるという影響に対するあなたの質問の背景に関する他のコメントがあります。

96

なぜ誰かがパターンマッチングの代わりにhead :: [a] -> aを使用するのですか?理由の1つは、引数を空にすることはできず、引数が空の場合を処理するコードを記述したくないためです。

もちろん、タイプhead'[a] -> Maybe aは、標準ライブラリで Data.Maybe.listToMaybe として定義されています。ただし、headの使用をlistToMaybeに置き換える場合は、空のケースを処理するコードを記述する必要があります。これにより、headを使用するこの目的が無効になります。

headを使用するのが良いスタイルだと言っているのではありません。例外が発生する可能性があるという事実を隠し、この意味では良くありません。しかし、それは時々便利です。重要なのは、headlistToMaybeでは提供できないいくつかの目的を果たすということです。

質問の最後の引用(ポリモーフィズムについて)は、空のリストに値を返す[a] -> a型の関数を定義することが不可能であることを単に意味します(Russell O'Connorが 彼の回答)で説明したように )。

23
Tsuyoshi Ito

これについては、さまざまな方法で考えることができます。ですから、head'について賛成と反対の両方について議論します。

head'に対して:

head'を使用する必要はありません。リストは具体的なデータ型であるため、head'で実行できることはすべてパターンマッチングで実行できます。

さらに、head'を使用すると、あるファンクタを別のファンクタとトレードオフすることになります。ある時点で、真ちゅうのタックに取り掛かり、基礎となるリスト要素でいくつかの作業を実行したいとします。

head'を弁護して:

しかし、パターンマッチングは何が起こっているかを覆い隠します。 Haskellでは、関数の計算に関心があります。関数の計算は、コンポジションとコンビネーターを使用してポイントフリースタイルで関数を記述することでより効率的に実行できます。

さらに、[]およびMaybeファンクタについて考えると、head'を使用すると、ファンクタ間を移動できます(特に、[]Applicativeインスタンス)。 pure = replicate付き。)

4
Lambdageek

ユースケースで空のリストがまったく意味をなさない場合は、代わりに常に NonEmpty を使用することを選択できます。ここで、neHeadは安全に使用できます。その角度から見ると、安全でないのはhead関数ではなく、リスト全体のデータ構造です(このユースケースでも)。

2
Landei

これはシンプルさと美しさの問題だと思います。もちろん、それは見る人の目にあります。

LISPの背景から来ている場合、リストはコンスセルで構成されており、各セルにはデータ要素と次のセルへのポインターがあります。空のリストはそれ自体がリストではなく、特別な記号です。そして、Haskellはこの推論を採用しています。

私の見解では、空のリストとリストが2つの異なるものである場合、それはより簡潔で、理由を説明するのが簡単で、より伝統的です。

...追加する可能性があります-頭が安全でないことが心配な場合-使用しないでください、代わりにパターンマッチングを使用してください:

sum     [] = 0
sum (x:xs) = x + sum xs
1
Johan Kotlinski