Was bedeutet die "Just" -Syntax in Haskell?

118

Ich habe das Internet nach einer tatsächlichen Erklärung durchsucht, was dieses Schlüsselwort bewirkt. Jedes Haskell-Tutorial, das ich mir angesehen habe, wird nur zufällig verwendet und erklärt nie, was es tut (und ich habe mir viele angesehen).

Hier ist ein grundlegender Code von Real World Haskell , der verwendet wird Just. Ich verstehe, was der Code tut, aber ich verstehe nicht, was der Zweck oder die Funktion von Justist.

lend amount balance = let reserve    = 100
                      newBalance = balance - amount
                  in if balance < reserve
                     then Nothing
                     else Just newBalance

Nach allem, was ich beobachtet habe, hängt es mit dem MaybeTippen zusammen, aber das ist so ziemlich alles, was ich gelernt habe.

Eine gute Erklärung, was JustMittel bedeutet, wäre sehr dankbar.

reem
quelle

Antworten:

211

Es ist eigentlich nur ein normaler Datenkonstruktor, der zufällig im Prelude definiert wird. Dies ist die Standardbibliothek, die automatisch in jedes Modul importiert wird.

Was vielleicht strukturell ist

Die Definition sieht ungefähr so ​​aus:

data Maybe a = Just a
             | Nothing

Diese Deklaration definiert einen Typ, Maybe ader durch eine Typvariable parametrisiert wird. aDies bedeutet lediglich, dass Sie ihn mit jedem Typ anstelle von verwenden können a.

Konstruieren und zerstören

Der Typ hat zwei Konstruktoren Just aund Nothing. Wenn ein Typ mehrere Konstruktoren hat, bedeutet dies, dass ein Wert des Typs mit nur einem der möglichen Konstruktoren erstellt worden sein muss. Für diesen Typ wurde ein Wert entweder über Justoder erstellt Nothing, es gibt keine anderen (fehlerfreien) Möglichkeiten.

Da Nothinges keinen Parametertyp gibt, benennt es bei Verwendung als Konstruktor einen konstanten Wert, der Maybe afür alle Typen vom Typ type ist a. Der JustKonstruktor verfügt jedoch über einen Typparameter. Wenn er als Konstruktor verwendet wird, verhält er sich wie eine Funktion von Typ abis Maybe a, dh er hat den Typa -> Maybe a

Die Konstruktoren eines Typs erstellen also einen Wert dieses Typs. Die andere Seite der Dinge ist, wenn Sie diesen Wert verwenden möchten, und hier kommt der Mustervergleich ins Spiel. Im Gegensatz zu Funktionen können Konstruktoren in Musterbindungsausdrücken verwendet werden. Auf diese Weise können Sie Fallanalysen von Werten durchführen, die zu Typen mit mehr als einem Konstruktor gehören.

Um einen Maybe aWert in einer Musterübereinstimmung zu verwenden, müssen Sie für jeden Konstruktor ein Muster bereitstellen, wie folgt:

case maybeVal of
    Nothing   -> "There is nothing!"
    Just val  -> "There is a value, and it is " ++ (show val)

In diesem Fall würde das erste Muster übereinstimmen, wenn der Wert wäre Nothing, und das zweite würde übereinstimmen, wenn der Wert mit konstruiert wurde Just. Wenn der zweite übereinstimmt, bindet er auch den Namen valan den Parameter, der an den JustKonstruktor übergeben wurde, als der Wert erstellt wurde, mit dem Sie übereinstimmen.

Was vielleicht bedeutet

Vielleicht waren Sie bereits damit vertraut, wie das funktionierte; MaybeWerte sind nicht wirklich magisch , sondern nur ein normaler alaskischer Haskell-Datentyp (ADT). Aber es wird ziemlich oft verwendet, weil es einen Typ, wie Integeraus Ihrem Beispiel, effektiv in einen neuen Kontext "hebt" oder erweitert, in dem es einen zusätzlichen Wert ( Nothing) hat, der einen Wertmangel darstellt! Das Typsystem erfordert dann , dass Sie für diesen Mehrwert zu überprüfen , bevor es Sie an dem erhalten lassen , Integerdie vielleicht da sein. Dies verhindert eine bemerkenswerte Anzahl von Fehlern.

Viele Sprachen verarbeiten diese Art von "No-Value" -Wert heutzutage über NULL-Referenzen. Tony Hoare, ein bedeutender Informatiker (er hat Quicksort erfunden und ist Turing-Preisträger), hält dies für seinen "Milliarden-Dollar-Fehler" . Der Vielleicht-Typ ist nicht der einzige Weg, dies zu beheben, aber er hat sich als effektiver Weg erwiesen, dies zu tun.

