Go: propuesta: Go 2: Sintaxis ligera de funciones anónimas

Creado en 17 ago. 2017  ·  53Comentarios  ·  Fuente: golang/go

Muchos lenguajes proporcionan una sintaxis ligera para especificar funciones anónimas, en las que el tipo de función se deriva del contexto circundante.

Considere un ejemplo ligeramente artificial del recorrido 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 })

Muchos lenguajes permiten elidir el parámetro y los tipos de retorno de la función anónima en este caso, ya que pueden derivarse del contexto. Por ejemplo:

// 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.

Propongo considerar agregar dicho formulario a Go 2. No estoy proponiendo ninguna sintaxis específica. En términos de la especificación del lenguaje, esto puede considerarse como una forma de literal de función sin tipo que se puede asignar a cualquier variable compatible de tipo de función. Los literales de esta forma no tendrían un tipo predeterminado y no podrían usarse en el lado derecho de := de la misma manera que x := nil es un error.

Usos 1: Cap'n Proto

Las llamadas remotas que usan Cap'n Proto toman un parámetro de función al que se le pasa un mensaje de solicitud para completar. Desde 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
})

Usando la sintaxis de Rust (solo como ejemplo):

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

Usos 2: errgroup

El paquete errgroup (http://godoc.org/golang.org/x/sync/errgroup) administra un grupo de goroutines:

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

Usando la sintaxis de Scala:

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

(Dado que la firma de la función es bastante pequeña en este caso, podría decirse que este es un caso en el que la sintaxis ligera es menos clara).

Go2 LanguageChange Proposal

Comentario más útil

Apoyo la propuesta. Ahorra tipeo y ayuda a la legibilidad. Mi caso de uso,

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

sin sintaxis de función anónima ligera:

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

con sintaxis de función anónima ligera:

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

Todos 53 comentarios

Simpatizo con la idea general, pero los ejemplos específicos dados no me parecen muy convincentes: los ahorros relativamente pequeños en términos de sintaxis no parecen valer la pena. Pero tal vez haya mejores ejemplos o notaciones más convincentes.

(Quizás con la excepción del ejemplo del operador binario, pero no estoy seguro de qué tan común es ese caso en el código típico de Go).

Por favor, no, claro es mejor que inteligente. Encuentro estas sintaxis de acceso directo
imposiblemente obtuso.

El viernes, 18 de agosto de 2017, 04:43 Robert Griesemer [email protected]
escribió:

Simpatizo con la idea general, pero encuentro los ejemplos específicos
dado que no es muy convincente: los ahorros relativamente pequeños en términos de sintaxis
no parece valer la pena. Pero tal vez hay mejores ejemplos o
notación más convincente.


Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/21498#issuecomment-323159706 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AAAcAxlgwt-iPryyY-d5w8GJho0bY9bkks5sZInfgaJpZM4O6pBB
.

Creo que esto es más convincente si restringimos su uso a casos en los que el cuerpo de la función es una expresión simple. Si estamos obligados a escribir un bloque y un return explícito, los beneficios se pierden un poco.

Tus ejemplos se convierten entonces

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

g.Go(=> nil)

La sintaxis es algo como

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

Esto solo se puede usar en una asignación a un valor de tipo función (incluida la asignación a un parámetro en el proceso de una llamada de función). El número de identificadores debe coincidir con el número de parámetros del tipo de función y el tipo de función determina los tipos de identificador. El tipo de función debe tener cero resultados o el número de parámetros de resultado debe coincidir con el número de expresiones de la lista. El tipo de cada expresión debe ser asignable al tipo del parámetro de resultado correspondiente. Esto es equivalente a una función literal de la manera obvia.

Probablemente haya una ambigüedad de análisis aquí. También sería interesante considerar la sintaxis

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

como en

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

Algunos casos más donde los cierres se usan comúnmente.

(Principalmente estoy tratando de recopilar casos de uso en este momento para proporcionar evidencia a favor o en contra de la utilidad de esta función).

De hecho, me gusta que Go no discrimine funciones anónimas más largas, como lo hace Java.

En Java, una función anónima corta, una lambda, es agradable y corta, mientras que una más larga es detallada y fea en comparación con la corta. Incluso he visto una charla/publicación en alguna parte (no puedo encontrarla ahora) que recomienda usar solo lambdas de una línea en Java, porque tienen todas esas ventajas sin verbosidad.

En Go, no tenemos este problema, las funciones anónimas tanto cortas como largas son relativamente detalladas (pero no demasiado), por lo que no hay ningún obstáculo mental para usar las más largas también, lo que a veces es muy útil.

La taquigrafía es natural en los lenguajes funcionales porque todo es una expresión y el resultado de una función es la última expresión en la definición de la función.

Tener una taquigrafía es bueno, por lo que otros idiomas donde lo anterior no es válido lo han adoptado.

Pero en mi experiencia, nunca es tan agradable cuando golpea la realidad de un idioma con declaraciones.

Es casi tan detallado porque necesita bloques y retornos o solo puede contener expresiones, por lo que es básicamente inútil para todo menos para las cosas más simples.

Las funciones anónimas en Go son lo más cercano posible a lo óptimo. No veo el valor de afeitarlo más.

El problema no es la sintaxis func , son las declaraciones de tipos redundantes.

Simplemente permitir que los literales de función eliden tipos inequívocos sería de gran ayuda. Para usar el ejemplo de Cap'n'Proto:

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

Sí, son las declaraciones de tipo las que realmente agregan ruido. Desafortunadamente, "func (p) error" ya tiene un significado. ¿Quizás permitir que _ sustituya a un tipo inferido funcionaría?

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

eso me gusta bastante; no se requiere ningún cambio sintáctico.

No me gusta el tartamudeo de _. Tal vez func podría reemplazarse por una palabra clave que infiere los parámetros de tipo:
s.Write(ctx, λ(p) { return p.SetData([]byte("Hello, ")) })

¿Es esto realmente una propuesta o solo estás diciendo cómo se vería Go si lo vistieras como Scheme para Halloween? Creo que esta propuesta es innecesaria y no guarda relación con el enfoque del lenguaje en la legibilidad.

Deje de intentar cambiar la sintaxis del idioma solo porque _se ve_ diferente a otros idiomas.

Creo que tener una sintaxis de función anónima concisa es más convincente en otros idiomas que dependen más de API basadas en devolución de llamada. En Go, no estoy seguro de que la nueva sintaxis se pague sola. No es que no haya muchos ejemplos en los que la gente use funciones anónimas, pero al menos en el código que leo y escribo, la frecuencia es bastante baja.

Creo que tener una sintaxis de función anónima concisa es más convincente en otros idiomas que dependen más de API basadas en devolución de llamada.

Hasta cierto punto, esa es una condición que se refuerza a sí misma: si fuera más fácil escribir funciones concisas en Go, es posible que veamos más API de estilo funcional. (Si eso es algo bueno o no, no lo sé.)

Quiero enfatizar que hay una diferencia entre las API "funcionales" y las de "devolución de llamada": cuando escucho "devolución de llamada" pienso en "devolución de llamada asíncrona", lo que conduce a una especie de código de espagueti que hemos tenido la suerte de evitar en Vamos. Las API sincrónicas (como filepath.Walk o strings.TrimFunc ) son probablemente el caso de uso que deberíamos tener en cuenta, ya que encajan mejor con el estilo sincrónico de los programas Go en general.

Me gustaría intervenir aquí y ofrecer un caso de uso en el que he llegado a apreciar la sintaxis lambda de estilo arrow para reducir en gran medida la fricción: curry.

considerar:

// 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))
}

