Was genau macht das Haskell-Typ-System so verehrt (vs sagen, Java)?

204

Ich fange an, Haskell zu lernen . Ich bin sehr neu darin und lese gerade ein paar Online-Bücher durch, um mir ein Bild von den grundlegenden Konstrukten zu machen.

Eines der 'Meme', von denen die Leute, die damit vertraut sind, oft gesprochen haben, ist das Ganze "Wenn es kompiliert, wird es funktionieren *" - was meiner Meinung nach mit der Stärke des Typensystems zusammenhängt.

Ich versuche zu verstehen, warum genau Haskell in dieser Hinsicht besser ist als andere statisch typisierte Sprachen.

Anders ausgedrückt, ich gehe davon aus, dass Sie in Java etwas Abscheuliches tun können, um etwas ArrayList<String>()zu enthalten, das wirklich sein sollte ArrayList<Animal>(). Das Abscheuliche dabei ist, dass Ihre stringInhalte elephant, giraffeusw. vorhanden sind und wenn jemand sie einfügt, Mercedeswird Ihnen Ihr Compiler nicht weiterhelfen.

Wenn ich esArrayList<Animal>() dann zu einem späteren Zeitpunkt getan habe und mein Programm sich nicht wirklich um Tiere, sondern um Fahrzeuge dreht, kann ich beispielsweise eine Funktion ändern, die produziert ArrayList<Animal>, um zu produzieren, ArrayList<Vehicle>und meine IDE sollte es mir überall mitteilen ist eine Zusammenstellungspause.

Ich gehe davon aus, dass dies mit einem starken Typensystem gemeint ist, aber es ist mir nicht klar, warum Haskells besser ist. Anders ausgedrückt, Sie können gutes oder schlechtes Java schreiben. Ich nehme an, Sie können dasselbe in Haskell tun (dh Dinge in Strings / Ints packen, die wirklich erstklassige Datentypen sein sollten).

Ich vermute, ich vermisse etwas Wichtiges / Grundlegendes.
Ich würde mich sehr freuen, wenn mir der Irrtum gezeigt würde!

Phatmanace
quelle
31
Ich lasse die Leute besser Bescheid wissen, als ich echte Antworten schreibe, aber das Wesentliche dabei ist: Statisch getippte Sprachen wie C # haben ein Typensystem, das Ihnen hilft, verteidigbaren Code zu schreiben . Typsysteme wie Haskells Versuch, Ihnen beim Schreiben von korrektem (dh nachweisbarem) Code zu helfen . Das Grundprinzip bei der Arbeit ist das Verschieben von Dingen, die in die Kompilierungsphase eingecheckt werden können; Haskell überprüft beim Kompilieren weitere Dinge.
Robert Harvey
8
Ich weiß nicht viel über Haskell, aber ich kann über Java sprechen. Obwohl es stark typisiert zu sein scheint , können Sie dennoch "abscheuliche" Dinge tun, wie Sie sagten. Für fast jede Garantie, die Java in Bezug auf sein Typsystem gibt, gibt es einen Ausweg.
12
Ich weiß nicht, warum alle Antworten Maybeerst gegen Ende erwähnen . Wenn ich nur eine Sache auswählen müsste, die populärere Sprachen von Haskell leihen sollten, wäre dies alles. Es ist eine sehr einfache Idee (aus theoretischer Sicht also nicht sehr interessant), aber dies allein würde unsere Arbeit so viel einfacher machen.
Paul
1
Hier wird es gute Antworten geben, aber in dem Versuch, diese zu unterstützen, sollten Sie die Typensignaturen studieren. Sie ermöglichen Menschen und Programme zur Vernunft über die Programme in einer Weise, wird zeigen , wie Java in der schwammigen Mitte re: Typen.
Michael Ostern
6
Der Fairness halber muss ich darauf hinweisen, dass "das Ganze, wenn es kompiliert wird, wird es funktionieren" ein Slogan ist, keine wörtliche Aussage über die Tatsachen. Ja, wir Haskell-Programmierer wissen, dass das Bestehen der Typprüfung bei einigen eingeschränkten Vorstellungen von Korrektheit eine gute Chance auf Korrektheit bietet, aber es ist sicherlich keine wörtliche und universelle "wahre" Aussage!
Tom Ellis

Antworten:

230