Vielleicht als Funktor

Die Idee, einen Typ in einen anderen zu transformieren, sodass Operationen für den alten Typ auch so transformiert werden können, dass sie für den neuen Typ funktionieren, ist das Konzept hinter der aufgerufenen Haskell-Typklasse Functor, die Maybe aeine nützliche Instanz von hat.

Functorstellt eine aufgerufene Methode bereit fmap, die Funktionen, die über Werte vom Basistyp (z. B. Integer) liegen, Funktionen zuordnet, die über Werte vom angehobenen Typ (z. B. Maybe Integer) reichen . Eine Funktion, die transformiert wurde fmap, um an einem MaybeWert zu arbeiten, funktioniert folgendermaßen:

case maybeVal of
  Nothing  -> Nothing         -- there is nothing, so just return Nothing
  Just val -> Just (f val)    -- there is a value, so apply the function to it

Wenn Sie also einen Maybe IntegerWert m_xund eine Int -> IntFunktion haben f, können Sie fmap f m_xdie Funktion fdirekt auf die anwenden , Maybe Integerohne sich Sorgen machen zu müssen, ob sie tatsächlich einen Wert hat oder nicht. Tatsächlich können Sie eine ganze Kette von angehobenen Integer -> IntegerFunktionen auf Maybe IntegerWerte anwenden und müssen sich nur noch Nothingeinmal explizit darum kümmern, wenn Sie fertig sind.

Vielleicht als Monade

Ich bin mir nicht sicher, wie vertraut Sie mit dem Konzept eines sind Monad, aber Sie haben es zumindest schon einmal verwendet IO a, und die Typensignatur IO asieht bemerkenswert ähnlich aus Maybe a. Obwohl IOes insofern besonders ist, als es seine Konstruktoren nicht für Sie verfügbar macht und daher nur vom Haskell-Laufzeitsystem "ausgeführt" werden kann, ist es Functorneben dem auch noch ein Monad. In der Tat gibt es einen wichtigen Sinn, in dem a Monadnur eine besondere Art Functormit einigen zusätzlichen Funktionen ist, aber dies ist nicht der richtige Ort, um darauf einzugehen.

Wie auch immer, Monaden mögen IOZuordnungstypen zu neuen Typen, die "Berechnungen, die zu Werten führen" darstellen, und Sie können Funktionen Monadüber eine sehr fmapähnliche Funktion liftM, die eine reguläre Funktion in eine "Berechnung umwandelt, die zu dem Wert führt, der durch Auswertung der Werte erhalten wird, in Typen umwandeln Funktion."

Sie haben wahrscheinlich erraten (wenn Sie so weit gelesen haben), dass dies Maybeauch ein ist Monad. Es stellt "Berechnungen dar, bei denen möglicherweise kein Wert zurückgegeben wird". Genau wie im fmapBeispiel können Sie so eine ganze Reihe von Berechnungen durchführen, ohne nach jedem Schritt explizit nach Fehlern suchen zu müssen. Tatsächlich wird bei der Art und Weise, wie die MonadInstanz aufgebaut ist, eine Berechnung der MaybeWerte gestoppt , sobald eine gefunden Nothingwird. Dies ist also wie ein sofortiger Abbruch oder eine wertlose Rückkehr mitten in einer Berechnung.

Du hättest vielleicht schreiben können

Wie ich bereits sagte, ist dem MaybeTyp, der in die Sprachsyntax oder das Laufzeitsystem integriert ist, nichts inhärent . Wenn Haskell es nicht standardmäßig zur Verfügung gestellt hat, können Sie alle Funktionen selbst bereitstellen! Tatsächlich könnten Sie es trotzdem selbst mit unterschiedlichen Namen erneut schreiben und die gleiche Funktionalität erhalten.

Hoffentlich verstehen Sie den MaybeTyp und seine Konstruktoren jetzt, aber wenn noch etwas unklar ist, lassen Sie es mich wissen!