Ahora imagine que estamos tratando de convertir un valor en mongo.FieldConvertFunc o algo que requiera un enfoque funcional, y verá que tener una sintaxis más liviana puede mejorar bastante las cosas al cambiar una función de no estar currada a ser curry (feliz de proporcionar un ejemplo más real si alguien quiere).

¿No convencido? No lo creo. También me encanta la simplicidad de go y creo que vale la pena protegerla.

Otra situación que me pasa mucho es donde tienes y quieres ahora curry el próximo argumento con curry.

ahora tendrías que cambiar
func (a, b) x
a
func (a) func(b) x { return func (b) { return ...... x } }

Si hubiera una sintaxis de flecha, simplemente cambiaría
(a, b) => x
a
(a) => (b) => x

@neild aunque todavía no he contribuido a este hilo, tengo otro caso de uso que se beneficiaría de algo similar a lo que propusiste.

Pero este comentario es en realidad sobre otra forma de lidiar con la verbosidad en el código de llamada: tener una herramienta como gocode (o similar) plantilla de valor de función para usted.

Tomando tu ejemplo:

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

Si suponemos que hemos tecleado:

var _ = compute(
                ^

con el cursor en la posición que muestra el ^ ; luego, invocar una herramienta de este tipo podría crear una plantilla de valor de función para usted dando:

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

Eso ciertamente cubriría el caso de uso que tenía en mente; cubre el tuyo?

El código se lee mucho más a menudo de lo que se escribe. No creo que ahorrar un poco de escritura valga la pena cambiar la sintaxis del idioma aquí. La ventaja, si la hay, estaría en gran medida en hacer que el código sea más legible. El soporte del editor no ayudará con eso.

Una pregunta, por supuesto, es si eliminar la información de tipo completo de una función anónima ayuda o perjudica la legibilidad.

No creo que este tipo de sintaxis reduzca la legibilidad, casi todos los lenguajes de programación modernos tienen una sintaxis para esto y eso es porque fomenta el uso de estilo funcional para reducir el modelo y hacer que el código sea más claro y fácil de mantener. Es un gran fastidio usar funciones anónimas en golang cuando se pasan como parámetros a funciones porque tienes que repetirte escribiendo de nuevo los tipos que sabes que debes pasar.

Apoyo la propuesta. Ahorra tipeo y ayuda a la legibilidad. Mi caso de uso,

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

sin sintaxis de función anónima ligera:

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

con sintaxis de función anónima ligera:

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

La falta de expresiones de funciones anónimas concisas hace que Go sea menos legible y viola el principio DRY. Me gustaría escribir y usar API funcionales/de devolución de llamada, pero el uso de tales API es desagradablemente detallado, ya que cada llamada API debe usar una función ya definida o una expresión de función anónima que repite información de tipo que debería ser bastante clara del contexto (si la API está diseñada correctamente).

Mi deseo de esta propuesta no es ni remotamente que creo que el Go debe parecerse o parecerse a otros idiomas. Mi deseo está completamente impulsado por mi disgusto por repetirme e incluir ruido sintáctico innecesario.

En Go, la sintaxis de las declaraciones de funciones se desvía un poco del patrón regular que tenemos para otras declaraciones. Para constantes, tipos, variables siempre tenemos:

keyword name type value

Por ejemplo:

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

En general, el tipo puede ser un tipo literal o puede ser un nombre. Para funciones esto se rompe, el tipo siempre debe ser una firma literal. Uno podría imaginar algo como:

type BinaryOp func(x, y Value) Value

func f BinaryOp { ... }

donde el tipo de función se da como nombre. Ampliando un poco, un cierre de BinaryOp podría escribirse como

BinaryOp{ return x.Add(y) }

lo que podría contribuir en gran medida a una notación de cierre más corta. Por ejemplo:

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

La principal desventaja es que los nombres de los parámetros no se declaran con la función. El uso del tipo de función los pone "dentro del alcance", similar a cómo usar un valor de estructura x de tipo S trae un campo f dentro del alcance en una expresión de selector x.f o una estructura literal S{f: "foo"} .

Además, esto requiere un tipo de función declarado explícitamente, lo que solo puede tener sentido si ese tipo es muy común.

Sólo otra perspectiva para esta discusión.

La legibilidad es lo primero, eso parece ser algo en lo que todos podemos estar de acuerdo.

Pero dicho esto, una cosa en la que también quiero intervenir (ya que no parece que nadie más lo haya dicho explícitamente) es que la cuestión de la legibilidad siempre dependerá de lo que estés acostumbrado. Tener una discusión como estamos sobre si perjudica o perjudica la legibilidad no va a llegar a ninguna parte en mi opinión.

@griesemer quizás alguna perspectiva de su tiempo trabajando en V8 sería útil aquí. Yo (al menos) puedo decir que estaba muy contento con la sintaxis anterior de javascript para las funciones ( function(x) { return x; } ) que era (en cierto modo) incluso más difícil de leer que la de Go en este momento. Estaba en el campo de "esta nueva sintaxis es una pérdida de tiempo" de @douglascrockford .

Pero, de todos modos, la sintaxis de la flecha _sucedió_ y la acepté _porque tenía que hacerlo_. Hoy, sin embargo, después de haberlo usado mucho más y sentirme más cómodo con él, puedo decir que ayuda enormemente a la legibilidad . Usé el caso de curry (y @hooluupog mencionó un caso similar de "encadenamiento de puntos") donde una sintaxis liviana produce código que es liviano sin ser demasiado inteligente.

Ahora, cuando veo un código que hace cosas como x => y => z => ... y es mucho más fácil de entender de un vistazo (nuevamente... porque estoy _familiarizado_ con él. No hace mucho tiempo sentí todo lo contrario).

Lo que estoy diciendo es: esta discusión se reduce a:

  1. Cuando no estás acostumbrado, parece _realmente_ extraño y casi inútil si no dañino para la legibilidad. Algunas personas simplemente tienen o no tienen un sentimiento de una forma u otra sobre esto.
  2. Cuanta más programación funcional esté haciendo, más se manifiesta la necesidad de tal sintaxis. Supongo que esto tiene algo que ver con conceptos funcionales (como aplicación parcial y curry) que introducen muchas funciones para trabajos pequeños que se traducen en ruido para el lector.

Lo mejor que podemos hacer es proporcionar más casos de uso.

En respuesta al comentario de @dimitropoulos , aquí hay un resumen aproximado de mi punto de vista:

Quiero usar patrones de diseño (como la programación funcional) que se beneficiarían mucho de esta propuesta, ya que su uso con la sintaxis actual es excesivamente detallado.

@dimitropoulos He estado trabajando bien en V8, pero eso fue construir la máquina virtual, que fue escrita en C++. Mi experiencia con Javascript real es limitada. Dicho esto, Javascript es un lenguaje de escritura dinámica, y sin tipos, gran parte de la escritura desaparece. Como varias personas han mencionado antes, un problema importante aquí es la necesidad de repetir tipos, un problema que no existe en Javascript.

Además, para que conste: en los primeros días del diseño de Go, en realidad observamos la sintaxis de flechas para las firmas de funciones. No recuerdo los detalles, pero estoy bastante seguro de notaciones como

func f (x int) -> float32

estaba en la pizarra blanca. Eventualmente descartamos la flecha porque no funcionaba tan bien con valores de retorno múltiples (no tuplas); y una vez que el func y los parámetros estaban presentes, la flecha era superflua; tal vez "bonito" (como en apariencia matemática), pero aún superfluo. También parecía una sintaxis que pertenecía a un tipo de lenguaje "diferente".

Pero tener cierres en un lenguaje de alto rendimiento y propósito general abrió las puertas a estilos de programación nuevos y más funcionales. Ahora, dentro de 10 años, uno podría verlo desde un ángulo diferente.

Aún así, creo que debemos tener mucho cuidado aquí para no crear una sintaxis especial para los cierres. Lo que tenemos ahora es simple y regular y ha funcionado bien hasta ahora. Cualquiera que sea el enfoque, si hay algún cambio, creo que deberá ser regular y aplicarse a cualquier función.

En Go, la sintaxis de las declaraciones de funciones se desvía un poco del patrón regular que tenemos para otras declaraciones. Para constantes, tipos, variables siempre tenemos:
keyword name type value
[…]
Para funciones esto se rompe, el tipo siempre debe ser una firma literal.

Tenga en cuenta que para las listas de parámetros y las declaraciones const y var tenemos un patrón similar, IdentifierList Type , que probablemente también deberíamos conservar. Parece que descartaría el token : al estilo de cálculo lambda para separar los nombres de las variables de los tipos.

Cualquiera que sea el enfoque, si hay algún cambio, creo que deberá ser regular y aplicarse a cualquier función.

El patrón keyword name type value es para _declaraciones_, pero los casos de uso que menciona @neild son todos para _literales_.

Si abordamos el problema de los literales, creo que el problema de las declaraciones se vuelve trivial. Para declaraciones de constantes, variables y ahora tipos, permitimos (o requerimos) un token = antes del value . Parece que sería bastante fácil extender eso a las funciones:

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

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

La expresión después del token = debe ser una función literal, o quizás una función devuelta por una llamada cuyos argumentos estén todos disponibles en tiempo de compilación. En la forma = , todavía se podría proporcionar un Signature para mover las declaraciones de tipo de argumento del literal al FunctionSpec .

Tenga en cuenta que la diferencia entre un ShortParameterDecl y el ParameterDecl existente es que IdentifierList únicos se interpretan como nombres de parámetros en lugar de tipos.


Ejemplos

Considere esta declaración de función aceptada hoy:

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

Podríamos conservar eso (por ejemplo, para la compatibilidad con Go 1) además de los ejemplos a continuación, o eliminar la producción Function y usar solo la versión ShortFunctionLit .

Para varias opciones ShortFunctionLit , la gramática que propongo arriba da:

Como óxido:

ShortFunctionLit = "|" ShortParameterList "|" Block .

Admite cualquiera de:

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

Personalmente, creo que todas las variantes, excepto las similares a Scala, son bastante legibles. (En mi opinión, la variante similar a Scala tiene demasiados paréntesis: hace que las líneas sean mucho más difíciles de escanear).

Personalmente, estoy principalmente interesado en esto si me permite omitir los tipos de parámetros y resultados cuando se pueden inferir. Incluso estoy bien con la sintaxis literal de la función actual si puedo hacer eso. (Esto se discutió anteriormente).

Es cierto que esto va en contra del comentario de @griesemer .

Cualquiera que sea el enfoque, si hay algún cambio, creo que deberá ser regular y aplicarse a cualquier función.

No sigo esto del todo. Las declaraciones de función necesariamente deben incluir la información de tipo completa para la función, ya que no hay forma de derivarla con suficiente precisión del cuerpo de la función. (Este no es el caso para todos los idiomas, por supuesto, pero lo es para Go).

Los literales de función, por el contrario, podrían inferir tipo de información a partir del contexto.

@neild Disculpas por ser impreciso: lo que quise decir con esta oración es que si hubiera una nueva sintaxis diferente (flechas o lo que sea), debería ser algo regular y aplicarse en todas partes. Si es posible que se puedan omitir los tipos, sería de nuevo ortogonal.

@griesemer Gracias; Estoy (en su mayoría) de acuerdo con ese punto.

Creo que la pregunta interesante para esta propuesta es si tener algo de sintaxis es una buena idea o no; cuál sería esa sintaxis es importante pero relativamente trivial.

Sin embargo, no puedo resistir la tentación de cambiar un poco mi propia propuesta.

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

La propuesta de @neild me parece correcta. Es bastante similar a la sintaxis existente, pero funciona para la programación funcional ya que elimina la repetición de las especificaciones de tipo. No es mucho menos compacto que (a, b) => a + b y encaja bien en la sintaxis existente.

@neild

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

¿Eso declararía una variable o una función? Si es una variable, ¿cómo sería la declaración de la función equivalente?

Según mi esquema de declaración anterior, si lo entiendo correctamente, sería:

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

No creo que sea un fanático: tartamudea un poco en func y no parece proporcionar suficiente separación visual entre el token func y los parámetros que siguen.

¿O dejaría fuera los paréntesis de la declaración, en lugar de asignarlos a los literales?

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

Sin embargo, todavía no me gusta la falta de interrupción visual...

¿Eso declararía una variable o una función? Si es una variable, ¿cómo sería la declaración de la función equivalente?

Una variable. La declaración de función equivalente presumiblemente sería func sum a, b { return a+b } , pero eso no sería válido por razones obvias: no puede eludir los tipos de parámetros de las declaraciones de funciones.

El cambio de gramática en el que estoy pensando sería algo como:

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

Un literal de función corto se distingue de un literal de función regular al omitir los paréntesis en la lista de parámetros, define solo los nombres de los parámetros entrantes y no define los parámetros salientes. Los tipos de parámetros entrantes y los tipos y el número de parámetros salientes se derivan del contexto circundante.

No creo que haya ninguna necesidad de permitir especificar tipos de parámetros opcionales en un literal de función corto; simplemente usa un literal de función regular en ese caso.

Como señaló @ianlancetaylor , la notación liviana realmente solo tiene sentido cuando permite la omisión de tipos de parámetros porque se pueden inferir fácilmente. Como tal, la sugerencia de @neild es la mejor y la más simple que he visto hasta ahora. Sin embargo, lo único que no permite fácilmente es una notación liviana para los literales de funciones que desean referirse a parámetros de resultados con nombre. Pero quizás en ese caso deberían usar la notación completa. (Es sólo un poco irregular).

Incluso podríamos analizar (x, y) { ... } como forma abreviada de func (x, y T) T { ... } ; aunque requeriría un poco de anticipación del analizador, pero tal vez no sea tan malo.

Como experimento, modifiqué gofmt para reescribir los literales de función en la sintaxis compacta y lo ejecuté contra src/. Puedes ver los resultados aquí:

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

No hice ningún intento de limitar esto a los casos en los que tiene sentido; Solo quería tener una idea de cómo podría funcionar la sintaxis compacta en la práctica. Todavía no he investigado lo suficiente como para desarrollar opiniones sobre los resultados.

@neild Buen análisis. Algunas observaciones:

  1. La fracción de casos en los que el literal de la función está enlazado usando := es decepcionante, ya que manejar esos casos sin anotaciones de tipo explícitas requeriría un algoritmo de inferencia más complicado.

  2. Los literales pasados ​​a las devoluciones de llamada son más fáciles de leer en algunos casos, pero más difíciles en otros.
    Por ejemplo, perder la información de tipo de retorno para los literales de función que abarcan muchas líneas es un poco desafortunado, ya que eso también le dice al lector si está buscando una API funcional o imperativa.

  3. La reducción en el modelo para los literales de función dentro de los segmentos es sustancial.

  4. Las declaraciones defer y go son un caso interesante: ¿inferiríamos los tipos de argumentos a partir de los argumentos realmente pasados ​​a la función?

  5. Faltan un par de tokens finales de ... en los ejemplos.

defer y go son de hecho un caso bastante interesante.

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

¿Derivaríamos el tipo de p del parámetro de función real? Esto sería bastante bueno para declaraciones go , aunque, por supuesto, puede lograr el mismo efecto simplemente usando un cierre:

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

Apoyaría totalmente esto. Francamente, no me importa cuánto "se parece a otros idiomas", solo quiero una forma menos detallada de usar funciones anónimas.

EDITAR: Tomando prestada la sintaxis literal compuesta...

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?

Solo una idea:
Así es como se vería el ejemplo de OP con una _función literal sin tipo_ con la sintaxis de Swift:

compute({ $0 + $1 })

Creo que esto tendría la ventaja de ser totalmente compatible con Go 1.

Acabo de encontrar esto porque estaba escribiendo una aplicación de chat TCP simple,
básicamente tengo una estructura con una rebanada dentro

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

y me gustaría aplicarle algunas operaciones al mismo tiempo (agregar conexiones, enviar mensajes a todos, etc.)

y en lugar de seguir la ruta normal de copiar y pegar el código de bloqueo mutex, o usar un demonio goroutine para administrar el acceso, pensé que solo pasaría un cierre

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

para operaciones cortas es demasiado detallado (aún mejor que lock y defer unlock() )

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

Esto viola el principio DRY ya que he escrito esa firma de función exacta en el método run .

Si se admite la inferencia de la firma de la función, podría escribirla así

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

No creo que esto haga que el código sea menos legible, puedes decir que es una porción debido a append , y como he nombrado bien mis variables, puedes adivinar que es un []net.Conn sin mirar en la firma del método run .

Evitaría tratar de inferir los tipos de parámetros en función del cuerpo de la función, en su lugar, agregaría inferencia solo para los casos en que sea obvio (como pasar cierres a funciones).

Diría que esto no daña la legibilidad, ya que le da al lector una opción, si no conocen el tipo de parámetro, pueden godef o pasar el cursor sobre él y hacer que el editor se lo muestre. .

Algo así como en un libro que no repiten la introducción de los personajes, excepto que tendríamos un botón para mostrarlo / saltar a él.

Soy malo escribiendo, así que espero que hayas sobrevivido leyendo esto :)

