In der Zwischensequenz wird eine Entschlüsselung des Berichts von Stefan Karpinsky, einem der wichtigsten Entwickler der Julia-Sprache, vorgeschlagen. In dem Bericht erörtert er die unerwarteten Ergebnisse des bequemen und effizienten Mehrfachversands, der als Hauptparadigma für Julia angesehen wird.
Von einem Übersetzer : Der Titel des Berichts bezieht sich auf einen Artikel von Eugene Wigner, "Die unverständliche Wirksamkeit der Mathematik in den Naturwissenschaften".
Multiple Dispatch - ein Schlüsselparadigma Julia Sprache, und während seiner Existenz, wir haben die Entwickler der Sprache, beachteten etwas erwartet, aber zugleich rätselhaft. Zumindest haben wir das nicht in dem Maße erwartet, wie wir es gesehen haben. Dies ist etwas - eine erstaunliche Stufe der Wiederverwendung von Code im Julia-Ökosystem, die viel höher ist als in jeder anderen Sprache, die ich kenne.
Wir sehen ständig, dass einige Leute verallgemeinerten Code schreiben, jemand anderes einen neuen Datentyp definiert, diese Leute nicht miteinander vertraut sind und dann jemand diesen Code auf diesen ungewöhnlichen Datentyp anwendet ... Und alles funktioniert einfach. Und das passiert überraschend oft .
Ich habe immer gedacht, dass ein solches Verhalten von objektorientierter Programmierung erwartet werden sollte, aber ich habe viele objektorientierte Sprachen verwendet, und es stellt sich heraus, dass normalerweise einfach nicht alles in ihnen funktioniert. Deshalb dachte ich irgendwann: Warum ist Julia in dieser Hinsicht eine so effektive Sprache? Warum ist der Code-Wiederverwendungsgrad dort so hoch? Und auch - welche Lehren können daraus gezogen werden, die andere Sprachen von Julia ausleihen könnten, um besser zu werden?
Manchmal, wenn ich sage, dass die Öffentlichkeit mich nicht glauben, aber man muss JuliaCon, so dass Sie wissen, was geschieht, so dass ich auf, warum konzentrieren werde, meiner Meinung nach, dies geschieht.
Aber für den Anfang - eines meiner Lieblingsbeispiele.

