Julia: Alternative Syntax für `map(func, x)`

Erstellt am 23. Sept. 2014  ·  283Kommentare  ·  Quelle: JuliaLang/julia

Dies wurde hier ausführlich diskutiert. Ich hatte Probleme, es zu finden, und dachte, es hätte eine eigene Ausgabe verdient.

breaking speculative

Hilfreichster Kommentar

Sobald ein Mitglied des Triumvirats (Stefan, Jeff, Viral) #15032 zusammenführt (was meiner Meinung nach bereit für die Zusammenführung ist), werde ich dies schließen und ein Roadmap-Problem einreichen, um die verbleibenden vorgeschlagenen Änderungen zu skizzieren: Broadcast-Typ-Berechnung korrigieren, veraltet @vectorize , verwandeln Sie .op in Broadcast-Zucker, fügen Sie „Broadcast-Fusion“ auf Syntaxebene hinzu und verschmelzen Sie schließlich mit der direkten Zuweisung. Die letzten beiden werden es wahrscheinlich nicht in 0,5 schaffen.

Alle 283 Kommentare

+1

Oder func.(args...) als syntaktischer Zucker für

broadcast(func, args...)

Aber vielleicht bin ich der Einzige, dem das lieber wäre?
So oder so, +1.

:-1: Wenn überhaupt, denke ich, dass Stefans anderer Vorschlag von f[...] eine schöne Ähnlichkeit mit Verständnis hat.

Wie @ihnorton bin auch ich von dieser Idee nicht besonders angetan. Insbesondere mag ich die Asymmetrie nicht, a .+ b und sin.(a) zu haben.

Vielleicht brauchen wir keine spezielle Syntax. Mit #1470 könnten wir so etwas machen

call(f::Callable,x::AbstractArray) = applicable(f,x) ? apply(f,x) : map(f,x)

rechts? Vielleicht wäre dies jedoch zu magisch, um eine automatische Zuordnung für eine Funktion zu erhalten.

@quinnj Diese eine Zeile fasst meine größten Befürchtungen zusammen, eine Anrufüberlastung zuzulassen. Ich werde tagelang nicht schlafen können.

Ich bin mir noch nicht sicher, ob es syntaktisch möglich ist, aber was ist mit .sin(x) ? Ist das eher a .+ b ähnlich?

Ich denke, [] wird viel zu überladen und wird für diesen Zweck nicht funktionieren. Beispielsweise können wir wahrscheinlich Int(x) schreiben, aber Int[x] konstruiert ein Array und kann daher nicht map bedeuten.

Ich wäre mit .sin(x) an Bord.

Dafür müssten wir etwas Syntax zurückholen, aber wenn Int(x) die skalare Version ist, dann ist Int[x] analog sinnvoll, um einen Vektor des Elementtyps Int zu konstruieren. Meiner Meinung nach ist die Möglichkeit, diese Syntax kohärenter zu machen, tatsächlich einer der ansprechendsten Aspekte des f[v] -Vorschlags.

Wie wird durch die f[v] -Syntax für map die Syntax kohärenter? Ich verstehe nicht. map hat eine andere „Form“ als die aktuelle T[...] -Array-Konstruktor-Syntax. Was ist mit Vector{Int}[...] ? Würde das nicht funktionieren?

lol, Entschuldigung für den Schrecken @JeffBezanson! Haha, die Anrufüberlastung ist definitiv ein wenig beängstigend, hin und wieder denke ich an die Arten von Code-Verschleierung, die Sie in julia und mit call machen können, Sie könnten einige knorrige Sachen machen.

Ich denke, .sin(x) klingt auch nach einer guten Idee. Gab es einen Konsens darüber, was mit Multi-Argumenten zu tun ist?

:-1:. Das Einsparen von ein paar Zeichen im Vergleich zur Verwendung von Funktionen höherer Ordnung ist meiner Meinung nach die Kosten für die Lesbarkeit nicht wert. Können Sie sich eine Datei vorstellen, in der überall .func() / func.() und func() eingestreut sind?

Es scheint wahrscheinlich, dass wir zumindest die Syntax a.(b) entfernen werden.

Wow, sprechen Sie über das Aufrühren eines Bienennests! Ich habe den Namen geändert, um die Diskussion besser widerzuspiegeln.

Wir könnten auch das 2-Argument map in zipWith umbenennen :)

Wenn eine Syntax wirklich notwendig ist, wie wäre es dann mit [f <- b] oder einem anderen Wortspiel über das Verständnis _innerhalb_ der Klammern?

( @JeffBezanson Sie haben nur Angst, dass jemand CJOS oder Moose.jl schreiben wird :) ... wenn wir diese Funktion bekommen, fügen Sie sie einfach in den Don't do stupid stuff: I won't optimize that -Abschnitt des Handbuchs ein)

Wenn Sie derzeit Int[...] schreiben, bedeutet dies, dass Sie ein Array des Elementtyps Int . Aber wenn Int(x) bedeutet, x in Int umzuwandeln, indem man Int als Funktion anwendet, dann könnte man auch Int[...] als „ Int anwenden“ betrachten Int erzeugt. Das Schreiben Int[v] würde also [ Int(x) for x in v ] entsprechen und Int[ f(x) for x in v ] würde [ Int(f(x)) for x in v ] entsprechen. Natürlich haben Sie dann etwas von der Nützlichkeit verloren, Int[ f(x) for x in v ] überhaupt zu schreiben – dh dass wir statisch wissen können, dass der Elementtyp Int ist – aber wenn Sie das Int(x) erzwingen Int erzeugen (keine unvernünftige Einschränkung), dann könnten wir diese Eigenschaft wiederherstellen.

Scheint mir mehr Vektorisierung / implizite Katzenhölle zu sein. Was würde Int[x, y] tun? Oder noch schlimmer, Vector{Int}[x] ?

Ich sage nicht, dass es die beste Idee aller Zeiten ist oder befürworte es sogar – ich weise nur darauf hin, dass es nicht _vollständig_ mit der bestehenden Verwendung kollidiert, die selbst ein bisschen wie ein Hack ist. Wenn wir die bestehende Nutzung in ein kohärenteres Muster integrieren könnten, wäre das ein Gewinn. Ich bin mir nicht sicher, was f[v,w] bedeuten würde – die offensichtlichen Möglichkeiten sind [ f(x,y) for x in v, y in w ] oder map(f,v,w) , aber es gibt noch mehr Möglichkeiten.

Ich habe das Gefühl, dass a.(b) kaum verwendet wird. Habe einen Schnelltest durchgeführt und es wird nur in 54 der ~4.000 Julia-Quelldateien in freier Wildbahn verwendet: https://gist.github.com/jakebolewski/104458397f2e97a3d57d.

Ich finde das kollidiert total. T[x] hat die "Form" T --> Array{T} , während map die Form Array{T} --> Array{S} hat. Die sind ziemlich inkompatibel.

Dazu müssen wir meiner Meinung nach T[x,y,z] als Konstruktor für Vector{T} aufgeben. Einfache alte Array-Indizierung, A[I] wobei I ein Vektor ist, kann als map(i->A[i], I) gesehen werden. Das "Anwenden" eines Arrays ist wie das Anwenden einer Funktion (natürlich verwendet Matlab sogar dieselbe Syntax für sie). In diesem Sinne funktioniert die Syntax wirklich, aber wir würden dabei die typisierte Vektorsyntax verlieren.

Ich habe irgendwie das Gefühl, dass die Debatte über die Syntax hier von der wichtigeren Änderung ablenkt: map schnell zu machen.

Offensichtlich ist es wichtiger, map schnell zu machen (was übrigens Teil einer ziemlich gründlichen Neugestaltung des Funktionsbegriffs in julia sein muss). Der Wechsel von sin(x) zu map(sin, x) ist jedoch aus Sicht der Benutzerfreundlichkeit sehr wichtig, daher ist die Syntax sehr wichtig, um die Vektorisierung wirklich zu beenden.

Der Wechsel von sin(x) zu map(sin, x) ist jedoch aus Sicht der Benutzerfreundlichkeit sehr wichtig, sodass die Syntax sehr wichtig ist, um die Vektorisierung wirklich zu beenden.

Völlig einverstanden.

Ich stimme @JeffBezanson zu, dass f[x] mit den aktuellen typisierten Array-Konstruktionen Int[x, y] usw. ziemlich unvereinbar ist.

Ein weiterer Grund, .sin gegenüber sin. zu bevorzugen, besteht darin, endlich zuzulassen, dass beispielsweise Base.(+) verwendet wird, um auf die Funktion + in Base zuzugreifen (einmal a.(b) wird entfernt).

Wenn ein Modul seine eigene sin (oder was auch immer) Funktion definiert und wir diese Funktion auf einen Vektor anwenden wollen, machen wir dann Module..sin(v) ? Module.(.sin(v)) ? Module.(.sin)(v) ? .Module.sin(v) ?

Keine dieser Optionen scheint mehr wirklich gut zu sein.

Ich habe das Gefühl, dass diese Diskussion am Kern des Problems vorbeigeht. Das heißt: Wenn ich einzelne Argumentfunktionen auf Container abbilde, habe ich das Gefühl, dass die map(func, container) -Syntax _bereits_ klar und prägnant ist. Stattdessen denke ich, dass wir nur dann von einer besseren Syntax für das Curry profitieren könnten, wenn es um mehrere Argumente geht.

Nehmen Sie zum Beispiel die Ausführlichkeit von map(x->func(x,other,args), container) oder verketten Sie eine Filteroperation, um sie noch schlimmer zu machen, filter(x->func2(x[1]) == val, map(x->func1(x,other,args), container)) .

In diesen Fällen würde eine verkürzte Kartensyntax meiner Meinung nach nicht viel helfen. Nicht, dass ich diese besonders schlecht finde, aber a) ich glaube nicht, dass eine Abkürzung map viel helfen würde, und b) ich liebe es, nach einigen von Haskells Syntax zu schmachten. ;)

IIRC, in Haskell kann das obige geschrieben werden filter ((==val) . func2 . fst) $ map (func1 other args) container mit einer leichten Änderung in der Reihenfolge der Argumente zu func1 .

In elm wird .func durch x->x.func definiert und das ist sehr nützlich, siehe elm records . Dies sollte berücksichtigt werden, bevor diese Syntax für map verwendet wird.

Ich mag es.

Obwohl der Feldzugriff in Julia keine so große Rolle spielt wie in vielen Sprachen.

Ja, es fühlt sich hier weniger relevant an, da Felder in Julia eher für den "privaten" Gebrauch bestimmt sind. Aber mit der anhaltenden Diskussion über die Überlastung des Feldzugriffs könnte das sinnvoller werden.

f.(x) scheint die weniger problematische Lösung zu sein, wenn da nicht die Asymmetrie mit .+ wäre. Aber IMHO ist es eine gute Idee, die symbolische Assoziation von . mit "elementweiser Operation" beizubehalten.

Wenn die aktuelle typisierte Array-Konstruktion veraltet sein kann, kann func[v...] in map(func, v...) übersetzt werden, und die literalen Arrays können dann in T[[a1, ..., an]] geschrieben werden (anstelle des aktuellen T[a1, ..., an] ).

Ich finde sin∘v auch ziemlich natürlich (wenn ein Array v als eine Anwendung von Indizes zu enthaltenen Werten gesehen wird), oder einfacher sin*v oder v*[sin]' ( was die Definition *(x, f::Callable) ) usw. erfordert.