Hier ist eine ungeordnete Liste von Typsystemfunktionen, die in Haskell verfügbar und in Java entweder nicht verfügbar oder weniger gut (meines Wissens nach zugegebenermaßen schwach für Java) sind.

  • Sicherheit . Haskells Typen haben ziemlich gute "Typensicherheit" -Eigenschaften. Dies ist ziemlich spezifisch, aber es bedeutet im Wesentlichen, dass Werte bei einem Typ nicht willentlich in einen anderen Typ umgewandelt werden können. Dies ist manchmal im Widerspruch zu Veränderlichkeit (OCaml sehen , Werteinschränkung )
  • Algebraische Datentypen . Die Typen in Haskell haben im Wesentlichen die gleiche Struktur wie die Mathematik der High School. Dies ist unglaublich einfach und konsequent, aber wie sich herausstellt, so mächtig, wie Sie es sich nur wünschen können. Es ist einfach eine gute Grundlage für ein Typensystem.
    • Datentyp-generische Programmierung . Dies ist nicht dasselbe wie bei generischen Typen (siehe Verallgemeinerung ). Stattdessen ist es aufgrund der Einfachheit der zuvor erwähnten Typstruktur relativ einfach, Code zu schreiben, der generisch über diese Struktur arbeitet. Später Eqspreche ich darüber, wie so etwas wie Qualität von einem Haskell-Compiler für einen benutzerdefinierten Typ automatisch abgeleitet werden kann. Im Wesentlichen geht man dabei über die gemeinsame, einfache Struktur, die jedem benutzerdefinierten Typ zugrunde liegt, und ordnet sie den Werten zu - eine sehr natürliche Form der strukturellen Gleichheit.
  • Gegenseitig rekursive Typen . Dies ist nur ein wesentlicher Bestandteil des Schreibens nicht-trivialer Typen.
    • Verschachtelte Typen . Auf diese Weise können Sie rekursive Typen über Variablen definieren, die bei verschiedenen Typen rekursiv sind. Eine Art ausgewogener Bäume ist zum Beispiel data Bt a = Here a | There (Bt (a, a)). Überlegen Sie genau, welche Werte gültig sind, Bt aund beachten Sie, wie dieser Typ funktioniert. Es ist schwierig!
  • Verallgemeinerung . Das ist fast zu dumm, um es nicht in einem Typensystem zu haben (ähm, dich anzusehen, los). Es ist wichtig, Begriffe von Typvariablen zu haben und über Code sprechen zu können, der von der Wahl dieser Variablen unabhängig ist. Hindley Milner ist ein Typensystem, das vom System F abgeleitet ist. Haskells Typensystem ist eine Ausarbeitung der HM-Typisierung, und System F ist im Wesentlichen der Herd der Verallgemeinerung. Was ich damit sagen will ist, dass Haskell eine sehr gute Verallgemeinerungsgeschichte hat.
  • Abstrakte Arten . Haskells Geschichte hier ist nicht großartig, aber auch nicht nicht existent. Es ist möglich, Typen zu schreiben, die eine öffentliche Schnittstelle, aber eine private Implementierung haben. Dies ermöglicht es uns, Änderungen am Implementierungscode zu einem späteren Zeitpunkt zuzulassen und, was wichtig ist, da dies die Grundlage aller Operationen in Haskell ist, "magische" Typen zu schreiben, die gut definierte Schnittstellen haben, wie z IO. Java hat, um ehrlich zu sein, wahrscheinlich tatsächlich eine schönere abstrakte Geschichte, aber ich denke nicht, bis Interfaces populärer wurden, war das wirklich wahr.
  • Parametrizität . Haskell-Werte haben keine universellen Operationen. Java verstößt dagegen mit Dingen wie Referenzgleichheit und Hashing und noch offensichtlicher mit Nötigungen. Dies bedeutet, dass Sie freie Theoreme über Typen erhalten, mit denen Sie die Bedeutung einer Operation oder eines Werts in einem bemerkenswerten Ausmaß vollständig anhand ihres Typs erkennen können. Bei bestimmten Typen kann es nur eine sehr kleine Anzahl von Einwohnern geben.
  • Höherwertige Typen zeigen den gesamten Typ beim Codieren schwierigerer Dinge. Functor / Applicative / Monad, Foldable / Traversable, das gesamte mtlEffektschreibsystem, verallgemeinerte Functor-Fixpunkte. Die Liste geht weiter und weiter. Es gibt eine Menge Dinge, die sich am besten bei höheren Arten ausdrücken lassen, und relativ wenige Typsysteme ermöglichen es dem Benutzer, über diese Dinge zu sprechen.
  • Typ Klassen . Wenn Sie sich Typsysteme als Logik vorstellen - was nützlich ist -, werden Sie oft aufgefordert, die Dinge zu beweisen. In vielen Fällen handelt es sich im Wesentlichen um Leitungsrauschen: Es gibt möglicherweise nur eine richtige Antwort, und es ist eine Verschwendung von Zeit und Mühe für den Programmierer, dies anzugeben. Typenklassen sind für Haskell eine Möglichkeit, die Proofs für Sie zu generieren. Konkreter ausgedrückt, können Sie so einfache "Typgleichungssysteme" lösen, wie "Bei welchem ​​Typ wollen wir die (+)Dinge gemeinsam angehen ? Oh Integer, ok! Lassen Sie uns jetzt den richtigen Code einbetten!". Bei komplexeren Systemen können Sie interessantere Einschränkungen festlegen.
    • Beschränkungsrechnung . Einschränkungen in Haskell, die den Mechanismus zum Eingreifen in das Typenklassen-Prologsystem darstellen, sind strukturell typisiert. Dies ergibt eine sehr einfache Form der Subtyping-Beziehung, mit der Sie komplexe Bedingungen aus einfacheren zusammensetzen können. Die gesamte mtlBibliothek basiert auf dieser Idee.
    • Ableiten . Um die Kanonizität des Typenklassensystems voranzutreiben, ist es notwendig, eine Menge häufig trivialen Codes zu schreiben, um die Einschränkungen zu beschreiben, die benutzerdefinierte Typen instanziieren müssen. Bei der sehr normalen Struktur von Haskell-Typen ist es häufig möglich, den Compiler aufzufordern, dieses Boilerplate für Sie zu erstellen.
    • Typenklassen-Prolog . Der Haskell-Typklassenlöser - das System, das die oben erwähnten "Beweise" generiert - ist im Wesentlichen eine verkrüppelte Form von Prolog mit besseren semantischen Eigenschaften. Dies bedeutet, dass Sie wirklich haarige Dinge in einem Typ-Prolog codieren können und erwarten, dass sie alle zur Kompilierungszeit verarbeitet werden. Ein gutes Beispiel könnte sein, nach einem Beweis zu suchen, dass zwei heterogene Listen äquivalent sind, wenn Sie die Reihenfolge vergessen - es handelt sich um äquivalente heterogene "Mengen".
    • Typklassen mit mehreren Parametern und funktionale Abhängigkeiten . Dies sind nur äußerst nützliche Verfeinerungen des Basistypenklassen-Prologs. Wenn Sie Prolog kennen, können Sie sich vorstellen, wie sehr sich die Ausdruckskraft erhöht, wenn Sie Prädikate von mehr als einer Variablen schreiben können.
  • Ziemlich gute Schlussfolgerung . Sprachen, die auf Systemen vom Typ Hindley Milner basieren, haben ziemlich gute Schlussfolgerungen. HM selbst hat eine vollständige Folgerung, was bedeutet, dass Sie niemals eine Typvariable schreiben müssen. Haskell 98, die einfachste Form von Haskell, wirft das bereits in einigen sehr seltenen Fällen aus. Im Allgemeinen war das moderne Haskell ein Experiment, bei dem es darum ging, den Raum der vollständigen Inferenz langsam zu verkleinern, HM mehr Leistung zu verleihen und zu sehen, wenn sich Benutzer beschweren. Die Leute beschweren sich sehr selten - Haskells Schlussfolgerung ist ziemlich gut.
  • Sehr, sehr, sehr schwache Subtypisierung . Ich habe bereits erwähnt, dass das Constraint-System aus dem Typeclass-Prolog den Begriff der strukturellen Untertypisierung hat. Dies ist die einzige Form der Subtypisierung in Haskell . Subtypisierung ist schrecklich für die Argumentation und Folgerung. Dies erschwert jedes dieser Probleme erheblich (ein System von Ungleichheiten anstelle eines Gleichheitssystems). Es ist auch sehr leicht zu missverstehen (Ist Subklassifizierung dasselbe wie Subtypisierung? Natürlich nicht! Aber die Leute verwechseln das sehr oft und viele Sprachen tragen zu dieser Verwirrung bei! Wie sind wir hier gelandet? Ich nehme an, dass niemand jemals den LSP untersucht hat.)
    • Beachten Sie vor kurzem (Anfang 2017) Steven Dolan veröffentlichte seine These auf MLsub , eine Variante von ML und Hindley-Milner Typinferenz , die eine hat sehr schöne Subtyping Geschichte ( siehe auch ). Dies vermeidet nicht das, was ich oben geschrieben habe - die meisten Subtyping-Systeme sind kaputt und haben schlechte Schlussfolgerungen -, legt jedoch nahe, dass wir erst heute einige vielversprechende Möglichkeiten gefunden haben, um vollständige Schlussfolgerungen zu ziehen und Subtyping gut zusammenspielen zu können. Um ganz klar zu sein, sind Javas Konzepte der Untertypisierung in keiner Weise in der Lage, Dolans Algorithmen und Systeme zu nutzen. Es erfordert ein Umdenken darüber, was Subtypisierung bedeutet.
  • Höherrangige Typen . Ich sprach über Verallgemeinerung früher, aber mehr als nur bloße Verallgemeinerung ist es nützlich zu Typen in der Lage sein zu sprechen , welche Variablen verallgemeinert haben in ihnen . Beispielsweise hat eine Abbildung zwischen Strukturen höherer Ordnung, die nicht berücksichtigt (siehe Parametrizität ), was diese Strukturen "enthalten", einen Typ wie (forall a. f a -> g a). In gerade HM können Sie eine Funktion in dieser Art schreiben, aber mit höherrangigen Typen , die Sie fordern eine solche Funktion als Argument wie folgt: mapFree :: (forall a . f a -> g a) -> Free f -> Free g. Beachten Sie, dass die aVariable nur innerhalb des Arguments gebunden ist. Dies bedeutet, dass der Definierer der Funktion mapFreeentscheiden kann, woran sie ainstanziiert wird, wenn sie sie verwendet, und nicht der Benutzer von mapFree.
  • Existenzielle Typen . Während höherrangigen Typen ermöglichen es uns , über universelle Quantifizierung, existentielle Typen sprechen lassen Sie uns über existenzielle Quantifizierung sprechen: die Idee , dass es nur existiert eine unbekannte Art einige Gleichungen genügen. Dies ist letztendlich nützlich und würde eine lange Zeit in Anspruch nehmen, um länger darüber nachzudenken.
  • Geben Sie Familien ein . Manchmal sind die Typklassenmechanismen unpraktisch, da wir nicht immer in Prolog denken. Typfamilien lassen uns gerade funktionale Beziehungen zwischen Typen schreiben .
    • Geschlossene Familien . Typfamilien sind standardmäßig offen, was ärgerlich ist, da Sie sie zwar jederzeit erweitern können, sie aber nicht mit der Hoffnung auf Erfolg "umkehren" können. Dies liegt daran , dass man nicht beweisen kann injectiveness , aber mit geschlossenen Typ Familien möglich.
  • Art indizierte Typen und Typenwerbung . Ich werde zu diesem Zeitpunkt sehr exotisch, aber diese haben von Zeit zu Zeit praktischen Nutzen. Wenn Sie eine Art von Griffen schreiben möchten, die entweder offen oder geschlossen sind, können Sie dies sehr gut tun. Beachten Sie im folgenden Snippet, dass Statees sich um einen sehr einfachen algebraischen Typ handelt, dessen Werte auch auf die Typebene hochgestuft wurden. Dann später, können wir darüber reden , Typkonstruktoren wie Handledie Einnahme Argumente zu bestimmten Arten wie State. Es ist verwirrend, alle Details zu verstehen, aber auch so richtig.

    data State = Open | Closed
    
    data Handle :: State -> * -> * where
      OpenHandle :: {- something -} -> Handle Open a
      ClosedHandle :: {- something -} -> Handle Closed a
    
  • Laufzeit-Typ-Darstellungen, die funktionieren . Java ist dafür berüchtigt, dass es Schreibmaschinenlöschungen und Regen auf den Paraden einiger Leute gibt. Das Löschen von Eingaben ist jedoch der richtige Weg, da Sie, wenn Sie eine Funktion haben, getRepr :: a -> TypeReprzumindest gegen die Parametrizität verstoßen. Was noch schlimmer ist: Wenn dies eine vom Benutzer generierte Funktion ist, die zur Laufzeit unsichere Nötigungen auslöst, dann besteht ein massives Sicherheitsrisiko . Das TypeableSystem von Haskell ermöglicht die Erstellung eines Safes coerce :: (Typeable a, Typeable b) => a -> Maybe b. Dieses System basiert auf Typeableder Implementierung im Compiler (und nicht im Userland) und könnte auch ohne den Typklassenmechanismus von Haskell und die Gesetze, denen es garantiert folgt, keine so schöne Semantik erhalten.

