Welches Problem lösen algebraische Datentypen?


18

Faire Warnung, ich bin neu in der funktionalen Programmierung, daher kann ich viele schlechte Annahmen vertreten.

Ich habe etwas über algebraische Typen gelernt. Viele funktionale Sprachen scheinen sie zu haben, und sie sind in Verbindung mit dem Mustervergleich ziemlich nützlich. Welches Problem lösen sie jedoch tatsächlich? Ich kann einen scheinbar (irgendwie) algebraischen Typ in C # wie folgt implementieren:

public abstract class Option { }
public class None : Option { }
public class Some<T> : Option
{
    public T Value { get; set; }
}

var result = GetSomeValue();
if(result is None)
{
}
else
{
}

Aber ich denke, die meisten würden zustimmen, dass dies eine Bastardisierung der objektorientierten Programmierung ist, und Sie sollten dies niemals tun. Fügt die funktionale Programmierung nur eine klarere Syntax hinzu, die diesen Programmierstil weniger grob erscheinen lässt? Was fehlt mir noch?


6
Funktionale Programmierung ist ein anderes Paradigma als objektorientierte Programmierung.
Basile Starynkevitch

@BasileStarynkevitch Mir ist klar, aber es gibt Sprachen wie F #, die ein bisschen von beidem sind. Die Frage bezieht sich nicht so sehr auf funktionale Sprachen, sondern vielmehr darauf, welches Problem algebraische Datentypen lösen.
ConditionRacer

7
Was passiert, wenn ich definiere class ThirdOption : Option{}und dir ein gebe, new ThirdOption()wo du es erwartet hast Someoder None?
amon

1
@amon-Sprachen, die Summentypen haben, haben im Allgemeinen die Möglichkeit, diese zu verbieten. Zum Beispiel definiert Haskell data Maybe a = Just a | Nothing(äquivalent zu data Option a = Some a | Nonein Ihrem Beispiel): Sie können keinen dritten Fall post-hoc hinzufügen. Zwar können Sie Summentypen in C # so emulieren, wie Sie es gezeigt haben, aber es ist nicht das Schönste.
Martijn

1
Ich denke, es ist weniger "Welches Problem lösen ADTs?" Als vielmehr "ADTs sind eine andere Herangehensweise an Dinge".
MathematicalOrchid

Antworten:


44

Klassen mit Schnittstellen und Vererbung stellen eine offene Welt dar: Jeder kann eine neue Art von Daten hinzufügen. Für eine bestimmte Schnittstelle kann es Klassen geben, die sie weltweit in verschiedenen Dateien, in verschiedenen Projekten und in verschiedenen Unternehmen implementieren. Sie erleichtern das Hinzufügen von Fällen zu den Datenstrukturen, aber da die Implementierungen der Schnittstelle dezentral sind, ist es schwierig, der Schnittstelle eine neue Methode hinzuzufügen. Sobald eine Schnittstelle öffentlich ist, ist sie grundsätzlich eingefroren. Niemand kennt alle möglichen Implementierungen.

Algebraische Datentypen sind das Doppelte, sie sind geschlossen . Alle Fälle der Daten werden an einer Stelle aufgelistet, und Vorgänge können die Varianten nicht nur vollständig auflisten , sie werden auch dazu aufgefordert. Folglich ist das Schreiben einer neuen Funktion, die mit einem algebraischen Datentyp arbeitet, trivial: Schreiben Sie einfach die verdammte Funktion. Im Gegenzug ist das Hinzufügen neuer Fälle kompliziert, da Sie im Grunde die gesamte Codebasis durchgehen und jede erweitern müssen match. Ähnlich wie bei Schnittstellen ist das Hinzufügen einer neuen Variante in der Rust-Standardbibliothek eine grundlegende Änderung (für öffentliche Typen).

Dies sind zwei Seiten des Ausdrucksproblems . Algebraische Datentypen sind für sie eine unvollständige Lösung, OOP jedoch auch. Beide haben Vorteile, je nachdem, wie viele Datenfälle vorhanden sind, wie oft sich diese Fälle ändern und wie oft die Vorgänge erweitert oder geändert werden. (Aus diesem Grund bieten viele moderne Sprachen beides oder etwas Ähnliches an oder setzen direkt auf leistungsfähigere und kompliziertere Mechanismen, die beide Ansätze zu subsumieren versuchen.)


Erhalten Sie eine Compiler-Fehlermeldung, wenn Sie eine Übereinstimmung nicht aktualisieren können, oder nur zur Laufzeit, wenn Sie es herausfinden?
Ian

6
@Ian Die meisten funktionalen Sprachen sind statisch typisiert und prüfen die Vollständigkeit des Mustervergleichs. Wenn es jedoch ein "catch all" -Muster gibt, ist der Compiler glücklich, auch wenn sich die Funktion mit dem neuen Fall befassen müsste, um ihre Arbeit zu erledigen. Außerdem müssen Sie den gesamten abhängigen Code neu kompilieren. Sie können nicht nur eine Bibliothek kompilieren und sie erneut mit einer bereits erstellten Anwendung verknüpfen.