Creo que esto es más convincente si restringimos su uso a casos en los que el cuerpo de la función es una expresión simple.

me atrevo a objetar. Esto todavía llevaría a dos formas de definir una función, y una de las razones por las que me enamoré de Go es que si bien tiene algo de verbosidad aquí y allá, tiene una expresividad refrescante: ves dónde está un cierre porque hay ya sea una palabra clave func o el parámetro es una función, si lo rastrea.

conns.run(func(conns []net.Conn) { conns = append(conns, conn) })
Esto viola el principio DRY ya que he escrito esa firma de función exacta en el método de ejecución.

SECO _es_ importante, sin duda. Pero aplicarlo a todas y cada una de las partes de la programación en aras de mantener el principio a costa de la capacidad de comprender el código con la menor cantidad de esfuerzo posible, es un poco pasarse de la raya, en mi humilde opinión.

Creo que el problema general aquí (y algunas otras propuestas) es que la discusión es principalmente sobre cómo asegurar el esfuerzo _escribiendo_ el código, mientras que en mi humilde opinión debería ser cómo asegurar el esfuerzo _leyendo_ el código. Años después de que uno lo haya escrito. Recientemente encontré un poc.pl mío y todavía estoy tratando de averiguar qué hace... ;)

conns.run(func(conns) { conns = append(conns, conn) })
No creo que esto haga que el código sea menos legible, puedes decir que es un segmento debido a la adición, y como he nombrado bien mis variables, puedes adivinar que es un []net.Conn sin mirar la firma del método de ejecución .

