Hinweis für andere potenzielle Mitwirkende: Bitte zögern Sie nicht, abstrakte oder mathematische Notationen zu verwenden, um Ihren Standpunkt zu verdeutlichen. Wenn ich Ihre Antwort unklar finde, werde ich um Aufklärung bitten, aber ansonsten können Sie sich auf bequeme Weise ausdrücken.
Um es klar auszudrücken: Ich suche weder einen "Tresor" head
, noch ist die Wahl von head
besonders außergewöhnlich aussagekräftig. Das Fleisch der Frage folgt der Diskussion von head
und head'
, die dazu dienen, Kontext zu liefern.
Ich habe mich jetzt seit ein paar Monaten mit Haskell abgehackt (bis zu dem Punkt, dass es meine Hauptsprache geworden ist), aber ich bin zugegebenermaßen nicht gut über einige der fortgeschritteneren Konzepte oder die Details der Sprachphilosophie informiert (obwohl) Ich bin mehr als bereit zu lernen. Meine Frage ist dann weniger eine technische (es sei denn, es ist und ich merke es einfach nicht), sondern eine der Philosophie.
Für dieses Beispiel spreche ich head
.
Wie ich mir vorstellen kann, werden Sie wissen,
Prelude> head []
*** Exception: Prelude.head: empty list
Dies folgt aus head :: [a] -> a
. Meinetwegen. Offensichtlich kann man kein Element von (handgewellt) keinem Typ zurückgeben. Gleichzeitig ist es einfach (wenn nicht trivial) zu definieren
head' :: [a] -> Maybe a
head' [] = Nothing
head' (x:xs) = Just x
Ich habe hier im Kommentarbereich bestimmter Aussagen eine kleine Diskussion darüber gesehen . Bemerkenswert ist ein Alex Stangl
"Es gibt gute Gründe, nicht alles" sicher "zu machen und Ausnahmen zu werfen, wenn die Voraussetzungen verletzt werden."
Ich stelle diese Behauptung nicht unbedingt in Frage, bin aber gespannt, was diese "guten Gründe" sind.
Zusätzlich sagt ein Paul Johnson:
'Zum Beispiel könnten Sie "safeHead :: [a] -> Vielleicht a" definieren, aber anstatt entweder eine leere Liste zu behandeln oder zu beweisen, dass dies nicht möglich ist, müssen Sie "nichts" behandeln oder beweisen, dass dies nicht möglich ist . '
Der Ton, den ich aus diesem Kommentar lese, deutet darauf hin, dass dies eine bemerkenswerte Zunahme von Schwierigkeit / Komplexität / etwas ist, aber ich bin nicht sicher, ob ich verstehe, was er da rausbringt.
Ein Steven Pruzina sagt (2011 nicht weniger),
"Es gibt einen tieferen Grund, warum z. B. 'head' nicht absturzsicher sein kann. Um polymorph zu sein und dennoch eine leere Liste zu verarbeiten, muss 'head' immer eine Variable des Typs zurückgeben, die in einer bestimmten leeren Liste nicht vorhanden ist Delphic, wenn Haskell das könnte ... ".
Geht der Polymorphismus verloren, wenn die Behandlung leerer Listen zugelassen wird? Wenn ja, wie und warum? Gibt es bestimmte Fälle, die dies offensichtlich machen würden? Dieser Abschnitt wurde von @Russell O'Connor ausführlich beantwortet. Alle weiteren Gedanken sind natürlich willkommen.
Ich werde dies bearbeiten, wie es die Klarheit und der Vorschlag vorschreiben. Alle Gedanken, Papiere usw., die Sie zur Verfügung stellen können, werden am meisten geschätzt.
quelle
Antworten:
Der freie Satz für
head
besagt, dassf . head = head . $map f
Die Anwendung dieses Theorems auf
[]
impliziert diesf (head []) = head (map f []) = head []
Dieser Satz muss für jeden gelten
f
, also muss er insbesondere fürconst True
und geltenconst False
. Dies impliziertTrue = const True (head []) = head [] = const False (head []) = False
Wenn
head
also richtig polymorph ist undhead []
ein GesamtwertTrue
wäre , dann wäre gleichFalse
.PS. Ich habe einige andere Kommentare zum Hintergrund Ihrer Frage, die besagen, dass Sie, wenn Sie die Voraussetzung haben, dass Ihre Liste nicht leer ist, diese erzwingen sollten, indem Sie einen nicht leeren Listentyp in Ihrer Funktionssignatur verwenden, anstatt eine Liste zu verwenden.
quelle
head :: [a] -> Maybe a
, die einen freien Satz habenfmap f . head = head . map f
und somit einfach bekommenNothing == Nothing
. Es sei denn, Sie versuchen nur zu zeigen, warum es unmöglich ist, einendefault :: forall a . a
Wert zu haben , für den zurückgegeben werden kannhead []
und für den wir Musterübereinstimmungen durchführen können ... aber es gibt einfachere Möglichkeiten, darüber zu sprechen.Warum verwendet jemand
head :: [a] -> a
anstelle des Mustervergleichs? Einer der Gründe ist, dass Sie wissen, dass das Argument nicht leer sein kann und den Code nicht schreiben möchten, um den Fall zu behandeln, in dem das Argument leer ist.Natürlich ist Ihr
head'
Typ[a] -> Maybe a
in der Standardbibliothek als definiertData.Maybe.listToMaybe
. Wenn Sie jedoch eine Verwendung vonhead
durch ersetzenlistToMaybe
, müssen Sie den Code schreiben, um den leeren Fall zu behandeln, wodurch dieser Verwendungszweck zunichte gemacht wirdhead
.Ich sage nicht, dass das Verwenden
head
ein guter Stil ist. Es verbirgt die Tatsache, dass es zu einer Ausnahme führen kann, und in diesem Sinne ist es nicht gut. Aber es ist manchmal bequem. Der Punkt ist, dasshead
einige Zwecke erfüllt werden, denen nicht gedient werden kannlistToMaybe
.Das letzte Zitat in der Frage (über Polymorphismus) bedeutet einfach, dass es unmöglich ist, eine Funktion vom Typ zu definieren,
[a] -> a
die einen Wert in der leeren Liste zurückgibt (wie Russell O'Connor in seiner Antwort erklärte ).quelle
Es ist nur natürlich zu erwarten, dass Folgendes zutrifft:
xs === head xs : tail xs
- Eine Liste ist identisch mit ihrem ersten Element, gefolgt vom Rest. Scheint logisch, oder?Zählen wir nun die Anzahl der Nachteile (Anwendungen von
:
), wobei die tatsächlichen Elemente außer Acht gelassen werden, wenn das angebliche 'Gesetz' angewendet wird auf[]
:[]
sollte identisch sein mitfoo : bar
, aber das erstere hat 0 Nachteile, während das letztere (mindestens) einen hat. Oh, hier stimmt etwas nicht!Das Typensystem von Haskell ist trotz all seiner Stärken nicht in der Lage, die Tatsache auszudrücken, dass Sie nur
head
eine nicht leere Liste aufrufen sollten (und dass das 'Gesetz' nur für nicht leere Listen gilt). Durchhead
die Verwendung wird die Beweislast auf den Programmierer verlagert, der sicherstellen sollte, dass sie nicht für leere Listen verwendet wird. Ich glaube, dass abhängig getippte Sprachen wie Agda hier helfen können.Zum Schluss noch eine etwas operativ-philosophischere Beschreibung: Wie soll
head ([] :: [a]) :: a
umgesetzt werden? Esa
ist unmöglich, einen Wert des Typs aus der Luft zu zaubern (denken Sie an unbewohnte Typen wiedata Falsum
), und es würde bedeuten, irgendetwas zu beweisen (über den Curry-Howard-Isomorphismus).quelle
Es gibt verschiedene Möglichkeiten, darüber nachzudenken. Also werde ich sowohl dafür als auch dagegen argumentieren
head'
:Gegen
head'
:Es ist nicht erforderlich
head'
: Da Listen ein konkreter Datentyp sind, könnenhead'
Sie alles, was Sie damit tun können, durch Mustervergleich tun.Außerdem
head'
tauschen Sie nur einen Funktor gegen einen anderen aus. Irgendwann möchten Sie sich den Messingnägeln zuwenden und das zugrunde liegende Listenelement bearbeiten.Zur Verteidigung von
head'
:Aber Pattern Matching verdeckt, was los ist. In Haskell sind wir an der Berechnung von Funktionen interessiert, was besser erreicht werden kann, indem sie mit Kompositionen und Kombinatoren punktfrei geschrieben werden.
Wenn Sie an die
[]
undMaybe
Funktoren denken ,head'
können Sie sich außerdem zwischen ihnen hin und her bewegen (insbesondere dieApplicative
Instanz von[]
mitpure = replicate
.)quelle
head
, bleibt es jedoch bestehen. Insgesamt gefällt mir Ihre Antwort sehr gut, und ich würde mich für weitere Gedanken interessieren, die Sie zu den von Ihnen gemachten Punkten haben.head
und andere, in denen ich mir gewünscht habe (oder die ich nur definiere)head'
.head'
, müssen Sie nicht weiter suchen alsData.Maybe.listToMaybe
.head'' :: MonadPlus m => [a] -> m a
wäre noch nützlicher.Wenn in Ihrem Anwendungsfall eine leere Liste überhaupt keinen Sinn ergibt , können Sie sich jederzeit für die Verwendung entscheiden
NonEmpty
, wennneHead
die Verwendung sicher ist. Wenn Sie es aus diesem Blickwinkel sehen, ist nicht diehead
Funktion unsicher, sondern die gesamte Listendatenstruktur (auch für diesen Anwendungsfall).quelle
Ich denke, das ist eine Frage der Einfachheit und Schönheit. Das liegt natürlich im Auge des Betrachters.
Wenn Sie aus einem Lisp-Hintergrund stammen, wissen Sie möglicherweise, dass Listen aus Nachteile bestehen, wobei jede Zelle ein Datenelement und einen Zeiger auf die nächste Zelle hat. Die leere Liste ist keine Liste an sich, sondern ein spezielles Symbol. Und Haskell geht mit dieser Argumentation.
Meiner Ansicht nach ist es sowohl sauberer, einfacher zu überlegen als auch traditioneller, wenn leere Liste und Liste zwei verschiedene Dinge sind.
... Ich kann hinzufügen - wenn Sie sich Sorgen machen, dass der Kopf unsicher ist - verwenden Sie ihn nicht, sondern verwenden Sie stattdessen den Mustervergleich:
sum [] = 0 sum (x:xs) = x + sum xs
quelle
head
. Dies ist eher eine Kuriosität als ein Anliegen. Zum Rest Ihres Kommentars: In welchem Sinne traditionell? Wessen Tradition? Warum ist es die Tradition?[]
entspricht dem leeren Satz. Im mathematischen Kontext ist die leere Menge einfach die Menge, in der sich nichts befindet. Ebenso[]
ist immer noch eine Liste und sollte in der Tat die Liste ohne Elemente (und möglicherweise ohne Typ) sein. Wenn dies (zum Beispiel) eine Einschränkung des typisierten Lambda-Kalküls ist, könnte dies helfen, die Behandlung von zu erklären[]
, aber ich bin nicht sicher, ob dies der Fall ist.