Darüber hinaus hängt der Wert von Haskells Typensystem auch davon ab, wie die Typen die Sprache beschreiben. Hier sind einige Merkmale von Haskell, die den Wert durch das Typensystem treiben.

  • Reinheit . Haskell erlaubt keine Nebenwirkungen für eine sehr, sehr, sehr weite Definition von "Nebenwirkungen". Dies zwingt Sie dazu, mehr Informationen in Typen einzugeben, da Typen Ein- und Ausgänge steuern und ohne Nebeneffekte alles in den Ein- und Ausgängen berücksichtigt werden muss.
    • IO . In der Folge brauchte Haskell eine Möglichkeit, über Nebenwirkungen zu sprechen - da jedes echte Programm einige enthalten muss -, und so entstand aus einer Kombination von Typklassen, höherwertigen Typen und abstrakten Typen die Idee, einen bestimmten, besonderen Typ zu verwenden, der als IO aRepräsentant bezeichnet wird Nebenwirkungsberechnungen, die zu Werten vom Typ führen a. Dies ist die Grundlage eines sehr schönen Effektsystems, das in eine reine Sprache eingebettet ist.
  • Mangel annull . Jeder weiß, dass dies nullder milliardenschwere Fehler moderner Programmiersprachen ist. Algebraische Typen, insbesondere die Möglichkeit, einen Status "Existiert nicht" an Typen anzuhängen, die Sie haben, indem Sie einen Typ Ain einen Typ umwandeln Maybe A, mindern das Problem von null.
  • Polymorphe Rekursion . Auf diese Weise können Sie rekursive Funktionen definieren, die Typvariablen verallgemeinern, obwohl sie in jedem rekursiven Aufruf in einer eigenen Verallgemeinerung bei unterschiedlichen Typen verwendet werden. Dies ist schwer zu beschreiben, aber besonders nützlich, wenn es um verschachtelte Typen geht. Schauen Sie zurück auf die Bt aArt aus der Zeit vor und versuchen , eine Funktion zu schreiben , um seine Größe zu berechnen: size :: Bt a -> Int. Es wird ein bisschen wie size (Here a) = 1und aussehen size (There bt) = 2 * size bt. Operativ ist das nicht allzu komplex, aber beachten Sie, dass der rekursive Aufruf sizein der letzten Gleichung bei einem anderen Typ auftritt , die Gesamtdefinition jedoch einen schönen verallgemeinerten Typ hat size :: Bt a -> Int. Beachten Sie, dass dies eine Funktion ist, die die totale Inferenz unterbricht. Wenn Sie jedoch eine Typensignatur bereitstellen, lässt Haskell dies zu.