Als ich mit frischem Verstand auf dieses Problem zurückkam, stellte ich fest, dass f.(x) als eine ganz natürliche Syntax angesehen werden kann. Anstatt es als f. und ( zu lesen, können Sie es als f und .( lesen. .( ist dann metaphorisch eine elementweise Version des Funktionsaufrufoperators ( , der vollständig mit .+ und seinen Freunden übereinstimmt.

Die Vorstellung, dass .( ein Operator für Funktionsaufrufe ist, macht mich sehr traurig.

@johnmyleswhite Möchtest du das näher erläutern? Ich sprach über die Intuitivität der Syntax oder ihre visuelle Konsistenz mit dem Rest der Sprache, überhaupt nicht über die technische Implementierung.

Für mich ist ( überhaupt kein Teil der Semantik der Sprache: Es ist nur ein Teil der Syntax. Ich möchte also keinen Weg erfinden müssen, wie sich .( und ( unterscheiden. Erzeugt Ersteres ein multicall Expr anstelle eines call Expr ?

Nein. Wie ich schon sagte, wollte ich damit überhaupt nicht sagen, dass es zwei verschiedene Anrufanbieter geben sollte. Ich versuche nur, eine _visuell_ konsistente Syntax für elementweise Operationen zu finden.

Was diese Optionen für mich zunichte macht, ist die Frage, wie man Funktionen mit mehreren Argumenten vektorisiert. Es gibt keinen einzigen Weg, dies zu tun, und alles, was allgemein genug ist, um jeden möglichen Weg zu unterstützen, sieht sehr nach den mehrdimensionalen Array-Verständnissen aus, die wir bereits haben.

Es ist ganz normal, dass map mit mehreren Argumenten über alle Argumente iteriert.
Wenn wir dies tun würden, würde ich .( Syntax für einen Aufruf an die Karte machen. Diese Syntax könnte
aus verschiedenen Gründen nicht so toll sein, aber mit diesen Aspekten wäre ich einverstanden.

Die Tatsache, dass mehrere Verallgemeinerungen für Funktionen mit mehreren Argumenten möglich sind, kann kein Argument gegen die Unterstützung zumindest einiger Spezialfälle sein – genauso wie die Matrixtransponierung nützlich ist, auch wenn sie für Tensoren auf verschiedene Weise verallgemeinert werden kann.

Wir müssen nur die nützlichste Lösung auswählen. Mögliche Optionen wurden bereits hier diskutiert: https://github.com/JuliaLang/julia/issues/8389#issuecomment -55953120 (und folgende Kommentare). Wie @JeffBezanson sagte, ist das aktuelle Verhalten von map vernünftig. Ein interessantes Kriterium ist, @vectorize_2arg ersetzen zu können.

Mein Punkt ist, dass die Koexistenz sin.(x) und x .+ y umständlich ist. Ich hätte lieber .sin(x) -> map(sin, x) und x .+ y -> map(+, x, y) .

.+ verwendet tatsächlich broadcast .

Einige andere Ideen, aus reiner Verzweiflung:

  1. Doppelpunkt überladen, sin:x . Lässt sich nicht gut auf mehrere Argumente verallgemeinern.
  2. sin.[x] --- diese Syntax ist verfügbar, derzeit bedeutungslos.
  3. sin@x --- nicht verfügbar, aber vielleicht möglich

Ich bin wirklich nicht davon überzeugt, dass wir das brauchen.

Ich auch nicht. Ich denke, f.(x) ist hier die beste Option, aber ich liebe es nicht.

Aber wie können wir ohne dies vermeiden, alle möglichen vektorisierten Funktionen zu erstellen, insbesondere Dinge wie int() ? Das hat mich veranlasst, diese Diskussion in https://github.com/JuliaLang/julia/issues/8389 zu starten.

Wir sollten die Leute ermutigen, map(func, x) zu verwenden. Es ist nicht so viel Tipparbeit und es ist jedem sofort klar, der aus einer anderen Sprache kommt.

Und natürlich darauf achten, dass es schnell geht.

Ja, aber für den interaktiven Gebrauch finde ich es sehr schmerzhaft. Das wird ein großes Ärgernis für Leute sein, die aus R kommen (zumindest kenne ich mich nicht mit anderen Sprachen aus), und ich möchte nicht, dass das den Eindruck erweckt, dass Julia nicht für die Datenanalyse geeignet ist.

Ein weiteres Problem ist die Konsistenz: Es sei denn, Sie sind bereit, alle derzeit vektorisierten Funktionen, einschließlich log , exp usw., zu entfernen und die Leute zu bitten, stattdessen map zu verwenden (was evtl ein interessanter Test für die Praktikabilität dieser Entscheidung), wird die Sprache inkonsistent sein, was es schwierig macht, im Voraus zu wissen, ob eine Funktion vektorisiert ist oder nicht (und auf welchen Argumenten).

Viele andere Sprachen verwenden seit Jahren map .

Als ich den Plan verstand, alles zu vektorisieren, war das Entfernen der meisten/aller vektorisierten Funktionen immer Teil der Strategie.

Ja, natürlich würden wir aufhören, alles zu vektorisieren. Die Inkonsistenz ist bereits da: Es ist bereits schwer zu wissen, welche Funktionen vektorisiert sind oder werden sollten, da es keinen wirklich überzeugenden Grund gibt, warum sin , exp usw. implizit über Arrays abgebildet werden sollten.

Und Bibliotheksautoren zu sagen, dass sie @vectorize auf alle geeigneten Funktionen setzen sollen, ist albern; Sie sollten in der Lage sein, einfach eine Funktion zu schreiben, und wenn jemand sie für jedes Element berechnen möchte, verwenden sie map .

Stellen wir uns vor, was passiert, wenn wir häufig verwendete vektorisierte mathematische Funktionen entfernen:

  1. Mir persönlich macht es nichts aus, map(exp, x) statt exp(x) zu schreiben, auch wenn letzteres etwas kürzer und sauberer ist. Es gibt jedoch einen _großen_ Leistungsunterschied. Die vektorisierte Funktion ist etwa 5x schneller als die Karte auf meinem Computer.
  2. Wenn Sie mit zusammengesetzten Ausdrücken arbeiten, ist das Problem interessanter. Betrachten Sie einen zusammengesetzten Ausdruck: exp(0.5 * abs2(x - y)) , dann haben wir
# baseline: the shortest way
exp(0.5 * abs2(x - y))    # ... takes 0.03762 sec (for 10^6 elements)

# using map (very cumbersome for compound expressions)
map(exp, 0.5 * map(abs2, x - y))   # ... takes 0.1304 sec (about 3.5x slower)

# using anonymous function (shorter for compound expressions)
map((u, v) -> 0.5 * exp(abs2(u - v)), x, y)   # ... takes 0.2228 sec (even slower, about 6x baseline)

# using array comprehension (we have to deal with two array arguments)

# method 1:  using zip to combine the arguments (readability not bad)
[0.5 * exp(abs2(u - v)) for (u, v) in zip(x, y)]  # ... takes 0.140 sec, comparable to using map

# method 2:  using index, resulting in a slightly longer statement
[0.5 * exp(abs2(x[i] - y[i])) for i = 1:length(x)]  # ... takes 0.016 sec, 2x faster than baseline 

Wenn wir vektorisierte mathematische Funktionen entfernen wollen, scheint der einzige Weg, der sowohl hinsichtlich der Lesbarkeit als auch der Leistung akzeptabel ist, das Array-Verständnis zu sein. Dennoch sind sie nicht so praktisch wie vektorisierte Mathematik.

-1 zum Entfernen vektorisierter Versionen. Tatsächlich bieten Bibliotheken wie VML und Yeppp eine viel höhere Leistung für vektorisierte Versionen, und wir müssen herausfinden, wie wir diese nutzen können.

Ob diese in der Basis sind oder nicht, ist eine andere Diskussion und eine größere Diskussion, aber der Bedarf ist real und die Leistung kann höher sein als das, was wir haben.

@lindahua und @ViralBShah : Einige Ihrer Bedenken scheinen auf der Annahme zu beruhen, dass wir vektorisierte Funktionen loswerden würden, bevor wir Verbesserungen an map vornehmen, aber ich glaube nicht, dass irgendjemand vorgeschlagen hat, dies zu tun.

Ich denke, das Beispiel von @lindahua ist ziemlich aufschlussreich: Die vektorisierte Syntax ist viel schöner und viel näher an der mathematischen Formel als die anderen Lösungen. Ich wäre ziemlich schlecht, wenn ich das verlieren würde, und Leute, die aus anderen Wissenschaftssprachen kommen, werden dies wahrscheinlich als negativen Punkt bei Julia betrachten.

Ich bin damit einverstanden, alle vektorisierten Funktionen zu entfernen (wenn map schnell genug ist) und zu sehen, wie es geht. Ich glaube, das Interesse an der Bereitstellung einer praktischen Syntax wird an diesem Punkt noch deutlicher sichtbar sein, und es wird immer noch an der Zeit sein, sie hinzuzufügen, wenn sich herausstellt, dass dies der Fall ist.

Ich denke, Julia unterscheidet sich von vielen anderen Sprachen, weil sie die interaktive Verwendung (längere Ausdrücke sind in diesem Fall lästig zu tippen) und mathematische Berechnungen (Formeln sollten mathematischen Ausdrücken so ähnlich wie möglich sein, um den Code lesbar zu machen) betont. Das ist zum Teil der Grund, warum Matlab, R und Numpy vektorisierte Funktionen anbieten (der andere Grund ist natürlich die Leistung, ein Problem, das in Julia verschwinden kann).

Mein Gefühl aus der Diskussion ist, dass die Bedeutung der vektorisierten Mathematik unterschätzt wird. Tatsächlich liegt einer der Hauptvorteile der vektorisierten Mathematik in der Prägnanz des Ausdrucks – sie ist viel mehr als nur eine Notlösung für „Sprachen mit langsamer For-Schleife“.

Vergleiche y = exp(x) und

for i = 1:length(x)
    y[i] = exp(x[i])
end

Ersteres ist offensichtlich viel prägnanter und lesbarer als letzteres. Die Tatsache, dass Julia Schleifen effizient macht, bedeutet nicht, dass wir Codes immer devektorisieren sollten, was meiner Meinung nach ziemlich kontraproduktiv ist.

Wir sollten Menschen ermutigen, Codes auf natürliche Weise zu schreiben. Einerseits bedeutet dies, dass wir nicht versuchen sollten, komplizierte Codes zu schreiben und vektorisierte Funktionen in einem Kontext zu verdrehen, in den sie nicht passen; Andererseits sollten wir die Verwendung vektorisierter Mathematik unterstützen, wann immer sie am sinnvollsten ist.

In der Praxis ist die Abbildung von Formeln auf Zahlenarrays eine sehr häufige Operation, und wir sollten uns bemühen, dies bequem statt umständlich zu machen. Dabei bleiben vektorisierte Codes der natürlichste und prägnanteste Weg. Abgesehen von der Leistung sind sie immer noch besser als der Aufruf der Funktion map , insbesondere für zusammengesetzte Ausdrücke mit mehreren Argumenten.

Wir wollen sicherlich keine vektorisierten Versionen von allem, aber die Verwendung von map jedes Mal zum Vektorisieren wäre aus den oben genannten Gründen, Dahua, ärgerlich.

Wenn map schnell wäre, könnten wir uns sicherlich darauf konzentrieren, einen kleineren und sinnvolleren Satz vektorisierter Funktionen zu haben.

Ich muss sagen, ich stehe stark auf der Seite der Unterstützung einer knappen Kartennotation. Ich denke, es ist der beste Kompromiss zwischen den verschiedenen Bedürfnissen.

Ich mag keine vektorisierten Funktionen. Es verbirgt, was vor sich geht. Diese Angewohnheit, vektorisierte Versionen von Funktionen zu erstellen, führt zu mysteriösem Code. Angenommen, Sie haben einen Code, in dem eine Funktion f aus einem Paket auf einem Vektor aufgerufen wird. Selbst wenn Sie eine ungefähre Vorstellung davon haben, was die Funktion tut, können Sie beim Lesen des Codes nicht sicher sein, ob sie dies elementweise tut oder auf den Vektor als Ganzes wirkt. Wenn wissenschaftliche Computersprachen keine Geschichte mit diesen vektorisierten Funktionen hätten, glaube ich nicht, dass wir sie jetzt annähernd so akzeptieren würden.

Es führt auch dazu, dass Sie implizit ermutigt werden, vektorisierte Versionen von Funktionen zu schreiben, um knappen Code dort zu ermöglichen, wo die Funktionen verwendet werden.

Der Code, der am deutlichsten beschreibt, was vor sich geht, ist die Schleife, aber wie @lindahua sagt, wird er am Ende sehr ausführlich, was seine eigenen Nachteile hat, insbesondere in einer Sprache, die auch für die interaktive Verwendung gedacht ist.

Dies führt zu dem map -Kompromiss, der meiner Meinung nach dem Ideal näher kommt, aber ich stimme @lindahua immer noch zu, dass er nicht knapp genug ist.

Wo ich @lindahua nicht zustimmen werde, ist, dass vektorisierte Funktionen aus den zuvor erwähnten Gründen die beste Wahl sind. Meine Überlegung führt dazu, dass Julia eine sehr knappe Notation für map haben sollte.

Ich finde es sehr ansprechend, wie Mathematica das mit seiner Kurzschreibweise macht. Die Kurzschreibweise für die Anwendung einer Funktion auf ein Argument in Mathematica ist @ , Sie würden also Apply die Funktion f auf einen Vektor übertragen als: f @ vector . Die zugehörige Kurzschreibweise für die Abbildung einer Funktion ist /@ , also ordnen Sie f dem Vektor wie folgt zu: f /@ vector . Dies hat mehrere ansprechende Eigenschaften. Beide Short-Hände sind knapp. Die Tatsache, dass beide das @ -Symbol verwenden, betont, dass es eine Beziehung zwischen dem gibt, was sie tun, aber das / in der Karte macht es immer noch visuell deutlich, wann Sie kartieren und wann Sie sind nicht. Das soll nicht heißen, dass Julia die Notation von Mathematica blind kopieren sollte, nur dass eine gute Notation für die Abbildung unglaublich wertvoll ist

Ich schlage nicht vor, alle vektorisierten Funktionen loszuwerden. Dieser Zug hat den Bahnhof längst verlassen. Stattdessen schlage ich vor, die Liste der vektorisierten Funktionen so kurz wie möglich zu halten und eine gute knappe Kartennotation bereitzustellen, um davon abzuhalten, sie zur Liste der vektorisierten Funktionen hinzuzufügen.

All dies ist natürlich an die Bedingung geknüpft, dass map und anonyme Funktionen schnell sind. Im Moment ist Julia in einer seltsamen Lage. In wissenschaftlichen Rechensprachen war es früher so, dass Funktionen vektorisiert wurden, weil Schleifen langsam sind. Das ist kein Problem. Stattdessen haben Sie in Julia Funktionen vektorisiert, da Karten- und anonyme Funktionen langsam sind. Wir sind also wieder da, wo wir angefangen haben, aber aus anderen Gründen.

Vektorisierte Bibliotheksfunktionen haben einen Nachteil – es sind nur die explizit von der Bibliothek bereitgestellten Funktionen verfügbar. Das heißt, zB ist sin(x) schnell, wenn es auf einen Vektor angewendet wird, während sin(2*x) plötzlich viel langsamer ist, da es ein Zwischenarray erfordert, das zweimal durchlaufen werden muss (zuerst schreiben, dann lesen).

Eine Lösung wäre eine Bibliothek mit vektorisierbaren mathematischen Funktionen. Dies wären Implementierungen von sin , cos usw., die LLVM zum Inlining zur Verfügung stehen. LLVM könnte diese Schleife dann vektorisieren und hoffentlich zu sehr effizientem Code führen. Yeppp scheint die richtigen Schleifenkerne zu haben, scheint sie aber nicht für das Inlining verfügbar zu machen.

Ein weiteres Problem mit der Vektorisierung als Paradigma ist, dass sie überhaupt nicht funktioniert, wenn Sie andere Containertypen als die von der Standardbibliothek gesegneten verwenden. Sie können dies in DataArrays sehen: Es gibt eine lächerliche Menge an Metaprogrammierungscode, der verwendet wird, um Funktionen für die neuen Containertypen, die wir definieren, neu zu vektorisieren.

Kombinieren Sie das mit dem Punkt von @eschnett und Sie erhalten:

  • Vektorisierte Funktionen funktionieren nur, wenn Sie sich auf die Funktionen der Standardbibliothek beschränken
  • Vektorisierte Funktionen funktionieren nur, wenn Sie sich auf die Containertypen der Standardbibliothek beschränken

Ich möchte klarstellen, dass es mir nicht darum geht, dass wir die vektorisierten Funktionen immer beibehalten sollten, sondern dass wir einen Weg brauchen, der so knapp ist wie das Schreiben vektorisierter Funktionen. Die Verwendung map erfüllt dies wahrscheinlich nicht.

Ich mag die Idee von @eschnett , einige Funktionen als _vektorisierbar_ zu kennzeichnen, und der Compiler kann eine vektorisierbare Funktion automatisch einem Array zuordnen, ohne dass die Benutzer explizit eine vektorisierte Version definieren müssen. Der Compiler kann auch eine Kette vektorisierbarer Funktionen zu einer verschmolzenen Schleife verschmelzen.

Hier ist, was ich im Sinn habe, inspiriert von @eschnetts Kommentaren:

# The <strong i="11">@vec</strong> macro tags the function that follows as vectorizable
<strong i="12">@vec</strong> abs2(x::Real) = x * x
<strong i="13">@vec</strong> function exp(x::Real) 
   # ... internal implementation ...
end

exp(2.0)  # simply calls the function

x = rand(100);
exp(x)    # maps exp to x, as exp is tagged as vectorizable

exp(abs2(x))  # maps v -> exp(abs2(v)), as this is applying a chain of vectorizable functions

Der Compiler kann auch die Berechnung (auf einer niedrigeren Ebene) neu vektorisieren, indem er die Möglichkeit der Verwendung von SIMD identifiziert.

Natürlich sollte @vec dem Endbenutzer zur Verfügung gestellt werden, damit Benutzer ihre eigenen Funktionen als vektorisierbar deklarieren können.

Danke, @lindahua : Ihre Klarstellung hilft sehr.

Würde sich @vec von der Deklaration einer Funktion @pure unterscheiden?

@vec gibt an, dass die Funktion elementweise abgebildet werden kann.

Nicht alle reinen Funktionen fallen in diese Kategorie, zB ist sum eine reine Funktion, und ich denke nicht, dass es ratsam ist, sie als _vektorisierbar_ zu deklarieren.

Könnte man nicht die Eigenschaft vec von sum aus den Tags pure und associative auf + zusammen mit dem Wissen darüber, wie reduce / foldl / foldr funktionieren, wenn die Funktionen pure und associative gegeben sind? Natürlich ist dies alles hypothetisch, aber wenn Julia sich auf Merkmale für Typen einlassen würde, könnte ich mir vorstellen, den Stand der Technik für die Vektorisierung erheblich zu verbessern, indem ich auch auf Merkmale für Funktionen einsteige.

Ich habe das Gefühl, dass das Hinzufügen einer neuen Syntax das Gegenteil von dem ist, was wir wollen (nachdem wir nur die spezielle Syntax für Any[] und Dict bereinigt haben). Der ganze _Punkt_ beim Entfernen dieser vektorisierten Funktionen besteht darin, Sonderfälle zu reduzieren (und ich denke nicht, dass die Syntax anders sein sollte als die Funktionssemantik). Aber ich stimme zu, dass eine knappe Karte nützlich wäre.

Warum also nicht einen knappen Infix map -Operator hinzufügen? Hier wähle ich willkürlich $ aus. Damit würde das Beispiel von @lindahua ausgehen

exp(0.5 * abs2(x - y))

zu

exp $ (0.5 * abs2 $ (x-y))

Wenn wir jetzt nur Haskell-ähnliche Unterstützung für benutzerdefinierte Infix-Operatoren hätten, wäre dies nur eine Änderung von einer Zeile ($) = map . :)

IMO, die anderen Syntaxvorschläge sind visuell zu nah an der bestehenden Syntax und würden weitere mentale Anstrengungen erfordern, um sie beim Durchsuchen des Codes zu analysieren:

  • foo.(x) -- visuell ähnlich dem Member-Zugriff vom Standardtyp
  • foo[x] – greife ich hier auf das x-te Mitglied des foo-Arrays zu oder rufe die Map auf?
  • .foo(x) -- hat Probleme, wie @kmsquire betonte

Und ich habe das Gefühl, dass die @vec -Lösung zu nahe an der @vectorize $-Lösung liegt, die wir von vornherein vermeiden wollen. Sicherlich wären einige Anmerkungen ala #8297 schön zu haben und könnten einem zukünftigen, intelligenteren Compiler helfen, diese Stream-Fusion- Möglichkeiten zu erkennen und entsprechend zu optimieren. Aber ich mag die Idee nicht, es zu erzwingen.

Infix Map plus schnelle anonyme Funktionen könnten auch beim Erstellen von Provisorien helfen, wenn Sie so etwas tun könnten:

(x, y) -> exp(0.5 * abs2(x - y)) $ x, y

Ich frage mich, ob die Idee aus dem coolen neuen Trait.jl im Zusammenhang mit der Bezeichnung einer vektorisierbaren Funktion übernommen werden kann. In diesem Fall betrachten wir natürlich einzelne _Instanzen_ des Function -Typs, die vektorisierbar sind oder nicht, anstatt eines julia-Typs mit einem bestimmten Merkmal.

Wenn wir jetzt nur Haskell-ähnliche Unterstützung für benutzerdefinierte Infix-Operatoren hätten

6582 #6929 nicht genug?

Es gibt einen Punkt in dieser Diskussion über das Vektorisieren ganzer Ausdrücke mit so wenig temporären Arrays wie möglich. Benutzer, die eine vektorisierte Syntax wünschen, möchten nicht nur ein vektorisiertes exp(x) ; Sie würden Ausdrücke wie schreiben wollen

y =  √π exp(-x^2) * sin(k*x) + im * log(x-1)

und lassen Sie es auf magische Weise vektorisieren

Es wäre nicht erforderlich, Funktionen als "vektorisierbar" zu kennzeichnen. Dies ist eher eine Eigenschaft davon, wie die Funktionen implementiert und Julia zur Verfügung gestellt werden. Wenn sie z. B. in C implementiert sind, müssen sie in LLVM-Bytecode (nicht Objektdateien) kompiliert werden, damit der LLVM-Optimierer weiterhin darauf zugreifen kann. Die Implementierung in Julia würde auch funktionieren.

Vektorisierbarkeit bedeutet, dass man die Funktion so implementiert, wie es das Yeppp-Projekt ganz gut beschreibt: Keine Verzweigungen, keine Tabellen, Divisionen oder Quadratwurzeln, wenn sie als Vektorbefehle in Hardware vorhanden sind, und ansonsten viele verschmolzene Multiplikations-Additions-Operationen und Vektor Operationen zusammenführen.

Leider sind solche Implementierungen hardwareabhängig, dh man muss möglicherweise unterschiedliche Algorithmen oder unterschiedliche Implementierungen wählen, je nachdem, welche Hardwareanweisungen effizient sind. Ich habe dies in der Vergangenheit (https://bitbucket.org/eschnett/vecmathlib/wiki/Home) in C++ und mit einer etwas anderen Zielgruppe (Stencil-basierte Operationen, die manuell vektorisiert werden, anstelle einer automatischen Vektorisierung) gemacht Compiler).

Hier in Julia wären die Dinge einfacher, da (a) wir wissen, dass der Compiler LLVM sein wird, und (b) wir dies in Julia anstelle von C++ implementieren können (Makros vs. Vorlagen).

Noch etwas ist zu beachten: Wenn man Teile des IEEE-Standards aufgibt, dann kann man die Geschwindigkeit stark verbessern. Viele Benutzer wissen, dass zB denormalisierte Zahlen nicht wichtig sind, oder dass die Eingabe immer kleiner als sqrt(max(Double)) sein wird, etc. Die Frage ist, ob man für diese Fälle schnelle Pfade anbieten soll. Ich weiß, dass mich das sehr interessieren wird, aber andere bevorzugen vielleicht stattdessen genau reproduzierbare Ergebnisse.

Lassen Sie mich in Julia einen vektorisierbaren exp -Prototyp erstellen. Wir können dann sehen, wie sich LLVM beim Vektorisieren einer Schleife verhält und welche Geschwindigkeiten wir erhalten.

Ist es zu beängstigend, Klammern voller Breite um das Argument der Funktion herum zu verwenden?

Tut mir leid, mir war nicht klar, dass ich genau dasselbe wiederhole, was @johnmyleswhite oben über die Funktion mit Trait gesprochen hat. Mach weiter.

@eschnett Ich halte es nicht für sinnvoll, die API (ob Funktionen vektorisierbar sind oder nicht) mit Implementierungsdetails (wie die Funktion kompiliert wird) zu verknüpfen. Es klingt ziemlich komplex zu verstehen und in der Zeit und über Architekturen hinweg stabil zu halten, und es würde nicht funktionieren, wenn Funktionen in externen Bibliotheken aufgerufen würden, zB würde log nicht als vektorisierbar erkannt werden, da es eine Funktion von openlibm aufruft.

Die Idee von OTOH @johnmyleswhite , Merkmale zu verwenden, um die mathematischen Eigenschaften einer Funktion zu kommunizieren, könnte eine großartige Lösung sein. ( Der Vorschlag von @lindahua ist eine Funktion, die ich vor einiger Zeit irgendwo vorgeschlagen hatte, aber die Lösung der Verwendung von Merkmalen könnte noch besser sein.)

Wenn wir jetzt nur Haskell-ähnliche Unterstützung für benutzerdefinierte Infix-Operatoren hätten

6582 #6929 nicht genug?

Ich hätte sagen sollen: ... benutzerdefinierte _Nicht-Unicode_-Infix-Operatoren, da ich nicht glaube, dass wir von Benutzern verlangen wollen, dass sie Unicode-Zeichen eingeben, um auf eine solche Kernfunktionalität zuzugreifen. Obwohl ich sehe, dass $ tatsächlich einer der hinzugefügten ist, also danke dafür! Wow, das funktioniert also tatsächlich in Julia _today_ (auch wenn es nicht "schnell" ist ... noch nicht):

julia> ($) = map
julia> sin $ (0.5 * (abs2 $ (x-y)))

Ich weiß nicht, ob es die beste Wahl für map ist, aber die Verwendung $ für xor scheint wirklich eine Verschwendung zu sein. Bitwise xor wird nicht so oft verwendet. map ist viel wichtiger.

Der obige Punkt von @jiahao ist sehr gut: Einzelne vektorisierte Funktionen wie exp sind eigentlich eine Art Hack, um vektorisierte _Ausdrücke_ wie exp(-x^2) zu erhalten. Eine Syntax, die so etwas wie @devec macht, wäre wirklich wertvoll: Sie würden eine devektorisierte Leistung erhalten und die Allgemeingültigkeit, Funktionen nicht einzeln als vektorisiert identifizieren zu müssen.

Die Möglichkeit, Funktionsmerkmale dafür zu verwenden, wäre cool, aber ich finde es immer noch weniger befriedigend. Was im Allgemeinen wirklich passiert, ist, dass eine Person eine Funktion schreibt und eine andere Person sie iteriert.

Ich stimme zu, dass dies keine Eigenschaft der Funktion ist, sondern eine Eigenschaft der Verwendung der Funktion. Die Diskussion über das Anwenden von Merkmalen scheint ein Fall zu sein, in dem man den falschen Baum anbellt.

Brainstorming: Wie wäre es, wenn Sie die Argumente markieren, die Sie zuordnen möchten, damit die Zuordnung von mehreren Argumenten unterstützt wird:

a = split("the quick brown")
b = split("fox deer bear")
c = split("jumped over the lazy")
d = split("dog cat")
e = string(a, " ", b., " ", c, " ", d.) # -> 3x2 Vector{String} of the combinations   
# e[1,1]: """["the","quick", "brown"] fox ["jumped","over","the","lazy"] dog"""

Ich bin mir nicht sicher, ob .b oder b. besser ist, um zu zeigen, dass Sie das gemappt haben möchten. Ich gebe in diesem Fall gerne ein mehrdimensionales 3x2-Ergebnis zurück, da es die Form des map -Pings darstellt.

Tal

Hier https://github.com/eschnett/Vecmathlib.jl ist ein Repo mit einem Beispiel
Implementierung von exp , geschrieben in einer Weise, die von LLVM optimiert werden kann.
Diese Implementierung ist etwa doppelt so schnell wie die standardmäßige exp
Implementierung auf meinem System. Es erreicht (wahrscheinlich) noch nicht die Geschwindigkeit von Yeppp,
wahrscheinlich, weil LLVM die jeweilige SIMD-Schleife nicht als entrollt
aggressiv wie Yeppp. (Ich habe die zerlegte Anleitung verglichen.)

Das Schreiben einer vektorisierbaren exp -Funktion ist nicht einfach. Die Verwendung sieht so aus:

function kernel_vexp2{T}(ni::Int, nj::Int, x::Array{T,1}, y::Array{T,1})
    for j in 1:nj
        <strong i="16">@simd</strong> for i in 1:ni
            <strong i="17">@inbounds</strong> y[i] += vexp2(x[i])
        end
    end
end

wobei die Schleife j und die Funktionsargumente nur für da sind
Benchmarking-Zwecke.

Gibt es ein @unroll -Makro für Julia?

-erik

Am Sonntag, den 2. November 2014 um 20:26 Uhr schrieb Tim Holy [email protected] :

Ich stimme zu, dass dies keine Eigenschaft der Funktion ist, sondern eine Eigenschaft von
die Verwendung der Funktion. Die Diskussion über das Anwenden von Merkmalen scheint wie a
wenn man den falschen Baum anbellt.

Antworten Sie direkt auf diese E-Mail oder zeigen Sie sie auf GitHub an
https://github.com/JuliaLang/julia/issues/8450#issuecomment -61433026.

Erik Schnetter [email protected]
http://www.perimeterinstitute.ca/personal/eschnetter/

Einzelne vektorisierte Funktionen wie exp sind eigentlich eine Art Hack, um vektorisierte _Ausdrücke_ wie exp(-x^2) zu erhalten

Die Kernsyntax zum Herausheben ganzer Ausdrücke aus dem Skalarbereich wäre sehr interessant. Vektorisierung ist nur ein Beispiel (bei dem die Zieldomäne Vektoren sind); Ein weiterer interessanter Anwendungsfall wäre das Heben in die Matrixdomäne (#5840), wo die Semantik ganz anders ist. In der Matrixdomäne wäre es auch nützlich zu untersuchen, wie die Verteilung auf verschiedene Ausdrücke funktionieren könnte, da Sie im allgemeinen Schur-Parlett und andere spezialisiertere Algorithmen benötigen würden, wenn Sie etwas Einfacheres wie sqrtm wollten. (Und mit einer cleveren Syntax könnten Sie die *m Funktionen komplett loswerden - expm , logm , sqrtm , ...)

Gibt es ein @unroll -Makro für Julia?

mit Base.Cartesian
@nexpr 4 d->(y[i+d] = exp(x[i+d])

(Siehe http://docs.julialang.org/en/latest/devdocs/cartesian/, wenn Sie Fragen haben.)

@jiahao Die Verallgemeinerung auf Matrixfunktionen klingt nach einer interessanten Herausforderung, aber mein Wissen darüber ist nahezu null. Hast du eine Idee, wie es funktionieren könnte? Wie würde sich das mit der Vektorisierung artikulieren? Wie würde die Syntax es ermöglichen, den Unterschied zwischen der elementweisen Anwendung exp auf einen Vektor/eine Matrix und der Berechnung ihrer Matrixexponential zu machen?

@timholy : Danke! Ich habe nicht daran gedacht, kartesisch zum Abrollen zu verwenden.

Leider ist der von @nexprs (oder durch manuelles Entrollen) erzeugte Code nicht mehr vektorisiert. (Dies ist LLVM 3.3, vielleicht wäre LLVM 3.5 besser.)

Betreff: Ausrollen, siehe auch den Beitrag von @toivoh auf julia-users . Es kann sich auch lohnen, #6271 auszuprobieren.

@nalimilan Ich habe das noch nicht durchdacht, aber Skalar-> Matrix-Lifting wäre mit einer einzigen matrixfunc -Funktion (sagen wir) recht einfach zu implementieren. Eine hypothetische Syntax (etwas vollständig erfinden) könnte sein

X = randn(10,10)
c = 0.7
lift(x->exp(c*x^2)*sin(x), X)

was dann wäre

  1. Identifizieren Sie die Quell- und Zieldomänen des Lifts von X , die vom Typ Matrix{Float64} sind und die Elemente (Typparameter) Float64 haben (wodurch implizit ein Float64 => Matrix{Float64} Lift definiert wird). , dann
  2. Rufen matrixfunc(x->exp(c*x^2)*sin(x), X) auf, um das Äquivalent von expm(c*X^2)*sinm(X) $ zu berechnen, aber vermeiden Sie die Matrixmultiplikation.

In einem anderen Code könnte X ein Vector{Int} sein und das implizierte Anheben wäre von Int auf Vector{Int} und dann könnte lift(x->exp(c*x^2)*sin(x), X) sein Rufen map(x->exp(c*x^2)*sin(x), X) .

Man könnte sich auch andere Methoden vorstellen, die Quell- und Zieldomänen explizit angeben, zB lift(Number=>Matrix, x->exp(c*x^2)*sin(x), X) .

@nalimilan Die Vektorisierung ist nicht wirklich eine Eigenschaft der API. Mit der heutigen Compiler-Technologie kann eine Funktion nur vektorisiert werden, wenn sie inline ist. Die Dinge hängen hauptsächlich von der Funktionsimplementierung ab - wenn sie "richtig" geschrieben ist, kann der Compiler sie vektorisieren (nachdem sie in eine umgebende Schleife eingefügt wurde).

@eschnett : Verwenden Sie die gleiche Bedeutung von Vektorisierung wie andere? Es hört sich so an, als würdest du über SIMD usw. sprechen, was ich unter @nalimilan nicht verstehe.

Rechts. Hier gibt es zwei verschiedene Begriffe der Vektorisierung. Man handelt
mit dem Abrufen von SIMD für enge innere Schleifen (Prozessorvektorisierung). Die wichtigsten
Das hier diskutierte Problem ist die Syntax / Semantik von irgendwie "automatisch"
in der Lage sein, eine Funktion mit einem (oder mehreren) Argumenten für eine Sammlung aufzurufen.

Am Dienstag, 4. November 2014 um 19:04 Uhr, John Myles White [email protected]
schrieb:

@eschnett https://github.com/eschnett : Verwenden Sie die gleiche Bedeutung
der Vektorisierung wie andere? Es hört sich so an, als ob es um SIMD usw. geht
ist nicht das, was ich unter @nalimilan https://github.com/nalimilan verstehe
bedeuten.


Antworten Sie direkt auf diese E-Mail oder zeigen Sie sie auf GitHub an
https://github.com/JuliaLang/julia/issues/8450#issuecomment -61738237.

Sollte $#$ f.(x) $#$ symmetrisch zu anderen . -Operatoren nicht eine Sammlung von Funktionen auf eine Sammlung von Werten anwenden? (Zum Beispiel für die Transformation von einem Koordinatensystem einer ND-Einheit in physikalische Koordinaten.)

Bei der Diskussion der Syntax kam der Gedanke auf, dass die Verwendung expliziter Schleifen zum Ausdrücken des Äquivalents von map(log, x) zu langsam sei. Wenn man dies schnell genug machen kann, dann sind der Aufruf map (oder die Verwendung einer speziellen Syntax) oder das Schreiben von Schleifen auf semantischer Ebene äquivalent, und man muss keine syntaktische Begriffsklärung einführen. Derzeit ist das Aufrufen einer Vektorprotokollfunktion viel schneller als das Schreiben einer Schleife über ein Array, was die Leute dazu veranlasst, nach einer Möglichkeit zu fragen, diese Unterscheidung in ihrem Code auszudrücken.

Hier gibt es zwei Problemebenen: (1) Syntax und Semantik, (2) Implementierung.

Bei der Syntax- und Semantikfrage geht es darum, wie der Benutzer die Absicht zum Ausdruck bringen kann, bestimmte Berechnungen elementweise/broadcastend auf bestimmte Arrays abzubilden. Derzeit unterstützt Julia zwei Möglichkeiten: die Verwendung vektorisierter Funktionen und die Möglichkeit, dass Benutzer die Schleife explizit schreiben (manchmal mit Hilfe von Makros). Beide Wege sind nicht ideal. Während vektorisierte Funktionen es einem ermöglichen, sehr prägnante Ausdrücke wie exp(0.5 * (x - y).^2) zu schreiben, haben sie zwei Probleme: (1) Es ist schwierig, eine Grenze zu ziehen, welche Funktionen eine vektorisierte Version liefern sollten und welche nicht, was oft dazu führt in endlosen Debatten auf der Entwicklerseite und Verwirrung auf der Benutzerseite (man muss oft in der Dokumentation nachschlagen, um herauszufinden, ob bestimmte Funktionen vektorisiert sind). (2) Es macht es schwierig, die Schleifen über Funktionsgrenzen hinweg zu verschmelzen. Zu diesem Zeitpunkt und wahrscheinlich noch in einigen Monaten/Jahren wird der Compiler wahrscheinlich nicht in der Lage sein, so komplexe Aufgaben auszuführen, wie das gemeinsame Betrachten mehrerer Funktionen, das Identifizieren des gemeinsamen Datenflusses und das Erzeugen eines optimierten Codepfads über Funktionsgrenzen hinweg.

Die Verwendung der map -Funktion behebt das Problem (1) hier. Dies hilft jedoch immer noch nicht bei der Lösung des Problems (2) -- die Verwendung von Funktionen, entweder einer bestimmten vektorisierten Funktion oder einer generischen map , erzeugt immer eine Funktionsgrenze, die das Verschmelzen von Schleifen behindert, was ist entscheidend für Hochleistungsberechnungen. Die Verwendung der Kartenfunktion führt auch zu Ausführlichkeit, z. B. wird der obige Ausdruck jetzt zu einer längeren Anweisung als map(exp, 0.5 * map(abs2, x - y)) . Sie können sich vernünftigerweise vorstellen, dass dieses Problem durch komplexere Ausdrücke verschärft würde.

Unter all den Vorschlägen, die in diesem Thread skizziert werden, bin ich persönlich der Meinung, dass die Verwendung spezieller Notationen zur Angabe von Zuordnungen der vielversprechendste Weg für die Zukunft ist. Erstens behält es die Prägnanz des Ausdrucks bei. Nehmen Sie zum Beispiel die $-Notation, die obigen Ausdrücke können jetzt als exp $(0.5 * abs2$(x - y)) geschrieben werden. Dies ist etwas länger als der ursprüngliche vektorisierte Ausdruck, aber es ist nicht allzu schlimm - es muss lediglich $ bei jedem Aufruf einer Zuordnung eingefügt werden. Andererseits dient diese Notation auch als eindeutiger Indikator für eine durchgeführte Abbildung, die der Compiler verwenden kann, um die Funktionsgrenze zu durchbrechen und eine verschmolzene Schleife zu erzeugen. In diesem Kurs muss sich der Compiler nicht mit der internen Implementierung der Funktion befassen – alles, was er wissen muss, ist, dass die Funktion jedem Element der gegebenen Arrays zugeordnet wird.

Angesichts aller Möglichkeiten moderner CPUs, insbesondere der SIMD-Fähigkeit, ist das Verschmelzen mehrerer Schleifen zu einer nur ein Schritt in Richtung Hochleistungsberechnung. Dieser Schritt selbst löst nicht die Verwendung der SIMD-Befehle aus. Die gute Nachricht ist, dass wir jetzt das Makro @simd haben. Der Compiler kann dieses Makro am Anfang der erzeugten Schleife einfügen, wenn er dies für sicher und vorteilhaft hält.

Zusammenfassend denke ich, dass die $-Notation (oder ähnliche Vorschläge) das Syntax- und Semantikproblem weitgehend lösen kann, während sie dem Compiler die notwendigen Informationen liefert, um Schleifen zu verschmelzen und SIMD auszunutzen und somit hochleistungsfähige Codes auszugeben.

Die Zusammenfassung von @lindahua ist meiner Meinung nach gut.

Aber ich denke, es wäre interessant, dies noch weiter auszubauen. Julia verdient ein ehrgeiziges System, das viele gängige Muster so effizient macht wie ausgerollte Schleifen.

  • Das Muster, verschachtelte Funktionsaufrufe zu einer einzigen Schleife zu verschmelzen, sollte auch auf Operatoren angewendet werden, damit A .* B .+ C nicht zur Erzeugung von zwei Temporären führt, sondern nur einer für das Ergebnis.
  • Die Kombination von elementweisen Funktionen und Reduktionen sollte ebenfalls gehandhabt werden, so dass die Reduktion nach der Berechnung des Werts jedes Elements on-the-fly angewendet wird. Normalerweise wird dadurch sumabs2(A) entfernt und durch eine Standardnotation wie sum(abs$(A)$^2) (oder sum(abs.(A).^2) ) ersetzt.
  • Schließlich sollten nicht standardmäßige Iterationsmuster für nicht standardmäßige Arrays unterstützt werden, sodass A .* B für dünn besetzte Matrizen nur Nicht-Null-Einträge verarbeiten muss und eine dünn besetzte Matrix zurückgibt. Dies wäre auch nützlich, wenn Sie eine elementweise Funktion auf ein Set , ein Dict oder sogar ein Range anwenden möchten.

Die beiden letzten Punkte könnten funktionieren, indem elementweise Funktionen einen speziellen AbstractArray -Typ zurückgeben, sagen wir LazyArray , der seine Elemente im laufenden Betrieb berechnet (ähnlich wie Transpose Typ von https://github.com/JuliaLang/julia/issues/4774#issuecomment-59422003). Aber anstatt naiv auf seine Elemente zuzugreifen, indem man sie mit linearen Indizes von 1 bis length(A) durchgeht, könnte das Iteratorprotokoll verwendet werden. Der Iterator für einen bestimmten Typ würde abhängig vom Speicherlayout des Typs automatisch auswählen, ob die zeilen- oder spaltenweise Iteration am effizientesten ist. Und für dünn besetzte Matrizen würde es das Überspringen von Nulleinträgen ermöglichen (das Original und das Ergebnis müssten eine gemeinsame Struktur haben, vgl. https://github.com/JuliaLang/julia/issues/7010, https://github. com/JuliaLang/julia/issues/7157).

Wenn keine Reduzierung angewendet wird, würde ein Objekt desselben Typs und derselben Form wie das ursprüngliche Array einfach durch Iterieren über LazyArray (entspricht collect , aber unter Berücksichtigung des Typs des ursprünglichen Arrays) gefüllt ). Das Einzige, was dazu benötigt wird, ist, dass der Iterator ein Objekt zurückliefert, mit dem getindex für LazyArray und setindex! für das Ergebnis aufgerufen werden können (z. B. linear oder kartesisch Koordinaten).

Wenn eine Reduktion angewendet wird, würde es die relevante Iterationsmethode für sein Argument verwenden, um über die erforderlichen Dimensionen von LazyArray zu iterieren und ein Array mit dem Ergebnis zu füllen (äquivalent zu reduce , aber unter Verwendung von a benutzerdefinierter Iterator zur Anpassung an den Array-Typ). Eine Funktion (die im letzten Absatz verwendete) würde einen Iterator zurückgeben, der alle Elemente auf die effizienteste Weise durchgeht; andere würden dies über bestimmte Dimensionen erlauben.

Dieses ganze System würde auch den Vor-Ort-Betrieb ganz unkompliziert unterstützen.

Ich habe ein wenig über die Syntax nachgedacht und an .= gedacht, um elementweise Operationen auf Arrays anzuwenden.
Das Beispiel von @nalimilan sum(abs.(A).^2)) müsste also leider in zwei Schritten geschrieben werden:

A = [1,2,3,4]
a .= abs(A)^2
result = sum(a)

Dies hätte den Vorteil, dass es leicht lesbar wäre, und würde bedeuten, dass elementweise Funktionen nur für eine einzelne (oder mehrere) Eingabe geschrieben und für diesen Fall optimiert werden müssten, anstatt Array-spezifische Methoden zu schreiben.

Natürlich hält nichts anderes als Leistung und Vertrautheit jemanden davon ab, jetzt einfach map((x) -> abs(x)^2, A) zu schreiben, wie gesagt wurde.

Alternativ könnte es funktionieren, einen zuzuordnenden Ausdruck mit .() zu umgeben.
Ich weiß nicht, wie schwierig es wäre, dies zu tun, aber .sin(x) und .(x + sin(x)) würden dann den Ausdruck entweder innerhalb der Klammer oder der Funktion zuordnen, die auf . folgt.
Dies würde dann Reduzierungen wie im Beispiel von @nalimilan ermöglichen, wo sum(.(abs(A)^2)) dann in einer einzigen Zeile geschrieben werden könnte.

Beide Vorschläge verwenden ein . -Präfix, das mich bei der internen Verwendung von Broadcast an elementweise Operationen auf Arrays denken ließ. Dies könnte leicht durch $ oder ein anderes Symbol ersetzt werden.
Dies ist nur eine Alternative dazu, einen Zuordnungsoperator um jede zuzuordnende Funktion zu setzen und stattdessen den gesamten Ausdruck zu gruppieren und stattdessen den zuzuordnenden anzugeben.

Ich habe mit der LazyArray -Idee experimentiert, die ich in meinem letzten Kommentar vorgestellt habe: https://gist.github.com/nalimilan/e737bc8b3b10288abdad

Dieser Proof of Concept enthält keinen syntaktischen Zucker, aber (a ./ 2).^2 würde in das übersetzt werden, was im Wesentlichen als LazyArray(LazyArray(a, /, (2,)), ^, (2,)) geschrieben ist. Das System funktioniert recht gut, bedarf aber weiterer Optimierung, um hinsichtlich Leistung auch nur annähernd mit Loops konkurrenzfähig zu sein. Das (erwartete) Problem scheint zu sein, dass der Funktionsaufruf in Zeile 12 nicht optimiert ist (fast alle Zuweisungen finden dort statt), sogar in einer Version, in der zusätzliche Argumente nicht erlaubt sind. Ich denke, ich muss die LazyArray für die Funktion, die sie aufruft, parametrisieren, aber ich habe nicht herausgefunden, wie ich das tun könnte, geschweige denn auch mit Argumenten umgehen. Irgendwelche Ideen?

Irgendwelche Vorschläge zur Verbesserung der Leistung von LazyArray ?

@nalimilan Ich habe vor einem Jahr mit einem ähnlichen Ansatz experimentiert und Funktortypen in NumericFuns verwendet, um die faulen Ausdruckstypen zu parametrisieren. Ich versuchte eine Vielzahl von Tricks, hatte aber kein Glück, die Leistungslücke zu überbrücken.

Die Compiler-Optimierung wurde im letzten Jahr sukzessive verbessert. Aber ich habe immer noch das Gefühl, dass es immer noch nicht in der Lage ist, den unnötigen Overhead zu optimieren. Diese Art von Dingen erfordert von den Compilern aggressive Inline-Funktionen. Sie können versuchen, @inline zu verwenden und sehen, ob es die Dinge besser macht.

@lindahua @inline macht keinen Unterschied bei den Timings, was für mich logisch ist, da getindex(::LazyArray, ...) auf eine bestimmte LazyArray -Signatur spezialisiert ist, die nicht angibt, welche Funktion sollte heißen. Ich bräuchte so etwas wie LazyArray{T1, N, T2, F} , wobei F die Funktion ist, die aufgerufen werden soll, damit beim Kompilieren getindex der Aufruf bekannt ist. Gibt es eine Möglichkeit, das zu tun?

Inlining wäre noch eine weitere große Verbesserung, aber im Moment sind die Timings viel schlechter als selbst ein Anruf ohne Inline.

Sie können erwägen, NumericFuns zu verwenden, und F kann ein Funktortyp sein.

Dahua

Ich habe Funktionen benötigt, bei denen ich den Rückgabetyp für verteilt kenne
Computing, wo ich Verweise auf das Ergebnis vor dem Ergebnis erstelle (und
damit seine Art) bekannt ist. Ich habe eine sehr ähnliche Sache selbst implementiert, und
sollte wahrscheinlich auf die Verwendung von "Funktoren" umsteigen. (Ich mag die nicht
Namen "Funktor", da sie normalerweise etwas anderes sind <
http://en.wikipedia.org/wiki/Functor>, aber ich denke, C++ hat das Wasser durcheinander gebracht
Hier.)

Ich denke, es wäre sinnvoll, Ihren Functor-Teil von der abzutrennen
mathematische Funktionen.

-erik

Am Donnerstag, 20. November 2014 um 10:35 Uhr, Dahua Lin [email protected]
schrieb:

Sie können NumericFuns verwenden, und F kann ein Funktortyp sein.

Antworten Sie direkt auf diese E-Mail oder zeigen Sie sie auf GitHub an
https://github.com/JuliaLang/julia/issues/8450#issuecomment -63826019.

Erik Schnetter [email protected]
http://www.perimeterinstitute.ca/personal/eschnetter/

@lindahua Ich habe versucht, Funktoren zu verwenden, und tatsächlich ist die Leistung viel vernünftiger:
https://gist.github.com/nalimilan/d345e1c080984ed4c89a

With functions:
# elapsed time: 3.235718017 seconds (1192272000 bytes allocated, 32.20% gc time)

With functors:
# elapsed time: 0.220926698 seconds (80406656 bytes allocated, 26.89% gc time)

Loop:
# elapsed time: 0.07613788 seconds (80187556 bytes allocated, 45.31% gc time) 

Ich bin mir nicht sicher, was noch getan werden kann, um die Dinge zu verbessern, da der generierte Code noch nicht optimal erscheint. Ich brauche mehr erfahrene Augen, um zu erkennen, was falsch ist.

Tatsächlich verwendete der obige Test Pow , was anscheinend einen großen Geschwindigkeitsunterschied ergibt, je nachdem, ob Sie eine explizite Schleife schreiben oder eine LazyArray verwenden. Ich denke, das hat mit der Verschmelzung von Anweisungen zu tun, die nur im letzteren Fall durchgeführt würden. Das gleiche Phänomen ist z. B. bei Addition sichtbar. Bei anderen Funktionen ist der Unterschied jedoch viel geringer, entweder bei einer 100x100- oder einer 1000x1000-Matrix, wahrscheinlich weil sie extern sind und Inlining daher nicht viel bringt:

# With sqrt()
julia> test_lazy!(newa, a);
julia> <strong i="8">@time</strong> for i in 1:1000 test_lazy!(newa, a) end
elapsed time: 0.151761874 seconds (232000 bytes allocated)

julia> test_loop_dense!(newa, a);
julia> <strong i="9">@time</strong> for i in 1:1000 test_loop_dense!(newa, a) end
elapsed time: 0.121304952 seconds (0 bytes allocated)

# With exp()
julia> test_lazy!(newa, a);
julia> <strong i="10">@time</strong> for i in 1:1000 test_lazy!(newa, a) end
elapsed time: 0.289050295 seconds (232000 bytes allocated)

julia> test_loop_dense!(newa, a);
julia> <strong i="11">@time</strong> for i in 1:1000 test_loop_dense!(newa, a) end
elapsed time: 0.191016958 seconds (0 bytes allocated)

Daher würde ich gerne herausfinden, warum Optimierungen bei LazyArray nicht stattfinden. Die generierte Assembly ist für einfache Operationen ziemlich lang. Zum Beispiel für x/2 + 3 :

julia> a1 = LazyArray(a, Divide(), (2.0,));

julia> a2 = LazyArray(a1,  Add(), (3.0,));

julia> <strong i="17">@code_native</strong> a2[1]
    .text
Filename: none
Source line: 1
    push    RBP
    mov RBP, RSP
Source line: 1
    mov RAX, QWORD PTR [RDI + 8]
    mov RCX, QWORD PTR [RAX + 8]
    lea RDX, QWORD PTR [RSI - 1]
    cmp RDX, QWORD PTR [RCX + 16]
    jae L64
    mov RCX, QWORD PTR [RCX + 8]
    movsd   XMM0, QWORD PTR [RCX + 8*RSI - 8]
    mov RAX, QWORD PTR [RAX + 24]
    mov RAX, QWORD PTR [RAX + 16]
    divsd   XMM0, QWORD PTR [RAX + 8]
    mov RAX, QWORD PTR [RDI + 24]
    mov RAX, QWORD PTR [RAX + 16]
    addsd   XMM0, QWORD PTR [RAX + 8]
    pop RBP
    ret
L64:    movabs  RAX, jl_bounds_exception
    mov RDI, QWORD PTR [RAX]
    movabs  RAX, jl_throw_with_superfluous_argument
    mov ESI, 1
    call    RAX

Im Gegensatz zum Äquivalent:

julia> fun(x) = x/2.0 + 3.0
fun (generic function with 1 method)

julia> <strong i="21">@code_native</strong> fun(a1[1])
    .text
Filename: none
Source line: 1
    push    RBP
    mov RBP, RSP
    movabs  RAX, 139856006157040
Source line: 1
    mulsd   XMM0, QWORD PTR [RAX]
    movabs  RAX, 139856006157048
    addsd   XMM0, QWORD PTR [RAX]
    pop RBP
    ret

Der Teil bis zu jae L64 ist eine Array-Grenzenprüfung. Die Verwendung @inbounds kann helfen (falls zutreffend).

Der Teil darunter, wo zwei aufeinanderfolgende Zeilen mit mov RAX, ... beginnen, ist eine doppelte Indirektion, dh der Zugriff auf einen Zeiger auf einen Zeiger (oder ein Array von Arrays oder einen Zeiger auf ein Array usw.). Dies kann mit der internen Darstellung von LazyArray zu tun haben - vielleicht kann die Verwendung von Unveränderlichen (oder eine andere Darstellung von Unveränderlichen durch Julia) hier helfen.

In jedem Fall ist der Code immer noch ziemlich schnell. Um es schneller zu machen, müsste es in den Aufrufer eingebettet werden, was weitere Optimierungsmöglichkeiten aufzeigt. Was passiert, wenn Sie diesen Ausdruck zB aus einer Schleife aufrufen?

Außerdem: Was passiert, wenn Sie dies nicht aus der REPL, sondern aus einer Funktion heraus disassemblieren?

Ich kann auch nicht umhin zu bemerken, dass die erste Version eine tatsächliche ausführt
Division, während die zweite x/2 in eine Multiplikation umgewandelt hat.

Danke für die Kommentare.

@eschnett LazyArray ist bereits unveränderlich, und ich verwende @inbounds in Schleifen. Nachdem Sie den Gist unter https://gist.github.com/nalimilan/d345e1c080984ed4c89a ausgeführt haben, können Sie in einer Schleife überprüfen, was dies ergibt:

function test_lazy!(newa, a)
    a1 = LazyArray(a, Divide(), (2.0,))
    a2 = LazyArray(a1, Add(), (3.0,))
    collect!(newa, a2)
    newa
end
<strong i="11">@code_native</strong> test_lazy!(newa, a); 

Also muss ich vielleicht nur Inlining erzwingen können? Bei meinen Versuchen ändert das Hinzufügen @inline zu getindex die Zeitangaben nicht.

@toivoh Was könnte erklären, dass im letzteren Fall die Teilung nicht vereinfacht wird?

Ich habe weiter mit der Zwei-Argument-Version (namens LazyArray2 ) experimentiert. Es stellt sich heraus, dass es für eine einfache Operation wie x .+ y tatsächlich schneller ist, ein LazyArray2 zu verwenden als das aktuelle .+ , und es ist auch ziemlich nah an expliziten Schleifen (diese sind für 1000 Aufrufe , siehe https://gist.github.com/nalimilan/d345e1c080984ed4c89a):

# With LazyArray2, filling existing array
elapsed time: 0.028212517 seconds (56000 bytes allocated)

# With explicit loop, filling existing array
elapsed time: 0.013500379 seconds (0 bytes allocated)

# With LazyArray2, allocating a new array before filling it
elapsed time: 0.098324278 seconds (80104000 bytes allocated, 74.16% gc time)

# Using .+ (thus allocating a new array)
elapsed time: 0.078337337 seconds (80712000 bytes allocated, 52.46% gc time)

Es sieht also so aus, als ob diese Strategie alle elementweisen Operationen ersetzen kann, einschließlich .+ , .* usw. Operatoren.

Es sieht auch sehr wettbewerbsfähig aus, allgemeine Operationen wie das Berechnen der Summe quadrierter Differenzen entlang einer Dimension einer Matrix zu erreichen, dh sum((x .- y).^2, 1) (siehe noch einmal das Wesentliche):

# With LazyArray2 and LazyArray (no array allocated except the result)
elapsed time: 0.022895754 seconds (1272000 bytes allocated)

# With explicit loop (no array allocated except the result)
elapsed time: 0.020376307 seconds (896000 bytes allocated)

# With element-wise operators (temporary copies allocated)
elapsed time: 0.331359085 seconds (160872000 bytes allocated, 50.20% gc time)

@nalimilan
Ihr Ansatz mit LazyArrays scheint ähnlich zu sein wie die Dampffusion von Haskell [1, 2]. Vielleicht können wir Ideen aus diesem Bereich anwenden?

[1] http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.104.7401
[2] http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.421.8551

@vchuravy Danke. Das ist zwar ähnlich, aber komplexer, weil Julia ein imperatives Modell verwendet. Im Gegensatz dazu muss der Compiler in Haskell eine Vielzahl von Fällen behandeln und sogar SIMD-Probleme behandeln (die von LLVM zu einem späteren Zeitpunkt in Julia behandelt werden). Aber ehrlich gesagt bin ich nicht in der Lage, alles in diesen Papieren zu analysieren.

@nalimilan Ich kenne das Gefühl. Ich fand das zweite Papier besonders interessant, da es sich mit Generalized Stream Fusion befasst, was anscheinend ein nettes Berechnungsmodell über Vektoren ermöglicht.

Ich denke, der Hauptpunkt, den wir daraus ziehen sollten, ist, dass Konstrukte wie map und reduce in Kombination mit Faulheit ausreichend schnell sein können (oder sogar schneller als explizite Schleifen).

Soweit ich das beurteilen kann, sind geschweifte Klammern immer noch in der Aufrufsyntax verfügbar. Was wäre, wenn daraus func{x} würde? Vielleicht etwas zu verschwenderisch?

Gibt es zum Thema Fast Vectoring (im Sinne von SIMD) eine Möglichkeit, die Art und Weise nachzuahmen, wie Eigen es tut?

Hier ist ein Vorschlag, alle aktuellen elementweisen Operationen durch eine Verallgemeinerung dessen zu ersetzen, was ich oben LazyArray und LazyArray2 genannt habe. Dies beruht natürlich auf der Annahme, dass wir dies für alle Funktionen schnell machen können, ohne auf Funktoren aus NumericFuns.jl angewiesen zu sein.

1) Fügen Sie eine neue Syntax f.(x) oder f$(x) oder was auch immer hinzu, die ein LazyArray erstellen würde, das f() für jedes Element von x $ aufruft.

2) Verallgemeinern Sie diese Syntax, indem Sie der aktuellen Funktionsweise von $ x broadcast f.(x, y, ...) f$(x, y, ...) LazyArray z x , y , ... um ihnen eine gemeinsame Größe zu geben. Dies würde natürlich spontan durch Berechnungen der Indizes erfolgen, so dass die erweiterten Arrays nicht tatsächlich zugewiesen werden.

3) Machen Sie .+ , .- , .* , ./ , .^ usw. und verwenden Sie LazyArray anstelle von broadcast .

4) Führen Sie einen neuen Zuweisungsoperator .= oder $= ein, der (durch Aufrufen collect ) ein LazyArray in ein reales Array umwandeln würde (von einem Typ, der von seinem Typ abhängt Eingaben über Heraufstufungsregeln und eines Elementtyps abhängig vom Elementtyp der Eingaben und der aufgerufenen Funktion).

5) Ersetzen Sie vielleicht sogar broadcast durch einen Aufruf von LazyArray und eine sofortige collect ion der Ergebnisse in ein echtes Array.

Punkt 4 ist ein Schlüsselpunkt: Elementweise Operationen würden niemals echte Arrays zurückgeben, sondern immer LazyArray s, sodass beim Kombinieren mehrerer Operationen keine Kopien erstellt werden und Schleifen aus Effizienzgründen verschmolzen werden können. Dies ermöglicht das Aufrufen von Reduktionen wie sum für das Ergebnis, ohne die Temporäre zuzuweisen. Ausdrücke dieser Art wären also sowohl für dichte Arrays als auch für spärliche Matrizen idiomatisch und effizient:

y .= sqrt.(x .+ 2)
y .=  √π exp.(-x .^ 2) .* sin.(k .* x) .+ im * log.(x .- 1)
sum((x .- y).^2, 1)

Ich denke, dass die Rückgabe dieser Art von leichtem Objekt perfekt in das neue Bild von Array-Ansichten und Transpose / CTranspose passt. Das bedeutet, dass Sie in Julia komplexe Operationen sehr effizient mit einer dichten und lesbaren Syntax ausführen können, obwohl Sie in einigen Fällen explizit copy aufrufen müssen, wenn Sie ein "Pseudo-Array" benötigen, von dem Sie unabhängig werden möchten das reale Array, auf dem es basiert.

Das klingt wirklich nach einem wichtigen Feature. Das aktuelle Verhalten von elementweisen Operatoren ist eine Falle für neue Benutzer, da die Syntax nett und kurz ist, aber die Leistung normalerweise schrecklich schlecht ist, anscheinend schlechter als in Matlab. Erst letzte Woche hatten mehrere Threads zu Julia-Benutzern Leistungsprobleme, die mit einem solchen Design verschwinden würden:
https://groups.google.com/d/msg/julia-users/t0KvvESb9fA/6_ZAp2ujLpMJ
https://groups.google.com/d/msg/julia-users/DL8ZsK6vLjw/w19Zf1lVmHMJ
https://groups.google.com/d/msg/julia-users/YGmDUZGOGgo/LmsorgEfXHgJ

Für die Zwecke dieses Problems würde ich die Syntax von der Faulheit trennen. Dein Vorschlag ist aber interessant.

Es scheint einen Punkt zu geben, an dem es nur noch _so viele Punkte_ gibt. Insbesondere das mittlere Beispiel wäre besser geschrieben als

x .|> x->exp(-x ^ 2) * sin(k * x) + im * log(x - 1)

die nur grundlegende Funktionen und ein effizientes map ( .|> ) erfordert.

Das ist ein interessanter Vergleich:

y .=  √π exp.(-x .^ 2) .* sin.(k .* x) .+ im * log.(x .- 1)
y =  [√π exp(-x[i]^ 2) .* sin(k * x[i]) .+ im * log(x[i] - 1) for i = 1:length(x)]

Wenn Sie den for ... -Teil abziehen, ist das Verständnis nur ein Zeichen länger. Ich hätte fast lieber eine abgekürzte Verständnissyntax als all diese Punkte.

Ein 1d-Verständnis bewahrt die Form nicht, aber jetzt, wo wir for i in eachindex(x) haben, könnte sich das auch ändern.

Ein Problem mit Comprehensions ist, dass sie DataArrays nicht unterstützen.

Ich denke, es könnte sich lohnen, sich eine ganze Reihe von Dingen anzusehen, die auf .Net passiert sind und der LazyArray-Idee sehr ähnlich sehen. Im Wesentlichen sieht es für mich ziemlich nah an einem Ansatz im LINQ-Stil aus, bei dem Sie eine Syntax haben, die wie das elementweise Zeug aussieht, das wir gerade haben, aber diese Syntax baut tatsächlich einen Ausdrucksbaum auf, und dieser Ausdrucksbaum wird dann später auf eine effiziente Weise ausgewertet . Ist das irgendwie nah?

Auf .Net gingen sie mit dieser Idee weit: Sie könnten diese Ausdrucksbäume parallel auf mehreren CPUs ausführen (indem Sie .AsParallel() hinzufügen), oder Sie könnten sie auf einem großen Cluster mit DryadLINQ oder sogar auf einem http:/ ausführen.

Meiner Meinung nach geht Blaze auch in diese Richtung, dh eine Möglichkeit, auf einfache Weise Objekte zu konstruieren, die Berechnungen beschreiben, und dann können Sie verschiedene Ausführungsmaschinen dafür haben.

Ich bin mir nicht sicher, ob dies sehr klar ist, aber es scheint mir, dass dieses ganze Problem im Zusammenhang damit betrachtet werden sollte, wie man auf niedrigem Niveau effizienten SIMD-ähnlichen Code generieren kann und wie dies für GPU-Computing, Clustering und parallel verwendet werden kann Berechnung usw.

Ja, Sie haben Recht, dass das längere Beispiel zu viele Punkte hat. Aber die beiden kürzeren sind typischer, und in diesem Fall ist es wichtig, eine kurze Syntax zu haben. Ich würde gerne Syntax von Faulheit trennen, aber wie Ihre Kommentare zeigen, scheint es sehr schwierig zu sein, wir mischen immer die beiden!

Man könnte sich vorstellen, die Comprehension-Syntax anzupassen, so etwas wie y = [sqrt(x + 2) over x] . Aber wie @johnmyleswhite bemerkte, sollten sie dann DataArrays unterstützen, aber auch Matrizen mit geringer Dichte und jeden neuen Array-Typ. Das ist also wieder ein Fall, in dem Syntax und Features gemischt werden.

Grundsätzlich denke ich, dass zwei Merkmale, die mein Vorschlag gegenüber den Alternativen bietet, folgende sind:
1) Unterstützung für zuweisungsfreie direkte Zuweisung mit y[:] = sqrt.(x .+ 2) .
2) Unterstützung für zuteilungsfreie Ermäßigungen wie sum((x .- y).^2, 1) .

Könnte das mit anderen Lösungen (ohne Berücksichtigung von Syntaxproblemen) bereitgestellt werden?

@davidanthoff Danke, schau es dir jetzt an (ich denke, LazyArray könnte gemacht werden, um auch paralleles Rechnen zu unterstützen).

Vielleicht könnte dies mit Generatoren kombiniert werden – sie sind auch eine Art Lazy Array. Ich mag die [f(x) over x] -Verständnissyntax etwas, obwohl sie für Neulinge konzeptionell schwierig sein könnte (da effektiv derselbe Name sowohl für die Elemente als auch für das Array selbst verwendet wird). Wenn Comprehensions ohne Klammern einen Generator erzeugen würden (wie ich vor langer Zeit damit gespielt habe), dann wäre es natürlich, diese neuen Over-X-Style-Comprehensions ohne Klammern zu verwenden, um ein LazyArray zurückzugeben, anstatt es sofort zu sammeln.

@mbauman Ja, Generatoren und Lazy Arrays haben viele Eigenschaften gemeinsam. Die Idee, Klammern zu verwenden, um ein Generator-/Lazy-Array zu sammeln, und sie nicht hinzuzufügen, um das Lazy-Objekt zu erhalten, klingt cool. In Bezug auf meine obigen Beispiele könnte man also sowohl 1) y[:] = sqrt(x + 2) over x als auch sum((x - y)^2 over (x, y), 1) schreiben (obwohl ich es selbst für Neulinge natürlich finde, lassen wir das Problem von over für die Bikeshedding-Session und konzentriere dich zuerst auf das Wesentliche).

Ich mag die f(x) over x Idee. Wir könnten sogar f(x) for x verwenden, um ein neues Schlüsselwort zu vermeiden. Tatsächlich funktioniert [f(x) for x=x] bereits. Wir müssten dann Verständnisse machen, die map entsprechen, damit sie für Nicht-Arrays funktionieren. Array wäre nur der Standardwert.

Ich muss zugeben, dass mir auch die over -Idee gefällt. Ein Unterschied zwischen over als Map und for als List Comprehension ist, was bei mehreren Iteratoren passiert: [f(x, y) for x=x, y=y] ergibt eine Matrix. Für den Map-Fall möchte man generell noch einen Vektor, dh [f(x, y) over x, y] wäre äquivalent zu [f(x, y) for (x,y) = zip(x, y)]] . Aus diesem Grund denke ich immer noch, dass es sich lohnt, ein zusätzliches Schlüsselwort over einzuführen, da, wie dieses Problem aufgeworfen hat, map ing über mehrere Vektoren sehr verbreitet ist und knapp gehalten werden muss.

Hey, ich habe Jeff von der Syntax überzeugt! ;-)

Dies gehört neben #4470 und wird daher vorerst zu 0.4-Projekten hinzugefügt.

Wenn ich den Kern der Diskussion verstehe, besteht das Hauptproblem darin, dass wir eine Mapping-ähnliche Syntax erhalten möchten, die:

  • arbeitet mit verschiedenen Datentypen, wie DataArrays, nicht nur mit nativen Arrays;
  • ist so schnell wie eine manuell geschriebene Schleife.

Es könnte möglich sein, dies mit Inlining zu tun, aber seien Sie wirklich vorsichtig, um sicherzustellen, dass Inlining funktioniert.

Was ist mit einem anderen Ansatz: Verwenden von Makros in Abhängigkeit vom abgeleiteten Datentyp. Wenn wir schlussfolgern können, dass die Datenstruktur DataArray ist, verwenden wir das Map-Makro, das von der DataArrays-Bibliothek bereitgestellt wird. Wenn es sich um SomeKindOfStream handelt, verwenden wir eine bereitgestellte Stream-Bibliothek. Wenn wir den Typ nicht ableiten können, verwenden wir einfach die dynamisch verteilte allgemeine Implementierung.

Dies könnte die Ersteller von Datenstrukturen zwingen, solche Makros zu schreiben, aber es wäre nur erforderlich, wenn der Autor wollte, dass es eine wirklich effiziente Ausführung hat.

Wenn das, was ich schreibe, unklar ist, könnte etwas wie [EXPR for i in collection if COND] in eval(collection_mapfilter_macro(:(i), :(EXPR), :(COND))) übersetzt werden, wobei collection_mapfilter_macro basierend auf dem abgeleiteten Sammlungstyp ausgewählt wird.

Nein, so etwas wollen wir nicht. Wenn DataArray map (oder das Äquivalent) definiert, sollte seine Definition immer für DataArrays aufgerufen werden, unabhängig davon, was gefolgert werden kann.

Bei diesem Thema geht es eigentlich nicht um die Implementierung, sondern um die Syntax. Im Moment sind viele Leute daran gewöhnt, sin(x) implizit abzubilden, wenn x ein Array ist, aber es gibt viele Probleme mit diesem Ansatz. Die Frage ist, welche alternative Syntax akzeptabel wäre.

1) Unterstützung für zuweisungsfreie direkte Zuweisung mit y[:] = sqrt.(x .+ 2)
2) Unterstützung für zuteilungsfreie Ermäßigungen wie sum((x .- y).^2, 1)

