Julia: Chaînage des fonctions

Créé le 27 janv. 2014  ·  232Commentaires  ·  Source: JuliaLang/julia

Serait-il possible d'autoriser l'appel de n'importe quelle fonction sur Any afin que la valeur soit transmise à la fonction en tant que premier paramètre et que les paramètres passés à l'appel de fonction sur la valeur soient ajoutés par la suite?
ex.

sum(a::Int, b::Int) -> a + b

a = 1
sum(1, 2) # = 3
a.sum(2) # = 3 or
1.sum(2) # = 3

Est-il possible d'indiquer de manière déterministe ce qu'une fonction retournera pour éviter les exceptions d'exécution?

Commentaire le plus utile

Donc, notre liste actuelle de divers efforts, etc.
Je pense que cela vaut la peine que les gens les vérifient, (idéalement avant d'émettre des opinions, mais sans)
ils sont tous légèrement différents.
(J'essaye de commander par ordre chronologique).

Paquets

Prototypes sans emballage

En relation:


Peut-être que cela devrait être modifié dans l'un des principaux messages.

mis à jour: 2020-04-20

Tous les 232 commentaires

La syntaxe . est très utile, nous n'allons donc pas en faire juste un synonyme d'appel de fonction. Je ne comprends pas l'avantage de 1.sum(2) sur sum(1,2) . Pour moi, cela semble confondre les choses.

La question des exceptions est-elle une question distincte? Je pense que la réponse est non, à part envelopper un corps de fonction dans try..catch.

L'exemple 1.sum (2) est trivial (je préfère aussi sum (1,2)) mais c'est juste pour démontrer qu'une fonction n'est pas possédée en soi par ce type ex. 1 peut être passé à une fonction avec le premier paramètre étant un Real, pas seulement aux fonctions qui s'attendent à ce que le premier paramètre soit un Int.

Edit: J'ai peut-être mal compris votre commentaire. Les fonctions de points seront utiles lors de l'application de certains modèles de conception tels que le modèle de générateur couramment utilisé pour la configuration. ex.

validate_for(name).required().gt(3) 
# vs 
gt(required(validate_for(name)), 3) 

Les exceptions auxquelles je faisais référence sont dues aux fonctions retournant des résultats non déterministes (ce qui est de toute façon une mauvaise pratique). Un exemple serait d'appeler a.sum (2) .sum (4) où .sum (2) renvoie parfois un String au lieu d'un Int mais .sum (4) attend un Int. Je suppose que le compilateur / runtime est déjà assez intelligent pour évaluer de telles circonstances - ce qui serait le même lors de l'imbrication de la fonction sum (sum (1, 2), 4) - mais la demande de fonctionnalité nécessiterait d'étendre ladite fonctionnalité pour appliquer des contraintes de type sur fonctions de points.

L'un des cas d'utilisation que les gens semblent aimer est "l'interface fluide". C'est parfois bien dans les API OOP lorsque les méthodes renvoient l'objet, vous pouvez donc faire des choses comme some_obj.move(4, 5).scale(10).display()

Pour moi, je pense que cela est mieux exprimé en composition de fonctions, mais le |> ne fonctionne pas avec des arguments à moins que vous n'utilisiez anon. fonctions, par exemple some_obj |> x -> move(x, 4, 5) |> x -> scale(x, 10) |> display , ce qui est assez moche.

Une option pour prendre en charge ce genre de chose serait si |> poussait le LHS comme premier argument du RHS avant l'évaluation, mais alors il ne pouvait pas être implémenté comme une fonction simple comme c'est le cas actuellement.

Une autre option serait une sorte de macro @composed qui ajouterait ce genre de comportement à l'expression suivante

Vous pouvez également transférer la responsabilité de cette prise en charge aux concepteurs de bibliothèques, où ils pourraient définir

function move(obj, x, y)
    # move the object
end

move(x, y) = obj -> move(obj, x, y)

Ainsi, lorsque vous ne fournissez pas d'objet, il effectue une application de fonction partielle (en renvoyant une fonction de 1 argument) que vous pouvez ensuite utiliser dans une chaîne normale |> .

En fait, la définition de |> pourrait probablement être remplacée maintenant par
comportement que vous demandez. Je serais pour ça.

Le lundi 27 janvier 2014, Spencer Russell [email protected]
a écrit:

L'un des cas d'utilisation que les gens semblent aimer est "l'interface fluide". Ses
parfois agréable dans les API OOP lorsque les méthodes retournent l'objet, vous pouvez donc le faire
des choses comme some_obj.move (4, 5) .scale (10) .display ()

Pour moi, je pense que cela s'exprime mieux en tant que composition de fonction, mais
le |> ne fonctionne pas avec les arguments sauf si vous utilisez anon. fonctions, par exemple some_obj
|> x -> move (x, 4, 5) |> x -> scale (x, 10) |> display, ce qui est joli
laid.

Une option pour prendre en charge ce genre de chose serait si |> poussait le LHS comme
le premier argument au RHS avant l'évaluation, mais alors il ne pouvait pas être
mis en œuvre comme une fonction simple comme c'est le cas actuellement.

Une autre option serait une sorte de macro @composed qui ajouterait ceci
sorte de comportement à l'expression suivante

Vous pouvez également transférer la responsabilité de cette prise en charge à la bibliothèque
designers, où ils pourraient définir

fonction déplacer (obj, x, y)
# déplacer l'objet
fin

déplacer (x, y) = obj -> déplacer (obj, x, y)

donc lorsque vous ne fournissez pas d'objet, il effectue une application de fonction partielle
(en renvoyant une fonction de 1 argument) que vous pourrez ensuite utiliser dans un
normal |> chaîne.

-
Répondez directement à cet e-mail ou consultez-le sur Gi tHubhttps: //github.com/JuliaLang/julia/issues/5571#issuecomment -33408448
.

ssfrr j'aime la façon dont tu penses! J'ignorais la composition de la fonction |> . Je vois qu'il y a eu récemment une discussion similaire [https://github.com/JuliaLang/julia/issues/4963].

kmsquire J'aime l'idée d'étendre la composition de la fonction courante pour vous permettre de spécifier des paramètres sur la fonction appelante ex. some_obj |> move(4, 5) |> scale(10) |> display . Le support natif signifierait une fermeture de moins, mais ce que ssfrr a suggéré est un moyen viable pour le moment et comme avantage supplémentaire, il devrait également être compatible avec la fonctionnalité de composition de fonction étendue si elle est implémentée.

Merci pour les réponses rapides :)

En fait, @ssfrr avait raison - il n'est pas possible de l'implémenter comme une simple fonction.

Ce que vous voulez, ce sont des macros de threading (ex. Http://clojuredocs.org/clojure_core/clojure.core/-%3E). Dommage que @ -> @ - >> @ -? >> ne soit pas une syntaxe viable dans Julia.

Ouais, je pensais que les macros infixes seraient un moyen d'implémenter cela. Je ne connais pas assez bien les macros pour savoir quelles sont les limites.

Je pense que cela fonctionne pour la macro de composition de @ssfrr :

Edit: Cela pourrait être un peu plus clair:

import Base.Meta.isexpr
_ispossiblefn(x) = isa(x, Symbol) || isexpr(x, :call)

function _compose(x)
    if !isa(x, Expr)
        x
    elseif isexpr(x, :call) &&    #
        x.args[1] == :(|>) &&     # check for `expr |> fn`
        length(x.args) == 3 &&    # ==> (|>)(expr, fn)
        _ispossiblefn(x.args[3])  #

        f = _compose(x.args[3])
        arg = _compose(x.args[2])
        if isa(f, Symbol)
            Expr(:call, f, arg) 
        else
            insert!(f.args, 2, arg)
            f
        end
    else
        Expr(x.head, [_compose(y) for y in x.args]...)
    end
end

macro compose(x)
    _compose(x)
end
julia> macroexpand(:(<strong i="11">@compose</strong> x |> f |> g(1) |> h('a',"B",d |> c(fred |> names))))
:(h(g(f(x),1),'a',"B",c(d,names(fred))))

Si nous allons avoir cette syntaxe |> , je serais certainement tout pour la rendre plus utile qu'elle ne l'est actuellement. Utiliser juste pour permettre de mettre la fonction à appliquer à droite au lieu de gauche a toujours semblé être un gaspillage colossal de syntaxe.

+1. C'est particulièrement important lorsque vous utilisez Julia pour l'analyse de données, où vous avez généralement des pipelines de transformation de données. En particulier, Pandas en Python est pratique à utiliser car vous pouvez écrire des choses comme df.groupby ("quelque chose"). Aggregate (sum) .std (). Reset_index (), qui est un cauchemar à écrire avec la syntaxe actuelle |> .

: +1: pour cela.

(J'avais déjà pensé en suggérant l'utilisation de l'opérateur infixe .. pour cela ( obj..move(4,5)..scale(10)..display ), mais l'opérateur |> sera bien aussi)

Une autre possibilité consiste à ajouter du sucre syntaxique pour le curry, comme
f(a,~,b) traduisant par x->f(a,x,b) . Alors |> pourrait garder sa signification actuelle.

Oooh, ce serait une très bonne façon de transformer n'importe quelle expression en fonction.

Peut-être quelque chose comme les littéraux de fonction anonyme de Clojure, où #(% + 5) est un raccourci pour x -> x + 5 . Cela se généralise également à plusieurs arguments avec% 1,% 2, etc. donc #(myfunc(2, %1, 5, %2) est un raccourci pour x, y -> myfunc(2, x, 5, y)

Esthétiquement, je ne pense pas que la syntaxe s'intègre très bien dans Julia par ailleurs très lisible, mais j'aime l'idée générale.

Pour utiliser mon exemple ci-dessus (et passer au tilde de

some_obj |> move(~, 4, 5) |> scale(~, 10) |> display

ce qui est plutôt joli.

C'est bien en ce sens que cela ne donne aucun traitement spécial au premier argument. L'inconvénient est qu'utilisé de cette façon, nous prenons un symbole.

C'est peut-être un autre endroit où vous pouvez utiliser une macro, donc la substitution ne se produit que dans le contexte de la macro.

Nous ne pouvons évidemment pas faire cela avec ~ puisque c'est déjà une fonction standard dans Julia. Scala fait cela avec _ , ce que nous pourrions aussi faire, mais il y a un problème important pour déterminer quelle partie de l'expression est la fonction anonyme. Par exemple:

map(f(_,a), v)

Laquelle cela signifie-t-il?

map(f(x->x,a), v)
map(x->f(x,a), v)
x->map(f(x,a), v)

Ce sont toutes des interprétations valables. Je semble me rappeler que Scala utilise les signatures de type des fonctions pour déterminer cela, ce qui me semble regrettable car cela signifie que vous ne pouvez pas vraiment analyser Scala sans connaître les types de tout. Nous ne voulons pas faire cela (et ne pourrions même pas si nous le voulions), il doit donc y avoir une règle purement syntaxique pour déterminer quelle signification est prévue.

Oui, je vois votre point de vue sur l'ambiguïté de la distance à parcourir. Dans Clojure, l'expression entière est enveloppée dans #(...) donc c'est sans ambiguïté.

Dans Julia, est-il idiomatique d'utiliser _ comme valeur indifférente? Comme x, _ = somfunc() si somefunc renvoie deux valeurs et vous ne voulez que la première?

Pour résoudre cela, je pense que nous aurions besoin d'une macro avec une utilisation de type interpolation:

some_obj |> @$(move($, 4, 5)) |> @$(scale($, 10)) |> display

mais encore une fois, je pense que ça devient assez bruyant à ce stade, et je ne pense pas que @$(move($, 4, 5)) nous donne quoi que ce soit sur la syntaxe existante x -> move(x, 4, 5) , qui est IMO à la fois plus jolie et plus explicite.

Je pense que ce serait une bonne application d'une macro infixe. Comme avec # 4498, si une règle quelconque définit les fonctions comme infixe appliqué aux macros également, nous pourrions avoir une macro @-> ou @|> qui aurait le comportement de thread.

Oui, j'aime l'idée de macro infixe, même si un nouvel opérateur pourrait simplement être introduit pour cette utilisation au lieu d'avoir tout un système pour les macros en place. Par exemple,
some_obj ||> move($,4,5) ||> scale($, 10) |> disp
ou peut-être simplement garder |> mais avoir une règle qui
x |> f transforme implicitement en x |> f($) :
some_obj |> scale($,10) |> disp

Les gens, tout a l'air vraiment moche: |> ||> etc.
Jusqu'à présent, j'ai découvert que la syntaxe de Julia était si claire que ces choses discutées ci-dessus ne sont pas si jolies par rapport à autre chose.

Dans Scala, c'est probablement la pire chose - ils ont tellement d'opérateurs comme ::,:, <<, >> + :: et ainsi de suite - cela rend tout code laid et non lisible pour quelqu'un sans quelques mois d'expérience dans l'utilisation la langue.

Désolé d'entendre que vous n'aimez pas les propositions, Anton. Il serait utile que vous fassiez une proposition alternative.

Oh désolé, je n'essaye pas d'être méchant. Et oui - critiques sans propositions
sont inutiles.

Malheureusement, je ne suis pas un scientifique qui construit des langues, donc je ne
savoir quoi proposer ... enfin, sauf faire des méthodes appartenant éventuellement à
objets comme dans certaines langues.

J'aime l'expression "scientifique qui construit des langages" - cela semble beaucoup plus grandiose que les programmeurs numériques en ont marre de Matlab.

Je pense que presque tous les langages ont un moyen d'enchaîner des fonctions - soit par application répétée de . dans les langages OO, soit par une syntaxe spéciale juste à cette fin dans des langages plus fonctionnels (Haskell, Scala, Mathematica, etc.). Ces derniers langages ont également une syntaxe spéciale pour les arguments de fonction anonymes, mais je ne pense pas que Julia va vraiment y aller.

Je réitère le soutien à la proposition de Spencer - x |> f(a) se traduit en f(x, a) , de manière très analogue au fonctionnement des blocs do (et cela renforce un thème commun que le premier argument d'un fonction est privilégiée dans Julia à des fins de sucre syntaxique). x |> f est alors considéré comme un raccourci pour x |> f() . C'est simple, n'introduit aucun nouvel opérateur, gère la grande majorité des cas pour lesquels nous voulons un chaînage de fonctions, est rétrocompatible et correspond aux principes de conception Julia existants.

Je pense aussi que c'est la meilleure proposition ici, le principal problème étant qu'elle semble empêcher de définir |> pour des choses comme la redirection d'E / S ou d'autres objectifs personnalisés.

Juste pour noter, . n'est pas une syntaxe spéciale de chaînage de fonctions, mais cela fonctionne de cette façon si la fonction de gauche renvoie l'objet qu'elle vient de modifier, ce que le développeur de la bibliothèque doit faire intentionnellement.

De manière analogue, dans Julia, un développeur de bibliothèque peut déjà prendre en charge le chaînage avec |> en définissant leurs fonctions de N arguments pour renvoyer une fonction de 1 argument lorsqu'on leur donne N-1 arguments, comme mentionné ici

Cela semblerait cependant poser des problèmes si vous _voulez_ que votre fonction supporte un nombre variable d'arguments, donc avoir un opérateur qui pourrait effectuer le bourrage d'arguments serait bien.

@JeffBezanson , il semble que cet opérateur pourrait être implémenté s'il y avait un moyen de faire des macros d'infixe. Savez-vous s'il y a un problème idéologique avec cela ou si cela n'est tout simplement pas mis en œuvre?

Récemment, ~ été casse spéciale afin de citer ses arguments et ses appels
la macro @~ par défaut. |> pourrait être amené à faire la même chose.

Bien sûr, dans quelques mois, quelqu'un demandera <| pour faire de même ...

Le jeudi 6 février 2014, Spencer Russell [email protected]
a écrit:

Juste pour noter,. n'est pas une syntaxe de chaînage de fonctions spéciale, mais cela arrive
pour travailler de cette façon si la fonction de gauche renvoie l'objet qu'elle
modifié, ce que le développeur de la bibliothèque doit faire
intentionnellement.

De même, dans Julia, un développeur de bibliothèque peut déjà prendre en charge le chaînage
avec |> en définissant leurs fonctions de N arguments pour renvoyer une fonction
de 1 argument lorsqu'on lui donne N-1 arguments, comme mentionné icihttps: //github.com/JuliaLang/julia/issues/5571#issuecomment -33408448

Cela semble poser des problèmes si vous _voulez_ que votre fonction prenne en charge
nombre variable d'arguments, cependant, donc avoir un opérateur qui pourrait effectuer
la farce d'argument serait bien.

@JeffBezanson https://github.com/JeffBezanson , il semble que cela
L'opérateur pourrait être implémenté s'il y avait un moyen de faire des macros d'infixe. Le faites vous
savoir s'il y a un problème idéologique avec cela, ou si cela n'est tout simplement pas mis en œuvre?

-
Répondez directement à cet e-mail ou consultez-le sur Gi tHubhttps: //github.com/JuliaLang/julia/issues/5571#issuecomment -34374347
.

à droite, je ne voudrais certainement pas que ce soit un cas spécial. Le gérer dans la conception de votre API n'est en fait pas si mal, et même la limitation des arguments variables n'est pas trop un problème si vous avez des annotations de type à lever l'ambiguïté.

function move(obj::MyType, x, y, args...)
    # do stuff
    obj
end

move(args...) = obj::MyType -> move(obj, args...)

Je pense que ce comportement pourrait être géré par une macro @composable qui gérerait la 2ème déclaration.

L'idée de macro infixe me plaît dans la situation où elle serait unifiée avec la déclaration de fonctions infixes, ce qui est discuté dans # 4498.

Pourquoi les créateurs de Julia sont-ils si opposés à ce que les objets contiennent leurs propres méthodes? Où pourrais-je en savoir plus sur cette décision? Quelles pensées et quelle théorie sont derrière cette décision?

@meglio un julia-lang . Voir le discours de Stefan et les archives des utilisateurs et dev listes de discussions précédentes sur ce sujet.

Juste en réponse, pour moi, la chose la plus intuitive est de remplacer un espace réservé par le
valeur de l'expression précédente dans la séquence de choses que vous essayez de composer, similaire à la macro as-> clojure. Donc ça:

<strong i="8">@as</strong> _ begin
    3+3
    f(_,y)
    g(_) * h(_,z)
end

serait étendu à:

g(f(3+3,y)) * h(f(3+3,y),z)

Vous pouvez penser à l'expression de la ligne précédente "déroulant" pour remplir le trou de soulignement sur la ligne suivante.

J'ai commencé à dessiner un petit quelque chose comme ce dernier quart dans un combat de procrastination de la semaine de finale.

Nous pourrions également prendre en charge une version en ligne en utilisant |> :

<strong i="19">@as</strong> _ 3+3 |> f(_,y) |> g(_) * h(_,z)

@porterjamesj , j'aime cette idée!

Je suis d'accord; c'est assez sympa et a une généralité attrayante.
Le 7 février 2014 à 15h19, "Kevin Squire" [email protected] a écrit:

@porterjamesj https://github.com/porterjamesj , j'aime cette idée!

Répondez directement à cet e-mail ou consultez-le sur Gi tHubhttps: //github.com/JuliaLang/julia/issues/5571#issuecomment -34497703
.

J'aime l'idée de @porterjamesj non seulement parce que c'est une bouffée d'air frais, mais parce qu'elle semble beaucoup plus flexible que les idées précédentes. Nous ne sommes pas mariés à utiliser uniquement le premier argument, nous avons le libre arbitre du choix de la variable intermédiaire, et cela semble également être quelque chose que nous pouvons implémenter dès maintenant sans avoir à ajouter une nouvelle syntaxe ou des cas spéciaux au langage.

Notez que dans Julia, parce que nous ne faisons pas beaucoup du modèle obj.method(args...) , et que nous faisons plutôt le modèle method(obj, args...) , nous avons tendance à ne pas avoir de méthodes qui retournent les objets sur lesquels elles opèrent pour l'express but du chaînage de méthodes. (C'est ce que fait jQuery , et c'est fantastique en javascript). Donc, nous n'économisons pas autant de frappe ici, mais dans le but d'avoir des "tuyaux" entre les fonctions, je pense que c'est vraiment sympa.

Étant donné que les -> et ->> clojure ne sont que des cas particuliers de ce qui précède, et assez courants, nous pourrions probablement les implémenter assez facilement aussi. Bien que la question de savoir comment les appeler soit un peu délicate. Peut-être @threadfirst et @threadlast ?

J'aime l'idée que ce soit une macro aussi.

N'est-il pas préférable que l'extension, en suivant l'exemple, ressemble à quelque chose comme

tmp = 3+3; tmp = f(tmp); return h(tmp, z)

pour éviter plusieurs appels à la même opération? (Peut-être que c'était déjà implicite dans l'idée de @porterjamesj )

Une autre suggestion: serait-il possible que la macro élargisse les raccourcis f à f(_) et f(y) à f(_,y) ? Peut-être que ce sera trop, mais je pense que nous avons alors la possibilité d'utiliser l'espace réservé uniquement lorsque cela est nécessaire ... (les raccourcis ne doivent cependant être autorisés que sur les appels de fonction seuls, pas sur des expressions comme g(_) * h(_,z) ci-dessus)

@cdsousa, il est bon d'éviter plusieurs appels. L' implémentation clojure utilise des liaisons let séquentielles pour y parvenir; Je ne suis pas sûr que nous puissions nous en sortir car je n'en sais pas assez sur les performances de notre let .

La macro @as utilise-t-elle des sauts de ligne et => comme points de partage pour décider quelle est l'expression de substitution et ce qui est remplacé?

let performances sont bonnes; maintenant, il peut être aussi rapide qu'une affectation de variable lorsque cela est possible, et aussi assez rapide autrement.

@ssfrr dans mon implémentation de jouet filtre simplement tous les nœuds liés aux sauts de ligne que l'analyseur insère (NB, je ne comprends pas vraiment tout cela, il serait probablement bon d'avoir une documentation sur eux dans le manuel) et réduit ensuite la substitution sur la liste des expressions qui reste. Utiliser let serait mieux, je pense.

@cdsousa :

Une autre suggestion: serait-il possible que la macro élargisse les raccourcis f à f(_) et f(y) à f(_,y)

f à f(_) sens pour moi. Pour le second, je suis d'avis qu'il est préférable de spécifier explicitement l'emplacement, car des gens raisonnables pourraient affirmer que f(_,y) ou f(y,_) est plus naturel.

Étant donné que les -> et ->> clojure ne sont que des cas particuliers de ce qui précède, et assez courants, nous pourrions probablement les implémenter assez facilement aussi. Bien que la question de savoir comment les appeler soit un peu délicate. Peut-être @threadfirst et @threadlast ?

Je pense que la spécification de l'emplacement explicite avec f(_,y...) ou f(y..., _) permet au code d'être tout à fait compréhensible. Bien que la syntaxe supplémentaire (et les opérateurs) aient du sens dans Clojure, nous n'avons pas vraiment d'opérateurs supplémentaires disponibles, et je pense que les macros supplémentaires rendraient généralement le code moins clair.

La macro @as utilise-t-elle des sauts de ligne et => comme points de partage pour décider quelle est l'expression de substitution et ce qui est substitué?

Je pense qu'il est plus naturel d'utiliser |> comme point de partage, car il est déjà utilisé pour le pipelining

Juste pour que vous le sachiez, il existe une implémentation de la macro de threading dans Lazy.jl , qui vous permettrait d'écrire, par exemple:

@>> range() map(x->x^2) filter(iseven)

Du côté positif, cela ne nécessite aucun changement de langue, mais cela devient un peu moche si vous souhaitez utiliser plus d'une ligne.

Je pourrais également implémenter @as> dans Lazy.jl s'il y a de l'intérêt. Lazy.jl a maintenant une macro @as , aussi.

Vous pouvez également faire quelque chose comme ceci (en utilisant une syntaxe de type Haskell) avec Monads.jl (note: il doit être mis à jour pour utiliser la syntaxe Julia actuelle). Mais je soupçonne qu'une version spécialisée pour le thread d'argumentation devrait être capable d'éviter les pièges de performance de l'approche générale.

Lazy.jl ressemble à un très bon package et est activement maintenu. Y a-t-il une raison impérieuse pour laquelle cela doit être dans Base?

Comment le chaînage de fonctions fonctionnera-t-il avec les fonctions renvoyant plusieurs valeurs?
Quel serait le résultat du chaînage par exemple:

function foo(a,b)
    a+b, a*b   # x,y respectively
end

et bar(x,z,y) = x * z - y être?

N'aurait-il pas besoin d'une syntaxe comme bar(_1,z,_2) ?

Jeter dans un autre exemple:

data = [2.255, 3.755, 6.888, 7.999, 9.001]

La manière propre d'écrire: log(sum(round(data))) est data|>round|>sum|>log
Mais si nous voulions faire un journal de base 2 et arrondir à 3 décimales,
alors: on ne peut utiliser que la première forme:
log(2,sum(round(data,3)))

Mais idéalement, nous aimerions pouvoir faire:
data|>round(_,3)|>sum|>log(2,_)
(ou similaire)

J'ai fait un prototype pour savoir comment je suggère que cela fonctionne.
https://github.com/oxinabox/Pipe.jl

Cela ne résout pas le point de @gregid , mais je travaille là-dessus maintenant.
Il ne gère pas non plus la nécessité d'élargir les arguments

Il est similaire aux macros de threads Lazy.jl de @ one-more-minute mais conserve le symbole |> pour plus de lisibilité (préférence personnelle).

Je vais lentement en faire un paquet, peut-être, à un moment donné

Une autre option est:

data |>   x -> round(x,2)  |> sum |>  x -> log(2,x)

Bien que plus longue que log(2,sum(round(data,2))) cette notation facilite parfois la lisibilité.

@shashi ce n'est pas mal, je n'ai pas pensé à ça,
Je pense généralement trop verbeux pour être facilement lisible

https://github.com/oxinabox/Pipe.jl Résout maintenant le problème de
Bien que si vous demandez à la fois _[1] et _[2] il le fait en effectuant plusieurs appels à la substitution
Ce dont je ne suis pas certain est le comportement le plus souhaitable.

En tant qu'étranger, je pense que l'exploitant du pipeline gagnerait à adapter le traitement de F #.
Certes, F # a le curry, mais un peu de magie pourrait peut-être être fait sur le back-end pour ne pas l'exiger. Comme, dans la mise en œuvre de l'opérateur, et non dans le langage de base.

Cela ferait [1:10] |> map(e -> e^2) résultat en [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] .

En regardant en arrière, @ssfrr y a fait allusion, mais l'argument obj dans leur exemple serait automatiquement donné à map comme deuxième argument dans mon exemple, évitant ainsi aux programmeurs d'avoir à définir leurs fonctions à soutiens le.

Que proposez-vous que cela signifie?

Le 5 juin 2015 à 17:22, H-225 [email protected] a écrit:

En tant qu'externe, je pense que l'une des meilleures façons de le faire serait d'adapter le traitement de F #.
Certes, F # a le curry, mais un peu de magie pourrait peut-être être fait sur le back-end pour ne pas l'exiger. Comme, dans la mise en œuvre de l'opérateur, et non dans le langage de base.

Cela rendrait [1:10] |> map (e -> e ^ 2) [1, 4, 9, 16, 25, 36, 49, 64, 81, 100].

Personnellement, je pense que c'est beau et clair sans être trop bavard.

Évidemment, on pourrait écrire result = map (sqr, [1:10]), mais pourquoi ont-ils l'opérateur de pipeline?
Peut-être qu'il me manque quelque chose?

-
Répondez directement à cet e-mail ou affichez-le sur GitHub.

@StefanKarpinski
Fondamentalement, faites travailler l'opérateur comme:

  • x |> y(f) = y(x, f)
  • x |> y(f) = y(f, x)

Peut-être avez-vous un modèle d'interface sur lequel toute fonction à utiliser avec l'opérateur prend les données pour fonctionner comme premier ou dernier argument, selon lequel des éléments ci-dessus est sélectionné pour être ce modèle.
Ainsi, pour la fonction map par exemple, map serait soit map(func, data) ou map(data, func) .

Est-ce que c'est plus clair?

Lazy.jl ressemble à un très bon package et est activement maintenu. Y a-t-il une raison impérieuse pour laquelle cela doit être dans Base?

Je pense que c'est là la question importante.

La raison pour laquelle cela peut être souhaitable en base est 2 fois:

1.) Nous pouvons vouloir encourager le pipelining comme étant la voie julienne - des arguments peuvent être avancés pour dire qu'il est plus lisible
2.) des choses comme Lazy.jl, FunctionalData.jl et mon propre Pipe.jl nécessitent une macro pour envelopper l'expression sur laquelle elle doit agir - ce qui la rend moins lisible.

Je pense que la réponse réside peut-être dans le fait d'avoir des macros Infix.
Et définir |> comme tel.

Je ne suis pas certain que |>, (ou leur cousin le bloc do) appartienne au noyau du tout.
Mais les outils n'existent pas pour les définir en dehors de l'analyseur.

La possibilité d'avoir ce genre de syntaxe de pipelining semble très agréable. Cela pourrait-il être ajouté à Base, c'est- x |> y(f) = y(f, x) dire

Après avoir examiné le code qui utilise les différentes implémentations de ceci dans des packages, je le trouve personnellement illisible et très peu julien. Le jeu de mots de pipeline de gauche à droite n'aide pas la lisibilité, il fait simplement ressortir votre code comme étant à l'envers du reste du code parfaitement normal qui utilise des parenthèses pour l'évaluation des fonctions. Je préfère décourager une syntaxe qui mène à 2 styles différents où le code écrit dans l'un ou l'autre style semble à l'envers et en arrière par rapport au code écrit dans l'autre. Pourquoi ne pas se contenter de la syntaxe parfaitement bonne que nous avons déjà et encourager à rendre les choses plus uniformes?

@tkelman
Personnellement, je le vois d'un point de vue quelque peu utilitaire.
Certes, peut-être que si vous faites quelque chose de simple, ce n'est pas nécessaire, mais si vous écrivez une fonction, par exemple, cela fait quelque chose d'assez compliqué, ou de longue haleine, (par exemple: manipulation de données), alors je pense que c'est là que la syntaxe du pipeline brille.

Je comprends ce que vous voulez dire cependant; ce serait plus uniforme si vous aviez une syntaxe d'appel de fonction pour tout. Personnellement, je pense qu'il vaut mieux faciliter l'écriture de code [compliqué] qui peut être facilement compris. Certes, vous devez apprendre la syntaxe et ce qu'elle signifie, mais, à mon humble avis, |> n'est pas plus difficile à comprendre que comment appeler une fonction.

@tkelman Je le regarderais d'un point de vue différent. De toute évidence, il y a des gens qui préfèrent ce style de programmation. Je peux voir que vous voudriez peut-être avoir un style cohérent pour le code source de Base, mais il ne s'agit que d'ajouter le support de l'analyseur pour leur style préféré de programmation _leurs_ applications Julia. Les juliens veulent-ils vraiment essayer de dicter ou d'étouffer quelque chose que les autres trouvent bénéfique?
J'ai trouvé le pipelining ensemble très utile sous Unix, donc même si je n'ai jamais utilisé un langage de programmation qui l'active dans le langage, je lui donnerais au moins le bénéfice du doute.

Nous avons |> comme opérateur de tuyauterie de fonction, mais il y a des limitations d'implémentation à la façon dont cela se fait actuellement qui le rendent assez lent pour le moment.

La tuyauterie est excellente dans un shell Unix où tout prend du texte et du texte. Avec des types plus compliqués et de multiples entrées et sorties, ce n'est pas aussi clair. Nous avons donc deux syntaxes, mais l'une a beaucoup moins de sens dans le cas MIMO. La prise en charge de l'analyseur pour les styles de programmation alternatifs ou DSL n'est généralement pas nécessaire car nous avons de puissantes macros.

OK, merci, je passais par le commentaire de @oxinabox :

Mais les outils n'existent pas pour les définir en dehors de l'analyseur.

A-t-on compris ce qui serait fait pour supprimer les limitations de mise en œuvre auxquelles vous avez fait allusion?

Certaines des suggestions précédentes pourraient être implémentées en faisant |> analyser ses arguments comme une macro plutôt que comme une fonction. L'ancienne signification de tuyauterie d'objet de commande de |> a été obsolète, donc cela pourrait en fait être libéré pour faire quelque chose de différent avec, come 0.5-dev.

Cependant, ce choix me rappelle un peu l'analyse spéciale de ~ qui me semble être une erreur pour des raisons que j'ai exposées ailleurs.

L'analyse de ~ est juste insensée, c'est une fonction de base. En utilisant _ , _1 , _2 , semble plus raisonnable (surtout si vous augmentez si ces variables sont définies ailleurs dans la portée). Jusqu'à ce que nous ayons des fonctions anonymes plus efficaces, cela semble ne pas fonctionner ...

implémenté en faisant |> analyser ses arguments comme une macro plutôt que comme une fonction

À moins que vous ne le fassiez!

L'analyse de ~ est juste insensée, c'est une fonction de base

C'est un opérateur unaire pour la version bit à bit. Infix binary ~ analyse comme une macro, réf https://github.com/JuliaLang/julia/issues/4882 , ce qui, je pense, est une utilisation étrange d'un opérateur ascii (https://github.com/ JuliaLang / julia / pull / 11102 # issueecomment-98477891).

@tkelman

Nous avons donc deux syntaxes, mais l'une a beaucoup moins de sens dans le cas MIMO.

3 Syntaxes. En quelque sorte.
Pipe in, appel de fonction normal et Do-blocks.
Débattable même 4, puisque les macros utilisent également une convention différente.


Pour moi,
l'ordre de lecture (c'est-à-dire de gauche à droite) == l'ordre d'application, rend, pour les chaînes de fonctions SISO, beaucoup plus clair.

Je fais beaucoup de code comme (En utilisant iterators.jl et pipe.jl):

  • loaddata(filename) |> filter(s-> 2<=length(s)<=15, _) |> take!(150,_) |> map(eval_embedding, _)
  • results |> get_error_rate(desired_results, _) |> round(_,2)

Pour SISO, c'est mieux (pour ma préférence personnelle), pour MIMO ce n'est pas le cas.

Julia semble avoir déjà décidé qu'il existe plusieurs façons correctes de faire les choses.
Ce qui, je ne suis pas sûr à 100%, est une bonne chose.

Comme je l'ai dit, j'aimerais un peu que les blocs Pipe and Do sortent de la langue principale.

Les blocs Do ont pas mal de cas d'utilisation très utiles, mais cela m'a un peu ennuyé qu'ils doivent utiliser la première entrée comme fonction, ne cadrent pas toujours tout à fait correctement avec la philosophie de répartition multiple (et les pandas / UFCS de style D avec postfix data.map(f).sum() , je sais que c'est populaire mais je ne pense pas qu'il puisse être combiné efficacement avec une distribution multiple).

Le piping peut probablement être abandonné assez tôt, et laissé aux packages à utiliser dans les DSL comme votre Pipe.jl.

Julia semble avoir déjà décidé qu'il existe plusieurs façons correctes de faire les choses.
Ce qui, je ne suis pas sûr à 100%, est une bonne chose.

C'est lié à la question de savoir si nous pouvons ou non appliquer rigoureusement un guide de style à l'échelle de la communauté. Jusqu'à présent, nous n'avons pas fait grand-chose ici, mais pour l'interopérabilité, la cohérence et la lisibilité des paquets à long terme, je pense que cela deviendra de plus en plus important à mesure que la communauté grandira. Si vous êtes la seule personne à lire votre code, devenez fou et faites ce que vous voulez. Si ce n'est pas le cas, il est utile de réduire légèrement (à votre avis) la lisibilité pour des raisons d'uniformité.

@tkelman @oxinabox
Je n'ai pas encore trouvé de raison claire pour laquelle il ne devrait pas être inclus dans la langue, ni même dans les packages "core". [par exemple: Base]
Personnellement, je pense que faire de |> une macro pourrait être la réponse.
Quelque chose comme ça peut-être? (Je ne suis pas un maître programmeur Julia!)

macro (|>) (x, y::Union(Symbol, Expr))
    if isa(y, Symbol)
        y = Expr(:call, y) # assumes y is callable
    end
    push!(y.args, x)
    return eval(y)
end

Sous Julia v0.3.9, je n'ai pas pu le définir deux fois - une fois avec un symbole et une fois avec une expression; ma compréhension [limitée] de Union est qu'il y a une perte de performance à son utilisation, donc je suppose que ce serait quelque chose à rectifier dans mon exemple de code de jouet.

Bien sûr, il y a un problème avec la syntaxe d'utilisation pour cela.
Par exemple, pour exécuter l'équivalent de log(2, 10) , vous devez écrire @|> 10 log(2) , ce qui n'est pas souhaitable ici.
Je crois comprendre que vous devez être capable de marquer d'une manière ou d'une autre les fonctions / macros comme "infixables", pour ainsi dire, de sorte que vous puissiez alors l'écrire ainsi: 10 |> log(2) . (Correct si faux!)
Exemple artificiel, je sais. Je ne peux pas penser à un bon pour le moment! =)