Levi Pearson
quelle
19
Was für eine hervorragende Antwort! Es sollte erwähnt werden, dass Haskell oft verwendet, Maybewo andere Sprachen verwenden würden nulloder nil(mit bösen NullPointerExceptionlauern in jeder Ecke). Jetzt verwenden auch andere Sprachen dieses Konstrukt: Scala as Optionund sogar Java 8 haben den OptionalTyp.
Landei
3
Dies ist eine hervorragende Erklärung. Viele der Erklärungen, die ich gelesen habe, deuten auf die Idee hin, dass Just ein Konstruktor für den Typ Vielleicht ist, aber keiner hat es jemals wirklich explizit gemacht.
Reem
@ Landi, danke für den Vorschlag. Ich habe eine Änderung vorgenommen, um auf die Gefahr von Nullreferenzen hinzuweisen.
Levi Pearson
@Landei Der Optionstyp gibt es seit ML in den 70er Jahren. Es ist wahrscheinlicher, dass Scala ihn von dort aufgegriffen hat, da Scala die Namenskonvention von ML verwendet, Option mit einigen und keinen Konstruktoren.
Steinmetz
@ Landi Apples Swift nutzt auch ziemlich viele Optionen
Jamin
37

Die meisten aktuellen Antworten sind hochtechnische Erklärungen dafür Just und Freunde arbeiten. Ich dachte, ich könnte versuchen zu erklären, wofür es ist.

Viele Sprachen haben einen solchen nullWert, der zumindest für einige Typen anstelle eines echten Werts verwendet werden kann. Dies hat viele Menschen sehr wütend gemacht und wurde allgemein als schlechter Schachzug angesehen. Trotzdem ist es manchmal nützlich, einen Wert nullzu haben, der das Fehlen einer Sache anzeigt.

Haskell löst dieses Problem, indem Sie explizit Orte markieren, an denen Sie a haben können Nothing(seine Version von a null). Wenn Ihre Funktion normalerweise den Typ zurückgeben würde Foo, sollte sie stattdessen den Typ zurückgeben Maybe Foo. Wenn Sie angeben möchten, dass kein Wert vorhanden ist, geben Sie zurück Nothing. Wenn Sie einen Wert zurückgeben möchten bar, sollten Sie stattdessen zurückgeben Just bar.

Wenn Sie also nicht haben können Nothing, brauchen Sie es nicht Just. Wenn du kannstNothing , brauchen Sie Just.

Es ist nichts Magisches daran Maybe; Es basiert auf dem System vom Typ Haskell. Das heißt, Sie können alle üblichen Haskell- Mustervergleichstricks verwenden .

Brent Royal-Gordon
quelle
1
Sehr schöne Antwort neben den anderen Antworten, aber ich denke, es würde immer noch von einem Codebeispiel profitieren :)
PascalVKooten
13

Bei einem gegebenen Typ tist ein Wert von Just tein vorhandener Wert vom Typ t, wobeiNothing bei dem ein Wert nicht erreicht werden kann, oder ein Fall, bei dem ein Wert bedeutungslos wäre.

In Ihrem Beispiel ist ein negativer Saldo nicht sinnvoll. Wenn so etwas auftreten würde, wird es durch ersetzt Nothing.

In einem anderen Beispiel könnte dies bei der Division verwendet werden, indem eine Divisionsfunktion definiert wird, die aund bnimmt und zurückgibt, Just a/bwenn sie bungleich Null ist, und Nothingansonsten. Es wird oft so verwendet, als bequeme Alternative zu Ausnahmen oder wie in Ihrem früheren Beispiel, um Werte zu ersetzen, die keinen Sinn ergeben.

Qaphla
quelle
1
Nehmen wir also an, ich habe im obigen Code das Just entfernt. Warum sollte das nicht funktionieren? Müssen Sie nur haben, wann immer Sie ein Nichts haben wollen? Was passiert mit einem Ausdruck, bei dem eine Funktion in diesem Ausdruck Nothing zurückgibt?
Reem
7
Wenn Sie das entfernen Just, wird Ihr Code nicht typecheck. Der Grund dafür Justist, die richtigen Typen beizubehalten. Es gibt einen Typ (eigentlich eine Monade, aber es ist einfacher, sich nur einen Typ vorzustellen) Maybe t, der aus Elementen der Form Just tund besteht Nothing. Da Nothinghat Typ Maybe t, wird ein Ausdruck, der entweder Nothingoder einen Wert von Typ auswerten kann, tnicht richtig typisiert. Wenn eine Funktion Nothingin einigen Fällen zurückgegeben wird, muss jeder Ausdruck, der diese Funktion verwendet, eine Möglichkeit haben, dies ( isJustoder eine case-Anweisung) zu überprüfen , um alle möglichen Fälle zu behandeln.
Qaphla
2
Das Just ist also einfach da, um innerhalb des Vielleicht-Typs konsistent zu sein, weil ein reguläres t nicht im Vielleicht-Typ ist. Jetzt ist alles viel klarer. Danke dir!
Reem
3
@qaphla: Dein Kommentar darüber, dass es sich um eine "Monade, [...]" handelt, ist irreführend. Maybe t ist nur ein Typ. Die Tatsache, dass es eine MonadInstanz für Maybegibt, verwandelt sie nicht in etwas, das kein Typ ist.
Sarah
2