y = √π exp(-x^2) * sin(k*x) + im * log(x-1)

Wenn ich mir diese drei Beispiele von anderen anschaue, denke ich, dass dies mit der for -Syntax in etwa so enden würde:
1) y[:] = [ sqrt(x + 2) for x ])
2) sum([ (x-y)^2 for x,y ], 1)
und
y = [ √π exp(-x^2) * sin(k*x) + im * log(x-1) for x,k ]

Das gefällt mir sehr gut! Die Tatsache, dass ein temporäres Array erstellt wird, ist ziemlich explizit und es ist immer noch lesbar und knapp.

Kleine Frage, könnte x[:] = [ ... for x ] etwas Magie haben, um das Array zu mutieren, ohne ein temporäres zuzuweisen?
Ich bin mir nicht sicher, ob dies viel Nutzen bringen würde, aber ich kann mir vorstellen, dass es bei großen Arrays helfen würde.
Ich kann jedoch glauben, dass es sich um einen völlig anderen Fischkessel handelt, der an anderer Stelle diskutiert werden sollte.

@ Mike43110 Ihr x[:] = [ ... for x ] könnte x[:] = (... for x) geschrieben werden, wobei die RHS einen Generator erstellt, der Element für Element gesammelt wird, um x zu füllen, ohne eine Kopie zuzuweisen. Das war die Idee hinter meinem LazyArray -Experiment oben.

