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 string
Inhalte elephant, giraffe
usw. vorhanden sind und wenn jemand sie einfügt, Mercedes
wird 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!
quelle
Maybe
erst 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.Antworten:
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.
Eq
spreche 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.data Bt a = Here a | There (Bt (a, a))
. Überlegen Sie genau, welche Werte gültig sind,Bt a
und beachten Sie, wie dieser Typ funktioniert. Es ist schwierig!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.mtl
Effektschreibsystem, 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.(+)
Dinge gemeinsam angehen ? OhInteger
, ok! Lassen Sie uns jetzt den richtigen Code einbetten!". Bei komplexeren Systemen können Sie interessantere Einschränkungen festlegen.mtl
Bibliothek basiert auf dieser Idee.(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 diea
Variable nur innerhalb des Arguments gebunden ist. Dies bedeutet, dass der Definierer der FunktionmapFree
entscheiden kann, woran siea
instanziiert wird, wenn sie sie verwendet, und nicht der Benutzer vonmapFree
.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
State
es 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 wieHandle
die Einnahme Argumente zu bestimmten Arten wieState
. Es ist verwirrend, alle Details zu verstehen, aber auch so richtig.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 -> TypeRepr
zumindest 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 . DasTypeable
System von Haskell ermöglicht die Erstellung eines Safescoerce :: (Typeable a, Typeable b) => a -> Maybe b
. Dieses System basiert aufTypeable
der 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.
IO a
Repräsentant bezeichnet wird Nebenwirkungsberechnungen, die zu Werten vom Typ führena
. Dies ist die Grundlage eines sehr schönen Effektsystems, das in eine reine Sprache eingebettet ist.null
. Jeder weiß, dass diesnull
der 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 TypA
in einen Typ umwandelnMaybe A
, mindern das Problem vonnull
.Bt a
Art aus der Zeit vor und versuchen , eine Funktion zu schreiben , um seine Größe zu berechnen:size :: Bt a -> Int
. Es wird ein bisschen wiesize (Here a) = 1
und aussehensize (There bt) = 2 * size bt
. Operativ ist das nicht allzu komplex, aber beachten Sie, dass der rekursive Aufrufsize
in der letzten Gleichung bei einem anderen Typ auftritt , die Gesamtdefinition jedoch einen schönen verallgemeinerten Typ hatsize :: 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.
quelle
char *p = NULL;
, wird Falle auf*p=1234
, wird aber nicht Falle aufchar *q = p+5678;
noch*q = 1234;
null
gibt, 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.p = undefined
so lange zu schreiben , wiep
es nicht ausgewertet wird. Zweckmäßigerweise können Sieundefined
eine 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.for
Schleife, um dieselbe Funktionalität zu implementieren, aber Sie haben nicht dieselben statischen Typgarantien, da einefor
Schleife kein Konzept für einen Rückgabetyp hat.quelle
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?)
quelle
Maybe
(z. B. JavasOptional
und ScalasOption
) vorhanden sind, aber in diesen Sprachen ist es eine halbherzige Lösung, da Sienull
einer 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.fromJust
wenn Sie eine habenNothing
, aber diese Funktionen sind wahrscheinlich verpönt.)IO Integer
näher an "Unterprogramm, das bei Ausführung eine ganze Zahl ergibt"? Da a)main = c >> c
der von first zurückgegebene Wert von demc
von second abweichen kann,c
während era
unabhä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).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:
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:
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.)
Es gibt einige Dinge, die diese Methode tun könnte:
in2
als das Ergebnis.Tatsächlich hat Haskell auch diese drei Optionen. C # bietet Ihnen jedoch auch die folgenden zusätzlichen Optionen:
in2
bevor Sie es zurücksenden . (Haskell verfügt nicht über eine direkte Änderung.)Reflexion ist ein besonders großer Hammer; Mit Reflektion kann ich ein neues
TY
Objekt 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
foobar
Gegensatz 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 -> Int
kö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!)quelle
fubar :: a -> b
, nicht wahr? (Ja, mir ist bewusstunsafeCoerce
. Ich gehe davon aus, dass wir über nichts mit "unsicher" im Namen sprechen, und auch Neulinge sollten sich darüber keine Sorgen machen!: D)foobar :: x
ist ziemlich nicht umsetzbar ...x -> y -> y
ist perfekt umsetzbar. Der Typ(x -> y) -> y
ist nicht. Der Typx -> y -> y
nimmt zwei Eingaben an und gibt die zweite zurück. Der Typ(x -> y) -> y
nimmt eine Funktion an , die funktioniertx
, und muss das irgendwiey
Eine verwandte SO-Frage .
Nein, das können Sie wirklich nicht - zumindest nicht so wie Java. In Java passiert so etwas:
und Java wird gerne versuchen, Ihren Nicht-String als String umzuwandeln. Haskell erlaubt so etwas nicht und eliminiert eine ganze Klasse von Laufzeitfehlern.
null
ist Teil des Typsystems (asNothing
) 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:
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.
quelle
interface
solche 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 zuinterface
s, bei denen zweiList<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.Object
.any
Typ 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 sichnull
in einem verschachtelten Kontext neu zu erfinden . Keine Sprache kann.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
Int32
und einesWord32
und 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.
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.
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,
null
ohne 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!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