Julia: La diffusion avait un seul travail (par exemple la diffusion sur des itérateurs et un générateur)

Créé le 21 sept. 2016  ·  69Commentaires  ·  Source: JuliaLang/julia

Il était surprenant de constater que broadcast ne fonctionne pas avec les itérateurs

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

Cela est dû au fait que Broadcast.containertype renvoie Any https://github.com/JuliaLang/julia/blob/413ed79ec54f3a754ac8bc57c1d29835d17bd274/base/broadcast.jl#L31
menant au repli sur : https://github.com/JuliaLang/julia/blob/413ed79ec54f3a754ac8bc57c1d29835d17bd274/base/broadcast.jl#L265

Définir containertype comme Array pour cet itérateur entraîne des problèmes d'appel de size dessus, car broadcast ne vérifie pas l'interface iteratorsize(IterType) l'itérateur

map résout ce problème avec la solution map(f, A) = collect(Generator(f,A)) repli broadcast(f, Any, A) = f(A)

broadcast

Commentaire le plus utile

C'est intentionnel. broadcast est destiné aux conteneurs avec des formes et traite par défaut les objets comme des scalaires. map est pour les conteneurs sans formes et traite par défaut les objets comme des itérateurs.

Par exemple, broadcast traite les chaînes comme des "scalaires", alors que map parcourt les caractères.

Tous les 69 commentaires

C'est intentionnel. broadcast est destiné aux conteneurs avec des formes et traite par défaut les objets comme des scalaires. map est pour les conteneurs sans formes et traite par défaut les objets comme des itérateurs.

Par exemple, broadcast traite les chaînes comme des "scalaires", alors que map parcourt les caractères.

Peut-être que le problème est que les gens trouvent la nouvelle syntaxe à points trop pratique. Cependant, il y a eu un désir d'avoir une manière compacte d'exprimer map dans le passé. Malheureusement, la syntaxe à points est déjà prise.

De plus, comme @stevengj l' a déjà souligné : il doit y avoir une différence entre map et broadcast , sinon, à quoi bon avoir les deux.

@stevengj Mais les itérateurs ont une forme (en particulier les générateurs) http://docs.julialang.org/en/release-0.5/manual/interfaces/#interfaces

Je dirais que les itérateurs sont dans ce domaine délicat étaient la plupart des choses que vous voudriez faire avec un conteneur que vous voulez également faire avec des itérateurs, et oui peut-être que c'est purement le fait que la syntaxe . est trop pratique (et l'erreur que vous obtenez est très opaque).

@pabloferz La principale différence entre map et broadcast est le traitement des scalaires. Maintenant, la définition de scalaire est discutable et je dirais que tout ce qui a length(x) > 1 ne doit pas être considéré comme un scalaire.

Marquer quels arguments doivent être traités comme itérables, au lieu de l'appel de fonction lui-même, supprimerait l'ambiguïté. Je pense?

Pour broadcast (je crois aussi en général) avoir une forme, signifie avoir size (pas seulement length ) et être indexable. À l'exception des tuples, tout le reste sans size est traité comme un scalaire. Compte tenu de l'implémentation actuelle, vous devez d'abord getindex ou pouvoir en définir un pour l'objet sur lequel vous souhaitez diffuser. Pour les itérateurs, ce n'est pas possible en général.

Je suis tombé sur ça aussi. Venant de #16769 où je cherche un moyen de fill! un tableau avec des évaluations répétées d'une fonction (au lieu d'une valeur fixe), je pensais que la syntaxe par points pouvait déjà faire l'affaire. Mais, quand a = zeros(2, 3); a .= [rand() for i=1:2, j=1:3] fonctionne, le (serait) moins cher a .= (rand() for i=1:2, j=1:3) ne fonctionne pas ; ce générateur est HasShape() , mais n'a en effet aucune capacité d'indexation. Je comprends très mal le fonctionnement de la diffusion/syntaxe par points, mais cela aiderait-il ici d'avoir un trait pour les capacités d'indexation ? il y a déjà un PR (#22489) pour ça...

@rfourquet , tu peux faire a = zeros(2, 3); a .= rand.()

Oui, mais j'aurais dû être plus précis : je veux utiliser une fonction qui récupère les indices en paramètres, comme a .= (f(i, j) for i=1:2, j=1:3) .

Quels seraient les inconvénients de la diffusion des dimensions des itérateurs HasShape ? Cela semble être une chose naturelle à faire.

@nalimilan , à première vue, je pense que ce serait raisonnable et probablement relativement facile à mettre en œuvre. Serait en rupture donc devrait être fait par 1.0.

Un problème potentiel avec ceci est que les itérateurs HasShape ne prennent pas nécessairement en charge getindex , et cela pourrait rendre la mise en œuvre difficile ?

Une possibilité serait de créer temporairement (pour la 1.0) une implémentation simple qui vient d'être copiée dans un tableau. Cela permettrait une optimisation post 1.0