Desde mi punto de vista, hay varios problemas con esta declaración. No sé cómo lo ven los demás, pero _odio_ adivinar. Uno puede tener razón, uno puede estar equivocado, pero seguramente uno tiene que esforzarse en ello, por el beneficio de ahorrar para "escribir" []net.Conn . Y la legibilidad y la comprensibilidad del código deben estar respaldadas por buenos nombres de variables, no basados ​​en ellos.

Para concluir: creo que el enfoque de la discusión debería alejarse de cómo reducir los esfuerzos menores al escribir código a cómo reducir los esfuerzos para comprender dicho código.

Cierro citando a Dave Cheney citando a Robert Pike (iirc)

Claro es mejor que inteligente.

El tedio de escribir firmas de funciones puede aliviarse un poco con la finalización automática. Por ejemplo, gopls ofrece terminaciones que crean funciones literales:
cb

Creo que esto proporciona un buen término medio en el que los nombres de los tipos todavía están en el código fuente, solo queda una forma de definir una función anónima y no es necesario escribir la firma completa.

¿Esto se agregará o no?
... para aquellos a quienes no les gusta esta función, aún pueden usar la sintaxis anterior.
... para nosotros que queremos una mayor simplicidad, podemos usar esta nueva característica con suerte, ha pasado 1 año desde que escribí go, no estoy seguro si la comunidad todavía piensa que esto es importante,
... ¿se agregará esto o no?