Eine Gesamtfunktion a-> b kann für jeden möglichen Wert vom Typ a einen Wert vom Typ b finden.

In Haskell sind nicht alle Funktionen vollständig. In diesem speziellen Fall Funktionlend nicht vollständig - sie ist nicht für den Fall definiert, dass das Guthaben unter der Reserve liegt (obwohl es nach meinem Geschmack sinnvoller wäre, nicht zuzulassen, dass newBalance unter der Reserve liegt - wie es ist, können Sie 101 ausleihen ein Saldo von 100).

Andere Designs, die sich mit nicht vollständigen Funktionen befassen:

  • Auslösen von Ausnahmen beim Überprüfen des Eingabewerts passt nicht in den Bereich
  • Rückgabe eines speziellen Werts (primitiver Typ): Die bevorzugte Auswahl ist ein negativer Wert für ganzzahlige Funktionen, die natürliche Zahlen zurückgeben sollen (z. B. String.indexOf - Wenn ein Teilstring nicht gefunden wird, wird der zurückgegebene Index normalerweise als negativ ausgelegt.)
  • Geben Sie einen speziellen Wert (Zeiger) zurück: NULL oder einen solchen
  • stillschweigend zurückkehren, ohne etwas zu tun: Könnte beispielsweise lendgeschrieben werden, um den alten Saldo zurückzugeben, wenn die Bedingung für die Kreditvergabe nicht erfüllt ist
  • Geben Sie einen speziellen Wert zurück: Nothing (oder Left, das ein Fehlerbeschreibungsobjekt umschließt)

Dies sind notwendige Designeinschränkungen in Sprachen, die nicht die Gesamtheit der Funktionen erzwingen können (z. B. Agda, dies führt jedoch zu anderen Komplikationen, z. B. Unvollständigkeit).

Das Problem bei der Rückgabe eines speziellen Werts oder beim Auslösen von Ausnahmen besteht darin, dass es für den Aufrufer leicht ist, die Behandlung einer solchen Möglichkeit versehentlich zu unterlassen.

Das Problem beim stillen Verwerfen eines Fehlers liegt ebenfalls auf der Hand: Sie beschränken die Möglichkeiten des Anrufers für die Funktion. Wenn beispielsweise das lendalte Guthaben zurückgegeben wird, kann der Anrufer nicht feststellen, ob sich das Guthaben geändert hat. Je nach Verwendungszweck kann dies ein Problem sein oder auch nicht.

Die Lösung von Haskell zwingt den Aufrufer einer Teilfunktion, sich mit dem Typ like Maybe aoder Either error aaufgrund des Rückgabetyps der Funktion zu befassen .

Diese lendDefinition ist eine Funktion, die nicht immer einen neuen Kontostand berechnet. Unter bestimmten Umständen wird kein neuer Kontostand definiert. Wir signalisieren dem Anrufer diesen Umstand, indem wir entweder den Sonderwert Nothing zurückgeben oder den neuen Kontostand in Just einwickeln. Der Anrufer hat nun die Freiheit zu wählen: Entweder das Versagen beim Ausleihen auf besondere Weise behandeln oder das alte Guthaben ignorieren und verwenden - zum Beispiel maybe oldBalance id $ lend amount oldBalance.

Sassa NF
quelle
-1

Die Funktion if (cond :: Bool) then (ifTrue :: a) else (ifFalse :: a)muss den gleichen Typ von ifTrueund haben ifFalse.

Wenn wir also schreiben then Nothing, müssen wir Maybe atype in verwendenelse f

if balance < reserve
       then (Nothing :: Maybe nb)         -- same type
       else (Just newBalance :: Maybe nb) -- same type
Witz
quelle
1
Ich bin sicher, Sie
wollten
1
Haskell hat Typinferenz. Es ist nicht erforderlich, den Typ von Nothingund Just newBalanceexplizit anzugeben.
Rechtsfalte
Für diese Erklärung für den Uneingeweihten verdeutlichen die expliziten Typen die Bedeutung der Verwendung von Just.
Philip