Il convient également de souligner un domaine que je n'ai pas couvert dans mon exemple ...
Donc par exemple:

julia> for e in ([1:10], [11:20] |> zip) println(e) end
(1,11)
(2,12)
(3,13)
(4,14)
(5,15)
(6,16)
(7,17)
(8,18)
(9,19)
(10,20)

Encore une fois - exemple artificiel, mais j'espère que vous comprenez!
J'ai fait du violon, mais au moment d'écrire ceci, je ne savais pas comment mettre en œuvre cela moi-même.

Veuillez consulter https://github.com/JuliaLang/julia/issues/554#issuecomment -110091527 et # 11608.

Le 9 juin 2015, à 21h37, H-225 [email protected] a écrit:

Je n'ai pas encore trouvé de raison claire pour laquelle il ne devrait pas être inclus dans la langue

C'est la mauvaise position mentale pour la conception du langage de programmation. La question doit par "pourquoi?" plutôt que "pourquoi pas?" Chaque fonctionnalité a besoin d'une raison convaincante pour son inclusion, et même avec une bonne raison, vous devriez réfléchir longuement avant d'ajouter quoi que ce soit. Pouvez-vous vivre sans? Existe-t-il une manière différente d'accomplir la même chose? Existe-t-il une variante différente de l'entité qui serait meilleure et plus générale ou plus orthogonale aux entités existantes? Je ne dis pas que cette idée particulière ne pourrait pas arriver, mais il doit y avoir une bien meilleure justification que "pourquoi pas?" avec quelques exemples qui ne sont pas meilleurs que la syntaxe normale.