@noypi No se ha tomado ninguna decisión. Este tema permanece abierto.

https://golang.org/wiki/NoPlusOne

Respaldo esta propuesta y creo que esta característica, junto con los genéricos, haría que la programación funcional en Go fuera más amigable para los desarrolladores.

Esto es lo que me gustaría ver, más o menos:

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

Me encantaría poder escribir (a, b) => a * b y continuar.

No puedo creer que las funciones de flecha todavía no estén disponibles en Go lang.
Es sorprendente lo claro y simple que es trabajar con Javascript.

JavaScript puede implementar esto de manera trivial, ya que no le importan los parámetros, la cantidad de ellos, los valores o sus tipos hasta que realmente se usan.

Ser capaz de omitir tipos en los literales de función ayudaría mucho con el estilo funcional que uso para la API de diseño de Gio. Vea los muchos literales "func() {...}" en https://git.sr.ht/~eliasnaur/gio/tree/master/example/kitchen/kitchen.go? Su firma real debería haber sido algo así como

func(gtx layout.Context) layout.Dimensions

pero debido a los nombres de tipos largos, el gtx es un puntero a un layout.Context compartido que contiene los valores entrantes y salientes de cada llamada de función.

Probablemente voy a cambiar a las firmas más largas independientemente de este problema, para mayor claridad y corrección. Sin embargo, creo que mi caso es un buen informe de experiencia en apoyo de literales de función más cortos.

