Julia: Syntaxe alternative pour `map(func, x)`

Créé le 23 sept. 2014  ·  283Commentaires  ·  Source: JuliaLang/julia

Cela a été discuté en détail ici . J'avais du mal à le trouver et je pensais qu'il méritait son propre problème.

breaking speculative

Commentaire le plus utile

Une fois qu'un membre du triumvirat (Stefan, Jeff, Viral) a fusionné #15032 (qui, je pense, est prêt pour la fusion), je fermerai ceci et déposerai un problème de feuille de route pour décrire les modifications proposées restantes : correction du calcul du type de diffusion, obsolescence @vectorize , transformez .op en sucre de diffusion, ajoutez une "fusion de diffusion" au niveau de la syntaxe et fusionnez enfin avec l'affectation sur place. Les deux derniers ne feront probablement pas partie de la version 0.5.

Tous les 283 commentaires

+1

Ou func.(args...) comme sucre syntaxique pour

broadcast(func, args...)

Mais je suis peut-être le seul à préférer ça ?
Dans tous les cas, +1.

:-1: Si quoi que ce soit, je pense que l' autre suggestion de Stefan de f[...] a une belle similitude avec les compréhensions.

Comme @ihnorton , je n'aime pas non plus cette idée. En particulier, je n'aime pas l'asymétrie d'avoir à la fois a .+ b et sin.(a) .

Peut-être que nous n'avons pas besoin de syntaxe spéciale. Avec #1470, nous pourrions faire quelque chose comme

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

droit? Peut-être que ce serait trop magique pour obtenir une carte automatique sur n'importe quelle fonction.

@quinnj Cette ligne résume mes plus grandes craintes concernant l'autorisation de la surcharge des appels. Je ne pourrai pas dormir pendant des jours.

Pas encore sûr que ce soit syntaxiquement possible, mais qu'en est-il de .sin(x) ? Est-ce plus similaire à a .+ b ?

Je pense que [] devient trop surchargé et ne fonctionnera pas à cette fin. Par exemple, nous pourrons probablement écrire Int(x) , mais Int[x] construit un tableau et ne peut donc pas signifier map .

Je serais à bord avec .sin(x) .

Nous aurions à récupérer une syntaxe pour cela, mais si Int(x) est la version scalaire alors Int[x] est raisonnable par analogie pour construire un vecteur de type d'élément Int . Dans mon esprit, la possibilité de rendre cette syntaxe plus cohérente est en fait l'un des aspects les plus attrayants de la proposition f[v] .

Comment rendre la syntaxe f[v] pour map rendre la syntaxe plus cohérente ? Je ne comprends pas. map a une "forme" différente de la syntaxe actuelle du constructeur de tableau T[...] . Qu'en est-il de Vector{Int}[...] ? Cela ne marcherait-il pas ?

lol, désolé pour la peur @JeffBezanson ! Haha, la surcharge d'appels est définitivement un peu effrayante, de temps en temps, je pense aux types d'obscurcissement de code que vous pouvez faire dans julia et avec call , vous pourriez faire des trucs épineux.

Je pense que .sin(x) semble être une bonne idée aussi. Y avait-il un consensus sur ce qu'il fallait faire avec les arguments multiples ?

:-1:. Économiser quelques caractères par rapport à l'utilisation de fonctions d'ordre supérieur, je ne pense pas que cela vaille le coût de la lisibilité. Pouvez-vous imaginer un fichier avec .func() / func.() et func() entrecoupés partout ?

Il semble probable que nous supprimerons la syntaxe a.(b) toute façon, au moins.

Wow, parlez de remuer un nid d'abeilles ! J'ai changé le nom pour mieux refléter la discussion.

Nous pourrions également renommer map à 2 arguments en zipWith :)

Si une syntaxe est vraiment nécessaire, que diriez-vous de [f <- b] ou d'un autre jeu de mots sur les compréhensions _à l'intérieur_ des crochets ?

( @JeffBezanson , vous avez juste peur que quelqu'un écrive CJOS ou Moose.jl :) ... si nous obtenons cette fonctionnalité, mettez-la simplement dans la section Don't do stupid stuff: I won't optimize that du manuel)

L'écriture actuelle Int[...] indique que vous construisez un tableau de type d'élément Int . Mais si Int(x) signifie convertir x en Int en appliquant Int en tant que fonction, alors vous pouvez également considérer Int[...] signifie "appliquer Int à chaque chose dans ...", oh qui, soit dit en passant, produit des valeurs de type Int . Ainsi, écrire Int[v] serait équivalent à [ Int(x) for x in v ] et Int[ f(x) for x in v ] serait équivalent à [ Int(f(x)) for x in v ] . Bien sûr, alors vous avez perdu une partie de l'utilité d'écrire Int[ f(x) for x in v ] en premier lieu - c'est-à-dire que nous pouvons savoir statiquement que le type d'élément est Int - mais si vous appliquez cela Int(x) doit produire une valeur de type Int (pas une contrainte déraisonnable), alors nous pourrions récupérer cette propriété.

Me frappe comme plus de vectorisation / enfer de chat implicite. Que ferait Int[x, y] ? Ou pire, Vector{Int}[x] ?

Je ne dis pas que c'est la meilleure idée de tous les temps ni même que je la préconise - je souligne simplement qu'elle n'est pas _complètement_ en conflit avec l'utilisation existante, qui est elle-même un peu un hack. Si nous pouvions intégrer l'utilisation existante dans un modèle plus cohérent, ce serait une victoire. Je ne suis pas sûr de ce que signifierait f[v,w] - les choix évidents sont [ f(x,y) for x in v, y in w ] ou map(f,v,w) mais il y a encore plus de choix.

J'ai l'impression que a.(b) est à peine utilisé. A effectué un test rapide et il n'est utilisé que dans 54 des ~ 4 000 fichiers source de julia dans la nature : https://gist.github.com/jakebolewski/104458397f2e97a3d57d.

Je pense que ça détonne complètement. T[x] a la "forme" T --> Array{T} , tandis que map a la forme Array{T} --> Array{S} . Ce sont à peu près incompatibles.

Pour ce faire, je pense que nous devrions abandonner T[x,y,z] en tant que constructeur pour Vector{T} . L'ancienne indexation de tableau simple, A[I]I est un vecteur, peut être vue comme map(i->A[i], I) . "Appliquer" un tableau, c'est comme appliquer une fonction (bien sûr, Matlab utilise même la même syntaxe pour eux). En ce sens, la syntaxe fonctionne vraiment, mais nous perdrions la syntaxe vectorielle typée dans le processus.

J'ai un peu l'impression que débattre de la syntaxe ici détourne l'attention du changement le plus important : rendre map rapide.

De toute évidence, rendre map rapide (ce qui, soit dit en passant, doit faire partie d'une refonte assez approfondie de la notion de fonctions dans julia) est plus important. Cependant, passer de sin(x) à map(sin, x) est très important du point de vue de la convivialité, donc pour vraiment tuer la vectorisation, la syntaxe est assez importante.

Cependant, passer de sin(x) à map(sin, x) est très important du point de vue de la convivialité, donc pour vraiment tuer la vectorisation, la syntaxe est assez importante.

Entièrement d'accord.

Je suis d'accord avec @JeffBezanson que f[x] est à peu près inconciliable avec les constructions de tableaux typées actuelles Int[x, y] etc.

Une autre raison de préférer .sin à sin. est de permettre enfin d'utiliser par exemple Base.(+) pour accéder à la fonction + dans Base (une fois a.(b) est supprimé).

Lorsqu'un module définit sa propre fonction sin (ou autre) et que nous voulons utiliser cette fonction sur un vecteur, faisons-nous Module..sin(v) ? Module.(.sin(v)) ? Module.(.sin)(v) ? .Module.sin(v) ?

Aucune de ces options ne semble plus vraiment bonne.

J'ai l'impression que cette discussion passe à côté de la viande du problème. C'est-à-dire: lors du mappage de fonctions à argument unique sur des conteneurs, j'ai l'impression que la syntaxe map(func, container) est _déjà_ claire et succincte. Au lieu de cela, ce n'est que lorsque nous traitons de multiples arguments que je pense que nous pourrions bénéficier d'une meilleure syntaxe pour le curry.

Prenez par exemple la verbosité de map(x->func(x,other,args), container) , ou enchaînez une opération de filtrage pour l'aggraver filter(x->func2(x[1]) == val, map(x->func1(x,other,args), container)) .

Dans ces cas, je pense qu'une syntaxe de carte raccourcie n'aiderait pas beaucoup. Non pas que je pense que ceux-ci sont particulièrement mauvais, mais a) je ne pense pas qu'un raccourci map aiderait beaucoup et b) j'adore me languir après une partie de la syntaxe de Haskell. ;)

IIRC, dans Haskell, ce qui précède peut être écrit filter ((==val) . func2 . fst) $ map (func1 other args) container avec un léger changement dans l'ordre des arguments en func1 .

Dans elm, .func est défini par x->x.func et c'est très utile, voir elm records . Ceci doit être pris en compte avant de prendre cette syntaxe pour map .

J'aime ça.

Bien que l'accès au terrain ne soit pas si important à Julia que dans de nombreuses langues.

Oui, cela semble moins pertinent ici car les champs de Julia sont plutôt destinés à un usage "privé". Mais avec la discussion en cours sur la surcharge de l'accès aux champs, cela peut devenir plus judicieux.

f.(x) ressemble à la solution la moins problématique, s'il n'y avait pas l'asymétrie avec .+ . Mais garder l'association symbolique de . à "l'opération par élément" est une bonne idée à mon humble avis.