Außerdem ist es im Allgemeinen teuer, statisch zu prüfen, ob ein Muster vollständig ist. Es löst das SAT-Problem im besten Fall und im schlimmsten Fall lässt die Sprache willkürliche Prädikate zu, was dies unentscheidbar macht.
USR

@usr Keine Sprache Mir sind Versuche bekannt, eine perfekte Lösung zu finden. Entweder versteht der Compiler, dass dies erschöpfend ist, oder Sie sind gezwungen, einen Sammelfall hinzuzufügen, in dem Sie abstürzen und sich verbrennen. Ich kenne keine Beziehung zu SAT, haben Sie einen Link zu einer Reduktion? Unabhängig davon ist die Vollständigkeitsprüfung für tatsächlichen Code, der in realen Programmen geschrieben wurde, ein Tropfen auf den heißen Stein.

Stellen Sie sich vor, Sie passen auf N Boolesche Werte. Dann fügen Sie Übereinstimmungsklauseln wie (a, b, _, d, ...) hinzu. Der Sammelfall ist dann! Klausel1 &&! Klausel2 && .... Das sieht für mich nach SAT aus.
USR

12

Fügt die funktionale Programmierung nur eine klarere Syntax hinzu, die diesen Programmierstil weniger grob erscheinen lässt?

Das ist vielleicht eine Vereinfachung, aber ja.

Was fehlt mir noch?

Lassen Sie uns klarstellen, welche algebraischen Datentypen vorliegen (Zusammenfassung dieses feinen Links aus Learn you as Haskell):

  • Ein Summentyp , der besagt, dass "dieser Wert ein A oder ein B sein kann".
  • Ein Produkttyp mit der Aufschrift "Dieser Wert ist sowohl ein A als auch ein B".

Ihr Beispiel funktioniert wirklich nur mit dem ersten.

Was Sie vielleicht vermissen, ist, dass Sie durch die Bereitstellung dieser beiden Grundoperationen mit funktionalen Sprachen alles andere aufbauen können. C # enthält darüber hinaus Strukturen, Klassen, Aufzählungen, Generika und Stapel von Regeln, mit denen das Verhalten dieser Dinge gesteuert werden kann.

In Kombination mit einer gewissen Syntax können die funktionalen Sprachen Operationen auf diese beiden Pfade aufteilen und so einen sauberen, einfachen und eleganten Ansatz für Typen bieten.

Welches Problem lösen algebraische Datentypen?

Sie lösen das gleiche Problem wie jedes andere Typsystem: "Welche Werte sind hier zulässig?" - Sie gehen einfach anders vor.


4
Ihr letzter Satz ist so, als würde man jemandem sagen, dass "Schiffe das gleiche Problem lösen wie Flugzeuge: Transport - sie gehen einfach anders vor". Es ist eine völlig korrekte Aussage und auch eine ziemlich nutzlose.
Mehrdad

@mehrdad - Ich denke, das ist ein bisschen übertrieben.
Telastyn

1
Zweifellos verstehen Sie algebraische Datentypen gut, aber Ihre Aufzählung ("Ein Summentyp ist ..." und "Ein Produkttyp ist ...") sieht eher aus wie eine Beschreibung von Vereinigungs- und Schnittpunkttypen, die es nicht sind Ganz das Gleiche wie Summe und Produkttypen.
Pyon

6

Es kann Sie überraschen, dass der Mustervergleich nicht als die idiomatischste Methode für die Arbeit mit Optionen angesehen wird. Weitere Informationen hierzu finden Sie in der Dokumentation zu Scala Options . Ich bin mir nicht sicher, warum so viele FP-Tutorials diese Verwendung fördern.

Meistens fehlt Ihnen eine ganze Reihe von Funktionen, um die Arbeit mit Optionen zu vereinfachen. Betrachten Sie das Hauptbeispiel aus den Scala-Dokumenten:

val name: Option[String] = request getParameter "name"
val upper = name map { _.trim } filter { _.length != 0 } map { _.toUpperCase }
println(upper getOrElse "")

Beachten Sie, wie mapund filterlassen Sie Operationen an Optionen verketten, ohne an jedem Punkt überprüfen zu müssen, ob Sie dies getan haben Noneoder nicht. Am Ende getOrElsegeben Sie einen Standardwert an. Zu keinem Zeitpunkt machen Sie etwas "Grobes" wie das Prüfen von Typen. Jede unvermeidbare Typprüfung erfolgt bibliotheksintern. Haskell hat seine eigenen analogen Funktionen, ganz zu schweigen von den zahlreichen Funktionen, die auf jeder Monade oder jedem Functor funktionieren.

Andere algebraische Datentypen haben ihre eigene idiomatische Arbeitsweise, und in den meisten Fällen ist die Mustererkennung am Totempfahl gering. Wenn Sie eigene Typen erstellen, müssen Sie ähnliche Funktionen bereitstellen, um mit ihnen arbeiten zu können.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.