Julia: Broadcast hatte einen Job (zB Broadcasting über Iteratoren und Generator)

Erstellt am 21. Sept. 2016  ·  69Kommentare  ·  Quelle: JuliaLang/julia

Es war überraschend festzustellen, dass broadcast nicht mit Iteratoren funktioniert

dict = Dict(:a => 1, :b =>2)
<strong i="7">@show</strong> string.(keys(dict)) # => Expected ["a", "b"]
"Symbol[:a,:b]"

Dies liegt daran, dass Broadcast.containertype Any zurückgibt https://github.com/JuliaLang/julia/blob/413ed79ec54f3a754ac8bc57c1d29835d17bd274/base/broadcast.jl#L31
führt zum Fallback unter: https://github.com/JuliaLang/julia/blob/413ed79ec54f3a754ac8bc57c1d29835d17bd274/base/broadcast.jl#L265

Die Definition von containertype als Array für diesen Iterator führte zu Problemen beim Aufrufen von size , da broadcast nicht gegen die Iteratorschnittstelle iteratorsize(IterType) prüft

map löst dies mit dem Fallback map(f, A) = collect(Generator(f,A)) der möglicherweise sinnvoller ist als die aktuelle Definition von broadcast(f, Any, A) = f(A)

broadcast

Hilfreichster Kommentar

Dies ist beabsichtigt. broadcast ist für Container mit Formen und behandelt Objekte standardmäßig als Skalare. map ist für Container ohne Formen und behandelt Objekte standardmäßig als Iteratoren.

broadcast behandelt Strings als "Skalare", während map über die Zeichen iteriert.

Alle 69 Kommentare

Dies ist beabsichtigt. broadcast ist für Container mit Formen und behandelt Objekte standardmäßig als Skalare. map ist für Container ohne Formen und behandelt Objekte standardmäßig als Iteratoren.

broadcast behandelt Strings als "Skalare", während map über die Zeichen iteriert.

Vielleicht ist das Problem, dass die Leute die neue Punktsyntax zu bequem finden. In der Vergangenheit bestand jedoch der Wunsch nach einer kompakten Ausdrucksweise von map . Leider ist die Punktsyntax bereits vergeben.

Außerdem hat @stevengj bereits darauf hingewiesen: Es muss einen Unterschied zwischen map und broadcast , wenn nicht, was ist der Sinn von beiden.

@stevengj Aber Iteratoren haben eine Form (insbesondere Generatoren) http://docs.julialang.org/en/release-0.5/manual/interfaces/#interfaces

Ich würde argumentieren, dass Iteratoren in diesem unangenehmen Bereich sind, und die meisten Dinge, die Sie mit einem Container tun möchten, die Sie auch mit Iteratoren tun möchten, und ja, vielleicht liegt es nur an der Tatsache, dass die . Syntax zu praktisch ist (und der Fehler, den Sie erhalten, ist sehr undurchsichtig).

@pabloferz Der Hauptunterschied zwischen map und broadcast ist die Behandlung von Skalaren. Nun ist die Definition von Skalar umstritten und ich würde sagen, dass alles, was length(x) > 1 nicht als Skalar betrachtet werden sollte.

Das Markieren, welche Argumente als iterierbar behandelt werden sollen, anstelle des Funktionsaufrufs selbst, würde die Mehrdeutigkeit beseitigen. Ich denke?

Für broadcast (ich glaube auch im Allgemeinen) Form zu haben bedeutet, size (nicht nur length ) und indexierbar zu sein. Mit Ausnahme von Tupeln wird alles andere ohne size als Skalar behandelt. Bei der aktuellen Implementierung benötigen Sie zunächst getindex oder die Möglichkeit, eines für das Objekt zu definieren, über das Sie übertragen möchten. Für Iteratoren ist das im Allgemeinen nicht möglich.

