Go: proposition : Go 2 : syntaxe de fonction anonyme légère

Créé le 17 août 2017  ·  53Commentaires  ·  Source: golang/go

De nombreux langages fournissent une syntaxe légère pour spécifier des fonctions anonymes, dans lesquelles le type de fonction est dérivé du contexte environnant.

Prenons un exemple légèrement artificiel de la tournée Go (https://tour.golang.org/moretypes/24) :

func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
}

var _ = compute(func(a, b float64) float64 { return a + b })

De nombreux langages permettent d'éliminer les types de paramètres et de retour de la fonction anonyme dans ce cas, car ils peuvent être dérivés du contexte. Par example:

// Scala
compute((x: Double, y: Double) => x + y)
compute((x, y) => x + y) // Parameter types elided.
compute(_ + _) // Or even shorter.
// Rust
compute(|x: f64, y: f64| -> f64 { x + y })
compute(|x, y| { x + y }) // Parameter and return types elided.

Je propose d'envisager d'ajouter un tel formulaire à Go 2. Je ne propose aucune syntaxe spécifique. En termes de spécification de langage, cela peut être considéré comme une forme de littéral de fonction non typé qui peut être attribué à toute variable compatible de type de fonction. Les littéraux de cette forme n'auraient pas de type par défaut et ne pourraient pas être utilisés sur le côté droit d'un := de la même manière que x := nil est une erreur.

Utilisations 1 : Cap'n Proto

Les appels distants utilisant Cap'n Proto prennent un paramètre de fonction auquel est transmis un message de requête à remplir. Depuis https://github.com/capnproto/go-capnproto2/wiki/Getting-Started :

s.Write(ctx, func(p hashes.Hash_write_Params) error {
  err := p.SetData([]byte("Hello, "))
  return err
})