PD Una de las razones por las que me inclino por las firmas más largas es porque se pueden acortar con alias de tipo:

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

que acorta los literales a func(gtx C) D { ... } .

Una segunda razón es que las firmas más largas son compatibles hacia adelante con lo que sea que resuelva este problema.

Vine aquí con una idea y descubrí que @networkimprov ya había sugerido algo similar aquí .

Me gusta la idea de usar un tipo de función (también podría ser un tipo de función sin nombre o un alias) como especificador de un literal de función, porque significa que podemos usar las reglas habituales de inferencia de tipo para parámetros y valores devueltos, porque conocemos el tipos exactos por adelantado. Esto significa que (por ejemplo) la finalización automática puede funcionar como de costumbre y no necesitaríamos introducir reglas originales de inferencia de tipos de arriba hacia abajo.

Dado:

type F func(a, b int) int

mi pensamiento original fue:

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

pero eso se parece demasiado a una llamada de función normal: no parece que a y b se estén definiendo allí.

Descartando otras posibilidades (no me gusta ninguna particularmente):

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}

Tal vez hay alguna buena sintaxis al acecho por aquí en alguna parte :)

Un punto clave de la sintaxis literal compuesta es que no requiere información de tipo en el analizador. La sintaxis para estructuras, arreglos, sectores y mapas es idéntica; el analizador no necesita saber el tipo de T para generar un árbol de sintaxis para T{...} .