Ich bin auch darauf gestoßen. Ausgehend von Nr. 16769, wo ich nach einem Weg suche, um fill! ein Array mit wiederholten Auswertungen einer Funktion (anstelle eines festen Werts) zu erstellen, dachte ich, dass die Punktsyntax bereits den Zweck erfüllen könnte. Aber wenn a = zeros(2, 3); a .= [rand() for i=1:2, j=1:3] funktioniert, funktioniert das (wäre) billigere a .= (rand() for i=1:2, j=1:3) nicht; dieser Generator ist HasShape() , hat aber tatsächlich keine Indizierungsfunktion. Ich habe sehr wenig Verständnis für die Funktionsweise von Broadcast / Dot-Syntax, aber wäre es hier hilfreich, eine Eigenschaft für die Indexierungsfunktionen zu haben? dafür gibt es schon eine PR (#22489)...

@rfourquet , du kannst a = zeros(2, 3); a .= rand.()

Ja, aber ich hätte genauer sein sollen: Ich möchte eine Funktion verwenden, die die Indizes als Parameter erhält, wie a .= (f(i, j) for i=1:2, j=1:3) .

Was wären die Nachteile der Übertragung von Dimensionen von HasShape Iteratoren? Das klingt nach einer Selbstverständlichkeit.

@nalimilan , auf den ersten Blick halte ich das für sinnvoll und wahrscheinlich relativ einfach umzusetzen. Wäre kaputt, sollte also mit 1.0 erledigt sein.

Ein potenzielles Problem dabei ist, dass HasShape Iteratoren getindex nicht unbedingt unterstützen, was die Implementierung schwierig machen könnte.

Eine Möglichkeit wäre, vorübergehend (für 1.0) eine einfache Implementierung vorzunehmen, die gerade in ein Array kopiert wurde. Das würde eine Optimierung nach 1.0 ermöglichen

Ein potenzielles Problem dabei ist, dass HasShape-Iteratoren nicht unbedingt getindex unterstützen, und dies könnte die Implementierung schwierig machen.

Wie ich oben sagte, habe ich einen PR bei #22489, um die Indizierung in Iteratoren zu ermöglichen, falls dies hilfreich sein kann.

Was muss für 1.0 getan werden, damit wir zumindest das Verhalten in 1.x verbessern können?

Danke @nalimilan , dass du das HasShape Generatoren auf der rechten Seite des Broadcast-Ausdrucks für 1.0 nicht möglich ist, sollten wir dies jetzt zu einem Fehler machen, anstatt Generatoren als Skalare zu behandeln? damit dies in 1.x aktiviert werden kann.

:+1: Triage empfiehlt, dies zu einem Fehler zu machen (die sichere Wahl) oder collect aufzurufen (wenn dies einfach ist).

map behandelt alle seine Argumente als Container und versucht, sie alle zu durchlaufen. In meiner idealen Welt wäre broadcast ähnlich und behandelt alle seine Argumente mit Formen, die gesendet werden können, und geben einen Fehler aus, wenn zB size nicht definiert ist. Ich weise darauf hin, dass jeder Wert in Broadcast als Skalar behandelt werden kann, indem man ihn mit fill umschließt, was ein 0-d-Array ergibt:

julia> fill("a")
0-dimensional Array{String,0}:
"a"

julia> fill([2])
0-dimensional Array{Array{Int64,1},0}:
[2]

Schlagen Sie wirklich vor, alle Skalare standardmäßig als Container zu behandeln? Das klingt nicht sehr praktisch.

Wenn wir uns ansehen, wie wir entweder jedes Iterable unterstützen oder einfach einen Fehler für sie ausgeben können, bis wir sie unterstützen, sieht es so aus, als ob wir eine Möglichkeit brauchen würden, Iteratoren in BroadcastStyle zu identifizieren. Das ist derzeit nicht möglich, da Base.iteratorsize HasLength auch für Skalare wie Symbol HasLength Base.iteratorsize zurückgibt. Wir könnten eine Base.isiterable Eigenschaft einführen (die für andere Dinge nützlich sein könnte) oder Base.iteratorsize standardmäßig auf NotIterable (was auch Sinn machen würde, da HasLength als Voreinstellung klingt immer etwas überraschend, wenn auch harmlos).

(Schwieriger Fall für zukünftige Diskussionen: UniformScaling .)

@timholy Da Sie broadcast , irgendwelche Vorschläge?

@JeffBezanson , der ganze Sinn von broadcast besteht darin, Skalare "rundsenden" zu können, um Container abzugleichen , zB um ["bug", "cow", "house"] .* "s" ----> ["bugs", "cows", "houses"] zu tun. Dies unterscheidet sich grundlegend vom Verhalten von map .

Aus diesem Grund behandelt broadcast Objekte standardmäßig als Skalare, sodass sie mit Containern kombiniert werden können. Das Ausgeben eines Fehlers für einen nicht erkannten Typ würde ihn viel weniger nützlich machen.

Es sollte möglich sein, einen bestimmten Typ als Container für broadcast zu deklarieren, indem eine geeignete Methode dafür definiert wird, aber ich denke, die Standardeinstellung sollte weiterhin darin bestehen, Objekte als Skalare zu behandeln.

In einer unabhängigen PR (https://github.com/JuliaLang/julia/pull/25339) schlug @Keno vor, applicable(start, (x,)) zu verwenden, um herauszufinden, ob x iterierbar ist oder nicht. Sollten wir hier den gleichen Ansatz verwenden? Ich würde es klarer finden, eine explizitere Definition von Iteratoren zu haben (entweder basierend auf Base.iteratorsize oder auf einem Merkmal), aber die Verwendung von start macht auch Sinn.

Wir könnten eine explizite Eigenschaft haben, die standardmäßig applicable(start, (x,)) ; das würde es ermöglichen, es bei Bedarf zu überschreiben.

Ich habe #25356 eingereicht, um die möglichen Lösungen und ihre Nachteile zu veranschaulichen.

Aus @stevengjs Beispiel ["bug", "cow", "house"] .* "s" ----> ["bugs", "cows", "houses"] scheint die Iterabilität nicht auszureichen, da Strings iterierbar sind, sich dort aber wie Skalare verhalten. Wenn Sie trotzdem ein Merkmal definieren müssen, ist es möglicherweise am besten, weiterhin die Zustimmung für die Übertragung zu verlangen, anstatt Anforderungen an alle Iteratoren hinzuzufügen.

Glücklicherweise gibt keys(dict) jetzt ein AbstractSet . Wenn wir also ein Broadcast-Merkmal für AbstractSet hinzufügen, würde dies das Beispiel im OP korrigieren. Wir könnten auch einen Fehler für die Übertragung eines Generator hinzufügen, um einige häufige Fälle abzufangen.

Das Senden über AbstractSet Container scheint von Natur aus etwas problematisch: Sie können ein AbstractSet mit einem Skalar kombinieren, aber nicht mit einem anderen Container, da die Iterationsreihenfolge für eine Menge nicht spezifiziert ist. Dies bricht die übliche Bedeutung einer "Broadcast"-Operation.

Ja, mir wurde bei der Vorbereitung der PR klar, dass Sets wirklich nicht das beste Beispiel für Iteratoren sind, die Broadcast unterstützen sollten. Dinge wie Generator und ProductIterator sind viel interessantere Fälle.

Vielleicht besteht die Antwort darin, Iteratoren mit HasShape zu übertragen (zu versuchen) und alles andere weiterhin als Skalare zu behandeln? Wird das OP nicht reparieren, ist aber ansonsten ziemlich elegant.

Anderer zufälliger Gedanke: Vielleicht sollte das Senden über 1 Argument (wie in string.(x) ) ein Sonderfall sein, der eher wie map funktioniert, da die Formkompatibilität kein Problem ist?

Vielleicht besteht die Antwort darin, Iteratoren mit HasShape (zu versuchen) zu senden und alles andere als Skalare zu behandeln? Wird das OP nicht reparieren, ist aber ansonsten ziemlich elegant.

Ich bin mir nicht sicher, ob wir einen triftigen Grund haben, HasLength Iteratoren auszuschließen. Wir unterstützen die Übertragung über Tupel (die size nicht implementieren), warum also nicht formlose Iteratoren wie Tupel behandeln? Zum Beispiel wäre es durchaus sinnvoll, das Ergebnis von keys(::OrderedDict) mit broadcast . Wenn wir es nicht unterstützen, werden die Leute versucht sein, ihre Iteratoren als HasShape nur um mit broadcast (und der schönen Punktsyntax) verwendbar zu sein.

Um Steve zu zitieren,

broadcast ist für Behälter mit Formen

HasShape scheint eine vernünftige Möglichkeit zu sein, das genauer zu definieren. Ansonsten scheint es mir, als müssten wir zum Beispiel das Verhalten von Broadcast auf Strings unterbrechen.

Wir haben bereits eine Inkonsistenz, bei der Tupel als Container und Strings als Skalare betrachtet werden. Strings sind sowieso etwas ganz Besonderes, ich glaube nicht, dass ihr Verhalten daran liegt, dass sie keine Form haben: Es hängt eher damit zusammen, dass sie die einzige Sammlung sind, die häufiger als Skalar als als Container betrachtet wird.

Vielleicht kann @stevengj entwickeln, warum er der Meinung ist, dass broadcast nur Container mit einer Form unterstützen sollte? Würden Sie Tupel auch als Skalare betrachten?

Ich denke, der Grund für die Behandlung von Tupeln als Container in broadcast (#16986) war, dass sie in der Praxis oft als im Wesentlichen statische Vektoren verwendet werden, und sie in broadcast als "Skalare" zu behandeln war einfach nicht auf jeden fall sehr nützlich. Im Gegensatz dazu werden Strings (a) oft als "Atome" für String-Verarbeitungsoperationen behandelt und (b) haben im Allgemeinen keine fortlaufende Indizierung, so dass sie sehr schlecht in das broadcast Framework passen.

Im Prinzip würde ich HasShape Iteratoren als Container in broadcast . Das Hauptproblem ist, wie ich oben erwähnt habe, dass HasShape nicht garantiert, dass getindex funktioniert.

Das Hauptproblem besteht, wie oben erwähnt, darin, dass HasShape nicht garantiert, dass getindex funktioniert

Würde etwas wie #22489 helfen, dh ein Iterator-Merkmal zu haben, das angibt, ob ein Iterator indiziert werden kann?

Würde etwas wie #22489 helfen, dh ein Iterator-Merkmal zu haben, das angibt, ob ein Iterator indiziert werden kann?

Aber dann würden nur indexierbare Iteratoren mit broadcast ? Das klingt zu restriktiv, da es sehr nützlich wäre, Dinge wie string.(itr, "1") für jedes iterierbare Element (zB das Ergebnis von keys(::OrderedDict) ) tun zu können, und es ist keine Indizierung erforderlich, um dies zu implementieren. Ich denke, wir sollten besser für alle Iteratoren, die die Indizierung in 0.7/1.0 nicht unterstützen, einen Fehler ausgeben und versuchen, sie in nachfolgenden Versionen zu unterstützen. Es ist sowieso nicht sehr sinnvoll, Iteratoren als Skalare zu behandeln. Dann können wir jedes gewünschte Verhalten in 1.x-Versionen implementieren.

@stevengj Ich stimme Ihren Argumenten in Bezug auf Strings und Tupel zu, aber warum sollten wir HasLength Iteratoren nicht als Tupel behandeln? Ich habe bisher keine Begründung dafür gelesen.

@nalimilan , ich denke eher, dass nur indexierbare+hasshape-Iteratoren von broadcast . Der Versuch, allgemeine Iteratoren in diese Funktion zu stopfen, verwirrt ihre Bedeutung zu sehr – irgendwann sollten Sie einfach map .

wäre sehr nützlich, um Dinge string.(itr, "1") für jedes iterierbare Element tun zu können … Es ist sowieso nicht sehr nützlich, Iteratoren als Skalare zu behandeln.

Der Fall von Strings widerspricht dem – das Argument "1" selbst ist in Ihrem Beispiel iterierbar. Tonnen von Dingen sind iterierbar (zB PyObject s in PyCall definieren start usw.), einschließlich Dinge wie ungeordnete Mengen, bei denen das Konzept von broadcast wirklich gebrochen ist.

Beachten Sie auch, dass #24990 map noch einfacher macht, als es jetzt ist, zB können Sie map(string(_,"1"), itr) .

@nalimilan , ich neige dazu, zu denken, dass nur indexierbare + hasshape-Iteratoren von Broadcast unterstützt werden sollten. Der Versuch, allgemeine Iteratoren in diese Funktion zu stopfen, verwirrt ihre Bedeutung zu sehr – irgendwann sollten Sie einfach map verwenden.

Wir haben derzeit keine Eigenschaft für indexierbare Iteratoren. Wie würden Sie vorschlagen, das zu übergeben? Mein WIP PR #25356 würde einen Fehler für Iteratoren auslösen, die keine Indizierung unterstützen, was nicht so schlecht klingt, vorausgesetzt, es ist nicht sehr nützlich, Iteratoren als Skalare zu behandeln. Wenn wir sie als Skalare behandeln wollen, brauchen wir eine andere Eigenschaft, oder?

Ich neige dazu, für alle Fälle, die nicht ganz offensichtlich sind, Fehler zu melden, damit wir in Zukunft jedes Verhalten implementieren können, anstatt uns in ein Standardverhalten einzuschließen, das nicht unbedingt sehr nützlich ist (dh einige Iteratoren als Skalare behandeln). . Wie dieses Problem zeigt, braucht das Verhalten von broadcast Zeit, um richtig zu entwerfen.

(FWIW, PyObject klingt für mich nicht nach einem guten Beispiel, da IIUC das Iterationsprotokoll implementiert, nur weil es nicht im Voraus weiß, ob es einen Python-Iterator umschließt oder nicht. PyObject ist hier eindeutig eine Ausnahme, genauso wie es eine getfield Überladung verwenden muss, um wie ein normales Julia-Objekt zu erscheinen. Sets sind ein eher julianisches Beispiel.)

Wir könnten ein Merkmal für indexierbare HasShape Iteratoren hinzufügen, wie es an anderer Stelle vorgeschlagen wurde.

Triage mag die Idee von Make Broadcast Iterierte über alle Argumente (wie Karte) und das Hinzufügen eines Operator (wie tut const & = Ref früher als vorgeschlagen in einem anderen Problem, oder vielleicht ~ ) explizit Zeichen 0-d-Argumente.

@vtjnash , was bedeutet das überhaupt für einen Nicht- HasShape Iterator? Meinen Sie, dass Sie möchten, dass Broadcast über Dinge wie Strings und Sets iteriert? Die aktuelle broadcast Implementierung ist eng mit getindex … haben Sie darüber nachgedacht, wie Sie sie ohne getindex implementieren würden, insbesondere für die Kombination von Argumenten unterschiedlicher Dimensionalität?

Theoretisch sollte es möglich sein, nicht indizierte Iteratoren zu unterstützen (zumindest solche, die eine sinnvolle Reihenfolge haben). Das ist einfach, wenn alle Eingaben die gleiche Form haben; wenn sie unterschiedliche Formen haben und der Iterator eine andere (kleinere) Form als das Ergebnis hat, wäre eine gewisse Zwischenspeicherung erforderlich.

Sieht so aus, als könnte das IteratorAccess Merkmal von PR https://github.com/JuliaLang/julia/pull/22489 angepasst/wiederverwendet werden, um indizierbare Iteratoren zu erkennen. Zu wissen, welche Iteratoren indexierbar sind (und daher keys implementieren sollten) ist auch für https://github.com/JuliaLang/julia/pull/24774 erforderlich

CC :

👍 Triage empfiehlt, dies zu einem Fehler zu machen (die sichere Wahl) oder das Sammeln aufzurufen (wenn dies einfach ist).

Könnte sich die Triage hier für eine bestimmte Strategie entscheiden? ZB was ist "das" in @JeffBezansons Kommentar oben? Sollten wir Fehler für alle Iteratoren auslösen, die keine Indizierung unterstützen (im Moment sicherste Wahl, damit wir später alles tun können, was wir wollen), oder sollten wir einige Iteratoren als Skalare behandeln? Sollten wir ein Merkmal für indexierbare Iteratoren hinzufügen, und wenn ja, in welcher Form (neues Merkmal vs. neue Auswahl für Base.IteratorSize )? Sollten wir generell ein Merkmal für Iteratoren hinzufügen (damit wir sie von Skalaren unterscheiden können)?

Das folgende Verhalten scheint gut zu sein:

  • Versuchen Sie standardmäßig, jedes Argument zu iterieren und zu übertragen.
  • Geben Sie einen Fehler ein, wenn das aus irgendeinem Grund nicht funktioniert.
  • Übergeben Sie Ref(x) oder [x] zu erzwingen, dass x als Skalar behandelt wird.
  • Fügen Sie ein Merkmal hinzu, das definiert werden kann, damit ein neuer Typ als Skalar behandelt wird, anstatt einen Fehler auszugeben. Beachten Sie, dass dies nicht verwendet werden sollte, um zwischen Iteration und nicht Iteration zu wählen. Es geht nur darum, den Fehler in skalares Verhalten umzuwandeln.

Könnten Sie den Hinweis zum letzten Punkt verdeutlichen (vielleicht mit einem Beispiel)? Ich bin mir nicht sicher, was es bedeutet, dass das Merkmal existiert, aber nicht verwendet wird, um zwischen Iterieren und Nicht-Iterieren zu wählen.

"Versuchen Sie also, jedes Argument zu iterieren und zu übertragen" impliziert, dass wir BroadcastStyle , um Scalar() für alle Nicht-Sammlungstypen zurückzugeben (insbesondere Number , Symbol und AbstractString )? Das klingt nach dem "Merkmal", das der letzte Aufzählungspunkt erwähnt.

Ehrlich gesagt fände ich es weniger kostspielig, ein Merkmal für Iterables zu definieren, als ein Merkmal für Nicht-Interables/Skalare zu definieren. Ich befürchte, dass alle Nicht-Sammlungstypen irgendwann dieses Scalar Merkmal implementieren werden, da dies in einigen (möglicherweise seltenen) Fällen nützlich sein kann.

Das klingt nach dem "Merkmal", das der letzte Aufzählungspunkt erwähnt

Nein, der letzte Aufzählungspunkt bedeutet, dass, wenn etwas Iteration implementiert, es dann durch Broadcast iteriert wird – das skalare Merkmal wird weg sein. Für einige gemeinsame deutlich nicht-iterable Typen (wie Subtypen von Type und Function ), dann könnten wir ein Baby haben NotIterable Charakterzug, die dann auch MethodError in eine Iteration, die einen Wert (dieses Objekt) erzeugt. Ich weiß allerdings nicht mehr genau, warum das nötig war.

Im Grunde bedeutet "Versuchen, jedes Argument zu iterieren und zu senden", dass wir BroadcastStyle definieren müssen, um Scalar() für alle Nicht-Sammlungstypen (insbesondere Number, Symbol und AbstractString) zurückzugeben? Das klingt nach dem "Merkmal", das der letzte Aufzählungspunkt erwähnt.

Nein, alle skalaren Untertypen von Number iterieren sich selbst und sind daher in Ordnung. Wir müssten es für Symbol definieren. AbstractString würde als Sammlung funktionieren.

Ich mag kein Design, bei dem wir eine Methode definieren müssen, damit ein Typ als Skalar behandelt wird. Das sollte die Standardeinstellung sein. Ich glaube auch nicht, dass Strings als Container für die Übertragung behandelt werden sollten.

Ich denke immer noch, Broadcast sollte nur HasShape-Iteratoren als Container behandeln; dies steht im Einklang mit dem Design von Broadcast von Anfang an. Was stimmt damit nicht?

Das Problem dabei ist das im OP; Wenn Sie einen Iterator ohne Form haben, ist die Behandlung als Skalar eine verrückte Antwort.

Außerdem würde ich mich sehr freuen, den "Eigenschaften"-Teil des Vorschlags fallen zu lassen. Niemand beschwert sich über

julia> map(string, [1,2], :a)
ERROR: MethodError: no method matching start(::Symbol)

Der Grund für das unerwartete Ergebnis im OP ist wohl, dass niemand wirklich beabsichtigt, dass ein Broadcast-Aufruf _alle_ Argumente als Skalare behandelt; wenn es nur ein Argument gibt und es überhaupt eine Möglichkeit gibt, es als Sammlung/Iterator zu behandeln, ist dies mit ziemlicher Sicherheit das, was der Benutzer beabsichtigt. Obwohl 1 .+ 1 natürlich weiterhin funktionieren sollte?

Das ist mir eingefallen, aber es scheint verwirrend, ein Argument zu einem Sonderfall zu machen.

Ich sehe die folgende Asymmetrie: Die Behandlung eines Iterablen als Skalar führt zu wirklich seltsamen Ergebnissen, aber die Behandlung eines Skalars als Iterable führt zu einem Fehler. Wenn Sie den Fehler erhalten, können Sie ihn leicht beheben, indem Sie das Argument umschließen. Während im ersten Fall Sie nichts Einfaches tun können, um es dazu zu bringen, das Argument zu durchlaufen.

Ich denke immer noch, Broadcast sollte nur HasShape-Iteratoren als Container behandeln; dies steht im Einklang mit dem Design von Broadcast von Anfang an. Was stimmt damit nicht?

@stevengj Was IMHO falsch ist, ist, dass einige Operationen nicht funktionieren, wenn ein vollkommen vernünftiges Verhalten implementiert werden könnte: Behandeln Sie HasLength Iteratoren genau wie Tuple , was derzeit eine Sonderform hat. Auch wenn wir sie im Moment nicht unterstützen, möchte ich die Möglichkeit offen lassen, sie irgendwann in 1.x zu unterstützen.

Niemand beschwert sich über

julia> map(string, [1,2], :a)
FEHLER: MethodError: kein Methodenvergleich start(::Symbol)

@JeffBezanson OTC, ich unterstütze das aktuelle Verhalten von broadcast , das :a nach Bedarf wiederholt. So etwas kann sehr nützlich sein, zB um eine Reihe von DataFrame Spalten umzubenennen. Schlagen Sie vor, broadcast zu ändern, um einen Fehler wie map auszulösen?

Ich sehe die folgende Asymmetrie: Die Behandlung eines Iterablen als Skalar führt zu wirklich seltsamen Ergebnissen, aber die Behandlung eines Skalars als Iterable führt zu einem Fehler. Wenn Sie den Fehler erhalten, können Sie ihn leicht beheben, indem Sie das Argument umschließen. Während im ersten Fall Sie nichts Einfaches tun können, um es dazu zu bringen, das Argument zu durchlaufen.

Es ist einfach, aber ziemlich unpraktisch. Ich stimme @stevengj zu, dass Skalare standardmäßig gesendet werden sollen und keinen Fehler Number Typen iterierbar sind, wäre der Ärger natürlich nicht immer sichtbar, aber wie das Symbol Beispiel zeigt, wäre es im Allgemeinen nicht sehr hilfreich. Char wäre ein anderer, und viele benutzerdefinierte Typen, die im Paket definiert sind, werden ebenfalls darunter leiden (und am Ende ihre BroadcastStyle als Scalar() ).

Ich denke, der Kern des Problems ist, dass wir keine Eigenschaft haben, um Sammlungen von Skalaren zu unterscheiden. Die direkteste Lösung bestand also darin, HasShape Iteratoren als Sammlungen und andere Typen als Skalare zu behandeln (einschließlich HasLength Iteratoren, da dies die Standardeinstellung für alle Typen ist). Persönlich denke ich, dass die Einführung eines Merkmals für Sammlungen/Iterables sehr sinnvoll wäre, aber wenn wir dazu nicht bereit sind und uns nicht darauf verlassen können, dass start definiert ist, um Iterables zu erkennen, bin ich Angst, wir müssen das aktuelle Verhalten beibehalten.

Jeffs Vorschlag in https://github.com/JuliaLang/julia/issues/18618#issuecomment -360594955 bietet Platz für die Übertragung von Symbol und Char als "skalare" Typen – sie einfach müssen Sie sich für das Verhalten entscheiden. Standardmäßig wären sie ein Fehler, da sie start nicht implementieren.

Der überzeugendste Teil hier ist, dass die einzig sinnvolle Definition für einen "Sammlungstyp" darin besteht, dass er iterierbar ist. Ja, das bedeutet, dass Strings Sammlungen sind. Manchmal werden sie als solche verwendet! Lassen Sie uns also standardmäßig das Verhalten verwenden, das es den Leuten leicht ermöglicht, sich auf der Anrufseite für den anderen zu entscheiden.

Hier ist allerdings eine Warze. Da Zahlen iterierbar sind (sie sogar HasShape ), werden sie als nulldimensionale Container behandelt. Das bedeutet, zu seiner logischen Schlussfolgerung gezogen, 1 .+ 2 != 3 . Es wäre stattdessen fill(3, ()) .

EDIT: Um eine weitere Entgleisung des Threads zu vermeiden, in den Diskurs verschoben:

https://discourse.julialang.org/t/lazycall-again-sorry/8629

Jeffs Vorschlag in Nr. 18618 (Kommentar) bietet Platz für die Übertragung von Symbol und Char als "skalare" Typen - sie müssen sich nur für das Verhalten anmelden. Standardmäßig wären sie ein Fehler, da sie start nicht implementieren.

Ja, meine Position basiert nur auf der Annahme, dass Skalare ein natürlicherer Fallback sind, insbesondere angesichts der Tatsache, dass Sammlungen einige Methoden (Iteration, möglicherweise Indizierung) implementieren müssen, während Skalare nur "der Rest" sind und nichts gemeinsam haben. Am Ende kann jeder Typ jedes gewünschte Verhalten implementieren, aber wir sollten es so bequem und logisch wie möglich gestalten, was insbesondere dazu beitragen soll, Inkonsistenzen zu vermeiden (zB einige Typen deklariert und verhalten sich als Skalare und andere nicht).

Es geht mir nicht sonderlich darum, ein paar Ausnahmen für wesentliche Typen wie Zeichenfolgen und Zahlen zu haben, solange die Regeln für andere Typen klar sind.

Ich habe ein wenig über unsere Iterations- und Indexierungsschnittstellen nachgedacht. Ich stelle fest, dass wir nützliche Objekte haben können, die iterieren (aber nicht indiziert werden können), Objekte, die indiziert werden können (aber nicht iterierbar sind) und Objekte, die beides tun. Auf dieser Grundlage frage ich mich, ob:

  • map könnte stark an das Iterationsprotokoll gebunden sein - es scheint gültig zu sein, dass wir ein faules out = map(f, iterable) für jedes beliebige iterable so dass zB first(out) ist wie f(first(iterable)) , und es scheint mir, dass diese generische faule Operation nützlich sein könnte.
  • broadcast könnte stark an die Indexierungsschnittstelle gebunden sein - es scheint gültig, dass wir ein faules out = broadcast(f, indexable) erstellen können, so dass out[i] dasselbe ist wie f(indexable[i]) , und es scheint mir, dass diese generische faule Operation nützlich sein könnte. Offensichtlich könnte broadcast mit mehreren Eingängen immer noch all die ausgefallenen Dinge tun, die es jetzt tut. Zum Zwecke der Übertragung wären Skalare die Dinge, die nicht indiziert werden können (oder trivial wie Number und Ref und AbstractArray{0} indexieren).

Ich denke auch, dass es wünschenswert wäre, wenn map mit einem Argument und broadcast einem Argument sehr ähnliche Dinge für Sammlungen tun, die sowohl iterierbar als auch indiziert werden können. Allerdings scheint die Tatsache, dass AbstractDict Iteration andere Dinge als getindex zurückgibt, eine nette Vereinheitlichung hier zu blockieren. :frowning_face: (Unsere anderen Sammlungstypen scheinen in Ordnung zu sein)

(Für mich klingt die Tatsache, dass Dinge wie Strings möglicherweise explizit wie ["bug", "cow", "house"] .* ("s",) verpackt werden müssen, hier nicht nach einem Deal-Breaker. Ich habe das gleiche Problem, wenn ich an einen 3-Vektor denken möchte als "einzelner 3D-Punkt" und nicht allzu schwer zu handhaben (xref #18379)).

Ich stimme zu, dass broadcast für indizierbare Container sein sollte, aber ich denke, das sollte fortlaufend indiziert werden, was Strings ausschließt. zB collect(eachindex("aαb🐨γz")) ergibt [1, 2, 4, 5, 9, 11] , was bei jeder broadcast Implementierung, die auf Indizierung basiert, schlecht funktioniert.

Aber für indizierte Container zu sein bedeutet im Wesentlichen, dass Container eine Eigenschaft benötigen, um sich anzumelden, was im Grunde das ist, was ich befürworte.

Ich bin mir nicht sicher, ob aufeinanderfolgende Indizes eine gute Einschränkung darstellen - Wörterbücher haben beispielsweise beliebige Indizes.

broadcast(f, ::String) kann jedoch kein neues String erstellen und garantieren, dass die Ausgabeindizes dieselben wie die Eingabeindizes bleiben, da sich die UTF-8-Zeichenbreiten unter f ( es müsste sich in so etwas wie ein AbstractDict{Int, Char} verwandeln, um diese Garantie zu geben, was wirklich nicht sehr nützlich erscheint!). Ich würde fast sagen, dass die Indizes eines String eher "Token" für eine schnelle Suche sind als semantisch wichtige Indizes (zB können Sie in einen äquivalenten UTF-32-String konvertieren und die Indizes würden sich ändern).

Es macht mir nichts aus, wenn wir das Sendeverhalten über eine Eigenschaft aktivieren; Ich sage nur, dass die Vorstellung, wie sich ein generisches broadcast(f, ::Any) verhält, ein guter Weg ist, um die Implementierung von Dingen wie broadcast(f, ::AbstractDict) leiten (und würde natürlich die Frage beantworten, die ich in #25904 gestellt habe, dh über die Sendung senden Wörterbuchwerte und nicht Schlüssel-Wert-Paare).

Sind die Menschen wirklich glücklich mit dieser Veränderung? Ich für meinen Teil musste noch nie über einen Container ohne Form senden, während ich über Dinge sende, die die ganze Zeit als Skalare behandelt werden sollten . Jede Abwertungswarnung, die ich „korrigiere“, lässt mich eine Träne vergießen.

Ich sende über Dinge, die _die ganze Zeit_ als Skalare behandelt werden sollten.

Was sind die Arten dieser Dinge?

Kann alles sein. In einem Paket, das beispielsweise einen Optimierungsmodelltyp Model und einen Entscheidungsvariablentyp Variable , haben Sie möglicherweise x::Vector{Variable} für die Sie die Werte erhalten möchten, nachdem Sie das Problem gelöst haben Modell model mit einer Funktion value(::Variable, ::Model)::Float64 . Bisher konnten Sie das wie folgt tun:

value.(x, model)

Es ist auch oft der Fall, dass die Argumenttypen aus anderen Paketen stammen, so dass das Hinzufügen einer Methode zu broadcastable für diese Typen in diesem Fall Typpiraterie wäre. Sie müssen also Ref oder ein Tupel mit einem Element verwenden. Das ist nicht unüberwindbar, aber es macht das übliche Gehäuse meiner Meinung nach viel weniger elegant, um ein relativ obskures Nutzungsmuster zu unterstützen.

Ja, ich verstehe Ihren Standpunkt, und ich stimme zu, dass es in solchen Situationen ärgerlich ist. Das alte Verhalten war jedoch absolut problematisch – es war eines dieser Dinge, "der Standardfallback ist in einigen Fällen definitiv falsch".

Kurz gesagt gibt es vier Optionen, die den falschen Fallback vermeiden:

  1. verlangen, dass _alles_ eine Methode implementiert, die beschreibt, wie sie übertragen
  2. Standardmäßig werden Dinge als Container und Fehler/veraltet für Nicht-Container behandelt.

    • Wir werden nur versuchen, unbekannte Objekte zu iterate bearbeiten und das wird bei Skalaren einen Fehler verursachen

    • Es gibt zwei Notausstiege für Skalare – Benutzer können sie an einer Anrufseite umhüllen und Bibliotheksautoren können sich für eine unverpackte skalarähnliche Übertragung entscheiden.

  3. Standardmäßig behandelt Dinge als Skalare und Fehler für unbekannte Container

    • Da es keine relevanten Methoden gibt, die nur für Skalare definiert sind, müssen wir behaupten, dass iterate einen Methodenfehler auslöst. Das ist langsam und umständlich.

    • Es würde nur eine Fluchtluke für benutzerdefinierte Container zur Verfügung stehen, um keine Fehler zu machen: ihre Bibliotheksautoren, sich explizit für die Übertragung zu entscheiden. Dies scheint für eine Funktion, deren Hauptzweck darin besteht, mit Containern zu arbeiten, ziemlich rückständig.

  4. Überprüfen Sie applicable(iterate, …) und ändern Sie das Verhalten entsprechend

    • Dies funktioniert derzeit aufgrund des Deprecation-Mechanismus von start/next/done nicht und kann im Allgemeinen für Wrapper-Typen falsch sein, die Methoden auf einen Member zurückstellen.

Option 1 ist für alle schlimmer, Option 2 ist der Status quo und Option 3 ist rückwärts und Option 4 ist etwas, das wir noch nie zuvor gemacht haben und wahrscheinlich fehlerhaft sein wird.

Ich denke, ein Teil der Diskussion muss hinter den Kulissen stattgefunden haben, aber ich bin einfach nicht überzeugt von den Argumenten, die ich in diesem Thread und in https://github.com/JuliaLang/julia/pull/25356 gegen nalimilan Positionen von stevengj .

Es würde nur eine Fluchtluke für benutzerdefinierte Container zur Verfügung stehen, um keine Fehler zu machen: ihre Bibliotheksautoren, sich explizit für die Übertragung zu entscheiden. Dies scheint für eine Funktion, deren Hauptzweck darin besteht, mit Containern zu arbeiten, ziemlich rückständig.

Dies ist mein Hauptgrund der Meinungsverschiedenheit. Mir scheint, dass in allen Julias Code # of iterator types << # of types that should be treated as scalars in a broadcast situation < # of broadcast calls . Ich würde es also vorziehen, wenn die Anzahl der "Extras" mit der Anzahl der Iteratortypen skaliert und nicht mit der Anzahl der Broadcast-Aufrufe. Und wenn ein Bibliotheksautor einen Iterator definiert, ist es nicht völlig unvernünftig, ihn aufzufordern, eine weitere Methode zu definieren, wohingegen es _ist_ völlig unvernünftig ist, jeden Paketautor aufzufordern, Base.broadcastable(x) = Ref(x) für alle seine nicht iterierbaren Typen zu definieren, um dies zu tun vermeiden Sie hässliche (IMHO) Ref s in einem hohen Prozentsatz von broadcast Anrufen.

Ich weiß, dass es schön ist, eine einzige Methode zum Definieren der Iteration zu implementieren, aber es ist nicht so viel Arbeit, eine weitere für ein neues Merkmal zu implementieren oder die Angabe von Base.iteratorsize für einen neuen Iterator erforderlich zu machen (und die problematische HasLength Standardeinstellung loszuwerden). Die Fallback-Methode broadcastable könnte dann auf dieser Eigenschaft basieren. Oder, wenn Sie wirklich daran interessiert sind, Iterationen mit einer einzigen Methode zu definieren, können Sie (nach der Deprecation-Removal) dieses explizite Merkmal standardmäßig auf applicable(iterate, ...) wie in https://github.com/JuliaLang/ julia/issues/18618#issuecomment -354618742, und überschreiben Sie diese Vorgabe bei Bedarf einfach. Eckfälle wie String könnten bei Bedarf auch noch durch weitere Spezialisierung von broadcastable gehandhabt werden.

Das ist effektiv das Design von 0.6, das zu diesem Problem und #26421 und #19577 und #23197 und #23746 und möglicherweise mehr geführt hat – die Suche danach ist schwierig.

Dies bedeutet, dass Base einen Standardfallback bereitstellt, der für eine ganze Klasse von Objekten falsch ist. Aus diesem Grund bevorzuge ich einen Mechanismus, der Fehler auslöst, es sei denn, Sie stimmen auf die eine oder andere Weise zu. Es ist eigensinnig und der Übergang ist schmerzhaft, aber es zwingt Sie, explizit zu sein.

Sie haben vielleicht Recht, dass es mehr "skalarähnliche" benutzerdefinierte Typen als iteratorähnliche gibt, aber ich stehe zu der Tatsache, dass Broadcasting in erster Linie eine Operation auf Containern ist. Ich erwarte, dass f.(x) eine Art von Zuordnung macht und nicht nur f(x) .

Und schließlich können Container, die die skalare Standardbehandlung erhalten, einfach nicht elementweise mit Broadcasting verwendet werden. Zum Beispiel ist String ein Sammlungstyp, den wir in eine spezielle Schreibweise umwandeln, um sich wie ein Skalar zu verhalten; es ist nicht möglich, "hineinzugreifen" und elementweise zu arbeiten, auch wenn dies in manchen Situationen sinnvoll erscheint (zB isletter.("a1b2c3") ). Das ist das Asymmetrie-Argument: Sie können Container effizienter in eine Ref einpacken, um sie als Skalare zu behandeln, als Sie collect sie in eine tatsächlich sendefähige Sammlung zu packen.

Das sind die Hauptargumente. Was die Hässlichkeit von Ref angeht, stimme ich voll und ganz zu. Eine Lösung gibt es #27608.

Meinetwegen. Ich habe keine Knockdown-Argumente oder magische Lösungen für diese Probleme, und https://github.com/JuliaLang/julia/pull/27608 wird die Dinge verbessern.

@tkoolen Ich hatte die gleichen Bedenken und den gleichen

@mbauman Die oben angegebenen Argumente sind möglicherweise nicht vollständig überzeugend. Hier sind zwei Fragen, um vollständiger zu sein:

1) Es wäre möglich, broadcastable einer erforderlichen Schnittstelle für jedes Iterable zu machen.
Dies wäre völlig systematisch und würde die Entwickler zwingen, darüber nachzudenken
wie sich ihr Iterator unter Broadcast verhalten soll.
Eine Empfehlung, es als collect(x) festzulegen, würde den Übergang in den meisten Fällen relativ einfach machen.
Es würde keinen Leistungsverlust geben, oder?

2) Es läuft also auf den Willen hinaus, einen Fehler für f.(x) wenn x als Skalar sendet.
Warum nicht eine Linter-Warnung/-Fehler für f.(x, y, z) , wie "alle Argumente von 'f' werden als Skalare übertragen" ?

Wie auch immer, es könnte ratsam sein, #27563 (zB durch #27608) zu beheben und Benutzer eine Weile vor 1.0 damit spielen zu lassen.
[0.7 und 1.0.0-rc1.0 wurden ohne Fix veröffentlicht].

Wie auch immer, es könnte ratsam sein, #27563 (zB durch #27608) zu beheben und Benutzer eine Weile vor 1.0 damit spielen zu lassen.
[0.7 und 1.0.0-rc1.0 wurden ohne Fix veröffentlicht].

Ich nehme an , Sie haben die Nachricht verpasst , dass 1.0 veröffentlicht wurde .

@StefanKarpinski Das habe ich tatsächlich verpasst. Herzlichen Glückwunsch an alle Entwickler, Julia ist großartig, weiter so!

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

yurivish picture yurivish  ·  3Kommentare

musm picture musm  ·  3Kommentare

iamed2 picture iamed2  ·  3Kommentare

dpsanders picture dpsanders  ·  3Kommentare

manor picture manor  ·  3Kommentare