Auf der Folie ist das Ergebnis der Arbeit von Chris Rakaukas. Er schreibt alle möglichen sehr verallgemeinerten Pakete zur Lösung von Differentialgleichungen. Sie können zwei Zahlen oder BigFloat füttern, was immer Sie wollen. Und irgendwie hat er beschlossen, dass er den Fehler des Integrationsergebnisses sehen will. Und es gab ein Messpaket, das sowohl den Wert einer physikalischen Größe als auch die Ausbreitung eines Fehlers durch eine Folge von Formeln verfolgen kann. Dieses Paket unterstützt auch die elegante Syntax für Unsicherheitswerte mit dem Unicode-Zeichen ±
. Hier auf dem Objektträger wird gezeigt, dass die Erdbeschleunigung, die Länge des Pendels, die Anfangsgeschwindigkeit und der Abweichungswinkel alle mit einer Art Fehler bekannt sind. Sie definieren also ein einfaches Pendel, führen seine Bewegungsgleichungen durch den Löser ODE und - bam! - Alles funktioniert . Und Sie sehen eine Grafik mit Schnurrbart-Ungenauigkeiten. Und ich zeige noch nicht den Code Grafiken zum Zeichnen - zu allgemein und Sie nur mit einem Fehler von Measurements.jl auf den Wert starten und eine Grafik mit den Fehlern bekommen.
Der Grad der Kompatibilität verschiedener Pakete und die Verallgemeinerung des Codes sind einfach hirnhaltig. Wie funktioniert es einfach ? Es stellt sich ja heraus.
Nun, nicht, dass wir das überhaupt nicht erwartet hätten. Schließlich haben wir das Konzept des Mehrfachversands genau deshalb in die Sprache aufgenommen , weil es uns ermöglicht, verallgemeinerte Algorithmen auszudrücken. Das alles ist also nicht so verrückt. Aber es ist eine Sache, dies theoretisch zu wissen und eine andere, in der Praxis zu sehen, dass der Ansatz wirklich funktioniert. Schließlich sollten auch Single Dispatching und Operator Overloading in C ++ zu einem ähnlichen Ergebnis führen - aber in Wirklichkeit funktionieren sie oft nicht so, wie sie es möchten.
Darüber hinaus erleben wir etwas mehr als wir bei der Entwicklung der Sprache vorausgesehen hatten: Es wird nicht nur verallgemeinerter Code geschrieben. Als nächstes werde ich versuchen zu sagen, was meiner Meinung nach mehr ist.
So gibt es zwei Arten von Wiederverwendung von Code, und sie sind ganz anders. Einer ist verallgemeinerte Algorithmen, und dies ist das erste, woran sie sich erinnern. Der zweite, weniger offensichtliche, aber wichtigere Aspekt scheint die Einfachheit zu sein, mit der Julia dieselben Datentypen in einer Vielzahl von Paketen verwendet. In gewissem Maße geschieht dies, weil Typmethoden kein Hindernis für ihre Verwendung darstellen: Sie müssen sich nicht mit dem Typautor über die von ihm geerbten Schnittstellen und Methoden einig sein. Sie können einfach sagen: „Oh, ich wie dieser RGB Art von Operationen darauf, den ich mit ihren eigenen kommen, aber die Struktur ist mir angenehm.“.
Vorwort Mehrfachplanung versus Funktionsüberlastung
Jetzt muss ich die Funktionsüberladung in C ++ oder Java erwähnen, da mir ständig Fragen dazu gestellt werden. Auf den ersten Blick unterscheidet es sich nicht von der Mehrfachplanung. Was ist der Unterschied und warum ist die Funktionsüberlastung schlimmer?
Ich beginne mit einem Beispiel für Julia:
abstract type Pet end struct Dog <: Pet; name::String end struct Cat <: Pet; name::String end function encounter(a::Pet, b::Pet) verb = meets(a, b) println("$(a.name) meets $(b.name) and $verb") end meets(a::Dog, b::Dog) = "sniffs" meets(a::Dog, b::Cat) = "chases" meets(a::Cat, b::Dog) = "hisses" meets(a::Cat, b::Cat) = "slinks"
:: Pet) abstract type Pet end struct Dog <: Pet; name::String end struct Cat <: Pet; name::String end function encounter(a::Pet, b::Pet) verb = meets(a, b) println("$(a.name) meets $(b.name) and $verb") end meets(a::Dog, b::Dog) = "sniffs" meets(a::Dog, b::Cat) = "chases" meets(a::Cat, b::Dog) = "hisses" meets(a::Cat, b::Cat) = "slinks"
Wir definieren den abstrakten Typ von Pet
, führen die Untertypen von Dog
und Cat
, sie haben ein Namensfeld (der Code wiederholt sich ein wenig, ist aber tolerierbar) und definieren eine verallgemeinerte Funktion des "Treffens", die zwei Objekte vom Typ Pet
Argumenten akzeptiert. Darin berechnen wir zuerst die „Aktion“, die durch das Ergebnis des Aufrufs der generalisierten Funktion meet()
wird, und drucken dann den Satz aus, der die Besprechung beschreibt. Die Funktion meets()
, wir mehrere Versand verwenden , um die Aktion zu bestimmen , die ein Tier bei einem Treffen mit dem anderen macht.
Fügen Sie ein paar Hunde und ein paar Katzen hinzu und sehen Sie sich die Ergebnisse des Treffens an:
fido = Dog("Fido") rex = Dog("Rex") whiskers = Cat("Whiskers") spots = Cat("Spots") encounter(fido, rex) encounter(rex, whiskers) encounter(spots, fido) encounter(whiskers, spots)
Jetzt werden wir dasselbe so wörtlich wie möglich in C ++ "übersetzen". Definieren Sie die Pet
Klasse mit dem Namensfeld - in C ++ können wir dies tun (einer der Vorteile von C ++ besteht übrigens darin, dass Datenfelder sogar zu abstrakten Typen hinzugefügt werden können. Dann definieren wir die Funktion base meets()
, definieren die Funktion meets()
für zwei Objekte vom Typ Pet
und, Definieren Sie abschließend die abgeleiteten Klassen Dog
und Cat
und führen Sie für sie Überlastungs- meets()
durch:
class Pet { public: string name; }; string meets(Pet a, Pet b) { return "FALLBACK"; } void encounter(Pet a, Pet b) { string verb = meets(a, b); cout << a.name << " meets " << b. name << " and " << verb << endl; } class Cat : public Pet {}; class Dog : public Pet {}; string meets(Dog a, Dog b) { return "sniffs"; } string meets(Dog a, Cat b) { return "chases"; } string meets(Cat a, Dog b) { return "hisses"; } string meets(Cat a, Cat b) { return "slinks"; }
Die Funktion main()
, wie in dem Code zu Julia, der Schaffung Hunde und Katzen und macht sie zu treffen:
int main() { Dog fido; fido.name = "Fido"; Dog rex; rex.name = "Rex"; Cat whiskers; whiskers.name = "Whiskers"; Cat spots; spots.name = "Spots"; encounter(fido, rex); encounter(rex, whiskers); encounter(spots, fido); encounter(whiskers, spots); return 0; }
Also Mehrfachversand gegen Funktionsüberlastung. Gong!

Was wird Ihrer Meinung nach den Code mit Mehrfachversand zurückgeben?
$ Julia Haustiere.jl Fido meets Rex and sniffs Rex meets Whiskers and chases Spots meets Fido and hisses Whiskers meets Spots and slinks
Die Tiere treffen sich, schnüffeln, zischen und spielen Aufholjagd - wie beabsichtigt.
$ g ++ -o Haustiere Haustiere.cpp && ./ Haustiere Fido meets Rex and FALLBACK Rex meets Whiskers and FALLBACK Spots meets Fido and FALLBACK Whiskers meets Spots and FALLBACK
In allen Fällen wird die Option "Fallback" zurückgegeben.
Warum? Denn so funktioniert Funktionsüberladung. Wenn mehrere Dispatchings funktionieren würden, würden meets(a, b)
innerhalb von encounter()
mit den spezifischen Typen aufgerufen, die a
und b
zum Zeitpunkt des Anrufs hatten. Da jedoch eine Überladung angewendet wird, wird meets()
für die statischen Typen a
und b
aufgerufen, die in diesem Fall beide Pet
.
Beim C ++ - Ansatz ergibt die direkte "Übersetzung" von generischem Julia-Code nicht das gewünschte Verhalten, da der Compiler Typen verwendet, die in der Kompilierungsphase statisch abgeleitet werden. Und der springende Punkt ist, dass wir eine Funktion auf der Grundlage des tatsächlichen konkreten Typs nennen wollen, dass die Variablen in der Runtime sind. Obwohl Vorlagenfunktionen die Situation etwas verbessern, erfordern sie zum Zeitpunkt der Kompilierung statische Kenntnisse aller im Ausdruck enthaltenen Typen, und es ist einfach, ein Beispiel zu finden, bei dem dies unmöglich wäre.
Für mich zeigen solche Beispiele, dass Mehrfachversand das Richtige tut, und alle anderen Ansätze sind einfach keine sehr gute Annäherung an das richtige Ergebnis.
Nun sehen wir uns eine solche Tabelle an. Ich hoffe, Sie finden es sinnvoll:
In Sprachen ohne Versand schreiben Sie einfach f(x, y, ...)
, die Typen aller Argumente sind festgelegt, d. H. Ein Aufruf von f()
ist ein Aufruf einer einzelnen Funktion f()
, die sich im Programm befinden kann. Der Grad der Ausdruckskraft ist konstant: Der Aufruf von f()
immer nur eine Sache. Der Einzelversand war ein wichtiger Durchbruch beim Übergang zu OOP in den 1990er und 2000er Jahren. Normalerweise wird die Punktsyntax verwendet, die die Leute wirklich mögen. Und es erscheint eine zusätzliche Ausdrucksmöglichkeit: Der Anruf wird entsprechend dem Objekttyp x 1 abgesetzt. Eine Ausdrucksmöglichkeit ist durch die Kraft der Menge | X 1 | gekennzeichnet Typen mit der Methode f()
. In der gleichen Scheduling mehrere Anzahl potentieller Ausführungsform für die Funktion f()
ist gleich die Leistung des kartesischen Produkts von Sätzen Typen , auf die Argumente gehören. In der Realität braucht natürlich kaum jemand so viele verschiedene Funktionen in einem Programm. Der entscheidende Punkt hierbei ist jedoch, dass der Programmierer eine einfache und natürliche Möglichkeit erhält, jedes Element dieser Art zu verwenden, was zu einem exponentiellen Wachstum der Möglichkeiten führt.
Teil 1. Allgemeine Programmierung
Lassen Sie uns über verallgemeinerten Code sprechen - das Hauptmerkmal des Mehrfachversands.
Hier ist ein (völlig künstliches) Beispiel für generischen Code:
using LinearAlgebra function inner_sum(A, vs) t = zero(eltype(A)) for v in vs t += inner(v, A, v)
Hier ist A
etwas Matrixartiges (obwohl ich die Typen nicht angegeben habe und ich kann etwas anhand des Namens erraten), vs
ist der Vektor einiger vektorähnlicher Elemente, und dann wird das Skalarprodukt durch diese "Matrix" betrachtet. für die eine verallgemeinerte Definition ohne Angabe von Typen angegeben wird. Die verallgemeinerte Programmierung besteht hier in diesem Aufruf der Funktion inner()
in einer Schleife (professioneller Rat: Wenn Sie verallgemeinerten Code schreiben möchten, entfernen Sie einfach alle Typbeschränkungen).
Also, "schau, Mama, es funktioniert":
julia> A = rand(3, 3) 3×3 Array{Float64,2}: 0.934255 0.712883 0.734033 0.145575 0.148775 0.131786 0.631839 0.688701 0.632088 julia> vs = [rand(3) for _ in 1:4] 4-element Array{Array{Float64,1},1}: [0.424535, 0.536761, 0.854301] [0.715483, 0.986452, 0.82681] [0.487955, 0.43354, 0.634452] [0.100029, 0.448316, 0.603441] julia> inner_sum(A, vs) 6.825340887556694
- julia> A = rand(3, 3) 3×3 Array{Float64,2}: 0.934255 0.712883 0.734033 0.145575 0.148775 0.131786 0.631839 0.688701 0.632088 julia> vs = [rand(3) for _ in 1:4] 4-element Array{Array{Float64,1},1}: [0.424535, 0.536761, 0.854301] [0.715483, 0.986452, 0.82681] [0.487955, 0.43354, 0.634452] [0.100029, 0.448316, 0.603441] julia> inner_sum(A, vs) 6.825340887556694
, julia> A = rand(3, 3) 3×3 Array{Float64,2}: 0.934255 0.712883 0.734033 0.145575 0.148775 0.131786 0.631839 0.688701 0.632088 julia> vs = [rand(3) for _ in 1:4] 4-element Array{Array{Float64,1},1}: [0.424535, 0.536761, 0.854301] [0.715483, 0.986452, 0.82681] [0.487955, 0.43354, 0.634452] [0.100029, 0.448316, 0.603441] julia> inner_sum(A, vs) 6.825340887556694
Nichts Besonderes, es berechnet einen Wert. Aber - der Code ist in einem verallgemeinerten Stil geschrieben und funktioniert für jedes A
und vs
, wenn es nur möglich wäre, die entsprechenden Operationen an ihnen auszuführen.
Was die Effizienz bei bestimmten Datentypen betrifft - wie viel Glück. Ich meine, dass dieser Code für dichte Vektoren und Matrizen "so macht, wie er sollte" - er erzeugt Maschinencode mit Aufruf von BLAS-Operationen usw. usw. Wenn Sie statische Arrays übergeben, berücksichtigt der Compiler dies, erweitert die Zyklen und wendet die Vektorisierung an - alles ist so, wie es sollte.
Aber was noch wichtiger ist, der Code funktioniert für neue Typen, und Sie können ihn nicht nur super effizient, sondern auch super effizient machen! Definieren wir einen neuen Typ (dies ist der reale Datentyp, der beim maschinellen Lernen verwendet wird), einen einheitlichen Vektor (One-Hot-Vektor). Dies ist ein Vektor, in dem eine der Komponenten 1 ist und alle anderen Null sind. Sie können es sich sehr kompakt vorstellen: Alles, was gespeichert werden muss, ist die Länge des Vektors und die Nummer der Nicht-Null-Komponente.
import Base: size, getindex, * struct OneHotVector <: AbstractVector{Int} len :: Int ind :: Int end size(v::OneHotVector) = (v.len,) getindex(v::OneHotVector, i::Integer) = Int(i == v.ind)
Tatsächlich ist dies die gesamte Typdefinition aus dem Paket, das sie hinzufügt. Und mit dieser Definition inner_sum()
auch:
julia> vs = [OneHotVector(3, rand(1:3)) for _ in 1:4] 4-element Array{OneHotVector,1}: [0, 1, 0] [0, 0, 1] [1, 0, 0] [1, 0, 0] julia> inner_sum(A, vs) 2.6493739294755123
Für ein skalares Produkt wird hier jedoch eine allgemeine Definition verwendet - für diese Art von Daten ist es langsam, nicht cool!
Allgemeine Definitionen funktionieren also, aber nicht immer optimal, und bei der Verwendung von Julia kann dies gelegentlich auftreten: "Nun, eine allgemeine Definition wird aufgerufen, deshalb funktioniert dieser GPU-Code seit der fünften Stunde ..."
In inner()
wird standardmäßig die allgemeine Definition des Matrixprodukts durch einen Vektor aufgerufen, die bei Multiplikation mit einem einheitlichen Vektor eine Kopie einer der Spalten vom Typ Vector{Float64}
. Dann rief er eine allgemeine Definition des Skalarprodukts dot()
mit einem einheitlichen Vektor und dieser Spalte, die eine Menge unnötiger Arbeit macht. Tatsächlich wird für jede Komponente geprüft: "Sind Sie gleich eins? Und Sie?" usw.
Wir können dieses Verfahren stark optimieren. Ersetzen Sie beispielsweise die Matrixmultiplikation durch OneHotVector
, indem OneHotVector
einfach eine Spalte auswählen. Gut, definieren Sie diese Methode und fertig.
*(A::AbstractMatrix, v::OneHotVector) = A[:, v.ind]
Und hier ist es, Macht : Wir sagen "wir wollen auf das zweite Argument loslegen ", egal was im ersten ist. Eine solche Definition zieht einfach die Zeile aus der Matrix und ist viel schneller als die allgemeine Methode - Iteration und Summierung über Spalten werden entfernt.
Sie können jedoch noch weiter gehen und inner()
direkt optimieren, da durch Multiplizieren zweier einheitlicher Vektoren durch eine Matrix einfach ein Element dieser Matrix herausgezogen wird:
inner(v::OneHotVector, A, w::OneHotVector) = A[v.ind, w.ind]
Das ist die versprochene Super-Duper-Effizienz. Und alles, was benötigt wird, ist diese inner()
Methode zu definieren.
Dieses Beispiel zeigt eine der Anwendungen der Mehrfachplanung: Es gibt eine allgemeine Definition einer Funktion, die jedoch für einige Datentypen nicht optimal funktioniert. Und dann fügen wir punktuell eine Methode hinzu, die das Funktionsverhalten für diese Typen beibehält, aber viel effizienter arbeitet .
Aber es gibt noch einen anderen Bereich - wenn es keine allgemeine Definition einer Funktion gibt, ich aber für einige Typen Funktionen hinzufügen möchte. Dann können Sie es mit minimalem Aufwand hinzufügen.
Und die dritte Option - Sie möchten nur den gleichen Funktionsnamen, aber mit unterschiedlichem Verhalten für verschiedene Datentypen -, zum Beispiel, damit sich eine Funktion beim Arbeiten mit Wörterbüchern und Arrays unterschiedlich verhält.
Wie kann man ein ähnliches Verhalten in einzelnen Versandsprachen erzielen? Es ist möglich, aber schwierig. Problem: Beim Überladen der Funktion *
das zweite Argument und nicht das erste Argument ausgelöst werden. Sie können eine Doppel - Ausgabe machen: zuerst dispetcherizuem das erste Argument und rufen Sie die Methode AbstractMatrix.*(v)
. Und diese Methode ruft wiederum so etwas wie v.__rmul__(A)
, d.h. Das zweite Argument im ursprünglichen Aufruf ist nun das Objekt geworden, dessen Methode tatsächlich aufgerufen wird. __rmul__
hier wird aus dem Python genommen, wo ein solches Verhalten - ein Standardmuster, aber es funktioniert, wie es scheint, nur für die Addition und Multiplikation. Das heißt, Das Problem des doppelten Versands ist gelöst, wenn wir eine Funktion namens +
oder *
aufrufen möchten, andernfalls leider nicht unseren Tag. In C ++ und anderen Sprachen müssen Sie Ihr Fahrrad bauen.
OK, was ist mit inner()
? Jetzt gibt es drei Argumente, und der Versand erfolgt über das erste und dritte. Was in Sprachen mit Einzelversand zu tun ist, ist nicht klar. „Triple Scheduling“ Ich lebe noch nie getroffen. Es gibt keine guten Lösungen. Wenn ein ähnlicher Bedarf auftritt (und in numerischen Codes sehr häufig vorkommt), implementieren die Benutzer normalerweise ihr Mehrfachversandsystem. Wenn Sie sich große Projekte für numerische Berechnungen in Python ansehen, werden Sie erstaunt sein, wie viele davon in diese Richtung gehen. Natürlich funktionieren solche Implementierungen situativ, schlecht designt, voller Fehler und langsam ( Verweis auf Greenspans zehnte Regel - ca. übersetzt ), da Jeff Besancon an keinem dieser Projekte gearbeitet hat (der Autor und Hauptentwickler des Typversandsystems in Julia - ca.. Perevi.).
Teil 2. Allgemeine Typen
Ich werde auf die Kehrseite des Julia-Paradigmas eingehen - die allgemeinen Typen. Dies ist meiner Meinung nach das wichtigste "Arbeitstier" der Sprache, da ich in diesem Bereich einen hohen Grad an Wiederverwendung von Code beobachte.
Angenommen, Sie eine Art von RGB haben, wie dieser in ColorTypes.jl verfügbar. Es ist nichts Kompliziertes daran, nur drei Werte werden zusammengeführt. Der Einfachheit halber nehmen wir an, dass der Typ nicht parametrisch ist (aber hätte sein können), und der Autor hat für ihn mehrere grundlegende Operationen definiert, die er für nützlich hielt. Sie nehmen diesen Typ und denken: "Hmm, ich möchte weitere Operationen für diesen Typ hinzufügen." Stellen Sie sich beispielsweise RGB als einen Vektorraum vor (was streng genommen falsch ist, aber auf eine erste Annäherung hinausläuft). In Julia nehmen Sie einfach alle fehlenden Operationen und fügen sie in Ihren Code ein.
Stellt sich die Frage - und Cho? Warum konzentriere ich mich so sehr darauf? Es stellt sich heraus, dass ein solcher Ansatz in objektorientierten Sprachen, die auf Klassen basieren, überraschend schwierig zu implementieren ist. Da sich die Methodendefinitionen in diesen Sprachen innerhalb der Klassendefinition befinden, gibt es nur zwei Möglichkeiten, eine Methode hinzuzufügen: Bearbeiten Sie entweder den Klassencode, um das gewünschte Verhalten hinzuzufügen, oder erstellen Sie eine Vererbungsklasse mit den erforderlichen Methoden.
Die erste Option erhöht die Definition der Basisklasse und zwingt den Entwickler der Basisklasse, sich beim Ändern des Codes um die Unterstützung aller hinzugefügten Methoden zu kümmern. Was könnte eines Tages dazu führen, dass eine solche Klasse nicht mehr unterstützt wird?
Vererbung ist eine klassische „empfohlene“ Option, aber auch nicht ohne Mängel. Zuerst müssen Sie den Klassennamen ändern - lassen Sie es jetzt nicht RGB
, sondern MyRGB
. Darüber hinaus funktionieren neue Methoden für die ursprüngliche RGB
Klasse nicht mehr. Wenn ich meine neue Methode auf ein RGB
Objekt anwenden möchte, das im Code einer anderen Person erstellt wurde, muss ich es konvertieren oder in MyRGB
. Aber das ist nicht das Schlimmste. Wenn ich eine MyRGB
Klasse mit zusätzlichen Funktionen erstellt habe, eine andere OurRGB
Klasse usw. - Wenn jemand eine Klasse mit allen neuen Funktionen haben möchte, müssen Sie die Mehrfachvererbung verwenden (und dies nur, wenn die Programmiersprache dies überhaupt zulässt!).
Also, beide Optionen sind so lala. Es gibt jedoch andere Lösungen:
- Fügen Sie die Funktion in eine externe Funktion anstelle der Klassenmethode ein - gehen Sie zu
f(x, y)
anstelle von xf(y)
. Aber dann geht verallgemeinertes Verhalten verloren. - Spit auf der Wiederverwendung von Code (und, wie ich glaube, in vielen Fällen ist es passiert). Kopieren Sie sich einfach eine außerirdische
RGB
Klasse und fügen Sie hinzu, was fehlt.
Das Hauptmerkmal von Julia bei der Wiederverwendung von Code ist fast vollständig auf die Tatsache reduziert, dass die Methode außerhalb des Typs definiert ist . Das ist alles. Machen Sie dasselbe in Einzelversand-Sprachen - und Typen können mit der gleichen Leichtigkeit wiederverwendet werden. Die ganze Geschichte mit „Machen wir Methoden zu einem Teil der Klasse“ ist in der Tat eine mittelmäßige Idee. Es stimmt, es gibt einen guten Punkt - die Verwendung von Klassen als Namespaces. Wenn ich schreibe, dass xf(y)
- f()
nicht im aktuellen Namespace sein muss, muss es im Namespace x
gesucht werden. Ja, es ist eine gute Sache - aber ist es all die anderen Probleme wert? Weiß nicht. Meiner Meinung nach nein (obwohl meine Meinung, wie Sie vielleicht vermuten, leicht voreingenommen ist).
Nachwort. Das Problem des Ausdrucks
Es gibt ein solches Programmierproblem, das in den 70er Jahren festgestellt wurde. Es hängt weitgehend mit der statischen Typprüfung zusammen, da es in solchen Sprachen vorkommt. Es stimmt, ich denke, dass es nichts mit statischer Typprüfung zu tun hat. Das Wesentliche des Problems ist das Folgende: Ist es möglich, das Datenmodell und den Satz von Operationen an den Daten gleichzeitig zu ändern, ohne auf zweifelhafte Techniken zurückzugreifen.
Das Problem kann mehr oder weniger auf Folgendes reduziert werden:
- ist es möglich, einfach und fehlerfrei neue Datentypen hinzuzufügen , auf die vorhandene Methoden anwendbar sind und
- Ist es möglich, neue Operationen für vorhandene Typen hinzuzufügen ?
(1) kann leicht in objektorientierten Sprachen durchgeführt werden, und es ist schwierig, Funktion, (2) - im Gegenteil. In diesem Sinne können wir nur über den Dualismus von OOP- und FP-Ansätzen sprechen.
In Multi-Dispatch-Sprachen sind beide Vorgänge einfach. (1) , (2) — . , . ( https://en.wikipedia.org/wiki/Expression_problem ), . ? , , . , " , " — " " . " , " , , .
, . , , — .
, Julia ( ), . .