En utilisant la syntaxe Rust (juste à titre d'exemple):

s.Write(ctx, |p| {
  err := p.SetData([]byte("Hello, "))
  return err
})

Utilise 2 : errgroup

Le package errgroup (http://godoc.org/golang.org/x/sync/errgroup) gère un groupe de goroutines :

g.Go(func() error {
  // perform work
  return nil
})

En utilisant la syntaxe Scala :

g.Go(() => {
  // perform work
  return nil
})

(Étant donné que la signature de la fonction est assez petite dans ce cas, cela pourrait sans doute être un cas où la syntaxe légère est moins claire.)

Go2 LanguageChange Proposal

Commentaire le plus utile

Je soutiens la proposition. Cela permet d'économiser de la frappe et d'améliorer la lisibilité.Mon cas d'utilisation,

// Type definitions and functions implementation.
type intSlice []int
func (is intSlice) Filter(f func(int) bool) intSlice { ... }
func (is intSlice) Map(f func(int) int) intSlice { ... }
func (is intSlice) Reduce(f func(int, int) int) int { ...  }
list := []int{...} 
is := intSlice(list)

sans syntaxe de fonction anonyme légère :

res := is.Map(func(i int)int{return i+1}).Filter(func(i int) bool { return i % 2 == 0 }).
             Reduce(func(a, b int) int { return a + b })

avec une syntaxe de fonction anonyme légère :

res := is.Map((i) => i+1).Filter((i)=>i % 2 == 0).Reduce((a,b)=>a+b)

Tous les 53 commentaires

Je suis sympathique à l'idée générale, mais je trouve les exemples spécifiques donnés peu convaincants : les économies relativement faibles en termes de syntaxe ne semblent pas en valoir la peine. Mais peut-être existe-t-il de meilleurs exemples ou des notations plus convaincantes.

(Peut-être à l'exception de l'exemple de l'opérateur binaire, mais je ne sais pas à quel point ce cas est courant dans le code Go typique.)

S'il vous plaît non, clair vaut mieux qu'intelligent. Je trouve ces syntaxes de raccourci
incroyablement obtus.

Le ven. 18 août 2017, 04:43 Robert Griesemer [email protected]
a écrit:

Je suis favorable à l'idée générale, mais je trouve les exemples précis
donnée peu convaincante : Le gain relativement faible en terme de syntaxe
ne semble pas en valoir la peine. Mais peut-être existe-t-il de meilleurs exemples ou
note plus convaincante.


Vous recevez ceci parce que vous êtes abonné à ce fil.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/21498#issuecomment-323159706 , ou muet
le fil
https://github.com/notifications/unsubscribe-auth/AAAcAxlgwt-iPryyY-d5w8GJho0bY9bkks5sZInfgaJpZM4O6pBB
.

Je pense que c'est plus convaincant si nous restreignons son utilisation aux cas où le corps de la fonction est une expression simple. Si nous devons écrire un bloc et un return explicite, les avantages sont quelque peu perdus.

Vos exemples deviennent alors

s.Write(ctx, p => p.SetData([]byte("Hello, "))

g.Go(=> nil)

La syntaxe est quelque chose comme

[ Identifier ] | "(" IdentifierList ")" "=>" ExpressionList

Ceci ne peut être utilisé que dans une affectation à une valeur de type fonction (y compris l'affectation à un paramètre dans le processus d'appel d'une fonction). Le nombre d'identificateurs doit correspondre au nombre de paramètres du type de fonction, et le type de fonction détermine les types d'identificateurs. Le type de fonction ne doit avoir aucun résultat, ou le nombre de paramètres de résultat doit correspondre au nombre d'expressions dans la liste. Le type de chaque expression doit pouvoir être affecté au type du paramètre de résultat correspondant. Cela équivaut à une fonction littérale de manière évidente.

Il y a probablement une ambiguïté d'analyse ici. Il serait également intéressant de considérer la syntaxe

λ [Identifier] | "(" IdentifierList ")" "." ExpressionList

un péché

s.Write(ctx, λp.p.SetData([]byte("Hello, "))

Quelques autres cas où les fermetures sont couramment utilisées.

(J'essaie principalement de collecter des cas d'utilisation pour le moment afin de fournir des preuves pour/contre l'utilité de cette fonctionnalité.)

En fait, j'aime le fait que Go ne discrimine pas les fonctions anonymes plus longues, comme le fait Java.

En Java, une courte fonction anonyme, un lambda, est agréable et courte, tandis qu'une plus longue est verbeuse et laide par rapport à la courte. J'ai même vu quelque part une conversation/poste (je ne la trouve pas maintenant) qui encourageait uniquement l'utilisation de lambdas d'une ligne en Java, car ceux-ci ont tous ces avantages de non-verbosité.

En Go, nous n'avons pas ce problème, les fonctions anonymes courtes et longues sont relativement (mais pas trop) verbeuses, il n'y a donc pas d'obstacle mental à utiliser aussi les plus longues, ce qui est parfois très utile.

La sténographie est naturelle dans les langages fonctionnels car tout est une expression et le résultat d'une fonction est la dernière expression dans la définition de la fonction.

Avoir un raccourci est bien, donc d'autres langues où ce qui précède ne tient pas l'ont adopté.

Mais d'après mon expérience, ce n'est jamais aussi agréable quand cela touche la réalité d'un langage avec des déclarations.

Il est soit presque aussi verbeux parce que vous avez besoin de blocs et de retours, soit il ne peut contenir que des expressions, il est donc fondamentalement inutile pour tout sauf pour les choses les plus simples.

Les fonctions anonymes dans Go sont à peu près aussi proches que possible de l'optimum. Je ne vois pas l'intérêt de le réduire davantage.

Ce n'est pas la syntaxe func qui pose problème, ce sont les déclarations de type redondantes.

Permettre simplement aux littéraux de fonction d'éliminer les types non ambigus irait loin. Pour reprendre l'exemple Cap'n'Proto :

s.Write(ctx, func(p) error { return p.SetData([]byte("Hello, ")) })

Oui, ce sont les déclarations de type qui ajoutent vraiment du bruit. Malheureusement, "func (p) error" a déjà un sens. Peut-être que permettre à _ de se substituer à un type inféré fonctionnerait?

s.Write(ctx, func(p _) _ { return p.SetData([]byte("Hello, ")) })

J'aime plutôt ça; aucun changement syntaxique n'est requis.

Je n'aime pas le bégaiement de _. Peut-être que func pourrait être remplacé par un mot-clé qui déduit les paramètres de type :
s.Write(ctx, λ(p) { return p.SetData([]byte("Hello, ")) })

Est-ce en fait une proposition ou êtes-vous juste en train de cracher à quoi ressemblerait Go si vous l'habilliez comme Scheme pour Halloween? Je pense que cette proposition est à la fois inutile et peu conforme à l'accent mis par le langage sur la lisibilité.

S'il vous plaît, arrêtez d'essayer de changer la syntaxe du langage simplement parce qu'il _semble_ différent des autres langages.

Je pense qu'avoir une syntaxe de fonction anonyme concise est plus convaincant dans d'autres langages qui reposent davantage sur des API basées sur le rappel. En Go, je ne suis pas sûr que la nouvelle syntaxe serait vraiment rentable. Ce n'est pas qu'il n'y ait pas beaucoup d'exemples où les gens utilisent des fonctions anonymes, mais au moins dans le code que je lis et écris, la fréquence est assez faible.

Je pense qu'avoir une syntaxe de fonction anonyme concise est plus convaincant dans d'autres langages qui reposent davantage sur des API basées sur le rappel.

Dans une certaine mesure, c'est une condition qui se renforce d'elle-même : s'il était plus facile d'écrire des fonctions concises dans Go, nous pourrions bien voir davantage d'API de style fonctionnel. (Est-ce une bonne chose ou non, je ne sais pas.)

Je tiens à souligner qu'il y a une différence entre les API "fonctionnelles" et "de rappel": quand j'entends "rappel", je pense "rappel asynchrone", ce qui conduit à une sorte de code spaghetti que nous avons eu la chance d'éviter dans Aller. Les API synchrones (telles que filepath.Walk ou strings.TrimFunc ) sont probablement le cas d'utilisation que nous devrions avoir à l'esprit, car elles s'intègrent mieux au style synchrone des programmes Go en général.

Je voudrais juste intervenir ici et proposer un cas d'utilisation où j'ai appris à apprécier la syntaxe lambda de style arrow pour réduire considérablement la friction : le curry.

envisager:

// current syntax
func add(a int) func(int) int {
    return func(b int) int {
        return a + b
    }
}

// arrow version (draft syntax, of course)
add := (a int) => (b int) => a + b

func main() {
    add2 := add(2)
    add3 := add(3)
    fmt.Println(add2(5), add3(6))
}

Imaginez maintenant que nous essayons de curry une valeur dans un mongo.FieldConvertFunc ou quelque chose qui nécessite une approche fonctionnelle, et vous verrez qu'avoir une syntaxe plus légère peut améliorer un peu les choses lors de la commutation d'une fonction de ne pas être curry à être curry (heureux de fournir un exemple plus réel si quelqu'un le souhaite).

Pas convaincu? Je ne le pensais pas. J'aime aussi la simplicité de go et je pense qu'il vaut la peine d'être protégé.

Une autre situation qui m'arrive souvent est celle où vous avez et vous voulez maintenant étoffer le prochain argument avec étriller.

maintenant tu devrais changer
func (a, b) x
à
func (a) func(b) x { return func (b) { return ...... x } }

S'il y avait une syntaxe de flèche, vous changeriez simplement
(a, b) => x
à
(a) => (b) => x

@neild bien que je n'aie pas encore contribué à ce fil, j'ai un autre cas d'utilisation qui bénéficierait de quelque chose de similaire à ce que vous avez proposé.

Mais ce commentaire concerne en fait une autre façon de gérer la verbosité dans le code d'appel : ayez un outil comme gocode (ou similaire) modèle une valeur de fonction pour vous.

Prenant ton exemple :

func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
}

Si nous supposons que nous avons tapé :

var _ = compute(
                ^

avec le curseur à la position indiquée par le ^ ; alors invoquer un tel outil pourrait trivialement modéliser une valeur de fonction pour vous donner:

var _ = compute(func(a, b float64) float64 { })
                                            ^

Cela couvrirait certainement le cas d'utilisation que j'avais en tête; couvre-t-il le vôtre ?

Le code est lu beaucoup plus souvent qu'il n'est écrit. Je ne crois pas qu'économiser un peu de frappe vaille la peine de modifier la syntaxe du langage ici. L'avantage, s'il y en a un, serait en grande partie de rendre le code plus lisible. Le support de l'éditeur n'aidera pas avec ça.

Une question, bien sûr, est de savoir si la suppression des informations de type complètes d'une fonction anonyme aide ou nuit à la lisibilité.

Je ne pense pas que ce type de syntaxe réduise la lisibilité, presque tous les langages de programmation modernes ont une syntaxe pour cela et c'est parce qu'il encourage l'utilisation d'un style fonctionnel pour réduire le passe-partout et rendre le code plus clair et plus facile à entretenir. C'est très pénible d'utiliser des fonctions anonymes dans golang lorsqu'elles sont transmises en tant que paramètres aux fonctions, car vous devez vous répéter en tapant à nouveau les types que vous savez que vous devez transmettre.

Je soutiens la proposition. Cela permet d'économiser de la frappe et d'améliorer la lisibilité.Mon cas d'utilisation,

// Type definitions and functions implementation.
type intSlice []int
func (is intSlice) Filter(f func(int) bool) intSlice { ... }
func (is intSlice) Map(f func(int) int) intSlice { ... }
func (is intSlice) Reduce(f func(int, int) int) int { ...  }
list := []int{...} 
is := intSlice(list)

sans syntaxe de fonction anonyme légère :

res := is.Map(func(i int)int{return i+1}).Filter(func(i int) bool { return i % 2 == 0 }).
             Reduce(func(a, b int) int { return a + b })

avec une syntaxe de fonction anonyme légère :

res := is.Map((i) => i+1).Filter((i)=>i % 2 == 0).Reduce((a,b)=>a+b)

Le manque d'expressions de fonction anonymes concises rend Go moins lisible et viole le principe DRY. Je voudrais écrire et utiliser des API fonctionnelles/de rappel, mais l'utilisation de telles API est extrêmement verbeuse, car chaque appel d'API doit soit utiliser une fonction déjà définie, soit une expression de fonction anonyme qui répète des informations de type qui devraient être assez claires du contexte (si l'API est conçue correctement).

Mon désir pour cette proposition n'est même pas de loin que je pense que Go devrait ressembler ou être comme d'autres langues. Mon désir est entièrement motivé par mon aversion pour me répéter et inclure des bruits syntaxiques inutiles.

Dans Go, la syntaxe des déclarations de fonction s'écarte un peu du modèle habituel que nous avons pour les autres déclarations. Pour les constantes, types, variables on a toujours :

keyword name type value

Par example:

const   c    int  = 0
type    t    foo
var     v    bool = true

En général, le type peut être un type littéral ou un nom. Pour les fonctions, cela se décompose, le type doit toujours être une signature littérale. On pourrait imaginer quelque chose comme :

type BinaryOp func(x, y Value) Value

func f BinaryOp { ... }

où le type de fonction est donné sous forme de nom. En développant un peu, une fermeture BinaryOp pourrait alors peut-être être écrite comme

BinaryOp{ return x.Add(y) }

ce qui pourrait contribuer grandement à une notation de fermeture plus courte. Par exemple:

vector.Apply(BinaryOp{ return x.Add(y) })

Le principal inconvénient est que les noms de paramètres ne sont pas déclarés avec la fonction. L'utilisation du type de fonction les amène "dans la portée", de la même manière que l'utilisation d'une valeur struct x de type S amène un champ f dans la portée d'une expression de sélecteur x.f ou un littéral de structure S{f: "foo"} .

En outre, cela nécessite un type de fonction explicitement déclaré, ce qui n'a de sens que si ce type est très courant.

Juste une autre perspective pour cette discussion.

La lisibilité vient en premier, cela semble être quelque chose sur lequel nous pouvons tous être d'accord.

Mais cela dit, une chose sur laquelle je veux également intervenir (puisqu'il ne semble pas que quelqu'un d'autre l'ait dit explicitement) est que la question de la lisibilité dépendra toujours de ce à quoi vous êtes habitué. À mon avis, avoir une discussion sur la question de savoir si cela nuit ou nuit à la lisibilité ne mènera nulle part.

@griesemer peut-être qu'une certaine perspective de votre temps de travail sur V8 serait utile ici. Je peux (au moins) dire que j'étais très satisfait de la syntaxe antérieure de javascript pour les fonctions ( function(x) { return x; } ) qui était (d'une certaine manière) encore plus lourde à lire que celle de Go en ce moment. J'étais dans le camp "cette nouvelle syntaxe est une perte de temps" de @douglascrockford .

Mais, tout de même, la syntaxe de la flèche _est arrivée_ et je l'ai acceptée _parce que je devais_. Aujourd'hui, cependant, après l'avoir utilisé beaucoup plus et être devenu plus à l'aise avec, je peux dire que cela améliore énormément la lisibilité . J'ai utilisé le cas du currying (et @hooluupog a évoqué un cas similaire de "dot-chaining") où une syntaxe légère produit un code léger sans être trop intelligent.

Maintenant, quand je vois du code qui fait des choses comme x => y => z => ... et qu'il est beaucoup plus facile à comprendre en un coup d'œil (encore une fois... parce que je suis _familier_ avec ça. Il n'y a pas si longtemps, j'ai ressenti le contraire).

Ce que je dis, c'est : cette discussion se résume à :

  1. Quand on n'y est pas habitué, cela semble _vraiment_ étrange et limite inutile sinon nuisible à la lisibilité. Certaines personnes ont juste ou n'ont pas de sentiment d'une manière ou d'une autre à ce sujet.
  2. Plus vous faites de programmation fonctionnelle, plus le besoin d'une telle syntaxe se fait sentir. Je suppose que cela a quelque chose à voir avec des concepts fonctionnels (comme l'application partielle et le curry) qui introduisent de nombreuses fonctions pour de petits travaux, ce qui se traduit par du bruit pour le lecteur.

La meilleure chose que nous puissions faire est de fournir plus de cas d'utilisation.

En réponse au commentaire de @dimitropoulos , voici un résumé approximatif de mon point de vue :

Je souhaite utiliser des modèles de conception (tels que la programmation fonctionnelle) qui bénéficieraient grandement de cette proposition, car leur utilisation avec la syntaxe actuelle est excessivement détaillée.

@dimitropoulos J'ai bien travaillé sur V8, mais c'était la construction de la machine virtuelle, qui a été écrite en C++. Mon expérience avec Javascript réel est limitée. Cela dit, Javascript est un langage à typage dynamique, et sans types, une grande partie du typage disparaît. Comme plusieurs personnes l'ont déjà mentionné, un problème majeur ici est la nécessité de répéter les types, un problème qui n'existe pas en Javascript.

Aussi, pour mémoire : au début de la conception de Go, nous avons en fait examiné la syntaxe des flèches pour les signatures de fonction. Je ne me souviens pas des détails, mais je suis à peu près sûr que des notations telles que

func f (x int) -> float32

était sur le tableau blanc. Finalement, nous avons laissé tomber la flèche car elle ne fonctionnait pas très bien avec plusieurs valeurs de retour (non-tuple) ; et une fois le func et les paramètres présents, la flèche était superflue ; peut-être "joli" (comme dans l'aspect mathématique), mais toujours superflu. Cela ressemblait également à une syntaxe appartenant à un type de langage "différent".

Mais le fait d'avoir des fermetures dans un langage performant et polyvalent a ouvert les portes à de nouveaux styles de programmation plus fonctionnels. Maintenant, 10 ans plus tard, on pourrait le voir sous un angle différent.

Pourtant, je pense que nous devons être très prudents ici pour ne pas créer de syntaxe spéciale pour les fermetures. Ce que nous avons maintenant est simple et régulier et a bien fonctionné jusqu'à présent. Quelle que soit l'approche, s'il y a un changement, je pense qu'il devra être régulier et s'appliquer à n'importe quelle fonction.

Dans Go, la syntaxe des déclarations de fonction s'écarte un peu du modèle habituel que nous avons pour les autres déclarations. Pour les constantes, types, variables on a toujours :
keyword name type value
[…]
Pour les fonctions, cela se décompose, le type doit toujours être une signature littérale.

Notez que pour les listes de paramètres et les déclarations const et var , nous avons un modèle similaire, IdentifierList Type , que nous devrions probablement également conserver. Cela semble exclure le jeton : style lambda-calculus pour séparer les noms de variables des types.

Quelle que soit l'approche, s'il y a un changement, je pense qu'il devra être régulier et s'appliquer à n'importe quelle fonction.

Le modèle keyword name type value est pour les _declarations_, mais les cas d'utilisation que @neild mentionne sont tous pour les _literals_.

Si nous abordons le problème des littéraux, alors je crois que le problème des déclarations devient trivial. Pour les déclarations de constantes, de variables et maintenant de types, nous autorisons (ou exigeons) un jeton = avant le value . Il semble qu'il serait assez facile d'étendre cela aux fonctions:

FunctionDecl = "func" ( FunctionSpec | "(" { FunctionSpec ";" } ")" ).
FunctionSpec = FunctionName Function |
               IdentifierList (Signature | [ Signature ] "=" Expression) .

FunctionLit = "func" Function | ShortFunctionLit .
ShortParameterList = ShortParameterDecl { "," ShortParameterDecl } .
ShortParameterDecl = IdentifierList [ "..." ] [ Type ] .

L'expression après le jeton = doit être un littéral de fonction, ou peut-être une fonction renvoyée par un appel dont les arguments sont tous disponibles au moment de la compilation. Dans la forme = , un Signature peut toujours être fourni pour déplacer les déclarations de type d'argument du littéral vers le FunctionSpec .

Notez que la différence entre un ShortParameterDecl et le ParameterDecl existant est que les singletons IdentifierList sont interprétés comme des noms de paramètres au lieu de types.


Exemples

Considérez cette déclaration de fonction acceptée aujourd'hui :

func compute(f func(x, y float64) float64) float64 { return f(3, 4) }

Nous pourrions soit conserver cela (par exemple pour la compatibilité Go 1) en plus des exemples ci-dessous, soit éliminer la production Function et utiliser uniquement la version ShortFunctionLit .

Pour diverses options ShortFunctionLit , la grammaire que je propose ci-dessus donne :

Rouille :

ShortFunctionLit = "|" ShortParameterList "|" Block .

Admet l'un des éléments suivants :

func compute = |f func(x, y float64) float64| { f(3, 4) }
func compute(func (x, y float64) float64) float64 = |f| { f(3, 4) }



md5-c712da47cbcf3d0379ff810dfd76ce59



```go
func (
    compute(func (x, y float64) float64) float64 = |f| { f(3, 4) }
)



md5-8a4d86e5ac5f718d8d35839eaf9f1029



ShortFunctionLit = "(" ShortParameterList ")" "=>" Expression .



md5-e429c4db0e2a76fe83f1f524910c0075



```go
func compute(func (x, y float64) float64) float64 = (f) => f(3, 4)



md5-bcb7677c087284f6121b65ce14d46d93



```go
func (
    compute(func (x, y float64) float64) float64 = (f) => f(3, 4)
)



md5-bf0cf8ca5f55bbedf92dc2047d871378



ShortFunctionLit = "λ" ShortParameterList "." Expression .



md5-3c1a0d273a1aee09721883f5be8fcfce



```go
func compute(func (x, y float64) float64) float64) = λf.f(3, 4)



md5-87735958588cf5a763da8a89d1f9a675



```go
func (
    compute(func (x, y float64) float64) float64) = λf.f(3, 4)
)



md5-d613a37ac429244205560535e5401d63



ShortFunctionLit = "\" ShortParameterList "->" Expression .



md5-95523002741f1036dff7837c1701336d



```go
func compute(func (x, y float64) float64) float64) = \f -> f(3, 4)



md5-818e7097669fe3bc7a333787735e5657



```go
func (
    compute(func (x, y float64) float64) float64) = \f -> f(3, 4)
)



md5-af63df358fad8d4beffd23e2d0c337a4



ShortFunctionLit = "[" ShortParameterList "]" Block .



md5-f66b9b33e7dca8cce60726de14cfc931



```go
func compute(func (x, y float64) float64) float64) = [f] { f(3, 4) }



md5-13e2e0ab357ce95a5a0e2fbd930ba841



```go
func (
    compute(func (x, y float64) float64) float64) = [f] { f(3, 4) }
)

Personnellement, je trouve que toutes les variantes de type Scala sont assez lisibles. (À mes yeux, la variante de type Scala est trop lourde sur les parenthèses : elle rend les lignes beaucoup plus difficiles à numériser.)

Personnellement, cela m'intéresse principalement si cela me permet d'omettre les types de paramètres et de résultats lorsqu'ils peuvent être déduits. Je suis même d'accord avec la syntaxe littérale de la fonction actuelle si je peux le faire. (Cela a été discuté ci-dessus.)

Certes, cela va à l'encontre du commentaire de @griesemer .

Quelle que soit l'approche, s'il y a un changement, je pense qu'il devra être régulier et s'appliquer à n'importe quelle fonction.

Je ne suis pas tout à fait cela. Les déclarations de fonction doivent nécessairement inclure les informations de type complètes pour la fonction, car il n'y a aucun moyen de les dériver avec une précision suffisante à partir du corps de la fonction. (Ce n'est pas le cas pour toutes les langues, bien sûr, mais c'est le cas pour Go.)

Les littéraux de fonction, en revanche, pourraient déduire des informations de type à partir du contexte.

@neild Toutes mes excuses pour être imprécis : ce que je voulais dire par cette phrase, c'est que s'il y avait une nouvelle syntaxe différente (flèches ou autre), elle devrait être quelque peu régulière et s'appliquer partout. S'il est possible que des types puissent être omis, ce serait à nouveau orthogonal.

@griesemer Merci ; Je suis (surtout) d'accord avec ce point.

Je pense que la question intéressante pour cette proposition est de savoir si avoir une certaine syntaxe est une bonne idée ou non ; ce que serait cette syntaxe est important mais relativement trivial.

Cependant, je ne peux pas résister à la tentation d'abandonner un peu ma propre proposition.

var sum func(int, int) int = func a, b { return a + b }

La proposition de @neild me convient . Elle est assez proche de la syntaxe existante, mais fonctionne pour la programmation fonctionnelle car elle élimine la répétition des spécifications de type. Ce n'est pas _que_ beaucoup moins compact que (a, b) => a + b , et il s'intègre bien dans la syntaxe existante.

@neild

var sum func(int, int) int = func a, b { return a + b }

Cela déclarerait-il une variable ou une fonction ? S'il s'agit d'une variable, à quoi ressemblerait la déclaration de fonction équivalente ?

Sous mon schéma de déclaration ci-dessus, si je comprends bien, ce serait:

ShortFunctionLit = "func" ShortParameterList Block .
func compute = func f func(x, y float64) float64 { return f(3, 4) }
func compute(func (x, y float64) float64) float64 = func f { return f(3, 4) }
func (
    compute = func f func(x, y float64) float64 { return f(3, 4) }
)
func (
    compute(func (x, y float64) float64) float64 = func f { return f(3, 4) }
)

Je ne pense pas être un fan : il bégaie un peu sur func , et ne semble pas fournir suffisamment de rupture visuelle entre le jeton func et les paramètres qui suivent.

Ou omettez-vous les parenthèses de la déclaration, plutôt que d'attribuer des littéraux ?

func compute f func(x, y float64) float64 { return f(3, 4) }

Je n'aime toujours pas le manque de pause visuelle, cependant...

Cela déclarerait-il une variable ou une fonction ? S'il s'agit d'une variable, à quoi ressemblerait la déclaration de fonction équivalente ?

Une variable. La déclaration de fonction équivalente serait vraisemblablement func sum a, b { return a+b } , mais ce ne serait pas valide pour des raisons évidentes - vous ne pouvez pas élider les types de paramètres des déclarations de fonction.

Le changement de grammaire auquel je pense serait quelque chose comme:

ShortFunctionLit = "func" [ IdentifierList ] [ "..." ] FunctionBody .

Un littéral de fonction court se distingue d'un littéral de fonction ordinaire en omettant les parenthèses dans la liste des paramètres, définit uniquement les noms des paramètres entrants et ne définit pas les paramètres sortants. Les types des paramètres entrants et les types et le nombre de paramètres sortants sont dérivés du contexte environnant.

Je ne pense pas qu'il soit nécessaire d'autoriser la spécification de types de paramètres facultatifs dans un littéral de fonction court ; vous utilisez simplement une fonction littérale régulière dans ce cas.

Comme @ianlancetaylor l' a souligné, la notation légère n'a vraiment de sens que lorsqu'elle permet l'omission de types de paramètres car ils peuvent être facilement déduits. En tant que telle, la suggestion de @neild est la meilleure et la plus simple que j'ai vue jusqu'à présent. La seule chose qu'il ne permet pas facilement est une notation légère pour les littéraux de fonction qui veulent faire référence à des paramètres de résultat nommés. Mais peut-être dans ce cas devraient-ils utiliser la notation complète. (C'est juste un peu irrégulier).

Nous pourrions même être en mesure d'analyser (x, y) { ... } sous forme courte pour func (x, y T) T { ... } ; bien que cela nécessiterait un peu d'anticipation de l'analyseur, mais peut-être pas trop mal.

À titre expérimental, j'ai modifié gofmt pour réécrire les littéraux de fonction dans la syntaxe compacte et l'ai exécuté sur src/. Vous pouvez voir les résultats ici :

https://github.com/neild/go/commit/2ff18c6352788aa8f8cbe8b5d5d4c73956ca7c6f

Je n'ai fait aucune tentative pour limiter cela aux cas où cela a du sens; Je voulais juste avoir une idée de la façon dont la syntaxe compacte pourrait se dérouler dans la pratique. Je ne l'ai pas encore assez creusé pour développer une opinion sur les résultats.

@neild Belle analyse. Quelques remarques :

  1. La fraction de cas dans lesquels la fonction littérale est liée à l'aide := est décevante, car la gestion de ces cas sans annotations de type explicites nécessiterait un algorithme d'inférence plus compliqué.

  2. Les littéraux transmis aux rappels sont plus faciles à lire dans certains cas, mais plus difficiles dans d'autres.
    Par exemple, perdre les informations de type de retour pour les littéraux de fonction qui s'étendent sur plusieurs lignes est un peu regrettable, car cela indique également au lecteur s'il regarde une API fonctionnelle ou impérative.

  3. La réduction du passe-partout pour les littéraux de fonction dans les tranches est substantielle.

  4. Les instructions defer et go sont un cas intéressant : déduirions-nous les types d'arguments à partir des arguments réellement passés à la fonction ?

  5. Quelques jetons de fin ... manquent dans les exemples.

defer et go sont en effet un cas assez intéressant.

go func p {
  // do something with p
}("parameter")

Dériverions-nous le type de p du paramètre de fonction réel ? Ce serait plutôt bien pour les instructions go , bien que vous puissiez bien sûr obtenir le même effet en utilisant simplement une fermeture :

p := "parameter"
go func() {
  // do something with p
}()

Je soutiendrais totalement cela. Franchement, je me fiche de savoir à quel point cela "ressemble à d'autres langages", je veux juste une manière moins verbeuse d'utiliser des fonctions anonymes.

EDIT : Emprunter la syntaxe littérale composite...

type F func(int) float64
var f F
f = F {      (i) (o) { o = float64(i); return } }
f = F {      (i) o   { o = float64(i); return } } // single return value
f = F { func (i) o   { o = float64(i); return } } // +func for good measure?

Juste une idée:
Voici à quoi ressemblerait l'exemple d'OP avec un _littéral de fonction non typé_ avec la syntaxe de Swift :

compute({ $0 + $1 })

Je pense que cela aurait l'avantage d'être entièrement rétrocompatible avec Go 1.

Je viens de trouver cela parce que j'écrivais une simple application de chat tcp,
fondamentalement, j'ai une structure avec une tranche à l'intérieur

type connIndex struct {
    conns []net.Conn
    mu    sync.Mutex
}

et j'aimerais lui appliquer certaines opérations simultanément (ajouter des connexions, envoyer des messages à tous, etc.)

et au lieu de suivre le chemin normal de copier-coller le code de verrouillage mutex, ou d'utiliser une goroutine démon pour gérer l'accès, je pensais que je passerais juste une fermeture

func (c *connIndex) run(f func([]net.Conn)) {
    c.mu.Lock()
    defer c.mu.Unlock()
    f(c.conns)
}

pour les opérations courtes, c'est trop verbeux (encore mieux lock et defer unlock() )

conns.run(func(conns []net.Conn) { conns = append(conns, conn) })

Cela viole le principe DRY car j'ai tapé cette signature de fonction exacte dans la méthode run .

Si go pris en charge en déduisant la signature de la fonction, je pourrais l'écrire comme ceci

conns.run(func(conns) { conns = append(conns, conn) })

Je ne pense pas que cela rende le code moins lisible, vous pouvez dire que c'est une tranche à cause de append , et parce que j'ai bien nommé mes variables, vous pouvez deviner qu'il s'agit d'un []net.Conn sans regarder à la signature de la méthode run .

J'éviterais d'essayer de déduire les types de paramètres basés sur le corps de la fonction, mais j'ajouterais plutôt une inférence uniquement pour les cas où cela est évident (comme le passage de fermetures à des fonctions).

je dirais que cela ne nuit pas à la lisibilité car cela donne au lecteur une option, s'il ne connaît pas le type de paramètre, il peut godef ou le survoler et demander à l'éditeur de le lui montrer .

Un peu comme dans un livre, ils ne répètent pas l'introduction des personnages, sauf que nous aurions un bouton pour l'afficher / y accéder.

Je suis mauvais en écriture alors j'espère que vous avez survécu en lisant ceci :)

Je pense que c'est plus convaincant si nous restreignons son utilisation aux cas où le corps de la fonction est une expression simple.

J'ose objecter. Cela conduirait toujours à deux façons de définir une fonction, et l'une des raisons pour lesquelles je suis tombé amoureux de Go est que même s'il a une certaine verbosité ici et là, il a une expressivité rafraîchissante : vous voyez où se trouve une fermeture parce qu'il y a soit un mot-clé func , soit le paramètre est une fonction, si vous le tracez.

conns.run(func(conns []net.Conn) { conns = append(conns, conn) })
Cela viole le principe DRY car j'ai tapé cette signature de fonction exacte dans la méthode d'exécution.

SEC _is_ important, sans aucun doute. Mais l'appliquer à chaque partie de la programmation dans le but de maintenir le principe au détriment de la capacité à comprendre le code avec le moins d'effort possible, c'est un peu dépasser la cible, à mon humble avis.

Je pense que le problème général ici (et quelques autres propositions) est que la discussion porte principalement sur la manière de sécuriser l'effort de _écriture_ du code, alors qu'à mon humble avis, il devrait s'agir de la manière de sécuriser l'effort de _lecture_ du code. Des années après qu'on l'ait écrit. J'ai récemment trouvé un de mes poc.pl et j'essaie toujours de comprendre ce qu'il fait... ;)

conns.run(func(conns) { conns = append(conns, conn) })
Je ne pense pas que cela rende le code moins lisible, vous pouvez dire que c'est une tranche à cause de l'ajout, et parce que j'ai bien nommé mes variables, vous pouvez deviner qu'il s'agit d'un []net.Conn sans regarder la signature de la méthode d'exécution .

De mon point de vue, il y a plusieurs problèmes avec cette déclaration. Je ne sais pas comment les autres le voient, mais je déteste deviner. On peut avoir raison, on peut avoir tort, mais il faut sûrement y mettre des efforts - pour l'avantage d'économiser pour "taper" []net.Conn . Et la lisibilité ainsi que la compréhensibilité du code doivent être soutenues par de bons noms de variables, et non basés sur celui-ci.

Pour conclure : je pense que l'objectif de la discussion devrait s'éloigner de la manière de réduire les efforts mineurs lors de l'écriture de code vers la manière de réduire les efforts pour comprendre ledit code.

Je termine en citant Dave Cheney citant Robert Pike (iirc)

Clair vaut mieux qu'intelligent.

L'ennui de taper des signatures de fonction peut être quelque peu soulagé par la complétion automatique. Par exemple, gopls propose des complétions qui créent des littéraux de fonction :
cb

Je pense que cela fournit un bon terrain d'entente où les noms de type sont toujours dans le code source, il ne reste qu'une seule façon de définir une fonction anonyme, et vous n'avez pas à taper la signature entière.

cela sera rajouté ou pas ?
... pour ceux qui n'aiment pas cette fonctionnalité, ils peuvent toujours utiliser l'ancienne syntaxe.
... pour nous qui voulons plus de simplicité, nous pouvons utiliser cette nouvelle fonctionnalité, espérons-le, cela fait 1 an que j'ai écrit go, je ne sais pas si la communauté pense toujours que c'est important,
... cela sera-t-il ajouté ou non ?

@noypi Aucune décision n'a été prise. Cette question reste ouverte.

https://golang.org/wiki/NoPlusOne

Je soutiens cette proposition et je pense que cette fonctionnalité, associée aux génériques, rendrait la programmation fonctionnelle dans Go plus conviviale pour les développeurs.

Voici ce que j'aimerais voir, grosso modo :

type F func(int, int) int

// function declaration
f := F (x, y) { return x * y}

// function passing 
// g :: func(F)
g((x, y) { return x * y })

// returning function
func h() F {
    return (x, y) { return x * y }
}

J'aimerais pouvoir taper (a, b) => a * b et passer à autre chose.

Je ne peux pas croire que les fonctions fléchées ne soient toujours pas disponibles dans Go lang.
Il est étonnant de voir à quel point il est clair et simple de travailler avec Javascript.

JavaScript peut implémenter cela de manière triviale car il ne se soucie pas des paramètres, de leur nombre, des valeurs ou de leurs types jusqu'à ce qu'ils soient réellement utilisés.

Pouvoir omettre des types dans les littéraux de fonction aiderait beaucoup avec le style fonctionnel que j'utilise pour l'API de mise en page Gio. Voir les nombreux littéraux "func() {...}" dans https://git.sr.ht/~eliasnaur/gio/tree/master/example/kitchen/kitchen.go ? Leur signature réelle aurait dû être quelque chose comme

func(gtx layout.Context) layout.Dimensions

mais à cause des noms de type longs, le gtx est un pointeur vers un layout.Context partagé qui contient les valeurs entrantes et sortantes de chaque appel de fonction.

Je vais probablement passer aux signatures plus longues quel que soit ce problème, pour plus de clarté et d'exactitude. Néanmoins, je pense que mon cas est un bon rapport d'expérience à l'appui de littéraux de fonction plus courts.

PS Une des raisons pour lesquelles je penche vers les signatures plus longues est qu'elles peuvent être raccourcies par des alias de type :

type C = layout.Context
type D = layout.Dimensions

qui raccourcit les littéraux à func(gtx C) D { ... } .

Une deuxième raison est que les signatures plus longues sont compatibles avec tout ce qui résout ce problème.

Je suis venu ici avec une idée et j'ai découvert que @networkimprov avait déjà suggéré quelque chose de similaire ici .

J'aime l'idée d'utiliser un type de fonction (pourrait aussi être un type de fonction sans nom ou un alias) comme spécificateur d'un littéral de fonction, car cela signifie que nous pouvons utiliser les règles d'inférence de type habituelles pour les paramètres et les valeurs de retour, car nous connaissons le types exacts à l'avance. Cela signifie que (par exemple) l'auto-complétion peut fonctionner comme d'habitude et nous n'aurions pas besoin d'introduire des règles d'inférence de type top-down géniales.

Donné:

type F func(a, b int) int

ma pensée originale était:

F(a, b){return a + b}

mais cela ressemble trop à un appel de fonction normal - il ne semble pas que a et b y soient définis.

Jeter d'autres possibilités (je n'aime aucune d'entre elles particulièrement):

F->(a, b){return a + b}
F::(a, b){return a + b}
(a, b := F){ return a + b }
F{a, b}{return a + b}
F{a, b: return a + b}
F{a, b; return a + b}

Peut-être y a-t-il une belle syntaxe qui se cache ici quelque part :)

Un point clé de la syntaxe littérale composite est qu'elle ne nécessite pas d'informations de type dans l'analyseur. La syntaxe des structures, des tableaux, des tranches et des cartes est identique ; l'analyseur n'a pas besoin de connaître le type de T pour générer un arbre de syntaxe pour T{...} .

Un autre point est que la syntaxe ne nécessite pas non plus de retour en arrière dans l'analyseur. Lorsqu'il y a ambiguïté quant à savoir si un { fait partie d'un littéral composite ou d'un bloc, cette ambiguïté est toujours résolue en faveur de ce dernier.

J'aime toujours assez la syntaxe que j'ai proposée quelque part plus tôt dans ce numéro, qui évite toute ambiguïté de l'analyseur en conservant le mot-clé func :

func a, b { return a + b }

J'ai enlevé mon :-1:. Je n'y suis toujours pas :+1: mais je reconsidère ma position. Les génériques vont entraîner une augmentation des fonctions courtes comme genericSorter(slice, func(a, b T) bool { return a > b }) . J'ai également trouvé https://github.com/golang/go/issues/37739#issuecomment -624338848 convaincant.

Il existe deux façons principales de rendre les littéraux de fonction plus concis :

  1. une forme abrégée pour les corps qui renvoient une expression
  2. élidant les types dans les littéraux de fonction.

Je pense que les deux doivent être traités séparément.

Si FunctionBody est remplacé par quelque chose comme

FunctionBody = Block | "->" ExpressionBody
ExpressionBody = Expression | "(" ExpressionList ")"

cela aiderait principalement les littéraux de fonction avec ou sans élision de type et permettrait également aux déclarations de fonctions et de méthodes très simples d'être plus légères sur la page :

func (*T) Close() error -> nil

func (e *myErr) Unwrap() error -> e.err

func Alias(x int) -> anotherPackage.OriginalFunc(x)

func Id(type T)(x T) T -> x

func Swap(type T)(x, y T) -> (y, x)

(godoc et ses amis pourraient encore cacher le corps)

J'ai utilisé la syntaxe de @ianlancetaylor dans cet exemple, dont le principal inconvénient est qu'il nécessite l'introduction d'un nouveau jeton (et un qui aurait l'air étrange dans func(c chan T) -> <-c !) mais ça pourrait être bien de réutiliser un jeton existant tel que "=", s'il n'y a pas d'ambiguïté. J'utiliserai "=" dans le reste de ce post.

Pour l'élision de type il y a deux cas

  1. quelque chose qui fonctionne toujours
  2. quelque chose qui ne fonctionne que dans un contexte où les types peuvent être déduits

L'utilisation de types nommés comme suggéré par @griesemer fonctionnerait toujours. Il semble y avoir des problèmes avec la syntaxe. Je suis sûr que cela pourrait s'arranger. Même s'ils l'étaient, je ne suis pas sûr que cela résoudrait le problème. Cela nécessiterait une prolifération de types nommés. Ceux-ci seraient soit dans le package définissant l'endroit où ils sont utilisés, soit ils devraient être définis dans chaque package les utilisant.

Dans le premier cas, vous obtenez quelque chose comme

slices.Map(s, slices.MapFunc(x) = math.Abs(x-y))

et dans ce dernier vous obtenez quelque chose comme

type mf func(float64) float64
slices.Map(s, mf(x) = math.Abs(x-y))

Quoi qu'il en soit, il y a suffisamment d'encombrement pour ne pas vraiment réduire le passe-partout à moins que chaque nom ne soit beaucoup utilisé.

Une syntaxe comme celle de @neild ne peut être utilisée que lorsque les types peuvent être déduits. Une méthode simple serait comme dans # 12854, répertoriez simplement tous les contextes où le type est connu - paramètre d'une fonction, affecté à un champ, envoyé sur un canal, etc. Le cas go/defer que @neild a évoqué semble également utile à inclure.

Cette approche ne permet spécifiquement pas ce qui suit

zero := func = 0
var f interface{} = func x, y = g(y, x)

mais ce sont des cas où il serait payant d'être plus explicite, même s'il était possible de déduire le type de manière algorithmique en examinant où et comment ceux-ci sont utilisés.

Il permet de nombreux cas utiles, y compris les plus utiles/demandés :

slices.Map(s, func x = math.Abs(x-y))
v := cond(useTls, FetchCertificate, func = nil)

pouvoir choisir d'utiliser un bloc indépendant de la syntaxe littérale permet aussi :

http.HandleFunc("/bar", func w, r {
  // many lines ...
})

qui est un cas particulier me poussant de plus en plus vers un :+1:

Une question que je n'ai pas vue soulevée est de savoir comment traiter les paramètres ... . Vous pourriez faire un argument pour l'un ou l'autre

f(func x, p = len(p))
f(func x, ...p = len(p))

Je n'ai pas de réponse à cela.

@jimmyfrasche

  1. élidant les types dans les littéraux de fonction.

Je pense que cela devrait être géré avec l'ajout de littéraux de type fonction. Où le type remplace 'func' et les types d'arguments sont émis (tels qu'ils sont définis par le type). Cela maintient la lisibilité et est assez cohérent avec les littéraux des autres types.

http.Handle("/", http.HandlerFunc[w, r]{
    fmt.Fprinf(w, "Hello World")
})
  1. une forme abrégée pour les corps qui renvoient une expression

Refactorisez la fonction comme son propre type, puis les choses deviennent beaucoup plus propres.

type ComputeFunc func(float64, float64) float64

func compute(fn ComputeFunc) float64 {
    return fn(3, 4)
}

compute(ComputeFunc[a,b]{return a + b})

Si c'est trop verbeux pour vous, tapez alias le type de fonction dans votre code.

{
    type f = ComputeFunc

    compute(f[a,b]{return a + b})
}

Dans le cas particulier d'une fonction sans arguments, les parenthèses doivent être omises.

type IntReturner func() int

fmt.Println(IntReturner{return 2}())

Je choisis des crochets parce que la proposition de contrats utilise déjà des crochets standard supplémentaires pour les fonctions génériques.

@Splizard Je maintiens l'argument selon lequel cela ne ferait que repousser l'encombrement de la syntaxe littérale dans de nombreuses définitions de type supplémentaires. Chacune de ces définitions devrait être utilisée au moins deux fois avant de pouvoir être plus courte que la simple écriture des types au littéral.

Je ne suis pas sûr non plus que cela jouerait trop bien avec les génériques dans tous les cas.

Considérez la fonction plutôt étrange

func X(type T)(v T, func() T)

Vous pouvez nommer un type générique à utiliser avec X :

type XFunc(type T) func() T

Si seule la définition de XFunc est utilisée pour dériver les types des paramètres, lors de l'appel X , vous devrez lui dire quel T utiliser même si cela est déterminé par le type de v :

X(v, XFunc(T)[] { /* ... */ })

Il pourrait y avoir un cas particulier pour des scénarios comme celui-ci pour permettre de déduire T , mais vous vous retrouveriez alors avec une grande partie de la machinerie nécessaire pour l'élision de type dans les littéraux func.

Vous pouvez également simplement définir un nouveau type pour chaque T avec lequel vous appelez X , mais il n'y a pas beaucoup d'économies à moins que vous n'appeliez X plusieurs fois pour chaque T .

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