Si la construction actuelle des tableaux typés peut être obsolète, alors func[v...] peut être traduit en map(func, v...) , et les tableaux littéraux peuvent alors être écrits T[[a1, ..., an]] (au lieu de l'actuel T[a1, ..., an] ).

Je trouve aussi sin∘v silencieux (quand un tableau v est vu comme une application d'index à des valeurs contenues), ou plus simplement sin*v ou v*[sin]' ( ce qui nécessite de définir *(x, f::Callable) ) etc.

En revenant à ce problème avec un esprit neuf, j'ai réalisé que f.(x) peut être considéré comme une syntaxe assez naturelle. Au lieu de le lire comme f. et ( , vous pouvez le lire comme f et .( . .( est alors métaphoriquement une version élément par élément de l'opérateur d'appel de fonction ( , qui est entièrement cohérent avec .+ et ses amis.

L'idée .( soit un opérateur d'appel de fonction me rend très triste.

@johnmyleswhite Voulez -vous élaborer? Je parlais de l'intuitivité de la syntaxe, ou de sa cohérence visuelle avec le reste du langage, pas du tout de l'implémentation technique.

Pour moi, ( ne fait pas du tout partie de la sémantique du langage : c'est juste une partie de la syntaxe. Donc, je ne voudrais pas avoir à inventer un moyen pour que .( et ( commencent à différer. Le premier génère-t-il un multicall Expr au lieu d'un call Expr ?

Non. Comme je l'ai dit, je ne sous-entendais pas du tout qu'il devrait y avoir deux opérateurs d'appel différents. J'essaie juste de trouver une syntaxe _visuellement_ cohérente pour les opérations élément par élément.

Pour moi, ce qui tue ces options, c'est la question de savoir comment vectoriser les fonctions multi-arguments. Il n'y a pas de manière unique de le faire et tout ce qui est assez général pour prendre en charge toutes les manières possibles commence à ressembler beaucoup aux compréhensions de tableaux multidimensionnels que nous avons déjà.

Il est assez standard pour les multi-arguments map d'itérer sur tous les arguments.
Si nous faisions cela, je ferais .( syntaxe pour un appel à la carte. Cette syntaxe pourrait
pas si bien pour diverses raisons, mais je serais d'accord avec ces aspects.

Le fait que plusieurs généralisations soient possibles pour les fonctions multi-arguments ne peut pas être un argument contre la prise en charge d'au moins certains cas particuliers - tout comme la transposition matricielle est utile même si elle peut être généralisée de plusieurs manières pour les tenseurs.

Il suffit de choisir la solution la plus utile. Les choix possibles ont déjà été discutés ici : https://github.com/JuliaLang/julia/issues/8389#issuecomment -55953120 (et les commentaires suivants). Comme @JeffBezanson l'a dit, le comportement actuel de map est raisonnable. Un critère intéressant est de pouvoir remplacer @vectorize_2arg .

Ce que je veux dire, c'est que la coexistence de sin.(x) et x .+ y est gênante. Je préfère avoir .sin(x) -> map(sin, x) et x .+ y -> map(+, x, y) .

.+ utilise en fait broadcast .

Quelques autres idées, par pur désespoir :

  1. Surcharge deux-points, sin:x . Ne généralise pas bien à plusieurs arguments.
  2. sin.[x] --- cette syntaxe est disponible, actuellement sans signification.
  3. sin@x --- pas aussi disponible, mais peut-être possible

Je ne suis vraiment pas convaincu que nous en ayons besoin.

Moi non plus. Je pense que f.(x) est en quelque sorte la meilleure option ici, mais je ne l'aime pas.

Mais sans cela, comment éviter de créer toutes sortes de fonctions vectorisées, notamment des choses comme int() ? C'est ce qui m'a incité à démarrer cette discussion sur https://github.com/JuliaLang/julia/issues/8389.

Nous devrions encourager les gens à utiliser map(func, x) . Ce n'est pas beaucoup de frappe et c'est immédiatement clair pour quiconque vient d'une autre langue.

Et bien sûr, assurez-vous que c'est rapide.

Ouais, mais pour une utilisation interactive je trouve ça très pénible. Cela va être un ennui majeur pour les personnes venant de R (du moins, je ne connais pas les autres langues), et je n'aimerais pas que cela donne l'impression que Julia n'est pas adaptée à l'analyse de données.

Un autre problème est la cohérence : à moins que vous ne vouliez supprimer toutes les fonctions actuellement vectorisées, y compris log , exp , etc., et demander aux gens d'utiliser map la place (ce qui pourrait être un test intéressant de la praticité de cette décision), le langage va être incohérent, rendant difficile de savoir à l'avance si une fonction est vectorisée ou non (et sur quels arguments).

De nombreuses autres langues utilisent map depuis des années.

Comme j'ai compris le plan pour arrêter de tout vectoriser, supprimer la plupart/toutes les fonctions vectorisées a toujours fait partie de la stratégie.

Oui, bien sûr on arrêterait de tout vectoriser. L'incohérence est déjà là : il est déjà difficile de savoir quelles fonctions sont ou devraient être vectorisées, car il n'y a aucune raison vraiment convaincante pour laquelle sin , exp etc. devraient être implicitement mappés sur des tableaux.

Et dire aux auteurs de bibliothèques de mettre @vectorize sur toutes les fonctions appropriées est stupide ; vous devriez pouvoir simplement écrire une fonction, et si quelqu'un veut la calculer pour chaque élément, il utilise map .

Imaginons ce qui se passe si nous supprimons les fonctions mathématiques vectorisées couramment utilisées :

  1. Personnellement, cela ne me dérange pas d'écrire map(exp, x) au lieu de exp(x) , même si ce dernier est légèrement plus court et plus propre. Cependant, il existe une _grosse_ différence de performances. La fonction vectorisée est environ 5 fois plus rapide que la carte sur ma machine.
  2. Lorsque vous travaillez avec des expressions composées, le problème est plus intéressant. Considérons une expression composée : exp(0.5 * abs2(x - y)) , alors nous avons
# 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 

Si nous allons supprimer les fonctions mathématiques vectorisées, la seule manière acceptable à la fois en termes de lisibilité et de performances semble être la compréhension des tableaux. Pourtant, ils ne sont pas aussi pratiques que les mathématiques vectorisées.

-1 pour supprimer les versions vectorisées. En fait, les bibliothèques telles que VML et Yeppp offrent des performances beaucoup plus élevées pour les versions vectorisées et nous devons trouver comment les exploiter.

Que ce soit dans la base ou non, c'est une discussion différente et une discussion plus large, mais le besoin est réel et les performances peuvent être supérieures à ce que nous avons.

@lindahua et @ViralBShah : Certaines de vos préoccupations semblent reposer sur l'hypothèse que nous nous débarrasserions des fonctions vectorisées avant d'apporter des améliorations à map , mais je ne crois pas que quiconque ait proposé de le faire.

Je pense que l'exemple de @lindahua est assez révélateur : la syntaxe vectorisée est beaucoup plus agréable et beaucoup plus proche de la formule mathématique que les autres solutions. Je serais bien mal de perdre ça, et les gens venant d'autres langages scientifiques vont probablement considérer cela comme un point négatif de Julia.

Je suis d'accord pour supprimer toutes les fonctions vectorisées (quand map est assez rapide), et voir comment ça se passe. Je pense que l'intérêt de fournir une syntaxe de commodité sera encore plus visible à ce stade, et il sera encore temps de l'ajouter s'il s'avère que c'est le cas.

Je pense que Julia est différente de beaucoup d'autres langages car elle met l'accent sur l'utilisation interactive (les expressions plus longues sont ennuyeuses à taper dans ce cas) et les calculs mathématiques (les formules doivent être aussi proches que possible des expressions mathématiques pour rendre le code lisible). C'est en partie pourquoi Matlab, R et Numpy proposent des fonctions vectorisées (l'autre raison est bien sûr la performance, un problème qui peut disparaître dans Julia).

Mon sentiment de la discussion est que l'importance des mathématiques vectorisées est sous-estimée. En fait, l'un des principaux avantages des mathématiques vectorisées réside dans la concision de l'expression - c'est bien plus qu'un simple palliatif pour les "langages à boucle for lente".

En comparant y = exp(x) et

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

Le premier est évidemment beaucoup plus concis et lisible que le second. Le fait que Julia rende les boucles efficaces ne signifie pas qu'il faille toujours dé-vectoriser les codes, ce qui, pour moi, est assez contre-productif.

Nous devrions encourager les gens à écrire des codes de manière naturelle. D'une part, cela signifie qu'il ne faut pas essayer d'écrire des codes alambiqués et de tordre des fonctions vectorisées dans un contexte auquel elles ne correspondent pas ; d'autre part, nous devrions soutenir l'utilisation des mathématiques vectorisées chaque fois qu'elles ont le plus de sens.

En pratique, mapper des formules sur des tableaux de nombres est une opération très courante, et nous devrions nous efforcer de rendre cela pratique au lieu de le rendre encombrant. Pour cela, les codes vectorisés restent le moyen le plus naturel et le plus concis. Mis à part les performances, elles sont toujours meilleures que l'appel de la fonction map , en particulier pour les expressions composées avec plusieurs arguments.

Nous ne voulons certainement pas de versions vectorisées de tout, mais utiliser la carte à chaque fois pour vectoriser serait ennuyeux pour les raisons que Dahua vient de mentionner ci-dessus.

Si map était rapide, cela nous permettrait certainement de nous concentrer sur un ensemble plus petit et significatif de fonctions vectorisées.

Je dois dire que je suis fortement en faveur d'une notation cartographique concise. Je pense que c'est le meilleur compromis entre les différents besoins.

Je n'aime pas les fonctions vectorisées. Il cache ce qui se passe. Cette habitude de créer des versions vectorisées des fonctions conduit à un code mystérieux. Supposons que vous ayez du code dans lequel une fonction f d'un package est appelée sur un vecteur. Même si vous avez une idée de ce que fait la fonction, vous ne pouvez pas être sûr, en lisant le code, qu'elle le fasse par élément ou qu'elle fonctionne sur le vecteur dans son ensemble. Si les langages informatiques scientifiques n'avaient pas l'habitude d'avoir ces fonctions vectorisées, je ne pense pas que nous les accepterions autant maintenant.

Cela conduit également à la situation où vous êtes en quelque sorte implicitement encouragé à écrire des versions vectorisées de fonctions afin de permettre un code concis où les fonctions sont utilisées.

Le code le plus explicite sur ce qui se passe est la boucle, mais comme le dit @lindahua , il finit par être très verbeux, ce qui a ses propres inconvénients, en particulier dans un langage également destiné à un usage interactif.

Cela conduit au compromis map , qui me semble plus proche de l'idéal, mais je suis toujours d'accord avec @lindahua sur le fait que ce n'est pas assez concis.

Là où je ne suis pas d'accord avec @lindahua , c'est que les fonctions vectorisées sont le meilleur choix, pour les raisons que j'ai mentionnées plus tôt. Ce à quoi mon raisonnement mène, c'est que Julia devrait avoir une notation très concise pour map .

Je trouve la façon dont Mathematica le fait avec sa notation abrégée vraiment attrayante. La notation abrégée pour appliquer une fonction à un argument dans Mathematica est @ , donc vous feriez Apply la fonction f à un vecteur comme : f @ vector . La notation abrégée associée pour mapper une fonction est /@ , donc vous mappez f au vecteur comme suit : f /@ vector . Cela a plusieurs caractéristiques attrayantes. Les deux raccourcis sont laconiques. Le fait que les deux utilisent le symbole @ souligne qu'il existe une relation entre ce qu'ils font, mais le / dans la carte le rend toujours visuellement distinct pour indiquer clairement quand vous cartographiez et quand vous ne sont pas. Cela ne veut pas dire que Julia devrait copier aveuglément la notation de Mathematica, seulement qu'une bonne notation pour la cartographie est incroyablement précieuse

Je ne suggère pas de se débarrasser de toutes les fonctions vectorisées. Ce train a depuis longtemps quitté la gare. Je suggère plutôt de garder la liste des fonctions vectorisées aussi courte que possible et de fournir une bonne notation de carte concise pour décourager l'ajout à la liste des fonctions vectorisées.

Tout cela est, bien sûr, conditionnel à la rapidité de map et des fonctions anonymes. En ce moment, Julia est dans une position bizarre. Autrefois, dans les langages de calcul scientifique, les fonctions étaient vectorisées car les boucles étaient lentes. Ce n'est pas un problème. Au lieu de cela, dans Julia, vous avez vectorisé les fonctions car les fonctions cartographiques et anonymes sont lentes. Nous sommes donc revenus à notre point de départ, mais pour des raisons différentes.

Les fonctions de bibliothèque vectorisées ont un inconvénient : seules les fonctions explicitement fournies par la bibliothèque sont disponibles. Autrement dit, par exemple, sin(x) est rapide lorsqu'il est appliqué à un vecteur, tandis que sin(2*x) est soudainement beaucoup plus lent car il nécessite un tableau intermédiaire qui doit être parcouru deux fois (d'abord en écriture, puis en lecture).

Une solution serait une bibliothèque de fonctions mathématiques vectorizABLE. Il s'agirait d'implémentations de sin , cos , etc. qui sont disponibles pour LLVM pour l'inlining. LLVM pourrait alors vectoriser cette boucle et, espérons-le, conduire à un code très efficace. Yeppp semble avoir les bons noyaux de boucle, mais ne semble pas les exposer pour l'inlining.

Un autre problème avec la vectorisation en tant que paradigme est qu'elle ne fonctionne tout simplement pas du tout si vous utilisez des types de conteneurs autres que ceux bénis par la bibliothèque standard. Vous pouvez le voir dans DataArrays : il y a une quantité ridicule de code de métaprogrammation utilisé pour re-vectoriser les fonctions pour les nouveaux types de conteneurs que nous définissons.

Combinez cela avec le point de @eschnett et vous obtenez :

  • Les fonctions vectorisées ne fonctionnent que si vous vous limitez aux fonctions de la bibliothèque standard
  • Les fonctions vectorisées ne fonctionnent que si vous vous limitez aux types de conteneurs de la bibliothèque standard

Je tiens à préciser que mon point n'est pas que nous devrions toujours conserver les fonctions vectorisées, mais que nous avons besoin d'un moyen aussi concis que d'écrire des fonctions vectorisées. L'utilisation map ne satisfait probablement pas cela.

J'aime l'idée de @eschnett de marquer certaines fonctions comme _vectorizable_, et le compilateur peut automatiquement mapper une fonction vectorizable à un tableau sans obliger les utilisateurs à définir explicitement une version vectorisée. Le compilateur peut également fusionner une chaîne de fonctions vectorisables dans une boucle fusionnée.

Voici ce que j'ai en tête, inspiré des commentaires de @eschnett :

# 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

Le compilateur peut également re-vectoriser le calcul (au niveau inférieur) en identifiant l'opportunité d'utiliser SIMD.

Bien sûr, @vec doit être mis à la disposition de l'utilisateur final, afin que les utilisateurs puissent déclarer leurs propres fonctions comme vectorisables.

Merci, @lindahua : vos éclaircissements m'aident beaucoup.

Est-ce que @vec serait différent de déclarer une fonction @pure ?

@vec indique que la fonction peut être mappée élément par élément.

Toutes les fonctions pures ne rentrent pas dans cette catégorie, par exemple sum est une fonction pure, et je ne pense pas qu'il soit conseillé de la déclarer comme _vectorizable_.

Ne pourrait-on pas redériver la propriété vec de sum partir des balises pure et associative sur + avec des connaissances sur la façon dont reduce / foldl / foldr fonctionne-t-il avec les fonctions pure et associative ? Évidemment, tout cela est hypothétique, mais si Julia se concentrait sur les traits pour les types, je pourrais imaginer améliorer considérablement l'état de l'art de la vectorisation en se concentrant également sur les traits pour les fonctions.

J'ai l'impression que l'ajout d'une nouvelle syntaxe est le contraire de ce que nous voulons (après avoir simplement nettoyé la syntaxe spéciale pour Any[] et Dict). Tout le _point_ de la suppression de ces fonctions vectorisées est de réduire les cas particuliers (et je ne pense pas que la syntaxe devrait être différente de la sémantique des fonctions). Mais je suis d'accord qu'une carte concise serait utile.

Alors pourquoi ne pas ajouter un opérateur infixe laconique map ? Ici, je vais choisir $ arbitrairement. Cela ferait passer l'exemple de @lindahua de

exp(0.5 * abs2(x - y))

pour

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

Maintenant, si nous n'avions qu'un support de type Haskell pour les opérateurs d'infixes définis par l'utilisateur, ce ne serait qu'un changement d'une ligne ($) = map . :)

IMO, les autres propositions de syntaxe sont visuellement trop proches de la syntaxe existante et nécessiteraient un effort mental supplémentaire pour analyser lors de la recherche dans le code :

  • foo.(x) - visuellement similaire à l'accès aux membres de type standard
  • foo[x] -- est-ce que j'accède au x-ème membre du tableau foo ou appelle map ici ?
  • .foo(x) - a des problèmes comme l'a souligné @kmsquire

Et j'ai l'impression que la solution @vec est trop proche de la @vectorize que nous essayons d'éviter en premier lieu. Certes, certaines annotations comme # 8297 seraient agréables à avoir et pourraient aider un futur compilateur plus intelligent à reconnaître ces opportunités de fusion de flux et à optimiser en conséquence. Mais je n'aime pas l'idée de forcer.

Infix map plus des fonctions anonymes rapides pourraient également aider à la création de temporaires si cela vous permettait de faire quelque chose comme :

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

Je me demande si l'idée du nouveau Trait.jl sympa peut être empruntée dans le contexte de la désignation d'une fonction vectorisable. Bien sûr, dans ce cas, nous examinons des _instances_ individuelles de type Function vectorisables ou non, au lieu d'un Type julia ayant un trait spécifique.

Maintenant, si nous n'avions qu'un support de type Haskell pour les opérateurs d'infixes définis par l'utilisateur

6582 #6929 pas assez ?

Il y a un point dans cette discussion sur la vectorisation d'expressions entières avec le moins de tableaux temporaires possible. Les utilisateurs qui veulent une syntaxe vectorisée ne voudront pas seulement un exp(x) vectorisé ; ils voudraient écrire des expressions comme

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

et faites-le vectoriser comme par magie

Il ne serait pas nécessaire de marquer les fonctions comme "vectorisables". Il s'agit plutôt d'une propriété de la façon dont les fonctions sont implémentées et mises à la disposition de Julia. S'ils sont implémentés, par exemple en C, ils doivent être compilés en bytecode LLVM (et non en fichiers objets) afin que l'optimiseur LLVM puisse toujours y accéder. Les mettre en œuvre dans Julia fonctionnerait également.

La vectorisabilité signifie que l'on implémente la fonction d'une manière qui est assez bien décrite par le projet Yeppp : pas de branches, pas de tables, de division ou de racine carrée si elles sont disponibles sous forme d'instructions vectorielles dans le matériel, et sinon de nombreuses opérations fusionnées de multiplication-addition et vectorielles opérations de fusion.

Malheureusement, de telles implémentations dépendront du matériel, c'est-à-dire que l'on devra choisir différents algorithmes ou différentes implémentations en fonction des instructions matérielles efficaces. Je l'ai déjà fait dans le passé (https://bitbucket.org/eschnett/vecmathlib/wiki/Home) en C++, et avec un public cible légèrement différent (opérations basées sur des gabarits qui sont vectorisées manuellement au lieu d'une vectorisation automatique compilateur).

Ici, dans Julia, les choses seraient plus faciles puisque (a) nous savons que le compilateur sera LLVM, et (b) nous pouvons l'implémenter dans Julia au lieu de C++ (macros vs modèles).

Il y a une autre chose à considérer : si l'on abandonne certaines parties de la norme IEEE, on peut alors améliorer considérablement la vitesse. De nombreux utilisateurs savent que, par exemple, les nombres dénormalisés ne sont pas importants, ou que l'entrée sera toujours inférieure à sqrt(max(Double)) , etc. La question est de savoir s'il faut proposer des chemins rapides pour ces cas. Je sais que cela m'intéressera beaucoup, mais d'autres préféreront peut-être des résultats exactement reproductibles.

Laissez-moi concocter un prototype vectorisable exp dans Julia. Nous pouvons alors voir comment LLVM réussit à vectoriser une boucle, et quelles vitesses nous obtenons.

Est-ce trop effrayant d'utiliser des parenthèses pleine largeur autour de l'argument de la fonction ~

Désolé, je n'avais pas réalisé que je répétais exactement la même chose que @johnmyleswhite parlait ci-dessus concernant la fonction avec le trait. Continuer.

@eschnett Je ne pense pas qu'il soit raisonnable de lier l'API (que les fonctions soient vectorisables ou non) aux détails d'implémentation (comment la fonction est compilée). Cela semble assez complexe à comprendre et à maintenir stable dans le temps et à travers les architectures, et cela ne fonctionnerait pas lors de l'appel de fonctions dans des bibliothèques externes, par exemple log ne serait pas détecté comme vectorisable car appelle une fonction d'openlibm.

L'idée d' OTOH @johnmyleswhite d'utiliser des traits pour communiquer quelles sont les propriétés mathématiques d'une fonction pourrait être une excellente solution. (La proposition de @lindahua est une fonctionnalité que j'avais suggérée quelque part il y a quelque temps, mais la solution consistant à utiliser des traits peut être encore meilleure.)

Maintenant, si nous n'avions qu'un support de type Haskell pour les opérateurs d'infixes définis par l'utilisateur

6582 #6929 pas assez ?

J'aurais dû dire : ... opérateurs d'infixe _non-unicode_ définis par l'utilisateur, car je ne pense pas que nous voulions obliger les utilisateurs à taper des caractères unicode pour accéder à une telle fonctionnalité de base. Bien que je vois que $ est en fait l'un de ceux qui ont été ajoutés, alors merci pour cela ! Wow, donc cela fonctionne réellement dans Julia _aujourd'hui_ (même si ce n'est pas "rapide"... encore):

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

Je ne sais pas si c'est le meilleur choix pour map mais utiliser $ pour xor semble vraiment être un gaspillage. Bitwise xor n'est pas souvent utilisé. map est bien plus important.

Le point de @jiahao ci-dessus est très bon : les fonctions vectorisées individuelles comme exp sont en fait une sorte de hack pour obtenir des _expressions_ vectorisées comme exp(-x^2) . Une syntaxe qui fait quelque chose comme @devec serait vraiment utile : vous obtiendrez des performances dévectorisées plus la généralité de ne pas avoir besoin d'identifier individuellement les fonctions comme vectorisées.

La possibilité d'utiliser des traits de fonction pour cela serait cool, mais je trouve toujours cela moins satisfaisant. Ce qui se passe vraiment en général, c'est qu'une personne écrit une fonction et qu'une autre personne l'itère.

Je suis d'accord que ce n'est pas une propriété de la fonction, c'est une propriété de l'utilisation de la fonction. La discussion sur l'application des traits ressemble à un cas d'aboiement du mauvais arbre.

Brainstorming : que diriez-vous de marquer les arguments que vous souhaitez mapper afin qu'ils prennent en charge le mappage multi-arg :

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"""

Je ne sais pas si .b ou b. est préférable pour montrer que vous voulez que cela soit mappé. J'aime renvoyer un résultat 3x2 multidimensionnel dans ce cas car il représente la forme du ping map .

Glen

Ici https://github.com/eschnett/Vecmathlib.jl est un repo avec un échantillon
implémentation de exp , écrit d'une manière qui peut être optimisée par LLVM.
Cette implémentation est environ deux fois plus rapide que la norme exp
implémentation sur mon système. Il n'atteint (probablement) pas encore la vitesse de Yeppp,
probablement parce que LLVM ne déroule pas la boucle SIMD respective comme
agressivement comme Yeppp. (J'ai comparé les instructions désassemblées.)

Écrire une fonction vectorisable exp n'est pas facile. Son utilisation ressemble à ceci :

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

où la boucle j et les arguments de la fonction ne sont là que pour
à des fins d'étalonnage.

Existe-t-il une macro @unroll pour Julia ?

-erik

Le dimanche 2 novembre 2014 à 20h26, Tim Holy [email protected] a écrit :

Je suis d'accord que ce n'est pas une propriété de la fonction, c'est une propriété de
l'utilisation de la fonction. La discussion sur l'application des traits semble être une
cas d'aboyer le mauvais arbre.

Répondez directement à cet e-mail ou consultez-le sur GitHub
https://github.com/JuliaLang/julia/issues/8450#issuecomment -61433026.

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

les fonctions vectorisées individuelles comme exp sont en fait une sorte de hack pour obtenir des _expressions_ vectorisées comme exp(-x^2)

La syntaxe de base pour extraire des expressions entières du domaine scalaire serait très intéressante. La vectorisation n'est qu'un exemple (où le domaine cible est les vecteurs) ; un autre cas d'utilisation intéressant serait de se hisser dans le domaine matriciel (#5840) où la sémantique est assez différente. Dans le domaine matriciel, il serait également utile d'explorer comment la répartition sur différentes expressions pourrait fonctionner, car dans le cas général, vous voudriez Schur-Parlett et d'autres algorithmes plus spécialisés si vous vouliez quelque chose de plus simple comme sqrtm . (Et avec une syntaxe intelligente, vous pourriez vous débarrasser entièrement des fonctions *m - expm , logm , sqrtm , ...)

Existe-t-il une macro @unroll pour Julia ?

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

(Voir http://docs.julialang.org/en/latest/devdocs/cartesian/ si vous avez des questions.)

@jiahao Généraliser cela aux fonctions matricielles semble être un défi intéressant, mais ma connaissance à ce sujet est proche de zéro. Avez-vous des idées sur la façon dont cela fonctionnerait? Comment cela s'articulerait-il avec la vectorisation ? Comment la syntaxe permettrait-elle de faire la différence entre l'application exp élément par élément sur un vecteur/matrice et le calcul de sa matrice exponentielle ?

@timholy : Merci ! Je n'ai pas pensé à utiliser le cartésien pour le déroulement.

Malheureusement, le code produit par @nexprs (ou par déroulement manuel) n'est plus vectorisé. (Il s'agit de LLVM 3.3, peut-être que LLVM 3.5 serait mieux.)

Re : déroulement, voir aussi le post de @toivoh sur julia-users . Cela peut également valoir la peine d'essayer # 6271.

@nalimilan Je n'y ai pas encore réfléchi, mais scalaire-> levage de matrice serait assez simple à implémenter avec une seule fonction matrixfunc (par exemple). Une syntaxe hypothétique (inventant complètement quelque chose) pourrait être

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

qui serait alors

  1. identifier les domaines source et cible de l'ascenseur à partir de X étant de type Matrix{Float64} et ayant des éléments (paramètre de type) Float64 (définissant ainsi implicitement un ascenseur Float64 => Matrix{Float64} ) , ensuite
  2. appelez matrixfunc(x->exp(c*x^2)*sin(x), X) pour calculer l'équivalent de expm(c*X^2)*sinm(X) , mais en évitant la multiplication de la matrice.

Dans un autre code, X pourrait être un Vector{Int} et la levée implicite serait de Int à Vector{Int} , puis lift(x->exp(c*x^2)*sin(x), X) pourrait alors appelez map(x->exp(c*x^2)*sin(x), X) .

On pourrait également imaginer d'autres méthodes qui spécifient explicitement les domaines source et cible, par exemple lift(Number=>Matrix, x->exp(c*x^2)*sin(x), X) .

@nalimilan La vectorisation n'est pas vraiment une propriété de l'API. Avec la technologie actuelle des compilateurs, une fonction ne peut être vectorisée que si elle est en ligne. Les choses dépendent principalement de l'implémentation de la fonction - si elle est écrite de la "bonne manière", alors le compilateur peut la vectoriser (après l'avoir intégrée dans une boucle environnante).

@eschnett : Utilisez-vous le même sens de vectorisation que les autres ? On dirait que vous parlez de SIMD, etc., ce qui n'est pas ce que je comprends que @nalimilan signifie.

Droit. Il existe ici deux notions différentes de vectorisation. Un traite
avec obtenir SIMD pour les boucles internes serrées (vectorisation du processeur). Le principal
le problème discuté ici est la syntaxe/sémantique d'une manière ou d'une autre "automatiquement"
être capable d'appeler une fonction à un (ou plusieurs) argument(s) sur une collection.

Le mardi 4 novembre 2014 à 19h04, John Myles White [email protected]
a écrit:

@eschnett https://github.com/eschnett : Utilisez-vous le même sens
de vectorisation comme les autres ? On dirait que vous parlez de SIMD, etc., ce qui
n'est pas ce que je comprends @nalimilan https://github.com/nalimilan à
signifier.


Répondez directement à cet e-mail ou consultez-le sur GitHub
https://github.com/JuliaLang/julia/issues/8450#issuecomment -61738237.

En symétrie avec les autres opérateurs . , f.(x) ne devrait-il pas appliquer une collection de fonctions à une collection de valeurs ? (Par exemple, pour passer d'un système de coordonnées de ème unité à des coordonnées physiques.)

Lors de la discussion de la syntaxe, l'idée est apparue que l'utilisation de boucles explicites pour exprimer l'équivalent de map(log, x) était trop lente. Donc, si on peut faire ça assez vite, alors appeler map (ou utiliser une syntaxe spéciale) ou écrire des boucles sont équivalents au niveau sémantique, et on n'a pas besoin d'introduire une désambiguïsation syntaxique. Actuellement, appeler une fonction vector-log est beaucoup plus rapide que d'écrire une boucle sur un tableau, incitant les gens à demander un moyen d'exprimer cette distinction dans leur code.

Il y a deux niveaux de problèmes ici : (1) syntaxe et sémantique, (2) implémentation.

Le problème de syntaxe et de sémantique concerne la façon dont l'utilisateur peut exprimer l'intention de mapper certains calculs d'une manière élément par élément/diffusion à des tableaux donnés. Actuellement, Julia prend en charge deux méthodes : utiliser des fonctions vectorisées et permettre aux utilisateurs d'écrire la boucle explicitement (parfois avec l'aide de macros). Ni l'un ni l'autre n'est idéal. Alors que les fonctions vectorisées permettent d'écrire des expressions très concises comme exp(0.5 * (x - y).^2) , elles ont deux problèmes : (1) il est difficile de tracer une ligne entre les fonctions qui doivent fournir une version vectorisée et celles qui ne le sont pas, ce qui entraîne souvent dans des débats interminables côté développeur et confusion côté utilisateur (il faut souvent chercher la doc pour savoir si certaines fonctions sont vectorisées). (2) Il est difficile de fusionner les boucles à travers les frontières des fonctions. À ce stade et probablement dans plusieurs mois/années à venir, le compilateur ne sera probablement pas en mesure d'effectuer des tâches aussi complexes que d'examiner plusieurs fonctions ensemble, d'identifier le flux de données commun et de produire un chemin de code optimisé à travers les limites des fonctions.

L'utilisation de la fonction map résout le problème (1) ici. Ceci, cependant, ne fournit toujours aucune aide pour résoudre le problème (2) - l'utilisation de fonctions, qu'il s'agisse d'une fonction vectorisée spécifique ou d'un map générique, crée toujours une limite de fonction qui empêche la fusion des boucles, ce qui est critique dans le calcul haute performance. L'utilisation de la fonction map conduit également à la verbosité, par exemple l'expression ci-dessus devient maintenant une déclaration plus longue sous la forme map(exp, 0.5 * map(abs2, x - y)) . Vous pouvez raisonnablement imaginer que ce problème serait aggravé par des expressions plus complexes.

Parmi toutes les propositions décrites dans ce fil, je pense personnellement que l'utilisation de notations spéciales pour indiquer la cartographie est la voie la plus prometteuse pour l'avenir. Tout d'abord, il maintient la concision de l'expression. Prenez la notation $ par exemple, les expressions ci-dessus peuvent maintenant être écrites sous la forme exp $(0.5 * abs2$(x - y)) . C'est un peu plus long que l'expression vectorisée d'origine, mais ce n'est pas si mal -- tout ce qu'il faut, c'est insérer un $ à chaque appel d'un mappage. D'autre part, cette notation sert également d'indicateur non ambigu d'un mappage en cours d'exécution, que le compilateur peut utiliser pour briser la frontière de la fonction et produire une boucle fusionnée. Dans ce cours, le compilateur n'a pas à se pencher sur l'implémentation interne de la fonction -- tout ce qu'il doit savoir, c'est que la fonction va être mappée à chaque élément des tableaux donnés.

Compte tenu de toutes les fonctionnalités des processeurs modernes, en particulier la capacité de SIMD, la fusion de plusieurs boucles en une seule n'est qu'une étape vers un calcul haute performance. Cette étape elle-même ne déclenche pas l'utilisation des instructions SIMD. La bonne nouvelle est que nous avons maintenant la macro @simd . Le compilateur peut insérer cette macro au début de la boucle produite lorsqu'il pense que cela est sûr et bénéfique.

Pour résumer, je pense que la notation $ (ou des propositions similaires) peut largement résoudre le problème de syntaxe et de sémantique, tout en fournissant les informations nécessaires au compilateur pour fusionner les boucles et exploiter SIMD, et ainsi émettre des codes très performants.

Le résumé de @lindahua est bon à mon humble avis.

Mais je pense qu'il serait intéressant d'aller encore plus loin. Julia mérite un système ambitieux qui rendra de nombreux modèles communs aussi efficaces que des boucles déroulées.

  • Le modèle de fusion des appels de fonction imbriqués dans une seule boucle doit également être appliqué aux opérateurs, de sorte que A .* B .+ C ne conduise pas à la création de deux temporaires, mais d'un seul pour le résultat.
  • La combinaison des fonctions élément par élément et des réductions doit également être gérée, de sorte que la réduction soit appliquée à la volée après le calcul de la valeur de chaque élément. Typiquement, cela permettra de se débarrasser de sumabs2(A) , en le remplaçant par une notation standard comme sum(abs$(A)$^2) (ou sum(abs.(A).^2) ).
  • Enfin, les modèles d'itération non standard doivent être pris en charge pour les tableaux non standard, de sorte que pour les matrices creuses, A .* B n'a besoin de gérer que des entrées non nulles et renvoie une matrice creuse. Cela serait également utile si vous souhaitez appliquer une fonction élément par élément à un Set , un Dict ou même un Range .

Les deux derniers points pourraient fonctionner en faisant en sorte que les fonctions élément par élément renvoient un type AbstractArray spécial, disons LazyArray , qui calculerait ses éléments à la volée (similaire au Transpose tapez de https://github.com/JuliaLang/julia/issues/4774#issuecomment-59422003). Mais au lieu d'accéder naïvement à ses éléments en les parcourant à l'aide d'index linéaires de 1 à length(A) , le protocole d'itérateur pourrait être utilisé. L'itérateur pour un type donné choisirait automatiquement si l'itération par ligne ou par colonne est la plus efficace, en fonction de la disposition de stockage du type. Et pour les matrices creuses, cela permettrait de sauter les entrées nulles (l'original et le résultat devraient avoir une structure commune, cf. https://github.com/JuliaLang/julia/issues/7010, https://github. com/JuliaLang/julia/issues/7157).

Lorsqu'aucune réduction n'est appliquée, un objet du même type et de la même forme que l'original serait simplement rempli en itérant sur le LazyArray (équivalent de collect , mais en respectant le type du tableau d'origine ). La seule chose nécessaire pour cela est que l'itérateur renvoie un objet qui peut être utilisé pour appeler getindex sur les LazyArray et setindex! sur le résultat (par exemple linéaire ou cartésien coordonnées).

Lorsqu'une réduction est appliquée, elle utilise la méthode d'itération appropriée sur son argument pour itérer sur les dimensions requises du LazyArray et remplit un tableau avec le résultat (équivalent de reduce mais en utilisant un itérateur personnalisé pour s'adapter au type de tableau). Une fonction (celle utilisée dans le dernier paragraphe) renverrait un itérateur parcourant tous les éléments de la manière la plus efficace ; d'autres permettraient de le faire sur des dimensions spécifiques.

L'ensemble de ce système prendrait également en charge les opérations sur place assez simplement.

Je pensais un peu à la syntaxe et j'ai pensé à .= pour appliquer des opérations élémentaires au tableau.
Donc l'exemple de @nalimilan sum(abs.(A).^2)) devrait malheureusement être écrit en deux étapes :

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

Cela aurait l'avantage d'être facile à lire et signifierait que les fonctions élément par élément n'ont besoin d'être écrites que pour une seule (ou plusieurs) entrée et optimisées pour ce cas au lieu d'écrire des méthodes spécifiques au tableau.

Bien sûr, rien d'autre que la performance et la familiarité n'empêche quiconque d'écrire simplement map((x) -> abs(x)^2, A) en ce moment, comme cela a été indiqué.

Alternativement, entourer une expression à mapper avec .() pourrait fonctionner.
Je ne sais pas à quel point il serait difficile de le faire, mais .sin(x) et .(x + sin(x)) mapperaient alors l'expression soit à l'intérieur de la parenthèse, soit dans la fonction qui suit le . .
Cela permettrait alors des réductions comme l'exemple de @nalimilansum(.(abs(A)^2)) pourrait alors être écrit sur une seule ligne.

Ces deux propositions utilisent un préfixe . qui, tout en utilisant la diffusion en interne, m'a fait penser à des opérations élémentaires sur des tableaux. Cela pourrait facilement être remplacé par $ ou un autre symbole.
Il s'agit simplement d'une alternative à la mise en place d'un opérateur de carte autour de chaque fonction à mapper et à la place de regrouper l'expression entière et de spécifier celle à mapper à la place.

J'ai expérimenté l'idée LazyArray que j'ai exposée dans mon dernier commentaire : https://gist.github.com/nalimilan/e737bc8b3b10288abdad

Cette preuve de concept n'a pas de sucre syntaxique, mais (a ./ 2).^2 serait traduit en ce qui est écrit dans l'essentiel comme LazyArray(LazyArray(a, /, (2,)), ^, (2,)) . Le système fonctionne assez bien, mais il a besoin de plus d'optimisation pour être compétitif à distance avec les boucles en termes de performances. Le problème (attendu) semble être que l'appel de fonction à la ligne 12 n'est pas optimisé (presque toutes les allocations s'y produisent), même dans une version où les arguments supplémentaires ne sont pas autorisés. Je pense que je dois paramétrer le LazyArray sur la fonction qu'il appelle, mais je n'ai pas compris comment je pourrais le faire, sans parler de la gestion des arguments aussi. Des idées?

Des suggestions sur la façon d'améliorer les performances de LazyArray ?

@nalimilan J'ai expérimenté une approche similaire il y a un an, en utilisant des types de foncteurs dans NumericFuns pour paramétrer les types d'expression paresseux. J'ai essayé une variété de trucs, mais je n'ai pas réussi à combler l'écart de performance.

L'optimisation du compilateur a été progressivement améliorée au cours de la dernière année. Mais j'ai toujours l'impression qu'il n'est toujours pas en mesure d'optimiser les frais généraux inutiles. Ce genre de choses oblige les compilateurs à utiliser des fonctions en ligne agressives. Vous pouvez essayer d'utiliser @inline et voir si cela améliore les choses.

@lindahua @inline ne fait aucune différence sur les timings, ce qui est logique pour moi puisque getindex(::LazyArray, ...) est spécialisé pour une signature LazyArray donnée, qui ne précise pas quelle fonction doit être appelé. J'aurais besoin de quelque chose comme LazyArray{T1, N, T2, F} , avec F la fonction qui devrait être appelée, de sorte que lors de la compilation getindex l'appel soit connu. Y a-t-il un moyen de faire ça ?

L'inlining serait encore une autre grande amélioration, mais pour le moment, les timings sont bien pires que même un appel non aligné.

Vous pouvez envisager d'utiliser NumericFuns et F peut être un type de foncteur.

Dahua

J'ai eu besoin de fonctions où je connais le type de retour pour distribué
informatique, où je crée des références au résultat avant le résultat (et
donc son type) est connu. J'ai implémenté une chose très similaire moi-même, et
devrait probablement passer à l'utilisation de ce que vous appelez "Functors". (je n'aime pas le
nom "foncteur", car ils sont généralement autre chose <
http://en.wikipedia.org/wiki/Functor>, mais je suppose que C++ a brouillé les eaux
ici.)

Je pense qu'il serait logique de séparer votre partie Functor de la
fonctions mathématiques.

-erik

Le jeudi 20 novembre 2014 à 10h35, Dahua Lin [email protected]
a écrit:

Vous pouvez envisager d'utiliser NumericFuns et F peut être un type de foncteur.

Répondez directement à cet e-mail ou consultez-le sur GitHub
https://github.com/JuliaLang/julia/issues/8450#issuecomment -63826019.

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

@lindahua j'ai essayé d'utiliser des foncteurs, et en effet les performances sont beaucoup plus raisonnables:
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) 

Je ne sais pas ce qui peut être fait de plus pour améliorer les choses, car le code généré ne semble pas encore optimal. J'ai besoin de plus d'yeux experts pour dire ce qui ne va pas.

En fait, le test ci-dessus utilisait Pow , ce qui donne apparemment une grande différence de vitesse selon que vous écriviez une boucle explicite ou utilisiez un LazyArray . Je suppose que cela a à voir avec la fusion d'instructions qui ne seraient exécutées que dans ce dernier cas. Le même phénomène est visible avec par exemple l'addition. Mais avec d'autres fonctions, la différence est beaucoup plus petite, que ce soit avec une matrice 100x100 ou 1000x1000, probablement parce qu'elles sont externes et donc l'inlining ne gagne pas beaucoup :

# 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)

J'aimerais donc savoir pourquoi les optimisations ne se produisent pas avec LazyArray . L'assembly généré est assez long pour des opérations simples. Par exemple, pour 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

Contrairement à l'é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

La partie jusqu'à jae L64 est une vérification des limites du tableau. L'utilisation @inbounds peut aider (le cas échéant).

La partie ci-dessous, où deux lignes consécutives commencent par mov RAX, ... , est une double indirection, c'est-à-dire l'accès d'un pointeur vers un pointeur (ou un tableau de tableaux, ou un pointeur vers un tableau, etc.). Cela peut être lié à la représentation interne de LazyArray - peut-être que l'utilisation d'immuables (ou la représentation différente d'immuables par Julia) peut aider ici.

De toute façon, le code est encore assez rapide. Pour le rendre plus rapide, il devrait être intégré à l'appelant, exposant ainsi d'autres opportunités d'optimisation. Que se passe-t-il si vous appelez cette expression par exemple à partir d'une boucle ?

Aussi: Que se passe-t-il si vous désassemblez ceci non pas du REPL mais de l'intérieur d'une fonction?

Je ne peux pas non plus m'empêcher de remarquer que la première version effectue une véritable
division tandis que le second a transformé x/2 en une multiplication.

Merci pour les commentaires.

@eschnett LazyArray est déjà un immuable, et j'utilise @inbounds dans les boucles. Après avoir exécuté l'essentiel sur https://gist.github.com/nalimilan/d345e1c080984ed4c89a , vous pouvez vérifier ce que cela donne en boucle avec ceci :

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); 

Alors peut-être que tout ce dont j'ai besoin est de pouvoir forcer l'inlining? Dans mes tentatives, ajouter @inline à getindex ne change pas les horaires.

@toivoh Qu'est-ce qui pourrait expliquer que dans ce dernier cas le découpage ne soit pas simplifié ?

J'ai continué à expérimenter avec la version à deux arguments (appelée LazyArray2 ). Il s'avère que pour une opération simple comme x .+ y , il est en fait plus rapide d'utiliser un LazyArray2 que le .+ actuel, et c'est aussi assez proche des boucles explicites (ce sont pour 1000 appels , voir 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)

Il semble donc que cette stratégie soit viable pour remplacer toutes les opérations par élément, y compris les opérateurs .+ , .* , etc.

Il semble également très compétitif pour réaliser des opérations courantes comme le calcul de la somme des différences au carré le long d'une dimension d'une matrice, c'est-à-dire sum((x .- y).^2, 1) (voir à nouveau l'essentiel):

# 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
Votre approche avec LazyArrays semble être similaire à la façon dont la fusion à la vapeur fonctionne Haskell [1, 2]. Peut-être pouvons-nous appliquer les idées de ce domaine ?

[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 Merci. C'est effectivement similaire, mais plus complexe car Julia utilise un modèle impératif. Au contraire, dans Haskell, le compilateur doit gérer une grande variété de cas, et même gérer les problèmes SIMD (qui sont traités par LLVM ultérieurement dans Julia). Mais honnêtement, je ne suis pas capable de tout analyser dans ces papiers.

@nalimilan Je connais le sentiment. J'ai trouvé le deuxième article particulièrement intéressant car il traite de la fusion de flux généralisée, qui permet apparemment un bon modèle de calcul sur les vecteurs.

Je pense que le point principal que nous devrions en tirer est que des constructions comme map et reduce en combinaison avec la paresse peuvent être suffisamment rapides (ou même plus rapides que les boucles explicites).

Autant que je sache, les accolades sont toujours disponibles dans la syntaxe d'appel. Et si cela devenait func{x} ? Peut-être un peu trop gaspillé ?

En ce qui concerne la vectorisation rapide (au sens de SIMD), existe-t-il un moyen d'imiter la façon dont Eigen le fait ?

Voici une proposition pour remplacer toutes les opérations actuelles par élément par une généralisation de ce que j'ai appelé LazyArray et LazyArray2 ci-dessus. Cela repose bien sûr sur l'hypothèse que nous pouvons rendre cela rapide pour toutes les fonctions sans compter sur les foncteurs de NumericFuns.jl.

1) Ajoutez une nouvelle syntaxe f.(x) ou f$(x) ou quoi que ce soit qui créerait un LazyArray appelant f() sur chaque élément de x .

2) Généralisez cette syntaxe en suivant le fonctionnement actuel broadcast , de sorte que, par exemple, f.(x, y, ...) ou f$(x, y, ...) crée un LazyArray , mais en développant les dimensions singleton de x , y , ... afin de leur donner une taille commune. Bien sûr, cela serait fait à la volée par des calculs sur les indices afin que les tableaux étendus ne soient pas réellement alloués.

3) Faites .+ , .- , .* , ./ , .^ , etc. utilisez LazyArray au lieu de broadcast .

4) Introduire un nouvel opérateur d'affectation .= ou $= qui transformerait (en appelant collect ) un LazyArray en un tableau réel (d'un type dépendant de sa entrées via des règles de promotion, et d'un type d'élément dépendant du type d'élément des entrées et de la fonction appelée).

5) Peut-être même remplacer broadcast par un appel à LazyArray et un ion immédiat collect des résultats dans un tableau réel.

Le point 4 est essentiel : les opérations élément par élément ne renverraient jamais de vrais tableaux, toujours des LazyArray s, de sorte que lors de la combinaison de plusieurs opérations, aucune copie n'est faite et les boucles peuvent être fusionnées pour plus d'efficacité. Cela permet d'appeler des réductions comme sum sur le résultat sans allouer les temporaires. Ainsi, les expressions de ce type seraient idiomatiques et efficaces, à la fois pour les tableaux denses et les matrices creuses :

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

Je pense que le retour de ce type d'objet léger s'intègre parfaitement dans la nouvelle image des vues de tableau et Transpose / CTranspose . Cela signifie que dans Julia, vous êtes capable d'effectuer des opérations complexes très efficacement avec une syntaxe dense et lisible, bien que dans certains cas, vous deviez appeler explicitement copy lorsque vous avez besoin qu'un "pseudo-tableau" soit rendu indépendant de le tableau réel sur lequel il est basé.

Cela ressemble vraiment à une caractéristique importante. Le comportement actuel des opérateurs élément par élément est un piège pour les nouveaux utilisateurs, car la syntaxe est agréable et courte mais les performances sont généralement terriblement mauvaises, apparemment pires que dans Matlab. Au cours de la semaine dernière, plusieurs discussions sur julia-users ont eu des problèmes de performances qui disparaîtraient avec une telle conception :
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

Aux fins de ce numéro, je séparerais la syntaxe de la paresse. Votre proposition est cependant intéressante.

Il semble arriver un point où il y a juste _tant de points_. L'exemple du milieu en particulier serait mieux écrit comme

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

qui ne nécessite que des fonctions de base et un efficace map ( .|> ).

Voici une comparaison intéressante :

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)]

Si vous escomptez la partie for ... , la compréhension n'est plus longue que d'un caractère. Je préférerais presque avoir une syntaxe de compréhension abrégée que tous ces points.

Une compréhension 1d ne préserve pas la forme, mais maintenant que nous avons for i in eachindex(x) cela pourrait aussi changer.

Un problème avec les compréhensions est qu'elles ne prennent pas en charge les DataArrays.

Je pense qu'il pourrait être intéressant d'examiner tout un tas de choses qui se sont produites sur .Net et qui ressemblent beaucoup à l'idée de LazyArray. Essentiellement, cela me semble assez proche d'une approche de style LINQ, où vous avez une syntaxe qui ressemble à ce que nous avons en ce moment, mais vraiment cette syntaxe construit un arbre d'expression, et cet arbre d'expression est ensuite évalué de manière efficace . Est-ce en quelque sorte proche?

Sur .Net, ils sont allés loin avec cette idée : vous pouvez exécuter ces arborescences d'expressions en parallèle sur plusieurs processeurs (en ajoutant .AsParallel()), ou vous pouvez les exécuter sur un grand cluster avec le DryadLINQ, ou même sur un http:/ /research.microsoft.com/en-us/projects/accelerator/ (ce dernier n'est peut-être pas entièrement intégré à LINQ, mais son esprit est proche si je me souviens bien), ou bien sûr, il pourrait être traduit en SQL s'il le les données étaient sous cette forme et vous n'utilisiez que des opérateurs pouvant être traduits en instructions SQL.

Mon sentiment est que Blaze va également dans cette direction, c'est-à-dire un moyen de construire facilement des objets qui décrivent des calculs, et vous pouvez alors avoir différents moteurs d'exécution pour cela.

Je ne suis pas sûr que ce soit très clair, mais il me semble que toute cette question devrait être examinée dans le contexte à la fois de la manière dont on peut générer un code SIMD efficace de bas niveau et de la manière dont cela pourrait être utilisé pour le calcul GPU, le clustering, parallèle calcul etc...

Oui, vous avez raison de dire que l'exemple le plus long comporte trop de points. Mais les deux plus courts sont plus typiques, et dans ce cas, avoir une syntaxe courte est important. J'aimerais séparer la syntaxe de la paresse, mais comme vos commentaires le montrent, cela semble très difficile, nous mélangeons toujours les deux !

On pourrait imaginer adapter la syntaxe de compréhension, quelque chose comme y = [sqrt(x + 2) over x] . Mais comme @johnmyleswhite l'a noté, ils devraient alors prendre en charge DataArrays , mais aussi les matrices creuses et tout nouveau type de tableau. Il s'agit donc à nouveau d'un mélange de syntaxe et de fonctionnalités.

Plus fondamentalement, je pense que deux fonctionnalités que ma proposition offre par rapport aux alternatives sont :
1) Prise en charge de l'affectation sur place sans allocation à l'aide y[:] = sqrt.(x .+ 2) .
2) Prise en charge des réductions sans allocation telles que sum((x .- y).^2, 1) .

Cela pourrait-il être fourni avec d'autres solutions (sans tenir compte des problèmes de syntaxe) ?

@davidanthoff Merci, regardez-le maintenant (je pense que LazyArray pourrait également être conçu pour prendre en charge le calcul parallèle).

Peut-être que cela pourrait être combiné avec des générateurs - ils sont aussi une sorte de tableau paresseux. J'aime un peu la syntaxe de compréhension [f(x) over x] , bien que cela puisse être conceptuellement difficile pour les nouveaux arrivants (car le même nom est effectivement utilisé à la fois pour les éléments et le tableau lui-même). Si les compréhensions sans crochets créaient un générateur (comme je l'ai fait il y a longtemps ), il serait naturel d'utiliser ces nouvelles compréhensions de style over-x sans crochets pour renvoyer un LazyArray au lieu de le collecter immédiatement.

@mbauman Oui, les générateurs et les tableaux paresseux partagent de nombreuses propriétés. L'idée d'utiliser des crochets pour collecter un générateur/tableau paresseux, et de ne pas les ajouter pour garder l'objet paresseux semble cool. Donc, concernant mes exemples ci-dessus, on pourrait écrire à la fois 1) y[:] = sqrt(x + 2) over x et sum((x - y)^2 over (x, y), 1) (bien que je trouve cela naturel même pour les nouveaux arrivants, laissons le problème de over pour la session bikeshedding et concentrez-vous d'abord sur les fondamentaux).

J'aime l'idée f(x) over x . Nous pourrions même utiliser f(x) for x pour éviter un nouveau mot-clé. En fait, [f(x) for x=x] fonctionne déjà. Nous aurions alors besoin de faire des compréhensions équivalentes à map , afin qu'elles puissent fonctionner pour les non-Arrays. Array serait simplement la valeur par défaut.

Je dois admettre que j'aime aussi l'idée over . Une différence entre over en tant que carte et for en compréhension de liste est ce qui se passe en cas d'itérateurs multiples : [f(x, y) for x=x, y=y] donne une matrice. Pour le cas de la carte, vous voulez généralement toujours un vecteur, c'est-à-dire [f(x, y) over x, y] serait équivalent à [f(x, y) for (x,y) = zip(x, y)]] . Pour cette raison, je pense toujours que l'introduction d'un mot-clé supplémentaire over en vaut la peine, car, comme ce problème l'a soulevé, map sur plusieurs vecteurs est très courant et doit être concis.

Hé, j'ai convaincu Jeff de la syntaxe ! ;-)

Cela appartient à côté de # 4470, donc s'ajoute aux projets 0.4 pour l'instant.

Si je comprends l'essentiel de la discussion, le problème principal est que nous voulons obtenir une syntaxe de type mappage qui :

  • fonctionne avec divers types de données, comme DataArrays, pas seulement des tableaux natifs ;
  • est aussi rapide qu'une boucle écrite manuellement.

Il est peut-être possible de le faire en utilisant l'inlining, mais en faisant très attention à ce que l'inlining fonctionne.

Qu'en est-il de l'approche différente : utiliser une macro en fonction du type de données déduit. Si nous pouvons en déduire que la structure de données est DataArray, nous utilisons map-macro fourni par la bibliothèque DataArrays. S'il s'agit de SomeKindOfStream, nous utilisons la bibliothèque de flux fournie. Si nous ne pouvons pas déduire le type, nous utilisons simplement une implémentation générale répartie dynamiquement.

Cela pourrait obliger les créateurs de structures de données à écrire de telles macros, mais cela ne serait nécessaire que si son auteur voulait qu'elle ait une exécution vraiment efficace.

Si ce que j'écris n'est pas clair, je veux dire quelque chose comme [EXPR for i in collection if COND] pourrait être traduit en eval(collection_mapfilter_macro(:(i), :(EXPR), :(COND))) , où collection_mapfilter_macro est choisi en fonction du type de collection déduit.

Non, nous ne voulons pas faire des choses comme ça. Si DataArray définit map (ou l'équivalent), sa définition doit toujours être appelée pour DataArrays indépendamment de ce qui peut être déduit.

Ce problème ne concerne pas l'implémentation, mais la syntaxe. À l'heure actuelle, de nombreuses personnes sont habituées à mapper implicitement sin(x) si x est un tableau, mais cette approche pose de nombreux problèmes. La question est de savoir quelle syntaxe alternative serait acceptable.

1) Prise en charge de l'affectation sur place sans allocation à l'aide y[:] = sqrt.(x .+ 2)
2) Prise en charge des réductions sans allocation telles que sum((x .- y).^2, 1)

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

En regardant ces trois exemples parmi d'autres, je pense qu'avec la syntaxe for , cela donnerait quelque chose comme ceci :
1) y[:] = [ sqrt(x + 2) for x ])
2) sum([ (x-y)^2 for x,y ], 1)
et
y = [ √π exp(-x^2) * sin(k*x) + im * log(x-1) for x,k ]

J'aime beaucoup ça ! Le fait qu'il crée un tableau temporaire est assez explicite et reste lisible et concis.

Question mineure cependant, x[:] = [ ... for x ] pourrait-il avoir un peu de magie pour muter le tableau sans en allouer un temporaire?
Je ne sais pas si cela apporterait beaucoup d'avantages, mais je peux imaginer que cela aiderait pour les grands tableaux.
Je peux croire cependant qu'il peut s'agir d'une paire de manches complètement différente qui devrait être discutée ailleurs.

@ Mike43110 Votre x[:] = [ ... for x ] pourrait être écrit x[:] = (... for x) , le RHS créant un générateur, qui serait collecté élément par élément pour remplir x , sans allouer de copie. C'était l'idée derrière mon expérience LazyArray ci-dessus.

La syntaxe [f <- y] serait bien si elle était combinée avec une syntaxe Int[f <- y] pour une carte qui connaît son type de sortie et n'a pas besoin d'interpoler à partir de f(y[1]) ce que seront les autres éléments.

Surtout, comme cela donne également une interface intuitive pour mapslices , [f <- rows(A)]rows(A) (ou columns(A) ou slices(A, dims) ) renvoie un Slice object donc dispatch peut être utilisé :

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

Lorsque vous ajoutez l'indexation, cela devient un peu plus difficile. Par exemple

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

Il est difficile d'égaler la concision de cela. L'explosion du style de compréhension est plutôt mauvaise :

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

où, pour aggraver les choses, il fallait de l'intelligence pour savoir qu'il s'agissait d'un cas d'itération imbriquée et qu'il ne pouvait pas être fait avec un seul over . Bien que si f et g soient un peu chers, cela pourrait être plus rapide :

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

mais encore plus longtemps.

Ce genre d'exemple semble plaider en faveur de "points", car cela pourrait donner f.(x[:,j]) .* g.(x[i,:]) .

@JeffBezanson Je ne sais pas quelle est l'intention de votre commentaire. Quelqu'un at-il suggéré de se débarrasser de la syntaxe .* ?

Non; Je me concentre sur f et g ici. Ceci est un exemple où vous ne pouvez pas simplement ajouter over x à la fin de la ligne.

OK, je vois, j'avais raté la fin du commentaire. En effet la version à pois est plus jolie dans ce cas.

Bien qu'avec les vues de tableau, il y aura une alternative raisonnablement efficace (AFAICT) et pas si laide :
[ f(y) * g(z) for y in x[:,j], z in x[i,:] ]

L'exemple ci-dessus pourrait-il être résolu en imbriquant des mots-clés ?

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

est interprété comme

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

tandis que

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

devient

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

Ensuite, l'exemple spécifique ci-dessus serait quelque chose comme

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

EDIT : Rétrospectivement, ce n'est pas aussi concis que je le pensais.

@JeffBezanson Que diriez-vous

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

donne l'équivalent de f.(x[:,n] .* g.(x[m,:]) . La nouvelle syntaxe x[:i,n] signifie que i est introduit localement en tant qu'itérateur sur les indices du conteneur x[:,n] . Je ne sais pas si c'est possible à mettre en place. Mais cela ne semble (prima facie) ni laid ni encombrant, et la syntaxe elle-même donne des bornes pour l'itérateur, à savoir 1:length(x[:,n]). En ce qui concerne les mots clés, "over" peut signaler que toute la plage doit être utilisée, tandis que "for" peut être utilisé si l'utilisateur souhaite spécifier une sous-plage de 1:length(x[:,n]):

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

@davidagold , :i signifie déjà le symbole i .

Ah oui, bon point. Eh bien, tant que les points sont équitables, qu'en est-il

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

où le point indique que i est introduit localement en tant qu'itérateur sur 1:length(x[:,n). Je suppose que cela fait passer la notation par points de la modification des fonctions à la modification des tableaux, ou plutôt de leurs indices. Cela sauverait quelqu'un du «fluage de points», a noté Jeff:

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

par opposition à

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

bien que je suppose que ce dernier est légèrement plus court. [EDIT : aussi, s'il est possible d'omettre le over i lorsqu'il n'y a pas d'ambiguïté, alors on a en fait une syntaxe légèrement plus courte :

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

Un avantage potentiel de la syntaxe de compréhension est qu'elle pourrait permettre une plus large gamme de modèles d'opération élément par élément. Par exemple, si l'analyseur comprenait que l'indexation avec i dans x[m, .i] est implicitement modulo length(x[m,:]), alors on pourrait écrire

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

Multiplier les éléments de x contre les éléments de y dans l'ordre inverse, c'est-à-dire le premier élément de x contre le dernier élément de y , etc. On pourrait écrire

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

multiplier le i ième élément de x par le i+1 ième élément de y (où le dernier élément de x serait multiplié par le premier élément de y car l'indexation est comprise dans ce contexte comme modulo length(x)). Et si p::Permutation permute (1, ..., longueur(x)) on pourrait écrire

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

pour multiplier le i ème élément de x par le p(i) ème élément de y .

Quoi qu'il en soit, ce n'est qu'une humble opinion d'un étranger sur une question entièrement spéculative. =p J'apprécie le temps que quelqu'un prend pour y réfléchir.

Une version gonflée de vectorize qui utilisera le recyclage de style r pourrait être très utile. Autrement dit, les arguments qui ne correspondent pas à la taille du plus grand argument sont étendus via le recyclage. Ensuite, les utilisateurs peuvent facilement vectoriser tout ce qu'ils veulent, quel que soit le nombre d'arguments, etc.

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]

J'ai tendance à penser que le recyclage sacrifie trop la sécurité pour la commodité. Avec le recyclage, il est très facile d'écrire du code bogué qui s'exécute sans générer d'erreur.

La première fois que j'ai lu ce comportement de R, je me suis immédiatement demandé pourquoi quelqu'un penserait que c'était une bonne idée. C'est une chose vraiment étrange et surprenante à faire implicitement sur des tableaux de taille non concordante. Il peut y avoir une poignée de cas où c'est la façon dont vous voudriez étendre le plus petit tableau, mais tout aussi bien vous voudrez peut-être un remplissage par zéro ou la répétition des éléments finaux, ou une extrapolation, ou une erreur, ou n'importe quel nombre d'autres applications dépendantes les choix.

L'utilisation ou non de @super_vectorize serait laissée entre les mains de l'utilisateur. Il serait également possible de donner des avertissements pour divers cas. Par exemple, dans 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

Je n'ai aucune objection à en faire une chose facultative que les utilisateurs peuvent choisir d'utiliser ou non, mais il n'est pas nécessaire de l'implémenter dans le langage de base alors que cela peut tout aussi bien être fait dans un package.

@vectorize_1arg et @vectorize_2arg sont déjà inclus dans Base, et les options qu'ils offrent à l'utilisateur semblent quelque peu limitées.

Mais ce problème se concentre sur la conception d'un système pour supprimer @vectorize_1arg et @vectorize_2arg de Base. Notre objectif est de supprimer les fonctions vectorisées du langage et de les remplacer par une meilleure abstraction.

Par exemple, le recyclage peut s'écrire

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

ce qui pour moi est assez proche de la manière idéale de l'écrire. Personne n'a besoin de le construire pour vous. Les principales questions sont (1) peut-on le rendre plus concis, (2) comment l'étendre à d'autres types de conteneurs.

En regardant la proposition de @davidagold , je me suis demandé si var: ne pouvait pas être utilisé pour ce genre de chose où la variable serait le nom avant les deux-points. J'ai vu cette syntaxe utilisée pour signifier A[var:end] donc elle semble être disponible.

f(x[:,j]) .* g(x[i,:]) serait alors f(x[a:,j]) * g(x[i,b:]) for a, b ce qui n'est pas trop pire.

Plusieurs deux-points seraient cependant un peu bizarres.

f(x[:,:,j]) .* g(x[i,:,:]) -> f(x[a:,a:,j]) * g(x[i,b:,b:]) for a, b était ma première pensée à ce sujet.

Ok, alors voici un bref aperçu d'un programme de recyclage. Il devrait être capable de gérer des tableaux à n dimensions. Il serait probablement possible d'incorporer des tuples de manière analogue aux vecteurs.

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)

De toute évidence, ce n'est pas le code le plus élégant ou le plus rapide (je suis un immigrant R récent). Je ne sais pas comment passer d'ici à une macro @vectorize .

EDIT : boucle redondante combinée
EDIT 2 : séparé de la contrainte à la taille. ne fonctionne actuellement que pour les dimensions 0-2.
EDIT 3 : Une façon un peu plus élégante de le faire serait de définir un type spécial de tableau avec indexation de mod. C'est-à-dire,

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

EDIT 4 : Les choses que je me demande existent parce que c'était difficile à écrire : une généralisation n-dimensionnelle de hcat et vcat ? Un moyen de remplir un tableau à n dimensions (correspondant à la taille d'un tableau donné) avec des listes ou des tuples des indices de chaque position particulière ? Une généralisation à n dimensions de repmat ?

[pao : coloration syntaxique]

Vous ne voulez vraiment pas définir des fonctions avec la syntaxe foo = function(x,y,z) ... end dans Julia, bien que cela fonctionne. Cela crée une liaison non constante du nom à une fonction anonyme. Dans Julia, la norme est d'utiliser des fonctions génériques et les liaisons aux fonctions sont automatiquement constantes. Sinon, vous allez obtenir des performances terribles.

Je ne vois pas pourquoi repmat est nécessaire ici. Les tableaux remplis avec l'index de chaque position sont également un signe d'avertissement : il ne devrait pas être nécessaire d'utiliser une grande quantité de mémoire pour représenter si peu d'informations. Je pense que de telles techniques ne sont vraiment utiles que dans les langages où tout doit être "vectorisé". Il me semble que la bonne approche consiste simplement à exécuter une boucle dans laquelle certains index sont transformés, comme dans https://github.com/JuliaLang/julia/issues/8450#issuecomment -111898906.

Oui, c'est logique. Voici un début, mais j'ai du mal à comprendre comment faire la boucle à la fin, puis créer une macro @vectorize .

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

Dans le discours, @JeffBezanson a mentionné la syntaxe sin(x) over x , pourquoi pas quelque chose comme :
sin(over x) ? (ou utilisez un caractère au lieu d'avoir over comme mot-clé)

Une fois que cela est résolu, nous pouvons également résoudre # 11872

J'espère que je ne suis pas en retard à la fête, mais je voudrais juste offrir un +1 à la proposition de syntaxe de @davidagold . C'est conceptuellement clair, concis et semble vraiment naturel à écrire. Je ne sais pas si . serait le meilleur caractère d'identification, ou dans quelle mesure une implémentation réelle serait faisable, mais on pourrait faire une preuve de concept en utilisant une macro pour l'essayer (essentiellement comme @devec , mais pourrait même être plus facile à mettre en œuvre).

Il a également l'avantage de "s'intégrer" à la syntaxe de compréhension de tableau existante :

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)]

La principale différence entre les deux étant que le premier aurait plus de restrictions sur la préservation de la forme, car cela implique une carte.

over , range et window ont de l'art antérieur dans l'espace OLAP en tant que modificateurs d'itération, cela semble cohérent.

Je n'aime pas la syntaxe . car cela ressemble à un glissement vers le bruit de ligne.

$ est peut-être cohérent, interne les valeurs d'itération i,j dans l'expression ?

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

Pour la vectorisation automatique d'une expression, ne pouvons-nous pas taint l'un des vecteurs de l'expression et faire en sorte que le système de type soulève l'expression dans l'espace vectoriel ?

Je fais la même chose qu'avec les opérations de séries chronologiques où l'expressivité de julia me permet déjà d'écrire

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

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

qui, lorsqu'il est observé, produit une série temporelle de vecteurs.

La principale partie manquante est la possibilité de soulever automatiquement les fonctions existantes dans l'espace. Cela doit être fait à la main

Essentiellement, j'aimerais pouvoir définir quelque chose comme ce qui suit ...

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

et pouvoir ensuite se spécialiser pour des opérations spécifiques

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

Salut @JeffBezanson ,

Si je comprends bien, je voudrais proposer une solution à votre commentaire JuliaCon 2015 concernant un commentaire fait ci-dessus :
"[...] Et dire aux rédacteurs de bibliothèques de mettre @vectorize sur toutes les fonctions appropriées est idiot ; vous devriez pouvoir simplement écrire une fonction, et si quelqu'un veut la calculer pour chaque élément, il utilise map."
(Mais je n'aborderai pas l'autre problème fondamental "[..] aucune raison vraiment convaincante pour laquelle sin, exp etc. devraient être implicitement mappés sur des tableaux.")

Dans Julia v0.40, j'ai pu obtenir une solution un peu plus agréable (à mon avis) que @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!

Ceci est plus ou moins raisonnable, mais est quelque peu similaire à la solution @vectorize . Pour que la vectorisation soit élégante, je suggère à Julia de prendre en charge les éléments suivants :

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

C'est ça! Avoir une fonction héritée d'une fonction vectorisable la rendrait vectorisable.

J'espère que cela correspond à ce que vous recherchiez.

Salutations,

MA

En l'absence d'héritage multiple, comment une fonction hérite-t-elle de Vectorizable et d'autre chose ? Et comment reliez-vous les informations d'héritage pour des méthodes spécifiques aux informations d'héritage pour une fonction générique ?

@ma-laforge Vous pouvez déjà le faire --- définir un type myadd <: Vectorizable2Arg , puis implémenter call pour myadd sur Number .

Merci pour ça @JeffBezanson !

En effet, je peux presque ma solution sembler presque aussi bonne que ce que je veux :

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

Maintenant, la seule chose qui manque serait un moyen de "sous-typer" n'importe quelle fonction, de sorte que toute cette section F puisse être remplacée par la syntaxe la plus élégante :

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

REMARQUE : j'ai créé le type "MyAddType" et le nom de la fonction dans un objet singleton "myadd" car je trouve la syntaxe résultante plus agréable que si l'on utilisait Type{Vectorizable2Arg} dans la signature d'appel :

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

Malheureusement , d'après votre réponse, il semble que ce ne serait pas une solution adéquate à la "sottise" de la macro @vectorize .

Salutations,

MA

@johnmyleswhite :

Je voudrais répondre à votre commentaire, mais je ne pense pas avoir compris. Pouvez-vous clarifier?

Une chose que je _peux_ dire :
"Vectorisable" n'a rien de spécial. L'idée est que n'importe qui peut définir sa propre "classe" de fonction (Ex: MyFunctionGroupA<:Function ). Ils pourraient alors intercepter les appels aux fonctions de ce type en définissant leur propre signature "d'appel" (comme démontré ci-dessus).

Cela dit: ma suggestion est que les fonctions définies dans Base doivent utiliser Base.Vectorizable <: Function (ou quelque chose de similaire) afin de générer automatiquement des algorithmes vectorisés automatiquement.

Je suggérerais alors aux développeurs de modules d'implémenter leurs propres fonctions en utilisant un modèle un peu comme :

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

Bien sûr, ils devraient fournir leur propre version de promote_type(::Type{MyType},::Type{MyType}) - si la valeur par défaut n'est pas déjà de retourner MyType .

Si l'algorithme de vectorisation par défaut est insuffisant, rien n'empêche l'utilisateur d'implémenter sa propre hiérarchie :

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

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

MA

@ma-laforge, Désolé de ne pas avoir été clair. Ma préoccupation est que toute hiérarchie manquera toujours d'informations importantes car Julia a un héritage unique, ce qui vous oblige à vous engager sur un seul type de parent pour chaque fonction. Si vous utilisez quelque chose comme myfunction(x::MyType, y::MyType) <: Base.Vectorizable , votre fonction ne bénéficiera pas de quelqu'un d'autre définissant un concept comme Base.NullableLiftable qui génère automatiquement des fonctions de nullables.

Il semble que ce ne serait pas un problème avec les traits (cf. https://github.com/JuliaLang/julia/pull/13222). Est également liée la nouvelle possibilité de déclarer des méthodes comme pures (https://github.com/JuliaLang/julia/pull/13555), ce qui pourrait impliquer automatiquement qu'une telle méthode est vectorisable (au moins pour les méthodes à argument unique).

@johnmyleswhite ,

Si j'ai bien compris : je ne pense pas que ce soit un problème pour _ce_ cas en particulier. C'est parce que je propose un modèle de conception. Vos fonctions ne _doivent_ pas hériter de Base.Vectorizable ... Vous pouvez utiliser les vôtres.

Je ne sais pas vraiment grand-chose sur NullableLiftables (je ne semble pas avoir ça dans ma version de Julia). Cependant, en supposant qu'il hérite de Base.Function (ce qui n'est pas non plus possible dans ma version de Julia):

NullableLiftable <: Function

Votre module pourrait alors implémenter (une seule fois) un _nouveau_ sous-type vectorisable :

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

Ainsi, à partir de maintenant, toute personne définissant une fonction <: VectorizableNullableLiftable verrait votre code de vectorisation appliqué automatiquement !

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

Je comprends qu'avoir plus d'un type Vectorisable est toujours un peu pénible (et un peu inélégant)... Mais au moins cela supprimerait l'une des répétitions ennuyeuses (1) dans Julia (devant enregistrer _chaque_ nouvellement ajouté fonction avec un appel à @vectorize_Xarg).

(1) Cela suppose que Julia prend en charge l'héritage sur les fonctions (ex: myfunction(...)<: Vectorizable ) - ce qui n'apparaît pas, sur la v0.4.0. La solution que j'ai eue dans Julia 0.4.0 n'est qu'un hack... Vous devez encore enregistrer votre fonction... pas beaucoup mieux que d'appeler @vectorize_Xarg

MA

Je pense toujours que c'est un peu la mauvaise abstraction. Une fonction qui peut ou doit être "vectorisée" n'est pas un type particulier de fonction. La fonction _Every_ peut être passée à map , lui donnant ce comportement.

BTW, avec le changement sur lequel je travaille dans la branche jb/functions, vous pourrez faire function f(x) <: T (bien que, clairement, seulement pour la première définition de f ).

Ok, je pense que je comprends mieux ce que vous cherchez... et ce n'est pas ce que j'ai suggéré. Je pense que cela pourrait aussi faire partie des problèmes que @johnmyleswhite a eu avec mes suggestions...

...Mais si je comprends maintenant quel est le problème, la solution m'apparaît encore plus simple :

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

Puisque myadd est de type Function , il devrait être piégé par la fonction call ... ce qu'il fait :

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

Mais call ne se répartit pas automatiquement sur les fonctions, pour une raison quelconque (je ne sais pas pourquoi):

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

Mais j'imagine que ce comportement ne devrait pas être trop difficile à changer . Personnellement, je ne sais pas ce que je ressens à l'idée de créer des fonctions fourre-tout aussi complètes, mais on dirait que c'est ce que vous voulez.

Quelque chose d'étrange que j'ai remarqué : Julia auto-vectorise déjà les fonctions si elles ne sont pas typées :

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

RE : BTW... :
Frais! Je me demande quelles choses intéressantes je pourrai faire en sous-typant les fonctions :).

Julia auto-vectorise déjà les fonctions si elles ne sont pas typées

Il apparaît ainsi parce que l'opérateur + utilisé à l'intérieur de la fonction est vectorisé.

Mais j'imagine que ce comportement ne devrait pas être trop difficile à changer. Personnellement, je ne sais pas ce que je ressens à l'idée de créer des fonctions fourre-tout aussi complètes, mais on dirait que c'est ce que vous voulez.

Je partage vos réserves. Vous ne pouvez pas raisonnablement avoir une définition qui dit "voici comment appeler n'importe quelle fonction", car chaque fonction elle-même dit quoi faire quand elle est appelée --- c'est ce qu'est une fonction !

J'aurais dû dire: ... opérateurs d'infixes non unicode définis par l'utilisateur, car je ne pense pas que nous voulions obliger les utilisateurs à taper des caractères unicode pour accéder à une telle fonctionnalité de base. Bien que je vois que $ est en fait l'un de ceux qui ont été ajoutés, alors merci pour cela ! Wow, donc cela fonctionne réellement dans Julia aujourd'hui (même si ce n'est pas "rapide"... encore):

julia> ($) = carte
julia> sin $ (0.5 * (abs2 $ (xy)))

@binarybana que diriez-vous de / \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

Il y a aussi:

FWIW, je supposerais au moins initialement que \mapsto était une syntaxe alternative pour les lambdas, car elle est couramment utilisée en mathématiques et, essentiellement (dans son incarnation ASCII, -> ) dans Julia, aussi . Je pense que ce serait plutôt déroutant.

En parlant de maths… Dans la théorie des modèles, j'ai vu map exprimé en appliquant une fonction à un tuple sans parenthèses. Autrement dit, si \bar{a}=(a_1, \dots, a_n) , alors f(\bar{a}) est f(a_1, \dots, a_n) (c'est-à-dire, essentiellement, apply ) et f\bar{a} est (f(a_1), \dots, f(a_n)) (c'est-à-dire, map ). Syntaxe utile pour définir des homomorphismes, etc., mais pas si facilement transférable dans un langage de programmation :-}

Que diriez-vous de l'une des autres alternatives comme \Mapsto , la confondriez-vous avec => (Paire) ? Je pense que les deux symboles se distinguent ici côte à côte :

  • ->

Il y a beaucoup de symboles qui se ressemblent, quelle serait la raison d'en avoir autant si nous n'utilisions que ceux qui semblent très différents ou qui sont de l'ASCII pur ?

Je pense que ce serait plutôt déroutant.

Je pense que la documentation et l'expérience résolvent ce problème, êtes-vous d'accord ?

Il y a aussi beaucoup d'autres flèches comme des symboles, je ne sais honnêtement pas à quoi ils servent en mathématiques ou alors, je n'ai proposé que ceux-ci parce qu'ils ont map dans leurs noms ! :le sourire:

Je suppose que mon point est que -> est la tentative de Julia de représenter en ASCII. Donc, utiliser pour signifier quelque chose d'autre semble peu judicieux. Ce n'est pas que je ne peux pas les distinguer visuellement :-)

Mon sentiment instinctif est simplement que si nous allons utiliser des symboles mathématiques bien établis, nous voudrons peut-être au moins réfléchir à la façon dont l'utilisation de Julia diffère de l'utilisation établie. Il semble logique de sélectionner des symboles avec map dans leurs noms, mais dans ce cas cela fait référence à la définition d'une carte (ou de cartes de différents types, ou des types de telles cartes). C'est également vrai de l'utilisation dans Pair, plus ou moins, où plutôt que de définir une fonction complète en définissant ce à quoi un paramètre correspond, vous répertoriez en fait à quoi correspond un argument donné (valeur de paramètre) - c'est-à-dire qu'il s'agit d'un élément d'un explicitement fonction énumérée, conceptuellement (par exemple, un dictionnaire).

@Ismael-VC Le problème avec votre suggestion est que vous devez appeler map deux fois, alors que la solution idéale n'impliquerait aucun tableau temporaire et se réduirait à map((a, b) -> sin(0.5 * abs2(a-b)), x, y) . De plus, répéter deux fois n'est pas génial à la fois visuellement et pour la saisie (pour laquelle un équivalent ASCII serait bien d'avoir).

Les utilisateurs de R pourraient ne pas aimer cette idée, mais si nous nous dirigeons vers la dépréciation de l'analyse actuelle des cas spéciaux infix-macro de ~ (des packages comme GLM et DataFrames devraient passer à l'analyse macro de leurs formules DSL, ref https:/ /github.com/JuliaStats/GLM.jl/issues/116), cela libérerait la denrée rare d'un opérateur infix ascii.

a ~ b pourrait être défini dans la base comme map(a, b) , et peut-être a .~ b pourrait être défini comme broadcast(a, b) ? S'il analyse comme un opérateur d'infixe conventionnel, les macro DSL comme une émulation de l'interface de formule R seraient libres d'implémenter leur propre interprétation de l'opérateur à l'intérieur des macros, comme le fait JuMP avec <= et == .

Ce n'est peut-être pas la plus jolie des suggestions, mais les abréviations de Mathematica ne le sont pas non plus si vous en abusez... ma préférée est .#&/@

:+1: pour une casse moins spéciale et plus de généralité et de cohérence, les significations que vous proposez pour ~ et .~ me semblent très bien.

+1 Tony.

@tkelman Pour être clair, comment écririez-vous, par exemple, sin(0.5 * abs2(a-b)) de manière entièrement vectorisée ?

Avec une compréhension, probablement. Je ne pense pas que la carte infixe fonctionnerait pour les varargs ou sur place, donc la possibilité d'une syntaxe libre ne résout pas tous les problèmes.

Cela ne résoudrait donc pas vraiment ce problème. :-/

Jusqu'à présent, la syntaxe sin(0.5 * abs2(a-b)) over (a, b) (ou une variante, utilisant éventuellement un opérateur infixe) est la plus attrayante.

Le titre de ce numéro est "Syntaxe alternative pour map(func, x) ". L'utilisation d'un opérateur infixe ne résout pas la fusion carte/boucle pour éliminer les temporaires, mais je pense que cela peut être un problème encore plus large, connexe mais techniquement distinct que la syntaxe.

Oui, je suis d'accord avec @tkelman , le but est d'avoir une syntaxe alternative pour map , c'est pourquoi j'ai suggéré d'utiliser \mapsto , . Ce que @nalimilan mentionne semble être plus large et mieux adapté à un autre problème IMHO, How to fully vecotrize expressions ?

<rambling>
Ce problème dure depuis plus d'un an maintenant (et pourrait continuer indéfiniment comme de nombreux autres problèmes le sont maintenant) ! Mais nous pourrions avoir Alternative syntax for map(func, x) maintenant . Sur ±450 contributeurs juliens, seuls 41 ont pu trouver ce problème et/ou ont voulu partager un avis (c'est beaucoup pour un problème github mais clairement pas suffisant dans ce cas), dans l'ensemble il n'y a pas tellement de suggestions différentes (qui ne sont pas seulement des variations mineures du même concept).

Je sais que certains d'entre vous n'aiment pas l'idée ou ne voient pas l'intérêt de faire des enquêtes/sondages (:shocked:), mais puisque je n'ai pas besoin de demander la permission à qui que ce soit pour quelque chose comme ça, je le ferai quand même. C'est un peu triste que nous ne tirions pas pleinement parti de notre communauté et de nos réseaux sociaux et d'autres communautés, encore plus tristes que nous n'en voyions pas la valeur, voyons si je peux recueillir des opinions plus différentes et plus fraîches , ou au moins vérifier Découvrez ce que la majorité pense des opinions actuelles sur ce problème particulier, à titre d'expérience, et voyez comment cela se passe. Peut-être que c'est effectivement inutile, peut-être que non, il n'y a qu'une seule façon de vraiment savoir.
</rambling>

@Ismael-VC : Si vous voulez vraiment faire un sondage, la première chose que vous devez faire est de bien réfléchir à la question que vous voulez poser. Vous ne pouvez pas vous attendre à ce que tout le monde lise l'intégralité du fil de discussion et résume individuellement les options qui ont fait l'objet de discussions.

map(func, x) couvre également des choses comme map(v -> sin(0.5 * abs2(v)), x) , et c'est ce dont ce fil a discuté. Ne déplaçons pas cela dans un autre fil, car cela rendrait plus difficile de garder à l'esprit toutes les propositions discutées ci-dessus.

Je ne suis pas opposé à l'ajout de syntaxe pour le simple cas d'application d'une fonction générique en utilisant map , mais si nous le faisons, je pense que ce serait une bonne idée de considérer l'image plus large en même temps. Si ce n'était pas pour cela, le problème aurait pu être résolu il y a longtemps déjà.

@Ismael-VC Les sondages sont peu susceptibles d'aider ici à mon humble avis. Nous n'essayons pas de savoir laquelle de plusieurs solutions est la meilleure, mais plutôt de trouver une solution que personne n'a encore vraiment trouvée. Cette discussion est déjà longue et a impliqué de nombreuses personnes, je ne pense pas qu'en ajouter plus aidera.

@Ismael-VC C'est bien, n'hésitez pas à faire un sondage. En fait, j'ai fait quelques sondages doodle sur des problèmes dans le passé (par exemple http://doodle.com/poll/s8734pcue8yxv6t4). D'après mon expérience, le même nombre de personnes ou moins votent dans les sondages que dans les fils de discussion. Cela a du sens pour des problèmes de syntaxe très spécifiques, souvent superficiels. Mais comment un sondage va-t-il générer de nouvelles idées alors que tout ce que vous pouvez faire est de choisir parmi les options existantes ?

Bien sûr, le véritable objectif de ce numéro est d'éliminer les fonctions implicitement vectorisées. En théorie, la syntaxe de map est suffisante pour cela, car toutes ces fonctions ne font que map dans tous les cas.

J'ai essayé de rechercher la notation mathématique existante pour cela, mais vous avez tendance à voir des commentaires indiquant que l'opération est trop peu importante pour avoir une notation ! Dans le cas de fonctions arbitraires dans un contexte mathématique, je peux presque le croire. Cependant, la chose la plus proche semble être la notation du produit Hadamard, qui a quelques généralisations : https://en.wikipedia.org/wiki/Hadamard_product_ (matrices)#Analogous_Operations

Cela nous laisse avec sin∘x comme @rfourquet l' a suggéré. Cela ne semble pas trop utile, car il nécessite Unicode et n'est pas largement connu de toute façon.

@nalimilan , je pense que vous feriez simplement sin(0.5 * abs2(a-b)) ~ (a,b) ce qui se traduirait par map((a,b)->sin(0.5 * abs2(a-b)), (a,b)) . Je ne sais pas si c'est tout à fait exact, mais je pense que cela fonctionnerait.

Je me méfie également de trop me plonger dans le problème de "laissez-moi vous donner une énorme expression compliquée et vous la vectorisez parfaitement automatiquement pour moi". Je pense que la solution ultime à cet égard consiste à créer un DAG complet d'expressions/tâches + planification de requêtes, etc. Mais je pense que c'est un bien plus gros poisson à frire que d'avoir simplement une syntaxe pratique pour map .

@quinnj Ouais, c'est essentiellement la syntaxe over proposée ci-dessus, sauf avec un opérateur infixe.

Commentaire sérieux: je pense que vous allez probablement réinventer SQL si vous poursuivez cette idée assez loin puisque SQL est essentiellement un langage pour composer des fonctions élément par élément de nombreuses variables qui sont ensuite appliquées via une "vectorisation" par ligne.

@johnmyleswhite d'accord, commence à ressembler à un DSL alias Linq

Sur le sujet posté sur les fils de discussion, vous pouvez spécialiser l'opérateur |> 'pipe' et obtenir la fonctionnalité de style de carte. Vous pouvez le lire en tant que tuyau de la fonction vers les données. En prime, vous pouvez utiliser la même chose pour effectuer la composition de fonctions.

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 C'est vrai, mais il y a des objectifs intermédiaires intéressants qui sont assez modestes. Sur ma branche, la version map des expressions vectorisées multi-opérations est déjà plus rapide que ce que nous avons maintenant. Il est donc quelque peu urgent de déterminer comment effectuer une transition en douceur.

@johnmyleswhite Pas sûr. Une grande partie de SQL consiste à sélectionner, ordonner et fusionner des lignes. Ici, nous ne parlons que de l'application d'une fonction élément par élément. De plus, SQL ne fournit aucune syntaxe pour distinguer les réductions (par exemple SUM ) des opérations élément par élément (par exemple > , LN ). Ces derniers sont simplement automatiquement vectorisés comme dans Julia actuellement.

@JeffBezanson La beauté de l'utilisation de \circ est que si vous interprétez une famille indexée comme une fonction à partir de l'ensemble d'index (qui est la "mise en œuvre" mathématique standard), alors la cartographie _est_ simplement une composition. Donc (sin ∘ x)(i)=sin(x(i)) , ou plutôt sin(x[i]) .

L'utilisation du tube, comme le mentionne @mdcfrancis , serait essentiellement une composition "d'ordre de diagramme", ce qui est souvent fait avec un point-virgule (éventuellement gras) en mathématiques (ou en particulier les applications CS de la théorie des catégories) - mais nous avons déjà le tube opérateur, bien sûr.

Si aucun de ces opérateurs de composition ne convient, on peut en utiliser d'autres. Par exemple, au moins certains auteurs utilisent le modeste \cdot pour la composition abstraite flèche/morphisme, car il s'agit essentiellement de la "multiplication" du groupoïde (plus ou moins) des flèches.

Et si l'on voulait un analogue ASCII : Il y a aussi des auteurs qui utilisent en fait un point pour indiquer la multiplication. (J'ai peut-être vu certains l'utiliser également pour la composition; je ne m'en souviens plus.)

Donc, on pourrait avoir sin . x … mais je suppose que ce serait déroutant :-}

Pourtant… cette dernière analogie pourrait être un argument pour l'une des toutes premières propositions, c'est-à-dire sin.(x) . (Ou peut-être que c'est tiré par les cheveux.)

Essayons sous un angle différent, ne me tirez pas dessus.

Si nous définissons .. par collect(..(A,B)) == ((a[1],..., a[n]), (b[1], ...,b[n])) == zip(A,B) , alors en utilisant T[x,y,z] = [T(x), T(y), T(z)] formellement, cela signifie que

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

Cela motive au moins une syntaxe pour map qui n'interfère pas avec la syntaxe pour la construction de tableaux. Avec :: ou table l'extension f[::(A,B)] = [f(a[i], b[j]) for i in 1:n, j in 1:n] conduit au moins à un deuxième cas d'utilisation intéressant.

réfléchissez bien à la question que vous voulez poser.

@toivoh Merci, je le ferai. J'évalue actuellement plusieurs logiciels de sondage/sondage. De plus, je ne sonderai que sur la syntaxe préférée, ceux qui veulent lire l'intégralité du fil le feront, ne supposons pas que personne d'autre ne sera intéressé à le faire.

trouver une solution que personne n'a vraiment déjà trouvée

@nalimilan personne parmi nous, c'est-à-dire. :le sourire:

Comment un sondage va-t-il générer de nouvelles idées alors que tout ce que vous pouvez faire est de choisir parmi les options existantes ?
autant ou moins de personnes votent dans les sondages que celles discutées dans les fils de discussion.

@JeffBezanson Je suis content d'apprendre que vous avez déjà fait des sondages, continuez comme ça !

  • Comment avez-vous fait la promotion de vos sondages ?
  • À partir du logiciel de sondage/enquête que j'ai évalué jusqu'à présent, kwiksurveys.com permet aux utilisateurs d'ajouter leurs propres opinions au lieu de l'option _aucune de ces options n'est pour moi_.

sin∘x Ne semble pas trop utile, car il nécessite Unicode et n'est pas largement connu de toute façon.

Nous avons tellement d'Unicode, utilisons-le, nous avons même une bonne façon de les utiliser avec l'achèvement des tabulations, qu'y a-t-il de mal à les utiliser alors ?, Si ce n'est pas connu, documentons et éduquons, s'il n'existe pas, quel est le problème l'inventer ? Avons-nous vraiment besoin d'attendre que quelqu'un d'autre l'invente et l'utilise pour pouvoir le considérer comme un précédent et seulement après l'avoir envisagé ?

a un précédent, donc le problème est que c'est Unicode ? Pourquoi? quand allons-nous commencer à utiliser le reste de l'Unicode peu connu alors ? jamais?

Selon cette logique, Julia n'est de toute façon pas très connue, mais ceux qui veulent apprendre le feront. Cela n'a tout simplement pas de sens pour moi, à mon très humble avis.

Assez juste, je ne suis pas totalement contre . Exiger que l'unicode utilise une fonctionnalité assez basique n'est qu'un point contre elle. Pas forcément suffisant pour le couler complètement.

Serait-il complètement fou d'utiliser/surcharger * comme alternative ASCII ? Je dirais qu'on pourrait dire que cela a du sens mathématiquement, mais je suppose qu'il peut être difficile de discerner sa signification parfois… (Encore une fois, si c'est limité à la fonctionnalité map , alors map est déjà une alternative ASCII, non ?)

La beauté de l'utilisation de \circ est que si vous interprétez une famille indexée comme une fonction à partir de l'ensemble d'index (qui est la "mise en œuvre" mathématique standard), alors le mappage _est_ simplement une composition.

Je ne suis pas sûr d'acheter ça.

@hayd Quelle partie de celui-ci? Qu'une famille indexée (par exemple, une séquence) peut être considérée comme une fonction à partir de l'ensemble d'index, ou que la cartographie dessus devient une composition ? Ou que c'est une perspective utile dans ce cas?

Les deux premiers points (mathématiques) sont assez incontestables, je pense. Mais, oui, je ne vais pas plaider fortement pour l'utiliser ici - c'était surtout un "Ah, ça va un peu!" réaction.

@mlhetland |> est assez proche de -> et fonctionne aujourd'hui - il a aussi "l'avantage" d'être correctement associatif.

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

@mdcfrancis Bien sûr. Mais cela renverse l'interprétation de la composition que j'ai esquissée. Autrement dit, sin∘x équivaudrait à x |> sin , non ?

PS: Peut-être qu'il s'est perdu dans "l'algèbre", mais le simple fait d'autoriser les fonctions dans la construction de tableaux typés T[x,y,z] tels que f[x,y,z] est [f(x),f(y),f(z)] donne directement

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

qui est assez lisible et pourrait être traité comme une syntaxe..

C'est malin. Mais je soupçonne que si nous pouvons le faire fonctionner, sin[x...] perd vraiment en verbosité au profit de sin(x) ou sin~x etc.

Qu'en est-il de la syntaxe [sin xs] ?

Ceci est similaire dans la syntaxe à la compréhension du tableau [sin(x) for x in xs] .

@mlhetland sin |> x === map( sin, x )

Ce serait l'ordre inverse de la signification actuelle du chaînage de fonctions . Non pas que cela ne me dérangerait pas de trouver une meilleure utilisation pour cet opérateur, mais j'aurais besoin d'une période de transition.

@mdcfrancis Oui, je comprends que c'est ce que vous visez. Ce qui inverse les choses (comme @tkelman réitère) wrt. l'interprétation de la composition que j'ai esquissée.

Je pense que l'intégration de la vectorisation et du chaînage serait plutôt cool. Je me demande si les mots seraient les opérateurs les plus clairs.
Quelque chose comme:

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

Plusieurs cartes consécutives peuvent être automatiquement combinées en une seule carte pour améliorer les performances. Notez également que je suppose que la carte aura une sorte de fonction de diffusion automatique. Remplacer [1, 2] par _ au début pourrait à la place créer une fonction anonyme. Notez que j'utilise les règles de magrittr de R pour le chaînage (voir mon message dans le fil de chaînage).

Peut-être que cela commence à ressembler davantage à un DSL.

Je suis ce problème depuis longtemps et je n'ai pas commenté jusqu'à présent, mais cela commence à devenir incontrôlable à mon humble avis.

Je soutiens fortement l'idée d'une syntaxe propre pour map. J'aime le plus la suggestion de @tkelman de ~ car elle reste dans ASCII pour une telle fonctionnalité de base, et j'aime bien sin~x . Cela permettrait une cartographie de style à une ligne assez sophistiquée, comme indiqué ci-dessus. L'utilisation de sin∘x serait également acceptable. Pour quelque chose de plus compliqué, j'ai tendance à penser qu'une boucle appropriée est juste beaucoup plus claire (et généralement la meilleure performance). Je n'aime pas trop la diffusion "magique", cela rend le code beaucoup plus difficile à suivre. Une boucle explicite est généralement plus claire.

Cela ne veut pas dire qu'une telle fonctionnalité ne devrait pas être ajoutée, mais commençons par une belle syntaxe concise map , d'autant plus qu'elle est sur le point de devenir super rapide (d'après mes tests de la branche jb/functions ) .

Notez que l'un des effets de jb/functions est que broadcast(op, x, y) est tout aussi performant que la version personnalisée x .op y qui spécialisait manuellement la diffusion sur op .

Pour quelque chose de plus compliqué, j'ai tendance à penser qu'une boucle appropriée est juste beaucoup plus claire (et généralement la meilleure performance). Je n'aime pas trop la diffusion "magique", cela rend le code beaucoup plus difficile à suivre. Une boucle explicite est généralement plus claire.

Je ne suis pas d'accord. exp(2 * x.^2) est parfaitement lisible et moins verbeux que [exp(2 * v^2) for v in x] . Le défi ici à mon humble avis est d'éviter de piéger les gens en les laissant utiliser le premier (qui alloue des copies et ne fusionne pas les opérations) : pour cela, nous devons trouver une syntaxe suffisamment courte pour que la forme lente puisse être obsolète.

Plus de pensées. Il y a plusieurs choses possibles que vous pourriez vouloir faire lors de l'appel d'une fonction :

boucle sans argument (chaîne)
boucler uniquement l'argument chaîné (carte)
boucle sur tous les arguments (mapall)

Chacun des éléments ci-dessus pourrait être modifié, par :
Marquage d'un élément à parcourir en boucle (~)
Marquage d'un élément à ne pas parcourir en boucle (un ensemble supplémentaire de [ ] )

Les éléments non lisibles doivent être gérés automatiquement, sans tenir compte de la syntaxe.
L'expansion des dimensions singleton devrait se produire automatiquement s'il y a au moins deux arguments en boucle

La diffusion ne fait une différence que lorsqu'il y aurait eu une dimension
inadéquation sinon. Donc, quand vous dites ne pas diffuser, vous voulez dire donner un
erreur à la place si la taille de l'argument ne correspond pas ?

sin[x...] perd vraiment en verbosité au profit de sin(x) ou sin~x etc.

De plus, poursuivant la réflexion, la carte sin[x...] est une version moins impatiente sur [f(x...)] .
La syntaxe

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

ou quelque chose de similaire comme [exp(2 * (x..)^2)] serait disponible et s'expliquerait de lui-même si jamais un véritable chaînage de fonctions tacites était introduit.

@nalimilan oui mais cela rentre dans ma catégorie 'one-liner' que j'ai dit bien sans boucle.

Pendant que nous énumérons tous nos souhaits : beaucoup plus important pour moi serait que les résultats de map soient assignables sans allocation ni copie. C'est une autre raison pour laquelle je préférerai toujours les boucles pour le code critique pour les performances, mais si cela peut être atténué (# 249 n'a actuellement pas l'air d'un ATM plein d'espoir), tout cela devient beaucoup plus attrayant.

les résultats de la carte peuvent être attribués sans allocation ni copie

Pouvez-vous nous en dire un peu plus ? Vous pouvez certainement muter le résultat de map .

Je suppose qu'il veut dire stocker la sortie de map dans un tableau préalloué.

Oui, exactement. Toutes mes excuses si c'est déjà possible.

Ah, bien sûr. Nous avons map! , mais comme vous le constatez, #249 demande une meilleure façon de le faire.

@jtravs J'ai proposé une solution ci-dessus avec LazyArray (https://github.com/JuliaLang/julia/issues/8450#issuecomment-65106563), mais jusqu'à présent, les performances n'étaient pas idéales.

@toivoh J'ai apporté plusieurs modifications à ce message après l'avoir publié. La question qui m'inquiétait était de savoir comment déterminer les arguments à parcourir et les arguments à ne pas faire (donc mapall pourrait être plus clair que diffusé). Je pense que si vous parcourez plus d'un argument, l'expansion des dimensions singleton pour produire des tableaux comparables devrait toujours être effectuée si nécessaire, je pense.

Oui map! est tout à fait exact. Ce serait bien si un bon sucre de syntaxe élaboré ici couvrait également ce cas. Ne pourrions-nous pas avoir que x := ... mappe implicitement le RHS sur x .

J'ai mis en place un package appelé ChainMap qui intègre la cartographie et le chaînage.

Voici un court exemple :

<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

Je n'arrêtais pas d'y penser et je pense avoir finalement trouvé une syntaxe cohérente et julienne pour mapper des tableaux dérivés de la compréhension des tableaux. La proposition suivante est julienne car elle s'appuie sur le langage de compréhension de tableau qui est déjà établi.

  1. A partir de f[a...] qui était en fait proposé par @Jutho , la convention
    que pour a vecteurs a, b
f[a...] == map(f, a[:])
f[a..., b...] == map(f, a[:], b[:])
etc

qui n'introduit pas de nouveaux symboles.

2.) En plus de cela, je proposerais l'introduction d'un opérateur supplémentaire : un opérateur _shape preserve_ splatting .. (disons). C'est parce que ... est un opérateur _flat_ spatting, donc f[a...] devrait retourner un vecteur et non un tableau même si a est n -dimensionnel. Si .. est choisi, alors dans ce contexte,

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

et le résultat hérite de la forme des arguments. Autoriser la diffusion

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

permettrait d'écrire pense comme

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

Heureka ?

Cela n'aide pas avec les expressions de mappage, n'est-ce pas ? L'un des avantages de la syntaxe over est son fonctionnement avec les expressions :

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

Eh bien, peut-être via [sin(x.. * y..)] ou sin[x.. * y..] ci-dessus si vous voulez autoriser cela. J'aime cela un peu plus que la syntaxe over parce qu'elle donne une indication visuelle que la fonction opère sur les éléments et non sur les conteneurs.

Mais ne pouvez-vous pas simplifier cela pour que x.. simplement mappé sur x ? Donc, l'exemple de @johansigfrids serait :

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

@jtravs En raison de la portée ( [println(g(x..))] vs println([g(x..)]) ) et de la cohérence [x..] = x .

Une autre possibilité est de prendre x.. = x[:, 1], x[:, 2], etc. comme splat partiel des sous-tableaux de tête (colonnes) et ..y comme splat partiel des sous-tableaux de fin ..y = y[1,:], y[2,:] . Si les deux fonctionnent sur des indices différents, cela couvre de nombreux cas intéressants

[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 un vecteur, A une matrice)

Je préfère over , car cela vous permet d'écrire une expression dans une syntaxe normale au lieu d'introduire de nombreux crochets et points.

Vous avez raison pour l'encombrement, je pense et j'ai essayé d'adapter et de systématiser ma proposition. Pour ne pas surcharger la patience de tout le monde, j'ai écrit mes réflexions sur les cartes et les indices, etc. dans un résumé https://gist.github.com/mschauer/b04e000e9d0963e40058 .

Après avoir lu ce fil, ma préférence serait jusqu'à présent d'avoir _both_ f.(x) pour les choses simples et les personnes habituées aux fonctions vectorisées (l'idiome " . = vectorisé" est assez courant), et f(x^2)-x over x pour les expressions plus compliquées.

Il y a juste trop de gens qui viennent de Matlab, Numpy, etc., pour abandonner complètement la syntaxe des fonctions vectorisées ; leur dire d'ajouter des points est facile à retenir. Une bonne syntaxe de type over pour vectoriser des expressions complexes en une seule boucle est également très utile.

La syntaxe over me frotte vraiment dans le mauvais sens. Il vient de me venir à l'esprit pourquoi: cela suppose que toutes les utilisations de chaque variable dans une expression sont vectorisées ou non vectorisées, ce qui peut ne pas être le cas. Par exemple, log(A) .- sum(A,1) – supposons que nous avons supprimé la vectorisation de log . Vous ne pouvez pas non plus vectoriser les fonctions sur les expressions, ce qui semble être une lacune assez importante et si je voulais écrire exp(log(A) .- sum(A,1)) et avoir le exp et le log vectorisés et le sum non ?

@StefanKarpinski , alors vous devriez faire soit exp.(log.(A) .- sum(A,1)) et accepter les temporaires supplémentaires (par exemple dans une utilisation interactive où les performances ne sont pas critiques), ou s = sum(A, 1); exp(log(A) - s) over A (bien que ce ne soit pas tout à fait correct si sum(A,1) est un vecteur et vous vouliez diffuser ); vous devrez peut-être simplement utiliser une compréhension. Peu importe la syntaxe que nous proposons, nous n'allons pas couvrir tous les cas possibles, et votre exemple est particulièrement problématique car toute syntaxe "automatisée" devrait savoir que sum est pur et qu'il peut être hissé hors de la boucle/carte.

Pour moi, la première priorité est une syntaxe f.(x...) pour broadcast(f, x...) ou map(f, x...) afin que nous puissions nous débarrasser de @vectorize . Après cela, nous pouvons continuer à travailler sur une syntaxe comme over (ou autre) pour abréger les utilisations plus générales de map et les compréhensions.

@stevengj Je ne pense pas que le deuxième exemple fonctionne, car le - ne sera pas diffusé. En supposant A est une matrice, la sortie serait une matrice de matrices à une seule ligne, dont chacune est le log d'un élément de A moins le vecteur des sommes le long de la première dimension. Vous auriez besoin broadcast((x, y)->exp(log(x)-y), A, sum(A, 1)) . Mais je pense qu'avoir une syntaxe concise pour map est utile et n'a pas nécessairement besoin d'être une syntaxe concise pour broadcast également.

Les fonctions qui ont été historiquement vectorisées comme sin continueront-elles à l'être avec la nouvelle syntaxe, ou cela deviendra-t-il obsolète ? Je crains que même la syntaxe f. ne donne l'impression d'être un "gotcha" pour un grand nombre de programmeurs scientifiques qui ne sont pas motivés par des arguments d'élégance conceptuelle.

Mon sentiment est que les fonctions historiquement vectorisées comme sin devraient être obsolètes au profit de sin. , mais elles devraient être obsolètes de manière quasi permanente (au lieu d'être entièrement supprimées dans la version suivante) pour le profit des utilisateurs d'autres langages scientifiques.

Un problème mineur (?) avec f.(args...) : bien que la syntaxe object.(field) soit en grande partie rarement utilisée et puisse probablement être remplacée par getfield(object, field) sans trop de peine, il y a un _beaucoup_ de définitions/références de méthode de la forme Base.(:+)(....) = .... , et il serait pénible de les changer en getfield .

Une solution de contournement serait:

  • Avoir Base.(:+) se transformer en map(Base, :+) comme tous les autres f.(args...) , mais définir une méthode obsolète map(m::Module, s::Symbol) = getfield(m, s) pour la rétrocompatibilité
  • prendre en charge la syntaxe Base.:+ (qui échoue actuellement) et la recommander dans l'avertissement de dépréciation pour Base.(:+)

J'aimerais demander à nouveau - si c'est quelque chose que nous pouvons faire dans la 0.5.0 ? Je pense que c'est important en raison de la dépréciation de nombreux constructeurs vectorisés. Je pensais que ça me conviendrait, mais je trouve map(Int32, a) , au lieu de int32(a) un peu fastidieux.

Est-ce simplement une question de choix de syntaxe à ce stade ?

Est-ce simplement une question de choix de syntaxe à ce stade ?

Je pense que @stevengj a donné de bons arguments en faveur de l'écriture de sin.(x) plutôt que de .sin(x) dans son PR https://github.com/JuliaLang/julia/pull/15032. Je dirais donc que le chemin a été dégagé.

J'avais une réserve sur le fait que nous n'avons pas encore de solution pour généraliser efficacement cette syntaxe aux expressions composées. Mais je pense qu'à ce stade, nous ferions mieux de fusionner cette fonctionnalité qui couvre la plupart des cas d'utilisation plutôt que de laisser cette discussion non résolue indéfiniment.

@JeffBezanson Je ramène le jalon à ce sujet à 0.5.0 afin de l'évoquer lors d'une discussion de triage - principalement pour m'assurer de ne pas oublier.

Est-ce que #15032 fonctionne aussi pour call - par exemple Int32.(x) ?

@ViralBShah , oui. Tout f.(x...) est transformé en map(f, broadcast, x...) au niveau de la syntaxe, quel que soit le type de f .

C'est le principal avantage de . par rapport à quelque chose comme f[x...] , qui est par ailleurs attrayant (et ne nécessiterait aucun changement d'analyseur) mais ne fonctionnerait que pour f::Function . f[x...] se heurte également un peu conceptuellement aux compréhensions de tableau T[...] . Bien que je pense que @StefanKarpinski aime la syntaxe des crochets ?

(Pour prendre un autre exemple, les objets o::PyObject dans PyCall sont appelables, appelant la méthode __call__ de l'objet Python o , mais les mêmes objets peuvent également prendre en charge o[...] indexation. Cela se heurterait un peu à la diffusion f[x...] , mais fonctionnerait bien avec la diffusion o.(x...) .)

call n'existe plus.

(J'aime aussi l'argument de @nalimilan selon lequel f.(x...) fait .( l'analogue de .+ etc.)

Oui, l'analogie ponctuelle est celle que j'aime le mieux aussi. Pouvons-nous aller de l'avant et fusionner?

Le getfield avec un module devrait-il être une véritable dépréciation?

@tkelman , par opposition à quoi ? Cependant, l'avertissement de dépréciation pour Base.(:+) (c'est-à-dire les arguments de symboles littéraux) devrait suggérer Base.:+ , et non getfield . (_Mise à jour_ : nécessite également une dépréciation de la syntaxe pour gérer les définitions de méthode.)

@ViralBShah , y a-t-il eu une décision à ce sujet lors de la discussion de triage de jeudi ? # 15032 est en assez bonne forme pour fusionner, je pense.

Je pense que Viral a raté cette partie de l'appel. J'ai l'impression que plusieurs personnes ont encore des réserves sur l'esthétique de f.(x) et pourraient préférer l'un ou l'autre

  1. un opérateur infixe qui serait plus simple sur le plan conceptuel et dans la mise en œuvre, mais nous n'en avons pas d'ascii disponibles d'après ce que je peux voir. Mon idée précédente de déprécier l'analyse macro de ~ nécessiterait un travail de remplacement dans les packages et il est probablement trop tard pour essayer de le faire dans ce cycle.
  2. ou une nouvelle syntaxe alternative qui facilite la fusion des boucles et l'élimination des temporaires. Aucune autre alternative n'a été implémentée à proximité du niveau de # 15032, il semble donc que nous devrions fusionner cela et l'essayer malgré les réserves restantes.

Oui, j'ai quelques réserves mais je ne vois pas de meilleure option que f.(x) le moment. Cela semble mieux que de choisir un symbole arbitraire comme ~ , et je parie que beaucoup de ceux qui sont habitués à .* (etc.) pourraient même deviner tout de suite ce que cela signifie.

Une chose que j'aimerais avoir une meilleure idée est de savoir si les gens sont d'accord pour _remplacer_ les définitions vectorisées existantes par .( . Si les gens ne l'aiment pas assez pour faire le remplacement, j'hésiterais davantage.

En tant qu'utilisateur qui se cache sur cette discussion, j'aimerais beaucoup l'utiliser pour remplacer mon code vectorisé existant.

J'utilise largement la vectorisation dans Julia pour la lisibilité car les boucles sont rapides. J'aime donc beaucoup l'utiliser pour exp, sin, etc., comme cela a été mentionné précédemment. Comme j'utiliserai déjà .^, .* dans de telles expressions en ajoutant le point supplémentaire à sin. exp. etc semble vraiment naturel, et encore plus explicite, pour moi ... surtout quand je peux facilement intégrer mes propres fonctions avec la notation générale au lieu de mélanger sin(x) et map(f, x).

Tout pour dire, en tant qu'utilisateur régulier, j'espère vraiment, vraiment que cela fusionnera !

J'aime plus la syntaxe fun[vec] proposée que fun.(vec) .
Que pensez-vous de [fun vec] ? C'est comme une compréhension de liste mais avec une variable implicite. Cela pourrait permettre de faire T[fun vec]

Cette syntaxe est gratuite dans Julia 0.4 pour les vecteurs de longueur > 1 :

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

Quelque chose comme [fun over vec] pourrait être transformé au niveau de la syntaxe et peut-être vaut-il la peine de simplifier [fun(x) for x in vec] mais n'est pas plus simple que map(fun,vec) .

Syntaxe similaire à [fun vec] : La syntaxe (fun vec) est libre et {fun vec} est obsolète.

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] a été exclu car il est en conflit avec T[vec] . (fun vec) est essentiellement la syntaxe Scheme, le cas à plusieurs arguments étant vraisemblablement (fun vec1 vec2 ...) ... c'est assez différent de toute autre syntaxe Julia. Ou aviez-vous l'intention (fun vec1, vec2, ...) , ce qui est en conflit avec la syntaxe tuple ? On ne sait pas non plus quel serait l'avantage sur fun.(vecs...) .

De plus, rappelez-vous qu'un objectif principal est d'avoir éventuellement une syntaxe pour remplacer les fonctions @vectorized (afin que nous n'ayons pas un sous-ensemble "béni" de fonctions qui "fonctionnent sur des vecteurs"), et cela signifie que le la syntaxe doit être acceptable/intuitive/pratique pour les personnes habituées aux fonctions vectorisées dans Matlab, Numpy, etc. Il doit également être facilement composable pour des expressions telles que sin(A .+ cos(B[:,1])) . Ces exigences excluent bon nombre des propositions les plus "créatives".

sin.(A .+ cos.(B[:,1])) n'a pas l'air si mal après tout. Cela va nécessiter une bonne documentation. Est-ce f.(x) va être documenté comme .( similaire à .+ ?
Est-ce .+ peut être déprécié en faveur de +. ?

# 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 inclut déjà la documentation, mais toute suggestion supplémentaire est la bienvenue.

.+ continuera à être orthographié .+ . Premièrement, ce placement du point est trop ancré, et il n'y a pas assez à gagner en changeant l'orthographe ici. Deuxièmement, comme @nalimilan l' a souligné, vous pouvez considérer .( comme étant un "opérateur d'appel de fonction vectorisé", et de ce point de vue, la syntaxe est déjà cohérente.

(Une fois que les difficultés avec le calcul de type dans broadcast (#4883) seront résolues, mon espoir est de faire un autre PR pour que a .⧆ b pour n'importe quel opérateur ne soit que du sucre pour un appel à broadcast(⧆, a, b) . De cette façon, nous n'aurons plus besoin d'implémenter explicitement .+ etc. — vous obtiendrez l'opérateur de diffusion automatiquement en définissant simplement + etc. être toujours capable d'implémenter des méthodes spécialisées, par exemple des appels à BLAS, en surchargeant broadcast pour des opérateurs particuliers.)

Il doit également être facilement composable pour des expressions telles que sin(A .+ cos(B[:,1])) .

Est-il possible d'analyser f1.(x, f2.(y .+ z)) comme broadcast((a, b, c)->(f1(a, f2(b + c))), x, y, z) ?

Edit : je vois que c'est déjà mentionné plus haut... dans le commentaire masqué par défaut par @github..

@yuyichao , la fusion de boucles semble être possible si les fonctions sont marquées comme @pure (au moins si les eltypes sont immuables), comme je l'ai commenté dans # 15032, mais c'est une tâche pour le compilateur, pas l'analyseur. (Mais une syntaxe vectorisée comme celle-ci est plus pratique que pour extraire le dernier cycle des boucles internes critiques.)

N'oubliez pas que l'objectif principal ici est d'éliminer le besoin de fonctions @vectorized ; cela nécessite une syntaxe au moins aussi générale, presque aussi pratique et au moins aussi rapide. Il ne nécessite pas de fusion de boucle automatisée, bien qu'il soit agréable d'exposer l'intention broadcast de l'utilisateur au compilateur d'ouvrir la possibilité d'une fusion de boucle à une date ultérieure.

Y a-t-il un inconvénient s'il fait aussi la fusion de boucles?

@yuyichao , la fusion de boucles est un problème beaucoup plus difficile, et il n'est pas toujours possible même de mettre de côté les fonctions non pures (par exemple, voir l'exemple exp(log(A) .- sum(A,1)) @StefanKarpinski ci-dessus). Attendre que cela soit implémenté aura probablement pour conséquence qu'il ne sera _jamais_ implémenté, à mon avis - nous devons le faire progressivement. Commencez par exposer l'intention de l'utilisateur. Si nous pouvons optimiser davantage à l'avenir, tant mieux. Sinon, nous avons toujours un remplacement généralisé pour la poignée de fonctions "vectorisées" disponibles maintenant.

Un autre obstacle est que .+ etc. n'est pas actuellement exposé à l'analyseur en tant qu'opération broadcast ; .+ est juste une autre fonction. Mon plan est de changer cela (faire du sucre .+ pour broadcast(+, ...) ), comme indiqué ci-dessus. Mais encore une fois, il est beaucoup plus facile de progresser si les changements sont incrémentiels.

Ce que je veux dire, c'est qu'il est difficile de faire la fusion de boucles en prouvant que cela est valide, nous pouvons donc laisser l'analyseur effectuer la transformation dans le cadre des schémas. Dans l'exemple ci-dessus, il peut être écrit comme. exp.(log.(A) .- sum(A,1)) et être analysé comme broadcast((x, y)->exp(log(x) - y), A, sum(A, 1)) .

C'est aussi bien si .+ n'appartient pas encore à la même catégorie (tout comme n'importe quel appel de fonction non intégré sera mis dans l'argument) et c'est même bien si nous ne ferons cela (fusion de boucle) que dans un version ultérieure. Je demande principalement s'il est possible d'avoir un tel schéma (c'est-à-dire non ambigu) dans l'analyseur et s'il y a un inconvénient en autorisant une boucle écrite vectorisée et fusionnée de cette façon.

faire une fusion de boucle en prouvant que cela est valide est difficile

Je veux dire que faire cela dans le compilateur est difficile (peut-être pas impossible), d'autant plus que le compilateur doit se pencher sur l'implémentation compliquée de broadcast , à moins que nous n'ayons un cas spécial broadcast dans le compilateur, qui est probablement une mauvaise idée et nous devrions l'éviter si possible...

Peut-être? C'est une idée intéressante, et il ne semble pas impossible de définir la syntaxe .( comme "fusionnante" de cette manière, et de laisser à l'appelant le soin de ne pas l'utiliser pour des fonctions impures. La meilleure chose serait de l'essayer et de voir s'il y a des cas difficiles (je ne vois aucun problème évident pour le moment), mais je suis enclin à le faire après le PR "sans fusion".

Je suis enclin à le faire après le PR "sans fusion".

Totalement d'accord, d'autant plus que .+ n'est pas géré de toute façon.

Je ne veux pas faire dérailler cela, mais la suggestion de @yuyichao m'a donné quelques idées. L'accent est mis ici sur les fonctions qui sont vectorisées, mais cela me semble toujours un peu déplacé - la vraie question est de savoir sur quelles variables vectoriser, ce qui détermine complètement la forme du résultat. C'est pourquoi j'ai été enclin à marquer des arguments pour la vectorisation, plutôt que de marquer des fonctions pour la vectorisation. Le marquage des arguments permet également des fonctions qui vectorisent sur un argument mais pas sur un autre. Cela dit, nous pouvons avoir les deux et ce PR a pour objectif immédiat de remplacer les fonctions vectorisées intégrées.

@StefanKarpinski , lorsque vous appelez f.(args...) ou broadcast(f, args...) , il vectorise sur _tous_ les arguments. (A cet effet, rappelez-vous que les scalaires sont traités comme des tableaux à 0 dimension.) Dans la suggestion de @yuyichao de f.(args...) = _fused broadcast syntax_ (que j'aime de plus en plus), je pense que la fusion serait " stop" à toute expression qui n'est pas func.(args...) (pour inclure .+ etc. dans le futur).

Ainsi, par exemple, sin.(x .+ cos.(x .^ sum(x.^2))) se transformerait (en julia-syntax.scm ) en broadcast((x, _s_) -> sin(x + cos(x^_s_)), x, sum(broacast(^, x, 2))) . Notez que la fonction sum serait une "frontière de fusion". L'appelant serait responsable de ne pas utiliser f.(args...) dans les cas où la fusion gâcherait les effets secondaires.

Avez-vous un exemple en tête où cela ne suffirait pas ?

que j'aime de plus en plus

Je suis content que vous l'aimez. =)

Juste une autre extension qui n'appartient probablement pas au même tour, il pourrait être possible d'utiliser .= , .*= ou similaire pour résoudre le problème d'affectation en place (en le distinguant du affectation normale)

Oui, le manque de fusion pour d'autres opérations était ma principale objection à .+= etc. dans #7052, mais je pense que cela serait résolu en faisant fusionner .= avec d'autres appels func.(args...) . Ou fusionnez simplement x[:] = ... .

:thumbsup: Il y a deux concepts entassés dans cette discussion qui sont en fait assez orthogonaux :
le matlab'y "fused broadcast operations" ou x .* y .+ z and apl'y "maps on products and zips" like f[product(I,J)...] and f[zip(I,J)...] . Le fait de se parler pourrait aussi avoir à voir avec cela.

@mschauer , f.(I, J) est déjà (dans #15032) équivalent à map(x -> f(x...), zip(I, J) si I et J ont la même forme. Et si I est un vecteur ligne et J est un vecteur colonne ou vice versa, alors broadcast est effectivement mappé sur l'ensemble de produits (ou vous pouvez faire f.(I, J') s'ils sont tous les deux des tableaux 1d). Je ne comprends donc pas pourquoi vous pensez que les concepts sont "assez orthogonaux".

Orthogonal n'était pas le bon mot, ils sont juste assez différents pour coexister.

Le fait est, cependant, que nous n'avons pas besoin de syntaxes distinctes pour les deux cas. func.(args...) peut prendre en charge les deux.

Une fois qu'un membre du triumvirat (Stefan, Jeff, Viral) a fusionné #15032 (qui, je pense, est prêt pour la fusion), je fermerai ceci et déposerai un problème de feuille de route pour décrire les modifications proposées restantes : correction du calcul du type de diffusion, obsolescence @vectorize , transformez .op en sucre de diffusion, ajoutez une "fusion de diffusion" au niveau de la syntaxe et fusionnez enfin avec l'affectation sur place. Les deux derniers ne feront probablement pas partie de la version 0.5.

Hé, je suis très heureux et reconnaissant à propos de 15032. Je ne serais pas dédaigneux de la discussion cependant. Par exemple, les vecteurs de vecteurs et d'objets similaires sont encore très difficiles à utiliser dans julia mais peuvent germer comme de la mauvaise herbe à la suite de compréhensions. Une bonne notation implicite non basée sur l'encodage de l'itération dans des dimensions singleton a le potentiel de beaucoup faciliter cela, par exemple avec les itérateurs flexibles et les nouvelles expressions de générateur.

Je pense que cela peut être fermé maintenant en faveur de #16285.

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