La question doit par "pourquoi?" plutôt que "pourquoi pas?"

+ 1_000_000

En effet.
Voir cet article de blog assez connu:
Chaque fonctionnalité commence par -100 points.
Il doit faire une grande amélioration pour valoir la peine d'être ajouté à la langue.

FWIW, Pyret (http://www.pyret.org/) est passé par cette discussion exacte il y a quelques mois. Le langage prend en charge une notation «boulet de canon» qui fonctionnait à l'origine de la même manière que les gens proposent avec |> . Dans Pyret,

[list: 1, 2, 3, 5] ^ map(add-one) ^ filter(is-prime) ^ sum() ^ ...

Ainsi, la notation du boulet de canon a décidé d'ajouter des arguments aux fonctions.

Il n'a pas fallu longtemps avant qu'ils décident que cette syntaxe était trop déroutante. Pourquoi sum() est-il appelé sans aucun argument? etc. Finalement, ils ont opté pour une alternative élégante au curry:

[list: 1, 2, 3, 5] ^ map(_, add-one) ^ filter(_, is-prime) ^ sum() ^ ...

Cela a l'avantage d'être plus explicite et de simplifier l'opérateur ^ en une fonction simple.

Oui, cela me semble beaucoup plus raisonnable. Il est également plus flexible que le curry.

@StefanKarpinski Je suis un peu confus. Vouliez-vous dire plus flexible que le chaînage (pas le curry)? Après tout, la solution de Pyret était d'utiliser simplement le curry, qui est plus général que le chaînage.

Peut-être que si nous modifions un peu la syntaxe |> (je ne sais vraiment pas à quel point c'est difficile à implémenter, peut-être que cela entre en conflit avec | et > ), nous pourrait définir quelque chose de flexible et lisible.

Définir quelque chose comme

foo(x,y) = (y,x)
bar(x,y) = x*y

Nous aurions:

randint(10) |_> log(_,2) |> sum 
(1,2) |_,x>  foo(_,x)   |x,_>   bar(_,2) |_> round(_, 2) |> sum |_> log(_, 2)

En d'autres termes, nous aurions un opérateur comme |a,b,c,d>a , b , c et d obtiendraient les valeurs renvoyées de la dernière expression (dans l'ordre) et utilisez-la dans des espaces réservés à l'intérieur de la suivante.

S'il n'y a pas de variables à l'intérieur de |> cela fonctionnerait comme cela fonctionne maintenant. Nous pourrions également définir une nouvelle norme: f(x) |> g(_, 1) obtiendrait toutes les valeurs renvoyées par f(x) et les associerait à l'espace réservé _ .

@samuela , ce que je voulais dire, c'est qu'avec le curry, vous ne pouvez omettre que les arguments de fin, alors qu'avec l'approche _ , vous pouvez omettre tous les arguments et obtenir une fonction anonyme. Ie donné f(x,y) avec curry, vous pouvez faire f(x) pour obtenir une fonction qui fait y -> f(x,y) , mais avec des traits de soulignement, vous pouvez faire f(x,_) pour la même chose mais aussi faites f(_,y) pour obtenir x -> f(x,y) .

Bien que j'aime la syntaxe des traits de soulignement, je ne suis toujours pas satisfait de la réponse proposée à la question de savoir quelle part de l'expression environnante elle «capture».

que faites-vous si une fonction renvoie plusieurs résultats? Devrait-il passer un tuple à la position _? Ou pourrait-il y avoir une syntaxe pour le diviser à la volée? Peut-être une question stupide, si oui, pardon!

@StefanKarpinski Ah, je vois ce que tu veux dire. D'accord.

@ScottPJones, la réponse évidente est d'autoriser les flèches d'art ASCII:
http://scrambledeggsontoast.github.io/2014/09/28/needle-announce/

@simonbyrne Cela semble encore pire que la programmation dans Fortran IV sur des cartes perforées, comme je l'ai fait dans ma jeunesse mal

@simonbyrne C'est génial. L'implémenter en tant que macro de chaîne serait un projet GSoC étonnant.

Pourquoi sum () est-il appelé sans aucun argument?

Je pense que l'argument implicite est également l'une des choses les plus déroutantes à propos do notation

@simonbyrne Vous ne pensez pas que cela pourrait être fait sans ambiguïté? Si c'est le cas, je pense que c'est quelque chose qui vaut la peine d'être cassé (la notation actuelle do ), si cela peut être rendu plus logique, plus général et cohérent avec le chaînage.

@simonbyrne Oui, je suis totalement d'accord. Je comprends la motivation de la notation actuelle do mais je pense fermement qu'elle ne justifie pas la gymnastique syntaxique.

@samuela concernant la carte (f, _) vs juste la carte (f). Je conviens que certains désugarages magiques seraient déroutants, mais je pense que map (f) est quelque chose qui devrait exister. Cela ne nécessite pas et le sucre ajoute simplement une méthode simple à la cartographie.
par exemple

map(f::Base.Callable) = function(x::Any...) map(f,x...) end

ie map prend une fonction puis retourne une fonction qui fonctionne sur des choses qui sont itérables (plus ou moins).

Plus généralement, je pense que nous devrions nous pencher vers des fonctions qui ont des méthodes "pratiques" supplémentaires, plutôt qu'une sorte de convention selon laquelle |> mappe toujours les données sur le premier argument (ou similaire).

Dans la même veine, il pourrait y avoir un

type Underscore end
_ = Underscore()

et une convention générale selon laquelle les fonctions devraient / pourraient avoir des méthodes qui prennent des traits de soulignement dans certains arguments, puis retournent des fonctions qui prennent moins d'arguments. Je suis moins convaincu que ce serait une bonne idée, car il faudrait ajouter 2 ^ n méthodes pour chaque fonction qui prend n arguments. Mais c'est une approche. Je me demande s'il serait possible de ne pas avoir à ajouter explicitement autant de méthodes, mais plutôt de se connecter à la recherche de méthode, de sorte que si des arguments sont de type Underscore, la fonction appropriée est renvoyée.

Quoi qu'il en soit, je pense vraiment qu'avoir une version de la carte et du filtre qui prend juste un appelable et renvoie un appelable a du sens, la chose avec le soulignement peut ou non être réalisable.

@patrickthebold
J'imagine que x |> map(f, _) => x |> map(f, Underscore()) => x |> map(f, x) , comme vous le proposez, serait le moyen le plus simple d'implémenter map(f, _) , non? - juste avoir _ être une entité spéciale pour laquelle vous programmez?

Cependant, je ne sais pas si cela serait mieux que de le faire automatiquement déduire par Julia - vraisemblablement en utilisant la syntaxe |> - plutôt que d'avoir à le programmer vous-même.

Aussi, en ce qui concerne votre proposition pour map - je l'aime un peu. En effet, pour le |> actuel, ce serait assez pratique. Cependant, j'imagine qu'il serait plus simple de mettre en œuvre l'inférence automatique de x |> map(f, _) => x |> map(f, x) place?

@StefanKarpinski Cela a du sens. J'avais pas pensé à ça comme ça.

Rien de ce que j'ai dit ne serait lié de quelque manière que ce soit à |> . Ce que je voulais dire à propos du _ serait par exemple d'ajouter des méthodes à < tant que telles:

<(_::Underscore, x) = function(z) z < x end
<(x, _::Underscore) = function(z) x < z end

Mais encore une fois, je pense que ce serait pénible à moins qu'il n'y ait un moyen d'ajouter automatiquement les méthodes appropriées.

Encore une fois, la chose avec les traits de soulignement est distincte de l'ajout de la méthode pratique pour mapper comme indiqué ci-dessus. Je pense que les deux devraient exister, sous une forme ou une autre.

@patrickthebold Une telle approche avec un type défini par l'utilisateur pour le trait de soulignement, etc. placerait une charge importante et inutile sur le programmeur lors de l'implémentation des fonctions. Devoir lister les 2 ^ n de

f(_, x, y) = ...
f(x, _, y) = ...
f(_, _, y) = ...
...

serait très ennuyeux, pour ne pas mentionner inélégant.

De plus, votre proposition avec map fournirait, je suppose, une syntaxe de contournement pour map(f) avec des fonctions de base comme map et filter mais en général, elle souffre de la même chose problème de complexité comme l'approche de soulignement manuel. Par exemple, pour func_that_has_a_lot_of_args(a, b, c, d, e) vous devrez passer par le processus exténuant de taper chaque "curry" possible

func_that_has_a_lot_of_args(a, b, c, d, e) = ...
func_that_has_a_lot_of_args(b, c, d, e) = ...
func_that_has_a_lot_of_args(a, b, e) = ...
func_that_has_a_lot_of_args(b, d, e) = ...
func_that_has_a_lot_of_args(a, d) = ...
...

Et même si vous le faisiez, vous seriez toujours confronté à une ambiguïté absurde lors de l'appel de la fonction: Est-ce que func_that_has_a_lot_of_args(x, y, z) référence à la définition où x=a,y=b,z=c ou x=b,y=d,z=e , etc. ? Julia discernerait entre eux avec des informations de type d'exécution, mais pour le programmeur profane lisant le code source, ce serait totalement flou.

Je pense que la meilleure façon de réussir le curry de soulignement est de simplement l'incorporer dans la langue. Ce serait une modification très simple du compilateur après tout. Chaque fois qu'un trait de soulignement apparaît dans une application de fonction, tirez-le simplement pour créer un lambda. J'ai commencé à chercher à mettre en œuvre cela il y a quelques semaines, mais malheureusement, je ne pense pas que j'aurai assez de temps libre dans les prochaines semaines pour y arriver. Pour quelqu'un familier avec le compilateur Julia, cela ne prendrait probablement pas plus d'un après-midi pour que les choses fonctionnent.

@samuela
Pouvez-vous clarifier ce que vous entendez par «tirez-le pour créer un lambda»? - Je suis curieux. Je me suis également demandé comment cela pouvait être mis en œuvre.

@patrickthebold
Ah, je vois. Vraisemblablement, vous pourriez alors utiliser une telle chose comme ceci: filter(_ < 5, [1:10]) => [1:4] ?
Personnellement, je trouverais filter(e -> e < 5, [1:10]) plus facile à lire; plus cohérent - moins de sens caché, bien que je vous l'accorde, il est plus concis.

A moins d'avoir un exemple où ça brille vraiment?

@samuela

De plus, votre proposition avec map fournirait, je suppose, une syntaxe de contournement pour map (f) avec des fonctions de base comme map et filter, mais en général, elle souffre du même problème de complexité que l'approche de soulignement manuel.

Je ne suggérais pas que cela soit fait en général, uniquement pour map et filter , et peut-être quelques autres endroits où cela semble évident. Pour moi, c'est ainsi que map devrait fonctionner: prendre une fonction et retourner une fonction. (à peu près sûr que c'est ce que fait Haskell.)

serait très ennuyeux, pour ne pas mentionner inélégant.

Je pense que nous sommes d'accord là-dessus. J'espère qu'il y aurait un moyen d'ajouter quelque chose au langage pour gérer les invocations de méthode où certains arguments sont de type Underscore. Après réflexion, je pense que cela revient à avoir un caractère spécial qui se développe automatiquement en lambda, ou un type spécial qui se développe automatiquement en lambda. Je ne me sens pas fortement de toute façon. Je peux voir les avantages et les inconvénients des deux approches.

@ H-225 oui le trait de soulignement est juste une commodité syntaxique. Je ne sais pas à quel point c'est courant, mais Scala l'a certainement. Personnellement, j'aime ça, mais je pense que c'est juste une de ces choses de style.

@ H-225 Eh bien, dans ce cas, je pense qu'un exemple convaincant et pertinent serait le chaînage de fonctions. Au lieu d'avoir à écrire

[1, 2, 3, 5]
  |> x -> map(addone, x)
  |> x -> filter(isprime, x)
  |> sum
  |> x -> 3 * x
  |> ...

on pourrait simplement écrire

[1, 2, 3, 5]
  |> map(addone, _)
  |> filter(isprime, _)
  |> sum
  |> 3 * _
  |> ...

Je me retrouve sans le savoir à utiliser cette syntaxe de soulignement (ou une légère variante) constamment dans les langues qui la prennent en charge et je ne réalise à quel point elle est utile lors de la transition pour travailler dans des langues qui ne la prennent pas en charge.

Pour autant que je sache, il existe actuellement au moins 3,5 bibliothèques / approches qui tentent de résoudre ce problème dans Julia: la fonction |> intégrée de Julia, Pipe.jl, Lazy.jl et 0.5 pour la fonction intégrée do de Julia Notation

@samuela si vous souhaitez jouer avec une implémentation de cette idée, vous pouvez essayer FunctionalData.jl, où votre exemple ressemblerait à ceci:

<strong i="7">@p</strong> map [1,2,3,4] addone | filter isprime | sum | times 3 _

La dernière partie montre comment diriger l'entrée vers le deuxième paramètre (la valeur par défaut est l'argument un, auquel cas le _ peut être omis). Commentaires très appréciés!


Edit: ce qui précède est simplement réécrit en:

times(3, sum(filter(map([1,2,3,4],addone), isprime)))

qui utilise FunctionalData.map et filter au lieu de Base.map et filter. La principale différence est l'ordre des arguments, la deuxième différence est la convention d'indexation (voir la documentation). Dans tous les cas, Base.map peut simplement être utilisé en inversant l'ordre des arguments. @p est une règle de réécriture assez simple (de gauche à droite devient interne vers externe, plus prise en charge du curry simple: <strong i="17">@p</strong> map data add 10 | showall devient

