Kategorietheorie für Programmierer. An den Fingern

Hallo Kollegen. Die







Entwicklung unserer unflagging Interesse an ernsthaften , könnte man sagen, wissenschaftliche Literatur, haben wir auf die Theorie der Kategorien . Dieses Thema ist in dem berühmten Vortrag von Bartosz Milevsky bereits erschienen auf Habré und jetzt kann es solche Indikatoren rühmt:







Es ist umso angenehmer , dass wir relativ frisches Material (Januar 2020) finden verwaltet, die in der Theorie der Kategorien als ausgezeichnetes und zugleich so kurz wie möglich Einführung dient. Wir hoffen, dass wir Sie für dieses Thema interessieren können.



Wenn Sie und ich, lieber Leser, mit ähnlichen Problemen konfrontiert waren, wurden Sie einmal von der Frage gequält: "Was zum Teufel ist eine Monade ?!" Dann haben Sie diese Frage gegoogelt, sind heimlich durch das Kaninchenloch der abstrakten Mathematik gerutscht und haben sich in Funktoren , Monoiden und Kategorien verwickeltbis sie bemerkten, dass sie bereits vergessen hatten, welche Frage Sie hierher brachte. Diese Erfahrung kann ziemlich überwältigend sein, wenn Sie noch nie funktionierende Programmiersprachen gesehen haben, aber keine Sorge! Ich habe viele Seiten dichter Mathematik für Sie studiert und mir stundenlange Vorträge zu diesem Thema angesehen. Um Ihnen diesen Bedarf zu ersparen, fasse ich das Thema hier zusammen und zeige Ihnen auch, wie Sie die Kategorietheorie anwenden können, damit Sie jetzt in einem funktionalen Stil denken (und Code schreiben) können.



Dieser Artikel richtet sich an alle, die sich als "Neuling" auf dem Gebiet der funktionalen Programmierung betrachten und gerade erst mit Scala, Haskell oder einer anderen ähnlichen Sprache beginnen. Nachdem Sie diesen Artikel gelesen haben, werden Sie sich ein wenig sicherer fühlen, die Grundlagen der Kategorietheorie zu interpretieren und ihre Prinzipien "vor Ort" zu identifizieren. Wenn Sie sich jemals in der theoretischen Mathematik versucht haben, sollten Sie die hier diskutierten Konzepte nicht direkt erwähnen. In der Regel kann über jeden viel mehr gesagt werden, als hier geschrieben steht, aber dieser Artikel wird für einen neugierigen Programmierer ausreichen.



Die Grundlagen



Was genau ist eine Kategorie und in welcher Beziehung steht sie zur Programmierung? Wie viele Konzepte, die in der Programmierung verwendet werden, ist eine Kategorie eine sehr einfache Sache mit einem ausgefallenen Namen. Dies ist nur ein beschrifteter gerichteter Graph mit einigen zusätzlichen Einschränkungen. Jeder der Knoten in einer Kategorie wird als "Objekt" bezeichnet, und jede seiner Kanten wird als "Morphismus" bezeichnet.







Wie Sie vielleicht vermutet haben, ist nicht jeder gerichtete Graph eine Kategorie. Damit ein Diagramm als Kategorie betrachtet werden kann, müssen einige zusätzliche Kriterien erfüllt sein. Im nächsten Bild stellen wir fest, dass jedes Objekt einen Morphismus hat, der auf sich selbst zeigt. Dies ist ein identischer Morphismus, und jedes Objekt muss so sein, dass der Graph als Kategorie betrachtet wird. Beachten Sie als nächstes, dass das Objekt Aeinen Morphismus aufweist, der fanzeigtBund ebenso hat das Objekt Beinen Morphismus, gauf den es zeigt C. Da es ein Weg aus ist Azu Bund von Bzu C, offensichtlich gibt es einen Weg aus Azu C, nicht wahr? Dies ist die nächsten Anforderungskategorien für morphisms notwendigerweise assoziative Zusammensetzung muss durchgeführt werden, so dass für einen Morphismus f: A = > B , und g: B = > Cdort ein Morphismus ist h = g(f): A = > C.



Diese Berechnungen mögen bereits ein wenig abstrakt erscheinen. Schauen wir uns also ein Beispiel an, das dieser Definition entspricht und in Scala geschrieben ist.



trait Rock
trait Sand
trait Glass
def crush(rock: Rock): Sand
def heat(sand: Sand): Glass


Ich denke, dieses Beispiel erleichtert die Beziehung ein wenig. Das Löschen von Steinen in Sand ist ein Morphismus, der ein Objekt rockin ein Objekt verwandelt sand, während das Schmelzen von Glas aus Sand ein Morphismus ist, der ein Objekt sandin ein Objekt verwandelt glass. In diesem Fall wird die Zusammensetzung dieser Beziehungen definitiv so aussehen



val glass: Glass = heat(crush(rock))