Un problème potentiel avec ceci est que les itérateurs HasShape ne prennent pas nécessairement en charge getindex, et cela pourrait le rendre difficile à implémenter ?

Comme je l'ai dit plus haut, j'ai un PR au n° 22489 pour permettre l'indexation dans les itérateurs, si cela peut aider.

Que faut-il faire pour 1.0 afin qu'au moins nous puissions améliorer le comportement en 1.x ?

Merci @nalimilan d'avoir soulevé cela, je voulais le faire aussi. Si autoriser les générateurs HasShape sur le côté droit de l'expression de diffusion n'est pas possible à implémenter pour 1.0, devrions-nous en faire une erreur maintenant, au lieu de traiter les générateurs comme des scalaires ? afin que cela puisse être activé dans 1.x.

:+1: Triage recommande de faire une erreur (le choix sûr) ou d'appeler collect dessus (si c'est facile à faire).

map traite tous ses arguments comme des conteneurs et essaie de les parcourir tous. Dans mon monde idéal, broadcast serait similaire, et traiterait tous ses arguments comme ayant des formes qui peuvent être diffusées, et donnerait une erreur si par exemple size n'est pas défini. Je soulignerai que toute valeur peut être traitée comme un scalaire dans la diffusion en l'enveloppant avec fill , ce qui donne un tableau 0-d :

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

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

Proposez-vous vraiment de traiter tous les scalaires comme des conteneurs par défaut ? Cela n'a pas l'air très pratique.