Ich könnte weitermachen, aber diese Liste sollte Ihnen den Einstieg erleichtern.

J. Abrahamson
quelle
7
Null war kein Milliarden-Dollar-Fehler. Es gibt Fälle , in denen es nicht möglich ist, statisch , dass Zeiger überprüfen nicht dereferenziert werden , bevor eine sinnvolle könnte möglicherweise existieren ; In einem solchen Fall ist es oft besser, eine versuchte Dereferenzierungsfalle zu haben, als zu verlangen, dass der Zeiger anfänglich ein bedeutungsloses Objekt identifiziert. Ich denke , die größte null bedingte Fehler Implementierungen wurde mit der, gegeben char *p = NULL;, wird Falle auf *p=1234, wird aber nicht Falle auf char *q = p+5678;noch*q = 1234;
supercat
37
Das ist nur ein Zitat von Tony Hoare: en.wikipedia.org/wiki/Tony_Hoare#Apologies_and_retractions . Obwohl ich mir sicher bin, dass es Zeiten nullgibt, in denen Zeigerarithmetik notwendig ist, interpretiere ich stattdessen, dass Zeigerarithmetik ein schlechter Ort ist, um die Semantik Ihrer Sprache zu hosten, und nicht, dass Null immer noch ein Fehler ist.
J. Abrahamson
18
@supercat, du kannst in der Tat eine Sprache ohne Null schreiben. Es ist eine Wahl, ob Sie es zulassen oder nicht.
Paul Draper
6
@supercat - Dieses Problem gibt es auch in Haskell, aber in einer anderen Form. Haskell ist normalerweise faul und unveränderlich und ermöglicht es Ihnen daher, p = undefinedso lange zu schreiben , wie pes nicht ausgewertet wird. Zweckmäßigerweise können Sie undefinedeine Art veränderlichen Verweis einfügen, solange Sie ihn nicht bewerten. Die schwerwiegendere Herausforderung besteht in faulen Berechnungen, die möglicherweise nicht beendet werden, was natürlich nicht zu entscheiden ist. Der Hauptunterschied besteht darin, dass dies alles eindeutige Programmierfehler sind und niemals dazu verwendet werden, gewöhnliche Logik auszudrücken.
Christian Conkle
6
Bei @supercat Haskell fehlt die Referenzsemantik vollständig (dies ist der Begriff der referenziellen Transparenz, der zur Folge hat, dass alles erhalten bleibt, indem Referenzen durch ihre Referenzen ersetzt werden). Daher denke ich, dass Ihre Frage schlecht gestellt ist.
J. Abrahamson
78
  • Vollständige Typinferenz. Sie können tatsächlich nutzen komplexe Typen ubiquitär , ohne das Gefühl wie : „Heilige Scheiße, alles , was ich jemals tun ist Typ Schreib Unterschriften.“
  • Typen sind vollständig algebraisch , was es sehr einfach macht, einige komplexe Ideen auszudrücken.
  • Haskell verfügt über Typklassen, die ähnlich wie Schnittstellen sind, mit der Ausnahme, dass Sie nicht alle Implementierungen für einen Typ an derselben Stelle platzieren müssen. Sie können Implementierungen Ihrer eigenen Typklassen für vorhandene Typen von Drittanbietern erstellen, ohne auf deren Quelle zugreifen zu müssen.
  • Funktionen höherer Ordnung und rekursive Funktionen tendieren dazu, mehr Funktionalität in den Bereich der Typprüfung zu stellen. Nehmen Sie zum Beispiel Filter . In einer imperativen Sprache schreiben Sie möglicherweise eine forSchleife, um dieselbe Funktionalität zu implementieren, aber Sie haben nicht dieselben statischen Typgarantien, da eine forSchleife kein Konzept für einen Rückgabetyp hat.
  • Das Fehlen von Untertypen vereinfacht den parametrischen Polymorphismus erheblich.
  • Höherwertige Typen (Typen von Typen) sind in Haskell relativ einfach anzugeben und zu verwenden, sodass Sie Abstraktionen für Typen erstellen können, die in Java völlig unergründlich sind.
