Das macht Haskell folgendermaßen: (Dies ist kein Widerspruch zu Lipperts Aussagen, da Haskell keine objektorientierte Sprache ist.)
WARNUNG: Langatmige Antwort von einem ernsthaften Haskell-Fan.
TL; DR
Dieses Beispiel zeigt genau, wie unterschiedlich sich Haskell von C # unterscheidet. Anstatt die Logistik des Tragwerksbaus an einen Konstrukteur zu delegieren, muss diese im umgebenden Code abgewickelt werden. Es gibt keine Möglichkeit, dass ein Nullwert (oder Nothing
in Haskell) auftritt, wenn ein Nicht-Nullwert erwartet wird, da Nullwerte nur in speziellen aufgerufenen Wrapper-Typen auftreten können, Maybe
die nicht mit / direkt in reguläre, nicht austauschbare Werte konvertierbar sind. nullbare Typen. Um einen Wert zu verwenden, der durch Umschließen mit einem nullwertfähig gemacht wurde Maybe
, müssen wir zuerst den Wert mithilfe des Mustervergleichs extrahieren. Dadurch werden wir gezwungen, den Kontrollfluss in einen Zweig umzuleiten, von dem wir sicher wissen, dass er nicht null ist.
Deshalb:
Können wir immer wissen, dass ein nicht nullwertfähiger Verweis unter keinen Umständen als ungültig angesehen wird?
Ja. Int
und Maybe Int
sind zwei völlig getrennte Typen. Das Finden Nothing
in einer Ebene Int
wäre vergleichbar mit dem Finden der Zeichenfolge "Fisch" in einer Ebene Int32
.
Was ist mit dem Konstruktor eines Objekts mit einem nicht nullwertfähigen Referenzfeldtyp?
Kein Problem: Wertekonstruktoren in Haskell können nichts anderes tun, als die angegebenen Werte zu nehmen und sie zusammenzufügen. Die gesamte Initialisierungslogik findet statt, bevor der Konstruktor aufgerufen wird.
Was ist mit dem Finalizer eines solchen Objekts, bei dem das Objekt finalisiert wird, weil der Code, der die Referenz ausfüllen sollte, eine Ausnahme ausgelöst hat?
Es gibt keine Finalizer in Haskell, daher kann ich das nicht wirklich ansprechen. Meine erste Antwort steht jedoch noch.
Vollständige Antwort :
Haskell hat keine Null und verwendet den Maybe
Datentyp, um Nullables darzustellen. Vielleicht ist ein algabraischer Datentyp wie folgt definiert:
data Maybe a = Just a | Nothing
Wenn Sie mit Haskell nicht vertraut sind, lesen Sie dies als "A Maybe
ist entweder ein Nothing
oder ein Just a
". Speziell:
Maybe
ist der Typkonstruktor : Er kann (fälschlicherweise) als generische Klasse betrachtet werden (wobei a
die Typvariable ist). Die C # Analogie ist class Maybe<a>{}
.
Just
ist ein Wertekonstruktor : Es ist eine Funktion, die ein Argument vom Typ a
annimmt und einen Wert vom Typ zurückgibt Maybe a
, der den Wert enthält. Der Code x = Just 17
ist also analog zu int? x = 17;
.
Nothing
ist ein anderer Wertekonstruktor, der jedoch keine Argumente akzeptiert und für den Maybe
kein anderer Wert als "Nothing" zurückgegeben wird. x = Nothing
ist analog zu int? x = null;
(unter der Annahme, dass wir unser a
in Haskell beschränkt haben Int
, was durch Schreiben geschehen kann x = Nothing :: Maybe Int
).
Maybe
Wie vermeidet Haskell die in der Frage des OP erörterten Probleme, nachdem die Grundlagen des Typs nicht mehr vorhanden sind?
Nun, Haskell unterscheidet sich wirklich von den meisten bisher diskutierten Sprachen. Ich beginne mit der Erläuterung einiger grundlegender Sprachprinzipien.
Zunächst einmal ist in Haskell alles unveränderlich . Alles. Namen beziehen sich auf Werte, nicht auf Speicherorte, an denen Werte gespeichert werden können (dies allein ist eine enorme Quelle für die Beseitigung von Fehlern). Anders als in C #, wobei variable Deklaration und Zuordnung sind zwei getrennte Vorgänge, in Haskell Werte durch Definition deren Wert erzeugt werden ( zum Beispiel x = 15
, y = "quux"
, z = Nothing
), die sich nie ändern kann. Daher Code wie:
ReferenceType x;
Ist in Haskell nicht möglich. Es gibt keine Probleme beim Initialisieren von Werten für, null
da alles explizit auf einen Wert initialisiert werden muss, damit es existiert.
Zweitens ist Haskell keine objektorientierte Sprache : Es ist eine rein funktionale Sprache, es gibt also keine Objekte im eigentlichen Sinne des Wortes. Stattdessen gibt es einfach Funktionen (Wertekonstruktoren), die ihre Argumente verwenden und eine verschmolzene Struktur zurückgeben.
Als nächstes gibt es absolut keinen zwingenden Stilcode. Damit meine ich, dass die meisten Sprachen einem ähnlichen Muster folgen:
do thing 1
add thing 2 to thing 3
do thing 4
if thing 5:
do thing 6
return thing 7
Das Programmverhalten wird als eine Reihe von Anweisungen ausgedrückt. In objektorientierten Sprachen spielen auch Klassen- und Funktionsdeklarationen eine große Rolle im Programmfluss, aber das "Fleisch" der Programmausführung besteht im Wesentlichen aus einer Reihe auszuführender Anweisungen.
In Haskell ist dies nicht möglich. Stattdessen wird der Programmfluss ausschließlich durch Verkettungsfunktionen bestimmt. Sogar die imperativ aussehende do
Anmerkung ist nur syntaktischer Zucker, um anonyme Funktionen an den >>=
Bediener weiterzugeben . Alle Funktionen haben folgende Form:
<optional explicit type signature>
functionName arg1 arg2 ... argn = body-expression
Wo body-expression
kann alles sein, was zu einem Wert ausgewertet wird. Offensichtlich stehen mehr Syntaxfunktionen zur Verfügung, aber der Hauptaspekt ist das völlige Fehlen von Anweisungsfolgen.
Schließlich und wahrscheinlich am wichtigsten ist Haskells Typensystem unglaublich streng. Wenn ich die zentrale Designphilosophie von Haskells Typensystem zusammenfassen müsste, würde ich sagen: "Machen Sie zur Kompilierungszeit so viele Fehler wie möglich, damit zur Laufzeit so wenig wie möglich schief geht." Es gibt keinerlei implizite Konvertierungen (Sie möchten eine Int
in eine Double
? Verwenden Sie die fromIntegral
Funktion). Das einzige, was möglicherweise zur Laufzeit zu einem ungültigen Wert führt, ist die Verwendung Prelude.undefined
(die anscheinend nur vorhanden sein muss und unmöglich zu entfernen ist ).
Schauen wir uns in diesem Sinne das "kaputte" Beispiel von amon an und versuchen, diesen Code in Haskell erneut auszudrücken. Zuerst die Datendeklaration (unter Verwendung der Datensatzsyntax für benannte Felder):
data NotSoBroken = NotSoBroken {foo :: Foo, bar :: Bar }
( foo
und bar
sind hier wirklich Accessorfunktionen für anonyme Felder anstelle von tatsächlichen Feldern, aber wir können dieses Detail ignorieren).
Der NotSoBroken
Wertekonstruktor ist nicht in der Lage, andere Aktionen als a Foo
und a Bar
(die nicht nullwertfähig sind) NotSoBroken
auszuführen und daraus ein zu machen. Es gibt keinen Platz, um Imperativcode einzufügen oder die Felder auch nur manuell zuzuweisen. Die gesamte Initialisierungslogik muss an einer anderen Stelle stattfinden, höchstwahrscheinlich in einer dedizierten Factory-Funktion.
Im Beispiel Broken
schlägt die Konstruktion von immer fehl. Es gibt keine Möglichkeit, den NotSoBroken
Wertekonstruktor auf ähnliche Weise zu unterbrechen (es gibt einfach keinen Ort, an dem der Code geschrieben werden kann), aber wir können eine Factory-Funktion erstellen, die ähnlich fehlerhaft ist.
makeNotSoBroken :: Foo -> Bar -> Maybe NotSoBroken
makeNotSoBroken foo bar = Nothing
(Die erste Zeile ist eine Typensignaturdeklaration: makeNotSoBroken
Nimmt ein Foo
und ein Bar
als Argument und erzeugt ein Maybe NotSoBroken
).
Der Rückgabetyp muss Maybe NotSoBroken
und nicht einfach sein, NotSoBroken
weil wir ihm gesagt haben, dass er ausgewertet werden Nothing
soll. Dies ist ein Wertekonstruktor für Maybe
. Die Typen würden einfach nicht in einer Reihe stehen, wenn wir etwas anderes schreiben würden.
Abgesehen davon, dass diese Funktion absolut sinnlos ist, erfüllt sie nicht einmal ihren eigentlichen Zweck, wie wir sehen werden, wenn wir versuchen, sie zu verwenden. Erstellen wir eine Funktion, useNotSoBroken
die a NotSoBroken
als Argument erwartet :
useNotSoBroken :: NotSoBroken -> Whatever
( useNotSoBroken
akzeptiert a NotSoBroken
als Argument und erzeugt a Whatever
).
Und benutze es so:
useNotSoBroken (makeNotSoBroken)
In den meisten Sprachen kann dieses Verhalten eine Nullzeigerausnahme verursachen. In Haskell stimmen die Typen nicht überein: makeNotSoBroken
Gibt a zurück Maybe NotSoBroken
, useNotSoBroken
erwartet jedoch a NotSoBroken
. Diese Typen sind nicht austauschbar und der Code kann nicht kompiliert werden.
Um dies zu umgehen, können wir eine case
Anweisung verwenden, um basierend auf der Struktur des Maybe
Werts zu verzweigen (mithilfe eines Features namens Mustervergleich ):
case makeNotSoBroken of
Nothing -> --handle situation here
(Just x) -> useNotSoBroken x
Natürlich muss dieses Snippet in einen bestimmten Kontext gestellt werden, um tatsächlich kompiliert zu werden, aber es demonstriert die Grundlagen, wie Haskell mit NULL-Werten umgeht. Hier finden Sie eine schrittweise Erklärung des obigen Codes:
- Zunächst
makeNotSoBroken
wird ausgewertet, was garantiert einen Wert vom Typ ergibt Maybe NotSoBroken
.
- Die
case
Anweisung überprüft die Struktur dieses Werts.
- Wenn der Wert ist
Nothing
, wird der Code "Situation hier behandeln" ausgewertet.
- Wenn der Wert stattdessen mit einem
Just
Wert übereinstimmt, wird der andere Zweig ausgeführt. Beachten Sie, wie die Übereinstimmungsklausel den Wert gleichzeitig als Just
Konstruktion identifiziert und sein internes NotSoBroken
Feld an einen Namen bindet (in diesem Fall x
). x
kann dann wie der normale NotSoBroken
Wert verwendet werden.
Die Mustererkennung bietet daher eine leistungsstarke Möglichkeit zur Durchsetzung der Typensicherheit, da die Struktur des Objekts untrennbar mit der Verzweigung der Steuerung verbunden ist.
Ich hoffe das war eine nachvollziehbare Erklärung. Wenn es keinen Sinn ergibt, springen Sie zu Learn You A Haskell For Great Good! , eines der besten Online-Tutorials, die ich je gelesen habe. Hoffentlich sehen Sie in dieser Sprache die gleiche Schönheit wie ich.