Die [f <- y] -Syntax wäre schön, wenn sie mit einer Int[f <- y] -Syntax für eine Karte kombiniert würde, die ihren Ausgabetyp kennt und nicht aus f(y[1]) interpolieren muss, was die anderen Elemente sein werden.

Da dies insbesondere eine intuitive Benutzeroberfläche für mapslices bietet, [f <- rows(A)] wobei rows(A) (oder columns(A) oder slices(A, dims) ) ein Slice zurückgibt

map(f, slice::Slice) = mapslices(f, slice.A, slice.dims)

Wenn Sie die Indizierung hinzufügen, wird dies etwas schwieriger. Zum Beispiel

f(x[:,j]) .* g(x[i,:])

Es ist schwer, die Prägnanz davon zu erreichen. Die Explosion des Verständnisstils ist ziemlich schlecht:

[f(x[m,j])*g(x[i,n]) for m=1:size(x,1), n=1:size(x,2)]

wobei, um die Sache noch schlimmer zu machen, Cleverness erforderlich war, um zu wissen, dass dies ein Fall einer verschachtelten Iteration ist und nicht mit einem einzigen over erledigt werden kann. Obwohl f und g etwas teuer sind, könnte dies schneller sein:

[f(x[m,j]) for m=1:size(x,1)] .* [g(x[i,n]) for _=1, n=1:size(x,2)]

aber noch länger.