Otro punto es que la sintaxis tampoco requiere retroceder en el analizador. Cuando hay ambigüedad sobre si { es parte de un literal compuesto o de un bloque, esa ambigüedad siempre se resuelve a favor de este último.

Todavía me gusta la sintaxis que propuse anteriormente en este número, que evita cualquier ambigüedad del analizador al retener la palabra clave func :

func a, b { return a + b }

Quité mi :-1:. Todavía no estoy :+1: en eso, pero estoy reconsiderando mi posición. Los genéricos provocarán un aumento en las funciones cortas como genericSorter(slice, func(a, b T) bool { return a > b }) . También encontré https://github.com/golang/go/issues/37739#issuecomment -624338848 convincente.

Se están discutiendo dos formas principales para hacer que los literales de función sean más concisos:

  1. una forma abreviada para los cuerpos que devuelven una expresión
  2. elidiendo los tipos en los literales de funciones.

Creo que ambos deben ser tratados por separado.

Si FunctionBody se cambia a algo como

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

eso ayudaría principalmente a los literales de función con o sin elisión de tipos y también permitiría que las declaraciones de métodos y funciones muy simples sean más ligeras en la página:

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 y sus amigos aún podrían esconder el cuerpo)

Utilicé la sintaxis de @ianlancetaylor en ese ejemplo, cuya principal desventaja es que requiere la introducción de un nuevo token (¡y uno que se vería extraño en func(c chan T) -> <-c !) pero podría estar bien reutilice un token existente como "=", si no hay ambigüedad. Usaré "=" en el resto de esta publicación.