showall(map(data, x->add(x,10)))

Hack peut introduire quelque chose comme ceci: https://github.com/facebook/hhvm/issues/6455. Ils utilisent $$ qui est hors de la table pour Julia ( $ est déjà trop surchargé).

FWIW, j'aime vraiment la solution de Hack à cela.

Je l'aime aussi, ma principale réserve étant que j'aimerais toujours une notation lambda terser qui pourrait utiliser _ pour les variables / slots et il serait bon de s'assurer que celles-ci ne sont pas en conflit.

Ne pourrait-on pas utiliser __ ? Quelle est la syntaxe lambda à laquelle vous pensez? _ -> sqrt(_) ?

Bien sûr, nous pourrions. Cette syntaxe fonctionne déjà, il s'agit plus d'une syntaxe qui ne nécessite pas la flèche, de sorte que vous puissiez écrire quelque chose le long des lignes de map(_ + 2, v) , le vrai problème étant de savoir quelle part de l'expression environnante le _ appartient à.

Mathematica n'a-t-il pas un système similaire pour les arguments anonymes? Comment faire
gèrent-ils la portée de la délimitation de ces arguments?
Le mar 3 novembre 2015 à 9 h 09 Stefan Karpinski [email protected]
a écrit:

Bien sûr, nous pourrions. Cette syntaxe fonctionne déjà, il s'agit plus d'une syntaxe qui
n'a pas besoin de la flèche, pour que vous puissiez écrire quelque chose le long des lignes
de la carte (_ + 2, v), le vrai problème étant de savoir quelle
expression à laquelle appartient _.

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

https://reference.wolfram.com/language/tutorial/PureFunctions.html , montrant
le symbole #, c'est ce à quoi je pensais.
Le mar 3 novembre 2015 à 9 h 34, Jonathan Malmaud [email protected] a écrit:

Mathematica n'a-t-il pas un système similaire pour les arguments anonymes? Comment faire
gèrent-ils la portée de la délimitation de ces arguments?
Le mar 3 novembre 2015 à 9 h 09 Stefan Karpinski [email protected]
a écrit:

Bien sûr, nous pourrions. Cette syntaxe fonctionne déjà, il s'agit plus d'une syntaxe qui
n'a pas besoin de la flèche, pour que vous puissiez écrire quelque chose le long des lignes
de la carte (_ + 2, v), le vrai problème étant de savoir quelle
expression à laquelle appartient _.

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

Mathematica utilise & pour le délimiter.

Plutôt que de faire quelque chose d'aussi général qu'une syntaxe lambda plus courte (qui pourrait prendre une expression arbitraire et renvoyer une fonction anonyme), nous pourrions contourner le problème du délimiteur en confinant les expressions acceptables aux appels de fonction et les variables / emplacements acceptables à des paramètres entiers. Cela nous donnerait une syntaxe de curry multi-paramètres très propre à la Open Dyln . Étant donné que _ remplace des paramètres entiers, la syntaxe peut être minimale, intuitive et sans ambiguïté. map(_ + 2, _) se traduirait par x -> map(y -> y + 2, x) . La plupart des expressions d'appel sans fonction que vous voudriez lambdafy seraient probablement plus longues et plus aimables à -> ou do toute façon. Je pense que le compromis entre convivialité et généralité en vaudrait la peine.

@durcan , cela semble prometteur - pouvez-vous expliquer un peu la règle? Pourquoi le premier _ reste-t-il à l'intérieur de l'argument de map alors que le second consomme toute l'expression map ? Je ne suis pas clair sur ce que signifie «confiner les expressions acceptables aux appels de fonction», ni ce que signifie «confiner les variables / emplacements acceptables à des paramètres entiers» ...

Ok, je pense avoir compris la règle, après avoir lu une partie de cette documentation de Dylan, mais je me demande si map(_ + 2, v) fonctionne mais map(2*_ + 2, v) ne fonctionne pas.

Il y a aussi l'affaire très pointilleuse que cela signifie que _ + 2 + _ signifiera (x,y) -> x + 2 + y alors que _ ⊕ 2 ⊕ _ signifiera y -> (x -> x + 2) + y parce que + et * sont les seuls opérateurs qui sont actuellement analysés comme des appels de fonctions multi-arguments au lieu d'opérations associatives par paires. Maintenant, on pourrait soutenir que cette incohérence devrait être corrigée, bien que cela semble impliquer que l '_parser_ ait une opinion sur quels opérateurs sont associatifs et lesquels ne le sont pas, ce qui semble mauvais. Mais je dirais que tout schéma nécessitant de savoir si l'analyseur analyse a + b + c comme un appel de fonction unique ou un appel imbriqué peut être quelque peu discutable. Peut-être que la notation infixe devrait être gérée spécialement? Mais non, cela semble louche aussi.

Oui, vous avez trouvé le compromis. D'une part, les longues chaînes d'opérateurs sont la syntaxe qui pose le plus de problèmes compte tenu de certains de nos choix d'analyse actuels (bien qu'il soit difficile de reprocher à la sémantique d'une fonctionnalité de langage de dépendre de la sémantique actuelle du langage). De l'autre, les longues chaînes d'appels de fonction sont là où il excelle. Par exemple, nous pourrions réécrire votre problème comme suit:

2*_ |> 2+_ |> map(_, v)

Quoi qu'il en soit, je ne pense pas que le problème d'infixe devrait empêcher d'avoir une option libre de délimiteur propre. Cela aiderait vraiment avec la plupart des appels de fonction normaux, ce qui est en quelque sorte le problème en question. Si vous voulez, vous pourriez avoir un délimiteur facultatif pour aider à résoudre cette ambiguïté particulière (ici je vole & pour ce rôle):

_ ⊕ 2 ⊕ _    # y -> (x -> x + 2) + y
_ ⊕ 2 ⊕ _ &  # (y , x) -> x + 2 + y

C'est la meilleure proposition à ce jour, mais je ne suis pas entièrement vendue. Ce qui se passe lorsque les appels de fonction sont explicites est assez clair, mais moins clair lorsque les appels de fonction sont impliqués par la syntaxe infixe.