Diese Art von Beispiel scheint für "Punkte" zu sprechen, da dies f.(x[:,j]) .* g.(x[i,:]) ergeben könnte.

@JeffBezanson Ich bin mir nicht sicher, was die Absicht deines Kommentars ist. Hat jemand vorgeschlagen, die .* -Syntax loszuwerden?

Nein; Ich konzentriere mich hier auf f und g . Dies ist ein Beispiel, bei dem Sie nicht einfach over x am Ende der Zeile hinzufügen können.

OK, ich verstehe, ich hatte das Ende des Kommentars verpasst. Tatsächlich ist die Punktversion in diesem Fall schöner.

Bei Array-Ansichten gibt es jedoch eine einigermaßen effiziente (AFAICT) und nicht so hässliche Alternative:
[ f(y) * g(z) for y in x[:,j], z in x[i,:] ]

Könnte das obige Beispiel durch Verschachteln über Schlüsselwörter gelöst werden?

f(x)*g(y) over x,y

wird interpretiert als

[f(x)*g(y) for (x,y) = zip(x,y)]

wohingegen

f(x)*g(y) over x over y

wird

[f(x)*g(y) for x=x, y=y]

Dann wäre das obige spezifische Beispiel so etwas wie

f(x[:,n])*g(x[m,:]) over x[:,n] over x[m,:]

EDIT: Im Nachhinein ist das nicht annähernd so prägnant, wie ich dachte.

@JeffBezanson Wie wäre es mit

f(x[:i,n]) * g(x[m,:i]) over i