Para la elisión de tipo hay dos casos

  1. algo que siempre funciona
  2. algo que solo funciona en un contexto donde los tipos se pueden deducir

Usar tipos con nombre como @griesemer sugirió que siempre funcionaría. Parece que hay algunos problemas con la sintaxis. Estoy seguro de que eso podría resolverse. Incluso si lo fueran, no estoy seguro de que resolvería el problema. Requeriría una proliferación de tipos con nombre. Estos estarían en el paquete que define el lugar donde se usan o tendrían que definirse en cada paquete que los use.

En el primero obtienes algo como

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

y en este último obtienes algo como

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

De cualquier manera, hay suficiente desorden que realmente no reduce mucho el modelo a menos que cada nombre se use mucho.

Una sintaxis como la de @neild solo se puede usar cuando se pueden deducir los tipos. Un método simple sería como en el #12854, simplemente enumere todos los contextos en los que se conoce el tipo: parámetro a una función, asignado a un campo, enviado a un canal, etc. El caso go/defer que planteó @neild también parece útil de incluir.

Ese enfoque específicamente no permite lo siguiente

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

pero esos son casos en los que valdría la pena ser más explícito, incluso si fuera posible inferir el tipo algorítmicamente al examinar dónde y cómo se usan.

Permite muchos casos útiles, incluidos los más útiles/solicitados:

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

poder elegir usar un bloque independiente de la sintaxis literal también permite:

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

que es un caso particular que me empuja cada vez más hacia un :+1:

Una pregunta que no he visto planteada es cómo manejar los parámetros ... . Podrías argumentar a favor de cualquiera

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

No tengo una respuesta para eso.

@jimmyfrasche

  1. elidiendo los tipos en los literales de funciones.

Creo que esto debería manejarse con la adición de literales de tipo función. Donde el tipo reemplaza 'func' y se emiten los tipos de argumento (tal como están definidos por el tipo). Esto mantiene la legibilidad y es bastante consistente con los literales de otros tipos.

http.Handle("/", http.HandlerFunc[w, r]{
    fmt.Fprinf(w, "Hello World")
})
  1. una forma abreviada para los cuerpos que devuelven una expresión

Refactorice la función como su propio tipo y luego las cosas se vuelven mucho más limpias.

type ComputeFunc func(float64, float64) float64

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

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

Si esto es demasiado detallado para usted, escriba alias el tipo de función dentro de su código.

{
    type f = ComputeFunc

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

En el caso especial de una función sin argumentos, se deben omitir los corchetes.

type IntReturner func() int

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

Elijo corchetes porque la propuesta de contratos ya usa corchetes estándar adicionales para funciones genéricas.

@Splizard Mantengo el argumento de que eso simplemente eliminaría el desorden de la sintaxis literal en muchas definiciones de tipos adicionales. Cada una de estas definiciones tendría que usarse al menos dos veces antes de que pudiera ser más corta que simplemente escribir los tipos en el literal.

Tampoco estoy seguro de que funcione demasiado bien con los genéricos en todos los casos.

Considere la función bastante extraña

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

Podría nombrar un tipo genérico para usar con X :

type XFunc(type T) func() T

Si solo se usa la definición de XFunc para derivar los tipos de los parámetros, al llamar a X deberá decirle qué T usar, aunque eso esté determinado por el tipo de v :

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

Podría haber un caso especial para escenarios como este para permitir que se infiera T , pero luego terminaría con gran parte de la maquinaria que sería necesaria para la elisión de tipo en literales de función.

También puede definir un nuevo tipo para cada T con los que llame a X pero no hay muchos ahorros a menos que llame a X muchas veces por cada T .

¿Fue útil esta página
0 / 5 - 0 calificaciones