Karl Bielefeldt
quelle
7
Schöne Antwort - könnten Sie mir ein einfaches Beispiel für einen höheren Typ geben, denken Sie, das würde mir helfen zu verstehen, warum es unmöglich ist, in Java zu tun.
Phatmanace
3
Es gibt einige gute Beispiele hier .
Karl Bielefeldt
3
Pattern Matching ist auch sehr wichtig. Es bedeutet, dass Sie den Typ eines Objekts verwenden können, um Entscheidungen sehr einfach zu treffen.
Benjamin Gruenbaum
2
@BenjaminGruenbaum Ich glaube nicht, dass ich das eine Typsystemfunktion nennen würde.
Doval
3
Obwohl ADTs und HKTs definitiv Teil der Antwort sind, bezweifle ich, dass jeder, der diese Frage stellt, wissen wird, warum sie nützlich sind, schlage ich vor, dass beide Abschnitte erweitert werden müssen, um dies zu erklären
jk.
62
a :: Integer
b :: Maybe Integer
c :: IO Integer
d :: Either String Integer

In Haskell: Eine Ganzzahl, eine Ganzzahl, die möglicherweise null ist, eine Ganzzahl, deren Wert von der Außenwelt stammt, und eine Ganzzahl, die möglicherweise stattdessen eine Zeichenfolge ist, sind unterschiedliche Typen - und der Compiler erzwingt dies . Sie können kein Haskell-Programm kompilieren, das diese Unterschiede nicht berücksichtigt.