Es gibt auch Identitätsfunktionen (in PredefScala definiert ), da es für jedes Objekt nicht schwierig ist, eine Funktion zu schreiben, die dasselbe Objekt zurückgibt. Daher ist dieses System eine Kategorie, wenn auch eine ziemlich einfache.



Vertiefte Vertrautheit mit Kategorien







Wir werden uns nun etwas eingehender mit der Terminologie der Kategorietheorie befassen und mit einer Kategorie namens Magma beginnen. Wenn Sie mit diesem Grundkonzept noch nicht allzu vertraut sind, lassen Sie mich erklären, dass Magma nur eine binäre Operation ist, dh eine Operation mit zwei Werten, durch die ein neuer Wert erhalten wird. Um nicht ins Detail zu gehen, werde ich hier keinen Beweis dafür geben, dass die Menge aller binären Operationen tatsächlich eine Kategorie ist, aber für diejenigen, die an Details interessiert sind, empfehle ich, den folgenden Artikel von Bartosz Milevsky zu lesen. Alle Arithmetik von der Addition bis zur Multiplikation gehört zu den Unterkategorien, die durch die magmatische Kategorie vereint sind (siehe Abbildung).



Hier gilt folgende Vererbungsreihenfolge:



  • 1. Magma: alle binären Operationen
  • 2. Halbgruppen: Alle assoziativen Binäroperationen
  • o : .
  • 3. : ,
  • o : , (aka )


Zurück zu unserem früheren Beispiel: Addition und Multiplikation sind Monoide, da sie assoziativ sind (a + (b + c) = (a + b) + c)und ein einziges Element haben (ax 1 = 1 xa = a). Der letzte Kreis in diesem Diagramm enthält Quasigruppen, für die möglicherweise andere Einbettungsprinzipien gelten als für Halbgruppen oder Monoide. Quasigruppen sind binäre Operationen, die reversibel sind. Diese Eigenschaft zu erklären ist nicht so einfach, deshalb verweise ich Sie auf die Artikelserie von Mark Siman, die sich diesem Thema widmet. Eine binäre Operation ist umkehrbar , wenn für beliebige Werte aund bgibt es solche Werte xund yzulassen , dass die Umwandlung azub... Ich weiß, dass es schwierig klingt. Schauen wir uns zur Verdeutlichung das folgende Beispiel für die Subtraktion an:



val x = a - b
val y = a + b
assert(a - x == b)
assert(y - a == b)


Bitte beachten Sie: Es ynimmt nicht an der Subtraktion als solche teil, aber das Beispiel zählt immer noch. Objekte in einer Kategorie werden abstrahiert und können fast alles sein; In diesem Fall ist es wichtig, dass diese Aussagen für alle aund die b, die generiert werden können, wahr bleiben.



Typen im Kontext



Unabhängig von Ihrer speziellen Disziplin sollte das Thema Typen jedem klar sein, der die Bedeutung von Datentypen in der Programmierung versteht. Ganzzahl, Boolescher Wert, Gleitkomma usw. sind alle Typen, aber wie würden Sie den idealen platonischen Typ in Worten beschreiben? In seinem Buch "Kategorietheorie für Programmierer", das sich in eine Reihe von Blog-Posts verwandelte, beschreibt Milevsky Typen einfach als "Wertesätze". Ein Boolescher Wert ist beispielsweise eine endliche Menge, die die Werte "true" und "false" (false) enthält. Ein Zeichen ist eine endliche Menge aller Zahlenbuchstaben, und eine Zeichenfolge ist eine unendliche Menge von Zeichen.



Das Problem ist, dass wir uns in der Kategorietheorie dazu neigen, uns von Mengen zu entfernen und in Objekten und Morphismen zu denken. Aber die Tatsache, dass Typen einfach Mengen sind, ist unvermeidlich. Glücklicherweise gibt es für diese Mengen einen Platz in der Kategorietheorie, da unsere Objekte Abstraktionen sind und alles darstellen können. Daher haben wir das Recht zu sagen, dass unsere Objekte Mengen sind, und betrachten unsere Scala-Programme weiterhin als Kategorien, in denen Typen Objekte und Funktionen Morphismen sind. Dies mag vielen schmerzlich offensichtlich erscheinen; Schließlich sind wir in Scala daran gewöhnt, mit Objekten umzugehen, aber es lohnt sich, darauf ausdrücklich hinzuweisen.



Wenn Sie mit einer objektorientierten Sprache wie Java gearbeitet haben, sind Sie wahrscheinlich mit dem Konzept der generischen Typen vertraut. Das sind Dinge wieLinkedListoder im Fall von Scala, Option[T]wobei Tder zugrunde liegende Datentyp dargestellt wird, der in einer Struktur gespeichert ist. Was wäre, wenn wir eine Zuordnung von einem Typ zu einem anderen erstellen möchten, damit die Struktur des ersten Typs erhalten bleibt? Willkommen in der Welt der Funktoren, definiert als Zuordnungen zwischen Kategorien, um die Struktur aufrechtzuerhalten. Bei der Programmierung müssen Sie normalerweise mit einer Unterkategorie von Funktoren arbeiten, die als Endofunktoren bezeichnet werden und dabei helfen, eine Kategorie sich selbst zuzuordnen. Also sage ich nur, wenn ich über Funktoren spreche, meine ich Endofunktoren.