ergibt das Äquivalent von f.(x[:,n] .* g.(x[m,:]) . Die neue Syntax x[:i,n] bedeutet, dass i lokal als Iterator über die Indizes des Containers x[:,n] eingeführt wird. Ob das umsetzbar ist, weiß ich nicht. Aber es scheint (prima facie) weder hässlich noch umständlich, und die Syntax selbst gibt dem Iterator Grenzen, nämlich 1:length(x[:,n]). Was Schlüsselwörter betrifft, kann „over“ signalisieren, dass der gesamte Bereich verwendet werden soll, während „for“ verwendet werden kann, wenn der Benutzer einen Teilbereich von 1:length(x[:,n]) angeben möchte:

f(x[:i,n]) * g(x[m,:i]) for i in 1:length(x[:,n])-1 .

@davidagold , :i bedeutet bereits das Symbol i .

Ah ja, guter Punkt. Nun, solange Punkte Freiwild sind, was ist damit

f(x[.i,n]) * g(x[m,.i]) over i

wobei der Punkt anzeigt, dass i lokal als Iterator über 1:length(x[:,n) eingeführt wird. Ich nehme an, dass dies im Wesentlichen die Punktnotation von der Änderung der Funktionen zur Änderung der Arrays oder besser gesagt ihrer Indizes umschaltet. Dies würde einen vor dem "Dot Creep" bewahren, bemerkte Jeff:

[ f(g(e^(x[m,.i]))) * p(e^(f(y[.i,n]))) over i ]

im Gegensatz zu

f.(g.(e.^(x[m,:]))) .* p.(e.^(f.(y[:,n])))

obwohl ich annehme, dass letzteres etwas kürzer ist. [BEARBEITEN: Wenn es möglich ist, over i wegzulassen, wenn es keine Mehrdeutigkeit gibt, dann hat man tatsächlich eine etwas kürzere Syntax:

[ f(g(e^(x[m,.i]))) * p(e^(f(y[.i,n]))) ] ]

Ein möglicher Vorteil der Comprehension-Syntax besteht darin, dass sie eine breitere Palette von Mustern elementweiser Operationen ermöglichen könnte. Wenn der Parser beispielsweise verstanden hätte, dass die Indizierung mit i in x[m, .i] implizit modulo length(x[m,:]) ist, dann könnte man schreiben

[ f(x[.i]) * g(y[.j]) over i, j=-i ]

Um die Elemente von x mit den Elementen von y in umgekehrter Reihenfolge zu multiplizieren, dh das erste Element von x mit dem letzten Element von y usw Man könnte schreiben

[ f(x[.i]) * g(y[.j]) over i, j=i+1 ]

um das i -te Element von x mit dem i+1 -ten Element von y zu multiplizieren (wobei das letzte Element von x multipliziert würde durch das erste Element von y , da Indizierung in diesem Zusammenhang als Modulo-Länge(x) verstanden wird). Und wenn p::Permutation (1, ..., Länge(x)) permutiert, könnte man schreiben

[ f(x[.i]) * g(y[.j]) over i, j=p(i) ]

um das i -te Element von x mit dem p(i) -ten Element von y zu multiplizieren.

Wie auch immer, das ist nur eine bescheidene Meinung eines Außenstehenden zu einem völlig spekulativen Thema. =p Ich schätze die Zeit, die sich jemand nimmt, um darüber nachzudenken.

Eine aufgemotzte Version von Vectorize, die Recycling im R-Stil verwendet, könnte ziemlich nützlich sein. Das heißt, Argumente, die nicht der Größe des größten Arguments entsprechen, werden durch Recycling erweitert. Dann können Benutzer alles, was sie wollen, einfach vektorisieren, unabhängig von der Anzahl der Argumente usw.

unvectorized_sum(a, b, c, d) = a + b + c + d
vectorized_sum = @super_vectorize(unvectorized_sum)

a = [1, 2, 3, 4]
b = [1, 2, 3]
c = [1, 2]
d = 1

A = [1, 2, 3, 4]
B = [1, 2, 3, 1]
C = [1, 2, 1, 2]
D = [1, 1, 1, 1]

vectorized_sum(a, b, c, d) = vectorized_sum(A, B, C, D) = [4, 7, 8, 8]

Ich neige dazu zu denken, dass beim Recycling zu viel Sicherheit gegen Bequemlichkeit eingetauscht wird. Mit Recycling ist es sehr einfach, fehlerhaften Code zu schreiben, der ohne Fehler ausgeführt wird.

Als ich das erste Mal von diesem Verhalten von R las, fragte ich mich sofort, warum jemand das jemals für eine gute Idee halten würde. Das ist eine wirklich seltsame und überraschende Sache, die man implizit bei Arrays mit nicht übereinstimmender Größe macht. Es mag eine Handvoll Fälle geben, in denen Sie das kleinere Array auf diese Weise erweitern möchten, aber Sie möchten möglicherweise auch Nullen auffüllen oder die Endelemente wiederholen, oder Extrapolation oder einen Fehler oder eine beliebige Anzahl anderer anwendungsabhängiger Auswahlmöglichkeiten.

Ob @super_vectorize verwendet wird oder nicht, liegt in den Händen des Benutzers. Es wäre auch möglich, Warnungen für verschiedene Fälle auszusprechen. Zum Beispiel in R,

c(1, 2, 3) + c(1, 2)
[1] 2 4 4
Warning message:
In c(1, 2, 3) + c(1, 2) :
  longer object length is not a multiple of shorter object length

Ich habe keine Einwände dagegen, dies zu einer optionalen Sache zu machen, die Benutzer wählen können, ob sie sie verwenden möchten oder nicht, aber es muss nicht in der Basissprache implementiert werden, wenn es genauso gut in einem Paket gemacht werden kann.

@vectorize_1arg und @vectorize_2arg sind beide bereits in Base enthalten, und die Optionen, die sie einem Benutzer geben, scheinen etwas begrenzt zu sein.

Aber diese Ausgabe konzentriert sich auf das Design eines Systems zum Entfernen @vectorize_1arg und @vectorize_2arg von Base. Unser Ziel ist es, vektorisierte Funktionen aus der Sprache zu entfernen und durch eine bessere Abstraktion zu ersetzen.

Zum Beispiel kann Recycling geschrieben werden als

[ A[i] + B[mod1(i,length(B))] for i in eachindex(A) ]

was für mich der idealen Art, es zu schreiben, ziemlich nahe kommt. Niemand muss es für Sie einbauen. Die Hauptfragen sind (1) kann dies prägnanter gemacht werden, (2) wie kann es auf andere Containertypen erweitert werden.

Als ich mir den Vorschlag von @davidagold ansah , fragte ich mich, ob var: nicht für so etwas verwendet werden könnte, wo die Variable der Name vor dem Doppelpunkt wäre. Ich habe gesehen, dass diese Syntax früher A[var:end] bedeutet, also scheint sie verfügbar zu sein.

f(x[:,j]) .* g(x[i,:]) wäre dann f(x[a:,j]) * g(x[i,b:]) for a, b , was nicht viel schlimmer ist.

Mehrere Doppelpunkte wären jedoch etwas seltsam.

f(x[:,:,j]) .* g(x[i,:,:]) -> f(x[a:,a:,j]) * g(x[i,b:,b:]) for a, b war mein erster Gedanke dazu.

Ok, also hier ist ein kurzer Stich in ein Programm zum Recycling. Es sollte in der Lage sein, n-dimensionale Arrays zu verarbeiten. Wahrscheinlich wäre es möglich, Tupel analog zu Vektoren einzubauen.

using DataFrames

a = [1, 2, 3]
b = 1
c = [1 2]
d = <strong i="6">@data</strong> [NA, 2, 3]

# coerce an array to a certain size using recycling
coerce_to_size = function(argument, dimension_extents...)

  # number of repmats needed, initialized to 1
  dimension_ratios = [dimension_extents...]

  for dimension in 1:ndims(argument)

    dimension_ratios[dimension] = 
      ceil(dimension_extents[dimension] / size(argument, dimension))
  end

  # repmat array to at least desired size
  if typeof(argument) <: AbstractArray
    rep_to_size = repmat(argument, dimension_ratios...)
  else
    rep_to_size = 
      fill(argument, dimension_ratios...)
  end

  # cut down array to exactly desired size
  dimension_ranges = [1:i for i in dimension_extents]
  dimension_ranges = tuple(dimension_ranges...)

  rep_to_size = getindex(rep_to_size, dimension_ranges...)  

end

recycle = function(argument_list...)

  # largest dimension in arguments
  max_dimension = maximum([ndims(i) for i in argument_list])
  # initialize dimension extents to 1
  dimension_extents = [1 for i in 1:max_dimension]

  # loop through argument and dimension
  for argument_index in 1:length(argument_list)
    for dimension in 1:ndims(argument_list[argument_index])
      # find the largest size for each dimension
      dimension_extents[dimension] = maximum([
        size(argument_list[argument_index], dimension),
        dimension_extents[dimension]
      ])
    end
  end

  expand_arguments = 
    [coerce_to_size(argument, dimension_extents...) 
     for argument in argument_list]
end

recycle(a, b, c, d)

mapply = function(FUN, argument_list...)
  argument_list = recycle(argument_list...)
  FUN(argument_list...)
end

mapply(+, a, b, c, d)

Dies ist eindeutig nicht der eleganteste oder schnellste Code (ich bin ein neuer R-Immigrant). Ich bin mir nicht sicher, wie ich von hier zu einem @vectorize- Makro komme.

EDIT: kombinierte redundante Schleife
EDIT 2: Zwang zur Größe getrennt. funktioniert derzeit nur für 0-2 Dimensionen.
EDIT 3: Ein etwas eleganterer Weg, dies zu tun, wäre, einen speziellen Array-Typ mit Mod-Indizierung zu definieren. Das ist,

special_array = [1 2; 3 5]
special_array.dims = (10, 10, 10, 10)
special_array[4, 1, 9, 7] = 3

EDIT 4: Dinge, die ich mich frage, existieren, weil dies schwer zu schreiben war: eine n-dimensionale Verallgemeinerung von hcat und vcat? Eine Möglichkeit, ein n-dimensionales Array (das der Größe eines bestimmten Arrays entspricht) mit Listen oder Tupeln der Indizes jeder bestimmten Position zu füllen? Eine n-dimensionale Verallgemeinerung von repmat?

[pao: Syntaxhervorhebung]

Sie möchten wirklich keine Funktionen mit der foo = function(x,y,z) ... end -Syntax in Julia definieren, obwohl es funktioniert. Dadurch entsteht eine nicht konstante Bindung des Namens an eine anonyme Funktion. In Julia ist es üblich, generische Funktionen zu verwenden, und die Bindungen an Funktionen sind automatisch konstant. Sonst wirst du eine schreckliche Leistung bekommen.

Ich sehe nicht, warum repmat hier notwendig ist. Arrays, die mit dem Index jeder Position gefüllt sind, sind auch ein Warnzeichen: Es sollte nicht notwendig sein, einen großen Teil des Speichers zu verwenden, um so wenig Informationen darzustellen. Ich glaube, solche Techniken sind wirklich nur in Sprachen nützlich, in denen alles "vektorisiert" werden muss. Es scheint mir der richtige Ansatz zu sein, einfach eine Schleife auszuführen, in der einige Indizes transformiert werden, wie in https://github.com/JuliaLang/julia/issues/8450#issuecomment -111898906.

Ja, das macht Sinn. Hier ist ein Anfang, aber ich habe Probleme herauszufinden, wie ich die Schleife am Ende mache und dann ein @vectorize -Makro erstelle.

function non_zero_mod(big::Number, little::Number)
  result = big % little
  result == 0 ? little : result
end

function mod_select(array, index...)
  # just return singletons
  if !(typeof(array) <: AbstractArray) return array end
  # find a new index with moded values
  transformed_index = 
      [non_zero_mod( index[i], size(array, i) )
       for i in 1:ndims(array)]
  # return value at moded index
  array[transformed_index...]
end

function mod_value_list(argument_list, index...)
  [mod_select(argument, index...) for argument in argument_list]
end

mapply = function(FUN, argument_list...)

  # largest dimension in arguments
  max_dimension = maximum([ndims(i) for i in argument_list])
  # initialize dimension extents to 1
  dimension_extents = [1 for i in 1:max_dimension]

  # loop through argument and dimension
  for argument_index in 1:length(argument_list)
    for dimension in 1:ndims(argument_list[argument_index])
      # find the largest size for each dimension
      dimension_extents[dimension] = maximum([
        size(argument_list[argument_index], dimension),
        dimension_extents[dimension]
      ])
    end
  end

  # more needed here
  # apply function over arguments using mod_value_list on arguments at each position
end

In dem Vortrag erwähnte @JeffBezanson die Syntax sin(x) over x , warum nicht eher so etwas wie:
sin(over x) ? (oder verwenden Sie ein Zeichen anstelle von over als Schlüsselwort)

Sobald dies gelöst ist, können wir auch #11872 beheben

Ich hoffe, ich komme nicht zu spät zur Party, aber ich möchte dem Syntaxvorschlag von @davidagold nur +1 geben. Es ist konzeptionell klar, knapp und fühlt sich beim Schreiben wirklich natürlich an. Ich bin mir nicht sicher, ob . das beste Identifizierungszeichen wäre oder wie machbar eine tatsächliche Implementierung wäre, aber man könnte einen Proof-of-Concept erstellen, indem man ein Makro verwendet, um es auszuprobieren (im Wesentlichen wie @devec , könnte aber sogar einfacher zu implementieren sein).

Es hat auch den Vorteil, dass es sich in die vorhandene Array-Verständnis-Syntax „einfügt“:

result = [g(f(.i), h(.j)) over i, j]

vs.

result = [g(f(_i), h(_j)) for _i in eachindex(i), _j in eachindex(j)]

Der Hauptunterschied zwischen den beiden besteht darin, dass erstere mehr Einschränkungen hinsichtlich der Formerhaltung aufweisen würde, da dies eine Karte impliziert.

over , range und window haben im OLAP-Bereich einigen Stand der Technik als Modifikatoren für die Iteration, dies scheint konsistent zu sein.

Ich bin nicht scharf auf die . -Syntax, da dies wie ein Schleichen in Richtung Leitungsrauschen erscheint.

$ ist vielleicht konsistent, intern die Werte der Iteration von i, j in den Ausdruck?

result = [g(f($i), h($j)) over i, j]

Können wir für die automatische Vektorisierung eines Ausdrucks nicht taint einen der Vektoren im Ausdruck eingeben und das Typsystem den Ausdruck in den Vektorraum heben lassen?

Ich mache es ähnlich wie bei Zeitreihenoperationen, wo mich die Ausdruckskraft von Julia schon schreiben lässt

ts_a = GetTS( ... )
ts_b = GetTS( ... ) 
factors = [ 1,  2, 3 ]

ts_x = ts_a * 2 + sin( ts_a * factors ) + ts_b 

die, wenn sie beobachtet wird, eine Zeitreihe von Vektoren ausgibt.

Der Hauptteil, der fehlt, ist die Fähigkeit, vorhandene Funktionen automatisch in den Raum zu heben. Dies muss von Hand erfolgen

Im Wesentlichen möchte ich in der Lage sein, etwas wie das Folgende zu definieren ...

abstract TS{K}
function {F}{K}( x::TS{K}, y::TS{K} ) = tsjoin( F, x, y ) 
# tsjoin is a time series iteration operator

und dann in der Lage sein, sich auf bestimmte Operationen zu spezialisieren

function mean{K}(x::TS{K}) = ... # my hand rolled form

Hallo @JeffBezanson ,

Wenn ich das richtig verstehe, würde ich gerne eine Lösung für Ihren Kommentar zur JuliaCon 2015 bezüglich eines oben gemachten Kommentars vorschlagen:
"[...] Und es ist albern, Bibliotheksschreibern zu sagen, dass sie @vectorize auf alle geeigneten Funktionen setzen sollen; Sie sollten in der Lage sein, einfach eine Funktion zu schreiben, und wenn jemand sie für jedes Element berechnen möchte, das er verwendet, map."
(Aber ich werde nicht auf das andere grundlegende Problem eingehen "[..] kein wirklich überzeugender Grund, warum sin, exp usw. implizit über Arrays abgebildet werden sollten.")

In Julia v0.40 konnte ich eine Lösung finden, die (meiner Meinung nach) etwas schöner ist als @vectrorize :

abstract Vectorizable{Fn}
#Could easily have added extra argument to Vectorizable, but want to show inheritance case:
abstract Vectorizable2Arg{Fn} <: Vectorizable{Fn}

call{F}(::Type{Vectorizable2Arg{F}}, x1, x2) = eval(:($F($x1,$x2)))
function call{F,T1,T2}(fn::Type{Vectorizable2Arg{F}}, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

#Function in need of vectorizing:
function _myadd(x::Number, y::Number)
    return x+y+1
end

#"Register" the function as a Vectorizable 2-argument (alternative to @vectorize):
typealias myadd Vectorizable2Arg{:_myadd}

<strong i="13">@show</strong> myadd(5,6)
<strong i="14">@show</strong> myadd(collect(1:10),collect(21:30.0)) #Type stable!

Dies ist mehr oder weniger vernünftig, ähnelt aber etwas der @vectorize- Lösung. Damit die Vektorisierung elegant ist, schlage ich Julia vor, Folgendes zu unterstützen:

abstract Vectorizable <: Function
abstract Vectorizable2Arg <: Vectorizable

function call{T1,T2}(fn::Vectorizable2Arg, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

#Note: by default, functions would normally be <: Function:
function myadd(x::Number, y::Number) <: Vectorizable2Arg
    return x+y+1
end

Das ist es! Eine Funktion, die von einer vektorisierbaren Funktion erbt, würde sie vektorisierbar machen.

Ich hoffe, das entspricht dem, wonach Sie gesucht haben.

Grüße,

MA

Wie erbt eine Funktion ohne Mehrfachvererbung von Vectorizable und von etwas anderem? Und wie verknüpfen Sie die Vererbungsinformationen für bestimmte Methoden mit den Vererbungsinformationen für eine generische Funktion?

@ma-laforge Sie können das bereits tun --- definieren Sie einen Typ myadd <: Vectorizable2Arg , implementieren Sie dann call für myadd auf Number .

Danke dafür @JeffBezanson!

Tatsächlich kann ich fast meine Lösung fast so gut aussehen wie das, was ich will:

abstract Vectorizable
#Could easily have parameterized Vectorizable, but want to show inheritance case:
abstract Vectorizable2Arg <: Vectorizable

function call{T1,T2}(fn::Vectorizable2Arg, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

#SECTION F: Function in need of vectorizing:
immutable MyAddType <: Vectorizable2Arg; end
const myadd = MyAddType()
function call(::MyAddType, x::Number, y::Number)
    return x+y+1
end

<strong i="7">@show</strong> myadd(5,6)
<strong i="8">@show</strong> myadd(collect(1:10),collect(21:30.0)) #Type stable

Jetzt fehlt nur noch eine Möglichkeit, eine beliebige Funktion zu "subtypisieren", sodass der gesamte Abschnitt F durch die elegantere Syntax ersetzt werden könnte:

function myadd(x::Number, y::Number) <: Vectorizable2Arg
    return x+y+1
end

HINWEIS: Ich habe den Typ „MyAddType“ und den Funktionsnamen in ein Singleton-Objekt „myadd“ umgewandelt, weil ich die resultierende Syntax schöner finde, als wenn man Type{Vectorizable2Arg} in der Aufrufsignatur verwenden würde:

function call{T1,T2}(fn::Type{Vectorizable2Arg}, v1::Vector{T1}, v2::Vector{T2})

Leider klingt es nach Ihrer Antwort so, als wäre dies _keine_ angemessene Lösung für die "Dummheit" des @vectorize- Makros.

Grüße,

MA

@johnmyleswhite :

Ich würde gerne auf Ihren Kommentar antworten, aber ich glaube nicht, dass ich ihn verstehe. Könntest Du das erläutern?

Eines kann ich sagen:
Es gibt nichts Besonderes an "Vektorisierbar". Die Idee ist, dass jeder seine eigene Funktions-"Klasse" definieren kann (Bsp.: MyFunctionGroupA<:Function ). Sie könnten dann Aufrufe von Funktionen dieses Typs abfangen, indem sie ihre eigene "Aufruf"-Signatur definieren (wie oben gezeigt).

Abgesehen davon: Mein Vorschlag ist, dass in Base definierte Funktionen Base.Vectorizable <: Function (oder etwas Ähnliches) verwenden sollten, um automatisch vektorisierte Algorithmen automatisch zu generieren.

Ich würde dann Modulentwicklern vorschlagen, ihre eigenen Funktionen mit einem Muster wie dem folgenden zu implementieren:

myfunction(x::MyType, y::MyType) <: Base.Vectorizable

Natürlich müssten sie ihre eigene Version von promote_type(::Type{MyType},::Type{MyType}) bereitstellen - wenn nicht bereits standardmäßig MyType zurückgegeben wird.

Wenn der Standard-Vektorisierungsalgorithmus nicht ausreicht, hindert nichts den Benutzer daran, seine eigene Hierarchie zu implementieren:

MyVectorizable{nargs} <: Function
call(fn::MyVectorizable{2}, x, y) = ...

myfunction(x::MyType, y:MyType) <: MyVectorizable{2}

MA

@ma-laforge, Entschuldigung für die Unklarheit. Meine Sorge ist, dass jeder Hierarchie immer wichtige Informationen fehlen werden, da Julia eine einfache Vererbung hat, was erfordert, dass Sie sich für jede Funktion auf einen einzigen übergeordneten Typ festlegen. Wenn Sie etwas wie myfunction(x::MyType, y::MyType) <: Base.Vectorizable verwenden, wird Ihre Funktion nicht davon profitieren, wenn jemand anderes ein Konzept wie Base.NullableLiftable definiert, das Funktionen von Nullwerten automatisch generiert.

Sieht so aus, als wäre dies kein Problem mit Traits (vgl. https://github.com/JuliaLang/julia/pull/13222). Damit verbunden ist auch die neue Möglichkeit, Methoden als rein zu deklarieren (https://github.com/JuliaLang/julia/pull/13555), was automatisch implizieren könnte, dass eine solche Methode vektorisierbar ist (zumindest für Single-Argument-Methoden).

@johnmyleswhite ,

Wenn ich das richtig verstehe: Ich glaube nicht, dass das speziell für _diesen_ Fall ein Problem darstellt. Das liegt daran, dass ich ein Entwurfsmuster vorschlage. Ihre Funktionen _müssen_ nicht von Base.Vectorizable erben ... Sie können Ihre eigenen verwenden.

Ich weiß nicht wirklich viel über NullableLiftables (ich scheine das in meiner Version von Julia nicht zu haben). Unter der Annahme, dass es von Base.Function erbt (was in meiner Version von Julia ebenfalls nicht möglich ist):

NullableLiftable <: Function

Ihr Modul könnte dann (nur einmal) einen _neuen_ vektorisierbaren Untertyp implementieren:

abstract VectorizableNullableLiftable <: NullableLiftable

function call{T1,T2}(fn::VectorizableNullableLiftable, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

Von nun an würde also jeder, der eine Funktion <: VectorizableNullableLiftable definiert, automatisch Ihren Vektorisierungscode anwenden!

function mycooladdon(scalar1, scalar2) <: VectorizableNullableLiftable
...

Ich verstehe, dass es immer noch etwas mühsam ist, mehr als einen vektorisierbaren Typ zu haben (und ein bisschen unelegant) ... Aber zumindest würde es eine der lästigen Wiederholungen (1) in Julia beseitigen (müssen _jeden_ neu hinzugefügten Funktion mit einem Aufruf von @vectorize_Xarg).

(1) Dies setzt voraus , dass Julia die Vererbung von Funktionen unterstützt (z. B.: myfunction(...)<: Vectorizable ) - was in v0.4.0 anscheinend nicht der Fall ist. Die Lösung, die ich in Julia 0.4.0 zum Laufen gebracht habe, ist nur ein Hack ... Sie müssen Ihre Funktion noch registrieren ... nicht viel besser als @vectorize_Xarg aufzurufen

MA

Ich denke immer noch, dass es eine Art falsche Abstraktion ist. Eine Funktion, die "vektorisiert" werden kann oder sollte, ist keine bestimmte Art von Funktion. _Jede_ Funktion kann an map übergeben werden und gibt ihr dieses Verhalten.

Übrigens, mit der Änderung, an der ich im Zweig jb/functions arbeite, können Sie function f(x) <: T (allerdings natürlich nur für die erste Definition von f ).

Ok, ich glaube, ich verstehe besser, wonach Sie suchen ... und es ist nicht das, was ich vorgeschlagen habe. Ich denke, das könnte auch ein Teil der Probleme sein, die @johnmyleswhite mit meinen Vorschlägen hatte ...

...Aber wenn ich jetzt verstehe, worum es geht, erscheint mir die Lösung noch einfacher:

function call{T1,T2}(fn::Function, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

myadd(x::Number, y::Number) = x+y+1

Da myadd vom Typ Function ist, sollte es von der Funktion call abgefangen werden... was es tut:

call(myadd,collect(1:10),collect(21:30.0)) #No problem

Aber call versendet Funktionen aus irgendeinem Grund nicht automatisch (nicht sicher warum):

myadd(collect(1:10),collect(21:30.0)) #Hmm... Julia v0.4.0 does not dispatch this to call...

Aber ich kann mir vorstellen, dass dieses Verhalten nicht allzu schwer zu ändern sein sollte . Ich persönlich weiß nicht, was ich davon halten soll, solche allumfassenden Catch-All-Funktionen zu erstellen, aber es hört sich so an, als wäre das das, was Sie wollen.

Etwas Seltsames ist mir aufgefallen: Julia vektorisiert Funktionen bereits automatisch, wenn sie nicht eingegeben werden:

myadd(x,y) = x+y+1 #This gets vectorized automatically, for some reason

RE: Übrigens...:
Cool! Ich frage mich, welche netten Dinge ich durch Subtyping-Funktionen tun kann :).

Julia vektorisiert Funktionen bereits automatisch, wenn sie nicht typisiert sind

Es scheint so, weil der innerhalb der Funktion verwendete + -Operator vektorisiert ist.

Aber ich kann mir vorstellen, dass dieses Verhalten nicht allzu schwer zu ändern sein sollte. Ich persönlich weiß nicht, was ich davon halten soll, solche allumfassenden Catch-All-Funktionen zu erstellen, aber es hört sich so an, als wäre das das, was Sie wollen.

Ich teile Ihre Vorbehalte. Sie können vernünftigerweise keine Definition haben, die besagt "So rufen Sie eine Funktion auf", da jede Funktion selbst sagt, was zu tun ist, wenn sie aufgerufen wird - das ist es, was eine Funktion ist!

Ich hätte sagen sollen: ... benutzerdefinierte Nicht-Unicode-Infix-Operatoren, da ich nicht glaube, dass wir von Benutzern verlangen möchten, Unicode-Zeichen einzugeben, um auf eine solche Kernfunktionalität zuzugreifen. Obwohl ich sehe, dass $ tatsächlich einer der hinzugefügten ist, also danke dafür! Wow, das funktioniert also tatsächlich heute in Julia (auch wenn es noch nicht "schnell" ist ... noch):

Julia> ($) = Karte
julia> sin $ (0,5 * (abs2 $ (xy)))

@binarybana wie wäre es mit / \mapsto ?

julia> x, y = rand(3), rand(3);

julia> ↦ = map    # \mapsto<TAB>
map (generic function with 39 methods)

julia> sin ↦ (0.5 * (abs2 ↦ (x-y)))
3-element Array{Float64,1}:
 0.271196
 0.0927406
 0.0632608

Es gibt auch:

FWIW, ich würde zumindest zunächst davon ausgehen, dass \mapsto eine alternative Syntax für Lambdas war, da sie häufig in der Mathematik und im Wesentlichen (in ihrer ASCII-Inkarnation -> ) auch in Julia verwendet wird . Ich denke, das wäre ziemlich verwirrend.

Apropos Mathematik … In der Modelltheorie habe ich gesehen, dass map ausgedrückt wird, indem eine Funktion ohne Klammern auf ein Tupel angewendet wird. Das heißt, wenn \bar{a}=(a_1, \dots, a_n) , dann ist f(\bar{a}) f(a_1, \dots, a_n) (dh im Wesentlichen apply ) und f\bar{a} ist (f(a_1), \dots, f(a_n)) (dh map ). Nützliche Syntax zum Definieren von Homomorphismen etc., aber nicht so einfach auf eine Programmiersprache übertragbar :-}

Wie wäre es mit einer der anderen Alternativen wie \Mapsto , würden Sie es mit => (Paar) verwechseln? Ich denke, beide Symbole sind hier nebeneinander zu unterscheiden:

  • ->

Es gibt viele Symbole, die gleich aussehen, was wäre der Grund dafür, so viele davon zu haben, wenn wir nur die verwenden, die sehr unterschiedlich aussehen oder reines ASCII sind?

Ich denke, das wäre ziemlich verwirrend.

Ich denke, Dokumentation und Erfahrung lösen dies, stimmen Sie zu?

Es gibt auch viele andere pfeilähnliche Symbole, ich weiß ehrlich gesagt nicht, wofür sie in der Mathematik verwendet werden, sonst habe ich diese nur vorgeschlagen, weil sie map in ihren Namen haben! :Lächeln:

Ich denke, mein Punkt ist, dass -> Julias Versuch ist, in ASCII darzustellen. Es scheint also nicht ratsam zu sein, für etwas anderes zu verwenden. Es ist nicht so, dass ich sie optisch nicht unterscheiden könnte :-)

Mein Bauchgefühl ist nur, dass wir, wenn wir gut etablierte mathematische Symbole verwenden, vielleicht zumindest darüber nachdenken sollten, wie sich die Verwendung von Julia von der etablierten Verwendung unterscheidet. Es scheint logisch, Symbole mit map in ihren Namen auszuwählen, aber in diesem Fall bezieht sich das auf die Definition einer Karte (oder von Karten verschiedener Typen oder der Typen solcher Karten). Das gilt mehr oder weniger auch für die Verwendung in Pair, wo Sie, anstatt eine vollständige Funktion zu definieren, indem Sie definieren, worauf ein Parameter abbildet, tatsächlich auflisten, worauf ein gegebenes Argument (Parameterwert) abbildet – dh es ist ein Element von explizit Aufzählungsfunktion, konzeptionell (z. B. ein Wörterbuch).

@Ismael-VC Das Problem mit Ihrem Vorschlag ist, dass Sie map zweimal aufrufen müssen, während die ideale Lösung keine temporären Arrays beinhalten und auf map((a, b) -> sin(0.5 * abs2(a-b)), x, y) reduzieren würde. Außerdem ist das zweimalige Wiederholen sowohl visuell als auch für die Eingabe nicht großartig (wofür ein ASCII-Äquivalent gut wäre).

R-Benutzer mögen diese Idee vielleicht nicht, aber wenn wir dazu übergehen, die aktuelle Infix-Makro-Sonderfallanalyse von ~ zu verwerfen (Pakete wie GLM und DataFrames müssten auf Makro-Analyse ihrer Formel-DSLs umstellen, siehe https:/ /github.com/JuliaStats/GLM.jl/issues/116), die das seltene Gut eines Infix-ASCII-Operators freigeben würde.

a ~ b könnte in base als map(a, b) definiert werden, und vielleicht könnte a .~ b als broadcast(a, b) definiert werden? Wenn es als herkömmlicher Infix-Operator analysiert wird, können Makro-DSLs wie eine Emulation der R-Formelschnittstelle ihre eigene Interpretation des Operators in Makros implementieren, wie es JuMP mit <= und == tut .

Es ist vielleicht nicht der schönste Vorschlag, aber die Abkürzungen in Mathematica sind es auch nicht, wenn Sie sie überbeanspruchen ... mein Favorit ist .#&/@

:+1: Für weniger spezielle Gehäuse und mehr Allgemeingültigkeit und Konsistenz sehen die Bedeutungen, die Sie für ~ und .~ vorschlagen, für mich großartig aus.

+1 Tony.

@tkelman Um es klar zu sagen, wie würden Sie zB sin(0.5 * abs2(a-b)) vollständig vektorisiert schreiben?

Vermutlich mit Verständnis. Ich glaube nicht, dass Infix Map für Varargs oder In-Place funktionieren würde, daher löst die Möglichkeit einer freien Syntax nicht alle Probleme.

Das würde dieses Problem also nicht wirklich beheben. :-/

Bisher ist die sin(0.5 * abs2(a-b)) over (a, b) -Syntax (oder eine Variante, die möglicherweise einen Infix-Operator verwendet) am attraktivsten.

Der Titel dieser Ausgabe lautet „Alternative Syntax für map(func, x) “. Die Verwendung eines Infix-Operators löst die Map/Loop-Fusion nicht, um Temporäre zu eliminieren, aber ich denke, dass dies ein noch umfassenderes, verwandtes, aber technisch separates Problem sein kann als die Syntax.

Ja, ich stimme @tkelman zu, der Punkt ist, eine alternative Syntax für map zu haben, weshalb ich vorgeschlagen habe, \mapsto , zu verwenden. Was @nalimilan erwähnt, scheint breiter und besser geeignet für ein anderes Problem zu sein, IMHO, How to fully vecotrize expressions ?

<rambling>
Diese Ausgabe läuft jetzt seit mehr als einem Jahr (und könnte endlos weitergehen, wie es viele andere Ausgaben jetzt tun)! Aber wir könnten jetzt Alternative syntax for map(func, x) haben . Von ±450 julianischen Mitwirkenden konnten nur 41 dieses Problem finden und/oder bereit sein, eine Meinung zu teilen (das ist viel für ein Github-Problem, aber in diesem Fall eindeutig nicht ausreichend), insgesamt gibt es nicht so viele verschiedene Vorschläge (das sind nicht nur geringfügige Variationen desselben Konzepts).

Ich weiß, dass einige von Ihnen die Idee nicht mögen oder keinen Wert darin sehen, Umfragen/Umfragen zu machen (:schockiert:), aber da ich für so etwas niemanden um Erlaubnis fragen muss, werde ich es trotzdem tun. Es ist irgendwie traurig, wie wir unsere Community und unsere sozialen Netzwerke und andere Communities nicht voll ausschöpfen, und noch trauriger, dass wir den Wert nicht erkennen. Mal sehen, ob ich mehr unterschiedliche und frische Meinungen sammeln oder zumindest überprüfen kann herauszufinden, was die Mehrheit über die aktuellen Meinungen in dieser speziellen Ausgabe denkt, als Experiment und zu sehen, wie es läuft. Vielleicht ist es tatsächlich nutzlos, vielleicht auch nicht, es gibt nur einen Weg, es wirklich zu wissen.
</rambling>

@Ismael-VC: Wenn Sie wirklich eine Umfrage machen möchten, müssen Sie sich zunächst die Frage, die Sie stellen möchten, sorgfältig überlegen. Sie können nicht erwarten, dass jeder den gesamten Thread durchliest und die zur Diskussion stehenden Optionen einzeln zusammenfasst.

map(func, x) deckt auch Dinge wie map(v -> sin(0.5 * abs2(v)), x) , und darüber wurde in diesem Thread gesprochen. Lassen Sie uns dies nicht in einen anderen Thread verschieben, da dies es schwieriger machen würde, alle oben diskutierten Vorschläge im Auge zu behalten.

Ich bin nicht dagegen, Syntax für den einfachen Fall der Anwendung einer generischen Funktion mit map hinzuzufügen, aber wenn wir es tun, wäre es meiner Meinung nach eine gute Idee, gleichzeitig das Gesamtbild zu betrachten. Wenn das nicht gewesen wäre, hätte das Problem schon vor langer Zeit behoben werden können.

@Ismael-VC Umfragen werden hier meiner Meinung nach wahrscheinlich nicht helfen. Wir versuchen nicht herauszufinden, welche von mehreren Lösungen die beste ist, sondern eine Lösung zu finden, die noch niemand wirklich gefunden hat. Diese Diskussion ist schon lang und hat viele Leute involviert, ich glaube nicht, dass das Hinzufügen von mehr helfen wird.

@Ismael-VC Das ist in Ordnung, machen Sie gerne eine Umfrage. Tatsächlich habe ich in der Vergangenheit ein paar Doodle-Umfragen zu Themen durchgeführt (zB http://doodle.com/poll/s8734pcue8yxv6t4). Meiner Erfahrung nach stimmen in Umfragen die gleichen oder weniger Personen ab als in Thementhreads diskutiert werden. Es ist sinnvoll für sehr spezifische, oft oberflächliche/Syntaxprobleme. Aber wie soll eine Umfrage neue Ideen generieren, wenn Sie nur aus vorhandenen Optionen auswählen können?

Natürlich ist das eigentliche Ziel dieser Ausgabe, implizit vektorisierte Funktionen zu eliminieren. Theoretisch reicht dafür die Syntax für map aus, weil all diese Funktionen in jedem Fall nur map machen.

Ich habe versucht, dafür nach vorhandener mathematischer Notation zu suchen, aber Sie neigen dazu, Kommentare zu sehen, die besagen, dass die Operation zu unwichtig ist, um eine Notation zu haben! Bei beliebigen Funktionen im mathematischen Kontext kann ich das fast glauben. Am nächsten scheint jedoch die Hadamard-Produktnotation zu sein, die einige Verallgemeinerungen enthält: https://en.wikipedia.org/wiki/Hadamard_product_ (matrices)#Analogous_Operations

Das lässt uns mit sin∘x zurück, wie @rfourquet vorgeschlagen hat. Scheint nicht allzu hilfreich zu sein, da es Unicode erfordert und sowieso nicht allgemein bekannt ist.

@nalimilan , ich würde denken, du würdest einfach sin(0.5 * abs2(a-b)) ~ (a,b) machen, was zu map((a,b)->sin(0.5 * abs2(a-b)), (a,b)) übersetzt würde. Ich bin mir nicht sicher, ob das so richtig ist, aber ich denke, es würde funktionieren.

Ich bin auch vorsichtig, mich zu sehr mit dem Problem „Lass-mich-dir-einen-riesigen-komplizierten-Ausdruck-und-du-perfekt-automatisch-vektorisiere-es-für-mich-zu-erzeugen“ zu befassen. Ich denke, die ultimative Lösung in dieser Hinsicht besteht darin, einen vollständigen DAG mit Ausdrücken/Aufgaben + Abfrageplanung usw. aufzubauen. Aber ich denke, das ist ein viel größerer Fisch zum Braten, als nur eine praktische Syntax für map zu haben.

@quinnj Ja, das ist im Wesentlichen die oben vorgeschlagene over -Syntax, außer mit einem Infix-Operator.

Ernster Kommentar: Ich denke, Sie werden SQL wahrscheinlich neu erfinden, wenn Sie diese Idee weit genug verfolgen, da SQL im Wesentlichen eine Sprache zum Zusammenstellen elementweiser Funktionen vieler Variablen ist, die anschließend über eine zeilenweise "Vektorisierung" angewendet werden.

@johnmyleswhite stimme zu, sieht aus wie ein DSL alias Linq

Auf dem geposteten Thread-Thema könnten Sie den |> 'Pipe'-Operator spezialisieren und die Map-Style-Funktionalität erhalten. Sie können es lesen, indem Sie die Funktion an die Daten weiterleiten. Als zusätzlichen Bonus können Sie dasselbe verwenden, um Funktionskompositionen durchzuführen.

julia> (|>)(x::Function, y...) = map(x, y... )
|> (generic function with 8 methods)

julia> (|>)(x::Function, y::Function) = (z...)->x(y(z...))
|> (generic function with 8 methods)

julia> sin |> cos |> [ 1,2,3 ]
3-element Array{Float64,1}:
  0.514395
 -0.404239
 -0.836022

julia> x,y = rand(3), rand(3)
([0.8883630054185454,0.32542923024720194,0.6022157767415313],    [0.35274912207468145,0.2331784754319688,0.9262490059844113])

julia> sin |> ( 0.5 *( abs( x - y ) ) )
3-element Array{Float64,1}:
 0.264617
 0.046109
 0.161309

@johnmyleswhite Das stimmt, aber es gibt lohnenswerte Zwischenziele, die recht bescheiden sind. In meinem Zweig ist die map -Version von vektorisierten Ausdrücken mit mehreren Operationen bereits schneller als das, was wir jetzt haben. Daher ist es etwas dringend, herauszufinden, wie ein reibungsloser Übergang dazu möglich ist.

@johnmyleswhite Nicht sicher. Bei vielen SQL geht es um das Auswählen, Ordnen und Zusammenführen von Zeilen. Wir sprechen hier nur von der elementweisen Anwendung einer Funktion. Außerdem bietet SQL keine Syntax, um Reduktionen (zB SUM ) von elementweisen Operationen (zB > , LN ) zu unterscheiden. Letztere werden einfach automatisch vektorisiert, so wie derzeit in Julia.

@JeffBezanson Das Schöne an der Verwendung von \circ ist, dass, wenn Sie eine indizierte Familie als Funktion aus dem Indexsatz interpretieren (was die standardmäßige mathematische "Implementierung" ist), die Zuordnung _is_ einfach eine Zusammensetzung ist. Also (sin ∘ x)(i)=sin(x(i)) , oder besser gesagt sin(x[i]) .

Die Verwendung der Pipe, wie @mdcfrancis erwähnt, wäre im Wesentlichen nur eine "Diagrammreihenfolge" -Komposition, die in der Mathematik (oder insbesondere in CS-Anwendungen der Kategorientheorie) häufig mit einem (möglicherweise dicken) Semikolon erfolgt - aber wir haben bereits die Pipe Betreiber natürlich.

Wenn keiner dieser Kompositionsoperatoren in Ordnung ist, könnte man andere verwenden. Zum Beispiel verwenden zumindest einige Autoren das niedrige \cdot für die abstrakte Pfeil/Morphismus-Komposition, da es im Wesentlichen die „Multiplikation“ des Gruppoids (mehr oder weniger) von Pfeilen ist.

Und wenn man ein ASCII-Analogon wollte: Es gibt auch Autoren, die tatsächlich einen Punkt verwenden, um Multiplikationen anzuzeigen. (Ich habe vielleicht gesehen, dass einige es auch für die Komposition verwendet haben; ich kann mich nicht erinnern.)

Also könnte man sin . x haben … aber ich denke, das wäre verwirrend :-}

Dennoch … diese letzte Analogie könnte ein Argument für einen der wirklich frühen Vorschläge sein, dh sin.(x) . (Oder vielleicht ist das weit hergeholt.)

Versuchen wir es aus einem anderen Blickwinkel, erschießen Sie mich nicht.

Wenn wir .. durch collect(..(A,B)) == ((a[1],..., a[n]), (b[1], ...,b[n])) == zip(A,B) definieren, dann gilt dies formal mit T[x,y,z] = [T(x), T(y), T(z)]

map(f,A,B) = [f(a[1],b[1]), ..., f(a[n],b[n])] = f[zip(A,B)...] = f[..(A,B)]

Das motiviert mindestens eine Syntax für map, die die Syntax für die Array-Konstruktion nicht stört. Mit :: oder table führt die Erweiterung f[::(A,B)] = [f(a[i], b[j]) for i in 1:n, j in 1:n] zumindest zu einem zweiten interessanten Anwendungsfall.

Überlegen Sie sich genau, welche Frage Sie stellen möchten.

@toivoh Danke, werde ich. Ich evaluiere derzeit mehrere Umfrage-/Umfragesoftware. Außerdem werde ich nur über die bevorzugte Syntax abfragen, diejenigen, die den gesamten Thread lesen möchten, werden es einfach tun, lassen Sie uns einfach nicht davon ausgehen, dass niemand sonst daran interessiert ist.

eine Lösung finden, die noch niemand wirklich gefunden hat

@nalimilan niemand unter uns, das heißt. :Lächeln:

Wie soll eine Umfrage neue Ideen hervorbringen, wenn Sie nur aus vorhandenen Optionen auswählen können?
dieselben oder weniger Personen stimmen in Umfragen ab als in Thementhreads diskutiert werden.

@JeffBezanson Ich freue mich zu hören, dass du bereits Umfragen durchgeführt hast, weiter so!

  • Wie haben Sie für Ihre Umfragen geworben?
  • Von der Umfrage-/Umfragesoftware, die ich bisher evaluiert habe, lässt kwiksurveys.com Benutzer ihre eigenen Meinungen hinzufügen, anstatt die Option _Keine dieser Optionen ist für mich_.

sin∘x Scheint nicht allzu hilfreich zu sein, da es Unicode erfordert und ohnehin nicht allgemein bekannt ist.

Wir haben so viele Unicode, lasst es uns verwenden, wir haben sogar eine nette Möglichkeit, sie mit Tab-Vervollständigung zu verwenden, was ist dann falsch daran, sie zu verwenden? Wenn es nicht bekannt ist, lassen Sie uns dokumentieren und aufklären, wenn es nicht existiert, was ist falsch mit es erfinden? Müssen wir wirklich darauf warten, dass jemand anderes es erfindet und verwendet, damit wir es als Präzedenzfall betrachten und erst danach darüber nachdenken können?

hat einen Präzedenzfall, also ist das Problem, dass es Unicode ist? warum? wann werden wir dann anfangen, den Rest des nicht weithin bekannten Unicode zu verwenden? noch nie?

Nach dieser Logik ist Julia sowieso nicht allgemein bekannt, aber diejenigen, die es lernen wollen, werden es tun. Es macht einfach keinen Sinn für mich, meiner sehr bescheidenen Meinung nach.

Fair genug, ich bin nicht total gegen . Unicode zu verlangen, um eine ziemlich grundlegende Funktion zu verwenden, ist nur ein Zeichen dagegen. Nicht unbedingt genug, um es vollständig zu versenken.

Wäre es völlig verrückt, * als ASCII-Alternative zu verwenden/zu überladen? Ich würde sagen, es könnte argumentiert werden, dass es mathematisch sinnvoll ist, aber ich denke, es könnte manchmal schwierig sein, seine Bedeutung zu erkennen … (Andererseits, wenn es auf die map -Funktionalität beschränkt ist, dann map ist bereits eine ASCII-Alternative, oder?)

Das Schöne an der Verwendung von \circ ist, dass, wenn Sie eine indizierte Familie als eine Funktion aus dem Indexsatz interpretieren (was die standardmäßige mathematische „Implementierung“ ist), die Abbildung _eine_ einfache Zusammensetzung ist.

Ich bin mir nicht sicher, ob ich das kaufe.

@hayd Welcher Teil davon? Dass eine indizierte Familie (z. B. eine Sequenz) als eine Funktion aus der Indexmenge betrachtet werden kann, oder dass das Mapping darüber zu einer Komposition wird? Oder dass dies in diesem Fall eine nützliche Perspektive ist?

Die ersten beiden (mathematischen) Punkte sind meiner Meinung nach ziemlich unstrittig. Aber, ja, ich werde nicht stark dafür plädieren, dies hier zu verwenden – es war meistens ein „Ah, das passt irgendwie!“ Reaktion.

@mlhetland |> ist ziemlich nah an -> und funktioniert heute - es hat auch den "Vorteil", richtig assoziativ zu sein.

x = parse( "sin |> cos |> [1,2]" )
:((sin |> cos) |> [1,2])

@mdcfrancis Sicher. Aber das stellt die von mir skizzierte Kompositionsinterpretation auf den Kopf. Das heißt, sin∘x wäre gleichbedeutend mit x |> sin , oder?

PS: Vielleicht ist es in der "Algebra" verloren gegangen, aber nur Funktionen in der typisierten Array-Konstruktion T[x,y,z] zuzulassen, so dass f[x,y,z] [f(x),f(y),f(z)] ist, ergibt direkt

map(f,A) == f[A...]

was gut lesbar ist und als Syntax behandelt werden könnte.

Das ist schlau. Aber ich vermute, dass, wenn wir es zum Laufen bringen können, sin[x...] wirklich an Ausführlichkeit gegenüber sin(x) oder sin~x usw. verliert.

Was ist mit der Syntax [sin xs] ?

Dies ähnelt in der Syntax dem Array-Verständnis [sin(x) for x in xs] .

@mlhetland Sünde |> x === map( Sünde, x )

Das wäre die umgekehrte Reihenfolge der aktuellen Bedeutung der Funktionsverkettung . Nicht, dass es mir nichts ausmachen würde, eine bessere Verwendung für diesen Operator zu finden, aber ich bräuchte eine Übergangszeit.

@mdcfrancis Ja, ich verstehe, dass Sie darauf abzielen. Was die Dinge umkehrt (wie @tkelman wiederholt) wrt. die Kompositionsinterpretation, die ich skizziert habe.

Ich denke, die Integration von Vektorisierung und Verkettung wäre ziemlich cool. Ich frage mich, ob Worte die klarsten Operatoren wären.
Etwas wie:

[1, 2] mapall
  +([2, 3]) map
  ^(2, _) chain
  { a = _ + 1
    b = _ - 1
    [a..., b...] } chain
  sum chain
  [ _, 2, 3] chain
  reduce(+, _)

Mehrere Karten hintereinander könnten automatisch zu einer einzigen Karte kombiniert werden, um die Leistung zu verbessern. Beachten Sie auch, dass ich davon ausgehe, dass die Karte eine Art Auto-Broadcast-Funktion haben wird. Das Ersetzen von [1, 2] durch _ am Anfang könnte stattdessen eine anonyme Funktion erstellen. Beachten Sie, dass ich die Magrittr-Regeln von R zum Verketten verwende (siehe meinen Beitrag im Verkettungsthread).

Vielleicht sieht das langsam eher nach DSL aus.

Ich verfolge dieses Problem schon lange und habe es bis jetzt noch nicht kommentiert, aber meiner Meinung nach gerät es allmählich außer Kontrolle.

Ich unterstütze nachdrücklich die Idee einer sauberen Syntax für map. Ich mag den Vorschlag von @tkelman von ~ am meisten, da er für solche grundlegenden Funktionen innerhalb von ASCII bleibt, und ich mag sin~x sehr. Dies würde eine ziemlich ausgeklügelte Abbildung im Stil eines Einzeilers ermöglichen, wie oben besprochen. Die Verwendung von sin∘x wäre auch OK. Für etwas Komplizierteres neige ich dazu zu denken, dass eine richtige Schleife viel klarer ist (und normalerweise die beste Leistung). Ich mag nicht zu viel „magisches“ Senden, es macht den Code viel schwerer zu befolgen. Eine explizite Schleife ist normalerweise übersichtlicher.

Das soll nicht heißen, dass eine solche Funktionalität nicht hinzugefügt werden sollte, aber lassen Sie uns zuerst eine schöne, prägnante map -Syntax haben, besonders da sie superschnell werden wird (aus meinen Tests des jb/functions -Zweigs). .

Beachten Sie, dass einer der Effekte von jb/functions darin besteht, dass broadcast(op, x, y) genauso performant ist wie die angepasste Version x .op y , die das Broadcasting manuell auf op spezialisiert hat.

Für etwas Komplizierteres neige ich dazu zu denken, dass eine richtige Schleife viel klarer ist (und normalerweise die beste Leistung). Ich mag nicht zu viel „magisches“ Senden, es macht den Code viel schwerer zu befolgen. Eine explizite Schleife ist normalerweise übersichtlicher.

Ich bin nicht einverstanden. exp(2 * x.^2) ist perfekt lesbar und weniger ausführlich als [exp(2 * v^2) for v in x] . Die Herausforderung hier besteht meiner Meinung nach darin, das Fangen von Personen zu vermeiden, indem sie die erstere verwenden (die Kopien zuweist und keine Operationen fusioniert): Dafür müssen wir eine Syntax finden, die kurz genug ist, damit die langsame Form veraltet ist.

Mehr Gedanken. Es gibt mehrere mögliche Dinge, die Sie beim Aufrufen einer Funktion tun möchten:

Keine Argumente durchlaufen (Kette)
Nur das verkettete Argument durchlaufen (map)
alle Argumente durchlaufen (mapall)

Jedes der oben genannten Elemente könnte geändert werden, indem:
Markieren eines Elements zum Durchlaufen (~)
Markieren eines Elements, das nicht durchgeschleift werden soll (ein zusätzlicher Satz von [ ] )

Vereinbare Elemente sollten automatisch gehandhabt werden, ohne Rücksicht auf die Syntax.
Das Erweitern von Singleton-Dimensionen sollte automatisch erfolgen, wenn mindestens zwei Argumente durchlaufen werden

Rundfunk macht nur dann einen Unterschied, wenn es eine Dimension gegeben hätte
Missverhältnis sonst. Wenn Sie also sagen, nicht senden, meinen Sie, eine zu geben
Fehler stattdessen, wenn die Größe des Arguments nicht übereinstimmt?

sin[x...] verliert wirklich an Ausführlichkeit gegenüber sin(x) oder sin~x usw.

Um den Gedanken fortzusetzen, ist die Karte sin[x...] eine weniger eifrige Version von [f(x...)] .
Die Syntax

[exp(2 * (...x)^2)]

oder etwas Ähnliches wie [exp(2 * (x..)^2)] wäre verfügbar und selbsterklärend, wenn jemals eine echte stillschweigende Funktionsverkettung eingeführt wird.

@nalimilan ja, aber das passt in meine Einzeiler-Kategorie, von der ich sagte, dass sie ohne Schleife in Ordnung ist.

Wo wir alle unsere Wünsche aufzählen: Viel wichtiger wäre mir, dass die Ergebnisse von map ohne Zuweisung oder Kopieren zuordenbar sind. Dies ist ein weiterer Grund, warum ich immer noch Schleifen für leistungskritischen Code bevorzuge, aber wenn dies gemildert werden kann (#249 sieht derzeit nicht nach einem hoffnungsvollen ATM aus), wird dies alles viel attraktiver.

Kartenergebnisse ohne Zuweisung oder Kopieren zuordenbar

Kannst du das etwas erweitern? Sie können das Ergebnis von map sicherlich mutieren.

Ich nehme an, er meint, die Ausgabe von map in einem vorab zugewiesenen Array zu speichern.

Ja genau. Entschuldigung, falls das schon möglich ist.

Ach, natürlich. Wir haben map! , aber wie Sie sehen, fragt #249 nach einem schöneren Weg, dies zu tun.

@jtravs Ich habe oben eine Lösung mit LazyArray (https://github.com/JuliaLang/julia/issues/8450#issuecomment-65106563) vorgeschlagen, aber bisher war die Leistung nicht ideal.

@toivoh Ich habe diesen Beitrag mehrfach bearbeitet, nachdem ich ihn gepostet habe. Die Frage, über die ich mir Sorgen gemacht habe, ist, wie man herausfindet, welche Argumente durchlaufen werden müssen und welche nicht (also könnte Mapall klarer sein als Broadcast). Ich denke, wenn Sie mehr als ein Argument durchlaufen, sollte das Erweitern von Singleton-Dimensionen, um vergleichbare Arrays zu erzeugen, bei Bedarf immer durchgeführt werden, denke ich.

Ja map! ist genau richtig. Es wäre gut, wenn ein netter Syntaxzucker, der hier ausgearbeitet wurde, auch diesen Fall abdecken würde. Könnten wir nicht haben, dass x := ... die RHS implizit auf x abbildet.

Ich habe ein Paket namens ChainMap erstellt, das Mapping und Verkettung integriert.

Hier ein kurzes Beispiel:

<strong i="7">@chain</strong> begin
  [1, 2]
  -(1)
  (_, _)
  map_all(+)
  <strong i="8">@chain_map</strong> begin
    -(1)
    ^(2. , _)
  end
  begin
    a = _ - 1
    b = _ + 1
    [a, b]
  end
  sum
end

Ich habe immer wieder darüber nachgedacht und ich glaube, ich habe endlich eine konsistente und julianische Syntax für die Zuordnung von Arrays herausgefunden, die aus dem Array-Verständnis abgeleitet wurden. Der folgende Vorschlag ist julianisch, weil er auf der bereits etablierten Array-Verständnissprache aufbaut.

  1. Ausgehend von f[a...] , was eigentlich von @Jutho , der Konvention, vorgeschlagen wurde
    das für a Vektoren a, b
f[a...] == map(f, a[:])
f[a..., b...] == map(f, a[:], b[:])
etc

die keine neuen Symbole einführt.

2.) Darüber hinaus würde ich die Einführung eines zusätzlichen Operators vorschlagen: einen _formerhaltenden_ Splatting-Operator .. (sagen wir). Dies liegt daran, dass ... ein _flacher_ Spatting-Operator ist, also sollte f[a...] einen Vektor und kein Array zurückgeben, selbst wenn a n -dimensional ist. Wenn .. gewählt wird, dann in diesem Zusammenhang,

f[a.., ] == map(f, a)
f[a.., b..] == map(f, a, b)

und das Ergebnis erbt die Form der Argumente. Rundfunk zulassen

f[a.., b..] == broadcast(f, a, b)

würde das Schreiben erlauben, denkt wie

sum(*[v.., v'..]) == dot(v,v)

Heureka?

Dies hilft nicht bei der Zuordnung von Ausdrücken, oder? Einer der Vorteile der over -Syntax ist, wie sie mit Ausdrücken funktioniert:

sin(x * (y - 2)) over x, y  == map((x, y) -> sin(x * (y - 2)), x, y) 

Nun, möglicherweise über [sin(x.. * y..)] oder sin[x.. * y..] oben, wenn Sie dies zulassen möchten. Ich mag das etwas mehr als die Over-Syntax, weil es einen visuellen Hinweis darauf gibt, dass die Funktionsoperatoren auf den Elementen und nicht auf den Containern liegen.

Aber können Sie das nicht so vereinfachen, dass x.. einfach über x wird? @johansigfrids Beispiel wäre also:

sin(x.. * (y.. - 2))  == map((x, y) -> sin(x * (y - 2)), x, y)

@jtravs Aufgrund des Umfangs ( [println(g(x..))] vs. println([g(x..)]) ) und der Konsistenz [x..] = x .

Eine weitere Möglichkeit besteht darin, x.. = x[:, 1], x[:, 2], etc. als partiellen Splat der führenden Subarrays (Spalten) und ..y als partiellen Splat der nachfolgenden Subarrays ..y = y[1,:], y[2,:] zu nehmen. Wenn beide über unterschiedliche Indizes laufen, deckt dies viele interessante Fälle ab

[f(v..)] == [f(v[i]) for i in 1:m ]
[v.. * v..] == [v[i] * v[i] for 1:m]
[v.. * ..v] == [v[i] * v[j] for i in 1:m, j in 1:n]
[f(..A)] == [f(A[:, j]) for j in 1:n]
[f(A..)] == [f(A[i, :]) for i in 1:m]
[dot(A.., ..A)] == [dot(A[:,i], A[j,:]) for i in 1:m, j in 1:n] == A*A
[f(..A..)] == [f(A[i,j]) for i in 1:m, j in 1:n]
[v..] == [..v] = v
[..A..] == A

( v ein Vektor, A eine Matrix)

Ich bevorzuge over , da Sie damit einen Ausdruck in normaler Syntax schreiben können, anstatt viele eckige Klammern und Punkte einzuführen.

Sie haben Recht mit dem Durcheinander, denke ich, und ich habe versucht, meinen Vorschlag anzupassen und zu systematisieren. Um die Geduld aller nicht weiter zu strapazieren habe ich meine Gedanken zu Maps und Indizes etc. in einer Zusammenfassung https://gist.github.com/mschauer/b04e000e9d0963e40058 niedergeschrieben.

Nachdem ich diesen Thread gelesen habe, bevorzuge ich bisher _both_ f.(x) für einfache Dinge und Leute, die an vektorisierte Funktionen gewöhnt sind (die Redewendung " . = vectorized" ist ziemlich verbreitet) und f(x^2)-x over x für kompliziertere Ausdrücke.

Es gibt einfach viel zu viele Leute, die von Matlab, Numpy usw. kommen, um die vektorisierte Funktionssyntax vollständig aufzugeben. Ihnen zu sagen, dass sie Punkte hinzufügen sollen, ist leicht zu merken. Eine gute over -ähnliche Syntax zum Vektorisieren komplexer Ausdrücke in einer einzigen Schleife ist ebenfalls sehr nützlich.

Die over -Syntax reibt mich wirklich in die falsche Richtung. Mir ist nur eingefallen, warum: Es wird davon ausgegangen, dass alle Verwendungen jeder Variablen in einem Ausdruck vektorisiert oder nicht vektorisiert sind, was möglicherweise nicht der Fall ist. Beispiel: log(A) .- sum(A,1) – nehmen Sie an, dass wir die Vektorisierung von log entfernt haben. Sie können auch keine Funktionen über Ausdrücke vektorisieren, was ein ziemlich großer Mangel zu sein scheint, wenn ich exp(log(A) .- sum(A,1)) schreiben und die exp und die log und das sum vektorisieren wollte

@StefanKarpinski , dann sollten Sie entweder exp.(log.(A) .- sum(A,1)) machen und die zusätzlichen Temporäre akzeptieren (z. B. bei interaktiver Verwendung, wo die Leistung nicht kritisch ist) oder s = sum(A, 1); exp(log(A) - s) over A (obwohl das nicht ganz richtig ist, wenn sum(A,1) ist ein Vektor und Sie wollten senden); Sie müssen möglicherweise nur ein Verständnis verwenden. Egal, welche Syntax wir uns ausdenken, wir werden nicht alle möglichen Fälle abdecken, und Ihr Beispiel ist besonders problematisch, weil jede "automatisierte" Syntax wissen müsste, dass sum rein ist und das sein kann aus der Schleife/Karte gehisst.

Für mich ist die erste Priorität eine f.(x...) Syntax für broadcast(f, x...) oder map(f, x...) , damit wir @vectorize loswerden können. Danach können wir weiter an einer Syntax wie over (oder was auch immer) arbeiten, um allgemeinere Verwendungen von map und Verständnis abzukürzen.

@stevengj Ich glaube nicht, dass das zweite Beispiel dort funktioniert, weil - nicht gesendet wird. Unter der Annahme, dass A eine Matrix ist, wäre die Ausgabe eine Matrix aus einzeiligen Matrizen, von denen jede der Logarithmus eines Elements von A abzüglich des Summenvektors entlang der ersten Dimension ist. Sie benötigen broadcast((x, y)->exp(log(x)-y), A, sum(A, 1)) . Aber ich denke, eine prägnante Syntax für map ist nützlich und muss nicht unbedingt auch eine prägnante Syntax für broadcast sein.

Werden die Funktionen, die in der Vergangenheit automatisch vektorisiert wurden, wie sin , auch mit der neuen Syntax so bleiben, oder wird das veraltet sein? Ich mache mir Sorgen, dass selbst die f. -Syntax für eine große Schar von wissenschaftlichen Programmierern, die nicht durch konzeptionelle Eleganzargumente motiviert sind, wie ein „Erwischt“ erscheinen wird.

Meiner Meinung nach sollten die historisch vektorisierten Funktionen wie sin zugunsten von sin. veraltet sein, aber sie sollten quasi-permanent (im Gegensatz zu einer vollständigen Entfernung in der nachfolgenden Version) für die Nutzen von Benutzern aus anderen Wissenschaftssprachen.

Ein kleines (?) Problem mit f.(args...) : Obwohl die Syntax object.(field) größtenteils selten verwendet wird und wahrscheinlich ohne großen Aufwand durch getfield(object, field) ersetzt werden kann, gibt es ein _lot_ von Methodendefinitionen/Referenzen der Form Base.(:+)(....) = .... , und es wäre mühsam, diese in getfield zu ändern.

Ein Workaround wäre:

  • Lassen Sie Base.(:+) $ wie alle anderen f.(args...) zu map(Base, :+) #$ werden, aber definieren Sie eine veraltete Methode map(m::Module, s::Symbol) = getfield(m, s) für die Abwärtskompatibilität
  • unterstützen die Syntax Base.:+ (die derzeit fehlschlägt) und empfehlen dies in der Verfallswarnung für Base.(:+)

Ich würde gerne noch einmal fragen – ob das etwas ist, was wir in 0.5.0 tun können? Ich denke, es ist wichtig, weil viele vektorisierte Konstruktoren veraltet sind. Ich dachte, ich wäre damit einverstanden, aber ich finde map(Int32, a) statt int32(a) etwas langweilig.

Ist dies im Grunde nur eine Frage der Syntaxauswahl an dieser Stelle?

Ist dies im Grunde nur eine Frage der Syntaxauswahl an dieser Stelle?

Ich denke, @stevengj hat in seiner PR https://github.com/JuliaLang/julia/pull/15032 gute Argumente dafür geliefert, sin.(x) statt .sin(x) zu schreiben. Also würde ich sagen, der Weg ist frei gemacht.

Ich hatte einen Vorbehalt bezüglich der Tatsache, dass wir noch keine Lösung haben, um diese Syntax effizient auf zusammengesetzte Ausdrücke zu verallgemeinern. Aber ich denke, zu diesem Zeitpunkt sollten wir diese Funktion, die die meisten Anwendungsfälle abdeckt, besser zusammenführen, als diese Diskussion auf unbestimmte Zeit ungelöst zu halten.

@JeffBezanson Ich setze den Meilenstein dazu auf 0.5.0 zurück, um ihn während einer Triage-Diskussion zur Sprache zu bringen - hauptsächlich, um sicherzustellen, dass ich ihn nicht vergesse.

Funktioniert #15032 auch für call - zB Int32.(x) ?

@ViralBShah , ja. Jedes f.(x...) wird auf Syntaxebene in map(f, broadcast, x...) umgewandelt, unabhängig vom Typ von f .

Das ist der Hauptvorteil von . gegenüber etwas wie f[x...] , das ansonsten attraktiv ist (und keine Parser-Änderungen erfordern würde), aber nur für f::Function funktionieren würde. f[x...] kollidiert konzeptionell auch ein wenig mit T[...] Array Comprehensions. Obwohl ich denke, dass @StefanKarpinski die Klammersyntax mag?

(Um ein weiteres Beispiel auszuwählen, o::PyObject Objekte in PyCall sind aufrufbar und rufen die Methode __call__ des Python-Objekts o , aber die gleichen Objekte unterstützen möglicherweise auch o[...] Indizierung. Dies würde ein wenig mit f[x...] Broadcasting kollidieren, aber mit o.(x...) Broadcasting gut funktionieren.)

call existiert nicht mehr.

(Ich mag auch das Argument von @nalimilan , dass f.(x...) .( zum Analogon von .+ usw. macht.)

Ja, die punktuelle Analogie gefällt mir auch am besten. Können wir weitermachen und fusionieren?

Sollte das Getfield mit einem Modul tatsächlich veraltet sein?

@tkelman , im Gegensatz zu was? Die Verfallswarnung für Base.(:+) (dh wörtliche Symbolargumente) sollte jedoch Base.:+ vorschlagen, nicht getfield . (_Update_: erforderte auch eine veraltete Syntax, um Methodendefinitionen zu handhaben.)

@ViralBShah , gab es bei der Triage-Diskussion am Donnerstag eine Entscheidung darüber? #15032 ist in ziemlich guter Verfassung zum Zusammenführen, denke ich.

Ich denke, Viral hat diesen Teil des Anrufs verpasst. Mein Eindruck ist, dass mehrere Leute immer noch Vorbehalte gegen die Ästhetik von f.(x) haben und beides bevorzugen könnten

  1. ein Infix-Operator, der konzeptionell und in der Implementierung einfacher wäre, aber soweit ich sehen kann, sind keine ASCII-Operatoren verfügbar. Meine frühere Idee, das Makro-Parsing von ~ zu verwerfen, würde Arbeit erfordern, um es in Paketen zu ersetzen, und es ist wahrscheinlich zu spät, dies in diesem Zyklus zu versuchen.
  2. oder eine alternative neue Syntax, die es einfacher macht, Schleifen zu verschmelzen und temporäre Zeichen zu eliminieren. Keine anderen Alternativen wurden auch nur annähernd auf dem Niveau von #15032 implementiert, also sieht es so aus, als sollten wir das trotz verbleibender Vorbehalte zusammenführen und ausprobieren.

Ja, ich habe einige Vorbehalte, aber ich sehe im Moment keine bessere Option als f.(x) . Es scheint besser zu sein, als ein willkürliches Symbol wie ~ , und ich wette, viele, die an .* (usw.) gewöhnt sind, könnten sogar sofort erraten, was es bedeutet.

Ich würde gerne ein besseres Gefühl dafür bekommen, ob die Leute damit einverstanden sind, vorhandene vektorisierte Definitionen durch .( zu _ersetzen_. Wenn es den Leuten nicht genug gefällt, um den Ersatz zu machen, würde ich mehr zögern.

Als Benutzer, der auf dieser Diskussion lauert, würde ich dies SEHR gerne verwenden, um meinen vorhandenen vektorisierten Code zu ersetzen.

Ich verwende die Vektorisierung in Julia hauptsächlich aus Gründen der Lesbarkeit, da Schleifen schnell sind. Ich benutze es also sehr gerne für Erfahrungsberichte, Sünden usw., wie bereits erwähnt. Da ich bereits .^, .* in solchen Ausdrücken verwenden werde, füge der Sünde den zusätzlichen Punkt hinzu. exp. etc fühlt sich für mich wirklich natürlich und noch expliziter an ... vor allem, wenn ich dann meine eigenen Funktionen mit der allgemeinen Notation leicht falten kann, anstatt sin (x) und map (f, x) zu mischen.

Alles in allem hoffe ich als normaler Benutzer wirklich, dass dies zusammengeführt wird!

Mir gefällt die vorgeschlagene fun[vec] Syntax besser als fun.(vec) .
Was denkst du über [fun vec] ? Es ist wie ein Listenverständnis, aber mit einer impliziten Variablen. Es könnte erlauben, T[fun vec] zu tun

Diese Syntax ist in Julia 0.4 für Vektoren mit einer Länge > 1 kostenlos:

julia> [sin rand(1)]
1x2 Array{Any,2}:
 sin  0.0976151

julia> [sin rand(10)]
ERROR: DimensionMismatch("mismatch in dimension 1 (expected 1 got 10)")
 in cat_t at abstractarray.jl:850
 in hcat at abstractarray.jl:875

Etwas wie [fun over vec] könnte auf Syntaxebene transformiert werden und vielleicht lohnt es sich, [fun(x) for x in vec] zu vereinfachen, ist aber nicht einfacher als map(fun,vec) .

Ähnliche Syntaxen wie [fun vec] : Die Syntax (fun vec) ist kostenlos und {fun vec} wurde als veraltet markiert.

julia> (fun vec)
ERROR: syntax: missing separator in tuple

julia> {fun vec}

WARNING: deprecated syntax "{a b ...}".
Use "Any[a b ...]" instead.
1x2 Array{Any,2}:
 fun  [0.3231600663395422,0.10208482721149204,0.7964663210635679,0.5064134055014935,0.7606900072242995,0.29583012284224064,0.5501131920491444,0.35466150455688483,0.6117729165962635,0.7138111929010424]

@diegozea , fun[vec] wurde ausgeschlossen, da es mit T[vec] in Konflikt steht. (fun vec) ist im Grunde eine Scheme-Syntax, wobei der Fall mit mehreren Argumenten vermutlich (fun vec1 vec2 ...) ist ... das ist ziemlich anders als jede andere Julia-Syntax. Oder wollten Sie (fun vec1, vec2, ...) , was mit der Tupelsyntax in Konflikt steht? Es ist auch nicht klar, was der Vorteil gegenüber fun.(vecs...) wäre.

Denken Sie außerdem daran, dass ein Hauptziel darin besteht, schließlich eine Syntax zu haben, um @vectorized -Funktionen zu ersetzen (damit wir keine "gesegnete" Teilmenge von Funktionen haben, die "mit Vektoren arbeiten"), und das bedeutet, dass die Die Syntax muss schmackhaft/intuitiv/bequem für Leute sein, die an vektorisierte Funktionen in Matlab, Numpy usw. gewöhnt sind. Es muss auch einfach für Ausdrücke wie sin(A .+ cos(B[:,1])) zusammengesetzt werden können. Diese Anforderungen schließen viele der „kreativeren“ Vorschläge aus.

sin.(A .+ cos.(B[:,1])) sieht gar nicht so schlecht aus. Dazu braucht es eine gute Dokumentation. Wird f.(x) als .( ähnlich wie .+ dokumentiert?
Kann .+ zugunsten von +. werden?

# Since 
sin.(A .+ cos.(B[:,1]))
# could be written as
sin.(.+(A, cos.(B[:,1])))
# +.
sin.(+.(A, cos.(B[:,1]))) #  will be more coherent.

@diegozea , #15032 enthält bereits Dokumentation, aber weitere Vorschläge sind willkommen.

.+ wird weiterhin .+ geschrieben. Erstens ist diese Platzierung des Punktes zu fest verankert, und es gibt nicht genug zu gewinnen, wenn man hier die Schreibweise ändert. Zweitens, wie @nalimilan betonte, können Sie sich .( als einen „vektorisierten Funktionsaufrufoperator“ vorstellen, und aus dieser Perspektive ist die Syntax bereits konsistent.

(Sobald die Schwierigkeiten mit der Typberechnung in broadcast (#4883) ausgebügelt sind, hoffe ich, eine weitere PR zu erstellen, sodass a .⧆ b für jeden Operator nur Zucker ist für einen Anruf an broadcast(⧆, a, b) . Auf diese Weise müssen wir .+ usw. nicht mehr explizit implementieren – Sie erhalten den Rundfunkbetreiber automatisch, indem Sie einfach + usw. definieren immer noch in der Lage sein, spezielle Methoden zu implementieren, z. B. Aufrufe von BLAS, indem Sie broadcast für bestimmte Operatoren überladen.)

Es muss auch einfach für Ausdrücke wie sin(A .+ cos(B[:,1])) zusammengesetzt werden können.

Ist es möglich, f1.(x, f2.(y .+ z)) als broadcast((a, b, c)->(f1(a, f2(b + c))), x, y, z) zu parsen?

Bearbeiten: Ich sehe, es wird bereits oben erwähnt ... im Kommentar, der standardmäßig von @github ausgeblendet wird.

@yuyichao , Schleifenfusion scheint möglich zu sein, wenn die Funktionen als @pure markiert sind (zumindest wenn die Eltypes unveränderlich sind), wie ich in # 15032 kommentiert habe, aber dies ist eine Aufgabe für den Compiler, nicht der Parser. (Aber eine vektorisierte Syntax wie diese dient eher der Bequemlichkeit als dem Herausquetschen des letzten Zyklus aus kritischen inneren Schleifen.)

Denken Sie daran, dass das Hauptziel hier darin besteht, die Notwendigkeit von @vectorized -Funktionen zu eliminieren; Dies erfordert eine Syntax, die mindestens so allgemein, fast so bequem und mindestens so schnell ist. Es erfordert keine automatisierte Schleifenfusion, obwohl es nett ist, dem Compiler die broadcast Absicht des Benutzers offenzulegen, um die Möglichkeit einer Schleifenfusion zu einem späteren Zeitpunkt zu eröffnen.

Gibt es einen Nachteil, wenn es auch Loop-Fusion macht?

@yuyichao , Schleifenfusion ist ein viel schwierigeres Problem, und es ist nicht immer möglich, nicht-reine Funktionen beiseite zu legen (siehe z. B. @StefanKarpinskis exp(log(A) .- sum(A,1)) Beispiel oben). Darauf zu warten, dass es umgesetzt wird, wird meiner Meinung nach wahrscheinlich dazu führen, dass es _nie_ umgesetzt wird – wir müssen dies schrittweise tun. Beginnen Sie damit, die Absicht des Benutzers offenzulegen. Wenn wir in Zukunft weiter optimieren können, großartig. Wenn nicht, haben wir immer noch einen verallgemeinerten Ersatz für die Handvoll "vektorisierter" Funktionen, die jetzt verfügbar sind.

Ein weiteres Hindernis besteht darin, dass .+ usw. dem Parser derzeit nicht als broadcast -Operation ausgesetzt sind; .+ ist nur eine weitere Funktion. Mein Plan ist, das zu ändern ( .+ Zucker für broadcast(+, ...) zu machen), wie oben erwähnt. Aber auch hier ist es viel einfacher, Fortschritte zu erzielen, wenn die Änderungen inkrementell erfolgen.

Was ich meine ist, dass es schwierig ist, die Loop-Fusion durchzuführen, indem man beweist, dass sie gültig ist, also können wir den Parser die Transformation als Teil der Schaltpläne durchführen lassen. Im obigen Beispiel kann es geschrieben werden als. exp.(log.(A) .- sum(A,1)) und als broadcast((x, y)->exp(log(x) - y), A, sum(A, 1)) geparst werden.

Es ist auch in Ordnung, wenn .+ noch nicht zur selben Kategorie gehört (genauso wie jeder Funktionsaufruf ohne Boardcast in das Argument eingefügt wird) und es ist sogar in Ordnung, wenn wir dies (Schleifenfusion) nur in a tun spätere Version. Ich frage hauptsächlich, ob ein solcher Schaltplan im Parser möglich (dh eindeutig) ist und ob es einen Nachteil gibt, wenn man auf diese Weise eine geschriebene vektorisierte und verschmolzene Schleife zulässt.

Es ist schwierig, die Loop-Fusion durchzuführen, indem man beweist, dass dies gültig ist

Ich meine, das im Compiler zu tun ist schwierig (vielleicht nicht unmöglich), zumal der Compiler sich mit der komplizierten Implementierung von broadcast befassen muss, es sei denn, wir haben im Compiler einen Sonderfall von broadcast , was der Fall ist wahrscheinlich eine schlechte Idee und wir sollten es wenn möglich vermeiden ...

Vielleicht? Es ist eine interessante Idee und scheint nicht unmöglich zu sein, die .( -Syntax auf diese Weise als "Fusing" zu definieren und es dem Aufrufer zu überlassen, sie nicht für unreine Funktionen zu verwenden. Das Beste wäre, es auszuprobieren und zu sehen, ob es irgendwelche harten Fälle gibt (ich sehe im Moment keine offensichtlichen Probleme), aber ich neige dazu, dies nach der "nicht verschmelzenden" PR zu tun.

Ich neige dazu, dies nach der "nicht verschmelzenden" PR zu tun.

Voll und ganz einverstanden, zumal .+ ohnehin nicht behandelt wird.

Ich möchte das nicht entgleisen lassen, aber der Vorschlag von @yuyichao hat mir einige Ideen gegeben. Die Betonung liegt hier darauf, welche Funktionen vektorisiert werden, aber das scheint mir immer etwas fehl am Platz zu sein – die eigentliche Frage ist, über welche Variablen vektorisiert werden soll, was die Form des Ergebnisses vollständig bestimmt. Aus diesem Grund tendiere ich dazu, Argumente für die Vektorisierung zu markieren, anstatt Funktionen für die Vektorisierung zu markieren. Das Markieren von Argumenten ermöglicht auch Funktionen, die über ein Argument vektorisieren, aber nicht über ein anderes. Allerdings können wir beides haben und diese PR dient dem unmittelbaren Zweck, die eingebauten vektorisierten Funktionen zu ersetzen.

@StefanKarpinski , wenn Sie f.(args...) oder broadcast(f, args...) aufrufen, werden _alle_ Argumente vektorisiert. (Erinnern Sie sich zu diesem Zweck daran, dass Skalare als 0-dimensionale Arrays behandelt werden.) In @yuyichaos Vorschlag von f.(args...) = _fused broadcast syntax_ (was mir immer mehr gefällt), denke ich, dass die Fusion " stop" bei jedem Ausdruck, der nicht func.(args...) ist (um in Zukunft .+ usw. einzuschließen).

So würde beispielsweise sin.(x .+ cos.(x .^ sum(x.^2))) (in julia-syntax.scm ) zu broadcast((x, _s_) -> sin(x + cos(x^_s_)), x, sum(broacast(^, x, 2))) werden. Beachten Sie, dass die Funktion sum eine "Fusionsgrenze" wäre. Der Anrufer wäre dafür verantwortlich, f.(args...) in Fällen nicht zu verwenden, in denen Fusion Nebenwirkungen vermasseln würde.

Haben Sie ein Beispiel im Kopf, wo dies nicht ausreichen würde?

was mir immer besser gefällt

Ich freu mich, dass es dir gefällt. =)

Nur eine weitere Erweiterung, die wahrscheinlich nicht zur selben Runde gehört, es könnte möglich sein, .= , .*= oder ähnliches zu verwenden, um das Problem der direkten Zuweisung zu lösen (indem es von der normaler Auftrag)

Ja, der Mangel an Verschmelzung für andere Operationen war mein Haupteinwand gegen .+= usw. in #7052, aber ich denke, das würde gelöst werden, indem .= mit anderen func.(args...) -Aufrufen verschmelzen würde . Oder verschmelzen Sie einfach x[:] = ... .

:thumbsup: In dieser Diskussion drängen sich zwei Konzepte zusammen, die eigentlich ziemlich orthogonal sind:
die matlab'y "Fused Broadcasting Operations" oder x .* y .+ z und viele "maps on products and zips" wie f[product(I,J)...] und f[zip(I,J)...] . Das Reden aneinander vorbei könnte auch damit zu tun haben.

@mschauer , f.(I, J) ist bereits (in #15032) äquivalent zu map(x -> f(x...), zip(I, J) wenn I und J die gleiche Form haben. Und wenn I ein Zeilenvektor und J ein Spaltenvektor ist oder umgekehrt, dann bildet broadcast tatsächlich die Produktmenge ab (oder Sie können f.(I, J') , wenn es sich bei beiden um 1d-Arrays handelt). Ich verstehe also nicht, warum Sie denken, dass die Konzepte "ziemlich orthogonal" sind.

Orthogonal war nicht das richtige Wort, sie sind einfach verschieden genug, um nebeneinander zu existieren.

Der Punkt ist jedoch, dass wir für die beiden Fälle keine getrennten Syntaxen benötigen. func.(args...) kann beides unterstützen.

Sobald ein Mitglied des Triumvirats (Stefan, Jeff, Viral) #15032 zusammenführt (was meiner Meinung nach bereit für die Zusammenführung ist), werde ich dies schließen und ein Roadmap-Problem einreichen, um die verbleibenden vorgeschlagenen Änderungen zu skizzieren: Broadcast-Typ-Berechnung korrigieren, veraltet @vectorize , verwandeln Sie .op in Broadcast-Zucker, fügen Sie „Broadcast-Fusion“ auf Syntaxebene hinzu und verschmelzen Sie schließlich mit der direkten Zuweisung. Die letzten beiden werden es wahrscheinlich nicht in 0,5 schaffen.

Hey, ich bin sehr glücklich und dankbar über 15032. Ich würde die Diskussion aber nicht abweisen. Zum Beispiel sind Vektoren von Vektoren und ähnliche Objekte immer noch sehr umständlich in Julia zu verwenden, können aber als Ergebnis von Verständnissen wie Unkraut sprießen. Eine gute implizite Notation, die nicht auf der Codierung von Iterationen in Singleton-Dimensionen basiert, hat das Potenzial, dies erheblich zu erleichtern, beispielsweise mit den flexibleren Iteratoren und neuen Generatorausdrücken.

Ich denke, das kann jetzt zugunsten von #16285 geschlossen werden.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

wilburtownsend picture wilburtownsend  ·  3Kommentare

arshpreetsingh picture arshpreetsingh  ·  3Kommentare

sbromberger picture sbromberger  ·  3Kommentare

ararslan picture ararslan  ·  3Kommentare

omus picture omus  ·  3Kommentare