(Sie können jedoch die Typdeklarationen weglassen. In den meisten Fällen kann der Compiler den allgemeinsten Typ für Ihre Variablen bestimmen, was zu einer erfolgreichen Kompilierung führt. Ist das nicht ordentlich?)

WolfeFan
quelle
11
+1, obwohl diese Antwort nicht vollständig ist, denke ich, dass es auf der Ebene der Frage viel besser ist
jk.
1
+1 Es wäre zwar hilfreich zu erklären, dass auch andere Sprachen Maybe(z. B. Javas Optionalund Scalas Option) vorhanden sind, aber in diesen Sprachen ist es eine halbherzige Lösung, da Sie nulleiner Variablen dieses Typs immer eine Variable zuweisen und Ihr Programm beim Ausführen explodieren lassen können. Zeit. Dies kann bei Haskell [1] nicht passieren, da es keinen Nullwert gibt , sodass Sie einfach nicht schummeln können. ([1]: Sie können tatsächlich einen ähnlichen Fehler wie eine NullPointerException erzeugen, indem Sie Teilfunktionen verwenden, z. B. fromJustwenn Sie eine haben Nothing, aber diese Funktionen sind wahrscheinlich verpönt.)
Andres F.
2
"eine ganze Zahl, deren Wert von der Außenwelt kommt" - wäre es nicht IO Integernäher an "Unterprogramm, das bei Ausführung eine ganze Zahl ergibt"? Da a) main = c >> cder von first zurückgegebene Wert von dem cvon second abweichen kann, cwährend er aunabhängig von seiner Position denselben Wert hat (solange wir uns in einem einzigen Bereich befinden), b) gibt es Typen, die Werte von außerhalb der Welt angeben, um seine Sanierung durchzusetzen (dh nicht um sie direkt zu platzieren, sondern zuerst um zu überprüfen, ob die Eingaben des Benutzers korrekt / nicht böswillig sind).
Maciej Piechotka
4
Maciej, das wäre genauer. Ich strebte nach Einfachheit.
WolfeFan
30

Viele Leute haben gute Dinge über Haskell aufgelistet. Aber als Antwort auf Ihre spezifische Frage "Warum macht das Typensystem Programme korrekter?", Vermute ich, dass die Antwort "parametrischer Polymorphismus" ist.

Betrachten Sie die folgende Haskell-Funktion:

foobar :: x -> y -> y

Es gibt buchstäblich nur eine Möglichkeit , diese Funktion zu implementieren. Nur durch die Art Unterschrift, kann ich sagen genau , was diese Funktion tut, weil es nur eine mögliche Sache ist , es kann tun. [OK, nicht ganz, aber fast!]

Halte inne und denke einen Moment darüber nach. Das ist eigentlich eine wirklich große Sache! Das bedeutet, wenn ich eine Funktion mit dieser Signatur schreibe, ist es für die Funktion tatsächlich unmöglich , etwas anderes als das zu tun, was ich beabsichtigt habe. (Die Typensignatur selbst kann natürlich immer noch falsch sein. Keine Programmiersprache wird jemals alle Fehler verhindern .)

Betrachten Sie diese Funktion:

fubar :: Int -> (x -> y) -> y

Diese Funktion ist nicht möglich . Sie können diese Funktion buchstäblich nicht implementieren. Das kann ich nur an der Typunterschrift erkennen.

Wie Sie sehen können, sagt Ihnen eine Signatur vom Typ Haskell verdammt viel!


Vergleiche mit C #. (Sorry, mein Java ist ein bisschen verrostet.)

public static TY foobar<TX, TY>(TX in1, TY in2)

Es gibt einige Dinge, die diese Methode tun könnte:

  • Rückkehr in2als das Ergebnis.
  • Endlos wiederholen und niemals etwas zurückgeben.
  • Wirf eine Ausnahme und gib niemals etwas zurück.

Tatsächlich hat Haskell auch diese drei Optionen. C # bietet Ihnen jedoch auch die folgenden zusätzlichen Optionen:

  • Gibt null zurück. (Haskell hat keine Null.)
  • Ändern Sie, in2bevor Sie es zurücksenden . (Haskell verfügt nicht über eine direkte Änderung.)
  • Verwenden Sie Reflexion. (Haskell hat kein Spiegelbild.)
  • Führen Sie mehrere E / A-Aktionen aus, bevor Sie ein Ergebnis zurückgeben. (Haskell lässt Sie keine E / A ausführen, es sei denn, Sie erklären, dass Sie hier E / A ausführen.)

Reflexion ist ein besonders großer Hammer; Mit Reflektion kann ich ein neues TYObjekt aus der Luft konstruieren und es zurückgeben! Ich kann beide Objekte untersuchen und verschiedene Aktionen ausführen, je nachdem, was ich finde. Ich kann an beiden übergebenen Objekten beliebige Änderungen vornehmen.

I / O ist ein ähnlich großer Hammer. Der Code könnte Nachrichten an den Benutzer anzeigen, Datenbankverbindungen öffnen oder Ihre Festplatte neu formatieren oder so.