Schauen wir uns als Beispiel für einen Funktor den Scala-Typ Option[T]in Verbindung mit unserem vorherigen Beispiel an, in dem Stein, Sand und Glas erwähnt wurden:



val rockOpt: Option[Rock] = Some(rock)


Oben haben wir den Typ, Rockwie wir ihn zuvor definiert haben, aber eingewickelt Option. Dies ist ein generischer Typ (und mehr dazu weiter unten), der uns sagt, dass ein Objekt entweder eine bestimmte Entität ist, nach der wir suchen, oder Nonedie nullin anderen Sprachen verglichen werden kann .



Wenn wir functors nicht verwenden, dann könnten wir uns vorstellen , wie wir eine Funktion anwenden crush()zu Rock, was einem Bediener zurückgreifen würde erfordern , if die Situation zu handhaben, in der es Optionist None.



var sandOpt: Option[Sand] = None
if(rockOpt != None) {
  sandOpt = Some(crush(rockOpt.get))
}


Wir könnten sagen, dass dies ein Nebeneffekt ist, aber bitte verwenden Sie nicht var - ein solcher Code ist in Scala aus mehreren Gründen schlecht. Zurück zum Thema: In Java oder C # wäre dies kein Problem. Sie überprüfen, ob Ihr Wert von dem Typ ist, den Sie erwartet haben, und tun damit, was Sie wollen. Aber mit der Kraft der Funktoren kann mit der Funktion alles etwas eleganter gemacht werden map():



val sandOpt: Option[Sand] = rockOpt.map(crush)


Boom, eine Zeile und du bist fertig. Es wäre möglich, das erste Beispiel mit dem ternären Operator oder ähnlichem in eine Zeile zu setzen, aber es wäre Ihnen nicht so prägnant gelungen. Dieses Beispiel ist in seiner Einfachheit wirklich wunderbar. Hier ist, was hier vor sich geht: Es map()nimmt eine Funktion und ordnet diese Funktion (im mathematischen Sinne) sich selbst zu. Die Struktur bleibt Optionerhalten, enthält aber jetzt entweder Sandoder Noneanstelle von Rockoder None. Dies lässt sich folgendermaßen veranschaulichen:







Beachten Sie, wie schön alles in einer Reihe steht, jedes Objekt und jeder Morphismus in beiden Systemen erhalten bleibt. Folglich ist der Morphismus in der Mitte ein Funktor, der eine Abbildung von Tbis darstellt Option[T].



Alle zusammen



Jetzt können wir endlich zu der ursprünglichen Frage zurückkehren: "Was zum Teufel ist eine Monade?" Es gibt eine Antwort, über die Sie blind stolpern können, wenn Sie nur versuchen, sie zu googeln, und sie klingt wie folgt: Eine Monade ist nur ein Monoid in der Kategorie der Endofunktoren, worauf oft eine sehr sarkastische Bemerkung folgt: "Was ist das Problem?" Auf diese Weise versuchen sie in der Regel scherzhaft zu zeigen, wie schwierig alles in diesem Thema ist, aber tatsächlich ist nicht alles so beängstigend - schließlich haben wir bereits herausgefunden, was dieser Satz bedeutet. Gehen wir noch einmal Schritt für Schritt vor.



Erstens wissen wir, dass Monoide assoziative binäre Operationen sind, von denen jede ein neutrales (einzelnes) Element enthält. Zweitens wissen wir, dass Endofunktoren es uns ermöglichen, eine Kategorie sich selbst zuzuordnen, während die Struktur beibehalten wird. Eine Monade ist also nur eine Art Wrapper-Typ (wie im obigen Beispiel für einen generischen Typ), der eine Methode zum Akzeptieren und Zuordnen einer Funktion zu sich selbst beibehält. List- Dies ist eine Monade Option(wie die, die wir oben betrachtet haben), eine Monade, und jemand kann Ihnen das sogar sagen und Futureist auch eine Monade. Beispiel:



val l: List[Int] = List(1, 2, 3)
def f(i: Int) = List(i, i*2, i*3)
println(l.flatMap(f)) // : List(1, 2, 3, 2, 4, 6, 3, 6, 9)


Täuschend einfach, nicht wahr? Zumindest sollte das Gefühl bestehen, dass es hier nicht schwierig ist, alles zu erfassen, auch wenn auf den ersten Blick nicht ganz klar ist, wie ein solches Konzept verwendet wird.



All Articles