En regardant comment nous pourrions soit prendre en charge n'importe quel itérable, soit simplement générer une erreur pour eux jusqu'à ce que nous les supportions, il semble que nous aurions besoin d'un moyen d'identifier les itérateurs dans BroadcastStyle . Actuellement, ce n'est pas possible, car Base.iteratorsize renvoie HasLength même pour des scalaires comme Symbol . Nous pourrions introduire un trait Base.isiterable (qui pourrait être utile pour d'autres choses), ou faire Base.iteratorsize par défaut à NotIterable (ce qui aurait du sens aussi avec HasLength par défaut semble toujours un peu surprenant, même s'il est inoffensif).

(Cas délicat pour une discussion future : UniformScaling .)

@timholy Depuis que vous avez fait la refonte de broadcast , des suggestions ?

@JeffBezanson , tout l'intérêt de broadcast est de pouvoir "diffuser" des scalaires pour correspondre aux conteneurs, par exemple pour faire ["bug", "cow", "house"] .* "s" ----> ["bugs", "cows", "houses"] . Ceci est fondamentalement différent du comportement de map .

C'est pourquoi broadcast traite les objets comme des scalaires par défaut, afin qu'ils puissent être combinés avec des conteneurs. Lancer une erreur pour un type non reconnu le rendrait beaucoup moins utile.

Il devrait être possible de déclarer un type particulier comme conteneur pour broadcast en définissant une méthode appropriée dessus, mais je pense que la valeur par défaut devrait continuer à être de traiter les objets comme des scalaires.

Dans un PR sans rapport (https://github.com/JuliaLang/julia/pull/25339), @Keno a suggéré d'utiliser applicable(start, (x,)) pour savoir si x est itérable ou non. Devrions-nous utiliser la même approche ici? Je trouverais plus clair d'avoir une définition plus explicite des itérateurs (basée soit sur Base.iteratorsize soit sur un trait), mais l'utilisation de start aussi du sens.

Nous pourrions avoir un trait explicite qui par défaut est applicable(start, (x,)) ; qui permettrait de la contourner si nécessaire.

J'ai déposé le #25356 pour illustrer les solutions possibles et leurs inconvénients.

D' après l'exemple ["bug", "cow", "house"] .* "s" ----> ["bugs", "cows", "houses"] @stevengj , l'itérabilité ne semble pas être suffisante, car les chaînes sont itérables mais agissent comme des scalaires. Si vous devez de toute façon définir un trait, il peut être préférable de continuer à exiger l'opt-in pour la diffusion, plutôt que d'ajouter des exigences à tous les itérateurs.

Heureusement, keys(dict) renvoie maintenant un AbstractSet , donc si nous ajoutions un trait de diffusion pour AbstractSet cela corrigerait l'exemple dans l'OP. Nous pourrions également ajouter une erreur de diffusion d'un Generator pour attraper certains cas courants.

Diffuser sur des conteneurs AbstractSet semble par nature un peu problématique : vous pouvez combiner un AbstractSet avec un scalaire, mais pas avec n'importe quel autre conteneur puisque l'ordre d'itération n'est pas spécifié pour un ensemble. Ce genre de brise le sens habituel d'une opération de "diffusion".

Oui, j'ai réalisé lors de la préparation du PR que les ensembles ne sont vraiment pas le meilleur exemple d'itérateurs qui devraient prendre en charge la diffusion. Des choses comme Generator et ProductIterator sont des cas beaucoup plus intéressants.

Peut-être que la réponse est de (essayer de) diffuser des itérateurs qui ont HasShape , et de continuer à traiter tout le reste comme des scalaires ? Ne résoudra pas l'OP, mais est assez élégant sinon.

Autre pensée aléatoire : peut-être que la diffusion sur 1 argument (comme dans string.(x) ) devrait être un cas spécial qui fonctionne plutôt comme map , puisque la compatibilité des formes n'est pas un problème ?

Peut-être que la réponse est de (essayer de) diffuser des itérateurs qui ont HasShape, et de continuer à traiter tout le reste comme des scalaires ? Ne résoudra pas l'OP, mais est assez élégant sinon.

Je ne suis pas sûr que nous ayons une bonne raison d'exclure les itérateurs HasLength . Nous prenons en charge la diffusion sur des tuples (qui n'implémentent pas size ), alors pourquoi ne pas traiter les itérateurs sans forme comme des tuples ? Par exemple, il serait parfaitement logique de pouvoir utiliser le résultat de keys(::OrderedDict) avec broadcast . Si nous ne le supportons pas, les gens seront tentés de définir leurs itérateurs comme HasShape juste pour être utilisables avec broadcast (et la belle syntaxe à points).

Pour citer Steve,

broadcast est pour les conteneurs avec des formes

HasShape semble être un moyen raisonnable de définir cela plus précisément. Sinon, il me semble qu'il faudrait casser le comportement de diffusion sur des chaînes, par exemple.

Nous avons déjà une incohérence, les tuples étant considérés comme des conteneurs et les chaînes comme des scalaires. Les cordes sont de toute façon très particulières, je ne pense pas que leur comportement soit dû à leur absence de forme : c'est plutôt lié au fait qu'elles sont la seule collection qui est plus souvent considérée comme un scalaire que comme un contenant.

Peut-être que @stevengj peut expliquer pourquoi il pense que broadcast ne devrait prendre en charge que les conteneurs avec une forme ? Souhaitez-vous considérer les tuples comme des scalaires également ?

Je pense que la justification pour traiter les tuples comme des conteneurs dans broadcast (#16986) était qu'en pratique, ils sont souvent utilisés comme des vecteurs essentiellement statiques, et les traiter comme des "scalaires" dans broadcast n'était tout simplement pas très utile en tout cas. En revanche, les chaînes (a) sont souvent traitées comme des "atomes" pour les opérations de traitement de chaînes et (b) n'ont pas d'indexation consécutive en général, elles s'intègrent donc très mal dans le framework broadcast .

En principe, je soutiendrais que les itérateurs HasShape soient utilisés comme conteneurs dans broadcast . Le principal problème, comme je l'ai noté ci-dessus, est qu'avoir HasShape ne garantit pas que getindex fonctionne.

Le principal problème, comme je l'ai noté ci-dessus, est que le fait d'avoir HasShape ne garantit pas que getindex fonctionne

Est-ce que quelque chose comme #22489 aiderait, c'est-à-dire avoir un trait d'itérateur qui indique si un itérateur est indexable ?

Est-ce que quelque chose comme #22489 aiderait, c'est-à-dire avoir un trait d'itérateur qui indique si un itérateur est indexable ?

Mais alors seuls les itérateurs indexables seraient supportés avec broadcast ? Cela semble trop restrictif, car il serait très utile de pouvoir faire des choses comme string.(itr, "1") pour n'importe quel itérable (par exemple le résultat de keys(::OrderedDict) ), et l'indexation n'est pas nécessaire pour implémenter cela. Je pense que nous ferions mieux de lancer une erreur pour tous les itérateurs qui ne prennent pas en charge l'indexation dans 0.7/1.0, et d'essayer de les prendre en charge dans les versions ultérieures. De toute façon, il n'est pas très utile de traiter les itérateurs comme des scalaires. Ensuite, nous pouvons implémenter le comportement que nous voulons dans les versions 1.x.

@stevengj Je suis d'accord sur vos arguments concernant les chaînes et les tuples, mais pourquoi ne devrions-nous pas traiter les itérateurs HasLength comme des tuples ? Je n'ai pas lu de justification pour cela jusqu'à maintenant.

@nalimilan , j'ai tendance à penser que seuls les itérateurs indexables+hasshape devraient être pris en charge par broadcast . Essayer d'entasser des itérateurs généraux dans cette fonction confond trop sa signification - à un moment donné, vous devriez simplement utiliser map .

serait très utile de pouvoir faire des choses string.(itr, "1") pour n'importe quel itérable… De toute façon, ce n'est pas très utile de traiter les itérateurs comme des scalaires.

Le cas des chaînes contredit cela - l'argument "1" lui-même est itérable dans votre exemple. Des tonnes de choses sont itérables (par exemple, les PyObject dans PyCall définissent start etc.), y compris des choses comme des ensembles non ordonnés où le concept broadcast est vraiment cassé.

Notez également que #24990 rendra map encore plus facile qu'il ne l'est maintenant, par exemple vous pourrez faire map(string(_,"1"), itr) .

@nalimilan , j'ai tendance à penser que seuls les itérateurs indexables + hasshape devraient être pris en charge par la diffusion. Essayer d'entasser des itérateurs généraux dans cette fonction confond trop sa signification - à un moment donné, vous devriez simplement utiliser map.

Nous n'avons pas de trait actuellement pour les itérateurs indexables. Comment suggéreriez-vous de transmettre cela? Mon WIP PR #25356 renverrait une erreur pour les itérateurs qui ne prennent pas en charge l'indexation, ce qui ne semble pas trop grave en supposant qu'il n'est pas très utile de traiter les itérateurs comme des scalaires. Si nous voulons les traiter comme des scalaires, nous aurions besoin d'un autre trait, n'est-ce pas ?

Je suis enclin à lever des erreurs pour tous les cas qui ne sont pas complètement évidents, afin que nous puissions implémenter n'importe quel comportement à l'avenir, plutôt que de nous enfermer dans un comportement par défaut qui n'est pas forcément très utile (c'est-à-dire traiter certains itérateurs comme des scalaires) . Comme le montre ce problème, le comportement de broadcast prend du temps pour être conçu correctement.

(FWIW, PyObject ne me semble pas être un bon exemple car IIUC implémente le protocole d'itération simplement parce qu'il ne sait pas à l'avance s'il encapsulera un itérateur Python ou non. PyObject est clairement une exception ici, tout comme il doit utiliser getfield surcharge de

Nous pourrions ajouter un trait pour les itérateurs indexables HasShape , comme cela a été suggéré ailleurs.

Triage aime l'idée de faire en sorte que la diffusion itère sur tous les arguments (comme map) et d'ajouter un caractère opérateur (comme faire const & = Ref comme proposé précédemment dans un autre numéro, ou peut-être ~ ) pour marquer explicitement arguments 0-d.

@vtjnash , qu'est-ce que cela signifie même pour un itérateur non HasShape ? Voulez-vous dire que vous souhaitez que la diffusion itère sur des éléments tels que des chaînes et des ensembles ? L'implémentation actuelle de broadcast est étroitement liée à getindex … avez-vous pensé à la façon dont vous l'implémenterez sans getindex , en particulier pour combiner des arguments de différentes dimensionnalités ?

En théorie, il devrait être possible de supporter des itérateurs non indexables (au moins ceux qui ont un ordre significatif). C'est facile lorsque toutes les entrées ont la même forme ; lorsqu'ils sont de formes différentes et que l'itérateur a une forme différente (plus petite) que le résultat, un stockage intermédiaire serait nécessaire.

On dirait que le trait IteratorAccess de PR https://github.com/JuliaLang/julia/pull/22489 pourrait être adapté/réutilisé pour détecter les itérateurs indexables. Savoir quels itérateurs sont indexables (et devraient donc implémenter keys ) est également nécessaire pour https://github.com/JuliaLang/julia/pull/24774.

Cc : @rfourquet

👍 Triage recommande d'en faire une erreur (le choix le plus sûr) ou d'appeler à frais virés (si c'est facile à faire).

Le triage pourrait-il décider d'une stratégie spécifique à adopter ici ? Par exemple, qu'est-ce que "cela" dans le commentaire de @JeffBezanson ci-dessus ? Devrions-nous lancer des erreurs pour tous les itérateurs qui ne prennent pas en charge l'indexation (choix le plus sûr pour le moment, afin que nous puissions faire tout ce que nous voulons plus tard), ou devrions-nous traiter certains itérateurs comme des scalaires ? Doit-on ajouter un trait pour les itérateurs indexables, et si oui, sous quelle forme (nouveau trait vs. nouveau choix pour Base.IteratorSize ) ? Doit-on ajouter un trait pour les itérateurs en général (afin de pouvoir les distinguer des scalaires) ?

Le comportement suivant semble bon :

  • Par défaut, essayez d'itérer et de diffuser chaque argument.
  • Donnez une erreur si cela ne fonctionnera pas pour une raison quelconque.
  • Passez Ref(x) ou [x] pour forcer x à être traité comme un scalaire.
  • Ajoutez un trait qui peut être défini pour permettre à un nouveau type d'être traité comme un scalaire au lieu de donner une erreur. Notez que cela ne doit pas être utilisé pour choisir entre itérer et ne pas itérer. C'est juste pour transformer l'erreur en comportement scalaire.

Pourriez-vous clarifier la note sur le dernier point (peut-être avec un exemple) ? Je ne suis pas sûr de ce que cela signifie pour le trait d'exister mais de ne pas être utilisé pour choisir entre itérer et ne pas itérer.

Donc, fondamentalement, "essayer d'itérer et de diffuser chaque argument" implique que nous devons définir BroadcastStyle pour retourner Scalar() pour tous les types de non-collection (notamment Number , Symbol et AbstractString ) ? Cela ressemble au "trait" mentionné dans la dernière puce.

Honnêtement, je trouverais moins coûteux de définir un trait pour les itérables que de définir un trait pour les non-interables/scalaires. Je crains que tous les types de non-collection implémentent à un moment donné ce trait Scalar , car cela peut être utile dans certains cas (peut-être rares).

Cela ressemble au "trait" mentionné par la dernière puce

Non, la dernière puce signifie que si quelque chose implémente l'itération, alors la diffusion l'itére - le trait Scalaire aura disparu. Pour certains types communs distinctement non itérables (tels que les sous-types de Type et Function ), alors nous pourrions vouloir avoir un trait NotIterable qui transforme le MethodError dans une itération qui produit une valeur (cet objet). Je ne me souviens pas vraiment pourquoi cela était nécessaire cependant.

Donc, fondamentalement, "essayer d'itérer et de diffuser chaque argument" implique que nous devons définir BroadcastStyle pour renvoyer Scalar() pour tous les types de non-collection (notamment Number, Symbol et AbstractString) ? Cela ressemble au "trait" mentionné dans la dernière puce.

Non, tous les sous-types scalaires de Number s'itérent d'eux-mêmes et vont donc bien. Nous aurions besoin de le définir comme symbole. AbstractString fonctionnerait comme une collection.

Je n'aime pas toute conception qui nous oblige à définir une méthode pour qu'un type soit traité comme un scalaire. Cela devrait être la valeur par défaut. Je ne pense pas non plus que les chaînes doivent être traitées comme des conteneurs pour la diffusion.

Je pense toujours que broadcast ne devrait traiter que les itérateurs HasShape comme des conteneurs ; cela est cohérent avec la conception de la diffusion depuis le début. Quel est le problème avec ça?

Le problème avec cela est celui du PO; si vous avez un itérateur sans forme, le traiter comme un scalaire donne une réponse folle.

Aussi, je serais parfaitement heureux de laisser tomber la partie "trait" de la proposition. Personne ne se plaint

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

On peut soutenir que la raison pour laquelle le résultat dans l'OP est inattendu est que personne n'a vraiment l'intention d'un appel de diffusion pour traiter _tous_ les arguments comme des scalaires ; s'il n'y a qu'un seul argument et qu'il existe un moyen de le traiter comme une collection/itérateur, c'est presque certainement ce que l'utilisateur souhaite. Bien sûr, 1 .+ 1 devrait continuer à fonctionner ?

Cela m'est venu à l'esprit, mais il semble déroutant de faire d'un argument un cas particulier.

Je vois l'asymétrie suivante : traiter un itérable comme un scalaire donne des résultats vraiment étranges, mais traiter un scalaire comme un itérable donne une erreur. Lorsque vous obtenez l'erreur, il est facile de la corriger en enveloppant l'argument. Alors que dans le premier cas, il n'y a rien de simple que vous puissiez faire pour le faire itérer sur l'argument.

Je pense toujours que broadcast ne devrait traiter que les itérateurs HasShape comme des conteneurs ; cela est cohérent avec la conception de la diffusion depuis le début. Quel est le problème avec ça?

@stevengj Ce qui ne va pas, à HasLength comme Tuple , qui est actuellement un cas particulier. Même si nous ne les supportons pas pour le moment, j'aimerais laisser ouverte la possibilité de les supporter à un moment donné dans 1.x.

Personne ne se plaint

julia> map(chaîne, [1,2], :a)
ERREUR : MethodError : aucune méthode correspondant à start(::Symbol)

@JeffBezanson OTC, je prends en charge le comportement actuel de broadcast , qui répète :a au besoin. Ce genre de chose peut être très utile par exemple pour renommer une série de colonnes DataFrame . Suggérez-vous de changer broadcast pour générer une erreur comme map ?

Je vois l'asymétrie suivante : traiter un itérable comme un scalaire donne des résultats vraiment étranges, mais traiter un scalaire comme un itérable donne une erreur. Lorsque vous obtenez l'erreur, il est facile de la corriger en enveloppant l'argument. Alors que dans le premier cas, il n'y a rien de simple que vous puissiez faire pour le faire itérer sur l'argument.

C'est facile, mais assez peu pratique. Je suis d'accord avec @stevengj pour Number sont itérables, la gêne ne serait pas toujours visible, mais comme le montre l'exemple Symbol , cela ne serait pas très utile en général. Char serait un autre, et de nombreux types personnalisés définis dans le package en souffriront également (et finiront par définir leur BroadcastStyle comme Scalar() ).

Je pense que le nœud du problème est que nous n'avons pas de trait pour distinguer les collections des scalaires. La solution la plus directe était donc de traiter les itérateurs HasShape comme des collections, et les autres types comme des scalaires (y compris les itérateurs HasLength puisque c'est la valeur par défaut pour tous les types). Personnellement, je pense que l'introduction d'un trait pour les collections/itérables aurait beaucoup de sens, mais si nous ne sommes pas prêts à le faire et si nous ne pouvons pas compter sur la définition de start pour détecter les itérables, je suis peur que nous devions garder le comportement actuel.

La proposition de Jeff dans https://github.com/JuliaLang/julia/issues/18618#issuecomment -360594955 a de la place pour permettre la diffusion en traitant Symbol et Char comme des types "scalaires" - ils besoin d'accepter le comportement. Par défaut, ce serait une erreur, car ils n'implémentent pas start .

La partie la plus convaincante ici est que la seule définition sensée d'un type "collection" est qu'il est itérable. Oui, cela signifie que les chaînes sont des collections. Parfois, ils sont utilisés comme tels ! Donc, optons par défaut pour le comportement qui permet facilement aux gens de s'inscrire à l'autre sur le site d'appel.

Il y a une verrue ici, cependant. Étant donné que les nombres sont itérables (ils sont même HasShape ), ils seront traités comme des conteneurs de dimension zéro. Cela signifie que, poussé à sa conclusion logique, 1 .+ 2 != 3 . Ce serait fill(3, ()) place.

EDIT : pour tenter d'éviter de faire dérailler davantage le fil, déplacé vers le discours :

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

La proposition de Jeff dans #18618 (commentaire) a de la place pour permettre la diffusion de traiter Symbol et Char comme des types "scalaires" - ils ont juste besoin d'accepter le comportement. Par défaut, il s'agirait d'une erreur, car ils n'implémentent pas start.

Oui, ma position est simplement basée sur l'hypothèse que les scalaires sont une solution de repli plus naturelle, d'autant plus que les collections doivent implémenter certaines méthodes (itération, éventuellement indexation) alors que les scalaires ne sont que "le reste" et n'ont rien en commun. En fin de compte, n'importe quel type pourra implémenter n'importe quel comportement, mais nous devons le rendre aussi pratique et logique que possible, ce qui devrait notamment aider à éviter les incohérences (par exemple certains types déclarés et se comportant comme des scalaires et d'autres non).

Je ne suis pas trop préoccupé par le fait d'avoir quelques exceptions pour les types essentiels comme les chaînes et les nombres, tant que les règles sont claires pour les autres types.

J'ai un peu réfléchi à nos interfaces d'itération et d'indexation. Je note que nous pouvons avoir des objets utiles qui itèrent (mais ne peuvent pas être indexés), des objets qui peuvent être indexés (mais ne sont pas itérables), et des objets qui font les deux. Sur cette base, je me demande si :

  • map pourrait être fortement lié au protocole d'itération - il semble valide que nous puissions faire un out = map(f, iterable) paresseux pour tout iterable arbitraire tel que par exemple first(out) est le identique à f(first(iterable)) , et il me semble que cette opération paresseuse générique pourrait être utile.
  • broadcast pourrait être fortement lié à l'interface d'indexation - il semble valide que nous puissions faire un out = broadcast(f, indexable) paresseux tel que out[i] soit le même que f(indexable[i]) , et il me semble que cette opération paresseuse générique pourrait être utile. Évidemment, broadcast avec plusieurs entrées pourrait toujours faire tout ce qu'il fait maintenant. Aux fins de la diffusion, les scalaires seraient les éléments qui ne peuvent pas être indexés (ou indexés de manière triviale comme Number et Ref et AbstractArray{0} ).

Je pense également qu'il serait souhaitable qu'un argument map et un argument broadcast fassent des choses très similaires pour les collections qui sont à la fois itérables et indexables. Cependant, le fait que l'itération AbstractDict renvoie des choses différentes de getindex semble bloquer une belle unification ici. :frowning_face: (Nos autres types de collections semblent bien)

(Pour moi, le fait que des choses comme les chaînes doivent être explicitement enveloppées comme ["bug", "cow", "house"] .* ("s",) ne sonne pas comme une rupture ici. J'ai le même problème quand je veux penser à un 3-vecteur comme étant un "point 3D unique" et ce n'est pas trop difficile à gérer (xref #18379)).

Je suis d'accord que broadcast devrait être pour les conteneurs indexables, mais je pense que cela devrait être indexable consécutivement , ce qui exclut les chaînes. par exemple, collect(eachindex("aαb🐨γz")) donne [1, 2, 4, 5, 9, 11] , qui fonctionnera mal avec n'importe quelle implémentation de broadcast basée sur l'indexation.

Mais être pour les conteneurs indexables, c'est essentiellement que les conteneurs ont besoin d'un trait pour s'inscrire, ce qui est essentiellement ce que j'ai préconisé.

Je ne suis pas sûr que les indices consécutifs soient une bonne contrainte - les dictionnaires auront des indices arbitraires, par exemple.

Cependant, broadcast(f, ::String) ne peut pas créer un nouveau String et garantir que les indices de sortie restent les mêmes que les indices d'entrée puisque les largeurs de caractères UTF-8 peuvent changer sous f ( il faudrait qu'il se transforme en quelque chose comme un AbstractDict{Int, Char} pour faire cette garantie, ce qui ne semble vraiment pas très utile !). Je dirais presque que les indices d'un String ressemblent davantage à des "jetons" pour une recherche rapide plutôt qu'à des indices sémantiquement importants (par exemple, vous pouvez convertir en une chaîne UTF-32 équivalente et les indices changeraient).

Cela ne me dérange pas si nous rendons le comportement de diffusion opt-in via un trait; Je dis juste qu'imaginer comment se comporte un broadcast(f, ::Any) générique est un bon moyen de guider la mise en œuvre de choses comme broadcast(f, ::AbstractDict) (et répondrait naturellement à la question que j'ai soulevée dans #25904, c'est-à-dire diffuser sur valeurs du dictionnaire et non des paires clé-valeur).

Les gens sont-ils vraiment satisfaits de ce changement ? Pour ma part, je n'ai jamais eu besoin de diffuser sur un conteneur sans forme, alors que je diffuse sur des choses qui devraient être traitées comme des scalaires tout le temps . Chaque avertissement de dépréciation que je "répare" me fait verser une larme.

Je diffuse sur des choses qui devraient être traitées comme des scalaires _tout le temps_.

Quels sont les types de ces choses?

Peut être n'importe quoi. Par exemple, dans un package qui définit un type de modèle d'optimisation Model et un type de variable de décision Variable , vous pouvez avoir x::Vector{Variable} dont vous souhaitez obtenir les valeurs après avoir résolu le modéliser model utilisant une fonction value(::Variable, ::Model)::Float64 . Auparavant, vous pouviez le faire comme :

value.(x, model)

Il arrive aussi souvent que les types d'arguments proviennent d'autres packages, donc ajouter une méthode à broadcastable pour ces types serait un piratage de type dans ce cas. Vous devez donc utiliser Ref ou un tuple à un élément. Ce n'est pas insurmontable, mais cela rend simplement le cas courant beaucoup moins élégant afin de prendre en charge un modèle d'utilisation relativement obscur, à mon avis.

Oui, je vois votre point de vue, et je suis d'accord pour dire que c'est ennuyeux dans des situations comme celle-là. Cela dit, l'ancien comportement était absolument problématique - c'était l'une de ces choses "le repli par défaut est définitivement mauvais dans certains cas".

En bref, il existe quatre options qui évitent le mauvais repli :

  1. nécessitent _everything_ pour implémenter une méthode qui décrit comment ils diffusent
  2. Par défaut, traite les choses comme des conteneurs et erreur/dépréciation pour les non-conteneurs.

    • Nous allons simplement essayer de iterate des objets inconnus et cela générera une erreur pour les scalaires

    • Il existe deux trappes d'échappement pour les scalaires : les utilisateurs peuvent les encapsuler sur un site d'appel et les auteurs de bibliothèques peuvent opter pour une diffusion de type scalaire non encapsulée.

  3. Par défaut, traiter les choses comme des scalaires et une erreur pour les conteneurs inconnus

    • Étant donné qu'il n'y a pas de méthodes pertinentes définies uniquement pour les scalaires, nous devrions affirmer que iterate renvoie une erreur de méthode. C'est lent et détourné.

    • Il n'y aurait qu'une seule trappe d'évacuation disponible pour que les conteneurs personnalisés ne se trompent pas : leurs auteurs de bibliothèques s'engagent explicitement à diffuser. Cela semble assez en arrière pour une fonction dont le but principal est de travailler avec des conteneurs.

  4. Vérifiez applicable(iterate, …) et changez de comportement en conséquence

    • Cela ne fonctionne actuellement pas en raison du mécanisme de dépréciation de start/next/done, et en général pourrait être erroné pour les types de wrapper qui reportent les méthodes à un membre.

L'option 1 est pire pour tout le monde, l'option 2 est le statu quo, et l'option 3 est à l'envers, et l'option 4 est quelque chose que nous n'avons jamais fait auparavant et qui risque d'être buggé.

Je suppose qu'une partie de la discussion a dû avoir lieu dans les coulisses, mais je ne suis tout simplement pas convaincu par les arguments que j'ai vus dans ce fil et dans https://github.com/JuliaLang/julia/pull/25356 contre nalimilan et stevengj .

Il n'y aurait qu'une seule trappe d'évacuation disponible pour que les conteneurs personnalisés ne se trompent pas : leurs auteurs de bibliothèques s'engagent explicitement à diffuser. Cela semble assez en arrière pour une fonction dont le but principal est de travailler avec des conteneurs.

C'est mon principal point de désaccord. Il me semble que dans tout le code Julia # of iterator types << # of types that should be treated as scalars in a broadcast situation < # of broadcast calls . Je préférerais donc que le nombre de fois où quelque chose de « extra » doit être fait dépend du nombre de types d'itérateurs, plutôt que du nombre d'appels de diffusion. Et si un auteur de bibliothèque définit un itérateur, il n'est pas complètement déraisonnable de lui demander de définir une méthode de plus, alors qu'il _est_ complètement déraisonnable de demander à chaque auteur de package de définir Base.broadcastable(x) = Ref(x) pour tous leurs types non itérables afin de évitez les appels laids (IMHO) Ref dans un pourcentage élevé d'appels broadcast .

Je sais que d' avoir une seule méthode à mettre en œuvre pour définir l' itération est agréable, mais ce n'est pas que beaucoup de travail à mettre en œuvre un plus soit pour un nouveau trait, ou pour le rendre nécessaire de préciser Base.iteratorsize pour un nouveau iterator (et se débarrasser du défaut HasLength problématique). La méthode de secours broadcastable pourrait alors être basée sur ce trait. Ou, si vous êtes vraiment amoureux de la définition de l'itération avec une seule méthode, vous pouvez (post-deprecation-removal) avoir ce trait explicite par défaut à applicable(iterate, ...) comme dans https://github.com/JuliaLang/ julia/issues/18618#issuecomment -354618742, et remplacez simplement cette valeur par défaut si nécessaire. Les cas de coin comme String pourraient également être traités en spécialisant davantage broadcastable si vous le souhaitez.

C'est effectivement la conception de 0.6, qui a conduit à ce problème et à #26421 et #19577 et #23197 et #23746 et peut-être plus - la recherche est difficile.

Cela signifie que Base fournit une solution de secours par défaut qui est incorrecte pour toute une classe d'objets. C'est pourquoi je préfère un mécanisme qui génère des erreurs à moins que vous ne l'acceptiez, d'une manière ou d'une autre. C'est opiniâtre et la transition est pénible, mais cela vous oblige à être explicite.

Vous avez peut-être raison de dire qu'il existe plus de types personnalisés "scalaires" que de types itérateurs, mais je maintiens le fait que la diffusion est avant tout une opération sur des conteneurs. Je m'attends f.(x) ce que f(x) .

Et, enfin, les conteneurs qui reçoivent le traitement scalaire par défaut ne peuvent tout simplement pas être utilisés par élément avec la diffusion. Par exemple, String est un type de collection que nous avons spécialement conçu pour se comporter comme un scalaire ; il n'est pas possible d'"atteindre" et de travailler par élément, même si cela semble logique dans certaines situations (par exemple, isletter.("a1b2c3") ). C'est l'argument de l'asymétrie : vous pouvez envelopper plus efficacement les conteneurs dans une Ref pour les traiter comme des scalaires que vous ne pouvez les collect dans une collection réellement diffusable.

Ce sont les principaux arguments. En ce qui concerne la laideur de Ref, je suis entièrement d'accord. Une solution il y a #27608.

Assez juste. Je n'ai pas d'arguments renversants ou de solutions magiques à ces problèmes, et https://github.com/JuliaLang/julia/pull/27608 améliorera les choses.

@tkoolen J'ai eu les mêmes préoccupations et cas d'utilisation .

@mbauman Les arguments donnés ci -

1) Il serait possible de faire de broadcastable une interface requise pour tout itérable.
Ce serait tout à fait systématique, et obligerait les développeurs à réfléchir à
comment leur itérateur doit se comporter en diffusion.
Une recommandation de le définir comme collect(x) rendrait la transition relativement facile dans la plupart des cas.
Il n'y aurait pas de perte de performances, non ?

2) Cela se résume donc à la volonté d'avoir une erreur pour f.(x) si x diffuse comme un scalaire.
Pourquoi pas un avertissement/erreur linter pour f.(x, y, z) , tel que "tous les arguments de 'f' diffusés en tant que scalaires" ?

Quoi qu'il en soit, il pourrait être sage de corriger #27563 (par exemple par #27608) et de laisser les utilisateurs jouer avec un peu avant la 1.0.
[0.7 et 1.0.0-rc1.0 ont été publiés sans correctif].

Quoi qu'il en soit, il pourrait être sage de corriger #27563 (par exemple par #27608) et de laisser les utilisateurs jouer avec un peu avant la 1.0.
[0.7 et 1.0.0-rc1.0 ont été publiés sans correctif].

Je suppose que vous avez manqué la nouvelle de la sortie de la 1.0 .

@StefanKarpinski J'ai raté ça, en effet. Félicitations à tous les développeurs, Julia est incroyable, continuez !

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

StefanKarpinski picture StefanKarpinski  ·  3Commentaires

i-apellaniz picture i-apellaniz  ·  3Commentaires

manor picture manor  ·  3Commentaires

iamed2 picture iamed2  ·  3Commentaires

ararslan picture ararslan  ·  3Commentaires