Im foobarGegensatz dazu kann die Haskell- Funktion nur einige Daten aufnehmen und diese Daten unverändert zurückgeben. Es kann die Daten nicht "anschauen", da ihr Typ zur Kompilierungszeit unbekannt ist. Es können keine neuen Daten erstellt werden, da ... Nun, wie werden Daten eines möglichen Typs erstellt? Dafür brauchst du Nachdenken. Es können keine E / A ausgeführt werden, da die Typensignatur nicht deklariert, dass E / A ausgeführt wird. Es kann also nicht mit dem Dateisystem oder dem Netzwerk interagieren oder Threads im selben Programm ausführen! (Dh es ist 100% garantiert threadsicher.)

Wie Sie sehen können, indem nicht lassen Sie eine ganze Reihe von Sachen zu tun, Haskell so dass Sie sehr starke Garantien über das, was Ihr Code tatsächlich tut machen. In der Tat so eng, dass (für wirklich polymorphen Code) es normalerweise nur eine Möglichkeit gibt, wie die Teile zusammenpassen können.

(Um es klar zu sagen: Es ist immer noch möglich, Haskell-Funktionen zu schreiben, bei denen die Typensignatur nicht viel aussagt. Sie Int -> Intkönnen so gut wie alles sein. Aber selbst dann wissen wir, dass dieselbe Eingabe immer mit 100% iger Sicherheit dieselbe Ausgabe erzeugt. Java garantiert das nicht einmal!)

MathematicalOrchid
quelle
4
+1 Tolle Antwort! Dies ist sehr mächtig und wird von Haskell-Neulingen oft unterschätzt. Übrigens wäre eine einfachere "unmögliche" Funktion fubar :: a -> b, nicht wahr? (Ja, mir ist bewusst unsafeCoerce. Ich gehe davon aus, dass wir über nichts mit "unsicher" im Namen sprechen, und auch Neulinge sollten sich darüber keine Sorgen machen!: D)
Andres F.
Es gibt viele einfachere Typensignaturen, die Sie nicht schreiben können, ja. Zum Beispiel foobar :: xist ziemlich nicht umsetzbar ...
MathematicalOrchid
Eigentlich Sie können nicht reinen Code faden unsicher machen, aber man kann es immer noch machen Multi-Threading. Ihre Optionen lauten "Bevor Sie dies bewerten, bewerten Sie dies", "Wenn Sie dies bewerten, möchten Sie dies möglicherweise auch in einem separaten Thread bewerten" und "Wenn Sie dies bewerten, möchten Sie dies möglicherweise auch bewerten in einem separaten Thread ". Die Standardeinstellung ist "Nach Belieben", dh "So spät wie möglich auswerten".
John Dvorak
Noch prosaischer können Sie Instanzmethoden für in1 oder in2 aufrufen, die Nebenwirkungen haben. Sie können auch den globalen Status ändern (der in Haskell als E / A-Aktion modelliert ist, aber möglicherweise nicht dem entspricht, was die meisten Benutzer als E / A betrachten).
Doug McClean
2
@isomorphismes Der Typ x -> y -> yist perfekt umsetzbar. Der Typ (x -> y) -> yist nicht. Der Typ x -> y -> ynimmt zwei Eingaben an und gibt die zweite zurück. Der Typ (x -> y) -> ynimmt eine Funktion an , die funktioniert x, und muss das irgendwie y
MathematicalOrchid
17

Eine verwandte SO-Frage .

Ich nehme an, Sie können dasselbe in haskell tun (dh Dinge in Strings / Ints packen, die wirklich erstklassige Datentypen sein sollten).

Nein, das können Sie wirklich nicht - zumindest nicht so wie Java. In Java passiert so etwas:

String x = (String)someNonString;

und Java wird gerne versuchen, Ihren Nicht-String als String umzuwandeln. Haskell erlaubt so etwas nicht und eliminiert eine ganze Klasse von Laufzeitfehlern.

nullist Teil des Typsystems (as Nothing) und muss explizit abgefragt und behandelt werden, wodurch eine ganze andere Klasse von Laufzeitfehlern beseitigt wird.

Es gibt auch eine Reihe anderer subtiler Vorteile - insbesondere in Bezug auf Wiederverwendung und Typenklassen -, die ich nicht gut genug kenne, um zu kommunizieren.

Meistens liegt es jedoch daran, dass das Typensystem von Haskell viel Ausdruckskraft zulässt. Mit nur wenigen Regeln können Sie eine ganze Menge Dinge erledigen. Betrachten Sie den allgegenwärtigen Haskell-Baum:

data Tree a = Leaf a | Branch (Tree a) (Tree a) 

Sie haben einen gesamten generischen Binärbaum (und zwei Datenkonstruktoren) in einer gut lesbaren Codezeile definiert. Alles nur mit ein paar Regeln (mit Summentypen und Produkttypen ). Das sind 3-4 Codedateien und Klassen in Java.

Gerade unter denjenigen, die dazu neigen, Typsysteme zu verehren, wird diese Art von Prägnanz / Eleganz hoch geschätzt.