J'aime penser à cette approche comme un curry plus flexible et généralisé, plutôt que comme un lambda court et sucré (et même dans ce cas, nous pouvons y arriver à peu près avec un délimiteur facultatif). J'adorerais quelque chose de plus parfait, mais sans ajouter plus de bruit symbolique (l'antithèse de ce problème), je ne sais pas comment y arriver.

Ouais, j'aime ça sauf pour le truc infixe. Cette partie peut être réparable.

Eh bien, curry en position infixe pourrait être une erreur de syntaxe:

map(+(*(2, _), 2), v)      # curry is OK syntax, but obviously not what you wanted
map(2*_ + 2, v)            # ERROR: syntax: infix curry requires delimitation
map(2*_ + 2 &, v)          # this means what we want
map(*(2,_) |> +(_,2), v)   # as would this

Cela pourrait aussi être un avertissement, je suppose.

Appeler ce curry me semble déroutant et faux.

Bien sûr, cela ressemble plus à une application de fonction partielle (qui devient éventuellement un lambda argumenté de manière anonyme), je suppose. Nommer de côté, des pensées?

Je pense à quelque chose comme ça:

  • Si _ apparaît seul comme l'un des arguments d'une expression d'appel de fonction, cet appel de fonction est remplacé par une expression de fonction anonyme prenant autant d'arguments que la fonction a _ arguments, dont le corps est le expression originale avec des arguments _ remplacés par des arguments lambda dans l'ordre.
  • Si _ apparaît ailleurs, l'expression environnante jusqu'au niveau de priorité des flèches, mais sans l'inclure _ dans cette expression, dont le corps est l'expression d'origine avec _ instances remplacées par des arguments lambda dans l'ordre.

Exemples:

  • f(_, b)x -> f(x, b)
  • f(a, _)x -> f(a, x)
  • f(_, _)(x, y) -> f(x, y)
  • 2_^2x -> 2x^2
  • 2_^_(x, y) -> 2x^y
  • map(_ + 2, v)map(x -> x + 2, v)
  • map(2_ + 2, v)map(x -> 2x + 2, v)
  • map(abs, _)x -> map(abs, x)
  • map(2_ + 2, _)x -> map(y -> 2y + 2, x)
  • map(2_ - _, v, w)map((x, y) -> 2x - y, v, w)
  • map(2_ - _, v, _)x -> map((y, z) -> 2y - z, v, x)
  • map(2_ - _, _, _)(x, y) -> map((z, w) -> 2z - w, x, y)
  • _x -> x
  • map(_, v)x -> map(x, v)
  • map((_), v)map(x -> x, v)
  • f = _f = x -> x
  • f = 2_f = x -> 2x
  • x -> x^_x -> y -> x^y
  • _ && _(x, y) -> x && y
  • !_ && _(x, y) -> !x && y

Le seul endroit où cela commence à devenir risqué est celui des conditions - ces exemples deviennent un peu étranges.

C'est encore un peu compliqué et sans principes et il y a des cas secondaires, mais je vais quelque part.

Cela me semble être une très mauvaise idée, presque toute la syntaxe de Julia est assez familière si vous avez utilisé d'autres langages de programmation. Les gens qui regardent le sucre de syntaxe comme celui-ci n'auront aucune idée de ce qui se passe pour "l'avantage" de sauvegarder quelques caractères.

Exemples qui me laissent un peu moins heureux:

  • 2v[_]x -> 2v[x] (bon)
  • 2f(_)2*(x -> f(x)) (pas si bien)
  • _ ? "true" : "false"(x -> x) ? "true" : "false"
  • _ ? _ : 0(x -> x) ? (y -> y) : 0

Je pense que quelque chose de plus profond se passe ici - il y a une certaine notion de positions syntaxiques dans lesquelles un objet fonction prend du sens - et vous voulez vous étendre à la plus proche de celles-ci. La position classique qui "veut" un objet fonction est un argument pour une autre fonction, mais le côté droit d'une affectation est un autre endroit où l'on peut dire "vouloir" un objet fonction (ou peut-être qu'il est plus exact de dire que ce serait prendre une fonction pour être le corps d'une fonction).

Peut-être, mais le même argument pourrait être (et a été) fait à propos de la syntaxe do-block, qui, je pense, dans l'ensemble, a été très réussie et utile. Ceci est étroitement lié à une meilleure syntaxe pour la vectorisation. Ce n'est pas non plus sans précédent - Scala utilise _ de la même manière, et Mathematica utilise # même manière.

Je pense que vous pourriez également faire valoir que le choix _unfamiliar_ de
envoi multiple au lieu d'un opérateur point à envoi unique essentiellement
oblige la décision d'avoir une syntaxe succincte pour les arguments pronominaux
récupérer l'ordre SVO avec lequel les gens _sont_ familiers.

Le mar 17 novembre 2015 à 12:09 PM Stefan Karpinski [email protected]
a écrit:

Peut-être, mais le même argument pourrait (et a été) fait sur le do-block
la syntaxe, qui, je pense, dans l'ensemble, a été très réussie et utile.
Ceci est étroitement lié à une meilleure syntaxe pour la vectorisation. Ce n'est pas non plus
sans précédent - Scala utilise _ de manière similaire, et Mathematica utilise #
De même.

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

Cela existe également en C ++ avec plusieurs solutions de bibliothèques dans Boost en particulier, qui utilisent _1, _2, _3 comme arguments (par exemple _1(x, y...) = x , _2(x, y, z...) = y etc), la limitation étant celle de pouvoir call eg fun(_1) for x -> fun(x) , fun doit être explicitement rendu compatible avec la bibliothèque (généralement via un appel de macro, pour que fun accepte un "type lambda "comme paramètre).

J'aimerais vraiment cette notation lambda laconique disponible dans Julia.
En ce qui concerne le problème de 2f(_) désucrage à 2*(x -> f(x)) : serait - il judicieux de modifier les règles le long des lignes de « si la première règle est applicable, à par exemple f(_) , puis re évaluer récursivement les règles avec f(_) jouant le rôle de _ . Cela permettrait aussi par exemple f(g(_))x -> f(g(x)) , avec la "règle des parenthèses" permettant arrêtez-vous au niveau souhaité, par exemple f((g(_)))f(x->g(x)) .

J'aime beaucoup le nom de «notation lambda concise» pour cela. (Bien mieux que le curry).

Je préférerais vraiment l'explicitation de _1 , _2 , _3 si vous passez des lambdas multi-arguments. En général, je trouve souvent que la réutilisation des noms de variables dans la même portée peut être déroutante ... et avoir _ be x et y dans _la même expression_ semble tout simplement déroutant.

J'ai trouvé que la même scala laconique _ -syntax a causé un peu de confusion (voir toutes les utilisations de _ dans scala ).

D'ailleurs, souvent vous voulez faire:

x -> f(x) + g(x)

ou similaire, et je pense que je serais surpris si ce qui suit ne fonctionne pas:

f(_) + g(_)

Vous pouvez également changer l'ordre des arguments:

x, y -> f(y, x)
f(_2, _1)  # can't do with other suggested _ syntax

Je pense que ce serait bien que la syntaxe permette une numérotation explicite des arguments anonymes ( _1 , _2 , _3 ... etc.), Mais le problème principal demeure : quand exactement promouvez-vous une fonction partiellement appliquée à un lambda laconique? Et quel est exactement le corps lambda? J'aurais probablement tort d'être explicite (avec un délimiteur) plutôt que d'utiliser implicitement une sorte de règles de promotion complexes. Qu'est-ce que doit

foo(_1, _1 + _2  + f(_1, v1) + g(_2, v3), _3 * _2, v2) + g(_4, v4) +
 f(_2, v2) + g(_3, v5) + bar(_1, v6)

signifie exactement? En utilisant un délimiteur (j'utiliserai λ ), les choses sont un peu plus claires:

λ(foo(_1, λ(_1 + _2)  + λ(f(_1, v1) + g(_2, v3)), _3 * _2, v2) + g(_4, v4)) + 
λ(f(_2, v2) + g(_3, v5) + bar(_1, v6))

C'est évidemment un MethodError: + has no method matching +(::Function, ::Function) , mais au moins je peux le dire d'après la façon dont il est écrit.

Je pense que @StefanKarpinski pourrait être sur quelque chose quand il a dit qu'il y a quelques positions syntaxiques apparemment évidentes que les expressions peuvent prendre qui impliquent fortement qu'il s'agit de corps de fonction. La priorité de flèche prend en charge un certain nombre d'entre eux, mais il reste encore quelques cas déroutants. Prometteur, mais nécessite certainement une réflexion approfondie.

Il s'agit certainement d'un compromis entre la lacune, la généralité et la lisibilité. Bien sûr, il ne sert à rien d'introduire quelque chose de moins laconique que la notation existante -> . Mais je pense aussi qu'un délimiteur semble utile.

Peut-être pas assez laconique, mais que diriez-vous d'une version préfixe de -> qui capture les arguments _ ? Par exemple

(-> 2f(_) + 1)

Je suppose que la forme du préfixe devrait avoir une priorité assez faible. Cela pourrait en fait permettre de laisser de côté les parenthèses dans certains cas, par exemple

map(->_ + 1, x)

En ce moment, je déconne avec la mise en œuvre de https://github.com/JuliaLang/julia/issues/5571#issuecomment -157424665

En tant que macro, cela transforme toutes ces occurrences dans la ligne.
La partie délicate est la mise en œuvre de la priorité.

Je ne le terminerai probablement pas dans les 12 prochaines heures car il est temps de rentrer à la maison
(peut-être dans les 24 prochains, mais je devrai peut-être m'en aller)
Quoi qu'il en soit, une fois que cela est fait, nous pouvons jouer avec.

Un étrange qui sort de https://github.com/JuliaLang/julia/issues/5571#issuecomment -157424665

  • f(_,_)x,y -> f(x,y) (c'est raisonnable)
  • f(_,2_) → ??

    • f(_,2_)x,y -> f(x,2y) (raisonnable)

    • f(_,2_)x-> f(x,y->2y) (ce que je pense que la règle suggère, et ce que mon prototype produit)

Mais je ne suis pas sûr d'avoir raison.

Voici donc mon prototype.
http://nbviewer.ipython.org/gist/oxinabox/50a1e17cfb232a7d1908

En fait, il échoue définitivement à certains des tests.

Il n'est pas possible d'envisager le bracketing dans la couche AST actuelle - ils sont souvent (toujours?) Déjà résolus.

Il suffit quand même de jouer avec je pense

Quelques règles de magrittr dans R qui pourraient être utiles:

Si une chaîne commence par. , c'est une fonction anonyme:

. %>% `+`(1)

est identique à la fonction (x) x + 1

Il existe deux modes de chaînage:
1) Chaînage en insérant comme premier argument, ainsi qu'à tous les points qui apparaissent.
2) Enchaîner uniquement aux points qui apparaissent.

Le mode par défaut est le mode 1. Cependant, si le point apparaît seul comme argument de la fonction en cours de chaînage, alors magrittr passe en mode 2 pour cette étape de la chaîne.

Alors

2 %>% `-`(1) 

vaut 2-1,

et

1 %>% `-`(2, . )

est également 2-1

Le mode 2 peut également être spécifié en mettant entre parenthèses:

2 %>% { `-`(2, . - 1) }

serait le même que 2 - (2 - 1).

Notez également que le fait de pouvoir basculer intelligemment entre le mode 1 et le mode 2 résout presque complètement le problème auquel Julia n'est pas très cohérente quant à l'argument qui serait probablement enchaîné en première position. J'ai également oublié que pour noter que les crochets peuvent permettre d'évaluer un morceau de code. Voici un exemple du manuel magrittr:

iris%>%
{
n <- échantillon (1:10, taille = 1)
H <- tête (., N)
T <- queue (., N)
rbind (H, T)
}%>%
sommaire

Ce n'est qu'une idée à moitié formée pour le moment, mais je me demande s'il existe un moyen de résoudre les problèmes de «lambda laconique» et de «variable factice» en même temps en modifiant le constructeur Tuple de telle sorte qu'une valeur manquante renvoie un lambda qui renvoie un Tuple au lieu d'un Tuple? Ainsi, (_, 'b', _, 4) renverrait (x, y) -> (x, 'b', y, 4) .

Ensuite, si nous changeons subtilement la sémantique de l'appel de fonction telle que foo(a, b) signifie "applique foo au Tuple (a, b) ou si l'argument est une fonction, alors applique foo au Tuple retourné par la fonction ". Cela rendrait foo(_, b, c)(1) équivalent à apply(foo, ((x) -> (x, b, c))(1)) .

Je pense que cela ne résout toujours pas le problème de la notation infixe, mais personnellement, je serais satisfait des lambdas laconiques qui ne fonctionnent qu'avec des appels de fonction entre parenthèses. Après tout, 1 + _ peut toujours être réécrit +(1, _) si c'est absolument nécessaire.

@jballanc Cependant, la construction de tuple et l'application de fonctions sont deux concepts bien distincts. Du moins, à moins que ma compréhension de la sémantique de Julia ne soit sérieusement imparfaite.

@samuela Ce que je voulais dire par là, c'est que foo(a, b) équivaut à foo((a, b)...) . Autrement dit, les arguments d'une fonction peuvent être considérés conceptuellement comme un Tuple, même si le Tuple n'est jamais construit en pratique.

@J'ai essayé de lire cette discussion, mais c'est trop long pour moi de garder une trace de tout ce qui a été dit - désolé si je répète plus que nécessaire ici.

Je voudrais juste voter pour faire de |> un complément à la "magie" de do . Pour autant que je sache, le moyen le plus simple de le faire serait de laisser cela signifier

3 |> foo == foo(3) # or foo() instead of just foo, but it would be nice if the parentheses were optional
3 |> foo(1) == foo(1, 3)
3 |> foo(1,2) == foo(1,2,3)

En d'autres termes, a |> f(x) fait au _last_ argument ce que f(x) do; a; end fait au _first_. Cela le rendrait immédiatement compatible avec map , filter , all , any et. al., sans ajouter la complexité de la portée des paramètres _ , et étant donné la syntaxe do déjà existante, je ne pense pas que cela impose un fardeau conceptuel déraisonnable aux lecteurs du code.

Ma principale motivation pour utiliser un opérateur de canalisation comme celui-ci est les pipelines de collecte (voir # 15612), qui je pense être une construction incroyablement puissante, et qui gagne du terrain dans de nombreux langages (indiquant que c'est à la fois une fonctionnalité que les gens veulent, et une ils comprendront).

@malmaud Nice! J'aime que cela soit déjà possible: D

Cependant, la différence de lisibilité entre ces deux variantes est vraiment grande:

# from Lazy.jl
@> x g f(y, z)

# if this became a first-class feature of |>
x |> g |> f(y, z)

Je pense que le principal problème de lisibilité est qu'il n'y a pas d'indices visuels pour dire où se trouvent les limites entre les expressions - les espaces dans x g et g f(x, affecteront considérablement le comportement du code, mais l'espace dans f(x, y) ne le sera pas.

Puisque @>> existe déjà, dans quelle mesure est-il possible d'ajouter ce comportement à |> en 0,5?

(Je ne veux pas dépenser trop d'énergie sur la notation de l'opérateur, alors ne rejetez pas cette proposition uniquement sur la question de la notation. Cependant, nous pouvons noter que le "piping" semble être une notion naturelle pour cela (cf. le terme "pipeline de collecte"), et que par exemple F # utilise déjà |> pour cela, bien que là, bien sûr, il ait une sémantique légèrement différente puisque les fonctions F # sont différentes des fonctions Julia.)

Oui, je suis d'accord sur le front de la lisibilité. Il ne serait pas techniquement difficile de faire en sorte que |> se comporte comme vous le décrivez en 0.5, c'est juste une question de conception.

De même, il serait possible de rendre les fonctions d'analyse macro @>> Lazy.jl enchaînées par |> .

Hm. Je vais alors commencer à travailler sur un PR pour Lazy.jl, mais cela ne veut pas dire que je voudrais que ce ne soit pas dans 0.5 :) Je ne pense pas en savoir assez sur l'analyseur Julia et comment changer le comportement de |> pour l'aider, à moins que je ne reçoive un mentorat assez complet.

Je ne pense pas avoir mentionné dans ce fil, mais j'ai un autre package de chaînage, ChainMap.jl. Il se substitue toujours à _ et insère conditionnellement dans le premier argument. Il essaie également d'intégrer la cartographie. Voir https://github.com/bramtayl/ChainMap.jl

Donc, notre liste actuelle de divers efforts, etc.
Je pense que cela vaut la peine que les gens les vérifient, (idéalement avant d'émettre des opinions, mais sans)
ils sont tous légèrement différents.
(J'essaye de commander par ordre chronologique).

Paquets

Prototypes sans emballage

En relation:


Peut-être que cela devrait être modifié dans l'un des principaux messages.

mis à jour: 2020-04-20

Il s'agit plus d'une expérience de système de type qu'une tentative réelle d'implémentation d'une application partielle, mais en voici une étrange: https://gist.github.com/fcard/b48513108a32c13a49a387a3c530f7de

usage:

include("partial_underscore_generated.jl")
using GeneratedPartial

const sub = partialize(-)
sub(_,2)(1) == 1-2
sub(_,_)(1,2) == 1-2
sub(_,__)(1)(2) == 1-2
sub(__,_)(2)(1) == 1-2 #hehehe

# or
<strong i="8">@partialize</strong> 2 Base.:+ # evily inserts methods in + and allows partializations for 2 arguments
(_+2)(1) == 1+2

# fun:
sub(1+_,_)(2,3) == sub(1+2,3)
sub(1+_,__)(2)(3) == sub(1+2,3)
(_(1)+_)(-,1) == -1+1

# lotsafun:
appf(x::Int,y::Int) = x*y
appf(f,x) = f(x)
<strong i="9">@partialize</strong> 2 appf

appf(1+_,3)(2) == appf(1+2,3)
appf(?(1+_),3) == appf(x->(1+x), 3)
appf(?sub(_,2),3) == appf(x->x-2,3) # I made a method *(::typeof(?),::PartialCall), what of it!!?

# wooooooooooooooooooooooooooooooooo
const f = sub
f(_,f(_,f(_,f(_,f(_,f(_,f(_,f(_,f(_,_)))))))))(1,2,3,4,5,6,7,8,9,10) == f(1,f(2,f(3,f(4,f(5,f(6,f(7,f(8,f(9,10)))))))))
f(_,f(__,f(___,f(____,f(_____,f(______,f(_______,f(________,f(_________,__________)))))))))(1)(2)(3)(4)(5)(6)(7)(8)(9)(10) == f(1,f(2,f(3,f(4,f(5,f(6,f(7,f(8,f(9,10)))))))))

# this answers Stefan's concern (which inspired me to make this hack in the first place)
#
#    const pmap = partialize(map)
#    map(f(_,a),   v) == map(x->f(x,a), v)
#    pmap(?f(_,a), v) == map(x->f(x,a), v)
#    pmap(f(_,a),  v) == x->map(f(x,a), v)
#
# it adds a few other issues, of course...


Ce n'est certainement pas une proposition de mise en œuvre, mais je trouve que c'est amusant de jouer avec, et peut-être que quelqu'un peut en tirer une demi-bonne idée.

PS J'ai oublié de mentionner que @partialize fonctionne également avec les littéraux et les plages de tableaux d'entiers:

<strong i="15">@partialize</strong> 2:3 Base.:- # partialized for 2 and 3 arguments!

(_-_-_)(1,2,3) == -4
(_-_+_)(1,2,3) == +2

OK, j'ai pensé à la composition des fonctions, et bien que l'OMI soit clair dans le cas du SISO, je suppose que je pensais à la façon dont j'utilise le MISO de Julia (quasi-MIMO?) Dans de petits blocs de code chaînés.

J'aime le REPL. Beaucoup. C'est mignon, c'est cool, ça te permet d'expérimenter comme MATLAB ou python ou le shell. J'aime prendre le code d'un package ou d'un fichier et le copier-coller dans le REPL, même plusieurs lignes de code. Le REPL évalue cela sur chaque ligne et me montre ce qui se passe.

Il renvoie / définit également ans après chaque expression. Chaque utilisateur de MATLAB le sait (bien qu'à ce stade, c'est un mauvais argument!). La plupart des utilisateurs de Julia l'ont probablement vu / utilisé auparavant. J'utilise ans dans les situations bizarres où je joue avec quelque chose par morceaux, réalisant que je veux ajouter une autre étape à ce que j'ai écrit ci-dessus. Je n'aime pas que son utilisation soit destructrice, donc j'ai tendance à l'éviter lorsque c'est possible, mais chaque proposition ici traite des durées de vie de retour d'une seule étape de composition.

Pour moi, _ être magique n'est que _odd_, mais je comprends que beaucoup de gens peuvent être en désaccord. Donc, si je _veux_ copier-coller le code des packages dans le REPL et le regarder fonctionner, et si je veux une syntaxe qui ne semble pas magique, alors je pourrais proposer:

<strong i="12">@repl_compose</strong> begin
   sin(x)
   ans + 1
   sqrt(ans)
end

Si la fonction renvoie plusieurs sorties, je peux insérer ans[1] , ans[2] , etc. sur la ligne suivante. Il s'adapte parfaitement au modèle de composition à un seul niveau déjà, le modèle MISO de Julia, et il est déjà une syntaxe Julia très standard, mais pas dans des fichiers.

La macro est facile à implémenter - il suffit de convertir Expr(:block, exprs...) en Expr(:block, map(expr -> :(ans = $expr), exprs) (également un let ans au début, et peut-être qu'il pourrait y avoir une version qui rend une fonction anoymous qui prend un entrée ou quelque chose?). Il ne _aurait_ pas vivre dans la base (cependant, le REPL est intégré à Julia, et cela va en quelque sorte avec ça).

Bref, juste mon point de vue! C'était un long fil, que je n'ai pas regardé depuis longtemps!

Il renvoie également / définit ans après chaque expression. Chaque utilisateur de MATLAB le sait (bien qu'à ce stade, c'est un mauvais argument!).

En fait, l'autre argument est que si _ est utilisé dans le chaînage de fonctions, alors le REPL devrait également renvoyer _ plutôt que ans (pour moi, ce serait suffisant pour supprimer la magie").

Il y a un bon nombre de précédents pour utiliser _ comme "valeur it" dans les langues. Bien sûr, cela entre en conflit avec l'idée proposée d'utiliser _ comme nom qui supprime les affectations et pour les lambdas terser.

Je suis presque sûr que cela existe quelque part dans Lazy.jl en tant que <strong i="5">@as</strong> and begin ...

L'idée d'utiliser . pour le chaînage a été écartée au début de la conversation, mais elle a une longue histoire d'utilité et minimiserait la courbe d'apprentissage pour les adoptants d'autres langues. La raison pour laquelle c'est important pour moi est que

type Track
  hit::Array{Hit}
end
type Event
  track::Array{Track}
end

event.track[12].hit[43]

me donne le 43e hit de la 12e piste d'un événement lorsque track et hit sont de simples tableaux, donc

event.getTrack(12).getHit(43)

devrait me donner la même chose s’ils doivent être servis dynamiquement. Je ne veux pas avoir à dire

getHit(getTrack(event, 12), 43)

Cela empire à mesure que vous vous enfoncez. Comme ce sont des fonctions simples, cela rend l'argument plus large que celui du chaînage de fonctions (à la Spark).

J'écris ceci maintenant parce que je viens d'apprendre les traits de Rust , ce qui pourrait être une bonne solution chez Julia pour les mêmes raisons. Comme Julia, Rust a des données uniquement structs (Julia type ), mais ils ont aussi impl pour lier des fonctions au nom d'un struct . Pour autant que je sache, c'est du sucre syntaxique pur, mais cela permet la notation par points que j'ai décrite ci-dessus:

impl Event {
  fn getTrack(&self, num: i32) -> Track {
    self.track[num]
  }
}

impl Track {
  fn getHit(&self, num: i32) -> Track {
    self.track[num]
  }
}

qui dans Julia pourrait être

impl Event
  function getTrack(self::Event, num::Int)
    self.track[num]
  end
end

impl Track
  function getHit(self::Track, num::Int)
    self.hit[num]
  end
end

La syntaxe proposée ci-dessus ne fait aucune interprétation de self : c'est juste un argument de fonction, donc il ne devrait y avoir aucun conflit avec une distribution multiple. Si vous voulez faire une interprétation minimale de self , vous pouvez rendre implicite le type du premier argument, de sorte que l'utilisateur n'ait pas à taper ::Event et ::Track dans chaque fonction, mais une bonne chose à ne pas faire d'interprétation est que les "méthodes statiques" ne sont que des fonctions dans impl qui n'ont pas de self . (Rust les utilise pour des usines new .)

Contrairement à Rust, Julia a une hiérarchie sur types . Il pourrait également avoir une hiérarchie similaire sur impls pour éviter la duplication de code. Une POO standard pourrait être construite en rendant les hiérarchies de données type et de méthode impl exactement les mêmes, mais cette mise en miroir stricte n'est pas nécessaire et n'est pas souhaitable dans certains cas.

Il y a un point collant avec ceci: supposons que j'ai nommé mes fonctions track et hit dans impl , plutôt que getTrack et getHit , afin qu'ils soient en conflit avec les tableaux track et hit dans le type . Alors event.track retournerait-il le tableau ou la fonction? Si vous l'utilisez immédiatement comme fonction, cela pourrait aider à lever l'ambiguïté, mais types peut aussi contenir des objets Function . Peut-être simplement appliquer une règle générale: après le point, vérifiez d'abord le impl , puis vérifiez le type ?

Après réflexion, pour éviter d'avoir deux "packages" pour ce qui est conceptuellement le même objet ( type et impl ), que diriez-vous de ceci:

function Event.getTrack(self, num::Int)
  self.track[num]
end

pour lier la fonction getTrack à des instances de type Event telles que

myEvent.getTrack(12)

renvoie le même bytecode que la fonction appliquée à (myEvent, 12) ?

Ce qui est nouveau, c'est la syntaxe typename-dot-functionname après le mot-clé function et comment il est interprété. Cela permettrait toujours une répartition multiple, un self type Python si le premier argument est le même que le type auquel il est lié (ou laissé implicite, comme ci-dessus), et cela permet une "méthode statique" si le premier argument n'est pas présent ou typé différemment du type auquel il est lié.

@jpivarski Y a-t-il une raison pour laquelle vous pensez que la syntaxe des points (qui, en lisant ce fil, a beaucoup d'inconvénients) est meilleure qu'une autre construction qui permet le chaînage? Je pense toujours que créer quelque chose comme do mais pour le dernier argument , soutenu par une forme de syntaxe de tuyauterie (par exemple |> ) serait la meilleure façon d'avancer:

event |> getTrack(12) |> getHit(43)

La principale raison pour laquelle je peux voir que quelque chose comme l'approche de Rust pourrait être meilleur est qu'il utilise efficacement le côté gauche comme espace de noms pour les fonctions, vous pouvez donc faire des choses comme parser.parse sans entrer en conflit avec Fonction Julia Base.parse . Je serais en faveur de fournir à la fois la proposition Rust et la tuyauterie de style Hack.

@tlycken C'est une syntaxe ambiguë, selon la priorité.
Se souvenir de l'appel Precidence de |> vs peut prêter à confusion, car cela ne donne pas vraiment d'indices.
(Plusieurs des autres options suggérées non plus.)

Considérer

foo(a,b) = a+b
foo(a) = b -> a-b

2 |> foo(10) == 12   #Pipe Precedence > Call Precedence 
2 |> foo(10) == 8     #Pipe Precedence < Call Precedence   

@oxinabox En fait, je ne suggère pas que ce soit "juste" un opérateur régulier, mais plutôt un élément de syntaxe du langage; 2 |> foo(10) desugars à foo(10, 2) peu près de la même manière foo(10) do x; bar(x); end desugars à foo(x -> bar(x), 10) . Cela implique la priorité du tuyau sur la priorité des appels (ce qui, je pense, est ce qui a le plus de sens de toute façon).

Juste au sujet de la syntaxe, . est moins gênant visuellement et certainement plus standard que |> . Je peux écrire une chaîne fluide de fonctions séparées par . sans espaces (un caractère chacun) et n'importe qui pourrait la lire; avec |> , je devrais ajouter des espaces (quatre caractères chacun) et ce serait un speedbump visuel pour la plupart des programmeurs. L'analogie avec le script shell | est bien, mais pas immédiatement reconnaissable à cause du > .

Est-ce que je lis correctement ce fil de discussion que l'argument contre le point est qu'il est ambigu de savoir s'il doit obtenir une donnée membre à partir de type ou une fonction membre à partir de impl (ma première suggestion) ou de la fonction namespace (ma deuxième suggestion)? Dans la deuxième suggestion, les fonctions définies dans l'espace de noms de fonction créé par un type doivent être définies _après_ le type est défini, donc il peut refuser d'éclipser une donnée membre juste là.

Ajouter les deux points d'espace de noms (ma deuxième suggestion) et |> me conviendrait; leur but et leur effet sont assez différents, malgré le fait qu'ils peuvent tous deux être utilisés pour un chaînage fluide. Cependant, |> comme décrit ci-dessus n'est pas complètement symétrique avec do , puisque do nécessite que l'argument qu'il remplisse soit une fonction. Si vous dites event |> getTrack(12) |> getHit(43) , alors |> s'applique aux non-fonctions ( Events et Tracks ).

Si vous dites event |> getTrack(12) |> getHit(43) , alors |> s'applique aux non-fonctions ( Events et Tracks ).

En fait, non - il s'applique aux incantations de fonction _à sa droite_ en insérant son opérande gauche comme dernier argument de l'appel de fonction. event |> getTrack(12) est getTrack(12, event) cause de ce qui se trouvait à droite, pas à cause de ce qui était à gauche.

Cela devrait signifier a) la priorité sur les appels de fonction (puisqu'il s'agit d'une réécriture de l'appel), et b) l'ordre d'application de gauche à droite (pour le rendre getHit(43, getTrack(12, event)) plutôt que getHit(43, getTrack(12), event) ) .

Mais getTrack's signature est

function getTrack(num::Int, event::Event)

donc si event |> getTrack(12) insère event dans le dernier argument getTrack's , il met un Event dans le deuxième argument, pas un Function . J'ai juste essayé l'équivalent avec do et le premier argument, et Julia 0.4 s'est plaint que l'argument doit être une fonction. (Peut-être parce que do event end est interprété comme une fonction retournant event , plutôt que le event lui-même.)

Le chaînage de fonctions me semble être un problème distinct de la plupart des discussions autour de la syntaxe dot ( . ). Par exemple, @jpivarski , vous pouvez déjà accomplir une grande partie de ce que vous mentionnez de Rust dans Julia sans aucune nouvelle fonctionnalité:

type TownCrier
  name::AbstractString
  shout::Function

  function TownCrier(name::AbstractString)
    self = new(name)
    self.shout = () -> "HELLO, $(self.name)!"
    self
  end
end

tc = TownCrier("Josh")
tc.shout()                                #=> "HELLO, Josh!"
tc.name = "Bob"
tc.shout()                                #=> "HELLO, Bob!"

Sans trop essayer de faire dérailler la conversation, je suggérerais que ce que nous devons vraiment résoudre, c'est comment faire un currying fonctionnel efficace dans Julia. Les questions sur la façon de spécifier les positions des arguments dans une chaîne de fonctions disparaîtraient si nous avions un bon moyen de curry les fonctions. De plus, des constructions comme celles-ci seraient plus propres si le corps de fonction pouvait être spécifié et simplement curé avec «soi» lors de la construction.

@andyferris J'utilise Python et j'aime beaucoup _ faire référence au résultat de l'expression précédente. Cela ne fonctionne pas à l'intérieur des fonctions. Ce serait formidable si nous pouvions le faire fonctionner n'importe où: à l'intérieur des blocs de début, des fonctions, etc.

Je pense que cela pourrait totalement remplacer le chaînage. Cela ne laisse aucune ambiguïté sur l'ordre des arguments. Par exemple,

begin
    1
    vcat(_, 2)
    vcat(3, _)
end

# [3, 1, 2]

Comme @MikeInnes l'a mentionné, cela est déjà disponible dans @_ dans Lazy.jl (et bien que cela n'ait pas fonctionné de cette façon à l'origine, ChainMap.jl utilise également ce type de chaînage maintenant).

Espérons que cela pourrait fonctionner avec la fusion de points, au moins à l'intérieur de blocs

begin
    [1, 2, 3]
    .+(_, 2)
    .*(_, 2)
    .-(10, _)
end

ou, en utilisant la syntaxe @chain_map ,

begin
    ~[1, 2, 3]
    +(_, 2)
    *(_, 2)
    -(10, _)
end

Actuellement, il existe un moyen de chaîner des fonctions avec des objets si la fonction est définie à l'intérieur du constructeur. Par exemple, la fonction Obj.times:

type Obj
    x
    times::Function
    function Obj(x)
       this = new(x)
       this.times =  (n) -> (this.x *= n; this)
       this
    end
end

>>>Obj(2).times(3)
Obj(6,#3)

Qu'en est-il de l'implémentation des fonctions membres (fonctions spéciales) définies en dehors de la définition de type. Par exemple, la fonction Obj.times s'écrirait comme suit:

member function times(this::Obj, n)
     this.x *= n
     return this
end

>>>Obj(2).times(3)
Obj(6,#3)

member est un mot clé spécial pour les fonctions membres.
Les fonctions membres ont accès aux données de l'objet. Plus tard, ils seront appelés en utilisant le point après la variable objet.
L'idée est de reproduire le comportement des fonctions définies à l'intérieur des constructeurs en utilisant des fonctions "membres" définies en dehors de la définition de type.

Quelque chose comme ça est fait dans Rust avec la syntaxe de la méthode . Il est conceptuellement distinct du chaînage de fonctions, bien qu'il puisse être utilisé pour faire ressembler le chaînage à certains langages OO. Il est probablement préférable de traiter les problèmes séparément.

J'ai lu ceci et quelques problèmes connexes, voici ma proposition:

Chaînage de base :
in1 |> function1
Idem: in1 |> function1(|>)

in2 |> function2(10)
Idem: in2 |> function2(|>,10)

Encore plus de chaînage:
in1 |> function1 |> function2(10,|>)

Ramification et fusion de chaînes:
Succursale deux fois avec succursales out1 , out2 :
function1(a) |out1>
function2(a,b) |out2>

Utilisez la branche out1 et out2 :
function3(|out1>,|out2>)

Et la paresse?
Avons-nous besoin de quelque chose comme la convention function!(mutating_var) ?
Pour les fonctions paresseuses, nous pourrions utiliser function?() ...

Et en utilisant correctement l'indentation, il est facile de suivre visuellement les dépendances de données sur le graphe d'appel associé.

Je viens de jouer avec un modèle de chaînage de fonctions avec l'opérateur |> existant. Par exemple, ces définitions:
julia

Filtre

immuable MyFilter {F}
flt :: F
fin

function (mf :: MyFilter) (source)
filtre (mf.flt, source)
fin

fonction Base.filter (flt)
MonFiltre (flt)
fin

Prendre

immuable MyTake
n :: Int64
fin

function (mt :: MyTake) (source)
prendre (source, mt.n)
fin

fonction Base.take (n)
MyTake (n)
fin

Carte

MyMap immuable {F}
f :: F
fin

function (mm :: MyMap) (source)
carte (mm.f, source)
fin

fonction Base.map (f)
Ma carte (f)
fin
enable this to work: Julia
1:10 |> filtre (i-> i% 2 == 0) |> take (2) |> map (i-> i ^ 2) |> collect
`` Essentially the idea is that functions like filter return a functor if they are called without a source argument, and then these functors all take one argument, namely whatever is "coming" from the left side of the |> . The |> `` puis enchaîne simplement tous ces foncteurs ensemble.

filter etc. pourrait aussi simplement renvoyer une fonction anonyme qui prend un argument, sans savoir laquelle de ces options serait la plus performante.

Dans mon exemple, j'écrase map(f::Any) dans Base, je ne comprends pas vraiment ce que fait la définition existante de map ...

Je viens juste de trouver ce modèle, et mon regard un peu superficiel autour de moi ne montre aucune discussion sur quelque chose comme ça. Que pensent les gens? Cela pourrait-il être utile? Les gens peuvent-ils penser aux inconvénients de cela? Si cela fonctionne, peut-être que la conception existante est suffisamment flexible pour permettre une histoire de chaînage assez complète?

Cela ne semble pas réalisable pour les fonctions arbitraires, uniquement celles pour lesquelles MyF a été défini?

Oui, cela ne fonctionne que pour les fonctions activées. Ce n'est clairement pas une solution très générale, et dans un certain sens la même histoire qu'avec la vectorisation, mais quand même, étant donné que tout ce que nous espérerions ne le rendra pas pour 1.0, ce modèle pourrait permettre tout un tas de scénarios où les gens ont dû recourir à macros dès maintenant.

Essentiellement, l'idée est que les fonctions comme filter renvoient un foncteur si elles sont appelées sans argument source, et alors ces foncteurs prennent tous un seul argument, à savoir tout ce qui "vient" du côté gauche du |>.

C'est, presque exactement, l'essence des transducteurs de Clojure. La différence notable est que Clojure a construit des transducteurs en plus du concept de réducteurs. En bref, chaque fonction qui opère sur une collection peut être décomposée en une fonction de "mappage" et une fonction "réductrice" (même si la fonction "réductrice" est simplement concat ). L'avantage de représenter les fonctions de collection de cette manière est que vous pouvez réorganiser l'exécution de sorte que tout le «mappage» puisse être pipeliné (particulièrement utile pour les grandes collections). Les transducteurs ne sont donc qu'une extraction de ces "mappages" renvoyés lorsqu'ils sont appelés sans collection sur laquelle opérer.

Inutile que cela soit si compliqué. Les fonctions peuvent opter pour le curry avec des fermetures:

Base.map(f)    = (xs...) -> map(f, xs...)
Base.filter(f) = x -> filter(f, x)
Base.take(n)   = x -> take(x, n)

Bien sûr, ce n'est pas quelque chose qu'un paquet devrait faire car cela change la signification de ces méthodes pour tous les paquets. Et le faire au coup par coup comme ça n'est pas très intuitif - quels arguments devraient avoir la priorité?

Je préférerais une solution syntaxique de site d'appel comme celle décrite ci-dessus, abaissant f(a, _, b) à x -> f(a, x, b) . C'est délicat, cependant, comme indiqué dans la longue discussion ci-dessus.

Inutile que cela soit si compliqué. Les fonctions peuvent opter pour le curry avec des fermetures

Oui, j'ai déjà suggéré que ci-dessus, je n'étais tout simplement pas sûr qu'il y ait une différence de performance entre les deux.

quels arguments devraient avoir la priorité?

Ouais, et puis nous avons en fait des choses comme filter et take , où dans un cas, nous avons la collection comme premier argument et dans l'autre comme dernier argument ... Je pense en quelque sorte que au moins pour les opérations de type itérateur, il peut y avoir une réponse évidente à cette question.

Une fois que _ est un symbole spécial disponible

Oui, je suis tout à fait d'accord pour dire qu'il existe une solution plus générale, et celle de @malmaud pourrait l'être.

Il n'y a pas de différence de performance car les fermetures génèrent de toute façon le code que vous avez écrit à la main. Mais puisque vous êtes juste curry, vous pouvez écrire une fonction pour le faire pour vous ( curry(f, as...) = (bs...) -> f(as..., bs...) ). Cela s'occupe de la carte et du filtre; il y a aussi eu des propositions dans le passé pour implémenter un curry qui implémente une valeur sentinelle comme curry(take, _, 2) .

Je suis venu ici, car j'apprends actuellement trois langues: D, Elixir et maintenant Julia. En D il y a la syntaxe d'appel de fonction uniforme , comme dans Rust, dans Elixir vous avez l' opérateur pipe . Les deux implémentent essentiellement le type de chaînage de fonctions suggéré ici et j'ai vraiment aimé cette fonctionnalité dans les deux langues, car elle est facile à comprendre, semble facile à implémenter et peut rendre le code utilisant des flux et d'autres types de pipelines de données beaucoup plus lisible.

Je n'ai vu qu'un bref tutoriel sur la syntaxe de Julia jusqu'à présent, mais j'ai immédiatement recherché cette fonctionnalité sur Google, car j'espérais que Julia aurait également quelque chose comme ça. Donc, je suppose que c'est un +1 pour cette demande de fonctionnalité de mon côté.

Salut les gens,

Veuillez m'autoriser à attribuer +1 à cette demande de fonctionnalité. C'est vraiment nécessaire. Considérez la syntaxe Scala suivante.

Array(1,2,3,4,5)
  .map(x => x+1)
  .filter(x => x > 5)
  .reduce(_ + _)

Les personnes qui ont utilisé Spark ou d'autres outils Big Data basés sur MapReduce seront très familiers avec cette syntaxe et auront écrit des travaux volumineux et compliqués de cette manière. Même R, relativement ancien, permet ce qui suit.

c(1,2,3,4,5) %>%
  {. + 1} %>%
  {.[which(. > 5)]} %>%
  sum

Notez l'utilisation intelligente des blocs de code comme substitut à une programmation fonctionnelle appropriée - pas la plus jolie, mais la plus puissante. Dans Julia, je peux faire ce qui suit.

[1,2,3,4,5] |> 
  _ -> map(__ -> __ + 1, _) |>
  _ -> filter(__ -> __ < 5, _) |>
  _ -> reduce(+, _)

Mais c'est horrible et moche. Si nous ne pouvons pas avoir de code orienté objet à la Scala, les opérateurs de canal deviennent extrêmement importants. La solution la plus simple est que le tube alimente le _premier argument_, sauf si un caractère générique tel que _ est utilisé explicitement, mais cela n'aurait de sens que si map étaient modifiés pour prendre la structure de données comme premier argument et fonction comme deuxième.

Il devrait également y avoir un équivalent de Array(1,2,3).map(_ + 1) de Scala pour éviter des _ -> _ + 1 excessifs et une syntaxe similaire. J'aime l'idée ci-dessus où [1,2,3] |> map(~ + 1, _) est traduit en map(~ -> ~ + 1, [1,2,3]) . Merci d'avoir regardé.

Pour ce dernier, nous avons la diffusion avec une syntaxe compacte [1, 2, 3] .+ 1 C'est assez addictif. Quelque chose comme ça pour la réduction (et peut-être le filtrage) serait incroyablement cool, mais semble être une grande demande.

Il est raisonnable de noter que le piping et do disputent le premier argument de fonction.

Je rappellerai que de nouveaux viennent au fil, que nous avons,
pas un, pas deux, mais CINQ packages fournissant des extensions à la fonctionnalité de tuyauterie SISO de base de julia, vers les syntaxes suggérées.
voir la liste sur: https://github.com/JuliaLang/julia/issues/5571#issuecomment -205754539

Il est raisonnable de noter que les deux piping et do se battent pour le premier argument de fonction.

Si nous devions obtenir une fonctionnalité de tuyauterie supplémentaire dans la base, ce n'était pas la position marquée avec _ etc.
Ensuite, je pense que cela ajouterait des arguments à la position finale, pas à la première.
cela ressemblerait plus à "faire semblant de curry / application partielle"

Mon article ci-dessus se veut un exemple simple conçu pour illustrer les problèmes de syntaxe en question.

En réalité, des centaines d'opérations sont souvent utilisées dans une chaîne, dont beaucoup sont non standard. Imaginez travailler avec du Big Data en langage naturel. Vous écrivez une séquence de transformations qui prennent une chaîne de caractères, filtrez les caractères chinois, séparés par des espaces, filtrez des mots tels que "le", transformez chaque mot en une forme "standardisée" via l'outil logiciel de boîte noire utilisé par votre site Web serveur, ajoutez des informations sur la fréquence de chaque mot via un autre outil de boîte noire, additionnez les poids de chaque mot unique, 100 autres opérations, etc.

Ce sont des situations que je considère. Faire de telles opérations sans utiliser d'appels de méthode ou de tuyaux est un non-démarreur en raison de sa taille.

Je ne sais pas quel est le meilleur design, j'encourage simplement tout le monde à considérer les cas d'utilisation et une syntaxe plus élaborée que ce qui est actuellement en place.

Cela devrait fonctionner dans Juno et Julia 0.6

`` `{julia}
en utilisant LazyCall
@lazy_call_module_methods Générateur de base
@lazy_call_module_methods Filtre d'itérateur

en utilisant ChainRecursive
start_chaining ()


```{julia}
[1, 2, 3, 4, 5]
<strong i="14">@unweave</strong> ~it + 1
Base.Generator(it)
<strong i="15">@unweave</strong> ~it < 5
Iterators.filter(it)
reduce(+, it)

J'ai une question concernant une syntaxe que j'ai vue dans les commentaires sur ce problème:
https://stackoverflow.com/questions/44520097/method-chaining-in-julia

@somedadaism , les problèmes sont pour les problèmes et non pour «annoncer» les questions de débordement de pile. De plus, Julia-people est très active sur SO et (encore plus) sur https://discourse.julialang.org/. Je serais très surpris que vous n'obteniez pas une réponse à la plupart des questions très rapidement. Et, bienvenue à Julia!

Jeez, incroyable à quel point cela peut être compliqué. +1 pour une syntaxe décente. Pour moi, l'utilisation principale de la tuyauterie est également de travailler avec des données (cadres). Pensez à dplyr. Personnellement, je ne me soucie pas vraiment de passer par le premier / le dernier par défaut, mais je suppose que la plupart des développeurs de packages verront leurs fonctions accepter les données comme premier argument - et qu'en est-il des arguments optionnels? +1 pour quelque chose comme

1 |> sin |> sum(2, _)

Comme cela a été mentionné précédemment, la lisibilité et la simplicité sont extrêmement importantes. Je ne voudrais pas manquer le style complet de dplyr / tidyverse de faire des choses pour l'analyse de données ...

Je voudrais ajouter que je trouve également très utile la syntaxe multiligne d'Elixir pour l'opérateur pipe.

1
|> sin
|> sum(2)
|> println

Est l'équivalent de println(sum(sin(1),2))

Juste pour noter une proposition dans le monde javascript . Ils utilisent l'opérateur ? au lieu de _ ou ~ dont nous avons déjà la signification ( _ pour ignorer quelque chose et ~ comme bitwise non ou formulaire). Étant donné que nous utilisons actuellement ? la même manière que javascript, nous pourrions également l'utiliser pour l'espace réservé de curry.

voici à quoi ressemble leur proposition (c'est en javascript, mais aussi valide en julia :)

const addOne = add(1, ?); // apply from the left
addOne(2); // 3

const addTen = add(?, 10); // apply from the right
addTen(2); // 12

// with pipeline
let newScore = player.score
  |> add(7, ?)
  |> clamp(0, 100, ?); // shallow stack, the pipe to `clamp` is the same frame as the pipe to `add`.

const maxGreaterThanZero = Math.max(0, ...);
maxGreaterThanZero(1, 2); // 2
maxGreaterThanZero(-1, -2); // 0

Un résumé parce que j'ai commencé à en écrire un pour d'autres raisons.
Voir aussi ma liste précédente sur les packages associés .

Tout problème avec _ est insécable et peut être fait en 1.x, car https://github.com/JuliaLang/julia/pull/20328

Tout cela se résume à deux options principales (autres que le statu quo).
Les deux peuvent (à toutes fins utiles) être implémentés avec une macro pour réécrire la syntaxe.

Jouer avec _ pour créer des fonctions anon

Terse Lambdas de @StefanKarpinski , ou syntaxe similaire où la présence d'un _ (dans une expression RHS), indique que toute cette expression est une fonction anonyme.

  • cela peut presque être géré par une macro see .

    • La seule chose qui ne peut pas être faite est que (_) n'est pas la même chose que _ . Ce qui est juste la fonction d'identité n'a donc pas vraiment d'importance

    • Cela s'appliquerait partout, ce serait donc non seulement utile avec |> , mais aussi par exemple pour écrire des choses de manière compacte comme map(log(7,_), xs) ou log(7, _).(xs) pour prendre le journal avec la base 7 de chaque élément sur xs .

    • Je suis personnellement favorable à cela, si nous faisions quelque chose.

Jouer avec |> pour lui faire effectuer des substitutions

Les options comprennent:

  • Faites-le faire agir son RHS comme un curry

    • en fait, je pense que c'est cassant, (bien qu'il existe peut-être une version insécable qui vérifie la table des méthodes. Je pense que c'est plutôt déroutant)

  • Faites en sorte qu'il rende _ act spécial (voir les options ci-dessus, et / ou diverses façons de le simuler via la réécriture)

    • une façon de le faire serait d'autoriser la création de macros d'infixes, puis d'écrire @|>@ et de le définir comme vous le souhaitez dans les packages (cela a déjà été fermé une fois https://github.com/JuliaLang/julia/ numéros / 11608)

    • ou lui donner ces propriétés spéciales intrinsèquement

  • Nous avons des tonnes d'implémentations de macros pour ce faire, comme je l'ai dit, consultez ma liste de packages associés
  • Certaines personnes proposent également de changer son pour le rendre (contrairement à tous les autres opérateurs) capable de provoquer une expression sur la ligne avant qu'elle ne se termine. Alors tu peux écrire
a
|> f
|>g

Plutôt que le courant:

a |>
f |>
g

(L'implémentation avec une macro n'est pas possible sans mettre le bloc entre crochets. Et le fait de mettre le bloc entre crochets le fait déjà fonctionner de toute façon)

  • Personnellement, je n'aime pas ces propositions car elles rendent |> (un opérateur déjà détesté) super magique.

Edit : comme le souligne @StefanKarpinski ci -
Parce que quelqu'un pourrait dépendre de typeof(|>) <: Function .
Et ces changements en feraient un élément de la syntaxe du langage.

Option bonus: cela n'arrive jamais Option: ajoutez du curry partout # 554

Il est trop tard dans la langue pour ajouter du curry.
Ce serait une rupture folle, ajoutant d'énormes tas d'ambiguïtés partout.
Et soyez juste très déroutant.

Je pense qu'avec ces deux options, cela couvre essentiellement tout ce qui mérite d'être pris en compte.
Je ne pense pas que quoi que ce soit d'autre de perspicace ait été dit (c'est-à-dire sans compter "+1 veux ça"; ou répétitions de microvariants sur ce qui précède).

Je suis assez tenté de déprécier |> en 0.7 afin que nous puissions l'introduire plus tard avec une sémantique plus utile et peut-être non fonctionnelle qui, je pense, est nécessaire pour faire fonctionner correctement la tuyauterie.

Je suis assez tenté de déprécier |> dans la version 0.7 afin que nous puissions l'introduire plus tard avec une sémantique plus utile et peut-être non fonctionnelle qui, je pense, est nécessaire pour que la tuyauterie fonctionne correctement.

Le seul cas de rupture sur cette liste est lorsque |> fait que son côté droit fait semblant de curry.
Soit pour insérer ses arguments dans la première, ou dans la dernière position (/ s) des derniers arguments, soit dans une autre position fixe (la deuxième peut avoir du sens).
Sans utiliser _ comme marqueur pour quel argument insérer.

Aucune autre proposition de rupture n'a été faite que quiconque dans ce fil a pris vaguement au sérieux.
Je serais surpris s'il y avait d'autres définitions sensées et pourtant cassantes pour cette opération,
que personne n'a encore suggéré au cours de ces presque 4 dernières années.

Quoi qu'il en soit, déprécier ce ne serait pas terrible.
Les packages qui l'utilisent peuvent toujours l'avoir via l'un des packages de macros.

Une autre idée pourrait être de conserver |> avec le comportement actuel et d'introduire la nouvelle fonctionnalité sous un nom différent qui ne nécessite pas l'utilisation de la touche Maj, comme \\ (ce qui ne même analyser en tant qu'opérateur en ce moment). Nous en avons parlé une fois sur Slack, mais je pense que l'histoire est probablement perdue dans le sable du temps.

La tuyauterie est souvent utilisée de manière interactive et la facilité de saisie de l'opérateur affecte la sensation de «légèreté» à utiliser. Un seul caractère | pourrait aussi être bien.

Un seul caractère | pourrait aussi être bien.

Interactivement oui, mais il suffit ensuite de l'avoir dans .juliarc.jl (que j'ai depuis longtemps ;-p)

qui ne nécessite pas l'utilisation de la touche Maj

Notez qu'il s'agit d'une propriété fortement dépendante des paramètres régionaux. Par exemple, mon clavier suédois a expédié un certain nombre de caractères à déplacer et des combinaisons AltGr (plutôt horribles) pour faire de la place pour trois lettres supplémentaires.

Existe-t-il une tradition d'utilisation de |> à cette fin? [Mathematica] (http://reference.wolfram.com/language/guide/Syntax.html) a // pour l'application de la fonction postfix, qui devrait être facile à taper dans la plupart des claviers et pourrait être disponible, si elle n'est pas déjà utilisé pour les commentaires (comme en C ++) ou la division entière (comme en Python).

Quelque chose avec | dedans a la connexion agréable avec le script shell, bien que si bien sûr un seul | devrait être un OU au niveau du bit. Est-ce que || pris pour un OU logique? Et pour ||| ? Taper trois fois un caractère difficile à atteindre n'est pas beaucoup plus difficile que de le taper une fois.

Existe-t-il une tradition d'utilisation de |> à cette fin?

Je crois que la tradition de |> dérive de la famille de langages ML. En ce qui concerne les opérateurs, peu de communautés de langage de programmation ont exploré cet espace comme la communauté ML / Haskell. Une petite sélection d'exemples:

Pour ajouter à la liste ci-dessus, R utilise%>% - et bien que ce langage soit daté, je pense que sa fonctionnalité de canal est très bien conçue. L'une des choses qui le rend efficace est la syntaxe d'accolades, qui permet d'écrire des choses comme

x %>% { if(. < 5) { a(.) } else { b(.) } }

ce qui serait un peu plus détaillé dans Julia en raison de son utilisation des instructions de fin. Bien que mon exemple ci-dessus soit abstrait, de nombreuses personnes utilisent une syntaxe similaire lors de l'écriture de code de prétraitement de données. Avec l'une des propositions actuelles en cours de discussion, peut-on réaliser quelque chose de similaire à ce qui précède - peut-être en utilisant des parenthèses?

J'encouragerais également l'utilisation d'un opérateur de canal qui donne une indication visuelle que les arguments sont transmis, comme si le symbole > . Cela fournit un repère utile pour les débutants et ceux qui ne connaissent pas la programmation fonctionnelle.

Même si les usages proposés de |> ne sont pas incompatibles syntaxiquement avec l'usage typique courant, ils _ sont_ incompatibles avec |> étant un opérateur - puisque la plupart d'entre eux impliquent de donner |> bien plus puissance qu'une simple fonction d'infixe. Même si nous sommes sûrs que nous voulons conserver x |> f |> g pour signifier g(f(x)) , le laisser comme un opérateur normal empêchera probablement toute autre amélioration. Bien que changer |> en un non-opérateur qui fait postfixer l'application de fonction pourrait ne pas casser son utilisation _typique_ pour l'application de fonction chaînée, cela ne serait toujours pas permis car cela briserait l'utilisation _atypical_ de |> - quoi que ce soit qui repose sur le fait qu'il soit un opérateur. L'usage atypique est toujours d'actualité et n'est donc pas autorisé dans les versions 1.x. Si nous voulons faire l'une des propositions ci-dessus avec |> pour autant que je sache, nous devons créer |> syntaxe

@StefanKarpinski Est-ce que |> plutôt qu'une fonction même sur la table en ce moment? Est-il possible de le mettre sur la table à temps pour l'avoir en place pour la version 1.0?

@StefanKarpinski Est-ce que faire de la syntaxe |> plutôt qu'une fonction, même sur la table en ce moment? Est-il possible de le mettre sur la table à temps pour l'avoir en place pour la version 1.0?

Le déprécier en 0.7 et le supprimer carrément de 1.0 est sur la table.
Puis ramenez-le quelque temps pendant 1.x comme élément de syntaxe.
Ce qui serait à ce stade un changement sans rupture.

Quelqu'un aurait besoin de le faire, mais je ne pense pas que ce soit un changement terriblement difficile, alors oui, c'est sur la table.

À quoi |> serait-il obsolète? Une implémentation dans Lazy.jl?

x |> f peut devenir f(x) .

Que diriez-vous de déprécier l> mais en même temps d'introduire, disons, ll> qui a le même comportement que l'actuel l> ?

Si nous n'acceptons que la dépréciation sans remplacement, les paquets qui reposent sur le comportement actuel se retrouveraient essentiellement sans une bonne option. S'ils obtiennent une expression un peu moins agréable entre-temps, ils peuvent continuer avec leur conception actuelle, mais nous laissons toujours l'option sur la table pour trouver une très bonne solution pour l> à l'avenir.

Cela affecte considérablement l'écosystème Query and friends: j'ai créé un système qui est assez similaire à la syntaxe du tube dans le tidyverse R. Le tout est assez complet: il couvre le fichier io pour actuellement sept formats de fichiers tabulaires (avec deux autres très proches), toutes les opérations de requête (comme dplyr) et le traçage (pas loin, mais je suis optimiste que nous pouvons avoir quelque chose qui ressemble à ggplot bientôt). Tout repose sur l'implémentation actuelle de l> ...

Je devrais dire que je suis tout à fait en faveur de garder les options pour quelque chose de mieux pour l> sur la table. Cela fonctionne bien pour ce que j'ai créé jusqu'à présent, mais je pourrais facilement voir une meilleure approche. Mais simplement déprécier semble une étape très radicale qui peut en quelque sorte retirer le tapis de beaucoup de paquets.

L'autre choix est de simplement faire de x |> f une syntaxe alternative pour f(x) . Cela briserait le code qui surcharge |> mais permettrait au code qui utilise |> pour le chaînage de fonctions de continuer à fonctionner tout en laissant les choses ouvertes pour des améliorations supplémentaires tant qu'elles sont compatibles avec cela.

L'alternative serait d'introduire une nouvelle syntaxe de chaînage syntaxique dans le futur, mais cela doit être quelque chose qui est actuellement une erreur de syntaxe, ce qui est assez mince pour le moment.

L'alternative serait d'introduire une nouvelle syntaxe de chaînage syntaxique dans le futur, mais cela doit être quelque chose qui est actuellement une erreur de syntaxe, ce qui est assez mince pour le moment.

Ma proposition d'en haut ne le permettrait-elle pas? C'est-à-dire faire |> une erreur de syntaxe dans Julia 1.0, et rendre ||> équivalent au |> . Pour julia 1.0, ce serait un inconvénient mineur pour le code qui utilise actuellement |> car il faudrait passer à ||> . Mais je pense que ce ne serait pas si mal, et cela pourrait être entièrement automatisé. Ensuite, une fois que quelqu'un a une bonne idée pour |> elle peut être réintroduite dans la langue. À ce stade, il y aurait à la fois ||> et |> autour, et je suppose que ||> disparaîtrait lentement en arrière-plan si tout le monde commence à adopter |> . Et puis, dans quelques années, julia 2.0 pourrait simplement supprimer ||> . Dans mon esprit, a) ne causerait aucun problème à qui que ce soit dans le délai de Julia 1.0, et b) laisserait toutes les options sur la table pour une très bonne solution pour |> éventuellement.

|>(x, f) = f(x)
|>(x, tuple::Tuple) = tuple[1](x, tuple[2:endof(tuple)]...) # tuple
|>(x, f, args...) = f(x, args...) # args

x = 1 |> (+, 1, 1) |> (-, 1) |> (*, 2) |> (/, 2) |> (+, 1) |> (*, 2) # tuple
y = 1 |> (+, 1, 1)... |> (-, 1)... |> (*, 2)... |> (/, 2)... |> (+, 1)... |> (*, 2)... # args

Il n'est pas facile d'écrire plusieurs fois mais de gauche à droite et n'utilise pas de macro.

function fibb_tuple(n)
    if n < 3
        return n
    end
    fibb_tuple(n-3) |> (+, fibb_tuple(n-2), fibb_tuple(n-1))
end

function fibb_args(n)
    if n < 3
        return n
    end
    fibb_args(n-3) |> (+, fibb_args(n-2), fibb_args(n-1))...
end

function fibb(n)
    if n < 3
        return n
    end
    fibb(n-3) + fibb(n-2) + fibb(n-1)
end

n = 25

println("fibb_tuple")
<strong i="8">@time</strong> fibb_tuple(1)
println("fibb_args")
<strong i="9">@time</strong> fibb_args(1)
println("fibb")
<strong i="10">@time</strong> fibb(1)

println("tuple")
<strong i="11">@time</strong> fibb_tuple(n)
println("args")
<strong i="12">@time</strong> fibb_args(n)
println("fibb")
<strong i="13">@time</strong> fibb(n)
fibb_tuple
  0.005693 seconds (2.40 k allocations: 135.065 KiB)
fibb_args
  0.003483 seconds (1.06 k allocations: 60.540 KiB)
fibb
  0.002716 seconds (641 allocations: 36.021 KiB)
tuple
  1.331350 seconds (5.41 M allocations: 151.247 MiB, 20.93% gc time)
args
  0.006768 seconds (5 allocations: 176 bytes)
fibb
  0.006165 seconds (5 allocations: 176 bytes)

|>(x, tuple::Tuple) = tuple[1](x, tuple[2:endof(tuple)]...) est horrible.
|>(x, f, args...) = f(x, args...) besoin de plus de lettres mais rapidement.

Je pense qu'autoriser la syntaxe subject |> verb(_, objects) comme verb(subject, objects) signifie prendre en charge SVO (mais la valeur par défaut de Julia est VSO ou VOS). Cependant Julia prend en charge mutltidipatch afin que le sujet puisse être des sujets. Je pense que nous devrions autoriser la syntaxe (subject1, subject2) |> verb(_, _, object1, object2) comme verb(subject1, subject2, object1, object2) si nous introduisons la syntaxe SVO.
C'est MIMO s'il est appréhendé comme pipeline, comme l' a noté

Que diriez-vous d'utiliser (x)f comme f(x) ?
(x)f(y) peut être lu à la fois comme f(x)(y) et f(y)(x) alors choisissez d'abord évaluer correctement:

(x)f # f(x)
(x)f(y) # f(y)(x)
(x)f(y)(z) # f(y)(z)(x)
(x)(y)f(z) # f(z)(y)(x)
(a)(b)f(c)(d) # f(c)(d)(b)(a)
1(2(3, 4), 5(6, 7), 8(9, 10)) # 1(2(3, 4), 5(6, 7), 8(9, 10))
1 <| (2 <| (3, 4), 5 <| (6, 7), 8 <| (9, 10)) # 1(2(3, 4), 5(6, 7), 8(9, 10)), but 2 <| (3, 4) == 2((3, 4)) so currently emit error
3 |> 2(_, 4) |> 1(_, 5(6, 7), 8(9, 10)) # 1(2(3, 4), 5(6, 7), 8(9, 10))
((3)2(_, 4))1(_, 5(6, 7), 8(9, 10)) # 1(2(3, 4), 5(6, 7), 8(9, 10))
(3, 4) |> 2(_, _) |> 1(_, 5(6, 7), 8(9, 10)) # 1(2(3, 4), 5(6, 7), 8(9, 10))
((3, 4)2)1(_, 5(6, 7), 8(9, 10)) # 1(2(3, 4), 5(6, 7), 8(9, 10))

Cela peut manipuler vararg clairement.
Mais cela cassera les opérateurs binaires sans espaces:

(a + b)+(c + d) # +(c + d)(a + b) == (c + d)(a + b): Error

Option alternative: ajoutez un autre cas pour la syntaxe de splatting. Avoir f ... (x) désugar vers (args ...) -> f (x, args ...)

Cela permettrait un curry syntaxiquement léger (manuel):

#Basic example:
f(a,b,c,d) = #some definition
f...(a)(b,c,d) == f(a,b,c,d)
f...(a,b)(c,d) == f(a,b,c,d)
f...(a,b,c)(d) == f(a,b,c,d)
f...(a)...(b)(c,d) == f(a,b,c,d) # etc etc

# Use in pipelining:
x |> map...(f) |> g  |> filter...(h) |> sum

Quand arrêtez-vous de curry? Les fonctions Julia n'ont pas d'arité fixe.

Julia a déjà un opérateur d'éclaboussure. Ce que je propose aurait exactement le même comportement que l'opérateur splat actuel.

I, e: f ... (x) == (args ...) -> f (x, args ...) est du sucre pour faire un lambda avec splatting.

Cette définition vous donne toujours un objet fonction. Vraisemblablement, vous voulez parfois une réponse.

Vous obtenez cela en appelant explicitement l'objet à la fin. Par exemple, notez l'absence de ... avant le dernier jeu de parenthèses dans mon dernier exemple f ... (a) ... (b) (c, d) == f (a, b, c, d) .

Vous pouvez également appeler l'objet fonction retourné avec |>, ce qui le rend agréable pour la tuyauterie.

@saolof

Exemple de base:

f (a, b, c, d) = #une définition
f ... (a) (b, c, d) == f (a, b, c, d)
f ... (a, b) (c, d) == f (a, b, c, d)
f ... (a, b, c) (d) == f (a, b, c, d)
f ... (a) ... (b) (c, d) == f (a, b, c, d) # etc etc

Bonne intuition pour utiliser splat avec chaînage de fonctions mais trop complexe à mon sens.
Vous avez effectué plusieurs demandes à un point de la chaîne.
Dans le chaînage de fonctions, vous créez une application étape par étape tout au long de la chaîne.

Et @StefanKarpinski a raison, vous ne savez pas quand vous arrêter pour appliquer des fonctions sur eux-mêmes et enfin les appliquer à un élément plus scalaire.

--(coupé)--

Désolé, c'est ce qui est plutôt inutile et illisible.
Voir mon deuxième msg ci-dessous pour obtenir des explications plus claires (j'espère).

Étant donné la fonctionnalité de Julia, j'aime assez l'idée de @saolof d'un opérateur de fonction-curry. Je ne comprends pas vraiment d'où viennent les objections sémantiques, car il semble que cela ait une interprétation très évidente.

Vous pouvez même le prototyper dans le confort de votre propre réplique:

ctranspose(f) = (a...) -> (b...) -> f(a..., b...)

map'(+)(1:10)

map'(+)'(1:10, 11:20)(21:30)

(+)'(1,2,3)(4,5)

1:10 |> map'(x->x^2) |> filter'(iseven)

A une sorte de sensation agréable, je pense.

Edit: On dirait que cela pourrait également être le chemin pour généraliser davantage cela. Si nous pouvons écrire map∘(+, 1:10) alors nous pouvons écrire map∘(_, 1:10) pour placer l'argument curry en premier, et l'opérateur curry détermine la portée du lambda, résolvant le plus gros problème pour un tel curry général.

Eh, c'est malin, @MikeInnes.

J'adore la façon dont l'extrême extensibilité de Julia se manifeste ici aussi. La solution unificatrice à un très large éventail d'exigences pour le chaînage de fonctions se révèle abuser de ctranspose ...

(clarification: je reçois 1:10 |> map'(x->x^2) |> filter'(iseven) avec cette proposition, donc je suis 💯% pour elle!)

Pour être clair, je ne pense pas que nous devrions abuser de l'opérateur adjoint pour cela, mais c'est une bonne preuve de concept que nous pouvons avoir une notation concise de currying de fonctions.

Peut-être devrions-nous introduire un nouvel opérateur Unicode? http://www.fileformat.info/info/unicode/char/1f35b/index.htm

(Désolé...)

J'ai l'impression que les _ sont toujours un moyen beaucoup plus flexible de faire des lambdas

@bramtayl Je pense que l'idée dans la modification de MikeInnes à son message est que les deux peuvent coexister - des soulignements standaline comme dans la demande de tirage de @stevengj fonctionneraient, le curry autonome comme dans l'idée de Mike ci-dessus fonctionnerait, et combiner les deux fonctionnerait également, vous permettant d'utiliser l'opérateur curry pour délimiter la portée de _ s à l'intérieur.

ah compris

Cela ne le rend pas trop différent de LazyCall.jl

Sur une note plus sérieuse:

Pour être clair, je ne pense pas que nous devrions abuser de l'opérateur adjoint pour cela

Probablement un bon choix. Cependant, je voudrais exprimer mes espoirs que si une telle solution est mise en œuvre, on lui donne un opérateur facile à taper. La possibilité de faire quelque chose comme 1:10 |> map'(x->x^2) est nettement moins utile si le caractère qui remplace ' m'oblige à le rechercher dans une table unicode (ou à utiliser un éditeur qui prend en charge les expansions LaTeX).

Plutôt que d'abuser de l'opérateur adjoint, nous pourrions réutiliser le splat.

  • dans un contexte de tuyauterie (linéaire)
  • à l'intérieur, dans un appel de fonction

    • faire splat avant plutôt qu'après

alors

  • splat peut induire un itérateur manquant arg

Une sorte de splat d'ordre élevé, (avec anacruse s'il y a un musicien là-bas).
En espérant que cela ne secouera pas trop la langue.

EXEMPLE

1:10
    |> map(...x->x^2)
    |> filter(...iseven)

EXEMPLE 2

genpie = (r, a=2pi, n=12) ->
  (0:n-1) |>
      map(...i -> a*i/n) |>
      map(...t -> [r*cos(t), r*sin(t)]) 

pourrait représenter

elmap = f -> (s -> map(f,s))

genpie = (r, a=2pi, n=12) ->
  (0:n-1) |>
      elmap(i -> a*i/n) |>
      elmap(t -> [r*cos(t), r*sin(t)]) 

Je ne sais pas si cela appartient ici, car la discussion a évolué vers un chaînage et une syntaxe plus avancés / flexibles ... mais revenons au message d'ouverture, le chaînage de fonctions avec la syntaxe de points semble possible en ce moment, avec un peu de configuration supplémentaire. La syntaxe est juste une conséquence de la syntaxe de points pour les structures avec des fonctions / fermetures de première classe.

mutable struct T
    move
    scale
    display
    x
    y
end

function move(x,y)
    t.x=x
    t.y=y
    return t
end
function scale(c)
    t.x*=c
    t.y*=c
    return t
end
function display()
    @printf("(%f,%f)\n",t.x,t.y)
end

function newT(x,y)
    T(move,scale,display,x,y)
end


julia> t=newT(0,0)
T(move, scale, display, 0, 0)

julia> t.move(1,2).scale(3).display()
(3.000000,6.000000)

La syntaxe semble très similaire à la POO conventionnelle, avec une bizarrerie de "méthodes de classe" étant mutable. Je ne sais pas quelles sont les implications sur les performances.

@ivanctong Ce que vous avez décrit s'apparente davantage à une interface fluide qu'au chaînage de fonctions.

Cela dit, résoudre plus généralement le problème du chaînage des fonctions aurait l'avantage supplémentaire d'être également utilisable pour des interfaces fluides. Bien qu'il soit certainement possible de créer quelque chose comme une interface fluide en utilisant des membres de structure dans Julia actuellement, cela me semble tout à fait contraire à l'esprit et à l'esthétique du design de Julia.

La façon dont elixir le fait où l'opérateur de tube passe toujours dans le côté gauche comme premier argument et autorise des arguments supplémentaires par la suite, a été assez utile, j'aimerais voir quelque chose comme "elixir" |> String.ends_with?("ixir") tant que citoyen de première classe dans Julia.

D'autres langages le définissent comme une syntaxe d'appel de fonction uniforme .
Cette fonctionnalité offre plusieurs avantages (voir Wikipedia), ce serait bien si Julia la supporte.

Alors, y a-t-il une interface fluide avec Julia à ce stade?

Veuillez poser vos questions sur le forum de discussion sur le discours de Julia .

Dans un accès de piratage (et de jugement discutable!?), J'ai créé une autre solution possible au resserrement de la liaison des espaces réservés de fonction:

https://github.com/c42f/MagicUnderscores.jl

Comme indiqué sur https://github.com/JuliaLang/julia/pull/24990 , ceci est basé sur l'observation que l'on veut souvent que certains emplacements d'une fonction donnée lient étroitement une expression d'espace réservé _ , et d'autres vaguement. MagicUnderscores rend cela extensible pour toute fonction définie par l'utilisateur (tout à fait dans l'esprit de la machine de diffusion). Ainsi, nous pouvons avoir des choses telles que

julia> <strong i="12">@_</strong> [1,2,3,4] |> filter(_>2, _)
2-element Array{Int64,1}:
 3
 4

julia> <strong i="13">@_</strong> [1,2,3,4] |> filter(_>2, _) |> length
2

"juste travailler". (Avec le @_ disparaissant évidemment s'il est possible d'en faire une solution générale.)

Une suggestion de variante @MikeInnes semblerait adéquate pour mes besoins (généralement de longues chaînes de filtre, carte, réduction, énumération, zip, etc. en utilisant la syntaxe do ).

c(f) = (a...) -> (b...) -> f(a..., b...)

1:10 |> c(map)() do x
    x^2
end |> c(filter)() do x
    x > 50
end

Cela fonctionne, même si je ne peux plus faire fonctionner ' . Il est légèrement plus court que:

1:10 |> x -> map(x) do x
    x^2
end |> x -> filter(x) do x
    x > 50
end

Aussi je suppose que l'on pourrait juste faire

cmap = c(map)
cfilter = c(filter)
cetc = c(etc)
...

1:10 |> cmap() do x
    x^2
end |> cfilter() do x
    x > 50
end |> cetc() do ...

À partir de 1.0, vous devrez surcharger adjoint au lieu de ctranspose . Vous pouvez également faire:

julia> Base.getindex(f::Function, x...) = (y...) -> f(x..., y...)

julia> 1:10 |> map[x -> x^2] |> filter[x -> x>50]
3-element Array{Int64,1}:
  64
  81
 100

Si nous pouvions surcharger apply_type alors nous pourrions obtenir map{x -> x^2} :)

@MikeInnes je viens de voler ça

Une contribution tardive et légèrement frivole - que diriez-vous de diriger des données vers n'importe quel emplacement de la liste d'arguments en utilisant une combinaison d'opérateurs de curry gauche et droit:

VERSION==v"0.6.2"
import Base: ctranspose, transpose  
ctranspose(f::Function) = (a...) -> ((b...) -> f(a..., b...))  
 transpose(f::Function) = (a...) -> ((b...) -> f(b..., a...))

"little" |> (*)'''("Mary ")("had ")("a ") |> (*).'(" lamb")

Clojure a de jolies macros de threading . En avons-nous quelque part dans l'écosystème Julia?

Clojure a de jolies macros de threading . En avons-nous quelque part dans l'écosystème Julia?

https://github.com/MikeInnes/Lazy.jl

Clojure a de jolies macros de threading . En avons-nous quelque part dans l'écosystème Julia?

nous en avons au moins 10.
J'ai posté une liste plus haut dans le fil.
https://github.com/JuliaLang/julia/issues/5571#issuecomment -205754539

Pouvez-vous modifier la liste pour avoir LightQuery au lieu des deux autres packages du mien?

Puisque l'opérateur |> vient d'Elixir, pourquoi ne pas s'inspirer de l'un des moyens dont ils disposent pour créer des fonctions anonymes?
dans elixir vous pouvez utiliser &expr pour définir une nouvelle fonction anonyme et &n pour capturer les arguments de position ( &1 est le premier argument, &2 est le second, etc.)
Dans elixir, il y a des choses supplémentaires à écrire (par exemple, vous avez besoin d'un point avant la parenthèse pour appeler une fonction anonyme &(&1 + 1).(10) )

Mais voici à quoi ça pourrait ressembler en Julia

&(&1 * 10)        # same as: v -> v * 10
&(&2 + 2*&5)      # same as: (_, x, _, _, y) -> x + 2*y
&map(sqrt, &1)    # same as: v -> map(sqtr, v)

Nous pouvons donc utiliser plus bien l'opérateur |>

1:9 |> &map(&1) do x
  x^2
end |> &filter(&1) do x
  x in 25:50
end

au lieu de

1:9 |> v -> map(v) do x
  x^2
end |> v -> filter(v) do x
  x in 25:50
end

notez que vous pouvez remplacer les lignes 2 et 3 par .|> &(&1^2) ou .|> (v -> v^2)

La principale différence avec les propositions avec _ placeholder est qu'ici il est possible d'utiliser des arguments de position, et le & devant les expressions rend la portée des espaces réservés évidente (pour le lecteur et le compilateur).

Notez que j'ai pris & dans mes exemples, mais l'utilisation de ? , _ , $ ou autre à la place, ne changerait rien au Cas.

Scala utilise _ pour le premier argument, _ pour le deuxième argument, etc., ce qui est concis, mais vous manquez rapidement de situations où vous pouvez l'appliquer (ne peut pas répéter ou inverser l'ordre des arguments). Il n'a pas non plus de préfixe ( & dans la suggestion ci-dessus) qui élimine l'ambiguïté des fonctions des expressions, et c'est, en pratique, un autre problème qui empêche son utilisation. En tant que praticien, vous finissez par encapsuler les fonctions inline prévues entre parenthèses et accolades supplémentaires, en espérant qu'elles seront reconnues.

Je dirais donc que la priorité absolue lors de l'introduction d'une syntaxe comme celle-ci est qu'elle soit sans ambiguïté.

Mais comme pour les préfixes pour les arguments, $ a une tradition dans le monde des scripts shell. Il est toujours bon d'utiliser des personnages familiers. Si le |> vient d'Elixir, alors cela pourrait être un argument pour prendre & d'Elixir, aussi, avec l'idée que les utilisateurs pensent déjà dans ce mode. (En supposant qu'il y ait beaucoup d'anciens utilisateurs d'Elixir là-bas ...)

Une chose qu'une syntaxe comme celle-ci ne peut probablement jamais capturer est de créer une fonction qui prend N arguments mais en utilise moins de N. Le $1 , $2 , $3 dans le corps implique l'existence de 3 arguments, mais si vous voulez mettre cela dans une position où il sera appelé avec 4 arguments (le dernier à ignorer), alors il n'y a pas de manière naturelle de l'exprimer. (Autre que de prédéfinir les fonctions d'identité pour chaque N et d'encapsuler l'expression avec l'un d'entre eux.) Cela n'est pas pertinent pour le cas motivant de le placer après un |> , qui n'a qu'un seul argument, cependant.

J'ai étendu l'astuce @MikeInnes de surcharge de getindex, en utilisant Colon comme si les fonctions étaient des tableaux:

struct LazyCall{F} <: Function
    func::F
    args::Tuple
    kw::Dict
end

Base.getindex(f::Function,args...;kw...) = LazyCall{typeof(f)}(f,args,kw)

function (lf::LazyCall)(vals...; kwvals...)

    # keywords are free
    kw = merge(lf.kw, kwvals)

    # indices of free variables
    x_ = findall(x->isa(x,Colon),lf.args)
    # indices of fixed variables
    x! = setdiff(1:length(lf.args),x_)

    # the calling order is aligned with the empty spots
    xs = vcat(zip(x_,vals)...,zip(x!,lf.args[x!])...)
    args = map(x->x[2],sort(xs;by=x->x[1]))

    # unused vals go to the end
    callit = lf.func(args...,vals[length(x_)+1:end]...; kw...)

    return callit
end

[1,2,3,4,1,1,5]|> replace![ : , 1=>10, 3=>300, count=2]|> filter[>(50)]  # == [300]

log[2](2) == log[:,2](2) == log[2][2]() == log[2,2]()  # == true

C'est beaucoup plus lent que les lambdas ou les macros de threading, mais je pense que c'est super cool: p

Pour rappeler aux gens qui commentent ici, jetez un œil à la discussion pertinente sur https://github.com/JuliaLang/julia/pull/24990.

De plus, je vous encourage à essayer https://github.com/c42f/Underscores.jl qui donne une implémentation conviviale pour le chaînage de fonctions _ syntaxe @jpivarski basé sur vos exemples, vous pouvez le trouver assez familier et confortable.

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

Questions connexes

Keno picture Keno  ·  3Commentaires

felixrehren picture felixrehren  ·  3Commentaires

wilburtownsend picture wilburtownsend  ·  3Commentaires

omus picture omus  ·  3Commentaires

dpsanders picture dpsanders  ·  3Commentaires