Telastyn
quelle
Ich habe nur NullPointerExceptions von Ihrer Antwort verstanden. Könnten Sie weitere Beispiele hinzufügen?
Jesvin Jose
2
Nicht unbedingt wahr, JLS §5.5.1 : Wenn T ein Klassentyp ist, dann entweder | S | <: | T | oder | T | <: | S |. Andernfalls tritt ein Fehler beim Kompilieren auf. Der Compiler erlaubt es Ihnen also nicht, nicht konvertierbare Typen zu konvertieren - es gibt offensichtlich Möglichkeiten, dies zu umgehen.
Boris die Spinne
Meiner Meinung nach ist der einfachste Weg, die Vorteile von Typklassen auszudrücken, dass sie wie interfacesolche sind , die nachträglich hinzugefügt werden können, und dass sie den Typ, der sie implementiert, nicht "vergessen". Das heißt, Sie können sicherstellen, dass zwei Argumente für eine Funktion denselben Typ haben, im Gegensatz zu interfaces, bei denen zwei List<String>s unterschiedliche Implementierungen haben können. Sie könnten in Java technisch etwas sehr Ähnliches tun, indem Sie jeder Schnittstelle einen Typparameter hinzufügen, aber 99% der vorhandenen Schnittstellen tun dies nicht, und Sie werden Ihre Kollegen verdammt verwirren.
Doval
2
@BoristheSpider Stimmt, aber das Umwandeln von Ausnahmen beinhaltet fast immer das Umwandeln von einer Superklasse in eine Unterklasse oder von einer Schnittstelle in eine Klasse, und das ist nicht ungewöhnlich für die Superklasse Object.
Doval
2
Ich denke , der Punkt in der Frage nach Strings ist nicht mit dem Gießen und Laufzeittyp Fehler zu tun, aber die Tatsache , dass , wenn Sie nicht wollen verwenden Typen, Java werden Sie nicht machen - wie in, tatsächlich die Speicherung Ihrer Daten in serialisierten Form, Zeichenfolgen als Ad-hoc- anyTyp zu missbrauchen . Haskell wird Sie auch nicht davon abhalten, denn ... nun, es hat Saiten. Haskell kann Ihnen Werkzeuge zur Verfügung stellen, und es kann Sie nicht zwangsweise davon abhalten, dumme Dinge zu tun, wenn Sie darauf bestehen, dass Greenspunning genug Interpret ist, um sich nullin einem verschachtelten Kontext neu zu erfinden . Keine Sprache kann.
Leushenko
0

Eines der 'Meme', von denen die Leute, die damit vertraut sind, oft gesprochen haben, ist das Ganze "Wenn es kompiliert, wird es funktionieren *" - was meiner Meinung nach mit der Stärke des Typensystems zusammenhängt.

Dies gilt vor allem für kleine Programme. Haskell verhindert, dass Sie Fehler machen, die in anderen Sprachen einfach sind (z. B. das Vergleichen eines Int32und eines Word32und etwas explodiert), aber es verhindert nicht, dass Sie alle Fehler machen.

Haskell macht eigentlich ein Refactoring viel einfacher. Wenn Ihr Programm zuvor korrekt war und es typüberprüft, besteht die Möglichkeit, dass es auch nach geringfügigen Änderungen noch korrekt ist.

Ich versuche zu verstehen, warum genau Haskell in dieser Hinsicht besser ist als andere statisch typisierte Sprachen.

Typen in Haskell sind relativ leicht, da es einfach ist, neue Typen zu deklarieren. Dies steht im Gegensatz zu einer Sprache wie Rust, in der alles etwas umständlicher ist.

Ich gehe davon aus, dass dies mit einem starken Typensystem gemeint ist, aber es ist mir nicht klar, warum Haskells besser ist.

Haskell bietet viele Funktionen, die über einfache Summen- und Produkttypen hinausgehen. es gibt auch universell quantifizierte Typen (zB id :: a -> a). Sie können auch Datensatztypen mit Funktionen erstellen, die sich deutlich von einer Sprache wie Java oder Rust unterscheiden.

GHC kann einige Instanzen auch nur auf der Basis von Typen ableiten, und seit dem Aufkommen von Generika können Sie Funktionen schreiben, die für Typen generisch sind. Dies ist recht praktisch und in Java fließender als das Gleiche.

Ein weiterer Unterschied ist, dass Haskell relativ gute Schreibfehler aufweist (zumindest beim Schreiben). Die Typinferenz von Haskell ist raffiniert und es kommt ziemlich selten vor, dass Sie Typanmerkungen bereitstellen müssen, um etwas zum Kompilieren zu erhalten. Dies steht im Gegensatz zu Rust, wo Typinferenz manchmal Anmerkungen erfordern kann, selbst wenn der Compiler den Typ im Prinzip ableiten könnte.

Schließlich hat Haskell Typenklassen, darunter die berühmte Monade. Monaden sind eine besonders gute Möglichkeit, mit Fehlern umzugehen. Sie bieten Ihnen im Grunde fast den ganzen Komfort, nullohne das schreckliche Debuggen und ohne Ihre Art von Sicherheit aufgeben zu müssen. Die Fähigkeit, Funktionen für diese Typen zu schreiben, spielt also eine große Rolle, wenn es darum geht, uns zu deren Verwendung zu ermutigen!

Anders ausgedrückt, Sie können gutes oder schlechtes Java schreiben. Ich gehe davon aus, dass Sie dasselbe in Haskell tun können

Das ist vielleicht richtig, aber es fehlt ein entscheidender Punkt: Der Punkt, an dem Sie sich in Haskell in den Fuß schießen, ist weiter entfernt als der Punkt, an dem Sie sich in Java in den Fuß schießen.


quelle