Go: propuesta: especificación: facilidades de programación genéricas

Creado en 14 abr. 2016  ·  816Comentarios  ·  Fuente: golang/go

Este número propone que Go debería admitir alguna forma de programación genérica.
Tiene la etiqueta Go2, ya que para Go1.x el lenguaje está más o menos hecho.

Acompañando a este número hay una propuesta general de genéricos de @ianlancetaylor que incluye cuatro propuestas defectuosas específicas de mecanismos de programación genéricos para Go.

La intención no es agregar genéricos a Go en este momento, sino mostrar a las personas cómo sería una propuesta completa. Esperamos que esto sea de ayuda para cualquiera que proponga cambios de idioma similares en el futuro.

Go2 LanguageChange NeedsInvestigation Proposal generics

Comentario más útil

Permítanme recordarles a todos de manera preventiva nuestra política de https://golang.org/wiki/NoMeToo . La fiesta de los emojis está arriba.

Todos 816 comentarios

CL https://golang.org/cl/22057 menciona este problema.

Permítanme recordarles a todos de manera preventiva nuestra política de https://golang.org/wiki/NoMeToo . La fiesta de los emojis está arriba.

Hay un resumen de las discusiones de Go Generics , que intenta brindar una descripción general de las discusiones de diferentes lugares. También proporciona algunos ejemplos de cómo resolver problemas, en los que le gustaría usar genéricos.

Hay dos "requisitos" en la propuesta vinculada que pueden complicar la implementación y reducir la seguridad del tipo:

  • Defina tipos genéricos basados ​​en tipos que no se conocen hasta que se crean instancias.
  • No requieren una relación explícita entre la definición de un tipo o función genéricos y su uso. Es decir, los programas no deberían tener que decir explícitamente que el tipo T implementa el G genérico.

Estos requisitos parecen excluir, por ejemplo, un sistema similar al sistema de rasgos de Rust, donde los tipos genéricos están restringidos por límites de rasgos. ¿Por qué son necesarios?

Se vuelve tentador construir genéricos en la biblioteca estándar a un nivel muy bajo, como en C++ std::basic_string, std::asignador>. Esto tiene sus beneficios, de lo contrario nadie lo haría, pero tiene efectos de gran alcance y, a veces, sorprendentes, como mensajes de error de C++ incomprensibles.

El problema en C++ surge de la verificación de tipo del código generado. Debe haber una verificación de tipo adicional antes de la generación del código. La propuesta de conceptos de C++ permite esto al permitir que el autor del código genérico especifique los requisitos de un tipo genérico. De esa forma, la compilación puede fallar en la verificación de tipos antes de que se genere el código y se puedan imprimir mensajes de error simples. El problema con los genéricos de C++ (sin conceptos) es que el código genérico _es_ la especificación del tipo genérico. Eso es lo que crea los mensajes de error incomprensibles.

El código genérico no debe ser la especificación de un tipo genérico.

@tamird Es una característica esencial de los tipos de interfaz de Go que puede definir un tipo T sin interfaz y luego definir un tipo de interfaz I tal que T implemente I. Consulte https://golang.org/doc/faq#implements_interface . Sería inconsistente si Go implementara una forma de genéricos para los cuales un tipo G genérico solo podría usarse con un tipo T que dijera explícitamente "Puedo usarse para implementar G".

No estoy familiarizado con Rust, pero no conozco ningún idioma que requiera que T indique explícitamente que se puede usar para implementar G. Los dos requisitos que menciona no significan que G no pueda imponer requisitos a T, solo ya que I impone requisitos a T. Los requisitos solo significan que G y T se pueden escribir de forma independiente. Esa es una característica muy deseable para los genéricos, y no puedo imaginar abandonarla.

@ianlancetaylor https://doc.rust-lang.org/book/traits.html explica los rasgos de Rust. Si bien creo que son un buen modelo en general, serían una mala opción para Go tal como existe hoy.

@sbunce También pensé que los conceptos eran la respuesta, y puedes ver la idea dispersa en las diversas propuestas antes de la última. Pero es desalentador que los conceptos se planificaron originalmente para lo que se convirtió en C ++ 11, y ahora estamos en 2016, y todavía son controvertidos y no están particularmente cerca de incluirse en el lenguaje C ++.

¿Sería útil consultar la literatura académica para obtener alguna orientación sobre la evaluación de enfoques?

El único documento que he leído sobre el tema es ¿Se benefician los desarrolladores de los tipos genéricos? (paywall lo siento, puede buscar en Google su camino a una descarga de pdf) que decía lo siguiente

En consecuencia, una interpretación conservadora del experimento
es que los tipos genéricos pueden considerarse como una compensación
entre las características de la documentación positiva y la
características de extensibilidad negativa. La parte emocionante de
el estudio es que mostró una situación en la que el uso de un
El sistema de tipo estático (más fuerte) tuvo un impacto negativo en el
tiempo de desarrollo y al mismo tiempo el beneficio esperado
fit, la reducción del tiempo de corrección del error de tipo, no apareció.
Creemos que tales tareas podrían ayudar en futuros experimentos en
identificar el impacto de los sistemas tipo.

También veo https://github.com/golang/go/issues/15295 también hace referencia a genéricos ligeros y flexibles orientados a objetos .

Si fuéramos a apoyarnos en la academia para guiar la decisión, creo que sería mejor hacer una revisión de la literatura por adelantado, y probablemente decidir pronto si evaluaríamos los estudios empíricos de manera diferente a los que se basan en pruebas.

Consulte: http://dl.acm.org/citation.cfm?id=2738008 de Barbara Liskov:

El soporte para la programación genérica en los lenguajes de programación orientados a objetos modernos es incómodo y carece del poder expresivo deseable. Presentamos un mecanismo de genericidad expresiva que agrega poder expresivo y fortalece la verificación estática, sin dejar de ser liviano y simple en casos de uso común. Al igual que las clases de tipos y los conceptos, el mecanismo permite que los tipos existentes modelen las restricciones de tipos de forma retroactiva. Para el poder expresivo, exponemos los modelos como construcciones con nombre que se pueden definir y seleccionar explícitamente para observar las restricciones; en los usos comunes de genericidad, sin embargo, los tipos son testigos implícitos de restricciones sin un esfuerzo adicional del programador.

Creo que lo que hicieron allí es genial. Lo siento si este es el lugar incorrecto para detenerme, pero no pude encontrar un lugar para comentar en /proposals y no encontré un problema apropiado aquí.

Podría ser interesante tener uno o más transpiladores experimentales: un código fuente genérico de Go para compilar el código fuente de Go 1.xy.
Quiero decir, demasiada charla/argumentos-para-mi-opinión, y nadie está escribiendo código fuente que _intenta_ implementar _algún tipo_ de genéricos para Go.

Solo para obtener conocimiento y experiencia con Go y los genéricos, para ver qué funciona y qué no funciona.
Si todas las soluciones genéricas de Go no son realmente buenas, entonces; No hay genéricos para Go.

¿La propuesta también puede incluir las implicaciones sobre el tamaño binario y la huella de memoria? Espero que haya duplicación de código para cada tipo de valor concreto para que las optimizaciones del compilador funcionen en ellos. Espero una garantía de que no habrá duplicación de código para tipos de punteros concretos.

Ofrezco una matriz de decisión de Pugh. Mis criterios incluyen impactos de perspicuidad (complejidad de la fuente, tamaño). También obligué a clasificar los criterios para determinar los pesos de los criterios. El tuyo puede variar, por supuesto. Utilicé "interfaces" como la alternativa predeterminada y comparé esto con genéricos de "copiar/pegar", genéricos basados ​​en plantillas (tenía en mente algo así como el funcionamiento del lenguaje D), y algo que llamé genéricos de estilo de creación de instancias en tiempo de ejecución. Estoy seguro de que esto es una gran simplificación. No obstante, puede generar algunas ideas sobre cómo evaluar las opciones... este debería ser un enlace público a mi Hoja de Google, aquí

Hacer ping a @yizhouzhang y @andrewcmyers para que puedan expresar sus opiniones sobre géneros como los genéricos en Go. Parece que podría ser una buena pareja :)

El diseño de genéricos que se nos ocurrió para Genus tiene una verificación de tipo modular estática, no requiere una declaración previa de que los tipos implementan alguna interfaz y viene con un rendimiento razonable. Definitivamente lo miraría si está pensando en genéricos para Go. Parece una buena opción desde mi comprensión de Go.

Aquí hay un enlace al documento que no requiere acceso a la Biblioteca digital ACM:
http://www.cs.cornell.edu/andru/papers/genus/

La página de inicio de Genus está aquí: http://www.cs.cornell.edu/projects/genus/

Todavía no hemos lanzado el compilador públicamente, pero planeamos hacerlo bastante pronto.

Feliz de responder cualquier pregunta que la gente tenga.

En términos de la matriz de decisión de @mandolyte , Genus obtiene un 17, empatado en el puesto n.º 1. Sin embargo, agregaría algunos criterios más para calificar. Por ejemplo, la verificación de tipo modular es importante, como otros como @sbunce observaron anteriormente, pero los esquemas basados ​​en plantillas carecen de ella. El informe técnico del documento Genus tiene una tabla mucho más grande en la página 34, que compara varios diseños genéricos.

Acabo de leer todo el documento Resumen de Go Generics , que fue un resumen útil de debates anteriores. En mi opinión, el mecanismo genérico de Genus no sufre los problemas identificados para C++, Java o C#. Los genéricos de género están cosificados, a diferencia de Java, por lo que puede encontrar tipos en tiempo de ejecución. También puede crear instancias en tipos primitivos, y no obtiene el encuadre implícito en los lugares donde realmente no lo desea: matrices de T donde T es un primitivo. El sistema de tipos es el más cercano a Haskell y Rust, en realidad es un poco más potente, pero creo que también es intuitivo. La especialización primitiva ala C# no se admite actualmente en Genus, pero podría serlo. En la mayoría de los casos, la especialización se puede determinar en el momento del enlace, por lo que no sería necesaria una verdadera generación de código en tiempo de ejecución.

CL https://golang.org/cl/22163 menciona este problema.

Una forma de restringir los tipos genéricos que no requiere agregar nuevos conceptos de lenguaje: https://docs.google.com/document/d/1rX4huWffJ0y1ZjqEpPrDy-kk-m9zWfatgCluGRBQveQ/edit?usp=sharing.

Genus se ve muy bien y es claramente un avance importante del arte, pero no veo cómo se aplicaría a Go. ¿Alguien tiene un boceto de cómo se integraría con el sistema/filosofía del tipo Go?

El problema es que el equipo go está intentando obstruir. El título establece claramente las intenciones del equipo go. Y si eso no fuera suficiente para disuadir a todos los interesados, las características exigidas de un dominio tan amplio en las propuestas de ian dejan claro que si quieres genéricos, entonces ellos no te quieren a ti. Es una estupidez incluso intentar dialogar con el equipo go. A los que buscan genéricos en go, les digo fracturan el lenguaje. Comience un nuevo viaje, muchos lo seguirán. Ya he visto un gran trabajo hecho en horquillas. Organícense, únanse en torno a una causa

Si alguien quiere intentar desarrollar una extensión genérica para Go basada en el diseño de Genus, estaremos encantados de ayudarle. No sabemos Go lo suficientemente bien como para producir un diseño que armonice con el lenguaje existente. Creo que el primer paso sería una propuesta de diseño de hombre de paja con ejemplos resueltos.

@andrewcmyers esperando que @ianlancetaylor trabaje contigo en eso. Solo tener algunos ejemplos para mirar ayudaría mucho.

He leído el artículo de Genus. En la medida en que lo entiendo, parece bueno para Java, pero no parece un ajuste natural para Go.

Un aspecto clave de Go es que cuando escribe un programa Go, la mayor parte de lo que escribe es código. Esto es diferente de C++ y Java, donde mucho más de lo que escribes son tipos. Genus parece ser principalmente sobre tipos: escribe restricciones y modelos, en lugar de código. El sistema de tipos de Go es muy, muy simple. El sistema de tipos de Genus es mucho más complejo.

Las ideas del modelado retroactivo, si bien son claramente útiles para Java, no parecen encajar en Go en absoluto. La gente ya usa tipos de adaptadores para hacer coincidir los tipos existentes con las interfaces; no se necesita nada más cuando se usan genéricos.

Sería interesante ver estas ideas aplicadas a Go, pero no soy optimista sobre el resultado.

No soy un experto en Go, pero su sistema de tipos no parece más simple que los pre-genéricos de Java. La sintaxis de tipo es un poco más liviana de una manera agradable, pero la complejidad subyacente parece casi la misma.

En Genus, las restricciones son tipos pero los modelos son código. Los modelos son adaptadores, pero se adaptan sin agregar una capa de envoltura real. Esto es muy útil cuando desea, por ejemplo, adaptar una matriz completa de objetos a una nueva interfaz. El modelado retroactivo le permite tratar la matriz como una matriz de objetos que satisfacen la interfaz deseada.

No me sorprendería si fuera más complicado que (pre-genéricos) de Java en un sentido teórico de tipos, aunque es más simple de usar en la práctica.

Dejando a un lado la complejidad relativa, son lo suficientemente diferentes como para que Genus no pueda mapear 1:1. Ningún subtipo parece grande.

Si estás interesado:

El resumen más breve de las diferencias filosóficas/de diseño relevantes que mencioné se encuentran en las siguientes entradas de preguntas frecuentes:

A diferencia de la mayoría de los idiomas, la especificación de Go es muy breve y clara acerca de las propiedades relevantes del sistema de tipos que comienzan en https://golang.org/ref/spec#Constants y continúan hasta la sección titulada "Bloques" (todas las cuales tiene menos de 11 páginas impresas).

A diferencia de los genéricos de Java y C#, el mecanismo de genéricos de Genus no se basa en la creación de subtipos. Por otro lado, me parece que Go sí tiene subtipificación, pero subtipificación estructural. Esa también es una buena combinación para el enfoque Genus, que tiene un sabor estructural en lugar de depender de relaciones predeclaradas.

No creo que Go tenga subtipos estructurales.

Mientras que dos tipos cuyo tipo subyacente es idéntico son, por lo tanto, idénticos
pueden sustituirse entre sí, https://play.golang.org/p/cT15aQ-PFr

Esto no se extiende a dos tipos que comparten un subconjunto común de campos,
https://play.golang.org/p/KrC9_BDXuh.

El jueves 28 de abril de 2016 a la 1:09 p. m., Andrew Myers [email protected]
escribió:

A diferencia de los genéricos de Java y C#, el mecanismo de genéricos Genus no se basa en
subtipificación Por otro lado, me parece que Go sí tiene subtipado,
sino subtipificación estructural. Esa también es una buena combinación para el enfoque Genus,
que tiene un sabor estructural en lugar de depender de predeclarado
relaciones


Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/golang/go/issues/15292#issuecomment-215298127

Gracias, estaba malinterpretando parte del lenguaje sobre cuándo los tipos implementan interfaces. En realidad, me parece que las interfaces de Go, con una extensión modesta, podrían usarse como restricciones de estilo Genus.

Es exactamente por eso que te hice ping, el género parece un enfoque mucho mejor que los genéricos de Java/C#.

Hubo algunas ideas con respecto a especializarse en los tipos de interfaz; por ejemplo, el enfoque de _plantillas de paquete_ "propuestas" 1 2 son ejemplos de ello.

tl;dr; el paquete genérico con especialización de interfaz se vería así:

package set
type E interface { Equal(other E) bool }
type Set struct { items []E }
func (s *Set) Add(item E) { ... }

Versión 1. con especialización en el ámbito del paquete:

package main
import items set[[E: *Item]]

type Item struct { ... }
func (a *Item) Equal(b *Item) bool { ... }

var xs items.Set
xs.Add(&Item{})

Versión 2. la declaración de ámbito de especialización:

package main
import set

type Item struct { ... }
func (a *Item) Equal(b *Item) bool { ... }

var xs set.Set[[E: *Item]]
xs.Add(&Item{})

Los genéricos con alcance de paquete evitarán que las personas abusen significativamente del sistema de genéricos, ya que el uso está limitado a algoritmos básicos y estructuras de datos. Básicamente evita la construcción de nuevas abstracciones de lenguaje y código funcional.

La especialización de ámbito de declaración tiene más posibilidades al costo, lo que la hace más propensa al abuso y es más detallada. Pero, el código funcional sería posible, por ejemplo:

type E interface{}
func Reduce(zero E, items []E, fn func(a, b E) E) E { ... }

Reduce[[E: int]](0, []int{1,2,3,4}, func(a, b int)int { return a + b } )
// there are probably ways to have some aliases (or similar) to make it less verbose
alias ReduceInt Reduce[[E: int]]
func ReduceInt Reduce[[E: int]]

El enfoque de especialización de interfaz tiene propiedades interesantes:

  • Los paquetes ya existentes que usan interfaces serían especializables. por ejemplo, podría llamar a sort.Sort[[Interface:MyItems]](...) y hacer que la clasificación funcione en el tipo concreto en lugar de en la interfaz (con ganancias potenciales de la inserción).
  • Las pruebas se simplifican, solo tengo que asegurar que el código genérico funciona con interfaces.
  • Es fácil decir cómo funciona. es decir, imagine que [[E: int]] reemplaza todas las declaraciones de E con int .

Pero, hay problemas de verbosidad cuando se trabaja con paquetes:

type op
import "set"

type E interface{}
func Union(a, b set.Set[[set.E: E]]) set.Set[[set.E: E]] {
    result := set.New[[set.E: E]]()
    ...
}

_Por supuesto, todo el asunto es más simple de enunciar que de implementar. Internamente, probablemente haya toneladas de problemas y formas en que podría funcionar._

_PS, a los que se quejan del lento progreso de los genéricos, aplaudo al equipo de Go por dedicar más tiempo a los problemas que tienen un mayor beneficio para la comunidad, por ejemplo, errores del compilador/tiempo de ejecución, SSA, GC, http2._

@egonelbre su punto de que los genéricos a nivel de paquete evitarán el "abuso" es realmente importante que creo que la mayoría de la gente pasa por alto. Eso más su relativa simplicidad semántica y sintáctica (solo se ven afectadas las construcciones de paquete e importación) los hace muy atractivos para Go.

@andrewcymyers es interesante que creas que las interfaces de Go funcionan como restricciones de estilo Genus. Hubiera pensado que todavía tienen el problema de que no se pueden expresar restricciones de parámetros de tipo múltiple con ellos.

Sin embargo, una cosa de la que me acabo de dar cuenta es que en Go puedes escribir una interfaz en línea. Entonces, con la sintaxis correcta, podría poner la interfaz en el alcance de todos los parámetros y capturar restricciones de múltiples parámetros:

tipo [V, E] Gráfico [interfaz V { Edges() E }, interfaz E { Endpoints() (V, V) }] ...

Creo que el mayor problema con las interfaces como restricciones es que los métodos no son tan omnipresentes en Go como en Java. Los tipos incorporados no tienen métodos. No existe un conjunto de métodos universales como los de java.lang.Object. Los usuarios normalmente no definen métodos como Equals o HashCode en sus tipos a menos que lo necesiten específicamente, porque esos métodos no califican un tipo para usarlo como claves de mapa o en cualquier algoritmo que necesite igualdad.

(La igualdad en Go es una historia interesante. El idioma le da a su tipo "==" si cumple con ciertos requisitos (consulte https://golang.org/ref/spec#Logical_operators, busque "comparable"). Cualquier tipo con " ==" puede servir como una clave de mapa. Pero si su tipo no merece "==", entonces no hay nada que pueda escribir que lo haga funcionar como una clave de mapa).

Debido a que los métodos no son omnipresentes, y debido a que no hay una manera fácil de expresar las propiedades de los tipos incorporados (como con qué operadores trabajan), sugerí usar el código como mecanismo de restricción genérico. Vea el enlace en mi comentario del 18 de abril, arriba. Esta propuesta tiene sus problemas, pero una buena característica es que el código numérico genérico aún podría usar los operadores habituales, en lugar de llamadas a métodos engorrosos.

La otra forma de hacerlo es agregar métodos a los tipos que carecen de ellos. Puede hacer esto en el idioma existente de una manera mucho más ligera que en Java:

escriba Int int
func (i Int) Menos (j Int) bool { return i < j }

El tipo Int "hereda" todos los operadores y otras propiedades de int. Aunque tienes que lanzar entre los dos para usar Int e int juntos, lo que puede ser una molestia.

Los modelos de género podrían ayudar aquí. Pero tendrían que mantenerse muy simples. Creo que @ianlancetaylor fue demasiado estrecho en su caracterización de Go como escribir más código, menos tipos. El principio general es que Go aborrece la complejidad. Nos fijamos en Java y C++ y estamos decididos a nunca ir allí. (Sin ofender.)

Entonces, una idea rápida para una característica similar a un modelo sería: hacer que el usuario escriba tipos como Int arriba, y en instancias genéricas permitir "int con Int", lo que significa usar el tipo int pero tratarlo como Int. Entonces no hay una construcción de lenguaje abierta llamada modelo, con su palabra clave, semántica de herencia, etc. No entiendo los modelos lo suficientemente bien como para saber si esto es factible, pero está más en el espíritu de Go.

@jba Ciertamente estamos de acuerdo con el principio de evitar la complejidad. "Tan simple como sea posible, pero no más simple". Probablemente dejaría algunas características de Genus fuera de Go por esos motivos, al menos al principio.

Una de las cosas buenas del enfoque Genus es que maneja los tipos incorporados sin problemas. Recuerde que los tipos primitivos en Java no tienen métodos y Genus hereda este comportamiento. En cambio, Genus trata los tipos primitivos _como si_ tuvieran un conjunto bastante grande de métodos con el propósito de satisfacer las restricciones. Una tabla hash requiere que sus claves se puedan codificar y comparar, pero todos los tipos primitivos satisfacen esta restricción. Por lo tanto, las instancias de tipo como Map[int, boolean] son perfectamente legales sin más problemas. No hay necesidad de distinguir entre dos sabores de enteros (int vs Int) para lograr esto. Sin embargo, si int no estuviera equipado con suficientes operaciones para algunos usos, usaríamos un modelo casi exactamente como el uso de Int anterior.

Otra cosa que vale la pena mencionar es la idea de "modelos naturales" en Genus. Por lo general, no tiene que declarar un modelo para usar un tipo genérico: si el argumento de tipo satisface la restricción, se genera automáticamente un modelo natural. Nuestra experiencia es que este es el caso habitual; Normalmente no es necesario declarar modelos explícitos con nombre. Pero si se necesitara un modelo, por ejemplo, si quisiera hacer hash de entradas de una manera no estándar, entonces la sintaxis es similar a la que sugirió: Map[int with fancyHash, boolean] . Yo diría que Genus es sintácticamente ligero en casos de uso normal pero con energía en reserva cuando es necesario.

@egonelbre Lo que está proponiendo aquí parece tipos virtuales, que son compatibles con Scala. Hay un artículo de ECOOP'97 de Kresten Krab Thorup, "Genericidad en Java con tipos virtuales", que explora esta dirección. También desarrollamos mecanismos para tipos virtuales y clases virtuales en nuestro trabajo ("J&: intersección anidada para la composición de software escalable", OOPSLA'06).

Dado que las inicializaciones literales son omnipresentes en Go, tuve que preguntarme cómo sería una función literal. Sospecho que el código para manejar esto existe en gran medida en Go generar, corregir y cambiar el nombre. Tal vez inspire a alguien :-)

// la definición del tipo de función (genérica)
tipo Sum64 func (X, Y) float64 {
volver float64(X) + float64(Y)
}

// instanciar uno, posicionalmente
yo := 42
var j uint = 86
suma := &Suma64{i, j}

// instanciar uno, por tipos de parámetros con nombre
suma := &Suma64{ X: int, Y: uint}

// ahora utilízalo...
resultado := suma(i, j) // el resultado es 128

La propuesta de Ian exige demasiado. No podemos desarrollar todas las características a la vez, existirá en un estado inacabado durante muchos meses.

Mientras tanto, el proyecto inacabado no puede llamarse idioma oficial de Go hasta que esté terminado porque eso correrá el riesgo de fragmentar el ecosistema.

Así que la pregunta es cómo planificar esto.

También una gran parte del proyecto sería desarrollar el corpus de referencia.
desarrollando las colecciones genéricas reales, algoritmos y otras cosas de tal manera que todos estemos de acuerdo en que son idiomáticos, mientras usamos las nuevas características de go 2.0

¿Una posible sintaxis?

// Module defining generic type
module list(type t)

type node struct {
    next *node
    data t
}
// Module using generic type:
import (
    intlist "module/path/to/list" (int)
    funclist "module/path/to/list" (func (int) int)
)

l := intlist.New()
l.Insert(5)

@ md2perpe , la sintaxis no es la parte difícil de este problema. De hecho, es de lejos el más fácil. Consulte la discusión y los documentos vinculados anteriores.

@ md2perpe Hemos discutido la parametrización de paquetes completos ("módulos") como una forma de genericidad interna; parece ser una forma de reducir la sobrecarga sintáctica. Pero tiene otros problemas; por ejemplo, no está claro cómo se parametrizaría con tipos que no están a nivel de paquete. Pero aún puede valer la pena explorar la idea en detalle.

Me gustaría compartir una perspectiva: en un universo paralelo, todas las firmas de funciones de Go siempre se han visto limitadas a mencionar solo los tipos de interfaz y, en lugar de la demanda de genéricos hoy en día, hay una forma de evitar la indirección asociada con los valores de interfaz. Piensa en cómo resolverías ese problema (sin cambiar el idioma). tengo algunas ideas

@thwd Entonces, el autor de la biblioteca continuaría usando interfaces, pero sin el cambio de tipo y las aserciones de tipo que se necesitan hoy. ¿Y el usuario de la biblioteca simplemente pasaría tipos concretos como si la biblioteca usara los tipos tal cual... y luego el compilador reconciliaría los dos? ¿Y si no pudiera decir por qué? (como el operador de módulo se usó en la biblioteca, pero el usuario suministró una porción de algo.

¿Estoy cerca? :-)

@mandolyte si! intercambiemos correos electrónicos para no contaminar este hilo. Puede ponerse en contacto conmigo en "me at thwd dot me". Cualquier otra persona que lea esto que pueda estar interesada; envíame un correo electrónico y te agrego al hilo.

Es una gran característica para type system y collection library .
Una sintaxis potencial:

type Element<T> struct {
    prev, next *Element<T>
    list *List<T>
    value T
}
type List<E> struct {
    root Element<E>
    len int
}

Por interface

type Collection<E> interface {
    Size() int
    Add(e E) bool
}

super type o type implement :

func contain(l List<parent E>, e E) bool
<V> func (c Collection<child E>)Map(fn func(e E) V) Collection

Lo anterior también conocido como en Java:

boolean contain(List<? super E>, E e)
<V> Collection Map(Function<? extend E, V> mapFunc);

@leaxoy como se dijo antes, la sintaxis no es la parte difícil aquí. Consulte la discusión anterior.

Solo tenga en cuenta que el costo de la interfaz es increíblemente alto.

Explique por qué cree que el costo de la interfaz es "increíblemente"
grande.
No debería ser peor que las llamadas virtuales no especializadas de C++.

@minux No puedo decir sobre los costos de rendimiento, pero en relación con la calidad del código. interface{} no se puede verificar en tiempo de compilación, pero los genéricos sí. En mi opinión, esto es, en la mayoría de los casos, más importante que los problemas de rendimiento de usar interface{} .

@xoviat

Realmente no hay inconveniente en esto porque el procesamiento requerido para esto no ralentiza el compilador.

Hay (al menos) dos inconvenientes.

Uno es aumentar el trabajo para el enlazador: si las especializaciones para dos tipos dan como resultado el mismo código de máquina subyacente, no queremos compilar y enlazar dos copias de ese código.

Otra es que los paquetes parametrizados son menos expresivos que los métodos parametrizados. (Consulte las propuestas vinculadas desde el primer comentario para obtener más detalles).

¿Es el hipertipo una buena idea?

func getAddFunc (aType type) func(aType, aType) aType {
    return func(a, b aType) aType {
        return a+b
    }
}

¿Es el hipertipo una buena idea?

Lo que está describiendo aquí es solo tipo de parametrización ala C ++ (es decir, plantillas). No verifica el tipo de forma modular porque no hay forma de saber que el tipo aType tiene una operación + a partir de la información dada. La parametrización de tipo restringido como en CLU, Haskell, Java, Genus es la solución.

@ golang101 Tengo una propuesta detallada en ese sentido. Enviaré una CL para agregarlo a la lista, pero es poco probable que se adopte.

CL https://golang.org/cl/38731 menciona este problema.

@andrewcmyers

No verifica el tipo de forma modular porque no hay forma de saber que el tipo aType tiene una operación + a partir de la información dada.

Seguro que lo hay. Esa restricción está implícita en la definición de la función, y las restricciones de esa forma se pueden propagar a todas las llamadas (transitivas) en tiempo de compilación de getAddFunc .

La restricción no forma parte de un _tipo_ de Go, es decir, no se puede codificar en el sistema de tipos de la parte de tiempo de ejecución del lenguaje, pero eso no significa que no se pueda evaluar de forma modular.

Agregué mi propuesta como 2016-09-compile-time-functions.md .

No espero que se adopte, pero al menos puede servir como un punto de referencia interesante.

@bcmills Siento que las funciones de tiempo de compilación son una idea poderosa, aparte de cualquier consideración de los genéricos. Por ejemplo, escribí un solucionador de sudoku que necesita un conteo de pops. Para acelerar eso, calculé previamente los popcounts para los diversos valores posibles y los almacené como Go source . Esto es algo que uno podría hacer con go:generate . Pero si hubiera una función de tiempo de compilación, esa tabla de búsqueda también podría calcularse en el momento de la compilación, evitando que el código generado por la máquina tenga que confirmarse en el repositorio. En general, cualquier tipo de función matemática memorizable es una buena opción para las tablas de búsqueda prefabricadas con funciones de tiempo de compilación.

Más especulativamente, uno también podría querer, por ejemplo, descargar una definición de protobuf de una fuente canónica y usarla para construir tipos en tiempo de compilación. ¿Pero tal vez eso es demasiado para poder hacer en tiempo de compilación?

Siento que las funciones de tiempo de compilación son demasiado poderosas y demasiado débiles al mismo tiempo: son demasiado flexibles y pueden generar errores de formas extrañas / ralentizar la compilación de la forma en que lo hacen las plantillas de C++, pero por otro lado son demasiado estáticas y difíciles de adaptarse a cosas como funciones de primera clase.

Para la segunda parte, no veo una forma de que puedas hacer algo como una "porción de funciones que procesan porciones de un tipo particular y devuelven un elemento", o en una sintaxis ad-hoc []func<T>([]T) T , que es muy fácil de hacer en prácticamente todos los lenguajes funcionales tipificados estáticamente. Lo que realmente se necesita es que los valores puedan asumir tipos paramétricos, no una generación de código a nivel de código fuente.

@bunsim

Para la segunda parte, no veo una forma de que puedas hacer algo como una "porción de funciones que procesan porciones de un tipo particular y devuelven un elemento",

Si estás hablando de un solo parámetro de tipo, en mi propuesta estaría escrito:

const func SliceOfSelectors(T gotype) gotype { return []func([]T)T (type) }

Si está hablando de mezclar parámetros de tipo y parámetros de valor, no, mi propuesta no permite eso: parte del objetivo de las funciones en tiempo de compilación es poder operar en valores sin caja y el tipo de parametricidad en tiempo de ejecución. Creo que lo que estás describiendo requiere un boxeo de valores.

Sí, pero en mi opinión, ese tipo de cosas que requieren encajonamiento deberían permitirse manteniendo la seguridad de tipo, tal vez con una sintaxis especial que indique el "encuadre". Una gran parte de agregar "genéricos" es realmente evitar la inseguridad de tipo de interface{} incluso cuando la sobrecarga de interface{} no se puede evitar. (¿Quizás solo permita ciertas construcciones de tipos paramétricos con punteros y tipos de interfaz que "ya" están en caja? Los objetos en caja Integer de Java, etc., no son del todo una mala idea, aunque las porciones de tipos de valor son complicadas)

Siento que las funciones en tiempo de compilación son muy parecidas a C++, y sería extremadamente decepcionante para las personas como yo esperar que Go2 tenga un sistema de tipo paramétrico moderno basado en una teoría de tipo sólida en lugar de un truco basado en la manipulación de piezas de código fuente escrito en un lenguaje sin genéricos.

@bcmills
Lo que propongas no será modular. Si el módulo A usa el módulo B, que usa el módulo C, que usa el módulo D, un cambio en la forma en que se usa un parámetro de tipo en D puede necesitar propagarse hasta A, incluso si el implementador de A no tiene idea de que D está en el sistema. El acoplamiento flojo proporcionado por los sistemas de módulos se debilitará y el software será más frágil. Este es uno de los problemas con las plantillas de C++.

Si, por otro lado, las firmas de tipo capturan los requisitos de los parámetros de tipo, como en lenguajes como CLU, ML, Haskell o Genus, un módulo se puede compilar sin ningún acceso a las partes internas de los módulos de los que depende.

@bunsim

Una gran parte de agregar "genéricos" es realmente evitar la falta de seguridad de tipo de la interfaz{} incluso cuando la sobrecarga de la interfaz{} no se puede evitar.

"no evitable" es relativo. Tenga en cuenta que la sobrecarga del boxeo es el punto # 3 en la publicación de Russ de 2009 (https://research.swtch.com/generic).

esperar que Go2 tenga un sistema de tipo paramétrico moderno basado en una teoría de tipo sólida en lugar de un truco basado en la manipulación de piezas de código fuente

Una buena "teoría del tipo de sonido" es descriptiva, no prescriptiva. Mi propuesta en particular se basa en el cálculo lambda de segundo orden (en la línea del Sistema F), donde gotype representa el tipo type y todo el sistema de tipo de primer orden se eleva al segundo -order ("tiempo de compilación") tipos.

También está relacionado con el trabajo de teoría de tipo modal de Davies, Pfenning, et al en CMU. Para obtener algunos antecedentes, comenzaría con Un análisis modal de la computación por etapas y los tipos modales como especificaciones de preparación para la generación de código en tiempo de ejecución .

Es cierto que la teoría de tipos subyacente en mi propuesta está menos formalmente especificada que en la literatura académica, pero eso no significa que no esté ahí.

@andrewcmyers

Si el módulo A usa el módulo B, que usa el módulo C, que usa el módulo D, un cambio en la forma en que se usa un parámetro de tipo en D puede necesitar propagarse hasta A, incluso si el implementador de A no tiene idea de que D está en el sistema.

Eso ya es cierto en Go hoy: si observa detenidamente, notará que los archivos de objeto generados por el compilador para un paquete Go dado incluyen información sobre las partes de las dependencias transitivas que afectan la API exportada.

El acoplamiento flojo proporcionado por los sistemas de módulos se debilitará y el software será más frágil.

Escuché que se usa el mismo argumento para recomendar la exportación de tipos interface en lugar de tipos concretos en las API de Go, y lo contrario resulta ser más común: la abstracción prematura restringe en exceso los tipos y dificulta la extensión de las API. (Para ver un ejemplo de este tipo, consulte #19584). Si desea confiar en esta línea de argumentación, creo que debe proporcionar algunos ejemplos concretos.

Este es uno de los problemas con las plantillas de C++.

Tal como lo veo, los principales problemas con las plantillas de C++ son (sin ningún orden en particular):

  • Ambigüedad sintáctica excesiva.
    una. Ambigüedad entre nombres de tipos y nombres de valores.
    B. Soporte excesivamente amplio para la sobrecarga del operador, lo que lleva a una capacidad debilitada para inferir restricciones del uso del operador.
  • Dependencia excesiva de la resolución de sobrecarga para la metaprogramación (o, de manera equivalente, evolución ad-hoc del soporte de metaprogramación).
    una. Especialmente wrt reglas de colapso de referencia.
  • Aplicación demasiado amplia del principio SFINAE, lo que genera restricciones muy difíciles de propagar y demasiados condicionales implícitos en las definiciones de tipo, lo que genera un informe de errores muy difícil.
  • Uso excesivo del pegado de tokens y la inclusión textual (el preprocesador C) en lugar de la sustitución de AST y los artefactos de compilación de orden superior (lo que afortunadamente parece solucionarse, al menos en parte, con los Módulos).
  • La falta de buenos lenguajes de arranque para los compiladores de C++, lo que genera informes de errores deficientes en los linajes de compiladores de larga duración (por ejemplo, la cadena de herramientas de GCC).
  • La duplicación (ya veces la multiplicación) de nombres resultantes de la asignación de conjuntos de operadores a "conceptos" con nombres diferentes (en lugar de tratar a los operadores como restricciones fundamentales).

He estado codificando en C++ de vez en cuando durante una década y estoy feliz de discutir las deficiencias de C++ en detalle, pero el hecho de que las dependencias del programa sean transitivas nunca ha estado ni remotamente en la parte superior de mi lista de quejas.

Por otro lado, ¿necesita actualizar una cadena de dependencias O(N) solo para agregar un solo método a un tipo en el módulo A y poder usarlo en el módulo D? Ese es el tipo de problema que me frena regularmente. Cuando la parametricidad y el acoplamiento débil entren en conflicto, elegiré la parametricidad cualquier día.

Aún así, creo firmemente que la metaprogramación y el polimorfismo paramétrico deben separarse, y la confusión de C++ sobre ellos es la causa principal de por qué las plantillas de C++ son molestas. En pocas palabras, C ++ intenta implementar una idea de teoría de tipos usando esencialmente macros con esteroides, lo cual es muy problemático ya que a los programadores les gusta pensar en las plantillas como polimorfismo paramétrico real y se ven afectados por un comportamiento inesperado. Las funciones de tiempo de compilación son una gran idea para metaprogramar y reemplazar el truco que es go generate , pero no creo que deba ser la forma bendita de hacer programación genérica.

El polimorfismo paramétrico "real" ayuda a perder el acoplamiento y no debería entrar en conflicto con él. También debe estar estrechamente integrado con el resto del sistema de tipos; por ejemplo, probablemente debería integrarse en el sistema de interfaz actual, de modo que muchos usos de los tipos de interfaz podrían reescribirse en cosas como:

func <T io.Reader> ReadAll(in T)

lo que debería evitar la sobrecarga de la interfaz (como el uso de Rust), aunque en este caso no es muy útil.

Un mejor ejemplo podría ser el paquete sort , donde podría tener algo como

func <T Comparable> Sort(slice []T)

donde Comparable es simplemente una buena interfaz antigua que los tipos pueden implementar. Luego se puede llamar a Sort en una porción de tipos de valor que implementan Comparable , sin incluirlos en tipos de interfaz.

@bcmills Las dependencias transitivas no restringidas por el sistema de tipos son, en mi opinión, el núcleo de algunas de sus quejas sobre C++. Las dependencias transitivas no son un gran problema si controla los módulos A, B, C y D. En general, está desarrollando el módulo A y es posible que solo tenga poca conciencia de que el módulo D está ahí abajo y, por el contrario, el desarrollador de D. puede desconocer A. Si el módulo D ahora, sin hacer ningún cambio en las declaraciones visibles en D, comienza a usar algún operador nuevo en un parámetro de tipo, o simplemente usa ese parámetro de tipo como un argumento de tipo para un nuevo módulo E con su propio Restricciones implícitas: esas restricciones se filtrarán a todos los clientes, que pueden no estar utilizando argumentos de tipo que satisfagan las restricciones. Nada le dice al desarrollador D que lo están arruinando. En efecto, tienes una especie de inferencia de tipo global, con todas las dificultades de depuración que eso conlleva.

Creo que el enfoque que tomamos en Genus [ PLDI'15 ] es mucho mejor. Los parámetros de tipo tienen restricciones explícitas, pero livianas (tomo su punto sobre las restricciones de operaciones compatibles; CLU mostró cómo hacerlo bien en 1977). La verificación de tipo de género es completamente modular. El código genérico se puede compilar solo una vez para optimizar el espacio del código o se puede especializar en argumentos de tipo particular para un buen rendimiento.

@andrewcmyers

Si el módulo D ahora, sin hacer ningún cambio en las declaraciones visibles en D, comienza a usar algún operador nuevo en un parámetro de tipo […] [los clientes] pueden no estar usando argumentos de tipo que satisfagan las restricciones. Nada le dice al desarrollador D que lo están arruinando.

Claro, pero eso ya es cierto para muchas restricciones implícitas en Go, independientemente de cualquier mecanismo de programación genérico.

Por ejemplo, una función puede recibir un parámetro de tipo interfaz e inicialmente llamar a sus métodos secuencialmente. Si esa función cambia posteriormente para llamar a esos métodos al mismo tiempo (al generar gorrutinas adicionales), la restricción "debe ser seguro para el uso concurrente" no se refleja en el sistema de tipos.

De manera similar, el sistema de tipo Go actual no especifica restricciones en la duración de las variables: algunas implementaciones de io.Writer asumen erróneamente que pueden mantener una referencia al segmento pasado y leerlo más tarde (por ejemplo, haciendo la escritura real de forma asíncrona en una goroutine de fondo), pero eso provoca carreras de datos si la persona que llama a Write intenta reutilizar el mismo segmento de respaldo para un subsiguiente Write .

O una función que usa un cambio de tipo puede tomar una ruta diferente de un método que se agrega a uno de los tipos en el cambio.

O una función que verifica un código de error en particular podría fallar si la función que genera el error cambia la forma en que informa esa condición. (Por ejemplo, consulte https://github.com/golang/go/issues/19647).

O una función que verifica un tipo de error en particular podría fallar si se agregan o eliminan los envoltorios alrededor del error (como sucedió en el paquete estándar net en Go 1.5).

O el almacenamiento en búfer en un canal expuesto en una API puede cambiar, introduciendo interbloqueos y/o carreras.

...y así.

Go no es inusual en este sentido: las restricciones implícitas son omnipresentes en los programas del mundo real.


Si intenta capturar todas las restricciones relevantes en anotaciones explícitas, terminará yendo en una de dos direcciones.

En una dirección, crea un sistema complejo y extremadamente completo de tipos y anotaciones dependientes, y las anotaciones terminan recapitulando una parte sustancial del código que anotan. Como espero que pueda ver claramente, esa dirección no está del todo en consonancia con el diseño del resto del lenguaje Go: Go favorece la simplicidad de la especificación y la concisión del código sobre la tipificación estática integral.

En la otra dirección, las anotaciones explícitas cubrirían solo un subconjunto de las restricciones relevantes para una API determinada. Ahora las anotaciones brindan una falsa sensación de seguridad: el código aún puede romperse debido a cambios en las restricciones implícitas, pero la presencia de restricciones explícitas induce al desarrollador a pensar erróneamente que cualquier cambio "seguro para tipos" también mantiene la compatibilidad.


No es obvio para mí por qué ese tipo de estabilidad de la API debe lograrse a través de una anotación explícita del código fuente: el tipo de estabilidad de la API que está describiendo también se puede lograr (con menos redundancia en el código) a través del análisis del código fuente. Por ejemplo, puede imaginar que la herramienta api analice el código y genere un conjunto de restricciones mucho más rico que el que se puede expresar en el sistema de tipo formal del lenguaje, y que le dé a la herramienta guru la capacidad de consultar el conjunto calculado de restricciones para cualquier función, método o parámetro de la API.

@bcmills ¿No estás convirtiendo lo perfecto en enemigo de lo bueno? Sí, hay restricciones implícitas que son difíciles de capturar en un sistema de tipos. (Y un buen diseño modular evita la introducción de tales restricciones implícitas cuando es factible). Sería genial tener un análisis integral que pueda verificar estáticamente todas las propiedades que desea verificar, y brindar explicaciones claras y no engañosas a los programadores sobre dónde deben verificar. están cometiendo errores. Incluso con el progreso reciente en el diagnóstico y la localización automáticos de errores , no estoy conteniendo la respiración. Por un lado, las herramientas de análisis solo pueden analizar el código que les proporcionas. Los desarrolladores no siempre tienen acceso a todo el código que podría vincularse con el suyo.

Entonces, donde hay restricciones que son fáciles de capturar en un sistema de tipos, ¿por qué no dar a los programadores la capacidad de escribirlas? Tenemos 40 años de experiencia en programación con parámetros de tipo restringidos estáticamente. Esta es una anotación estática simple e intuitiva que vale la pena.

Una vez que comienza a crear un software más grande que superpone los módulos de software, comienza a querer escribir comentarios que expliquen tales restricciones implícitas de todos modos. Suponiendo que hay una forma buena y verificable de expresarlos, ¿por qué no dejar que el compilador se entere de la broma para que pueda ayudarlo?

Observo que algunos de sus ejemplos de otras restricciones implícitas implican el manejo de errores. Creo que nuestra verificación estática ligera de excepciones [ PLDI 2016 ] abordaría estos ejemplos.

@andrewcmyers

Entonces, donde hay restricciones que son fáciles de capturar en un sistema de tipos, ¿por qué no dar a los programadores la capacidad de escribirlas?
[…]
Una vez que comienza a crear un software más grande que superpone los módulos de software, comienza a querer escribir comentarios que expliquen tales restricciones implícitas de todos modos. Suponiendo que hay una forma buena y verificable de expresarlos, ¿por qué no dejar que el compilador se entere de la broma para que pueda ayudarlo?

De hecho, estoy completamente de acuerdo con este punto y, a menudo, uso un argumento similar con respecto a la gestión de la memoria. (Si va a tener que documentar las invariantes en el aliasing y la retención de datos de todos modos, ¿por qué no aplicar esas invariantes en tiempo de compilación?)

Pero llevaría ese argumento un paso más allá: ¡lo contrario también es válido! Si _no_ necesita escribir un comentario para una restricción (porque es obvio en contexto para los humanos que trabajan con el código), ¿por qué debería escribir ese comentario para el compilador? Independientemente de mis preferencias personales, el uso de recolección de basura y valores cero de Go indica claramente un sesgo hacia "no requerir que los programadores establezcan invariantes obvios". Puede darse el caso de que el modelado de estilo Genus pueda expresar muchas de las restricciones que se expresarían en los comentarios, pero ¿cómo le va en términos de eliminar las restricciones que también se eliminarían en los comentarios?

Me parece que los modelos de estilo Genus son más que simples comentarios de todos modos: en realidad cambian la semántica del código en algunos casos, no solo lo restringen. Ahora tendríamos dos mecanismos diferentes, interfaces y modelos de tipo, para parametrizar comportamientos. Eso representaría un cambio importante en el lenguaje Go: hemos descubierto algunas mejores prácticas para las interfaces a lo largo del tiempo (como "definir interfaces en el lado del consumidor") y no es obvio que esa experiencia se traduzca en un sistema tan radicalmente diferente, incluso descuidando la compatibilidad con Go 1.

Además, una de las excelentes propiedades de Go es que su especificación se puede leer (y comprender en gran medida) en una tarde. No es obvio para mí que se pueda agregar un sistema de restricciones al estilo Genus al lenguaje Go sin complicarlo sustancialmente; me gustaría ver una propuesta concreta de cambios en la especificación.

Aquí hay un punto de datos interesante para la "metaprogramación". Sería bueno que ciertos tipos en los paquetes sync y atomic , a saber, atomic.Value y sync.Map , admitan métodos CompareAndSwap , pero esos solo funcionan para tipos que resultan ser comparables. El resto de las API atomic.Value y sync.Map siguen siendo útiles sin esos métodos, por lo que para ese caso de uso necesitamos algo como SFINAE (u otros tipos de API definidas condicionalmente) o tenemos que caer volver a una jerarquía de tipos más compleja.

Quiero abandonar esta idea de sintaxis creativa de usar sílabas aborígenes.

@bcmills ¿Puede explicar más sobre estos tres puntos?

  1. Ambigüedad entre nombres de tipos y nombres de valores.
  2. Soporte excesivamente amplio para la sobrecarga del operador
    3. Exceso de confianza en la resolución de sobrecarga para la metaprogramación

@mahdix Claro.

  1. Ambigüedad entre nombres de tipos y nombres de valores.

Este artículo da una buena introducción. Para analizar un programa C++, debe saber qué nombres son tipos y cuáles son valores. Cuando analiza un programa C++ con plantilla, no tiene esa información disponible para los miembros de los parámetros de la plantilla.

Surge un problema similar en Go para los literales compuestos, pero la ambigüedad es entre valores y nombres de campo en lugar de valores y tipos. En este código Go:

const a = someValue
x := T{a: b}

¿ a es un nombre de campo literal, o es la constante a que se usa como clave de mapa o índice de matriz?

  1. Soporte excesivamente amplio para la sobrecarga del operador

La búsqueda dependiente de argumentos es un buen lugar para comenzar. Las sobrecargas de operadores en C++ pueden ocurrir como métodos en el tipo de receptor o como funciones libres en cualquiera de varios espacios de nombres, y las reglas para resolver esas sobrecargas son bastante complejas.

Hay muchas formas de evitar esa complejidad, pero la más simple (como lo hace Go actualmente) es prohibir por completo la sobrecarga de operadores.

  1. Exceso de confianza en la resolución de sobrecarga para la metaprogramación

La biblioteca <type_traits> es un buen lugar para comenzar. Consulte la implementación en su vecindario amigable libc++ para ver cómo entra en juego la resolución de sobrecarga.

Si Go alguna vez admite la metaprogramación (e incluso eso es muy dudoso), no esperaría que implique una resolución de sobrecarga como la operación fundamental para proteger las definiciones condicionales.

@bcmills
Como nunca he usado C ++, ¿podría arrojar algo de luz sobre dónde se encuentra la sobrecarga del operador mediante la implementación de 'interfaces' predefinidas en términos de complejidad? Python y Kotlin son ejemplos de esto.

Creo que ADL en sí mismo es un gran problema con las plantillas de C ++ que en su mayoría no se mencionaron, porque obligan al compilador a retrasar la resolución de todos los nombres hasta el momento de la creación de instancias, y pueden generar errores muy sutiles, en parte porque el "ideal" y " Los compiladores perezosos" se comportan de manera diferente aquí y el estándar lo permite. El hecho de que admita la sobrecarga de operadores no es realmente la peor parte, ni mucho menos.

Esta propuesta se basa en Plantillas, ¿no sería suficiente un sistema de macro expansión? No estoy hablando de go generate o proyectos como gotemplate. Estoy hablando de más como este:

macro MacroFoo(stmt ast.Statement) {
    ....
}

Macro podría reducir el modelo y el uso de la reflexión.

Creo que C++ es un ejemplo suficientemente bueno de que los genéricos no deberían basarse en plantillas o macros. Especialmente considerando que Go tiene cosas como funciones anónimas que realmente no pueden ser "instanciadas" en tiempo de compilación, excepto como una optimización.

@samadadi , puede expresar su punto de vista sin decir "qué les pasa a ustedes". Habiendo dicho eso, el argumento de la complejidad ya se ha planteado varias veces.

Go no es el primer lenguaje que intenta lograr la simplicidad al omitir el soporte para el polimorfismo paramétrico (genéricos), a pesar de que esa característica se ha vuelto cada vez más importante en los últimos 40 años; en mi experiencia, es un elemento básico de los cursos de programación del segundo semestre.

El problema de no tener la función en el lenguaje es que los programadores terminan recurriendo a soluciones que son aún peores. Por ejemplo, los programadores de Go a menudo escriben plantillas de código que se amplían con macros para producir el código "real" para varios tipos deseados. Pero el verdadero lenguaje de programación es el que escribes, no el que ve el compilador. Entonces, esta estrategia significa que está utilizando un lenguaje (que ya no es estándar) que tiene toda la fragilidad y la sobrecarga de código de las plantillas de C++.

Como se indica en https://blog.golang.org/toward-go2 , debemos proporcionar "informes de experiencia", para que se puedan determinar las necesidades y los objetivos de diseño. ¿Podría tomarse unos minutos y documentar los casos macro que ha observado?

Por favor, mantenga este error en el tema y civilizado. Y de nuevo, https://golang.org/wiki/NoMeToo. Solo comente si tiene información única y constructiva para agregar.

@mandolyte Es muy fácil encontrar explicaciones detalladas en la web que defienden la generación de código como un sustituto (parcial) de los genéricos:
https://appliedgo.net/generics/
https://www.calhoun.io/using-code-generation-to-survive-without-generics-in-go/
http://blog.ralch.com/tutorial/golang-code-generation-and-generics/

Claramente, hay muchas personas que adoptan este enfoque.

@andrewcmyers , existen algunas limitaciones, así como advertencias de conveniencia al usar la generación de código PERO.
En general, si cree que este enfoque es el mejor/lo suficientemente bueno, creo que el esfuerzo por permitir una generación algo similar desde dentro de la cadena de herramientas go sería una bendición.

  • La optimización del compilador puede ser un desafío en este caso, pero el tiempo de ejecución será consistente, Y el mantenimiento del código, la experiencia del usuario (simplicidad...), las mejores prácticas estándar y los estándares de código unificado pueden mantenerse.
    Además, toda la cadena de herramientas se mantendrá igual, aparte de las herramientas de depuración (generadores de perfiles, depuradores de pasos, etc.) que verán líneas de código que no fueron escritas por el desarrollador, pero eso es un poco como ingresar al código ASM durante la depuración, solo es un código legible :).

Desventaja: no hay precedentes (que yo sepa) de este enfoque dentro de la cadena de herramientas go.

Para resumir, considere la generación de código como parte del proceso de compilación, no debería ser demasiado complicado, bastante seguro, optimizado para el tiempo de ejecución, puede mantener la simplicidad y cambios muy pequeños en el lenguaje.

En mi humilde opinión: es un compromiso fácil de lograr, con un precio bajo.

Para ser claros, no considero que la generación de código de estilo macro, ya sea que se haga con gen, cpp, gofmt -r u otras herramientas de macro/plantilla, sea una buena solución para el problema de los genéricos, incluso si está estandarizada. Tiene los mismos problemas que las plantillas de C++: exceso de código, falta de comprobación de tipos modulares y dificultad para la depuración. Empeora cuando comienza, como es natural, a construir código genérico en términos de otro código genérico. En mi opinión, las ventajas son limitadas: mantendría la vida relativamente simple para los escritores del compilador Go y produce un código eficiente, a menos que haya presión en la caché de instrucciones, ¡una situación frecuente en el software moderno!

Creo que el punto era más bien que la generación de código se usa para sustituir
genéricos, por lo que los genéricos deberían buscar resolver la mayoría de esos casos de uso.

El miércoles 26 de julio de 2017 a las 22:41, Andrew Myers, [email protected] escribió:

Para ser claros, no considero la generación de código de estilo macro, ya sea que se haga
con gen, cpp, gofmt -r u otras herramientas de macro/plantilla, para ser un buen
solución al problema de los genéricos aunque estandarizados. tiene lo mismo
problemas como plantillas de C++: exceso de código, falta de verificación de tipos modulares y
dificultad para depurar. Empeora a medida que se empieza, como es natural, a construir
código genérico en términos de otro código genérico. En mi opinión, las ventajas son
limitada: mantendría la vida relativamente simple para los escritores del compilador Go
y produce código eficiente, a menos que haya caché de instrucciones
presión, una situación frecuente en el software moderno!


Estás recibiendo esto porque comentaste.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/15292#issuecomment-318242016 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AT4HVb2SPMpe5dlEDUQeadIRKPaB74zoks5sR_jSgaJpZM4IG-xv
.

Sin duda, la generación de código no es una solución REAL, incluso si se incluye con algún soporte de lenguaje para que se vea y se sienta como una "parte del lenguaje".

Mi punto era que era MUY rentable.

Por cierto, si observa algunos de los sustitutos de generación de código, puede ver fácilmente cómo podrían haber sido mucho más legibles, más rápidos y carecer de algunos conceptos incorrectos (por ejemplo, iteración sobre matrices de punteros frente a valores) si el lenguaje les hubiera dado mejores herramientas. para esto.

Y tal vez ese sea un mejor camino para resolver a corto plazo, eso no se sentiría como un parche:
antes de pensar en el "mejor soporte genérico que también será idiomático" (creo que algunas implementaciones anteriores tardarían años en lograr la integración completa), implemente algunos conjuntos de funciones compatibles "en el idioma" que se necesitan de todos modos (como una compilación en copia profunda de estructuras) haría que estas soluciones de generación de código fueran mucho más utilizables.

Después de leer las propuestas de genéricos de @bcmills y @ianlancetaylor , hice las siguientes observaciones:

Funciones de tiempo de compilación y tipos de primera clase

Me gusta la idea de la evaluación en tiempo de compilación, pero no veo el beneficio de limitarla a funciones puras. Esta propuesta introduce el gotype incorporado, pero limita su uso a funciones const y cualquier tipo de datos definido dentro del alcance de la función. Desde la perspectiva de un usuario de la biblioteca, la creación de instancias se limita a funciones de constructor como "Nuevo" y conduce a firmas de funciones como esta:

const func New(K, V gotype, hashfn Hashfn(K), eqfn Eqfn(K)) func()*Hashmap(K, V, hashfn, eqfn)

El tipo de retorno aquí no se puede separar en un tipo de función porque estamos limitados a funciones puras. Además, la firma define dos nuevos "tipos" en la propia firma (K y V), lo que significa que para analizar un solo parámetro, debemos analizar toda la lista de parámetros. Esto está bien para un compilador, pero me pregunto si agrega complejidad a la API pública de un paquete.

Escriba los parámetros en Go

Los tipos parametrizados permiten la mayoría de los casos de uso de la programación genérica, por ejemplo, la capacidad de definir estructuras de datos genéricas y operaciones sobre diferentes tipos de datos. La propuesta enumera de manera exhaustiva las mejoras al verificador de tipos que serían necesarias para producir mejores errores de compilación, tiempos de compilación más rápidos y archivos binarios más pequeños.

En la sección "Verificador de tipo", la propuesta también enumera algunas restricciones de tipo útiles para acelerar el proceso, como "Indexable", "Comparable", "Llamable", "Compuesto", etc... Lo que no entiendo es ¿Por qué no permitir que el usuario especifique sus propias restricciones de tipo? La propuesta establece que

No hay restricciones sobre cómo se pueden usar los tipos parametrizados en una función parametrizada.

Sin embargo, si los identificadores tuvieran más restricciones vinculadas a ellos, ¿no tendría el efecto de ayudar al compilador? Considerar:

HashMap[Anything,Anything] // Compiler must always compare the implementation and usages to make sure this is valid.

contra

HashMap[Comparable,Anything] // Compiler can first filter out instantiations for incomparable types before running an exhaustive check.

Separar las restricciones de tipo de los parámetros de tipo y permitir restricciones definidas por el usuario también podría mejorar la legibilidad, haciendo que los paquetes genéricos sean más fáciles de entender. Curiosamente, las fallas enumeradas al final de la propuesta con respecto a la complejidad de las reglas de deducción de tipos podrían mitigarse si el usuario las define explícitamente.

@smasher164

Me gusta la idea de la evaluación en tiempo de compilación, pero no veo el beneficio de limitarla a funciones puras.

El beneficio es que hace posible la compilación por separado. Si una función en tiempo de compilación puede modificar el estado global, entonces el compilador debe tener ese estado disponible o registrar las modificaciones en él de tal manera que el enlazador pueda secuenciarlas en el momento del enlace. Si una función en tiempo de compilación puede modificar el estado local, entonces necesitaríamos alguna forma de rastrear qué estado es local versus global. Ambos agregan complejidad, y no es obvio que ninguno de los dos brinde suficientes beneficios para compensarlo.

@smasher164

Lo que no entiendo es por qué no permitir que el usuario especifique sus propias restricciones de tipo.

Las restricciones de tipo en esa propuesta corresponden a operaciones en la sintaxis del lenguaje. Eso reduce el área superficial de las nuevas características: no hay necesidad de especificar sintaxis adicional para restringir tipos, porque todas las restricciones sintácticas se pueden deducir del uso.

si los identificadores tuvieran más restricciones vinculadas a ellos, ¿no tendría eso el efecto de ayudar al compilador?

El lenguaje debe estar diseñado para sus usuarios, no para los compiladores-escritores.

no es necesario especificar una sintaxis adicional para restringir los tipos porque todas las restricciones sintácticas se pueden deducir del uso.

Esta es la ruta que siguió C++. Requiere un análisis de programa global para identificar los usos relevantes. Los programadores no pueden razonar sobre el código de forma modular, y los mensajes de error son detallados e incomprensibles.

Puede ser tan fácil y ligero especificar las operaciones necesarias. Ver CLU (1977) para un ejemplo.

@andrewcmyers

Requiere un análisis de programa global para identificar los usos relevantes. Los programadores no pueden razonar sobre el código de forma modular,

Eso es usar una definición particular de "modular", que no creo que sea tan universal como parece suponer. Según la propuesta de 2013, cada función o tipo tendría un conjunto inequívoco de restricciones inferidas de abajo hacia arriba a partir de paquetes importados, exactamente de la misma manera que el tiempo de ejecución (y las restricciones de tiempo de ejecución) de las funciones no paramétricas se derivan de abajo hacia arriba. de las cadenas de llamadas hoy.

Presumiblemente, podría consultar las restricciones inferidas usando guru o una herramienta similar, y podría responder esas consultas usando información local de los metadatos del paquete exportado.

y los mensajes de error son detallados e incomprensibles.

Tenemos un par de ejemplos (GCC y MSVC) que demuestran que los mensajes de error generados ingenuamente son incomprensibles. Creo que es exagerado suponer que los mensajes de error para restricciones implícitas son intrínsecamente malos.

Creo que la mayor desventaja de las restricciones inferidas es que facilitan el uso de un tipo de una manera que introduce una restricción sin comprenderla por completo. En el mejor de los casos, esto solo significa que sus usuarios pueden encontrarse con fallas inesperadas en el tiempo de compilación, pero en el peor de los casos, esto significa que puede romper el paquete para los consumidores al introducir una nueva restricción sin darse cuenta. Las restricciones explícitamente especificadas evitarían esto.

Personalmente, tampoco creo que las restricciones explícitas estén fuera de línea con el enfoque Go existente, ya que las interfaces son restricciones de tipo de tiempo de ejecución explícitas, aunque tienen una expresividad limitada.

Tenemos un par de ejemplos (GCC y MSVC) que demuestran que los mensajes de error generados ingenuamente son incomprensibles. Creo que es exagerado asumir que los mensajes de error para restricciones implícitas son intrínsecamente malos.

La lista de compiladores en los que la inferencia de tipo no local, que es lo que propone, da como resultado mensajes de error incorrectos es bastante más larga que eso. Incluye SML, OCaml y GHC, donde ya se ha realizado un gran esfuerzo para mejorar sus mensajes de error y donde hay al menos alguna estructura de módulo explícita que ayuda. Es posible que pueda hacerlo mejor, y si se le ocurre un algoritmo para buenos mensajes de error con el esquema que propone, tendrá una buena publicación. Como punto de partida hacia ese algoritmo, puede encontrar útiles nuestros documentos POPL 2014 y PLDI 2015 sobre localización de errores. Son más o menos lo último en tecnología.

porque todas las restricciones sintácticas se pueden inferir del uso.

¿Eso no limita la amplitud de los programas genéricos comprobables de tipo? Por ejemplo, tenga en cuenta que la propuesta type-params no especifica una restricción "Iterable". En el lenguaje actual, esto correspondería a un segmento o canal, pero un tipo compuesto (por ejemplo, una lista enlazada) no necesariamente cumpliría con esos requisitos. Definición de una interfaz como

type Iterable[T] interface {
    Next() T
}

ayuda en el caso de la lista enlazada, pero ahora los tipos de canales y sectores integrados deben ampliarse para satisfacer esta interfaz.

Una restricción que dice "Acepto el conjunto de todos los tipos que son iterables, segmentos o canales" parece una situación en la que todos ganan para el usuario, el autor del paquete y el implementador del compilador. El punto que estoy tratando de hacer es que las restricciones son un superconjunto de programas sintácticamente válidos, y algunos pueden no tener sentido desde la perspectiva del lenguaje, pero solo desde la perspectiva de la API.

El lenguaje debe estar diseñado para sus usuarios, no para los compiladores-escritores.

Estoy de acuerdo, pero tal vez debería haberlo expresado de otra manera. La eficiencia mejorada del compilador podría ser un efecto secundario de las restricciones definidas por el usuario. El principal beneficio sería la legibilidad, ya que el usuario tiene una mejor idea del comportamiento de su API que el compilador de todos modos. La compensación aquí es que los programas genéricos tendrían que ser un poco más explícitos sobre lo que aceptan.

¿Qué pasa si en lugar de

type Iterable[T] interface {
    Next() T
}

separamos la idea de "interfaces" de "restricciones". Entonces podríamos tener

type T generic

type Iterable class {
    Next() T
}

donde "clase" significa una clase de tipo de estilo Haskell, no una clase de estilo Java.

Tener "clases de tipos" separadas de las "interfaces" podría ayudar a aclarar algo de la falta de ortogonalidad de las dos ideas. Entonces Sortable (ignorando sort.Interface) podría verse así:

type T generic

type Comparable class {
    Less(a, b T) bool
}

type Sortable class {
    Next() Comparable
}

Aquí hay algunos comentarios sobre la sección "Clases y conceptos de tipos" en Genus por @andrewcmyers y su aplicabilidad a Go.

Esta sección aborda las limitaciones de las clases de tipos y conceptos, indicando

primero, la satisfacción de la restricción debe ser presenciada de manera única

No estoy seguro de entender esta limitación. ¿Atar una restricción a identificadores separados no evitaría que sea único para un tipo dado? Me parece que la cláusula "where" en Genus esencialmente construye un tipo/restricción a partir de una restricción dada, pero esto parece análogo a instanciar una variable de un tipo dado. Una restricción de esta manera se asemeja a un tipo .

Aquí hay una simplificación dramática de las definiciones de restricciones, adaptadas a Go:

kind Any interface{} // accepts any type that satisfies interface{}.
type T Any // Declare a type of Any kind. Also binds it to an identifier.
kind Eq T == T // accepts any type for which equality is defined.

Así que una declaración de mapa aparecería como:

type Map[K Eq, V Any] struct {
}

donde en Genus, podría verse así:

type Map[K, V] where Eq[K], Any[V] struct {
}

y en la propuesta Type-Params existente se vería así:

type Map[K,V] struct {
}

Creo que todos podemos estar de acuerdo en que permitir que las restricciones aprovechen el sistema de tipos existente puede eliminar la superposición entre las características del lenguaje y facilitar la comprensión de las nuevas.

y segundo, sus modelos definen cómo adaptar un solo tipo, mientras que en un lenguaje con subtipos, cada tipo adaptado en general representa todos sus subtipos.

Esta limitación parece menos pertinente para Go, ya que el lenguaje ya tiene buenas reglas de conversión entre tipos con nombre/sin nombre e interfaces superpuestas.

Los ejemplos dados proponen modelos como solución, lo que parece ser una característica útil pero no necesaria para Go. Si una biblioteca espera que un tipo implemente http.Handler, por ejemplo, y el usuario desea comportamientos diferentes según el contexto, escribir adaptadores es simple:

type handleFunc func(http.ResponseWriter, *http.Request)
func (f handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { f(w,r) }

De hecho, esto es lo que hace la biblioteca estándar .

@smasher164

primero, la satisfacción de la restricción debe ser presenciada de manera única
No estoy seguro de entender esta limitación. ¿Atar una restricción a identificadores separados no evitaría que fuera único para un tipo dado?

La idea es que en Genus puedes satisfacer la misma restricción con el mismo tipo en más de una forma, a diferencia de Haskell. Por ejemplo, si tiene un HashSet[T] , puede escribir HashSet[String] para hacer hash de cadenas de la forma habitual, pero HashSet[String with CaseInsens] para hacer hash y comparar cadenas con CaseInsens model, que presumiblemente trata las cadenas sin distinguir entre mayúsculas y minúsculas. Género en realidad distingue estos dos tipos; esto podría ser excesivo para Go. Incluso si el sistema de tipos no realiza un seguimiento, parece importante poder anular las operaciones predeterminadas proporcionadas por un tipo.

kind Cualquier interfaz{} // acepta cualquier tipo que satisfaga la interfaz{}.
type T Any // Declara un tipo de Any kind. También lo vincula a un identificador.
kind Eq T == T // acepta cualquier tipo para el que se defina la igualdad.
tipo Mapa[K Eq, V Cualquiera] estructura { ...
}

El equivalente moral de esto en Genus sería:

constraint Any[T] {}
// Just use Any as if it were a type
constraint Eq[K] {
   boolean equals(K);
}
class Map[K, V] where Eq[K] { ... }

En Familia escribiríamos simplemente:

interface Eq {
    boolean equals(This);
}
class Map[K where Eq, V] { ... }

Editar: retractarse de esto a favor de una solución basada en reflejo como se describe en # 4146 Una solución basada en genéricos como describí a continuación crece linealmente en el número de composiciones. Si bien una solución basada en reflexión siempre tendrá una desventaja de rendimiento, puede optimizarse en tiempo de ejecución para que la desventaja sea constante independientemente de la cantidad de composiciones.

Esta no es una propuesta sino un posible caso de uso a considerar al diseñar una propuesta.

Dos cosas son comunes en el código Go hoy

  • envolviendo un valor de interfaz para proporcionar funcionalidad adicional (envolviendo un http.ResponseWriter para un marco)
  • tener métodos opcionales que a veces tienen los valores de interfaz (como Temporary() bool en net.Error )

Estos son buenos y útiles, pero no se mezclan. Una vez que haya envuelto una interfaz, perderá la capacidad de acceder a cualquier método no definido en el tipo de envoltorio. Es decir, dado

type MyError struct {
  error
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.error)
}

Si envuelve un error en esa estructura, oculta cualquier método adicional en el error original.

Si no envuelve el error en la estructura, no puede proporcionar el contexto adicional.

Digamos que la propuesta genérica aceptada te permite definir algo como lo siguiente (sintaxis arbitraria que traté de hacer intencionalmente fea para que nadie se enfocara en ella)

type MyError generic_over[E which_is_a_type_satisfying error] struct {
  E
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.E)
}

Al aprovechar la incrustación, podemos incrustar cualquier tipo concreto que satisfaga la interfaz de error y envolverlo y tener acceso a sus otros métodos. Desafortunadamente, esto solo nos lleva a una parte del camino.

Lo que realmente necesitamos aquí es tomar un valor arbitrario de la interfaz de error e incrustar su tipo dinámico.

Esto plantea inmediatamente dos preocupaciones.

  • el tipo tendría que ser creado en tiempo de ejecución (probablemente necesitado por el reflejo de todos modos)
  • la creación de tipos tendría que entrar en pánico si el valor del error es nulo

Si eso no le ha molestado con la idea, también necesita un mecanismo para "saltar" sobre la interfaz a su tipo dinámico, ya sea mediante una anotación en la lista de parámetros genéricos para decir "siempre instanciar en el tipo dinámico de valores de interfaz "o por alguna función mágica que solo se puede llamar durante la creación de instancias de tipo para desempaquetar la interfaz para que su tipo y valor se puedan empalmar correctamente.

Sin eso, solo está instanciando MyError en el tipo de error en sí, no en el tipo dinámico de la interfaz.

Digamos que tenemos una función mágica unbox para extraer y (de alguna manera) aplicar la información:

func wrap(ec extraContext, err error) error {
  if err == nil {
    return nil
  }
  return MyError{
    E: unbox(err),
    extraContext: ec,
  }
}

Ahora digamos que tenemos un error no nulo, err , cuyo tipo dinámico es *net.DNSError . Luego esto

wrapped := wrap(getExtraContext(), err)
//wrapped 's dynamic type is a MyStruct embedding E=*net.DNSError
_, ok := wrapped.(net.Error)
fmt.Println(ok)

imprimiría true . Pero si el tipo dinámico de err hubiera sido *os.PathError , se habría impreso falso.

Espero que la semántica propuesta sea clara dada la sintaxis obtusa utilizada en la demostración.

También espero que haya una mejor manera de resolver ese problema con menos mecanismo y ceremonia, pero creo que lo anterior podría funcionar.

@jimmyfrasche Si entiendo lo que quiere, es un mecanismo de adaptación sin envoltura. Desea poder expandir el conjunto de operaciones que ofrece un tipo sin envolverlo en otro objeto que oculte el original. Esta es una funcionalidad que ofrece Genus.

@andrewcmyers No.

Las estructuras en Go permiten incrustar. Si agrega un campo sin nombre pero con un tipo a una estructura, hace dos cosas: crea un campo con el mismo nombre que el tipo y permite el envío transparente a cualquier método de ese tipo. Eso suena terriblemente a herencia, pero no lo es. Si tenía un tipo T que tenía un método Foo (), entonces los siguientes son equivalentes

type S struct {
  T
}

y

type S struct {
  T T
}
func (s S) Foo() {
  s.T.Foo()
}

(cuando se llama Foo, su "esto" siempre es de tipo T).

También puede incrustar interfaces en estructuras. Esto le da a la estructura todos los métodos en el contrato de la interfaz (aunque necesita asignar algún valor dinámico al campo implícito o causará pánico con el equivalente de una excepción de puntero nulo)

Go tiene interfaces que definen un contrato en términos de los métodos de un tipo. Un valor de cualquier tipo que satisfaga el contrato se puede encuadrar en un valor de esa interfaz. Un valor de una interfaz es un puntero al manifiesto de tipo interno (tipo dinámico) y un puntero a un valor de ese tipo dinámico (valor dinámico). Puede escribir aserciones en un valor de interfaz para (a) obtener el valor dinámico si afirma su tipo sin interfaz o (b) obtener un nuevo valor de interfaz si afirma en una interfaz diferente que el valor dinámico también satisface. Es común usar este último para "probar características" de un objeto para ver si admite métodos opcionales. Para reutilizar un ejemplo anterior, algunos errores tienen un método "Temporary() bool" para que pueda ver si algún error es temporal con:

func isTemp(err error) bool {
  if t, ok := err.(interface{ Temporary() bool}); ok {
    return t.Temporary()
  }
  return false
}

También es común envolver un tipo en otro tipo para proporcionar funciones adicionales. Esto funciona bien con tipos sin interfaz. Cuando envuelve una interfaz, también oculta los métodos que no conoce y no puede recuperarlos con aserciones de tipo "prueba de características": el tipo envuelto solo expone los métodos requeridos de la interfaz incluso si tiene métodos opcionales . Considerar:

type A struct {}
func (A) Foo()
func (A) Bar()

type I interface {
  Foo()
}

type B struct {
  I
}

var i I = B{A{}}

No puede llamar a Bar en i o incluso saber que existe a menos que sepa que el tipo dinámico de i es una B para que pueda desenvolverlo y acceder al campo I para escribir afirmar en eso .

Esto causa problemas reales, especialmente al tratar con interfaces comunes como error o Reader.

Si hubiera una manera de extraer el tipo y el valor dinámicos de una interfaz (de alguna manera segura y controlada), podría parametrizar un nuevo tipo con eso, establecer el campo incrustado en el valor y devolver una nueva interfaz. Luego, obtiene un valor que satisface la interfaz original, tiene cualquier funcionalidad mejorada que desee agregar, pero el resto de los métodos del tipo dinámico original aún están ahí para probar las funciones.

@jimmyfrasche De hecho. Lo que Genus le permite hacer es usar un tipo para satisfacer un contrato de "interfaz" sin encasillarlo. El valor todavía tiene su tipo original y sus operaciones originales. Además, el programa puede especificar qué operaciones debe usar el tipo para satisfacer el contrato; por defecto, son las operaciones que proporciona el tipo, pero el programa puede proporcionar otras nuevas si el tipo no tiene las operaciones necesarias. También puede reemplazar las operaciones que usaría el tipo.

@jimmyfrasche @andrewcmyers Para ese caso de uso, consulte también https://github.com/golang/go/issues/4146#issuecomment -318200547.

@jimmyfrasche Para mí, parece que el problema clave aquí es obtener el tipo/valor dinámico de una variable. Dejando de lado la incrustación, un ejemplo simplificado sería

type MyError generic_over[E which_is_a_type_satisfying error] struct {
  e E
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.e)
}

El valor que se asigna a e debe tener un tipo dinámico (o concreto) de algo como *net.DNSError , que implementa error . Aquí hay un par de formas posibles en que un futuro cambio de idioma podría abordar este problema:

  1. Tenga una función mágica similar a unbox que descubra el valor dinámico de una variable. Esto aplica para cualquier tipo que no sea concreto, por ejemplo uniones.
  2. Si el cambio de idioma admite variables de tipo, proporcione un medio para obtener el tipo dinámico de la variable. Con la información de tipo, podemos escribir la función unbox nosotros mismos. Por ejemplo,
func unbox(v T1) T2 {
    t := dynTypeOf(v)
    return v.(t)
}

wrap se puede escribir de la misma forma que antes, o como

func wrap(ec extraContext, err error) error {
  if err == nil {
    return nil
  }
  t := dynTypeOf(err)
  return MyError{
    e: v.(t),
    extraContext: ec,
  }
}
  1. Si el cambio de idioma admite restricciones de tipo, he aquí una idea alternativa:
type E1 which_is_a_type_satisfying error
type E2 which_is_a_type_satisfying error

func wrap(ec extraContext, err E1) E2 {
  if err == nil {
    return nil
  }
  return MyError{
    e: err,
    extraContext: ec,
  }
}

En este ejemplo, aceptamos un valor de cualquier tipo que implemente error. Cualquier usuario de wrap que espera un error recibirá uno. Sin embargo, el tipo de e dentro MyError es el mismo que el de err que se pasa, que no se limita a un tipo de interfaz. Si uno quisiera el mismo comportamiento que 2,

var iface error = ...
wrap(getExtraContext(), unbox(iface))

Dado que nadie más parece haberlo hecho, me gustaría señalar los "informes de experiencia" muy obvios para los genéricos según lo solicitado por https://blog.golang.org/toward-go2.

El primero es el tipo map incorporado:

m := make(map[string]string)

El siguiente es el tipo chan incorporado:

c := make(chan bool)

Finalmente, la biblioteca estándar está plagada de interface{} alternativas donde los genéricos funcionarían de manera más segura:

  • heap.Interface (https://golang.org/pkg/container/heap/#Interface)
  • list.Element (https://golang.org/pkg/container/list/#Element)
  • ring.Ring (https://golang.org/pkg/container/ring/#Ring)
  • sync.Pool (https://golang.org/pkg/sync/#Pool)
  • próximos sync.Map (https://tip.golang.org/pkg/sync/#Map)
  • atomic.Value (https://golang.org/pkg/sync/atomic/#Value)

Puede haber otros que me faltan. El punto es que cada uno de los anteriores es donde esperaría que los genéricos fueran útiles.

(Nota: no incluyo sort.Sort aquí porque es un excelente ejemplo de cómo se pueden usar las interfaces en lugar de las genéricas).

http://www.yinwang.org/blog-cn/2014/04/18/golang
Creo que el genérico es importante. De lo contrario, no puede manejar tipos similares. En algún momento, la interfaz no puede resolver el problema.

La sintaxis simple y el sistema de tipos son las ventajas importantes de Go. Si agrega genéricos, el lenguaje se convertirá en un desastre feo como Scala o Haskell. Además, esta función atraerá a fanáticos seudoacadémicos, que eventualmente transformarán los valores de la comunidad de "Hagámoslo" a "Hablemos de teoría y matemáticas de CS". Evita los genéricos, es un camino al abismo.

@bxqgit por favor mantenga esto civilizado. No hay necesidad de insultar a nadie.

En cuanto a lo que traerá el futuro, ya veremos, pero sé que aunque el 98 % de mi tiempo no necesito medicamentos genéricos, cada vez que los necesito, desearía poder usarlos. Cómo se usan frente a cómo se usan indebidamente es una discusión diferente. Educar a los usuarios debe ser parte del proceso.

@bxqgit
Hay situaciones en las que se necesitan genéricos, como estructuras de datos genéricas (Árboles, Pilas, Colas, ...) o funciones genéricas (Mapear, Filtrar, Reducir, ...) y estas cosas son inevitables, usando interfaces en lugar de genéricos en estas situaciones solo agregan una gran complejidad tanto para el escritor de código como para el lector de código y también tienen un efecto negativo en la eficiencia del código en tiempo de ejecución, por lo que debería ser mucho más racional agregar genéricos de lenguaje que tratar de usar interfaces y reflexionar para escribir complejos y código ineficiente.

@bxqgit Agregar genéricos no necesariamente agrega complejidad al lenguaje, esto también se puede lograr con una sintaxis simple. Con los genéricos, está agregando una restricción de tipo de tiempo de compilación variable que es muy útil con las estructuras de datos, como dijo @riwogo .

El sistema de interfaz actual en go es muy útil, sin embargo es muy malo cuando se necesita, por ejemplo, una implementación general de lista, que con las interfaces necesita una restricción de tipo de tiempo de ejecución, sin embargo, si agrega genéricos, el tipo genérico se puede sustituir en tiempo de compilación con el tipo real, haciendo que la restricción sea innecesaria.

Además, recuerde, las personas detrás van, desarrollan el lenguaje usando lo que usted llama "teoría y matemáticas de CS", y también son las personas que "están logrando esto".

Además, recuerde, las personas detrás van, desarrollan el lenguaje usando lo que usted llama "teoría y matemáticas de CS", y también son las personas que "están logrando esto".

Personalmente, no veo mucha teoría CS y matemáticas en el diseño del lenguaje Go. Es un lenguaje bastante primitivo, lo cual es bueno en mi opinión. También esas personas de las que hablas decidieron evitar los genéricos e hicieron las cosas. Si funciona bien, ¿por qué cambiar algo? En general, creo que evolucionar y ampliar constantemente la sintaxis del lenguaje es una mala práctica. Solo agrega complejidad que conduce al caos de Haskell y Scala.

La plantilla es complicada pero Generics es simple

Mire las funciones SortInts, SortFloats, SortStrings en el paquete de clasificación. O SearchInts, SearchFloats, SearchStrings. O los métodos Len, Less y Swap de byName en el paquete io/ioutil. Pura copia repetitiva.

Las funciones de copiar y agregar existen porque hacen que los segmentos sean mucho más útiles. Los genéricos significarían que estas funciones son innecesarias. Los genéricos harían posible escribir funciones similares para mapas y canales, sin mencionar los tipos de datos creados por el usuario. Por supuesto, los segmentos son el tipo de datos compuestos más importante, y es por eso que se necesitaban estas funciones, pero otros tipos de datos siguen siendo útiles.

Mi voto es no a los genéricos de aplicaciones generalizados, sí a funciones genéricas más integradas como append y copy que funcionan en múltiples tipos base. ¿Quizás se podrían agregar sort y search para los tipos de colección?

Para mis aplicaciones, el único tipo que falta es un conjunto desordenado (https://github.com/golang/go/issues/7088), me gustaría que fuera un tipo integrado para que tenga la tipificación genérica como slice y map . Ponga el trabajo en el compilador (evaluaciones comparativas para cada tipo base y un conjunto seleccionado de tipos struct y luego ajuste para obtener el mejor rendimiento) y mantenga las anotaciones adicionales fuera del código de la aplicación.

smap integrado en lugar de sync.Map también, por favor. Según mi experiencia, usar interface{} para la seguridad del tipo de tiempo de ejecución es una falla de diseño. La verificación de tipos en tiempo de compilación es una de las principales razones para usar Go.

@pciet

Según mi experiencia, el uso de la interfaz{} para la seguridad del tipo de tiempo de ejecución es una falla de diseño.

¿Puedes simplemente escribir un envoltorio pequeño (tipo seguro)?
https://play.golang.org/p/tG6hd-j5yx

@pierrre Ese envoltorio es mejor que un cheque de reflect.TypeOf(item).AssignableTo(type) . Pero escribir su propio tipo con map + sync.Mutex o sync.RWMutex es la misma complejidad sin la afirmación de tipo que requiere sync.Map .

Mi uso de mapas sincronizados ha sido para mapas globales de mutexes con un var myMapLock = sync.RWMutex{} al lado en lugar de hacer un tipo. Esto podría ser más limpio. Un tipo integrado genérico me suena bien, pero requiere un trabajo que no puedo hacer, y prefiero mi enfoque en lugar de la aserción de tipo.

Sospecho que la reacción visceral negativa a los genéricos que muchos programadores de Go parecen tener surge porque su principal exposición a los genéricos fue a través de plantillas de C++. Esto es desafortunado porque C ++ se equivocó trágicamente en los genéricos desde el día 1 y ha estado agravando el error desde entonces. Los genéricos para Go podrían ser mucho más simples y menos propensos a errores.

Sería decepcionante ver que Go se vuelve cada vez más complejo al agregar tipos parametrizados incorporados. Sería mejor simplemente agregar el soporte de lenguaje para que los programadores escriban sus propios tipos parametrizados. Entonces, los tipos especiales podrían proporcionarse como bibliotecas en lugar de abarrotar el lenguaje central.

@andrewcmyers "Los genéricos para Go podrían ser mucho más simples y menos propensos a errores". --- como genéricos en C#.

Es decepcionante ver que Go se vuelve cada vez más complejo al agregar tipos parametrizados integrados.

A pesar de la especulación en este tema, creo que es muy poco probable que esto suceda.

El exponente de la medida de complejidad de los tipos parametrizados es la varianza.
Los tipos de Go (exceptuando las interfaces) son invariantes y esto puede y debe ser
mantuvo la regla.

Una implementación genérica de tipo "copiar y pegar" mecánica y asistida por compilador
resolvería el 99% del problema de una manera fiel a la base subyacente de Go
principios de superficialidad y no sorpresa.

Por cierto, esta y docenas de otras ideas viables han sido discutidas.
antes y algunos incluso culminaron en enfoques buenos y viables. En este
punto, estoy en el borde del papel de aluminio sobre cómo desaparecieron todos
en silencio al vacío.

El 28 de noviembre de 2017 a las 23:54, "Andrew Myers" [email protected] escribió:

Sospecho que la reacción visceral negativa a los genéricos que muchos Go
parece que los programadores surgen porque su principal exposición a los genéricos fue
a través de plantillas de C++. Esto es desafortunado porque C ++ obtuvo genéricos trágicamente
mal desde el día 1 y ha ido agravando el error desde entonces. Genéricos para
Go podría ser mucho más simple y menos propenso a errores.

Es decepcionante ver que Go se vuelve cada vez más complejo al agregar
tipos parametrizados incorporados. Sería mejor simplemente agregar el idioma.
soporte para que los programadores escriban sus propios tipos parametrizados. Entonces la
los tipos especiales podrían proporcionarse como bibliotecas en lugar de abarrotar
el lenguaje central.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/15292#issuecomment-347691444 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AJZ_jPsQd2qBbn9NI1wZeT-O2JpyraTMks5s7I81gaJpZM4IG-xv
.

Sí, puede tener genéricos sin plantillas. Las plantillas son una forma de polimorfismo paramétrico avanzado principalmente para instalaciones de metaprogramación.

@ianlancetaylor Rust permite que un programa implemente un rasgo T en un tipo existente Q , siempre que su caja defina T o Q .

Solo un pensamiento: me pregunto si Simon Peyton Jones (sí, de la fama de Haskell) y/o los desarrolladores de Rust podrían ayudar. Rust y Haskell tienen probablemente los dos sistemas de tipos más avanzados de todos los lenguajes de producción, y Go debería aprender de ellos.

También está Phillip Wadler , que trabajó en Generic Java , que finalmente condujo a la implementación de genéricos que Java tiene hoy.

@tarcieri No creo que los genéricos de Java sean muy buenos, pero están probados en batalla.

@DemiMarie Hemos tenido a Andrew Myers colaborando aquí, afortunadamente.

Basado en mi experiencia personal, creo que las personas que saben mucho sobre diferentes idiomas y diferentes tipos de sistemas pueden ser muy útiles para examinar ideas. Pero para producir las ideas en primer lugar, lo que necesitamos son personas que estén muy familiarizadas con Go, cómo funciona hoy y cómo puede funcionar razonablemente en el futuro. Go está diseñado para ser, entre otras cosas, un lenguaje simple. Importar ideas de lenguajes como Haskell o Rust, que son significativamente más complicados que Go, es poco probable que sea una buena opción. Y, en general, es poco probable que las ideas de personas que aún no han escrito una cantidad razonable de código Go encajen bien; no es que las ideas sean malas como tales, solo que no encajarán bien con el resto del lenguaje.

Por ejemplo, es importante entender que Go ya tiene soporte parcial para programación genérica usando tipos de interfaz y ya tiene soporte (casi) completo usando el paquete reflect. Si bien esos dos enfoques de la programación genérica no son satisfactorios por varias razones, cualquier propuesta de genéricos en Go tiene que interactuar bien con ellos y, al mismo tiempo, abordar sus deficiencias.

De hecho, mientras estoy aquí, hace un tiempo pensé en la programación genérica con interfaces y se me ocurrieron tres razones por las que no es satisfactoria.

  1. Las interfaces requieren que todas las operaciones se expresen como métodos. Eso hace que sea doloroso escribir una interfaz para tipos integrados, como tipos de canales. Todos los tipos de canales admiten el operador <- para las operaciones de envío y recepción, y es bastante fácil escribir una interfaz con los métodos Send y Receive , pero para asignar un valor de canal para ese tipo de interfaz, debe escribir los métodos repetitivos Send y Receive . Esos métodos repetitivos se verán exactamente iguales para cada tipo de canal diferente, lo cual es tedioso.

  2. Las interfaces se escriben dinámicamente, por lo que los errores que combinan diferentes valores escritos estáticamente solo se detectan en tiempo de ejecución, no en tiempo de compilación. Por ejemplo, una función Merge que combina dos canales en un solo canal usando sus métodos Send y Receive requerirá que los dos canales tengan elementos del mismo tipo, pero que La verificación solo se puede hacer en tiempo de ejecución.

  3. Las interfaces siempre están enmarcadas. Por ejemplo, no hay forma de usar interfaces para agregar un par de otros tipos sin poner esos otros tipos en valores de interfaz, lo que requiere asignaciones de memoria adicionales y búsqueda de punteros.

Estoy feliz de comentar sobre propuestas genéricas para Go. Quizás también sea de interés la creciente cantidad de investigación sobre genéricos en Cornell últimamente, aparentemente relevante para lo que podría hacerse con Go:

http://www.cs.cornell.edu/andru/papers/familia/ (Zhang & Myers, OOPSLA'17)
http://io.livecode.ch/learn/namin/unsound (Amin y Tate, OOPSLA'16)
http://www.cs.cornell.edu/projects/genus/ (Zhang et al., PLDI '15)
https://www.cs.cornell.edu/~ross/publications/shapes/shapes-pldi14.pdf (Greenman, Muehlboeck & Tate, PLDI '14)

En la evaluación comparativa del mapa frente al segmento para un tipo de conjunto desordenado, escribí pruebas unitarias separadas para cada uno, pero con los tipos de interfaz puedo combinar esas dos listas de pruebas en una sola:

type Item interface {
    Equal(Item) bool
}

type Set interface {
    Add(Item) Set
    Remove(Item) Set
    Combine(...Set) Set
    Reduce() Set
    Has(Item) bool
    Equal(Set) bool
    Diff(Set) Set
}

Probando la eliminación de un elemento:

type RemoveCase struct {
    Set
    Item
    Out Set
}

func TestRemove(t *testing.T) {
    for i, c := range RemoveCases {
        if c.Out.Equal(c.Set.Remove(c.Item)) == false {
            t.Fatalf("%v failed", i)
        }
    }
}

De esta manera, puedo juntar mis casos previamente separados en una porción de casos sin ningún problema:

var RemoveCases = []RemoveCase{
    {
        Set: MapPathSet{
            &Path{{0, 0}}:         {},
            &Path{{0, 1}, {1, 1}}: {},
        },
        Item: Path{{0, 0}},
        Out: MapPathSet{
            &Path{{0, 1}, {1, 1}}: {},
        },
    },
    {
        Set: SlicePathSet{
            {{0, 0}},
            {{0, 1}, {1, 1}},
        },
        Item: Path{{0, 0}},
        Out: SlicePathSet{
            {{0, 1}, {1, 1}},
        },
    },
}

Para cada tipo concreto tuve que definir los métodos de interfaz. Por ejemplo:

func (the MapPathSet) Remove(an Item) Set {
    return MapDelete(the, an.(Path))
}
func (the SlicePathSet) Remove(an Item) Set {
    return SliceDelete(the, an.(Path))
}

Estas pruebas genéricas podrían usar una verificación de tipo de tiempo de compilación propuesta:

type Item generic {
    Equal(Item) bool
}
func (the SlicePathSet) Remove(an Item) Set {
    return SliceDelete(the, an)
}

Fuente: https://github.com/pciet/pathsetbenchmark

Pensando en eso más, no parece que sea posible una verificación de tipo en tiempo de compilación para tal prueba, ya que tendría que ejecutar el programa para saber si un tipo se pasa al método de interfaz correspondiente.

Entonces, ¿qué pasa con un tipo "genérico" que es una interfaz y tiene una afirmación de tipo invisible agregada por el compilador cuando se usa concretamente?

@andrewcmyers El artículo de "Familia" fue interesante (y muy por encima de mi cabeza). Una noción clave era la herencia. ¿Cómo cambiarían los conceptos para un lenguaje como Go que se basa en la composición en lugar de la herencia?

Gracias. La parte de la herencia no se aplica a Go; si solo está interesado en los genéricos para Go, puede dejar de leer después de la sección 4 del documento. Lo principal de ese documento que es relevante para Go es que muestra cómo usar las interfaces tanto en la forma en que se usan para Go ahora como en las restricciones de tipos para abstracciones genéricas. Lo que significa que obtiene el poder de las clases de tipos de Haskell sin agregar una construcción completamente nueva al lenguaje.

@andrewcmyers ¿Puede dar un ejemplo de cómo se vería esto en Go?

Lo principal de ese documento que es relevante para Go es que muestra cómo usar las interfaces tanto en la forma en que se usan para Go ahora como en las restricciones de tipos para abstracciones genéricas.

Según tengo entendido, la interfaz Go define una restricción en un tipo (por ejemplo, "este tipo se puede comparar por igualdad usando la 'interfaz comparable de tipo' porque satisface tener un método Eq"). No estoy seguro de entender lo que quiere decir con una restricción de tipo.

No estoy familiarizado con Haskell, pero leer una descripción general rápida me hace adivinar que los tipos que se ajustan a una interfaz Go encajarían en esa clase de tipo. ¿Puede explicar en qué se diferencian las clases de tipos de Haskell?

Una comparación concreta entre Familia y Go sería interesante. Gracias por compartir tu papel.

Las interfaces de Go pueden verse como una descripción de una restricción en los tipos, a través de subtipos estructurales. Sin embargo, esa restricción de tipo, tal como está, no es lo suficientemente expresiva para capturar las restricciones que desea para la programación genérica. Por ejemplo, no puede expresar la restricción de tipo denominada Eq en el documento de Familia.

Algunas ideas sobre la motivación para instalaciones de programación más genéricas en Go:

Así que ahí está mi lista de prueba genérica que realmente no necesita agregar nada al lenguaje. En mi opinión, el tipo genérico que propuse no satisface el objetivo de Go de comprensión directa, no tiene mucho que ver con el término de programación generalmente aceptado, y hacer la afirmación de tipo no fue feo, ya que el pánico por falla es multa. Ya estoy satisfecho con las instalaciones de programación genéricas de Go para mi necesidad.

Pero sync.Map es un caso de uso diferente. Hay una necesidad en la biblioteca estándar de una implementación de mapas sincronizados genéricos maduros más allá de una estructura con un mapa y mutex. Para el manejo de tipos, podemos envolverlo con otro tipo que establezca un tipo que no sea de interfaz{} y haga una aserción de tipo, o podemos agregar una verificación de reflejo internamente para que los elementos que siguen al primero deben coincidir con el mismo tipo. Ambos tienen verificaciones de tiempo de ejecución, el ajuste requiere reescribir cada método para cada tipo de uso, pero agrega una verificación de tipo de tiempo de compilación para la entrada y oculta la afirmación de tipo de salida, y con la verificación interna todavía tenemos que hacer una afirmación de tipo de salida de todos modos. De cualquier manera, estamos haciendo conversiones de interfaz sin ningún uso real de las interfaces; interface{} es un truco del lenguaje y no será claro para los nuevos programadores de Go. Aunque json.Marshal es un buen diseño en mi opinión (incluidas las etiquetas de estructura feas pero sensatas).

Agregaré que, dado que sync.Map está en la biblioteca estándar, idealmente debería cambiar la implementación para los casos de uso medidos donde la estructura simple es más eficaz. El mapa no sincronizado es un error temprano común en la programación concurrente de Go y una solución de biblioteca estándar debería funcionar.

El mapa normal solo tiene una verificación de tipo en tiempo de compilación y no requiere ninguno de estos andamios. Argumento que sync.Map debería ser el mismo o no debería estar en la biblioteca estándar para Go 2.

Propuse agregar sync.Map a la lista de tipos incorporados y hacer lo mismo para futuras necesidades similares. Pero entiendo que darles a los programadores de Go una manera de hacer esto sin tener que trabajar en el compilador y pasar por el guantelete de aceptación de código abierto es la idea detrás de esta discusión. En mi opinión, arreglar sync.Map es un caso real que define parcialmente lo que debería ser esta propuesta genérica.

Si agrega sync.Map como una función integrada, ¿hasta dónde llegará? ¿Eres un caso especial de cada contenedor?
sync.Map no es el único contenedor y algunos son mejores para algunos casos que para otros.

@Azareal : @chowey enumeró estos en agosto:

Finalmente, la biblioteca estándar está plagada de alternativas de interfaz{} donde los genéricos funcionarían de manera más segura:

• montón.Interfaz (https://golang.org/pkg/container/heap/#Interface)
• lista.Elemento (https://golang.org/pkg/container/list/#Element)
• anillo.Anillo (https://golang.org/pkg/container/ring/#Ring)
• grupo de sincronización (https://golang.org/pkg/sync/#Pool)
• próximo mapa de sincronización (https://tip.golang.org/pkg/sync/#Map)
• valor.atómico (https://golang.org/pkg/sync/atomic/#Value)

Puede haber otros que me faltan. El punto es que cada uno de los anteriores es donde esperaría que los genéricos fueran útiles.

Y me gustaría el conjunto desordenado de tipos cuya igualdad se puede comparar.

Me gustaría poner mucho trabajo en una implementación variable en el tiempo de ejecución para cada tipo en función de la evaluación comparativa, de modo que la mejor implementación posible sea la que se use.

Me pregunto si hay implementaciones alternativas razonables con Go 1 que logren el mismo objetivo para estos tipos de biblioteca estándar sin interfaz{} y sin genéricos.

Las interfaces golang y las clases de tipo haskell superan dos cosas (¡que son geniales!):

1.) (Restricción de tipo) Agrupan diferentes tipos con una etiqueta, el nombre de la interfaz
2.) (Despacho) Ofrecen enviar de manera diferente en cada tipo para un conjunto determinado de funciones a través de la implementación de la interfaz

Pero,

1.) A veces solo desea grupos anónimos como un grupo de int, float64 y string. ¿Cómo debería nombrar una interfaz de este tipo, NumericandString?

2.) Muy a menudo, no desea enviar de manera diferente para cada tipo de interfaz, sino proporcionar solo un método para todos los tipos de interfaz enumerados (Tal vez sea posible con los métodos predeterminados de las interfaces )

3.) Muy a menudo, no desea enumerar todos los tipos posibles para un grupo. En su lugar, sigue el camino perezoso y dice que quiero que todos los tipos T implementen alguna Interfaz A y el compilador, luego busque todos los tipos en todos los archivos fuente que edite y en todas las bibliotecas que use para generar las funciones apropiadas en tiempo de compilación.

Aunque el último punto es posible en el polimorfismo de la interfaz, tiene el inconveniente de ser un polimorfismo en tiempo de ejecución que involucra conversiones y cómo restringe la entrada de parámetros de una función para que contenga tipos que implementen más de una interfaz o una de muchas interfaces. El camino a seguir es introducir nuevas interfaces que extiendan otras interfaces (mediante el anidamiento de interfaces) para lograr algo similar pero no con las mejores prácticas.

Por cierto.
Admito a los que dicen que go ya tiene polimorfismo y por lo tanto go ya no es un lenguaje simple como C. Es un lenguaje de programación de sistemas de alto nivel. Entonces, ¿por qué no expandir las ofertas de go de polimorfismo?

Aquí hay una biblioteca que comencé hoy para tipos genéricos de conjuntos desordenados: https://github.com/pciet/unordered

Esto brinda documentación y ejemplos de prueba que escriben el patrón de envoltura (gracias @pierrre) para la seguridad de tipo en tiempo de compilación y también tiene la verificación de reflexión para la seguridad de tipo en tiempo de ejecución.

¿Qué necesidades hay de genéricos? Mi actitud negativa hacia los tipos genéricos de la biblioteca estándar anteriormente se centraba en el uso de la interfaz{}; mi queja podría resolverse con un tipo específico de paquete para la interfaz{} (como type Item interface{} en pciet/unordered) que documenta las restricciones inexpresables previstas.

No veo la necesidad de una función de idioma adicional cuando solo la documentación podría llevarnos allí ahora. Ya hay una gran cantidad de código probado en batalla en la biblioteca estándar que proporciona funciones genéricas (consulte https://github.com/golang/go/issues/23077).

Su tipo de código verifica en tiempo de ejecución (y desde esa perspectiva, no es mejor que solo interface{} si no es peor). Con los genéricos, podría haber tenido los tipos de colección con comprobaciones de tipo en tiempo de compilación.

Las verificaciones en tiempo de ejecución de @zerkms se pueden desactivar configurando afirmar = falso (esto no iría en la biblioteca estándar), hay un patrón de uso para las verificaciones en tiempo de compilación y, de todos modos, una verificación de tipo solo mira la estructura de la interfaz (usando interfaz agrega más gastos que la verificación de tipos). Si la interfaz no funciona, tendrá que escribir su propio tipo.

Está diciendo que el código genérico de rendimiento maximizado es una necesidad clave. No ha sido para mis casos de uso, pero tal vez la biblioteca estándar podría volverse más rápida, y tal vez otros necesiten algo así.

las comprobaciones en tiempo de ejecución se pueden desactivar configurando afirmar = falso

entonces nada garantiza la corrección

Está diciendo que el código genérico de rendimiento maximizado es una necesidad clave.

Yo no dije eso. Escriba la seguridad sería una gran oferta. Su solución aún está interface{} -infectada.

pero tal vez la biblioteca estándar podría volverse más rápida, y tal vez otros necesiten algo así.

puede ser, si el equipo de desarrollo central está feliz de implementar lo que necesite a pedido y rápidamente.

@pciet

No veo la necesidad de una función de idioma adicional cuando solo la documentación podría llevarnos allí ahora.

Usted dice esto, pero no tiene ningún problema al usar las características genéricas del lenguaje en forma de cortes y la función de creación.

No veo la necesidad de una función de idioma adicional cuando solo la documentación podría llevarnos allí ahora.

Entonces, ¿por qué molestarse en usar un lenguaje escrito estáticamente? Puede usar un lenguaje de escritura dinámica como Python y confiar en la documentación para asegurarse de que se envíen los tipos de datos correctos a su API.

Creo que una de las ventajas de Go son las facilidades para hacer cumplir algunas restricciones por parte del compilador para evitar futuros errores. Esas instalaciones se pueden ampliar (con compatibilidad con genéricos) para aplicar otras restricciones a fin de evitar más errores en el futuro.

Usted dice esto, pero no tiene ningún problema al usar las características genéricas del lenguaje en forma de cortes y la función de creación.

Estoy diciendo que las características existentes nos llevan a un buen punto de equilibrio que tiene soluciones de programación genéricas y debería haber fuertes razones reales para cambiar el sistema de tipo Go 1. No se trata de cómo un cambio mejoraría el idioma, sino de los problemas que enfrentan las personas ahora, como mantener una gran cantidad de cambios de tipo en tiempo de ejecución para la interfaz{} en los paquetes de biblioteca estándar de base de datos y fmt, que se solucionarían.

Entonces, ¿por qué molestarse en usar un lenguaje escrito estáticamente? Puede usar un lenguaje de escritura dinámica como Python y confiar en la documentación para asegurarse de que se envíen los tipos de datos correctos a su API.

He escuchado sugerencias para escribir sistemas en Python en lugar de lenguajes y organizaciones tipificados estáticamente.

La mayoría de los programadores de Go que usan la biblioteca estándar usan tipos que no se pueden describir completamente sin documentación o sin mirar la implementación. Los tipos con subtipos paramétricos o tipos generales con restricciones aplicadas solo corrigen un subconjunto de estos casos mediante programación y generarían una gran cantidad de trabajo ya realizado en la biblioteca estándar.

En la propuesta para los tipos de suma, sugerí una función de compilación para el cambio de tipo de interfaz donde un uso de interfaz en una función o método tiene un error de compilación emitido cuando un posible valor asignado a la interfaz no coincide con ningún caso de cambio de tipo de interfaz contenido.

Una función/método que toma una interfaz podría rechazar algunos tipos en la compilación al no tener un caso predeterminado ni un caso para el tipo. Esto parece una adición de programación genérica razonable si la característica es factible de implementar.

Si las interfaces de Go pudieran capturar el tipo de implementador, podría haber una forma de genéricos que sea completamente compatible con la sintaxis de Go actual: una forma de genéricos de un solo parámetro ( demostración ).

@dc0d para tipos de contenedores genéricos, creo que la característica agrega verificación de tipo en tiempo de compilación sin requerir un tipo de contenedor: https://gist.github.com/pciet/36a9dcbe99f6fb71f5fc2d3c455971e5

@pciet Tienes razón. En el código proporcionado, No. 4, la muestra indica que el tipo se captura para segmentos y canales (y matrices). Pero no para los mapas, porque solo hay uno y solo un tipo de parámetro: el implementador. Y dado que un mapa necesita dos parámetros de tipo, se necesitan interfaces de envoltura.

Por cierto, tengo que enfatizar el propósito demostrativo de ese código, como una línea de pensamiento. No soy un diseñador de idiomas. Esta es solo una forma hipotética de pensar sobre la implementación de genéricos en Go:

  • Compatible con Go actual
  • Simple (parámetro de tipo genérico único, que se _siente_ como _esto_ en otro OO, en referencia al implementador actual)

La discusión de la genericidad y todos los casos de uso posibles en el contexto de un deseo de minimizar los impactos mientras se maximizan los casos de uso importantes y la flexibilidad de expresión es un análisis muy complejo. No estoy seguro de si alguno de nosotros será capaz de destilarlo en un conjunto corto de principios, también conocido como esencia generativa. Lo estoy intentando. De todos modos, aquí algunos de mis pensamientos iniciales de mi lectura _superficial_ de este hilo...

@adg escribió:

Acompañando a este número hay una propuesta general de genéricos de @ianlancetaylor que incluye cuatro propuestas defectuosas específicas de mecanismos de programación genéricos para Go.

Afaics, la sección vinculada extraída de la siguiente manera no establece un caso de falta de genericidad con las interfaces actuales, _ "No hay forma de escribir un método que tome una interfaz para el tipo T proporcionado por la persona que llama, para cualquier T, y devuelve un valor de el mismo tipo T.”_.

No hay forma de escribir una interfaz con un método que toma un argumento de tipo T, para cualquier T, y devuelve un valor del mismo tipo.

Entonces, ¿de qué otra manera podría el código en el tipo de sitio de llamada verificar que tiene un tipo T como valor de resultado? Por ejemplo, dicha interfaz puede tener un método de fábrica para construir el tipo T. Es por eso que necesitamos parametrizar las interfaces en el tipo T.

Las interfaces no son simplemente tipos; también son valores. No hay forma de usar tipos de interfaz sin usar valores de interfaz, y los valores de interfaz no siempre son eficientes.

Estuvo de acuerdo en que, dado que las interfaces actualmente no se pueden parametrizar explícitamente en el tipo T en el que operan, el tipo T no es accesible para el programador.

Entonces, esto es lo que hacen los límites de la clase de tipo en el sitio de definición de la función tomando como entrada un tipo T y teniendo una cláusula where o requires que indica la(s) interfaz(es) que se requieren para el tipo T. En muchas circunstancias estos diccionarios de interfaz se pueden monomorfizar automáticamente en tiempo de compilación para que no se pasen punteros de diccionario (para las interfaces) a la función en tiempo de ejecución (¿monomorfización que supongo que el compilador Go aplica a las interfaces actualmente?). Por 'valores' en la cita anterior, supongo que se refiere al tipo de entrada T y no al diccionario de métodos para el tipo de interfaz implementado por el tipo T.

Si luego permitimos parámetros de tipo en tipos de datos (por ejemplo struct ), entonces dicho tipo T anterior puede parametrizarse en sí mismo, por lo que realmente tenemos un tipo T<U> . Las fábricas para tales tipos que necesitan retener el conocimiento de U se denominan tipos de tipo superior (HKT) .

Los genéricos permiten contenedores polimórficos con seguridad de tipos.

Véase también el tema de los contenedores _heterogéneos_ discutido más adelante. Entonces, por polimórfico nos referimos a la genericidad del tipo de valor del contenedor (por ejemplo, tipo de elemento de la colección), pero también está la cuestión de si podemos poner más de un tipo de valor en el contenedor simultáneamente, haciéndolos heterogéneos.


@tamird escribió:

Estos requisitos parecen excluir, por ejemplo, un sistema similar al sistema de rasgos de Rust, donde los tipos genéricos están restringidos por límites de rasgos.

Los límites de rasgos de Rust son esencialmente límites de clase de tipos.

@alex escribió:

Rasgos de Rust. Si bien creo que son un buen modelo en general, serían una mala opción para Go tal como existe hoy.

¿Por qué crees que encajan mal? ¿Quizás está pensando en los objetos de rasgos que emplean el despacho en tiempo de ejecución, por lo que tienen menos rendimiento que el monomorfismo? Pero esos pueden ser considerados por separado del principio de genericidad de los límites de la clase de tipo (ver mi discusión de contenedores/colecciones heterogéneas a continuación). Afaics, las interfaces de Go ya son límites parecidos a rasgos y logran el objetivo de las clases de tipos, que es vincular en forma tardía los diccionarios a los tipos de datos en el sitio de la llamada, en lugar del antipatrón de programación orientada a objetos que se vincula en forma temprana (incluso si todavía está en compilación). tiempo) diccionarios a tipos de datos (en instanciación/construcción). Las clases de tipos pueden (al menos una mejora parcial de los grados de libertad) resolver el Problema de expresión que OOP no puede.

@jimmyfrasche escribió:

  • https://golang.org/doc/faq#covariant_types

Estoy de acuerdo con el enlace anterior en que las clases de tipos no están subtipificando y no expresan ninguna relación de herencia. Y esté de acuerdo con no confundir innecesariamente la "genericidad" (como un concepto más general de reutilización o modularidad que el polimorfismo paramétrico) con la herencia como lo hace la subclasificación.

Sin embargo, también quiero señalar que las jerarquías de herencia (también conocidas como subtipos) son inevitables 1 en la asignación a (entradas de funciones) y desde (salidas de funciones) si el lenguaje admite uniones e intersecciones, porque por ejemplo int ν string puede aceptar una asignación de un int o un string pero tampoco puede aceptar una asignación de un int ν string . Sin uniones afaik, las únicas formas alternativas de proporcionar contenedores/colecciones heterogéneos tipificados estáticamente son la subclasificación o el polimorfismo delimitado existencialmente (también conocido como objetos de rasgos en Rust y cuantificación existencial en Haskell). Los enlaces anteriores contienen una discusión sobre las compensaciones entre existenciales y sindicatos. Afaik, la única forma de hacer contenedores/colecciones heterogéneos en Go ahora es subsumir todos los tipos en un interface{} vacío que está desechando la información de tipeo y supongo que requiere conversión e inspección de tipos en tiempo de ejecución, que tipo de 2 derrota el punto de tipeo estático.

El "antipatrón" a evitar es la subclasificación , también conocida como herencia virtual (ver también "EDIT#2" sobre los problemas con la subsunción implícita y la igualdad, etc.).

1 Independientemente de si coinciden estructural o nominalmente porque la subtipificación se debe al principio de sustitución de Liskov basado en conjuntos comparativos y la dirección de asignación con entradas de funciones opuestas a los valores devueltos, por ejemplo, un parámetro de tipo de struct o interface no puede residir tanto en las entradas de la función como en los valores devueltos a menos que sea invariable en lugar de covariante o contravariante.

2 El absolutismo no se aplicará porque no podemos verificar el tipo de no determinismo ilimitado del universo. Por lo que entiendo, este hilo se trata de elegir un límite óptimo ("punto dulce") al nivel de indicar escribir wrt para los problemas de genericidad.

@andrewcmyers escribió:

A diferencia de los genéricos de Java y C#, el mecanismo de genéricos de Genus no se basa en la creación de subtipos.

Es la herencia y la subclasificación ( no la subtipificación estructural ) el peor antipatrón que no desea copiar de Java, Scala, Ceylon y C++ (sin relación con los problemas con las plantillas de C++ ).

@thwd escribió:

El exponente de la medida de complejidad de los tipos parametrizados es la varianza. Los tipos de Go (exceptuando las interfaces) son invariantes y esto puede y debe ser la regla.

La subtipificación con inmutabilidad evita la complejidad de la covarianza. La inmutabilidad también mejora algunos de los problemas con la subclasificación (p. ej Rectangle frente a Square ), pero no otros (p. ej., subsunción implícita, igualdad, etc.).

@bxqgit escribió:

La sintaxis simple y el sistema de tipos son las ventajas importantes de Go. Si agrega genéricos, el lenguaje se convertirá en un desastre feo como Scala o Haskell.

Tenga en cuenta que Scala intenta fusionar OOP, subclases, FP, módulos genéricos, HKT y clases de tipos (a través implicit ) en un solo PL. Quizás las clases de tipos por sí solas podrían ser suficientes.

Haskell no es necesariamente obtuso debido a los genéricos de clase de tipo, sino más probablemente porque está imponiendo funciones puras en todas partes y empleando la teoría de categorías monádicas para modelar efectos imperativos controlados.

Por lo tanto, creo que no es correcto asociar la torpeza y la complejidad de esos PL con clases de tipos, por ejemplo, en Rust. Y no culpemos a las clases de tipos por las vidas de Rust + abstracción de préstamo de mutabilidad exclusiva.

Afaics, en la sección Semántica de _Type Parameters in Go_, el problema encontrado por @ianlancetaylor es un problema de conceptualización porque aparentemente ( afaics ) está reinventando clases de tipos sin darse cuenta:

¿Podemos fusionar SortableSlice y PSortableSlice para tener lo mejor de ambos mundos? No exactamente; no hay forma de escribir una función parametrizada que admita un tipo con un método Less o un tipo incorporado. El problema es que SortableSlice.Less no se puede instanciar para un tipo sin un método Less , y no hay forma de instanciar un método solo para algunos tipos pero no para otros.

La cláusula requires Less[T] para el límite de la clase de tipos (incluso si el compilador la infiere implícitamente) en el método Less para []T está en T no []T . La implementación de la clase de tipos Less[T] (que contiene un método Less ) para cada T proporcionará una implementación en el cuerpo de la función del método o asignará el < función incorporada como implementación. Sin embargo, creo que esto requiere HKT U[T] si los métodos de Sortable[U] necesitan un parámetro de tipo U que represente el tipo de implementación, por ejemplo, []T . Afair @keean tiene otra forma de estructurar una ordenación empleando una clase de tipos separada para el tipo de valor T que no requiere un HKT.

Tenga en cuenta que esos métodos para []T podrían estar implementando una clase de tipo Sortable[U] , donde U es []T .

(Aparte de lo técnico: puede parecer que podríamos fusionar SortableSlice y PSortableSlice al tener algún mecanismo para instanciar un método solo para algunos argumentos de tipo pero no para otros. Sin embargo, el resultado sería sacrificar la compilación -Tipo de seguridad en el tiempo, ya que usar el tipo incorrecto provocaría un pánico en el tiempo de ejecución. En Go, ya se pueden usar tipos y métodos de interfaz y aserciones de tipo para seleccionar el comportamiento en el tiempo de ejecución. No hay necesidad de proporcionar otra forma de hacerlo usando parámetros de tipo. .)

La selección de la clase de tipo enlazada en el sitio de llamada se resuelve en tiempo de compilación para un T estáticamente conocido. Si se necesita un envío dinámico heterogéneo, consulte las opciones que expliqué en mi publicación anterior.

Espero que @keean pueda encontrar tiempo para venir aquí y ayudar a explicar las clases de tipos, ya que es más experto y me ayudó a aprender estos conceptos. Puedo tener algunos errores en mi explicación.

Nota de PD para aquellos que ya leyeron mi publicación anterior, tenga en cuenta que la edité extensamente unas 10 horas después de publicarla (después de dormir un poco) para que los puntos sobre los contenedores heterogéneos sean más coherentes.


La sección Ciclos parece ser incorrecta. La construcción en tiempo de ejecución de la instancia S[T]{e} de un struct no tiene nada que ver con la selección de la implementación de la función genérica llamada. Presumiblemente, está pensando que el compilador no sabe si está especializando la implementación de la función genérica para el tipo de argumentos, pero todos esos tipos se conocen en tiempo de compilación.

Tal vez la especificación de la sección Verificación de tipos podría simplificarse estudiando el concepto de @keean de un gráfico conectado de distintos tipos como nodos para un algoritmo de unificación. Cualquier tipo distinto conectado por un borde debe tener tipos congruentes, con bordes creados para cualquier tipo que se conecte mediante asignación o de otra manera en el código fuente. Si hay unión e intersección (de mi publicación anterior), entonces se debe tener en cuenta la dirección de asignación (¿de alguna manera? ). Cada tipo desconocido distinto comienza con un límite superior mínimo (LUB) de Top y un límite inferior máximo (GLB) de Bottom y luego las restricciones pueden alterar estos límites. Los tipos conectados deben tener límites compatibles. Todas las restricciones deben ser límites de clase de tipo.

En Implementación :

Por ejemplo, siempre es posible implementar funciones parametrizadas generando una nueva copia de la función para cada instanciación, donde la nueva función se crea reemplazando los parámetros de tipo con los argumentos de tipo.

Creo que el término técnico correcto es monomorfización .

Este enfoque produciría el tiempo de ejecución más eficiente a costa de un tiempo de compilación adicional considerable y un mayor tamaño de código. Es probable que sea una buena opción para las funciones parametrizadas que son lo suficientemente pequeñas para estar en línea, pero sería una mala compensación en la mayoría de los demás casos.

La creación de perfiles le diría al programador qué funciones pueden beneficiarse más de la monomorfización. ¿Quizás el optimizador Java Hotspot optimiza la monomorfización en tiempo de ejecución?

@egonelbre escribió:

Hay un resumen de las discusiones de Go Generics , que intenta brindar una descripción general de las discusiones de diferentes lugares.

La sección Información general parece implicar que el uso universal de Java de referencias de boxeo para instancias en un contenedor es el único eje de diseño que se opone diametralmente a la monomorfización de plantillas de C++. Pero los límites de clase de tipo (que también se pueden implementar con plantillas de C++ pero siempre monomorfizados) se aplican a funciones, no a parámetros de tipo de contenedor. Por lo tanto, a la descripción general le falta el eje de diseño para las clases de tipos en el que podemos elegir si monomorfizar cada función limitada de la clase de tipos. Con las clases de tipos, siempre hacemos que los programadores sean más rápidos (menos repetitivos) y podemos obtener un equilibrio más refinado entre hacer compiladores/ejecución más rápida/lenta y mayor/menor aumento del código. Según mi publicación anterior, quizás lo óptimo sería si la elección de funciones para monomorfizar fuera impulsada por el perfilador (automáticamente o más probablemente por anotación).

En la sección Problemas: Estructuras de datos genéricas :

Contras

  • Las estructuras genéricas tienden a acumular características de todos los usos, lo que resulta en mayores tiempos de compilación o código inflado o en la necesidad de un enlazador más inteligente.

Para las clases de tipos, esto no es cierto o representa un problema menor, porque las interfaces solo deben implementarse para los tipos de datos que se suministran a las funciones que usan esas interfaces. Las clases de tipos tienen que ver con el enlace tardío de la implementación a la interfaz, a diferencia de OOP, que une cada tipo de datos a sus métodos para la implementación de class .

Además, no es necesario poner todos los métodos en una sola interfaz. La cláusula requires (incluso si el compilador la infiere implícitamente) en un límite de clase de tipo para una declaración de función puede mezclar y combinar las interfaces requeridas.

  • Las estructuras genéricas y las API que operan en ellas tienden a ser más abstractas que las API especialmente diseñadas, lo que puede imponer una carga cognitiva a las personas que llaman.

Un contraargumento que creo que mejora significativamente esta preocupación es que la carga cognitiva de aprender un número ilimitado de reimplementaciones de casos especiales de los mismos algoritmos genéricos esencialmente es ilimitada. Mientras que el aprendizaje de las API genéricas abstractas está limitado.

  • Las optimizaciones en profundidad son muy no genéricas y específicas del contexto, por lo que es más difícil optimizarlas en un algoritmo genérico.

Esto no es una estafa válida. La regla 80/20 dice que no agregue complejidad ilimitada (por ejemplo, optimización prematura) para el código que, cuando se perfila, no lo requiere. El programador es libre de optimizar en el 20 % de los casos, mientras que el 80 % restante lo maneja la complejidad limitada y la carga cognitiva de las API genéricas.

A lo que realmente nos referimos aquí es a la regularidad de un idioma y las API genéricas ayudan, no dañan eso. Esos contras realmente no están correctamente conceptualizados.

Soluciones alternativas:

  • usar estructuras más simples en lugar de estructuras complicadas

    • por ejemplo, use map[int]struct{} en lugar de Set

Rob Pike (y también lo vi señalar ese punto en el video) parece no entender que los contenedores genéricos no son suficientes para crear funciones genéricas. Necesitamos ese T en map[T] para que podamos pasar el tipo de datos genérico en funciones para entradas, salidas y para nuestro propio struct . Los genéricos solo en parámetros de tipo de contenedor son totalmente insuficientes para expresar API genéricas y las API genéricas son necesarias para una complejidad limitada y una carga cognitiva y para obtener regularidad en un ecosistema lingüístico. Además, no he visto el mayor nivel de refactorización (por lo tanto, la componibilidad reducida de los módulos que no se pueden refactorizar fácilmente) que requiere el código no genérico, que es de lo que se trata el problema de expresión que mencioné en mi primera publicación.

En la sección Enfoques genéricos :

Plantillas de paquetes
Este es un enfoque utilizado por Modula-3, OCaml, SML (los llamados "funtores") y Ada. En lugar de especificar un tipo individual para la especialización, todo el paquete es genérico. El paquete se especializa fijando los parámetros de tipo al importar.

Puedo estar equivocado, pero esto no parece del todo correcto. Los funtores ML (que no deben confundirse con los funtores FP) también pueden devolver una salida que permanece parametrizada. De lo contrario, no habría forma de usar los algoritmos dentro de otras funciones genéricas, por lo que los módulos genéricos no podrían reutilizarse (importando con tipos concretos en) otros módulos genéricos. Esto parece ser un intento de simplificar demasiado y luego perder por completo el punto de los genéricos, la reutilización de módulos, etc.

Más bien, tengo entendido que la parametrización del tipo de paquete (también conocido como módulo) permite la capacidad de aplicar parámetros de tipo a una agrupación de struct , interface y func .

Sistema de tipos más complicado
Este es el enfoque que adoptan Haskell y Rust.
[…]
Contras:

  • difícil de encajar en un lenguaje más simple (https://groups.google.com/d/msg/golang-nuts/smT_0BhHfBs/MWwGlB-n40kJ)

Citando a @ianlancetaylor en el documento vinculado:

Si crees eso, entonces vale la pena señalar que el núcleo de la
El código de mapa y división en el tiempo de ejecución de Go no es genérico en el sentido de
usando polimorfismo de tipo. Es genérico en el sentido de que mira
escriba la información de reflexión para ver cómo mover y comparar el tipo
valores. Entonces tenemos prueba por existencia de que es aceptable escribir
código "genérico" en Go escribiendo código no polimórfico que usa tipo
información de reflexión de manera eficiente, y luego envolver ese código en
repetitivo de tipo seguro en tiempo de compilación (en el caso de mapas y sectores
esta placa de caldera es, por supuesto, proporcionada por el compilador).

Y eso es lo que un compilador transpilando de un superconjunto de Go con genéricos agregados generaría como código Go. Pero el envoltorio no se basaría en alguna delimitación como el paquete, ya que carecería de la componibilidad que ya mencioné. El punto es que no hay un atajo para un buen sistema de tipos de genéricos componibles. O lo hacemos correctamente o no hacemos nada, porque agregar algún truco no componible que no sea realmente genérico creará eventualmente una inercia de retazos de genericidad a medias e irregularidad de casos de esquina y soluciones alternativas que hacen que el código del ecosistema Go ininteligible

También es cierto que la mayoría de las personas que escriben programas Go grandes y complejos tienen
No se encontró una necesidad significativa de genéricos. Hasta ahora ha sido más como
una verruga irritante: la necesidad de escribir tres líneas de texto modelo para
cada tipo para ser clasificado, en lugar de una barrera importante para escribir útiles
código.

Sí, este ha sido uno de los pensamientos en mi mente sobre si es justificable ir a un sistema de clase de tipo completo. Si todas sus bibliotecas se basan en él, aparentemente podría ser una armonía hermosa, pero si estamos contemplando la inercia de los trucos de Go existentes para la genericidad, entonces tal vez la sinergia adicional obtenida será baja para muchos proyectos. ?

Pero si un transpilador de una sintaxis de clase de tipos emuló la forma manual existente en que Go puede modelar genéricos (Editar: lo que acabo de leer que @andrewcmyers afirma que es plausible ), esto podría ser menos oneroso y encontrar sinergias útiles. Por ejemplo, me di cuenta de que se pueden emular dos tipos de clases de parámetros con interface implementado en un struct que emula una tupla, o @jba mencionó una idea para emplear interface en línea en contexto . Aparentemente struct se escriben estructuralmente en lugar de nominalmente, a menos que se les dé un nombre con type . También confirmé un método de un interface puede ingresar otro interface por lo que es posible transpilar desde HKT en su ejemplo de clasificación sobre el que escribí en mi publicación anterior aquí. Pero necesito pensar más en esto cuando no tengo tanto sueño.

Creo que es justo decir que a la mayoría del equipo de Go no le gusta C++
plantillas, en las que se ha superpuesto un lenguaje completo de Turing
otro lenguaje completo de Turing tal que los dos lenguajes tienen
sintaxis completamente diferentes, y los programas en ambos lenguajes son
escrito de formas muy diferentes. Las plantillas de C++ sirven como advertencia
cuento porque la implementación compleja ha impregnado todo el
biblioteca estándar, lo que provoca que los mensajes de error de C++ se conviertan en una fuente de
maravilla y asombro. Este no es un camino que Go seguirá alguna vez.

¡Dudo que alguien esté en desacuerdo! El beneficio de la monomorfización es ortogonal a las desventajas de un motor de metaprogramación de genéricos completos de Turing.

Por cierto, el error de diseño de las plantillas de C++ me parece ser la misma esencia generativa de la falla de los funtores ML generativos (en oposición a los aplicativos). Se aplica el principio de mínima potencia.


@ianlancetaylor escribió:

Es decepcionante ver que Go se vuelve cada vez más complejo al agregar tipos parametrizados integrados.

A pesar de la especulación en este tema, creo que es muy poco probable que esto suceda.

Eso espero. Creo firmemente que Go debería agregar un sistema de genéricos coherente o simplemente aceptar que nunca tendrá genéricos.

Creo que es más probable que ocurra una bifurcación a un transpiler, en parte porque tengo fondos para implementarlo y estoy interesado en hacerlo. Sin embargo, sigo analizando la situación.

Sin embargo, eso fracturaría el ecosistema, pero al menos Go puede permanecer puro en sus principios minimalistas. Por lo tanto, para evitar fracturar el ecosistema y permitir algunas otras innovaciones que me gustaría, probablemente no lo convertiría en un superconjunto y lo llamaría Cero en su lugar.

@pciet escribió:

Mi voto es no a los genéricos de aplicaciones generalizados, sí a funciones genéricas más integradas como append y copy que funcionan en múltiples tipos base. ¿Quizás se podrían agregar sort y search para los tipos de colección?

Expandir esta inercia tal vez evitará que una función genérica integral llegue a Go. Es probable que aquellos que querían genéricos se vayan a pastos más verdes. @andrewcmyers reiteró esto:

~Es~ sería decepcionante ver que Go se vuelve cada vez más complejo al agregar tipos parametrizados integrados. Sería mejor simplemente agregar el soporte de lenguaje para que los programadores escriban sus propios tipos parametrizados.

@shelby3

Afaik, la única forma de hacer contenedores/colecciones heterogéneas en Go ahora es subsumir todos los tipos en una interfaz vacía{} que está desechando la información de tipeo y supongo que requiere conversión e inspección de tipos en tiempo de ejecución, que de alguna manera2 anula el punto de tipeo estático.

Consulte el patrón de envoltorio en los comentarios anteriores para verificar el tipo estático de las colecciones de interfaz{} en Go.

El punto es que no hay un atajo para un buen sistema de tipos de genéricos componibles. O lo hacemos correctamente o no hacemos nada, porque agregando algún truco no componible que no es realmente genérico...

¿Puedes explicar esto más? Para el caso de tipos de colección, tener una interfaz que defina el comportamiento genérico necesario de los elementos contenidos parece razonable para escribir funciones.

@pciet este código está literalmente haciendo exactamente lo que @shelby3 estaba describiendo y considerando un antipatrón. Citándote de antes:

Esto brinda documentación y ejemplos de prueba que escriben el patrón de envoltura (gracias @pierrre) para la seguridad de tipo en tiempo de compilación y también tiene la verificación de reflexión para la seguridad de tipo en tiempo de ejecución.

Está tomando código que carece de información de tipo y, tipo por tipo, agregando conversiones e inspección de tipos en tiempo de ejecución mediante reflect. Esto es exactamente de lo que se quejaba @shelby3 . Tiendo a llamar a este enfoque "monomorfización a mano" y es exactamente el tipo de tarea tediosa que creo que es mejor para un compilador.

Este enfoque tiene una serie de desventajas:

  • Requiere envoltorios tipo por tipo, mantenidos a mano o con una herramienta similar a go generate
  • (Si se hace a mano en lugar de una herramienta) oportunidad de cometer errores en el texto modelo que no se detectarán hasta el tiempo de ejecución
  • Requiere envío dinámico en lugar de envío estático, que es más lento y usa más memoria
  • Utiliza la reflexión en tiempo de ejecución en lugar de aserciones de tipo en tiempo de compilación, que también es lenta
  • No componible: actúa completamente en tipos concretos sin oportunidades de usar límites tipo typeclass (o incluso tipo interfaz) en los tipos, a menos que coloque manualmente otra capa de direccionamiento indirecto para cada interfaz no vacía sobre la que también desee abstraerse.

¿Puedes explicar esto más? Para el caso de tipos de colección, tener una interfaz que defina el comportamiento genérico necesario de los elementos contenidos parece razonable para escribir funciones.

Ahora, en cualquier lugar donde desee utilizar un límite en lugar de un tipo concreto o además de él, también debe escribir el mismo modelo de verificación de tipo para cada tipo de interfaz. Simplemente agrava aún más la explosión (quizás combinatoria) de envoltorios de tipo estático que tiene que escribir.

También hay ideas que, hasta donde yo sé, simplemente no se pueden expresar en el sistema de tipos de Go en la actualidad, como un límite en una combinación de interfaces. Imagina que tenemos:

type Foo interface {
    ...
}

type Bar interface {
    ...
}

¿Cómo expresamos, utilizando una verificación de tipo puramente estática, que queremos un tipo que implemente tanto Foo como Bar ? Por lo que sé, esto no es posible en Go (salvo que se recurra a comprobaciones de tiempo de ejecución que pueden fallar, evitando la seguridad de tipo estático).

Con un sistema de genéricos basado en clases de tipos, podríamos expresar esto como:

func baz<T Foo + Bar>(t T) {
    ...
}

@tarcieri

¿Cómo expresamos, utilizando una verificación de tipo puramente estática, que queremos un tipo que implemente tanto Foo como Bar?

simplemente así:

type T interface {
    Foo
    Bar
}

func baz(t T) { ... }

@sbinet limpio, TIL

Personalmente, considero que la reflexión en tiempo de ejecución es una característica incorrecta, pero solo soy yo ... Puedo explicar por qué si alguien está interesado.

Creo que cualquiera que implemente genéricos de cualquier tipo debería leer "Elementos de programación" de Stepanov varias veces primero. Evitaría muchos problemas de Not Invented Here y reinventar la rueda. Después de leer eso, debería quedar claro por qué "C ++ Concepts" y "Haskell Typeclasses" son la forma correcta de hacer genéricos.

Veo que este problema parece estar activo de nuevo
Aquí hay un patio de juegos de propuesta de hombre de paja
https://go-li.github.io/test.html
simplemente pegue los programas de demostración desde aquí
https://github.com/go-li/demo

Muchas gracias por su evaluación de este solo parametrizado.
Funciones genéricas.

Mantenemos el gccgo hackeado y
este proyecto sería imposible sin ti, así que
quería contribuir de nuevo.

También esperamos cualquier genérico que adopte, ¡siga con el gran trabajo!

@anlhord , ¿dónde están los detalles de implementación sobre esto? ¿Dónde se puede leer sobre la sintaxis? ¿Qué se implementa? ¿Qué no está implementado? ¿Cuáles son las especificaciones para estas implementaciones? ¿Cuáles son los pros y los contras de ello?

El enlace del patio de recreo contiene el peor ejemplo posible de esto:

package main

import "fmt"

func main() {
    fmt.Println("Hello, playground")
}

Ese código no me dice nada sobre cómo usarlo y qué puedo probar.

Si pudiera mejorar esas cosas, ayudaría a comprender mejor cuál es su propuesta y cómo se compara con las anteriores / ver cómo se aplican o no los otros puntos planteados aquí.

Espero que esto te ayude a entender los problemas con tu comentario.

@joho escribió:

¿Sería útil consultar la literatura académica para obtener alguna orientación sobre la evaluación de enfoques?

El único documento que he leído sobre el tema es ¿Se benefician los desarrolladores de los tipos genéricos ? (paywall lo siento, puede buscar en Google su camino a una descarga de pdf) que decía lo siguiente

En consecuencia, una interpretación conservadora del experimento
es que los tipos genéricos pueden considerarse como una compensación
entre las características de la documentación positiva y la
características de extensibilidad negativa.

Supongo que OOP y subclases (por ejemplo, clases en Java y C ++) no se considerarán seriamente porque Go ya tiene una clase de tipo interface (sin el parámetro de tipo genérico T explícito), Java es citado como lo que no se debe copiar, y porque muchos han argumentado que son un anti-patrón. Upthread He vinculado a algunos de esos argumentos. Podríamos profundizar en ese análisis si alguien está interesado.

Todavía no he estudiado investigaciones más recientes, como el sistema Genus mencionado upthread . Desconfío de los sistemas de "fregadero de cocina" que intentan mezclar tantos paradigmas (por ejemplo, subclases, herencia múltiple, programación orientada a objetos, linealización de rasgos, implicit , clases de tipos, tipos abstractos, etc.), debido a las quejas sobre Scala teniendo tantos casos de esquina en la práctica, aunque tal vez eso mejore con Scala 3 (también conocido como Dotty y el cálculo DOT). Tengo curiosidad por saber si su tabla de comparación se compara con Scala 3 experimental o con la versión actual de Scala.

Entonces, lo que queda son los funtores de ML y las clases de tipos de Haskell en términos de sistemas de genericidad comprobados, que mejoran significativamente la extensibilidad y la flexibilidad en comparación con OOP+subclases.

Escribí parte de la discusión privada que @keean y yo tuvimos sobre los módulos funtores de ML versus las clases de tipos. Los aspectos más destacados parecen ser:

  • typeclasses _modelan un álgebra_ (pero sin axiomas comprobados ) e implementan cada tipo de datos para cada interfaz de una sola manera. Por lo tanto, permite la selección implícita de las implementaciones por parte del compilador sin anotación en el sitio de la llamada.

  • Los funtores aplicativos tienen transparencia referencial, mientras que los funtores generativos crean una nueva instancia en cada instanciación, lo que significa que no son invariantes en el orden de inicialización.

  • Los funtores de ML son más poderosos/flexibles que las clases de tipos, pero esto tiene el costo de más anotaciones y potencialmente más interacciones de casos de esquina. Y según @keean , requieren tipos dependientes (para tipos asociados ), que es un sistema de tipo más complejo. @keean cree que la _expresión de genericidad como un álgebra_ de Stepanov más las clases de tipos es lo suficientemente poderosa y flexible , por lo que parece ser el punto óptimo para la generidad de vanguardia y bien probada (en Haskell y ahora en Rust). Sin embargo, las clases de tipos no imponen los axiomas.

  • Sugerí agregar uniones para contenedores heterogéneos con clases de tipos para extender a lo largo de otro eje del problema de expresión, aunque esto requiere inmutabilidad o copia (solo para los casos en los que se emplea la extensibilidad heterogénea) que se sabe que tiene un O (log n) desaceleración en comparación con la imperatividad mutable sin restricciones.

@larsth escribió:

Podría ser interesante tener uno o más transpiladores experimentales: un código fuente genérico de Go para compilar el código fuente de Go 1.xy.

PD Dudo que Go adopte un sistema de escritura tan sofisticado, pero estoy contemplando un transpilador a la sintaxis de Go existente como mencioné en mi publicación anterior (ver la edición en la parte inferior). Y quiero un sistema genérico robusto junto con esas características Go tan deseables. Typeclass generics on Go parece ser lo que quiero.

@bcmills escribió sobre su propuesta sobre funciones de tiempo de compilación para genericidad:

Escuché que se usa el mismo argumento para recomendar la exportación de tipos interface en lugar de tipos concretos en las API de Go, y lo contrario resulta ser más común: la abstracción prematura restringe en exceso los tipos y dificulta la extensión de las API. (Para ver un ejemplo de este tipo, consulte #19584). Si desea confiar en esta línea de argumentación, creo que debe proporcionar algunos ejemplos concretos.

Ciertamente, es cierto que las abstracciones del sistema de tipos necesariamente renuncian a algunos grados de libertad y, a veces, hemos superado esas restricciones con "inseguro" (es decir, en violación de la abstracción controlada estáticamente), pero eso debe compensarse con los beneficios de desacoplamiento modular con invariantes anotados sucintamente.

Al diseñar un sistema genérico, es probable que queramos aumentar la regularidad y la previsibilidad del ecosistema como uno de los principales objetivos, especialmente si se tiene en cuenta la filosofía central de Go (por ejemplo, que los programadores promedio son una prioridad).

Se aplica el principio de mínima potencia. El poder/flexibilidad de las funciones de tiempo de compilación "ocultas en" invariantes para la genericidad debe sopesarse frente a su capacidad para dañar, por ejemplo, la legibilidad del código fuente en el ecosistema (donde el desacoplamiento modular es extremadamente importante porque el lector no ¡No tiene que leer una cantidad potencialmente ilimitada de código debido a dependencias transitivas implícitas, para comprender un módulo/paquete dado!). La resolución implícita de instancias de implementación de clases de tipos tiene este problema si no se cumple su álgebra .

Claro, pero eso ya es cierto para muchas restricciones implícitas en Go, independientemente de cualquier mecanismo de programación genérico.

Por ejemplo, una función puede recibir un parámetro de tipo interfaz e inicialmente llamar a sus métodos secuencialmente. Si esa función cambia posteriormente para llamar a esos métodos al mismo tiempo (al generar gorrutinas adicionales), la restricción "debe ser seguro para el uso concurrente" no se refleja en el sistema de tipos.

Pero afaik Go no intentó diseñar una abstracción para modularizar esos efectos. Rust tiene tal abstracción (que, por cierto, creo que es demasiado pita/tsuris/limitante para algunos/la mayoría de los casos de uso y abogo por una abstracción de modelo de un solo subproceso más fácil, pero lamentablemente Go no admite la restricción de todas las rutinas generadas al mismo subproceso ) . Y Haskell requiere control monádico sobre los efectos debido a la aplicación de funciones puras para la transparencia referencial .


@alercah escribió:

Creo que la mayor desventaja de las restricciones inferidas es que facilitan el uso de un tipo de una manera que introduce una restricción sin comprenderla por completo. En el mejor de los casos, esto solo significa que sus usuarios pueden encontrarse con fallas inesperadas en el tiempo de compilación, pero en el peor de los casos, esto significa que puede romper el paquete para los consumidores al introducir una nueva restricción sin darse cuenta. Las restricciones explícitamente especificadas evitarían esto.

Acordado. Ser capaz de descifrar subrepticiamente el código en otros módulos porque los invariantes de los tipos no están anotados explícitamente es atrozmente insidioso.


@andrewcmyers escribió:

Para ser claros, no considero que la generación de código de estilo macro, ya sea que se haga con gen, cpp, gofmt -r u otras herramientas de macro/plantilla, sea una buena solución para el problema de los genéricos, incluso si está estandarizada. Tiene los mismos problemas que las plantillas de C++: exceso de código, falta de comprobación de tipos modulares y dificultad para la depuración. Empeora cuando comienza, como es natural, a construir código genérico en términos de otro código genérico. En mi opinión, las ventajas son limitadas: mantendría la vida relativamente simple para los escritores del compilador Go y produce un código eficiente, a menos que haya presión en la caché de instrucciones, ¡una situación frecuente en el software moderno!

@keean parece estar de acuerdo contigo.

@shelby3 gracias por los comentarios. ¿Puede la próxima vez hacer los comentarios / ediciones directamente en el documento mismo? Es más fácil rastrear dónde se deben arreglar las cosas y más fácil asegurarse de que todas las notas obtengan una respuesta adecuada.

La sección Descripción general parece implicar que el uso universal de Java de referencias de boxeo para instancias...

Se agregó un comentario para dejar en claro que no pretende ser una lista completa. Está ahí principalmente para que la gente entienda la esencia de las diferentes compensaciones. La lista completa de diferentes enfoques se encuentra más abajo.

Las estructuras genéricas tienden a acumular características de todos los usos, lo que resulta en mayores tiempos de compilación o código inflado o en la necesidad de un enlazador más inteligente.
Para las clases de tipos, esto no es cierto o representa un problema menor, porque las interfaces solo deben implementarse para los tipos de datos que se suministran a las funciones que usan esas interfaces. Las clases de tipos tienen que ver con el enlace tardío de la implementación a la interfaz, a diferencia de OOP, que une cada tipo de datos a sus métodos para la implementación de la clase.

Esa declaración es sobre lo que sucede con las estructuras de datos genéricas a largo plazo. En otras palabras, una estructura de datos genérica a menudo termina recopilando todos los usos diferentes, en lugar de tener múltiples implementaciones más pequeñas para diferentes propósitos. Solo como ejemplo, mire https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html.

Es importante tener en cuenta que, solo el "diseño mecánico" y "tanta flexibilidad" no es suficiente para crear una buena "solución genérica". También necesita buenas instrucciones, cómo se deben usar las cosas y qué evitar, y considerar cómo las personas terminan usándolo.

Las estructuras genéricas y las API que operan en ellas tienden a ser más abstractas que las API especialmente diseñadas...

Un contraargumento que creo que mejora significativamente esta preocupación es que la carga cognitiva de aprender un número ilimitado de reimplementaciones de casos especiales de los mismos algoritmos genéricos esencialmente es ilimitada...

Se agregó una nota sobre la carga cognitiva de muchas API similares.

Las reimplementaciones de casos especiales no son ilimitadas en la práctica. Solo verá un número fijo de especialización.

Esto no es una estafa válida.

Puede que no esté de acuerdo con algunos de los puntos, yo no estoy de acuerdo con algunos de ellos hasta cierto punto, pero entiendo su punto de vista y trato de entender los problemas a los que se enfrenta la gente día a día. El objetivo del documento es recoger diferentes opiniones, no juzgar "cuán molesto es algo para alguien".

Sin embargo, el documento toma una postura sobre "problemas rastreables a problemas del mundo real", porque los problemas abstractos y facilitados en los foros tienden a convertirse en charlas sin sentido sin que se construya ningún entendimiento.

A lo que realmente nos referimos aquí es a la regularidad de un idioma y las API genéricas ayudan, no dañan eso.

Claro, en la práctica, es posible que necesite este estilo de optimización solo en menos del 1% de los casos.

Soluciones alternativas:

Las soluciones alternativas no pretenden sustituir a los genéricos. Sino más bien una lista de posibles soluciones para diferentes tipos de problemas.

Plantillas de paquetes

Puedo estar equivocado, pero esto no parece del todo correcto. Los funtores ML (que no deben confundirse con los funtores FP) también pueden devolver una salida que permanece parametrizada.

¿Puede proporcionar una redacción más clara y, si es necesario, dividirla en dos enfoques diferentes?

@egonelbre gracias también por responder para poder saber en qué puntos necesito aclarar más mis pensamientos.

¿Puede la próxima vez hacer los comentarios / ediciones directamente en el documento mismo?

Disculpe, desearía poder cumplir, pero nunca he usado las funciones de discusión de Google Doc, no tengo tiempo para aprenderlo, y también prefiero poder vincular mis discusiones en Github para futuras referencias.

Solo como ejemplo, mire https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html.

El diseño de la biblioteca de colecciones de Scala fue criticado por muchas personas, incluido uno de los antiguos miembros clave del equipo . Un comentario publicado en LtU es representativo. Tenga en cuenta que agregué lo siguiente a una de mis publicaciones anteriores en este hilo para abordar esto:

Desconfío de los sistemas de "fregadero de cocina" que intentan mezclar tantos paradigmas (por ejemplo, subclases, herencia múltiple, programación orientada a objetos, linealización de rasgos, implicit , clases de tipos, tipos abstractos, etc.), debido a las quejas sobre Scala teniendo tantos casos de esquina en la práctica, aunque quizás eso mejore con Scala 3 (también conocido como Dotty y el cálculo DOT).

No creo que la biblioteca de colección de Scala sea representativa de las bibliotecas creadas para un PL con solo clases de tipos para polimorfismo. De hecho, las colecciones de Scala emplean el antipatrón de herencia , que provocó las jerarquías complejas, combinado con ayudantes implicit como CanBuildFrom que explotaron el presupuesto de complejidad. Y creo que si se cumple el punto de @keean acerca de que los _Elementos de programación_ de Stepanov son un álgebra , se podría crear una elegante biblioteca de colecciones. Era la primera alternativa que había visto a una biblioteca de colecciones basada en funtor (FP) (es decir, sin copiar Haskell ) también basada en matemáticas. Quiero ver esto en la práctica, que es una de las razones por las que estoy colaborando/hablando con él sobre el diseño de un nuevo PL. Y a partir de este momento, estoy planeando que ese idioma se transfiera inicialmente a Go (aunque he estado durante años tratando de encontrar una manera de evitarlo). Así que con suerte podremos experimentar pronto para ver cómo funciona.

Mi percepción es que la comunidad/filosofía de Go preferiría esperar a ver qué funciona en la práctica y adoptarlo más tarde una vez probado, que apresurarse y contaminar el lenguaje con experimentos fallidos. Porque como reiteró, todas estas afirmaciones abstractas no son tan constructivas (excepto quizás para los teóricos del diseño de PL). También es probable que no sea plausible diseñar un sistema de genéricos coherente por comité.

También necesita buenas instrucciones, cómo se deben usar las cosas y qué evitar, y considerar cómo las personas terminan usándolo.

Y creo que ayudará a no mezclar tantos paradigmas diferentes a disposición del programador en un mismo lenguaje. Aparentemente no es necesario ( @keean y necesito probar esa afirmación). Creo que ambos adscribimos a la filosofía de que el presupuesto de complejidad es finito y es lo que dejas fuera del PL lo que es tan importante como las características incluidas.

Sin embargo, el documento toma una postura sobre "problemas rastreables a problemas del mundo real", porque los problemas abstractos y facilitados en los foros tienden a convertirse en charlas sin sentido sin que se construya ningún entendimiento.

Acordado. Y también es difícil para todos seguir los puntos abstractos. El diablo está en los detalles y los resultados reales en la naturaleza.

Claro, en la práctica, es posible que necesite este estilo de optimización solo en menos del 1% de los casos.

Go ya tiene interface para genericidad, por lo que puede manejar los casos en los que no se necesita polimorfismo paramétrico en el tipo T para la instancia de la interfaz proporcionada por el sitio de la llamada.

Creo que leí en alguna parte, tal vez fue upthread, el argumento de que en realidad la biblioteca estándar de Go sufre de inconsistencia en el uso óptimo de los modismos más actualizados. No sé si eso es cierto, porque todavía no tengo experiencia con Go. Lo que quiero decir es que el paradigma genérico elegido infecta todas las bibliotecas. Entonces, sí, a partir de ahora puede afirmar que solo el 1% del código lo necesitaría, porque ya hay inercia en los modismos que evitan la necesidad de genéricos.

Puede que tengas razón. También tengo mi escepticismo acerca de cuánto usaré cualquier característica de lenguaje en particular. Creo que la experimentación para averiguarlo es la forma en que procederé. El diseño de PL es un proceso iterativo, entonces el problema es luchar contra la inercia que se desarrolla y dificulta la iteración del proceso. Así que supongo que Rob Pike tiene razón en el video donde sugiere escribir programas que escriban código para programas (lo que significa escribir herramientas de generación y transpiladores) para experimentar y probar ideas.

Cuando podamos demostrar que un conjunto particular de funciones es superior en la práctica (y, con suerte, también en popularidad de uso) a las que se encuentran actualmente en Go, entonces tal vez podamos ver una forma de consenso sobre agregarlas a Go. Animo a otros a crear también sistemas experimentales que se trasladen a Go.

¿Puede proporcionar una redacción más clara y, si es necesario, dividirla en dos enfoques diferentes?

Sumo mi voz a aquellos que querrían desalentar el intento de poner alguna función de plantilla demasiado simplista en Go y afirmar que son genéricos. IOW, creo que un sistema de genéricos que funcione correctamente y que no termine siendo una mala inercia es fundamentalmente incompatible con el deseo de tener un diseño excesivamente simplista para los genéricos. Afaik, un sistema de genéricos necesita un diseño holístico bien pensado y probado. Haciéndome eco de lo que escribió @larsth , animo a aquellos con propuestas serias a que primero construyan un transpiler (o lo implementen en una bifurcación de la interfaz de gccgo) y luego experimenten con la propuesta para que todos podamos entender mejor sus limitaciones. Me animó a leer que @ianlancetaylor no creía que se agregaría una contaminación de mala inercia a Go. En cuanto a mi queja específica sobre la propuesta de parametrización a nivel de paquete, mi sugerencia para quien la proponga, considere hacer un compilador que todos podamos usar para jugar y luego todos podamos hablar sobre ejemplos de lo que nos gusta y lo que no. No me gusta. De lo contrario, estamos hablando entre nosotros porque tal vez ni siquiera entiendo correctamente la propuesta tal como se describe de forma abstracta. No debo entender la propuesta, porque no entiendo cómo se puede reutilizar el paquete parametrizado en otro paquete que también está parametrizado. IOW, si un paquete toma parámetros, entonces también necesita crear instancias de otros paquetes con parámetros. Pero parecía que la propuesta decía que la única forma de crear instancias de un paquete parametrizado era con un tipo concreto, no con parámetros de tipo.

Disculpa tan prolijo. Quiero asegurarme de que no me malinterpreten.

@ shelby3 ah, entonces entendí mal la queja inicial. En primer lugar, debo aclarar que las secciones de "Enfoques genéricos" no son propuestas concretas. Son enfoques o, en otras palabras, decisiones de diseño más grandes que uno podría tomar en un enfoque genérico concreto. Sin embargo, las agrupaciones están fuertemente motivadas por implementaciones existentes o propuestas concretas/informales. Además, sospecho que todavía faltan al menos 5 grandes ideas en esa lista.

Para el enfoque de "plantillas de paquetes", hay dos variaciones (consulte las discusiones vinculadas en el documento):

  1. paquetes genéricos basados ​​en "interfaz",
  2. paquetes explícitamente genéricos.

Para 1. no requiere que el paquete genérico haga nada especial; por ejemplo, el container/ring actual se volvería utilizable para la especialización. Imagine la "especialización" aquí como el reemplazo de todas las instancias de la interfaz en el paquete con el tipo concreto (e ignorando las importaciones circulares). Cuando ese paquete se especializa en otro paquete, puede usar la "interfaz" como la especialización; se deduce que entonces este uso también se especializará.

Para 2. puedes verlos de dos maneras. Una es la especialización concreta recursiva en cada importación, similar a la creación de plantillas/macros, en ningún momento habría un "paquete aplicado parcialmente". Por supuesto, también se puede ver desde el lado funcional, que el paquete genérico es parcial con parámetros y luego lo especializas.

Entonces, sí, puede usar un paquete parametrizado en otro.

Haciéndome eco de lo que escribió @larsth , animo a aquellos con propuestas serias a que primero construyan un transpiler (o lo implementen en una bifurcación de la interfaz de gccgo) y luego experimenten con la propuesta para que todos podamos entender mejor sus limitaciones.

Sé que esto no se dirigió explícitamente a ese enfoque, pero tiene 4 prototipos diferentes para probar la idea. Por supuesto, no son transpiladores completos, pero son suficientes para probar algunas de las ideas. es decir, no estoy seguro de si alguien ha implementado el caso de "usar un paquete parametrizado de otro".

Los paquetes parametrizados se parecen mucho a los módulos de ML (y los funtores de ML son los parámetros que pueden ser otros paquetes). Hay dos formas en que estos pueden funcionar "aplicativo" o "generativo". Un funtor aplicativo es como un valor o un alias de tipo. Se debe construir un funtor generativo y cada instancia es diferente. Otra forma de pensar en esto es que, para que un paquete sea aplicable, debe ser puro (es decir, no hay variables mutables en el nivel del paquete). Si hay un estado en el nivel del paquete, debe ser generativo, ya que ese estado debe inicializarse, y es importante qué "instancia" de un paquete generativo realmente pasa como parámetro a otros paquetes que, a su vez, deben ser generativos. Por ejemplo, los paquetes de Ada son generativos.

El problema con el enfoque de paquetes generativos es que crea muchos repetitivos, en los que se crean instancias de paquetes con parámetros. Puede mirar los genéricos de Ada para ver cómo se ve esto.

Las clases de tipos evitan este repetitivo seleccionando implícitamente la clase de tipos en función de los tipos utilizados solo en la función. También puede ver las clases de tipos como sobrecarga restringida con envío múltiple, donde la resolución de sobrecarga casi siempre ocurre estáticamente en el momento de la compilación, con excepciones para la recursión polimórfica y los tipos existenciales (que son esencialmente variantes que no puede expulsar, solo puede usar las interfaces a las que se confirma la variante).

Un funtor aplicativo es como un valor o un alias de tipo. Se debe construir un funtor generativo y cada instancia es diferente. Otra forma de pensar en esto es que, para que un paquete sea aplicable, debe ser puro (es decir, no hay variables mutables en el nivel del paquete). Si hay un estado en el nivel del paquete, debe ser generativo, ya que ese estado debe inicializarse, y es importante qué "instancia" de un paquete generativo realmente pasa como parámetro a otros paquetes que, a su vez, deben ser generativos. Por ejemplo, los paquetes de Ada son generativos.

Gracias por la terminología exacta, necesito pensar cómo integrar estas ideas en el documento.

Además, no puedo ver una razón por la que no podría tener un "alias de tipo automático para un paquete generado", en cierto sentido, algo entre el enfoque de "funtor aplicativo" y "funtor generativo". Obviamente, cuando el paquete contiene algún tipo de estado, puede resultar complicado depurar y comprender.

El problema con el enfoque de paquetes generativos es que crea muchos repetitivos, en los que se crean instancias de paquetes con parámetros. Puede mirar los genéricos de Ada para ver cómo se ve esto.

Por lo que veo, crearía menos repeticiones que las plantillas de C++ pero más que clases de tipos. ¿Tiene un buen programa del mundo real para Ada que demuestre el problema? _(Por mundo real, me refiero al código que alguien está/estaba usando en producción)._

Claro, echa un vistazo a mi go-board de Ada: https://github.com/keean/Go-Board-Ada/blob/master/go.adb

Aunque esta es una definición bastante vaga de producción, el código está optimizado, funciona tan bien como la versión C++ y es de código abierto, y el algoritmo se ha perfeccionado durante varios años. También puede consultar la versión de C++: https://github.com/keean/Go-Board/blob/master/go.cpp

Esto muestra (creo) que los genéricos de Ada son una solución más ordenada que las plantillas de C ++ (pero eso no es difícil), por otro lado, es difícil acceder rápidamente a las estructuras de datos en Ada debido a las restricciones para devolver una referencia. .

Si desea ver un sistema de genéricos de paquetes para un lenguaje imperativo, creo que Ada es uno de los mejores para ver. Es una pena que decidieran adoptar múltiples paradigmas y agregar todo el material de OO a Ada. Ada es un Pascal mejorado, y Pascal era un lenguaje pequeño y elegante. Los genéricos de Pascal más Ada aún habrían sido un lenguaje bastante pequeño, pero habrían sido mucho mejores en mi opinión. Debido a que el enfoque de Ada cambió a un enfoque OO, parece difícil encontrar buena documentación y ejemplos de cómo hacer las mismas cosas con genéricos.

Aunque creo que las clases de tipos tienen algunas ventajas, podría vivir con los genéricos de estilo Ada, hay un par de problemas que me impiden usar Ada más ampliamente, creo que obtiene valores/objetos incorrectos (creo que muy pocos lenguajes lo hacen bien, 'C' es uno de los únicos), es difícil trabajar con punteros (variables de acceso) y crear abstracciones de puntero seguro, y no proporciona una forma de usar paquetes con polimorfismo en tiempo de ejecución (proporciona un modelo de objeto para esto, pero agrega un paradigma completamente nuevo en lugar de tratar de encontrar una manera de tener polimorfismo en tiempo de ejecución usando paquetes).

La solución al polimorfismo en tiempo de ejecución es hacer que los paquetes sean de primera clase para que las instancias de las firmas de los paquetes se puedan pasar como argumentos de función, esto lamentablemente requiere tipos dependientes (vea el trabajo realizado en Tipos de objetos dependientes para Scala para aclarar el lío que hicieron con su sistema tipo original).

Así que creo que los genéricos de paquete pueden funcionar, pero a Ada le tomó décadas lidiar con todos los casos extremos, así que miraría un sistema de genéricos de producción para ver qué mejoras se utilizan en la producción producida. Sin embargo, Ada todavía se queda corta porque los paquetes no son de primera clase y no se pueden usar en el polimorfismo en tiempo de ejecución, y esto debería abordarse.

@kean escribió :

Personalmente, considero que la reflexión en tiempo de ejecución es una característica incorrecta, pero solo soy yo ... Puedo explicar por qué si alguien está interesado.

El borrado de tipos habilita "Teoremas gratis", lo que tiene implicaciones prácticas . La reflexión en tiempo de ejecución escribible (¿y tal vez incluso legible debido a las relaciones transitivas con el código imperativo?) hace que sea imposible garantizar la transparencia referencial en cualquier código y, por lo tanto, ciertas optimizaciones del compilador no son posibles y las mónadas seguras de tipos no son posibles. Me doy cuenta de que Rust ni siquiera tiene una función de inmutabilidad todavía. OTOH, la reflexión permite otras optimizaciones que de otro modo no serían posibles si no pudieran escribirse estáticamente.

También había declarado upthread:

Y eso es lo que un compilador transpilando de un superconjunto de Go con genéricos agregados generaría como código Go. Pero el envoltorio no se basaría en alguna delimitación como el paquete, ya que carecería de la componibilidad que ya mencioné. El punto es que no hay un atajo para un buen sistema de tipos de genéricos componibles. O lo hacemos correctamente o no hacemos nada, porque agregar algún truco no componible que no sea realmente genérico creará eventualmente una inercia de retazos de genericidad a medias e irregularidad de casos de esquina y soluciones alternativas que hacen que el código del ecosistema Go ininteligible


@kean escribió:

[…] para que un paquete sea aplicable, debe ser puro (es decir, no hay variables mutables en el nivel del paquete)

Y no se pueden emplear funciones impuras para inicializar variables inmutables.

@egonelbre escribió:

Entonces, sí, puede usar un paquete parametrizado en otro.

Aparentemente, lo que tenía en mente eran "paquetes parametrizados de primera clase" y el polimorfismo de tiempo de ejecución proporcional (también conocido como dinámico) que @keean mencionó posteriormente, porque supuse que los paquetes parametrizados se propusieron en lugar de clases de tipos u programación orientada a objetos.

EDITAR: pero hay dos significados posibles para los módulos de "primera clase": módulos como valores de primera clase, como en Successor ML y MixML , que se distinguen de los módulos como valores de primera clase con tipos de primera clase como en 1ML, y la compensación necesaria en la recursividad del módulo (es decir, la mezcla ) entre ellos.

@kean escribió:

La solución al polimorfismo en tiempo de ejecución es hacer que los paquetes sean de primera clase para que las instancias de las firmas de los paquetes se puedan pasar como argumentos de función, esto lamentablemente requiere tipos dependientes (vea el trabajo realizado en Tipos de objetos dependientes para Scala para aclarar el lío que hicieron con su sistema tipo original).

¿Qué quiere decir con tipos dependientes? (EDITAR: Supongo que ahora se refería a la escritura "no dependiente del valor", es decir, " funciones cuyo tipo de resultado depende del [tiempo de ejecución] argumento [tipo]") Ciertamente no depende de los valores de, por ejemplo int datos, como en Idris. Creo que se refiere a escribir de forma dependiente (es decir, rastrear) el tipo de valores que representan instancias de módulos instanciados en la jerarquía de llamadas para que tales funciones polimórficas puedan monomorfizarse en tiempo de compilación. ¿El polimorfismo en tiempo de ejecución ingresa debido a que tales tipos monomorfizados son el tipo existencial vinculado a los tipos dinámicos? F-ing Modules demostró que los tipos "dependientes" no son absolutamente necesarios para modelar módulos ML en el sistema F ω . ¿He simplificado demasiado si supongo que @rossberg reformuló el modelo de escritura para eliminar todos los requisitos de monomorfización?

El problema con el enfoque del paquete generativo es que crea muchos repetitivos […]
Las clases de tipos evitan este repetitivo seleccionando implícitamente la clase de tipos en función de los tipos utilizados solo en la función.

¿No hay también repetitivo con funtores ML aplicativos? No existe una unificación conocida de clases de tipos y functores de ML (módulos) que conserve la brevedad sin introducir restricciones necesarias para evitar (véase también ) la antimodularidad inherente del criterio de unicidad global de las instancias de implementación de clases de tipos.

Las clases de tipos solo pueden implementar cada tipo de una manera y, de lo contrario, requieren newtype envoltorio repetitivo para superar la limitación. Aquí hay otro ejemplo de múltiples formas de implementar un algoritmo. Afaics, @keean resolvió esta limitación en su ejemplo de ordenación de clase de tipo anulando la selección implícita con un Relation explícitamente seleccionado empleando tipos de envoltura data para nombrar diferentes relaciones genéricamente en el tipo de valor, pero estoy dudando si tales tácticas son generales a todas las variantes de modularidad. Sin embargo, una solución más generalizada (que puede ayudar a mejorar el problema de la modularidad de la unicidad global, posiblemente combinada con una restricción de huérfanos como mejora del control de versiones propuesto para la resolución de huérfanos mediante el empleo de una implementación no predeterminada que podría quedar huérfana) puede ser tener un parámetro de tipo adicional implícitamente en todas las clases de tipos interface , que cuando no se especifica por defecto es la coincidencia implícita normal, pero cuando se especifica (o cuando no se especifica no coincide con ningún otro 2 ) luego selecciona la implementación que tiene el mismo valor en su lista delimitada por comas de valores personalizados (por lo que esta es una coincidencia modular más generalizada que nombrar una instancia específica implement ). La lista delimitada por comas es para que una implementación se pueda diferenciar en más de un grado de libertad, como si tiene dos especializaciones ortogonales. El especializado no predeterminado deseado podría especificarse en la declaración de función o en el sitio de llamada. En el lugar de la llamada, por ejemplo, f<non-default>(…) .

Entonces, ¿por qué necesitaríamos módulos parametrizados si tenemos clases de tipos? Afaics solo para (← enlace importante para hacer clic) sustitución porque la reutilización de clases de tipos para ese propósito no encaja bien porque, por ejemplo, queremos que un módulo de paquete pueda abarcar varios archivos y queremos poder abrir implícitamente el contenido de el módulo en el alcance sin repetitivo adicional . Entonces, tal vez avanzar con una parametrización del paquete _solo sintáctica_ solo de sustitución (no de primera clase) es un primer paso razonable que puede abordar la genericidad a nivel de módulo mientras permanece abierto a la compatibilidad y no se superpone la funcionalidad si se agregan clases de tipo más tarde para el nivel de función genericidad. macros , por ejemplo , se escriben o simplemente se reemplazan sintácticamente (también conocido como "preprocesador"). Si se escriben, los módulos duplican la funcionalidad de las clases de tipos, lo que no es deseable tanto desde el punto de vista de minimizar los paradigmas/conceptos superpuestos del PL como los posibles casos de esquina debido a las interacciones de la superposición ( como cuando se intenta ofrecer funciones de ML y clases de tipos). ). Los módulos con tipo son más modulares porque las modificaciones a cualquier implementación encapsulada dentro del módulo que no modifique las firmas exportadas no pueden hacer que los consumidores del módulo se vuelvan incompatibles (aparte del problema de anti-modularidad antes mencionado de las instancias de implementación superpuestas de clase de tipo). Estoy interesado en leer los pensamientos de @keean sobre esto.

[…] con excepciones para la recursión polimórfica y los tipos existenciales (que son esencialmente variantes de las que no se puede lanzar, solo se pueden usar las interfaces que confirma la variante).

Para ayudar a otros lectores. Por "recursión polimórfica", creo que se refiere a tipos de mayor rango, por ejemplo, devoluciones de llamada parametrizadas establecidas en tiempo de ejecución donde el compilador no puede monomorfizar el cuerpo de la función de devolución de llamada porque no se conoce en tiempo de compilación. Los tipos existenciales son, como mencioné antes, equivalentes a los objetos de rasgos de Rust, que son una forma de lograr contenedores heterogéneos con un enlace posterior en el Problema de expresión que class subclasificando la herencia virtual, pero no tan abiertos a la extensión en el Expresión Problema como uniones con estructuras de datos inmutables o copiar 3 que tienen un costo de rendimiento O (log n) .

1 Que no requiere HKT en el ejemplo anterior, porque SET no requiere el tipo elem es un parámetro de tipo del tipo genérico de set , es decir, no es set<elem> .

2 Sin embargo, si existiera más de una implementación no predeterminada y ninguna implementación predeterminada, la selección sería ambigua, por lo que el compilador debería generar un error.

3 Tenga en cuenta que mutar con estructuras de datos inmutables no requiere necesariamente copiar toda la estructura de datos, si la estructura de datos es lo suficientemente inteligente como para aislar el historial, como una lista de enlaces únicos.

Implementar func pick(a CollectionOfT, count uint) []T sería un buen ejemplo de aplicación de genéricos (de https://github.com/golang/go/issues/23717):

// pick returns a slice (len = n) of pseudorandomly chosen elements 
// in unspecified order from c which is an array, slice, or map.
for i, e := range pick(c, n) {

El enfoque de interfaz{} aquí es complicado.

He comentado varias veces sobre este problema que uno de los principales problemas con el enfoque de plantilla de C++ es su dependencia de la resolución de sobrecarga como mecanismo para la metaprogramación en tiempo de compilación.

Parece que Herb Sutter ha llegado a la misma conclusión: ahora hay una propuesta interesante para la programación en tiempo de compilación en C++ .

Tiene algunos elementos en común con el paquete Go reflect y mi propuesta anterior para las funciones de tiempo de compilación en Go .

Hola.
He escrito una propuesta de genéricos con restricciones para Go. Puedes leerlo aquí . Tal vez se pueda agregar como un documento de 15292. Se trata principalmente de restricciones y se lee como una enmienda a los parámetros de tipo de Taylor en Go .
Tiene la intención de ser un ejemplo de una forma viable (creo) de hacer genéricos "seguros para escribir" en Go, con suerte puede agregar algo a esta discusión.
Tenga en cuenta que, si bien he leído (la mayor parte) de este extenso hilo, no he seguido todos los enlaces que contiene, por lo que es posible que otros hayan hecho sugerencias similares. Si ese es el caso, pido disculpas.

hermano Cr.

Sintaxis bikeshedding:

constraint[T] Array {
    :[#]T
}

podría ser

type [T] Array constraint {
    _ [...]T
}

que se parece más a Go to me. :-)

Varios elementos aquí.

Una cosa es reemplazar : con _ y reemplazar # con ... .
Supongo que podrías hacerlo si lo prefieres.

Otra cosa es reemplazar constraint[T] Array con type[T] Array constraint .
Eso parecería indicar que las restricciones son tipos, lo que no creo que sea correcto. Formalmente, una restricción es un _predicado_ en el conjunto de todos los tipos, es decir. un mapeo del conjunto de tipos al conjunto { true , false }.
O si lo prefiere, puede pensar en una restricción como simplemente _un conjunto de_ tipos.
No es un tipo _a_.

hermano Cr.

¿Por qué ese constraint es solo un interface ?

type [T io.Writer] List struct { 
    element T; 
    next *List[T];
}

Una interfaz sería un poco más útil como restricción con la siguiente propuesta: #23796 que, a su vez, también le daría algún mérito a la propuesta en sí.

Además, si la propuesta de tipos de suma se acepta de alguna forma (#19412), entonces se deben usar para restringir el tipo.

Aunque creo que la palabra clave de restricción, se debe agregar algo similar, para no repetir grandes restricciones y evitar errores debido a la distracción.

Finalmente, para la porción de bicicletas, creo que las restricciones deben enumerarse al final de una definición, para evitar el hacinamiento (el óxido parece tener una buena idea aquí):

// similar to the map[T]... syntax
// also no constraint
type List[T] struct {
    element T
    next *List[T]
}

// with constraint
type List[T] struct {
    element T
    next *List[T]
} where T is io.Writer | encoding.BinaryMarshaler

type BigConstraint constraint {
     io.Writer
     SomeFunc() int
     AnotherFunc()
     AField int64
     StringField string
}


// with predefined constraint
type List[T, U] struct {
    element T
    val U
    next *List[T, U]
} where T is BigConstraint | encoding.BinaryMarshaler,
    U is io.Reader

@urandom : creo que es una gran ventaja para go tener interfaces implementadas implícitamente en lugar de explícitamente. La propuesta de @surlykke en este comentario creo que se parece mucho más a otra sintaxis de Go en espíritu.

@surlykke Me disculpo si la propuesta tiene la respuesta a alguno de estos.

Un uso de los genéricos es permitir funciones de estilo integradas. ¿Cómo implementaría len a nivel de aplicación con esto? El diseño de la memoria es diferente para cada entrada permitida, entonces, ¿cómo es esto mejor que una interfaz?

La "selección" descrita anteriormente tiene un problema similar en el que la indexación en un mapa y la indexación en un segmento son diferentes. En el caso del mapa, si hubo una conversión para cortar primero, entonces se puede usar el mismo código de selección, pero ¿cómo se hace esto?

Colecciones es otro uso:

// An unordered collection of comparable items.
type [T Comparable] Set []T

func (a Set) Diff(from Set) Set {
    // the implementation is the same as one with
    //     type Comparable interface { Equal(Comparable) bool }
    //     type Set []Comparable
}

// compile error
d := Set[int]{1, 2}.Diff(Set[string]{“abc”, “def”})

// Go 1, easier to read but runtime error
d := Set{1, 2}.Diff(Set{“abc”, “def”})

Para el caso de tipo de colección, no estoy convencido de que esta sea una gran victoria sobre los genéricos Go 1, ya que hay compensaciones de legibilidad.

Acepto que los parámetros de tipo deben tener algún tipo de restricción. De lo contrario, estaremos repitiendo los errores de las plantillas de C++. La pregunta es, ¿qué tan expresivas deben ser las restricciones?

En un extremo, podríamos simplemente usar interfaces. Pero como usted señala, muchos patrones útiles no se pueden capturar de esa manera.

Luego está su idea, y otras similares, que intentan crear un conjunto de restricciones útiles y proporcionar una nueva sintaxis para expresarlas. Aparte del problema de agregar aún más sintaxis, no está claro dónde detenerse. Como usted señala, su propuesta captura muchos patrones, pero de ninguna manera todos.

En el otro extremo está la idea que propongo en este documento . Utiliza el propio código Go como lenguaje de restricción. Puede capturar virtualmente cualquier restricción de esa manera y no requiere una sintaxis nueva.

@jba
Es un poco detallado. Tal vez si Go tuviera una sintaxis lambda, sería un poco más aceptable. Por otro lado, parece que el mayor problema que está tratando de resolver es verificar si un tipo admite un operador de algún tipo. Podría ser más fácil si Go tuviera interfaces predefinidas para varios operadores:

func equal[T](x, y T) bool
    where T is runtime.Equitable {
    return x == y
}

func copyable[T](x, y []T) int {
    return copy(x, y)
}

o algo por el estilo.

Si el problema es con la extensión de las funciones integradas, entonces tal vez el problema radica en la forma en que el lenguaje crea tipos de adaptadores. Por ejemplo, ¿no es la hinchazón asociada con sort.Interface toda la razón detrás de https://github.com/golang/go/issues/16721 y sort.Slice?
Mirando https://github.com/golang/go/issues/21670#issuecomment -325739411, la idea de @Sajmani de tener literales de interfaz podría ser el ingrediente necesario para que los parámetros de tipo funcionen fácilmente con los elementos integrados.
Mira la siguiente definición de iterador:

type [T] Iterator interface {
    Next() (elem T, done bool)
}

Si print es una función que simplemente itera sobre una lista e imprime su contenido, entonces el siguiente ejemplo usa literales de interfaz para construir una interfaz satisfactoria para print .

func SliceIterator(slice []T) Iterator {
    i := 0
    return Iterator{
        Next: func() (elem int, done bool) {
            v := slice[i]
            if i+1 == len(slice) {
                return v, true
            }
            i++
            return v, false
        },
    }
}

func main() {
    arr := []int{1,2,3,4,5}
    // SliceIterator works for an arbitrary slice
    print(SliceIterator(arr))
}

Ya se puede hacer esto si declaran globalmente tipos cuya única responsabilidad es satisfacer una interfaz. Sin embargo, esta conversión de una función a un método hace que las interfaces (y por lo tanto las "restricciones") sean más fáciles de satisfacer. No contaminamos las declaraciones de nivel superior con adaptadores simples (como "widgetsByName" en la clasificación).
Los tipos definidos por el usuario obviamente también pueden aprovechar esta función, como se ve en este ejemplo de LinkedList:

type ListNode struct {
    v string
    next *ListNode
}
func (l *ListNode) Iterator() Iterator {
    ptr := l
    return Iterator{
        Next: func() (elem int, done bool) {
            v := ptr.v
            if ptr.next == nil {
                return v, true
            }
            ptr = ptr.next
            return v, false
        },
    }
}

@ geovanisouza92 : Las restricciones, tal como las describí, son más expresivas que las interfaces (campos, operadores). Consideré brevemente extender las interfaces en lugar de introducir restricciones, pero creo que sería un cambio demasiado intrusivo para un elemento existente de Go.

@pciet No estoy muy seguro de lo que quiere decir con 'nivel de aplicación'. Go tiene una función len incorporada que se puede aplicar a una matriz, un puntero a una matriz, un segmento, una cadena y un canal, por lo tanto, en mi propuesta, si un parámetro de tipo está restringido a tener uno de estos como tipo subyacente , se le puede aplicar len .

@pciet Acerca de su ejemplo con Comparable restricción/interfaz. Tenga en cuenta que si define (la variante de interfaz):

type Comparable interface { Equal(Comparable) bool }
type Set []Comparable

Luego puede poner cualquier cosa que implemente Comparable en Set . Compara eso con:

constraint [T] Comparable { Equal(t T) bool }
type [T Comparable[T]] Set []T
...
type FooSet Set[Foo] // Where Foo satisfies constraint Comparable

donde solo puede poner valores de tipo Foo en FooSet . Esa es una seguridad de tipo más fuerte.

@urandom Nuevamente, no soy fanático de:

type MyConstraint constraint {....}

ya que no creo que una constante sea un tipo. Además, definitivamente no permitiría:

var myVar MyConstraint

lo que no tiene sentido para mí. Otra indicación de que las restricciones no son tipos.

@urandom En bikeshedding: creo que las restricciones deben declararse justo al lado de los parámetros de tipo. Considere una función ordinaria, definida así:

func MyFunc(i) {
     if (i>0) fmt.Println("It's positive")
} with i being an integer

No podrías leer esto de izquierda a derecha. En su lugar, primero leería func MyFunc(i) para determinar que es una definición de función. Luego tendría que saltar hasta el final para averiguar qué es i y luego regresar al cuerpo de la función. No es ideal, en mi opinión. Y no veo cómo las definiciones genéricas deberían ser diferentes.
Pero obviamente, esta discusión es ortogonal a la de si Go debe tener restricciones o genéricos.

@surlykke
Estoy bien con que no sea un tipo. Lo más importante es que tengan un nombre para que puedan ser referidos por múltiples tipos.

Para las funciones, si seguimos la sintaxis de rust, sería:

func MyFunc[I](i I) int64
     where I is being an integer {
   return 42
}

Por lo tanto, no ocultará cosas como el nombre de la función o sus parámetros, y no necesitará ir al final del cuerpo de la función para ver cuáles son las restricciones sobre los tipos genéricos.

@surlykke para la posteridad, ¿podría ubicar dónde se podría agregar su propuesta a:
https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4

Es un gran lugar para "recopilar" todas las propuestas.

Otra pregunta que les planteo a todos ustedes es cómo se trataría la especialización de diferentes instanciaciones de un tipo genérico. En la propuesta type-params , la forma de hacerlo es generar la misma función con plantilla para cada tipo instanciado, reemplazando el parámetro de tipo con el nombre del tipo. Para tener una funcionalidad separada para diferentes tipos, realice un cambio de tipo en el parámetro de tipo.

¿Es seguro asumir que cuando el compilador ve un cambio de tipo en un parámetro de tipo, se le permite generar una implementación separada para cada aserción? ¿O está demasiado involucrado en una optimización, ya que los parámetros de tipo anidados en las estructuras afirmadas pueden crear un aspecto paramétrico en la generación de código?

En la propuesta de funciones en tiempo de compilación , debido a que sabemos que estas declaraciones se generan en tiempo de compilación, un cambio de tipo no representa ningún costo de tiempo de ejecución.

Un escenario práctico: si consideramos un caso del paquete math/bits , realizar una afirmación de tipo para llamar a OnesCount por cada uintXX superaría el punto de tener una biblioteca de manipulación de bits eficiente. Sin embargo, si las afirmaciones de tipo se transformaran en las siguientes

func OnesCount(x T) int {
    switch x.(type) {
    case uint:
        // separate uint functionality...
    case uint8:
        // separate uint8 functionality...
    case uint16:
        // separate uint16 functionality...
    case uint32:
        // separate uint32 functionality...
    case uint64:
        // separate uint64 functionality...
    }
}

una llamada a

var x uint8 = 255
bits.OnesCount(x)

luego llamaría a la siguiente función generada (el nombre no es importante aquí):

func $OnesCount_uint8(x uint8) {
    // separate uint8 functionality...
}

@jba Esa es una propuesta interesante, pero para mí destaca principalmente el hecho de que la definición de la función paramétrica en sí misma suele ser suficiente para definir sus restricciones.

Si va a utilizar "operadores utilizados en una función" como restricciones, ¿qué ventaja le ofrece escribir una segunda función que contenga un subconjunto de los operadores utilizados en la primera?

@bcmills Uno de ellos es una especificación y el otro es la implementación. Es la misma ventaja que la escritura estática: puede detectar errores antes.

Si la implementación es la especificación, al estilo de las plantillas de C++, entonces cualquier cambio en la implementación potencialmente rompe los dependientes. Es posible que no se descubra hasta mucho más tarde, cuando los dependientes vuelvan a compilar y los descubridores no tengan contexto para comprender el mensaje de error. Con la especificación en el mismo paquete, puede detectar roturas localmente.

@mandolyte No estoy muy seguro de dónde agregarlo, ¿tal vez un párrafo debajo de 'Enfoques genéricos' llamado 'Genéricos con restricciones'?
El documento no parece contener mucho sobre la restricción de parámetros de tipo, por lo que si agrega un párrafo donde se mencionaría mi propuesta, entonces también se podrían enumerar otros enfoques para las restricciones.

@surlykke , el enfoque general del documento es hacer un cambio que se sienta bien y trataré de aceptarlo, incorporarlo y organizarlo con el resto del documento. Agregué una sección aquí . Siéntete libre de agregar cosas que me perdí.

@egonelbre Eso es muy bueno. ¡Gracias!

@jba
Me gusta tu propuesta, pero creo que es demasiado pesada para el golang. Me recuerda mucho a las plantillas en C++. Creo que el principal problema es que puedes escribir código realmente complejo con él.
Decidir si dos instancias de interfaz genérica se superponen porque el conjunto restringido de tipos se superpone sería una tarea difícil que provocaría tiempos de compilación más lentos. Lo mismo para la generación de código.

Creo que las restricciones propuestas son más ligeras para go. Por lo que escuché, las restricciones, también conocidas como clases de tipos, podrían implementarse ortogonalmente al sistema de tipos de un idioma.

Tengo que estar totalmente de acuerdo en que no debemos ir con restricciones implícitas del cuerpo de la función. Son ampliamente considerados como una de las fallas más significativas de las plantillas de C++:

  • Las restricciones no son fácilmente visibles. Si bien godoc teóricamente podría enumerar todas las restricciones en la documentación, no son visibles en el código fuente, excepto de manera implícita.
  • Por eso, es posible incluir accidentalmente una restricción adicional que solo es visible cuando intenta usar la función de una manera que no se esperaba. Al requerir una especificación explícita de las restricciones, el programador debe saber exactamente qué restricciones está introduciendo.
  • Toma la decisión sobre qué tipos de restricciones se permiten mucho más ad-hoc. Por ejemplo, ¿puedo definir la siguiente función? ¿Cuáles son las restricciones reales sobre T, U y V aquí? Si requerimos que el programador especifique restricciones explícitamente, entonces somos conservadores en el tipo de restricciones que permitimos (permitiéndonos expandir eso lenta y deliberadamente). Si tratamos de ser conservadores de todos modos, ¿cómo damos un mensaje de error para una función como esta? "Error: no se puede asignar uv() a T porque impone una restricción ilegal"?
func[T, U, V] Foo(u U, v V) {
  var t T = u.v(V) + 1;
}
  • Llamar a funciones genéricas en otras funciones genéricas empeora las situaciones anteriores, ya que ahora necesita revisar todas las restricciones de los llamados para comprender las restricciones de la función que está escribiendo o leyendo.
  • La depuración puede ser muy difícil porque los mensajes de error no deben proporcionar suficiente información para encontrar el origen de la restricción o deben filtrar detalles internos de la función. Por ejemplo, si F tiene algún requisito en un tipo T , y el autor de F está tratando de averiguar de dónde proviene ese requisito, le gustaría que el compilador alertarlos sobre exactamente qué declaración da lugar a la restricción (especialmente si proviene de un destinatario genérico). Pero un usuario de F no quiere esa información y, de hecho, si está incluida en los mensajes de error, estamos filtrando los detalles de implementación de F en los mensajes de error de sus usuarios, que son una experiencia de usuario terrible.

@alercah

Por ejemplo, ¿puedo definir la siguiente función?

func[T, U, V] Foo(u U, v V) {
  var t T = u.v(V) + 1;
}

No. u.v(V) es un error de sintaxis porque V es un tipo y la variable t no se usa.

Sin embargo, podría definir esta función, que puede ser la que pretendía:

func[T, U, V] Foo(u U, v V) {
    var _ T = u.v(v) + 1;
}

¿Cuáles son las restricciones reales sobre T, U y V aquí?

  • El tipo V no tiene restricciones.
  • El tipo U debe tener un método v que acepte un solo parámetro o varargs de algún tipo asignable desde V , porque u.v se invoca con un solo argumento de tipo V .

    • U.v podría ser un campo de tipo función, pero podría decirse que eso debería implicar un método; ver #23796.

  • El tipo devuelto por U.v debe ser numérico, porque se le suma la constante 1 .
  • El tipo de valor devuelto de U.v debe ser asignable a T , porque u.v(…) + 1 se asigna a una variable de tipo T .
  • El tipo T debe ser numérico, porque el tipo de valor devuelto de U.v es numérico y asignable a T .

(Aparte: podría argumentar que U y V deberían tener la restricción "copiable" porque los argumentos de esos tipos se pasan por valor, pero el sistema de tipo no genérico existente no aplica esa restricción tampoco. Ese es un asunto para una propuesta separada.)

Si requerimos que el programador especifique restricciones explícitamente, entonces somos conservadores en el tipo de restricciones que permitimos (permitiéndonos expandir eso lenta y deliberadamente).

Sí, eso es cierto: pero omitir una restricción sería un defecto grave, ya sea que esas restricciones sean implícitas o no. En mi opinión, el papel más importante de las restricciones es resolver la ambigüedad. Por ejemplo, en las restricciones anteriores, el compilador debe estar preparado para crear instancias de u.v como método de argumento único o variable.

La ambigüedad más interesante ocurre con los literales, donde necesitamos eliminar la ambigüedad entre los tipos de estructura y los tipos compuestos:

func[T] Foo() (t T) {
    x := 42;
    t = T{x: "some string"}  // Is x an index, or a field name?
    _ = x
}

Si tratamos de ser conservadores de todos modos, ¿cómo damos un mensaje de error para una función como esta? "Error: no se puede asignar uv() a T porque impone una restricción ilegal"?

No estoy muy seguro de lo que está preguntando, ya que no veo restricciones en conflicto para este ejemplo. ¿Qué quiere decir con una "restricción ilegal"?

La depuración puede ser muy difícil porque los mensajes de error no deben proporcionar suficiente información para encontrar el origen de la restricción o deben filtrar detalles internos de la función.

No todas las restricciones relevantes pueden expresarse mediante el sistema de tipos (ver también https://github.com/golang/go/issues/22876#issuecomment-347035323). Algunas restricciones son impuestas por situaciones de pánico en tiempo de ejecución; algunos son aplicados por el detector de carreras; las restricciones más peligrosas simplemente se documentan y no se detectan en absoluto.

Todos esos "filtran detalles internos" hasta cierto punto. (Ver también https://xkcd.com/1172/.)

Por ejemplo, si […] el autor de F está tratando de averiguar de dónde proviene ese requisito, le gustaría que el compilador le avise exactamente qué declaración da lugar a la restricción (especialmente si proviene de un destinatario genérico). Pero un usuario de F no quiere esa información[.]

¿Quizás? Así es como los autores de API usan las anotaciones de tipo en lenguajes de tipo inferido como Haskell y ML, pero también conduce a un agujero de conejo de tipos profundamente paramétricos ("de orden superior") en general.

Por ejemplo, suponga que tiene esta función:

func [F, Arg, Result] InvokeAsync(f F, x Arg) (<-chan Result) {
    c := make(chan result, 1)
    go func() { c <- f(x) }()
    return c
}

¿Cómo expresa las restricciones explícitas en el tipo Arg ? Dependen de la instanciación específica de F . Ese tipo de dependencia parece faltar en muchas de las propuestas recientes de restricciones.

No. uv(V) es un error de sintaxis porque V es un tipo y la variable t no se usa.

Sin embargo, podría definir esta función, que puede ser la que pretendía:

Sí, esa era la intención, mis disculpas.

El tipo T debe ser numérico, porque el tipo de valor devuelto de U.v es numérico y asignable a T .

¿Deberíamos realmente considerar esto como una restricción? Es deducible de las otras restricciones, pero ¿es más o menos útil llamar a esto una restricción distinta? Las restricciones implícitas hacen esta pregunta de una manera que no lo hacen las restricciones explícitas.

Sí, eso es cierto: pero omitir una restricción sería un defecto grave, ya sea que esas restricciones sean implícitas o no. En mi opinión, el papel más importante de las restricciones es resolver la ambigüedad. Por ejemplo, en las restricciones anteriores, el compilador debe estar preparado para instanciar uv como método de argumento único o variable.

Quise decir "restricciones que permitimos" como en el lenguaje. Con restricciones explícitas, es mucho más fácil para nosotros decidir qué tipo de restricciones estamos dispuestos a permitir que los usuarios escriban, en lugar de simplemente decir que la restricción es "lo que hace que las cosas se compilen". Por ejemplo, mi ejemplo Foo anterior en realidad involucra un tipo adicional implícito separado de T , U o V , ya que debemos considerar el tipo de retorno de u.v . Este tipo no se menciona explícitamente de ninguna manera en la declaración de f ; las propiedades que debe tener están completamente implícitas. Del mismo modo, ¿estamos dispuestos a permitir tipos de mayor rango ( forall )? No puedo pensar en un ejemplo, pero tampoco puedo convencerme de que no puedes escribir implícitamente un límite de tipo de rango superior.

Otro ejemplo es si debemos permitir que una función aproveche la sintaxis sobrecargada. Si una función restringida implícitamente hace for i := range t para algunos t de tipo genérico T , la sintaxis funciona si T es cualquier matriz, sector, canal, o mapa. Pero la semántica es bastante diferente, especialmente si T es un tipo de canal. Por ejemplo, si t == nil (que puede suceder siempre que T sea ​​una matriz), entonces la iteración no hace nada, ya que no hay elementos en un segmento o mapa nulo, o bloques para siempre ya que eso es lo que recibe en los canales nil . Esta es una gran pistola esperando a suceder. Del mismo modo está haciendo m[i] = ... ; si tengo la intención de que m sea ​​un mapa, tendré que protegerme de que en realidad sea una porción, ya que de lo contrario el código podría entrar en pánico en una asignación fuera de rango.

De hecho, creo que esto se presta a otro argumento contra las restricciones implícitas: los autores de API pueden escribir declaraciones artificiales solo para agregar restricciones. Por ejemplo for _, _ := range t { break } evita un canal mientras permite mapas, cortes y arreglos; x = append(x) obliga a x a tener un tipo de corte. var _ = make(T, 0) permite cortes, mapas y canales, pero no arreglos. Habrá un libro de recetas sobre cómo agregar implícitamente restricciones para que nadie pueda llamar a su función con un tipo para el que no ha escrito el código correcto. Ni siquiera puedo pensar en una forma de escribir código que solo compile para tipos de mapas a menos que también conozca el tipo de clave. Y no creo que esto sea hipotético en absoluto; los mapas y los cortes se comportan de manera bastante diferente para la mayoría de las aplicaciones.

No estoy muy seguro de lo que está preguntando, ya que no veo restricciones en conflicto para este ejemplo. ¿Qué quiere decir con una "restricción ilegal"?

Me refiero a una restricción que no está permitida por el idioma, como si el idioma decide no permitir restricciones de mayor rango.

No todas las restricciones relevantes pueden expresarse mediante el sistema de tipos (ver también #22876 (comentario)). Algunas restricciones son impuestas por situaciones de pánico en tiempo de ejecución; algunos son aplicados por el detector de carreras; las restricciones más peligrosas simplemente se documentan y no se detectan en absoluto.

Todos esos "filtran detalles internos" hasta cierto punto. (Ver también https://xkcd.com/1172/.)

Realmente no veo cómo #22876 entra en esto; eso es tratar de usar el sistema de tipos para expresar un tipo diferente de restricción. Siempre será cierto que no podemos expresar algunas restricciones sobre valores, o sobre programas, incluso con un sistema tipo de complejidad arbitraria. Pero aquí solo estamos hablando de restricciones en los tipos . El compilador debe poder responder a la pregunta "¿Puedo crear una instancia de este genérico con el tipo T ?" lo que significa que debe comprender las restricciones, ya sean implícitas o explícitas. (Tenga en cuenta que algunos lenguajes, como C ++ y Rust, no pueden decidir esta pregunta en general porque puede depender de un cálculo arbitrario y, por lo tanto, se convierte en el problema de detención, pero aún expresan las restricciones que deben cumplirse).

Lo que quiero decir es más como "¿qué mensaje de error debería dar el siguiente ejemplo?"

func [U] DirectlyConstrained(U t) {
    t.DoSomething();
}
func [T] IndirectlyConstrained(T t) {
    DirectlyConstrainted(t);
}
func Illegal() {
    IndirectlyConstrained(4);
}

Podemos decir Error: cannot call IndirectlyConstrained with [T = int]; T must have a method with signature func (T t) DoSomething() . Este mensaje de error es útil para un usuario de IndirectlyConstrained porque establece claramente las restricciones que faltan. Pero no proporciona información a alguien que intenta depurar por qué IndirectlyConstrained tiene esa restricción, lo cual es un gran problema de usabilidad si se trata de una función grande. Podríamos agregar Note: this constraint exists on T because IndirectlyConstrained calls DirectlyConstrained with [U = T] on line N , pero ahora estamos filtrando detalles de la implementación de IndirectlyConstrained . Además de eso, no hemos explicado por qué IndirectlyConstrained tiene la restricción, entonces, ¿agregamos otro Note: this constraint exists on U because DirectlyConstrained calls t.DoSomething() on line M ? ¿Qué sucede si la restricción implícita proviene de algún destinatario cuatro niveles más abajo en la pila de llamadas?

Además, ¿cómo formateamos estos mensajes de error para los tipos que no se enumeran explícitamente como parámetros? Por ejemplo, si en el ejemplo anterior, IndirectlyConstrained llama a DirectlyConstrained(t.U()) . ¿Cómo nos referimos al tipo? En este caso podríamos decir the type of t.U() , pero el valor no será necesariamente el resultado de una sola expresión; podría construirse sobre múltiples declaraciones. Entonces necesitaríamos sintetizar una expresión con los tipos correctos para poner en el mensaje de error, una que nunca aparece en el código, o tendríamos que encontrar alguna otra forma de referirnos a ella que sería menos clara para el pobre llamador que violó la restricción.

¿Cómo expresa las restricciones explícitas en el tipo Arg? Dependen de la instanciación específica de F. Ese tipo de dependencia parece faltar en muchas de las propuestas recientes de restricciones.

Coloque F y haga que el tipo de f sea func (Arg) Result . Sí, ignora las funciones variádicas, pero el resto de Go también lo hace. Una propuesta para hacer varargs funcs asignables a firmas compatibles podría hacerse por separado.

Para los casos en los que realmente requerimos límites de tipo de orden superior, puede o no tener sentido incluirlos en genéricos v1. Las restricciones explícitas nos obligan a decidir explícitamente si queremos admitir tipos de orden superior y cómo. La falta de consideración hasta ahora es un síntoma, creo, del hecho de que Go actualmente no tiene forma de referirse a las propiedades de los tipos integrados. Es una cuestión abierta general de cómo cualquier sistema genérico permitirá funciones genéricas sobre todos los tipos numéricos, o todos los tipos de enteros, y la mayoría de las propuestas no se han centrado mucho en esto.

Evalúe mi implementación de genéricos en su próximo proyecto
http://go-li.github.io/

Podemos decir Error: cannot call IndirectlyConstrained with [T = int]; T must have a method with signature func (T t) DoSomething() . Este mensaje de error […] no proporciona información a alguien que intenta depurar por qué IndirectlyConstrained tiene esa restricción, lo cual es un gran problema de usabilidad si se trata de una función grande.

Quiero señalar una gran suposición que estás haciendo aquí: que el mensaje de error de go build es la _única_ herramienta que el programador tiene disponible para diagnosticar el problema.

Para usar una analogía: si encuentra un error en tiempo de ejecución, tiene varias opciones para la depuración. El error en sí contiene solo un mensaje simple, que puede o no ser adecuado para describir el error. Pero no es la única información que tiene disponible: por ejemplo, también tiene las declaraciones de registro que emitió el programa, y ​​si es un error realmente complicado, puede cargarlo en un depurador interactivo.

Es decir, la depuración en tiempo de ejecución es un proceso interactivo. Entonces, ¿por qué deberíamos asumir una depuración no interactiva para errores en tiempo de compilación? Como una alternativa, podríamos enseñarle a la herramienta guru sobre las restricciones de tipo. Entonces, la salida del compilador sería algo como:

somefile.go:123: Argument `4` to DirectlyConstrained has type `int`,
    but DirectlyConstrained requires a type `T` with method `DoSomething()`.
    (For more detail, run `guru contraints path/to/somefile.go:#1033`.)

Eso le brinda al usuario del paquete genérico la información que necesita para depurar el sitio de la llamada inmediata, pero _también_ brinda una ruta de navegación para que el mantenedor del paquete (y, lo que es más importante, su entorno de edición) investigue más a fondo.

Podríamos agregar Note: this constraint exists on T because IndirectlyConstrained calls DirectlyConstrained with [U = T] on line N , pero ahora estamos filtrando detalles de la implementación de IndirectlyConstrained .

Sí, eso es lo que quiero decir acerca de la filtración de información de todos modos. Ya puede usar guru describe para echar un vistazo dentro de una implementación. Puede echar un vistazo dentro de un programa en ejecución usando un depurador, y no solo buscar en la pila, sino también descender a funciones arbitrarias de bajo nivel.

Estoy totalmente de acuerdo en que debemos ocultar información probablemente irrelevante _por defecto_, pero eso no significa que debamos ocultarla en absoluto.

Si una función restringida implícitamente hace i := range t para algunos t de tipo genérico T , la sintaxis funciona si T es cualquier matriz, segmento, canal , o mapa. Pero la semántica es bastante diferente, especialmente si T es un tipo de canal.

Creo que ese es el argumento más convincente para las restricciones de tipo, pero eso no requiere que las restricciones explícitas sean tan detalladas como lo que algunas personas proponen. Para eliminar la ambigüedad de los sitios de llamadas, parece suficiente restringir los parámetros de tipo a algo más cercano a reflect.Kind . No necesitamos describir operaciones que ya están claras en el código; en cambio, solo necesitamos decir cosas como " T es un tipo de segmento". Eso lleva a un conjunto mucho más simple de restricciones:

  • un tipo sujeto a operaciones de índice debe etiquetarse como lineal o asociativo,
  • un tipo sujeto a operaciones range debe etiquetarse como vacío nulo o bloqueo nulo,
  • un tipo con literales debe etiquetarse como si tuviera campos o índices, y
  • (quizás) un tipo con operaciones numéricas debe etiquetarse como punto fijo o flotante.

Eso lleva a un lenguaje de restricciones mucho más estrecho, tal vez algo como:

TypeConstraint = "sliceable" | "map" | "chan" | "struct" | "integer" | "float" | "type"

con ejemplos como:

func[T:integer, U, V] Foo(u U, v V) {
    var _ T = u.v(v) + 1;
}
func [S:sliceable, T] append(s S, x ...T) S {
    dst := s
    if cap(s) - len(s) < len(x) {
        dst = make(S, len(s), nextSizeClass(cap(s)))
        copy(dst, s)
    }
    copy(dst[len(s):cap(s)], x)
    return dst[:len(s)+len(x)]
}

Siento que hemos dado un gran paso hacia el genérico personalizado al introducir el alias de tipo.
El alias de tipo hace posibles los supertipos (tipo de tipos).
Podemos tratar los tipos como valores al usar.

Para simplificar las explicaciones, podemos agregar un nuevo elemento de código, genre .
La relación entre géneros y tipos es como la relación entre tipos y valores.
En otras palabras, un género significa un tipo de tipos.

Cada clase de tipo, excepto las clases de estructura, interfaz y función, corresponde a un género predeclarado.

  • bool
  • Cuerda
  • Int8, Uint8, Int16, Uint16, Int32, Uint32, Int64, Uint64, Int, Uint, Uintptr
  • Flotador32, Flotador64
  • Complejo64, Complejo128
  • Matriz, Sector, Mapa, Canal, Puntero, UnsafePointer

Hay algunos otros géneros predeclarados, como Comparable, Numérico, Entero, Flotante, Complejo, Contenedor, etc. Podemos usar Type o * denota el género de todos los tipos.

Los nombres de todos los géneros incorporados comienzan con una letra mayúscula.

Cada estructura, interfaz y tipo de función corresponde a un género.

También podemos declarar géneros personalizados:

genre Addable = Numeric | String
genre Orderable = Interger | Float | String
genre Validator = func(int) bool // each parameter and result type must be a specified type.
genre HaveFieldsAndMethods = {
    width  int // we must use a specific type to define the fields.
    height int // we can't use a genre to define the fields.
    Load(v []byte) error // each parameter and result type must be a specified type.
    DoSomthing()
}
genre GenreFromStruct = aStructType // declare a genre from a struct type
genre GenreFromInterface = anInterfaceType // declare a genre from an interface type
genre GenreFromStructInterface = aStructType + anInterfaceType
genre ComparableStruct = HaveFieldsAndMethods & Comprable
genre UncomparableStruct = HaveFieldsAndMethods &^ Comprable

Para que la siguiente explicación sea consistente, se necesita un modificador de género.
El modificador de género se indica con Const . Por ejemplo:

  • Const Integer es un género (diferente de Integer ) y su instancia debe ser un valor constante cuyo tipo debe ser un número entero. Sin embargo, el valor constante puede verse como un tipo especial.
  • Const func(int) bool es un género (diferente de func(int) bool ) y su instancia debe ser un valor de función declarado. Sin embargo, la declaración de función se puede ver como un tipo especial.

(La solución del modificador es algo complicada, tal vez haya otras mejores soluciones de diseño).

Bien, continuemos.
Necesitamos otro concepto. Encontrar un buen nombre para él no es fácil,
Llamémoslo crate .
Generalmente, la relación entre cajas y géneros es como la relación entre funciones y tipos.
Una caja puede tomar tipos como parámetros y devolver tipos.

Una declaración de caja (supongamos que el siguiente código se declara en el paquete lib ):

crate Example [T Float, S {width, height T}, N Const Integer] [*, *, *] {
    type MyArray [N]T

    func Add(a, b T) T {
        return a+b
    }

    type M struct {
        x T
        y S
    }

    func (m *M) Area() T {
        m.DoSomthing()
        return m.y.width * m.y.height
    }

    func (m *M) Perimeter() T {
        return 2 * Add(m.y.width, m.y.height)
    }

    export M, Add, MyArray
}

Usando la caja de arriba.

import "lib"

// We can use AddFunc as a normal delcared function.
// Its genre is "Const func (a, b T) T"
type Rect, AddFunc, Array = lib.Example[float32, struct{x, y float32}, 100]

func demo() {
    var r Rect
    a, p = r.Area(), r.Perimeter()
    _ = AddFunc(a, p)
}

Mis ideas absorben muchas ideas de otros que se muestran arriba.
No están muy maduros ahora.
Los publico aquí solo porque siento que son interesantes,
y no quiero mejorarlo más.
Se mataron tantas células cerebrales al arreglar los agujeros en las ideas.
Espero que estas ideas puedan inspirar a otras tuzas.

Lo que llamas “género” en realidad se llama “clase”, y es bien conocido en el
comunidad de programación funcional. Lo que llamas una caja es una caja restringida.
tipo de funtor ML.

El miércoles 4 de abril de 2018 a las 12:41 p. m., dotaheor [email protected] escribió:

Siento que hemos dado un gran paso hacia el genérico personalizado al introducir
escriba alias.
El alias de tipo hace posibles los supertipos (tipo de tipos).
Podemos tratar los tipos como valores al usar.

Para simplificar las explicaciones, podemos agregar un nuevo elemento de código, género.
La relación entre géneros y tipos es como la relación entre tipos
y valores
En otras palabras, un género significa un tipo de tipos.

Cada tipo de tipo, excepto los tipos de estructura, interfaz y función,
corresponde a un género predeclarado.

  • bool
  • Cuerda
  • Int8, Uint8, Int16, Uint16, Int32, Uint32, Int64, Uint64, Int, Uint,
    Uintptr
    & Flotador32, Flotador64
  • Complejo64, Complejo128
  • Matriz, Sector, Mapa, Canal, Puntero, UnsafePointer

Hay algunos otros géneros predeclarados, como Comaprable, Numérico,
Entero, Flotante, Complejo, Contenedor, etc. Podemos usar Tipo o * denota
el género de todo tipo.

Los nombres de todos los géneros incorporados comienzan con una letra mayúscula.

Cada estructura, interfaz y tipo de función corresponde a un género.

También podemos declarar géneros personalizados:

género Agregable = Numérico | Cuerda
genero Ordenable = Interger | Flotador | Cuerda
gender Validator = func(int) bool // cada parámetro y tipo de resultado debe ser un tipo específico.
genero HaveFieldsAndMethods = {
width int // debemos usar un tipo específico para definir los campos.
height int // no podemos usar un género para definir los campos.
Error de carga (v [] byte) // cada parámetro y tipo de resultado debe ser un tipo especificado.
HacerAlgo()
}
género GenreFromStruct = aStructType // declara un género a partir de un tipo de estructura
género GenreFromInterface = anInterfaceType // declara un género a partir de un tipo de interfaz
género GenreFromStructInterface = aStructType | un tipo de interfaz

Para que la siguiente explicación sea consistente, se necesita un modificador de género.
El modificador de género se denota por Const. Por ejemplo:

  • Const Integer es un género y su instancia debe ser un valor constante
    cuyo tipo debe ser un número entero.
    Sin embargo, el valor constante puede verse como un tipo especial.
  • Const func(int) bool es un género y su instancia debe ser un delcared
    valor de la función.
    Sin embargo, la declaración de función se puede ver como un tipo especial.

(La solución del modificador es un poco complicada, tal vez haya otros mejores diseños
soluciones.)

Bien, continuemos.
Necesitamos otro concepto. Encontrar un buen nombre para él no es fácil,
Llamémoslo caja.
En general, la relación entre cajas y géneros es como la relación
entre funciones y tipos.
Una caja puede tomar tipos como parámetros y devolver tipos.

Una declaración de caja (supongamos que el siguiente código se declara en lib
paquete):

crate Ejemplo [T Float, S {ancho, alto T}, N Const Integer] [*, *, *] {
escriba MiArray [N]T

func Añadir(a, b T) T {
volver a+b
}

// Un género de alcance de caja. Solo se puede usar en la caja.

// M es un tipo de género G
estructura tipo M {
x T
yS
}

func (m *M) Area() T {
m.Hacer Algo()
devuelve mi ancho * mi altura
}

func (m *M) Perímetro() T {
return 2 * Add(miancho, mialtura)
}

exportar M, Añadir, MiArray
}

Usando la caja de arriba.

importar "lib"

// Podemos usar AddFunc como una función delcared normal.
escriba Rect, AddFunc, Array = lib.Example(float32, struct{x, y float32})

función de demostración() {
var r rect
a, p = r.Área(), r.Perímetro()
_ = AgregarFunc(a, p)
}

Mis ideas absorben muchas ideas de otros que se muestran arriba.
No están muy maduros ahora.
Los publico aquí solo porque siento que son interesantes,
y no quiero mejorarlo más.
Se mataron tantas células cerebrales al arreglar los agujeros en las ideas.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/15292#issuecomment-378665695 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AGGWB78BrjN0BxRfroH-jRNy4mCXgSwCks5tlPfMgaJpZM4IG-xv
.

Siento que hay algunas diferencias entre Tipo y Género.

Por cierto, si una caja solo devuelve un tipo, podemos usar su llamada como tipo directamente.

package lib

// export a type
crate List [T *] * {
    type List struct {
        ...
    }

    export List
}

úsalo:

import "lib"

var l lib.List[int]

Habría algunas reglas de "deducción de género", al igual que "deducción de tipo" en el sistema actual.

@dotaheor , @DemiMarie tiene razón. Su concepto de "género" suena exactamente como el "tipo" de la teoría de tipos. (Su propuesta requiere una regla de subclasificación, pero eso no es poco común).

La palabra clave genre de su propuesta define nuevos tipos como supertipos de tipos existentes. La palabra clave crate define objetos con "firmas de caja", que son un tipo que no es un subtipo de Type .

Como sistema formal, su propuesta parece ser algo como:

Caja ::= χ | ⋯
Escriba ::= τ | χ | int | bool | ⋯ | func(τ) | func(τ) τ | []τ | χ[τ₁, …]

CrateSig ::= [κ₁, …] ⇒ [κₙ, …]
Clase ::= κ | exactly τ | kindOf κ | Map | Chan | ⋯ | Const κ | Type | CrateSig

Para abusar de alguna notación de teoría de tipos:

  • Lee “⊢” como “implica”.
  • Lee “ k1k2 ” como “ k1 es un subtipo de k2 ”.
  • Léase “:” como “es de especie”.

Entonces las reglas se ven algo como:

τ : exactly τ
exactly τkindOf exactly τ
kindOf exactly τType

τ : κ₁κ₁κ₂τ : κ₂

τ₁ : Typeτ₂ : TypekindOf exactly map[τ₁]τ₂Map
MapType

κ₁κ₂Const κ₁Const κ₂

[…]
(Y así sucesivamente, para todos los tipos incorporados)


Las definiciones de tipos confieren tipos, y los tipos subyacentes se colapsan en los tipos de tipos incorporados:

type τ₁ τ₂τ₂ : κτ₁ : kindOf κ

kindOf kindOf κkindOf κ
kindOf MapMap
[…]


genre define nuevas relaciones de subtipo:
genre κ = κ₁ | κ₂κ₁κ
genre κ = κ₁ | κ₂κ₂κ

(Puede definir Numeric y similares en términos de | .)

genre κ = κ₁ & κ₂ ∧ ( κ₃κ₁ ) ∧ ( κ₃κ₂ ) ⊢ κ₃κ


La regla de expansión de cajas es similar:
type τₙ, … = χ[τ₁, …] ∧ ( χ : [κ₁, …] ⇒ [κₙ, …] ) ∧ ( τ₁ : κ₁ ) ∧ ⋯ ⊢ τₙ : κₙ

Todo esto es solo hablar de los tipos, por supuesto. Si desea convertirlo en un sistema de tipos, también necesita reglas de tipos. 🙂


Entonces, lo que estás describiendo es una forma de parametricidad bastante bien entendida. Eso es bueno, ya que se entiende bien, pero decepcionante porque no ayuda a resolver los problemas únicos que presenta Go.

Los problemas realmente interesantes y retorcidos que presenta Go se relacionan principalmente con la inspección de tipos dinámicos. ¿Cómo deberían interactuar los parámetros de tipo con las aserciones de tipo y la reflexión?

(Por ejemplo, ¿debería ser posible definir interfaces con métodos de tipos paramétricos? Si es así, ¿qué sucede si escribe un valor de esa interfaz con un parámetro novedoso en tiempo de ejecución?)

En una nota relacionada, ¿ha habido una discusión sobre cómo hacer que el código sea genérico sobre tipos integrados y definidos por el usuario? ¿Como crear código que pueda manejar bigints y enteros primitivos?

En una nota relacionada, ¿ha habido una discusión sobre cómo hacer que el código sea genérico sobre tipos integrados y definidos por el usuario? ¿Como crear código que pueda manejar bigints y enteros primitivos?

Los mecanismos basados ​​en clases de tipo, como en Genus y Familia, pueden hacer esto de manera eficiente. Vea nuestro documento PLDI 2015 para más detalles.

@DemiMarie
Creo que "género" == "conjunto de rasgos".

[editar]
Tal vez traits sea ​​una palabra clave mejor.
Podemos ver que cada tipo es también un conjunto de rasgos.

La mayoría de los rasgos se definen para un solo tipo solamente.
Pero un rasgo más complejo puede definir una relación entre dos tipos.

[editar 2]
Supongamos que hay dos conjuntos de rasgos A y B, podemos hacer las siguientes operaciones:

A + B: union set
A - B: difference set
A & B: intersection set

El conjunto de características de un tipo de argumento debe ser un superconjunto del género de parámetros correspondiente (un conjunto de características).
El conjunto de rasgos de un tipo de resultado debe ser un subconjunto del género de resultado correspondiente (un conjunto de rasgos).

(EN MI HUMILDE OPINIÓN)

Aún así, creo que volver a enlazar Type Aliases es el camino a seguir, para agregar genéricos a Go. No necesita un gran cambio en el idioma. Los paquetes que se generalizan de esta manera aún se pueden usar en Go 1.x. Y no hay necesidad de agregar restricciones porque es posible hacerlo configurando el tipo predeterminado para el alias de tipo, a algo que ya cumpla con esas restricciones. Y el aspecto más importante de la revinculación de alias de tipo es que los tipos compuestos integrados (cortes, mapas y canales) no necesitan cambiarse ni generalizarse.

@dc0d

¿Cómo deberían los alias de tipo reemplazar a los genéricos?

@sighoya Rebinding Type Aliases puede reemplazar genéricos (no solo escribir alias). Supongamos que un paquete introduce algunos alias de tipo de nivel de paquete como:

package likedlist

type T = interface{}

type LinkedList struct {
    // ...
}

Si se proporciona Type Alias ​​Rebinding (y funciones de compilación), entonces es posible usar este paquete para crear listas vinculadas para diferentes tipos concretos, en lugar de una interfaz vacía:

package main

import (
    "likedlist"
)

type intLL = likedlist.LinkedList(likedlist.T = int)
type stringLL = likedlist.LinkedList(likedlist.T = string)

func main() {}

Si usamos alias como tal, la siguiente forma es más limpia.

// pkg.go
package pkg

type ListNode struct {
    prev, next *ListNode
    element    ?Element
}

func Add(x, y ?T) ?T {
    return x+y
}



// main.go
package main

import "pkg"

type intList = pkg.ListNode[Element=int]
func stringAdd = pkg.Add[T=string]

func main() {
}

@dc0d y ¿cómo se implementaría exactamente eso? El código es agradable, pero no dice nada sobre cómo funciona realmente por dentro. Y, mirando la historia de las propuestas de genéricos, para Go es muy importante, no solo cómo se ve y se siente.

@dotaheor Eso es incompatible con Go 1.x.

@creker Implementé una herramienta (llamada goreuse ) que usa esta técnica para generar código y nació como un concepto para Type Alias ​​Rebinding.

Se puede encontrar aquí . Hay un video de 15 minutos que explica la herramienta.

@dc0d , por lo que funciona como plantillas de C++ que generan implementaciones especializadas. No creo que sea aceptado ya que el equipo de Go (y, francamente, yo y muchas otras personas aquí) parece estar en contra de algo similar a las plantillas de C++. Aumenta los binarios, ralentiza la compilación, posiblemente no podría producir errores significativos. Y, además de eso, no es compatible con los paquetes solo binarios que admite Go. Es por eso que C++ optó por escribir plantillas en archivos de encabezado.

@creker

por lo que funciona como plantillas de C++ generando implementaciones especializadas para cada tipo utilizado.

No lo sé (Han pasado unos 16 años desde que escribí C++). Pero a partir de su explicación parece ser el caso. Sin embargo, no estoy seguro de si o cómo son iguales.

No creo que sea aceptado ya que el equipo de Go (y, francamente, yo y muchas otras personas aquí) parece estar en contra de algo similar a las plantillas de C++.

Seguro que todos aquí tienen buenas razones para sus preferencias basadas en sus prioridades. Lo primero en mi lista es la compatibilidad con Go 1.x.

Aumenta los binarios,

Que podría.

ralentiza la compilación,

Lo dudo mucho (ya que se puede experimentar con goreuse ).

Y, además de eso, no es compatible con los paquetes solo binarios que admite Go.

No estoy seguro. ¿Otras formas de implementar genéricos respaldan esto?

posiblemente no sería capaz de producir errores significativos.

Esto podría ser un poco problemático. Todavía sucede en tiempo de compilación y puede compensarse empleando algunas herramientas, en gran medida. Además, si el alias de tipo que actúa como parámetro de tipo para el paquete es una interfaz, simplemente se puede comprobar que se puede asignar desde el tipo concreto proporcionado. Aunque el problema para tipos primitivos como int y string y estructuras permanece.

@dc0d

Pienso un poco en ello.
Además de eso, se establece internamente en las interfaces, la 'T' en su ejemplo

type T=interface{}

se trata como una variable de tipo mutable, pero debe ser un alias para un tipo específico, es decir, una referencia constante a un tipo.
Lo que quiere es T Type, pero esto implicaría la introducción de genéricos.

@sighoya No estoy seguro de haber entendido lo que dijiste.

Se establece internamente en las interfaces.

No es verdad. Como se describe en mi comentario original, es posible usar tipos específicos que cumplan con una restricción. Por ejemplo, el alias de tipo de parámetro de tipo se puede declarar como:

type T = int

Y solo los tipos que tienen el operador + (o - o * ; depende de si ese operador se usa en el cuerpo del paquete) se pueden usar como valor de tipo que se encuentra en ese parámetro de tipo.

Por lo tanto, no son solo las interfaces las que se pueden usar como marcador de posición de parámetros de tipo.

pero esto implicaría la introducción de genéricos.

Esta _es_ una forma de introducir/implementar genéricos en el propio lenguaje Go.

@dc0d

Para proporcionar polimorfismo, usará la interfaz{} ya que esto permite establecer T en cualquier tipo más adelante.

Establecer 'tipo T = Int' no ganaría mucho.

Si diría que el 'tipo T' no está declarado/indefinido primero, lo que se puede configurar más tarde, entonces tiene algo como genéricos.

El problema con esto es que 'T' contiene un módulo/paquete amplio y no es local para ninguna función o estructura (está bien, tal vez una declaración de tipo anidada en una estructura a la que se puede acceder desde el exterior).

¿Por qué no escribir en su lugar?:

fun<type T>(t T)

o

fun[type T](t T)

Además, necesitamos alguna maquinaria de inferencia de tipos para deducir los tipos correctos al llamar a una función o estructura genérica sin especialización de parámetros de tipo al principio.

@dc0d escribió

Y solo los tipos que tienen el operador + (o - o *; depende de si ese operador se usa en el cuerpo del paquete) se pueden usar como un valor de tipo que se encuentra en ese parámetro de tipo.

¿Puedes dar más detalles sobre esto?

@sighoya

Para proporcionar polimorfismo, usará la interfaz{} ya que esto permite establecer T en cualquier tipo más adelante.

El polimorfismo no se logra al tener tipos compatibles, al volver a vincular los alias de tipo. La única restricción real es el cuerpo del paquete genérico. Tienen que ser compatibles mecánicamente.

¿Puedes dar más detalles sobre esto?

Por ejemplo, si un alias de tipo de parámetro de nivel de paquete se define como:

package genericadd

type T = int

func Add(a, b T) T { return a + b }

Luego, prácticamente todos los tipos numéricos se pueden asignar a T , como:

package main

import (
    "genericadd"
)

var add = genericadd.Add(
    T = float64
)

func main() {
    var (
        a, b float64
    )

    println(add(a, b))
}

@dc0d

Sin embargo, no estoy seguro de si o cómo son iguales.

Son iguales en el sentido de que funcionan de manera bastante idéntica por lo que veo. Para cada plantilla de clase, el compilador de creación de instancias generaría una implementación única si es la primera vez que ve el uso de la combinación particular de plantilla de clase y su lista de parámetros. Eso aumenta el tamaño binario ya que ahora tiene múltiples implementaciones de la misma plantilla de clase. Ralentiza la compilación, ya que el compilador ahora necesitaría generar estas implementaciones y realizar todo tipo de comprobaciones. En el caso de C++, el aumento del tiempo de compilación podría ser enorme. Sus ejemplos de juguetes son rápidos, pero también lo son los de C++.

No estoy seguro. ¿Otras formas de implementar genéricos respaldan esto?

Otros idiomas no tienen problema con eso. En particular, C# como el más familiar para mí. Pero utiliza la generación de código en tiempo de ejecución que el equipo de Go descarta por completo. Java también funciona pero su implementación no es la mejor, por decir lo menos. Algunas de las propuestas de ianlancetaylor podrían manejar solo paquetes binarios por lo que entiendo.

Lo único que no entiendo es si los paquetes solo binarios deben ser compatibles. No veo que se mencionen explícitamente en las propuestas. Realmente no me importan, pero aún así, es una característica del lenguaje.

Solo para probar mi comprensión... considere este repositorio de algoritmos de copiar/pegar [ aquí ]. A menos que desee usar "int", el código no se puede usar directamente. Debe ser copiado y pegado y modificado para que funcione. Y por modificaciones, me refiero a que cada instancia de "int" debe cambiarse al tipo que realmente necesite.

El enfoque de alias de tipo haría las modificaciones una vez, digamos T, e insertaría una línea "type T int". Luego, el compilador necesitaría volver a vincular T a otra cosa, digamos float64.

Por lo tanto:
a) Yo diría que no habría ralentización del compilador a menos que realmente usara esta técnica. Así que es tu elección.
b) Dadas las nuevas cosas de vgo, donde se pueden usar múltiples versiones del mismo código... lo que significa que debe haber algún método para esconder las fuentes usadas fuera de la vista, entonces seguramente el compilador puede realizar un seguimiento de si dos Se utilizan usos de la misma reencuadernación y se evita la duplicación. Así que creo que el código hinchado sería lo mismo que las técnicas actuales de copiar/pegar.

Me parece que entre los alias de tipo y el próximo vgo, las bases para este enfoque de los genéricos están casi completas...

Hay algunas "incógnitas" enumeradas en la propuesta [ aquí ]. Así que sería bueno desarrollarlo un poco más.

@mandolyte puede agregar otro nivel de direccionamiento indirecto envolviendo tipos especializados en algún contenedor general. De esa manera, su implementación puede permanecer igual. El compilador entonces hará toda la magia. Creo que la propuesta de parámetros de tipo de Ian funciona de esa manera.

Creo que el usuario necesita elegir entre el borrado de tipos y la monomorfización.
Esta última es la razón por la cual Rust proporciona abstracciones de costo cero. Ir debería también.

El lunes 9 de abril de 2018 a las 8:32 a. m. Antonenko Artem [email protected]
escribió:

@mandolyte https://github.com/mandolyte puedes agregar otro nivel de
indirectamente envolviendo tipos especializados en algún contenedor general. Ese
forma en que su implementación puede permanecer igual. El compilador entonces hará todo
la magia. Creo que la propuesta de parámetros de tipo de Ian funciona de esa manera.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/15292#issuecomment-379735199 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AGGWB1v9h5kWmuHCBuoewTTSX751OHgrks5tm1TsgaJpZM4IG-xv
.

Me parece que hay una confusión comprensible en esta discusión sobre la compensación entre modularidad y rendimiento. La técnica de C ++ de volver a escribir y crear instancias de código genérico en cada tipo para el que se utiliza es mala para la modularidad, mala para las distribuciones binarias y, debido a la sobrecarga de código, mala para el rendimiento. Lo bueno de ese enfoque es que especializa automáticamente el código generado en los tipos que se usan, lo que es particularmente útil cuando los tipos que se usan son tipos primitivos como int . Java traduce código genérico de manera homogénea, pero paga un precio en rendimiento, particularmente cuando el código usa el tipo T[] .

Afortunadamente, hay un par de formas de abordar esto sin la falta de modularidad de C++ y sin la generación completa de código en tiempo de ejecución:

  1. Genere instanciaciones especializadas para tipos primitivos. Esto podría hacerse automáticamente o por directiva del programador. Se necesita algo de envío para acceder a la instanciación correcta, pero se puede incorporar al envío que ya se necesita mediante una traducción homogénea. Esto funcionaría de manera similar a C#, pero no requiere la generación completa de código en tiempo de ejecución; un poco de soporte adicional podría ser deseable en el tiempo de ejecución para configurar las tablas de envío a medida que se carga el código.
  2. Use una implementación genérica única en la que una matriz de T se represente realmente como una matriz de un tipo primitivo cuando se crea una instancia de T como un tipo primitivo. Este enfoque, que usamos en PolyJ, Genus y Familia, mejora en gran medida el rendimiento en relación con el enfoque de Java, aunque no es tan rápido como una implementación totalmente especializada.

@dc0d

El polimorfismo no se logra al tener tipos compatibles, al volver a vincular los alias de tipo. La única restricción real es el cuerpo del paquete genérico. Tienen que ser compatibles mecánicamente.

Escriba los alias de forma incorrecta, porque debería ser una referencia constante.
Es mejor escribir 'Tipo T' directamente y luego verá que usa genéricos.

Por qué desea usar una variable de tipo global 'T' para todo el paquete/módulo, las variables de tipo local en <> o [] son ​​más modulares.

@creker

En particular, C# como el más familiar para mí. Pero utiliza la generación de código en tiempo de ejecución que el equipo de Go descarta por completo.

Para tipos de referencia, pero no para tipos de valor.

@DemiMarie

Creo que el usuario necesita elegir entre el borrado de tipos y la monomorfización.
Esta última es la razón por la cual Rust proporciona abstracciones de costo cero. Ir debería también.

"Type Erasure" es ambiguo, supondré que te refieres a Type Parameter Erasure, lo que proporciona Java que tampoco es del todo cierto.
Java tiene monomorfización, pero monomorfiza (semi) constantemente hasta el límite superior en la restricción genérica que es principalmente Objeto.
Para proporcionar métodos y campos de otros tipos, el límite superior se convierte internamente en su tipo apropiado, lo cual es bastante feo.
Si se acepta el proyecto Valhalla , las cosas cambiarán para los tipos de valor, pero lamentablemente no para los tipos de referencia.

Go no tiene que seguir el estilo de Java porque:

"La compatibilidad binaria para paquetes compilados no está garantizada entre versiones"

mientras que esto no es posible en Java.

Me parece que hay una confusión comprensible en esta discusión sobre la compensación entre modularidad y rendimiento. La técnica de C ++ de volver a escribir y crear instancias de código genérico en cada tipo para el que se utiliza es mala para la modularidad, mala para las distribuciones binarias y, debido a la sobrecarga de código, mala para el rendimiento.

¿De qué tipo de actuación estás hablando aquí?

Si por "inflación de código" y "rendimiento" quiere decir "tamaño binario" y "presión de caché de instrucciones", entonces el problema es bastante sencillo de resolver: siempre que no retenga en exceso la información de depuración para cada especialización, puede colapsar funciones con los mismos cuerpos en la misma función en tiempo de enlace (el llamado "modelo de Borland" ). Eso maneja trivialmente especializaciones para tipos primitivos y tipos sin llamadas a métodos no triviales.

Si por "inflación de código" y "rendimiento" te refieres a "tamaño de entrada del enlazador" y "tiempo de enlace", entonces el problema también es bastante sencillo, si puedes hacer ciertas suposiciones (razonables) sobre tu sistema de compilación. En lugar de emitir cada especialización en cada unidad de compilación, puede emitir una lista de las especializaciones necesarias y hacer que el sistema de compilación cree instancias de cada especialización única exactamente una vez antes de vincularlas (el "modelo Cfront"). IIRC, este es uno de los problemas que los módulos de C++ intentan abordar.

Entonces, a menos que se refiera a un tercer tipo de "inflación de código" y "rendimiento" que me he perdido, parece que está hablando de un problema con la implementación, no con la especificación: _siempre que la implementación no retenga demasiado la depuración información,_ los problemas de rendimiento son bastante sencillos de abordar.


El problema más grande para Go es que, si no tenemos cuidado, es posible usar aserciones de tipo o reflexión para producir una instancia novedosa de un tipo parametrizado en tiempo de ejecución, que no requiere mucha inteligencia de implementación, salvo un todo costoso. ‐análisis del programa — puede arreglar.

De hecho, eso es una falla de la modularidad, pero no tiene nada que ver con la sobrecarga de código: en cambio, proviene del hecho de que los tipos de funciones (y métodos) de Go no capturan un conjunto suficientemente completo de restricciones en sus argumentos.

@sighoya

Para tipos de referencia, pero no para tipos de valor.

Por lo que he leído, C# JIT se especializa en tiempo de ejecución para cada tipo de valor y una vez para todos los tipos de referencia. No hay especialización en tiempo de compilación (tiempo IL). Es por eso que el enfoque de C# se ignora por completo: el equipo de Go no quiere depender de la generación de código en tiempo de ejecución, ya que limita las plataformas en las que se puede ejecutar Go. En particular, en iOS no está permitido generar código en tiempo de ejecución. Funciona y de hecho hice algo, pero Apple no lo permite en la AppStore.

¿Cómo lo hiciste?

El lunes 9 de abril de 2018 a las 15:41 Antonenko Artem [email protected]
escribió:

@sighoya https://github.com/sighoya

Para tipos de referencia, pero no para tipos de valor.

Por lo que he leído, C# JIT se especializa en tiempo de ejecución para cada valor
tipo y una vez para todos los tipos de referencia. No hay tiempo de compilación
especialización. Es por eso que el enfoque de C # se ignora por completo: vaya equipo
no quiere depender de la generación de código en tiempo de ejecución, ya que limita las plataformas Go
puede continuar. En particular, en iOS no está permitido generar código
en tiempo de ejecución. Funciona y de hecho hice algo, pero Apple no lo permite.
en la AppStore.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/15292#issuecomment-379870005 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AGGWB-tslGeUSGXl2ZlEDLf0dCATUaYvks5tm7lvgaJpZM4IG-xv
.

@DemiMarie lanzó mi antiguo código de investigación solo para estar seguro (esa investigación se abandonó por otros motivos). Una vez más, el depurador me engañó. Asigno una página, le escribo algunas instrucciones, la protejo con PROT_EXEC y salto a ella. Bajo el depurador funciona. Sin la aplicación del depurador, SIGKILLed con el mensaje CODESIGN en el registro de fallas, como se esperaba. Por lo tanto, no funciona incluso sin AppStore. Un argumento aún más fuerte contra la generación de código en tiempo de ejecución si iOS es importante para Go.

Primero, sería útil reflexionar una vez más sobre las 5 reglas de programación de Rob Pike .

Segundo (en mi humilde opinión):

Acerca de la compilación lenta y el tamaño binario, ¿cuántos tipos genéricos se usan en los tipos comunes de aplicaciones que se desarrollan con Go (_n suele ser pequeño_ de la regla 3)? A menos que el problema necesite un alto nivel de cardinalidad en conceptos concretos (gran número de tipos), esa sobrecarga puede pasarse por alto. Incluso entonces diría que algo anda mal con ese enfoque. Al implementar un sistema de comercio electrónico, nadie define un tipo separado para cada tipo de producto y sus variaciones y quizás las posibles personalizaciones.

La verbosidad es una buena forma de simplicidad y familiaridad (por ejemplo, en la sintaxis) que hace que las cosas sean más obvias y limpias. Si bien dudo que el código hinchado sea mayor usando Type Alias ​​Rebinding, me gusta la sintaxis Go-ish familiar y la evidente verbosidad que la acompaña. Uno de los objetivos de Go es que sea fácil de leer (aunque personalmente también lo encuentro relativamente fácil y agradable de escribir).

No entiendo cómo puede dañar el rendimiento porque en tiempo de ejecución, solo se utilizan tipos delimitados concretos que se generaron en tiempo de compilación. No hay sobrecarga de tiempo de ejecución.

La única preocupación con Type Alias ​​Rebinding que veo, podría ser la distribución binaria.

El daño al rendimiento de @dc0d generalmente significa llenar el caché de instrucciones debido a las diferentes implementaciones de las plantillas de clase. Cómo se relaciona exactamente con el rendimiento real es una pregunta abierta, no conozco ningún punto de referencia, pero teóricamente es un problema.

En cuanto al tamaño binario. Es otro problema teórico que la gente suele mencionar (como lo hice antes), pero cómo sufrirá el código real es, nuevamente, una pregunta abierta. Por ejemplo, creo que la especialización para todos los tipos de punteros e interfaces podría ser la misma. Pero la especialización para todos los tipos de valores sería única. Y eso también incluye estructuras. El uso de contenedores genéricos para almacenarlos es común y provocaría un aumento significativo del código, ya que las implementaciones de contenedores genéricos no son pequeñas.

La única preocupación con Type Alias ​​Rebinding que veo, podría ser la distribución binaria.

Aquí todavía no estoy seguro. ¿La propuesta de genéricos tiene que admitir paquetes solo binarios o simplemente podríamos mencionar que los paquetes solo binarios no son compatibles con genéricos? Sería mucho más fácil, eso seguro.

Como se mencionó anteriormente, si uno no necesita soportar la depuración, uno
puede combinar instancias de plantillas idénticas.

El martes 10 de abril de 2018 a las 5:46 Kaveh Shahbazian [email protected]
escribió:

Primero, sería útil reflexionar sobre las 5 reglas de programación de Rob Pike
https://users.ece.utexas.edu/%7Eadnan/pike.html una vez más.

Segundo (en mi humilde opinión):

Acerca de la compilación lenta y el tamaño binario, ¿cuántos tipos genéricos se utilizan en
tipos comunes de aplicaciones que se están desarrollando usando Go ( n esgeneralmente pequeño de la Regla 3)? A menos que el problema requiera un alto nivel de
cardinalidad en conceptos concretos (alto número de tipos) que la sobrecarga puede
ser pasado por alto Incluso entonces diría que algo anda mal con eso
Acercarse. Al implementar un sistema de comercio electrónico, nadie define un
tipo para cada tipo de producto y sus variaciones y tal vez la posible
personalizaciones

La verbosidad es una buena forma de simplicidad y familiaridad (por ejemplo, en
sintaxis) que hace las cosas más obvias y limpias. Mientras dudo que
la hinchazón del código sería mayor usando Type Alias ​​Rebinding, me gusta el
sintaxis Go-ish familiar y la obvia verbosidad que la acompaña. Uno de
los objetivos de Go es que sea fácil de leer (aunque personalmente lo encuentro
relativamente fácil y agradable de escribir también).

No entiendo cómo puede dañar el rendimiento porque en tiempo de ejecución, solo
Se están utilizando tipos acotados concretos que se generaron en
tiempo de compilación. No hay sobrecarga de tiempo de ejecución.

La única preocupación con Type Alias ​​Rebinding que veo, podría ser el binario
distribución.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/15292#issuecomment-380040032 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AGGWB6aDfoHz2wbsmu8mCGEt652G_VE9ks5tnH9xgaJpZM4IG-xv
.

Las instanciaciones ni siquiera necesitan ser "idénticas" en el sentido de "usar los mismos argumentos", o incluso "usar argumentos con el mismo tipo subyacente". Solo necesitan estar lo suficientemente cerca como para generar el mismo código generado. (Para Go, eso también implica “las mismas máscaras de puntero”).

@creker

Por lo que he leído, C# JIT se especializa en tiempo de ejecución para cada tipo de valor y una vez para todos los tipos de referencia. No hay especialización en tiempo de compilación (tiempo IL).

Bueno, esto a veces es un poco complicado porque su código de bytes se interpreta justo antes de que se ejecute el código, por lo que la generación de código se realiza antes de ejecutar el programa pero después de la compilación, por lo que tiene razón en el sentido de la máquina virtual que se ejecuta mientras se ejecuta el código. es generado.

Creo que el sistema genérico de C# estaría bien si, en cambio, generamos código en tiempo de compilación.
La generación de código en tiempo de ejecución en el sentido de c# no es posible con go, porque go no es una máquina virtual.

@dc0d

La única preocupación con Type Alias ​​Rebinding que veo, podría ser la distribución binaria.

¿Puedes elaborar un poco?

@sighoya Mi error; No quise decir distribución binaria sino paquetes binarios, que personalmente no tengo idea de cuán importante es.

@creker ¡ Buen resumen! (MO) A menos que se encuentre una razón de peso, se debe evitar cualquier forma de sobrecargar las construcciones del lenguaje Go. Una de las razones para utilizar Type Alias ​​Rebinding es evitar la sobrecarga de tipos compuestos integrados, como sectores o mapas.

La verbosidad es una buena forma de simplicidad y familiaridad (por ejemplo, en la sintaxis) que hace que las cosas sean más obvias y limpias. Si bien dudo que el código hinchado sea mayor usando Type Alias ​​Rebinding, me gusta la sintaxis Go-ish familiar y la evidente verbosidad que la acompaña. Uno de los objetivos de Go es que sea fácil de leer (aunque personalmente también lo encuentro relativamente fácil y agradable de escribir).

No estoy de acuerdo con esta noción. Su propuesta obligará a los usuarios a hacer lo más difícil que conoce cualquier programador: nombrar cosas. Así que terminaremos con un código plagado de notación húngara, que no solo se ve mal, sino que es innecesariamente detallado y provoca tartamudeos. Además, otras propuestas también incorporan una sintaxis go-ish y, al mismo tiempo, no tienen estos problemas.

Hay tres categorías de nombres que tenemos que idear a diario:

  • Para entidades de dominio/lógica
  • Tipos de datos/lógica del flujo de trabajo del programa
  • Servicios/Tipos de datos de interfaz/Lógica

¿Cuántas veces un programador había logrado evitar nombrar algo en su código?

Difícil o no, debe hacerse a diario. Y la mayoría de sus obstáculos provienen de la incompetencia en la estructuración de una base de código, no de las dificultades del proceso de denominación en sí. Esa cita, al menos en su forma actual, ha hecho un gran flaco favor al mundo de la programación hasta ahora. Simplemente trata de enfatizar la importancia de nombrar. Porque nos comunicamos a través de nombres en nuestro código.

Y los nombres se vuelven mucho más poderosos cuando acompañan una práctica de estructuración de código; tanto en términos de diseño de código (un archivo, estructura de directorio, paquetes/módulos) como de prácticas (patrones de diseño, abstracciones de servicios, como REST, administración de recursos, programación simultánea, acceso al disco duro, rendimiento/latencia).

En cuanto a la sintaxis y la verbosidad, prefiero la verbosidad sobre la concisión inteligente (al menos en el contexto de Go); de nuevo, Go está destinado a ser fácil de leer, no necesariamente fácil de escribir (lo cual, curiosamente, también me parece bueno en eso) .

Leí muchos informes de experiencias y propuestas sobre por qué y cómo implementar genéricos en Go.

¿Te importa si trato de implementarlos en mi Go interpreter gomacro ?

Tengo algo de experiencia en el tema, ya que he agregado genéricos a dos idiomas en el pasado.

  1. un lenguaje ahora abandonado que creé cuando era ingenuo :) Se transpiló al código fuente C
  2. Common Lisp con mi biblioteca cl-parametric-types : también admite especializaciones parciales y completas de tipos y funciones genéricos

@cosmos72 sería un buen informe de experiencia ver un prototipo de una técnica que preserva la seguridad de tipos.

Acabo de empezar a trabajar en ello. Puede seguir el progreso en https://github.com/cosmos72/gomacro/tree/generics-v1

Por el momento, estoy comenzando con una combinación (ligeramente modificada) de la tercera y cuarta propuesta de Ian que se enumeran en https://github.com/golang/proposal/blob/master/design/15292-generics.md#Proposal

@cosmos72 Hay un resumen de las propuestas en el siguiente enlace. ¿Tu mezcla es una de ellas?
https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4

He leído ese documento, resume muchos enfoques diferentes a los genéricos por varios lenguajes de programación.

Por el momento, voy hacia la técnica de "especialización de tipo" utilizada por C++, Rust y otros, posiblemente con un poco de "ámbitos de plantilla parametrizados" porque la sintaxis más general de Go para nuevos tipos es type ( Foo ...; Bar ...) y estoy ampliando a template[T1,T2...] type ( Foo ...; Bar ...) .
Además, mantengo la puerta abierta para la "especialización restringida".

También me gustaría implementar la "especialización de funciones polimórficas", es decir, hacer arreglos para que la especialización sea inferida automáticamente por el idioma en el sitio de la llamada si el programador no lo especifica, pero supongo que puede ser algo complejo de implementar. Veremos.

La combinación a la que me refería está entre https://github.com/golang/proposal/blob/master/design/15292/2013-10-gen.md y https://github.com/golang/proposal/blob/ master/design/15292/2013-12-type-params.md

Actualización: para evitar enviar spam a este problema oficial de Go más allá del anuncio inicial, probablemente sea mejor continuar la discusión específica de gomacro en el número 24 de gomacro: agregar genéricos

Actualización 2: primeras funciones de plantilla compiladas y ejecutadas con éxito. Consulte https://github.com/cosmos72/gomacro/tree/generics-v1

Solo para que conste, es posible reformular mi opinión (sobre genéricos y Type Alias ​​Rebinding):

Los genéricos deben agregarse como una característica del compilador (generación de código, plantillas, etc.), no como una característica del lenguaje (entrometiéndose con el sistema de tipos de Go en todos los niveles).

@dc0d
Pero, ¿las plantillas de C++ no son una característica del compilador y del lenguaje?

@sighoya La última vez que escribí C++ profesionalmente fue alrededor de 2001. Así que podría estar equivocado. Pero suponiendo que las implicaciones de la denominación sean precisas, la parte de la "plantilla", sí (o más bien no); podría ser una característica del compilador (y no una característica del lenguaje), acompañada de algunas construcciones del lenguaje, que muy probablemente no estén sobrecargando ninguna construcción del lenguaje que esté involucrada en el sistema de tipos.

Apoyo a @dc0d. Si lo considera, esta característica no sería más que un generador de código integrado.

Sí: el tamaño binario puede aumentar, pero ahora mismo usamos generadores de código, que son más o menos lo mismo pero como una característica externa. Si tengo que crear mi plantilla como:

type BinaryTreeOfStrings struct {
    left, right *BinaryTreeOfStrings;
    string content;
}

// Its methods here

type BinaryTreeOfBigInts struct {
    left, right *BinaryTreeOfBigInts;
    uint64 content;
}

// AGAIN the same methods but different type

... En serio, me gustaría que, en lugar de copiar y pegar o usar una herramienta externa, esta función se convierta en parte del compilador.

Tenga en cuenta:

  • Sí, el código final estaría duplicado. Justo como si usáramos un generador. Y el binario sería más grande.
  • Sí, la idea no es original, sino prestada de C++.
  • Sí, funciones de MyTypeno involucrar nada con el tipo T (directa o indirectamente) también se repetiría. Eso podría optimizarse (por ejemplo, los métodos que hacen referencia a algo del tipo T que no sea el puntero al objeto que recibe el mensaje, se generarán para cada T ; métodos que contienen invocaciones a métodos que se generará para cada T , también se generará para cada T , recursivamente, mientras que los métodos en los que su única referencia a T es *T en el receptor, y otros métodos que llamen solo a esos métodos seguros y que satisfagan los mismos criterios, podrían hacerse solo una vez). De todos modos, en mi opinión, este punto es grande y menos importante: estaría muy feliz incluso si esta optimización no existe.
  • Los argumentos de tipo deben ser explícitos en mi opinión. Especialmente cuando un objeto satisface interfaces potencialmente infinitas. De nuevo: un generador de código.

Hasta ahora en mi comentario, mi propuesta es implementarlo tal como está: como un generador de código compatible con el compilador , en lugar de una herramienta externa.

Sería desafortunado que Go siguiera la ruta de C++. Mucha gente ve el enfoque de C++ como un desastre que ha puesto a los programadores en contra de la idea de los genéricos: dificultad de depuración, falta de modularidad, exceso de código. Todas las soluciones de "generador de código" son realmente solo sustitución de macros: si esa es la forma en que desea escribir código, ¿por qué necesitamos soporte de compilador?

@andrewcmyers Tenía esta propuesta Type Alias ​​Rebinding en la que solo escribimos paquetes normales y en lugar de usar interface{} explícitamente, solo lo usamos como type T = interface{} como un parámetro genérico a nivel de paquete. Y eso es todo.

  • Lo depuramos como un paquete normal: es un código real, no una criatura de vida media intermedia.
  • No hay necesidad de entrometerse con el sistema de tipo Go en todos los niveles; piense solo en la asignabilidad.
  • es explícito Sin mojo oculto. Por supuesto, uno podría encontrar que no poder encadenar llamadas genéricas sin problemas, es un inconveniente. ¡Lo veo como una atracción! Cambio de tipo en dos convocatorias consecutivas, en una declaración no es Goish (OMI).
  • Y lo mejor de todo es que es retrocompatible con la serie Go 1.x (x >= 8).

Si bien la idea no es nueva, la forma en que Go permite implementarla es pragmática y clara.

Bonificación adicional: no hay sobrecarga de operadores en Go. Pero al definir el valor predeterminado del alias de tipo como (por ejemplo) type T = int , no los únicos tipos válidos que se pueden usar para personalizar este paquete genérico, son tipos numéricos que tienen una implementación interna para + operador

Además, el parámetro de tipo de alias se puede forzar para cumplir con más de una interfaz simplemente agregando algunos tipos de validador y declaraciones.

Ahora, eso sería muy feo usando cualquier notación explícita para un tipo genérico que tiene un parámetro que implementa las interfaces Error y Stringer y también es un tipo numérico que admite el operador + !

en este momento usamos generadores de código, que son más o menos lo mismo pero como una característica externa.

La diferencia es que la forma ampliamente aceptada de generar código (a través go generate ) ocurre en el momento de la confirmación/desarrollo, no en el momento de la compilación. Hacerlo en tiempo de compilación implica que debe permitir la ejecución de código arbitrario en el compilador, las bibliotecas pueden aumentar los tiempos de compilación en órdenes de magnitud y/o tendrá dependencias de compilación separadas (es decir, el código ya no se puede compilar solo con Go herramienta). Me gusta Go por empujar la invocación de la metaprogramación al desarrollador ascendente.

Es decir, como todos los enfoques para resolver estos problemas, este enfoque también tiene desventajas e implica compensaciones. Personalmente, diría que los genéricos reales con soporte en el sistema de tipos no solo son mejores (es decir, tienen un conjunto de funciones más potente) sino que también pueden conservar la ventaja de una compilación predecible y segura.

Leeré todo lo anterior, lo prometo, pero también agregaré un poco: GoLang SDK para Apache Beam parece un ejemplo/muestra bastante brillante de los problemas que el diseñador de bibliotecas tiene que soportar para lograr algo _adecuadamente_ de alto nivel.

Hay al menos dos implementaciones experimentales para los genéricos de Go. A principios de esta semana pasé un tiempo con (1). Me complació descubrir que el impacto en la legibilidad del código fue mínimo. Y encontré que el uso de funciones anónimas para proporcionar pruebas de igualdad funcionó bien; así que estoy convencido de que no es necesaria la sobrecarga del operador. El único problema que encontré fue en el manejo de errores. El modismo común de "return nil,err" no funcionará si el tipo es, por ejemplo, un número entero o una cadena. Hay varias formas de evitar esto, todas con un costo de complejidad. Puede que sea un poco raro, pero me gusta el manejo de errores de Go. Esto me lleva a observar que una solución genérica de Go debe tener una palabra clave universal para el valor cero de un tipo. El compilador simplemente lo reemplazaría con cero para tipos numéricos, una cadena vacía para tipos de cadena y nil para estructuras.

Si bien esta implementación no impuso un enfoque a nivel de paquete, sin duda sería natural hacerlo. Y, por supuesto, esta implementación no abordó todos los detalles técnicos sobre dónde debería ir el código instanciado del compilador (si es que debe ir a algún lugar), cómo funcionarían los depuradores de código, etc.

Fue bastante bueno usar el mismo código de algoritmo para números enteros y algo así como un Punto:

type Point struct {
    x,y int
}

Ver (2) para mis pruebas y observaciones.

(1) https://github.com/albrow/fo; el otro es el mencionado https://github.com/cosmos72/gomacro#generics
(2) https://github.com/mandolyte/fo-experimentos

@mandolyte Puede usar *new(T) para obtener el valor cero de cualquier tipo.

Una construcción de lenguaje como default(T) o zero(T) (la primera es la
en C# IIRC) sería claro, pero OTOH más largo que *new(T) (aunque más
ejecutante).

2018-07-06 9:15 GMT-05:00 Tom Thorogood [email protected] :

@mandolyte https://github.com/mandolyte Puede usar *nuevo(T) para obtener el
valor cero de cualquier tipo.


Estás recibiendo esto porque comentaste.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/15292#issuecomment-403046735 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AlhWhQ5cQwnc3x_XUldyJXCHYzmr6aN3ks5uD3ETgaJpZM4IG-xv
.

--
Esta es una prueba para que las firmas de correo se utilicen en TripleMint

19642 es para discutir un valor cero genérico

@tmthrgd De alguna manera me perdí ese pequeño dato. ¡Gracias!

preludio

Los genéricos tienen que ver con la especialización de construcciones personalizables. Tres categorías de especialización son:

  • Tipos especializados, Type<T> - una _matriz_;
  • Cómputos especializados, F<T>(T) o F<T>(Type<T>) - una _matriz clasificable_;
  • Notación especializada, _LINQ_ por ejemplo - declaraciones select o for en Go;

Por supuesto, hay lenguajes de programación que presentan construcciones aún más genéricas. Pero los lenguajes de programación convencionales como _C++_, _C#_ o _Java_ proporcionan más o menos construcciones de lenguaje limitadas a esta lista.

pensamientos

La primera categoría de tipos/construcciones genéricas debe ser independiente del tipo.

La segunda categoría de tipos/construcciones genéricas necesita _actuar_ sobre una _propiedad_ del parámetro de tipo. Por ejemplo, una _matriz ordenable_ tiene que poder _comparar_ la _propiedad comparable_ de sus elementos. Suponiendo que T.(P) es una propiedad de T y A(T.(P)) es un cálculo/acción que actúa sobre esa propiedad, el (A, .(P)) se puede aplicar a cada elemento individual o ser declarado como un cómputo especializado, pasado al cómputo personalizable original. Un ejemplo del último caso en Go es la interfaz sort.Interface que también tiene la función equivalente separada sort.Reverse .

La tercera categoría de tipos/construcciones genéricas son notaciones de lenguaje _especializadas_ en tipo - parece no ser una cosa de Go _en general_.

preguntas

continuará ...

¡Cualquier comentario más descriptivo que un emoji es bienvenido!

@dc0d Recomendaría estudiar los "Elementos de programación" de Sepanov antes de intentar definir Genéricos. El TL; DR es que escribimos código concreto para empezar, digamos un algoritmo que ordena una matriz. Luego agregamos otros tipos de colección como un Btree, etc. Notamos que estamos escribiendo muchas copias del algoritmo de clasificación que son esencialmente iguales, por lo que definimos algún concepto, digamos 'ordenable'. Ahora queremos categorizar los algoritmos de ordenación, tal vez por el patrón de acceso que requieren, digamos solo reenvío, paso único (un flujo), reenvío solo paso múltiple (una lista con un solo enlace), bidireccional (una lista con doble enlace), acceso aleatorio (un formación). Cuando agregamos un nuevo tipo de colección, solo necesitamos indicar en qué categoría de "coordenada" se encuentra para obtener acceso a todos los algoritmos de clasificación relevantes. Estas categorías de algoritmos se parecen mucho a las interfaces 'Go'. Buscaría ampliar las interfaces en Go para admitir múltiples parámetros de tipo y tipos abstractos/asociados. No creo que las funciones necesiten una parametrización de tipo ad-hoc.

@dc0d Como un intento de dividir los genéricos en partes componentes, no había considerado 3, "notación especializada", como su propia parte separada antes. Tal vez podría caracterizarse como la definición de DSL mediante la utilización de restricciones de tipo.

Podría argumentar que su 1 y 2 son "estructuras de datos" y "algoritmos", respectivamente. Con esa terminología queda un poco más claro por qué podría ser difícil separarlos limpiamente, ya que a menudo dependen mucho unos de otros. Pero sort.Interface es un buen ejemplo de dónde puede trazar una línea entre el almacenamiento y el comportamiento (con un poco de azúcar reciente para hacerlo más agradable), ya que codifica los requisitos Indexable y Comparable en el comportamiento mínimo necesario para implementar el algoritmo de ordenación. con "swap" y "less" (y len). Pero esto parece fallar en estructuras de datos más complicadas como árboles o montones, los cuales actualmente requieren algunas contorsiones para mapear en un comportamiento puro como interfaces Go.

Podría imaginar una adición genérica relativamente pequeña a las interfaces (o de otro modo) que podría permitir que la mayoría de las estructuras de datos y algoritmos de los libros de texto se implementen de manera relativamente limpia y sin contorsiones (como sort.Interface hoy en día), pero que no sea lo suficientemente potente como para diseñar DSL. Si queremos limitarnos a una implementación de genéricos tan restringida cuando nos vamos a tomar la molestia de agregar genéricos es una cuestión diferente.

Las estructuras de coordenadas de @infogulch para árboles binarios son "coordenadas bifurcadas" y existen equivalentes para otros árboles. Sin embargo, también puede proyectar el orden de un árbol a través de uno de tres pedidos, pre-pedido, en orden y post-pedido. Habiendo decidido uno de estos, el árbol se puede abordar como una coordenada bidireccional, y la familia de algoritmos de clasificación definidos en coordenadas bidireccionales sería óptimamente eficiente.

El punto es que clasifica los algoritmos de clasificación por sus patrones de acceso. Solo hay un número finito de algoritmos de clasificación óptimos para cada patrón de acceso. No le importan las estructuras de datos en este punto. Hablar de estructuras más complejas no tiene sentido, queremos categorizar la familia de algoritmos de clasificación, no las estructuras de datos. Independientemente de los datos que tenga, tendrá que usar uno de los algoritmos que existen para clasificarlos, por lo que la pregunta es cuál de las categorizaciones de patrones de acceso a datos disponibles de los algoritmos de clasificación es óptima para las estructuras de datos que tiene.

(EN MI HUMILDE OPINIÓN)

@infogulch

Tal vez podría caracterizarse como la definición de DSL mediante la utilización de restricciones de tipo

Tienes razón. Pero dado que son parte del conjunto de construcciones de lenguaje, en mi opinión, llamarlos DSL sería un poco inexacto.

1 y 2... son a menudo muy dependientes

De nuevo cierto. Pero hay muchos casos en los que es necesario pasar un tipo de contenedor, mientras que el uso real aún no se decide, en ese punto de un programa. Por eso es necesario estudiar el 1 por sí solo.

sort.Interface es un buen ejemplo de dónde puedes trazar una línea entre _almacenamiento_ y _comportamiento_

Bien dicho;

esto parece fallar en estructuras de datos más complicadas

Esa es una de mis preguntas: ¿generalizar el parámetro de tipo y describirlo en términos de restricciones (como List<T> where T:new, IDisposable ) o proporcionar un _protocolo_ generalizado aplicable a todos los elementos (de un conjunto; de cierto tipo)?

@keean

la pregunta es cuál de las categorizaciones de patrones de acceso a datos disponibles de los algoritmos de clasificación es óptima para las estructuras de datos que tiene

Verdadero. Acceder por índice es una _propiedad_ de un segmento (o matriz). Por lo tanto, el primer requisito para un contenedor ordenable (o contenedor _árbol_, cualquiera que sea el algoritmo _árbol_) es proporcionar una utilidad _acceso y mutación (intercambio)_. El segundo requisito es que los elementos deben ser comparables. Esa es la parte confusa (para mí) sobre lo que llama algoritmos: los requisitos deben cumplirse en ambos lados (en el contenedor y en el parámetro de tipo). Ese es el punto No puedo imaginar una implementación pragmática de genéricos en Go. Cada lado del problema se puede describir perfectamente en términos de interfaces. Pero, ¿cómo combinar estos dos en una notación efectiva?

Los algoritmos @dc0d requieren interfaces, las estructuras de datos las proporcionan. Esto es suficiente para una generalidad completa, siempre que las interfaces sean lo suficientemente poderosas. Las interfaces están parametrizadas por tipos, pero necesita variables de tipo.

Tomando el ejemplo de 'sort', 'Ord' es una propiedad del tipo almacenado en el contenedor, no del contenedor en sí. El patrón de acceso es una propiedad del contenedor. Los patrones de acceso simple son 'iteradores', pero ese nombre proviene de C ++, Stepanov prefirió las 'coordenadas' ya que se puede aplicar a contenedores multidimensionales más complejos.

Tratando de definir el ordenamiento, queremos algo como esto:

bubble_sort : forall T U I => T U -> T U requires
   ForwardIterator<T>, Readable<T>, Writable<T>,
   Ord<U>,  ValueType(T) == U, Distance type(T) == I

Nota: no estoy sugiriendo esta notación, solo intento obtener algún otro trabajo relacionado, la cláusula require está en la sintaxis preferida por Stepanov, el tipo de función es de Haskell, cuyas clases de tipos probablemente representan una buena implementación de estos conceptos.

@keean
Tal vez no lo esté entendiendo bien, pero no creo que pueda restringir los algoritmos solo a las interfaces, al menos en la forma en que se definen las interfaces en este momento.
Considere sort.Slice, por ejemplo, estamos interesados ​​en clasificar las porciones, y no veo cómo se podría construir una interfaz que representara todas las porciones.

@urandom abstraes los algoritmos, no las colecciones. Entonces pregunta qué patrones de acceso a datos existen en los algoritmos de "clasificación" y luego los clasifica. Por lo tanto, no importa si el contenedor es un "segmento", no estamos tratando de definir todas las operaciones que desee realizar en un segmento, estamos tratando de determinar los requisitos de un algoritmo y usarlo para definir una interfaz. Un segmento no es especial, es solo un tipo T en el que podemos definir un conjunto de operaciones.

Entonces, las interfaces se relacionan con bibliotecas de algoritmos, y puede definir sus propias interfaces para sus propias estructuras de datos para poder usar esos algoritmos. Las bibliotecas podrían venir con interfaces predefinidas para los tipos integrados.

@keean
Pensé que eso es lo que querías decir. Pero en el contexto de Go, eso probablemente significaría que tendría que haber una revisión significativa de lo que las interfaces pueden definir. Me imagino que varias operaciones integradas, como iteraciones u operadores, deberían exponerse a través de métodos para que cosas como sort.Slice o math.Max se vuelvan genéricas en las interfaces.

Por lo tanto, debe tener soporte para la siguiente interfaz (pseudocódigo):

type [T] OrderedIterator interface {
   Len() int
   ValueAt(i int) *T
}

...
package sort

func [T] Slice(s [T]OrderedIterator, func(i, j int) bool) {
   ...
}

y todas las rebanadas tendrían estos métodos?

@urandom Un iterador no es una abstracción de una colección, sino una abstracción de la referencia/puntero en una colección. Por ejemplo, el iterador directo podría tener un solo método 'sucesor' (a veces 'siguiente'). Ser capaz de acceder a los datos en la ubicación de un iterador no es una propiedad del iterador (de lo contrario, terminará con versiones de iterador de lectura/escritura/mutable). Es mejor definir "referencias" por separado como interfaces de lectura, escritura y mutables:

type T ForwardIterator interface {
   type DistanceType D
   successor(x T) T
}

type T Readable interface {
   type ValueType U 
   source(x T) U
}

Nota: El tipo 'T' no es el sector, sino el tipo del iterador en el sector. Esto podría ser simplemente un puntero simple, si adoptamos el estilo C++ de pasar un iterador de inicio y fin a funciones como ordenar.

Para un iterador de acceso aleatorio, terminaríamos con algo como:

type T RandomIterator interface {
   type DistanceType D
   setPosition(x DistanceType)
}

Entonces, un iterador/coordenada es una abstracción de la referencia a una colección, no la colección en sí. El nombre 'coordenada' expresa esto muy bien, si piensa en el iterador como la coordenada y la colección como el mapa.

¿No estamos vendiendo Ir en corto al no aprovechar los cierres de funciones y las funciones anónimas? Tener funciones/métodos como un tipo de primera clase en Go puede ayudar. Por ejemplo, usando la sintaxis de albrow/fo , un tipo de burbuja podría verse así:

type SortableContainer[C,T] struct {
    Less func(C,T,T) bool
    Swap func(C,int,int)
    Next func(C) (T,bool)
}

func (bs *SortableContainer[C,T]) BubbleSort(container C, e1,e2 T) {    
    swapCount := 1
    var item1, item2 T
    item1, ok1 = bs.Next()
    if !ok1 {return}
    item2, ok2 = bs.Next()
    if !ok2 {return}
    for swapCount > 0 {
        swapCount = 0
        for {
            if Less(item2, item1) { 
                bs.Swap(C,item2,item1)
                swapCount += 1
            }
        }
    }
}

Por favor, pase por alto cualquier error... ¡completamente sin probar!

@mandolyte No estoy seguro de si esto estaba dirigido a mí. Realmente no veo ninguna diferencia entre lo que estaba sugiriendo y su ejemplo, excepto que está usando interfaces de parámetros múltiples, y estaba dando ejemplos usando tipos abstractos/asociados. Para ser claro, creo que necesita interfaces de parámetros múltiples y tipos abstractos/asociados para una generalidad completa, ninguno de los cuales es compatible actualmente con Go.

Sugeriría que sus interfaces sean menos generales que las que propuse, porque vinculan el orden de clasificación, el patrón de acceso y la accesibilidad en la misma interfaz, lo que por supuesto dará como resultado la proliferación de interfaces, por ejemplo, dos órdenes (menos , mayor), tres tipos de acceso (solo lectura, solo escritura, mutable) y cinco patrones de acceso (forward-single-pass, forward-multi-pass, bidireccional, indexado, aleatorio) conducirían a 36 interfaces en comparación con solo 11 si las preocupaciones se mantienen separadas.

Podría definir las interfaces que propongo con interfaces de parámetros múltiples en lugar de tipos abstractos como este:

type I ForwardIterator interface {
   successor(x I) I
}
type R V Readable interface {
   source(x R) V
}
type V Ord interface {
   less(x V, y V) : bool
}

Tenga en cuenta que el único que necesita dos parámetros de tipo es la interfaz de lectura. Sin embargo, perdemos esa capacidad para que un objeto iterador 'contenga' el tipo de los objetos iterados, lo cual es un gran problema ya que ahora tenemos que mover el tipo 'valor' en el sistema de tipos, y tenemos que corregirlo . Esto conduce a una proliferación de parámetros de tipo que no es bueno y aumenta la posibilidad de errores de codificación. También perdemos la capacidad de definir el 'DistanceType' en el iterador, que es el tipo de número más pequeño necesario para contar los elementos de la colección, lo cual es útil para mapear a int8, int16, int32, etc., para dar el tipo que necesita contar elementos sin desbordamiento.

Esto está estrechamente relacionado con el concepto de "dependencia funcional". Si un tipo depende funcionalmente de otro tipo, debe ser un tipo abstracto/asociado. Solo si los dos tipos son independientes deben ser parámetros de tipo separados.

Algunos problemas:

  1. No se puede usar la sintaxis f(x I) actual para interfaces multiparámetro. No me gusta que esta sintaxis confunda las interfaces (que son restricciones en los tipos) con los tipos de todos modos.
  2. Necesitaría una forma de declarar tipos parametrizados.
  3. Tendría que haber una forma de declarar tipos asociados para interfaces con un conjunto dado de parámetros de tipo.

@keean No estoy seguro de entender cómo o por qué el número de interfaces es tan alto. Aquí hay un ejemplo de trabajo completo: https://play.folang.org/p/BZa6BdsfBgZ (basado en rebanadas, no en un contenedor general, por lo tanto, no se necesita el método Next()).

Utiliza solo una estructura de tipo, sin interfaces en absoluto. Tengo que proporcionar todas las funciones y cierres anónimos (¿probablemente ahí es donde está la compensación?). El ejemplo utiliza el mismo algoritmo de ordenación de burbujas para ordenar tanto una porción de enteros como una porción de puntos "(x,y)", donde la distancia desde el origen es la base de la función Less().

En cualquier caso, esperaba mostrar cómo puede ayudar tener funciones en el sistema de tipos.

@mandolyte Creo que entendí mal lo que proponías. Veo de lo que estás hablando es "folang", que ya tiene algunas características de programación funcional agradables agregadas a Go. Lo que ha implementado es básicamente conectar una clase de tipo de parámetros múltiples a mano. Está pasando lo que se conoce como un diccionario de funciones a la función de clasificación. Esto está haciendo explícitamente lo que una interfaz haría implícitamente. Es probable que este tipo de funciones sean necesarias antes de las interfaces multiparámetro y los tipos asociados, pero eventualmente tendrá problemas para pasar todos esos diccionarios. Creo que las interfaces proporcionan un código más limpio y legible.

Ordenar una rebanada es un problema resuelto. Aquí está el código para un segmento quicksort.go implementado usando el lenguaje go-li (golang mejorado) .

func main(){
    var data = []int{5,3,1,8,9}

    Sort(data, func(a *int, b *int) int {
        return *a - *b
    })

    fmt.Println(data)
}

Puedes experimentar con esto en el patio de recreo .

Ejemplo completo que puede pegar en el patio de recreo , porque la importación del paquete de clasificación rápida no funciona en el patio de recreo.

@ go-li, estoy seguro de que puede ordenar una porción, sería un poco pobre si no pudiera. El punto es que, en general, le gustaría poder ordenar cualquier contenedor lineal con el mismo código, de modo que solo tenga que escribir un algoritmo de clasificación una vez, sin importar qué contenedor (estructura de datos) esté clasificando y sin importar cuál el contenido es.

Cuando puede hacer esto, la biblioteca estándar puede proporcionar funciones de clasificación universales, y nadie necesita volver a escribir una. Esto tiene dos beneficios, menos errores, ya que es más difícil de lo que piensa escribir un algoritmo de clasificación correcto, Stepanov usa el ejemplo de que la mayoría de los programadores no pueden definir correctamente el par 'min' y 'max', entonces, ¿qué esperanza tenemos de ser correcto para algoritmos más complejos. El otro beneficio es que cuando solo hay una definición de cada algoritmo de clasificación, cualquier mejora en la claridad o el rendimiento que se pueda realizar beneficia a todos los programas que lo utilizan. Las personas pueden dedicar su tiempo a intentar mejorar el algoritmo común en lugar de tener que escribir uno propio para cada tipo de datos diferente.

@keean
Otra pregunta relacionada con nuestra discusión anterior. No puedo entender cómo se podría definir una función de mapeo que cambie los elementos de un iterable, devolviendo un nuevo tipo iterable concreto cuyos elementos pueden ser de un tipo diferente al original.

E imagino que un usuario de tal función querría que se devolviera un tipo concreto, no otra interfaz.

@urandom Suponiendo que no queremos hacerlo 'en el lugar', lo que sería inseguro, lo que quiere es una función de mapa que tenga un 'iterador de lectura' de un tipo y un 'iterador de escritura' de otro tipo, que se puede definir algo como:

map<I, O, U>(first I, last I, out O, fn U) requires
   ForwardIterator<I>, Readable<I>,
   ForwardIterator<O>, Writable<O>,
   UnaryFunction<U>, Domain(U) == ValueType(I), Codomain(U) == ValueType(O)

Para mayor claridad, "ValueType" es un tipo asociado de las interfaces "Readable" y "Writable", "Domain" y "Codomain" son tipos asociados de la interfaz "UnaryFunction". Obviamente, ayuda mucho si el compilador puede derivar automáticamente las interfaces para tipos de datos como "UnaryFunction". Si bien esto parece un reflejo, no lo es, y todo sucede en tiempo de compilación usando tipos estáticos.

@keean ¿Cómo modelar esas restricciones de lectura y escritura en el contexto de las interfaces actuales de Go?

Quiero decir, cuando tenemos un tipo A y queremos convertirlo al tipo B , la firma de esa UnaryFunction sería func (input A) B (¿verdad?), pero ¿cómo puede eso modelarse usando solo interfaces y cómo se modelaría ese map genérico (o filter , reduce , etc.) para mantener la canalización de tipos?

@ geovanisouza92 Creo que "Familias de tipos" funcionaría bien, ya que pueden implementarse como un mecanismo ortogonal en el sistema de tipos y luego integrarse en la sintaxis de las interfaces como se hace en Haskell.

Una familia de tipos es como una función restringida en tipos (un mapeo). Como las implementaciones de interfaz se seleccionan por tipo, podemos proporcionar un mapeo de tipo para cada implementación.

Entonces si definimos:

ValueType MyIntArrayIterator -> Int

Las funciones son un poco engañosas, pero una función tiene un tipo, por ejemplo:

fn(x : Int) Float

Escribiríamos este tipo:

Int -> Float

Es importante darse cuenta de que -> es solo un constructor de tipo infijo, como '[]' para un Array es un constructor de tipo, podríamos escribir esto fácilmente;

Fn Int Float
Or
Fn<Int, Float>

Dependiendo de nuestra preferencia por la sintaxis de tipos. Ahora podemos ver claramente cómo podemos definir:

Domain  Fn<Int, Float> -> Int
Codomain Fn<Int, Float> -> Float

Ahora, aunque podríamos proporcionar todas estas definiciones a mano, el compilador puede derivarlas fácilmente.

Dadas estas familias de tipos, podemos ver que la definición de mapa que di arriba solo requiere los tipos IO y U para instanciar el genérico, ya que todos los demás tipos dependen funcionalmente de estos. Podemos ver que estos tipos son proporcionados directamente por los argumentos.

Gracias, @kean.

Esto funcionaría bien para funciones integradas/predefinidas. ¿Está diciendo que el mismo concepto se aplicaría para funciones definidas por el usuario o librerías de usuario?

¿Esas "Familias de tipos" se llevarán durante el tiempo de ejecución, en el caso de algún contexto de error?

¿Qué hay de las interfaces vacías, los interruptores de tipo y la reflexión?


EDITAR: Solo tengo curiosidad, no me quejo.

@giovanisouza92 bueno, nadie se ha comprometido a ir a tener genéricos, así que espero escepticismo. Mi enfoque es que si va a hacer genéricos, debe hacerlo bien.

En mi ejemplo, 'mapa' está definido por el usuario. No tiene nada de especial, y dentro de la función simplemente usa los métodos de las interfaces que ha requerido en esos tipos exactamente como lo hace en Go ahora. La única diferencia es que podemos requerir un tipo para satisfacer múltiples interfaces, las interfaces pueden tener múltiples parámetros de tipo (aunque el ejemplo del mapa no usa esto) y también hay tipos asociados (y restricciones en tipos como la igualdad de tipo '==' pero esto es como una igualdad de Prolog y unifica los tipos). Es por eso que existe una sintaxis diferente para especificar las interfaces requeridas por una función. Tenga en cuenta que hay otra diferencia importante:

f(x I, y I) requires ForwardIterator<I>

contra

f(x ForwardIterator, y ForwardIterator)

Tenga en cuenta que hay una diferencia en el último 'x' e 'y' pueden ser tipos diferentes que satisfacen la interfaz ForwardIterator, mientras que en la sintaxis anterior 'x' e 'y' deben ser del mismo tipo (que satisface el iterador directo). Esto es importante para que las funciones no estén limitadas y permite que los tipos concretos se propaguen mucho más durante la compilación.

No creo que cambie nada con respecto a los cambios de tipo y la reflexión, porque solo estamos ampliando el concepto de interfaces. Como go tiene información de tipo de tiempo de ejecución, no se mete en el mismo problema que Haskell y requiere tipos existenciales.

Pensando en Go, el polimorfismo en tiempo de ejecución y las familias de tipos, probablemente querríamos restringir la familia de tipos en sí misma a una interfaz para evitar tener que tratar cada tipo asociado como una interfaz vacía en el tiempo de ejecución, lo que sería lento.

Entonces, a la luz de esos pensamientos, modificaría mi propuesta anterior para que al declarar una interfaz declarara una interfaz/tipo para cada tipo asociado que todas las implementaciones de esa interfaz tendrían que proporcionar un tipo asociado que satisfaga esa interfaz. De esa manera, podemos saber que es seguro llamar a cualquier método desde esa interfaz en los tipos asociados en tiempo de ejecución sin tener que cambiar de tipo desde una interfaz vacía.

@keean
En aras de avanzar en el debate, permítanme aclarar el concepto erróneo que siento que es similar al síndrome no inventado aquí.

El iterador bidireccional (en la sintaxis T func (*T) *[2]*T ) tiene el tipo func (*) *[2]* en la sintaxis go-li. En palabras, toma un puntero a algún tipo y devuelve el puntero a dos punteros al elemento siguiente y anterior del mismo tipo. Es el tipo fundacional concreto fundamental utilizado por una lista doblemente enlazada .

Ahora puedes escribir lo que llamas map, lo que yo llamo la función genérica foreach. ¡No se equivoque, esto funciona no solo sobre la lista enlazada sino sobre cualquier cosa que exponga un iterador bidireccional!

func Foreach(link func(*) *[2]*, list **, direction byte, f func(*)) {

    if nil == *list {
        return
    }

    var end *
    end = *list

    var e *
    e = (*link(*list))[direction]
    f(end)

    for (e != end) && ((*link(e))[direction] != nil) {
        var newe = (*link(e))[direction]
        f(e)
        e = newe
    }
    return
}

El Foreach se puede usar de dos maneras, lo usa con una lambda en una iteración similar a un bucle for sobre elementos de lista o colección.

const forward = 1
const backwards = 0
Foreach(iterator, collection, forward, func(element *element_type){
    // do something with every element
})

O puede usarlo para asignar funcionalmente una función a cada elemento de la colección.

Foreach(iterator, collection, backwards, function_to_be_mapped_on_elements)

Por supuesto, el iterador bidireccional también se puede modelar usando interfaces en go 1.
interface Iterator { Iter() [2]Iterator } Debe modelarlo usando interfaces para envolver ("encuadrar") el tipo subyacente. El usuario del iterador luego escribe afirma el tipo conocido una vez que localiza y desea visitar un elemento de colección específico. Esto es potencialmente inseguro en el tiempo de compilación.

Lo que está describiendo a continuación son las diferencias entre el enfoque heredado y el enfoque basado en genéricos.

func modern(x func  (*) *[2]*, y func  (*) *[2]*){}

este enfoque compila el tipo de tiempo verifica que las dos colecciones tengan el mismo tipo subyacente, en otras palabras, si los iteradores realmente devuelven los mismos tipos concretos

func modern_T_syntax<T>(x func  (*T) *[2]*T, y func  (*T) *[2]*T){}

Igual que el anterior pero usando la T familiar significa escribir sintaxis de marcador de posición

func legacy(x Iterator, y Iterator){}

En este caso, el usuario puede pasar, por ejemplo, una lista enlazada de enteros como x y una lista enlazada flotante como y. Esto podría dar lugar a posibles errores en tiempo de ejecución, pánico u otras decoherencias internas, pero todo depende de lo que haga el legado con los dos iteradores.

Ahora el concepto erróneo. Usted afirma que hacer iteradores y hacer ordenaciones genéricas para ordenar esos iteradores sería el camino a seguir. Eso sería algo realmente pobre de hacer, he aquí por qué

El iterador y la lista enlazada son dos caras de la misma moneda. Prueba: cualquier colección que expone el iterador simplemente se anuncia como una lista enlazada. Digamos que necesitas ordenar eso. ¿Qué hacer?

Obviamente, elimina la lista vinculada de su base de código y la reemplaza con un árbol binario. O si quiere ser elegante, use un árbol de búsqueda equilibrado como avl, rojo-negro, como lo propusieron no sé cuántos años atrás Ian et al. Todavía esto no se ha hecho genéricamente en golang. Ahora ese sería el camino a seguir.

Otra solución es rápidamente en un bucle de tiempo O(N) sobre el iterador, recopilar los punteros a los elementos en una porción de punteros genéricos, denotados []*T y clasificar esos punteros genéricos usando la ordenación de porción pobre

Por favor, dale una oportunidad a las ideas de otras personas.

@ go-li Si queremos evitar el síndrome de no inventado aquí, deberíamos buscar una definición en Alex Stepanov, ya que prácticamente inventó la programación genérica. Así es como lo definiría, tomado de la página 111 de "Elementos de programación" de Stepanov:

Bidirectional iterator<T> =
    ForwardIterator<T>
/\ predecessor : T -> T
/\ predecessor takes constant time
/\ (forall i in T) successor(i) is defined =>
        predecessor(successor(i)) is defined and equals i
/\ (forall i in T) predecessor(i) is defined =>
        successor(predecessor(i)) is defined and equals i

Esto depende de la definición de ForwardIterator:

ForwardIterator<T> =
    Iterator<T>
/\ regular_unary_function(successor)

Básicamente, tenemos una interfaz que declara una función successor y una función predecessor , junto con algunos axiomas que deben cumplir para ser válidas.

Con respecto a legacy , no es que el legado salga mal, obviamente no sale mal en Go actualmente, pero el compilador está perdiendo oportunidades de optimización y el sistema de tipos está perdiendo la oportunidad de propagar tipos concretos más. También limita la capacidad de los programadores para especificar su intención con precisión. Un ejemplo sería una función de identidad, que me refiero a devolver exactamente el tipo que se pasa:

id(x T) T

Quizás también valga la pena mencionar la diferencia entre un tipo paramétrico y un tipo universalmente cuantificado. Un tipo paramétrico sería id<T>(x T) T mientras que el cuantificado universalmente es id(x T) T (normalmente omitimos el cuantificador universal más externo en este caso forall T ). Con los tipos paramétricos, el sistema de tipos debe tener un tipo para T proporcionado en el sitio de llamadas por id , con cuantificación universal que no es necesaria siempre que T se unifique con un tipo concreto antes de que finalice la compilación. Otra forma de entender esto es que la función paramétrica no es un tipo sino una plantilla para un tipo, y solo es un tipo válido después de que T haya sido sustituido por un tipo concreto. Con la función cuantificada universalmente, id en realidad tiene un tipo forall T . T -> T que el compilador puede pasar como Int .

@go-li

Obviamente, elimina la lista vinculada de su base de código y la reemplaza con un árbol binario. O si quiere ser elegante, use un árbol de búsqueda equilibrado como avl, rojo-negro, como lo propusieron no sé cuántos años atrás Ian et al. Todavía esto no se ha hecho genéricamente en golang. Ahora ese sería el camino a seguir.

Tener estructuras de datos ordenadas no significa que nunca necesite ordenar datos.

Si queremos evitar el síndrome de no inventado aquí, deberíamos buscar una definición en Alex Stepanov, ya que prácticamente inventó la programación genérica.

Contestaría cualquier afirmación de que la programación genérica fue inventada por C++. Lea el Liskov et al. Documento de CACM de 1977 si desea ver un modelo inicial de programación genérica que realmente funciona (con seguridad de tipos, modular, sin sobrecarga de código): https://dl.acm.org/citation.cfm?id=359789 (consulte la Sección 4 )

Creo que deberíamos detener esta discusión y esperar a que el equipo de golang (russ) publique algunas publicaciones de blog y luego implementar una solución 👍 (ver vgo) Simplemente lo harán 🎉

https://peter.bourgon.org/blog/2018/07/27/a-response-about-dep-and-vgo.html

Espero que esta historia sirva como una advertencia para los demás: si está interesado en hacer contribuciones sustanciales al proyecto Go, ninguna cantidad de diligencia debida independiente puede compensar un diseño que no se origina en el equipo central.

Este hilo muestra cómo el equipo central no está interesado en participar activamente en la búsqueda de una solución con la comunidad.

Pero al final, si pueden hacer una solución por sí mismos nuevamente, está bien para mí, simplemente háganlo 👍

@andrewcmyers Bueno, tal vez "inventado" fue un poco exagerado, probablemente sea más como David Musser en 1971, quien luego trabajó con Stepanov en algunas bibliotecas genéricas para Ada.

Elementos de programación no es un libro sobre C++, los ejemplos pueden estar en C++ pero eso es algo muy diferente. Creo que este libro es una lectura esencial para cualquiera que desee implementar genéricos en cualquier idioma. Antes de despedir a Stepanov, realmente deberías leer el libro para ver de qué se trata realmente.

Este problema ya se está esforzando por debajo de los límites de la escalabilidad de GitHub. Mantenga la discusión aquí enfocada en temas concretos para las propuestas de Go.

Sería desafortunado que Go siguiera la ruta de C++.

@andrewcmyers Sí, estoy totalmente de acuerdo, no use C ++ para sugerencias de sintaxis o como punto de referencia para hacer las cosas correctamente. En su lugar, eche un vistazo a D para inspirarse .

@nomad-software

Me gusta mucho D, pero ¿Go necesita las potentes funciones de metaprogramación en tiempo de compilación que ofrece D?

No me gusta la sintaxis de la plantilla, tampoco en C++, derivada de la edad de piedra.

Pero, ¿qué pasa con el ParametricType normal?estándar que se encuentra en Java o C #, si es necesario, también se puede sobrecargar esto con ParametricType

Y más aún, no me gusta la sintaxis de llamada de plantilla en D con su símbolo de explosión, el símbolo de explosión se usa más bien hoy en día para denotar acceso mutable o inmutable para los parámetros de una función.

@nomad-software No estaba sugiriendo que la sintaxis de C ++ o el mecanismo de plantilla sea la forma correcta de hacer genéricos. Más que los "conceptos", tal como los define Stepanov, tratan los tipos como un álgebra, que es en gran medida la forma correcta de hacer genéricos. Mire las clases de tipos de Haskell para ver cómo podría verse esto. Las clases de tipos de Haskell están muy cerca de las plantillas y conceptos de c ++ semánticamente, si comprende lo que está sucediendo.

Así que +1 por no seguir la sintaxis de C++ y +1 por no implementar un sistema de plantilla de tipo inseguro :-)

@keean El motivo de la sintaxis D es evitar <,> por completo y adherirse a la gramática independiente del contexto. Esto es parte de mi punto de usar D como inspiración. <,> es una muy mala elección para la sintaxis de parámetros genéricos.

@nomad-software Como señalé anteriormente (en un comentario ahora oculto), debe especificar los parámetros de tipo para los tipos paramétricos, pero no para los tipos universalmente cuantificados (de ahí la diferencia entre Rust y Haskell, la forma en que se manejan los tipos es en realidad diferente en el sistema de tipos). También conceptos de C++ == clases de tipo Haskell == interfaces Go, al menos a nivel conceptual.

Es la sintaxis D realmente preferible:

auto add(T)(T lhs, T rhs) {
    return lhs + rhs;

¿Por qué es mejor que el estilo C++/Java/Rust?

T add<T>(T lhs, T rhs) {
    return lhs + rhs;
}

O estilo Scala:

T add[T](T lhs, T rhs) {
    return lhs + rhs;
}

He pensado un poco en la sintaxis de los parámetros de tipo. Nunca he sido fanático de los "paréntesis angulares" en C++ y Java porque hacen que el análisis sea bastante complicado y, por lo tanto, impiden el desarrollo de herramientas. Los corchetes son en realidad una opción clásica (de CLU, System F y otros lenguajes antiguos con polimorfismo paramétrico).

Sin embargo, la sintaxis de Go es bastante delicada, quizás porque ya es muy concisa. Las posibles sintaxis basadas en corchetes o paréntesis crean ambigüedades gramaticales aún peores que las que introducen los corchetes angulares. Entonces, a pesar de mis predisposiciones, los soportes angulares parecen ser la mejor opción para Go. (Por supuesto, también hay paréntesis angulares reales que no crearían ninguna ambigüedad, ⟨⟩, pero requerirían el uso de caracteres Unicode).

Por supuesto, la sintaxis precisa utilizada para los parámetros de tipo es menos importante que tener la semántica correcta. En ese punto, el lenguaje C++ es un mal modelo. El trabajo de mi grupo de investigación sobre genéricos en Genus (PLDI 2015) y Familia (OOPSLA 2017) ofrece otro enfoque que amplía las clases de tipos y las unifica con interfaces.

@andrewcmyers Creo que ambos documentos son interesantes, pero diría que no es una buena dirección para Go, ya que Genus está orientado a objetos y Go no, y Familia unifica la subtipificación y el polimorfismo paramétrico, y Go no tiene ninguno. Creo que Go simplemente debería adoptar el polimorfismo paramétrico o la cuantificación universal, no necesita subtipificación y, en mi opinión, es un lenguaje mejor porque no la tiene.

Creo que Go debería buscar genéricos que no requieran orientación a objetos y que no requieran subtipado. Go ya tiene interfaces, que creo que son un buen mecanismo para los genéricos. Si puede ver que las interfaces de Go == conceptos de c++ == clases de tipos de Haskell, me parece que la forma de agregar genéricos manteniendo el sabor de 'Go' sería extender las interfaces para tomar múltiples parámetros de tipo (Yo lo haría como los tipos asociados en las interfaces también, pero eso podría ser una extensión separada que ayuda a que se acepten múltiples parámetros de tipo). Ese sería el cambio clave, pero para habilitar esto, debería haber una sintaxis 'alternativa' para las interfaces en las firmas de funciones, de modo que pueda obtener los parámetros de tipo múltiple para las interfaces, que es donde entra toda la sintaxis de paréntesis angular. .

Las interfaces de Go no son clases de tipos, son simplemente tipos, pero unificar las interfaces con las clases de tipos es lo que Familia muestra una manera de hacer. Los mecanismos de Género y Familia no están ligados a que los lenguajes estén completamente orientados a objetos. Las interfaces de Go ya hacen que Go esté "orientado a objetos" en las formas que importan, por lo que creo que las ideas podrían adaptarse en una forma ligeramente simplificada.

@andrewcmyers

Las interfaces de Go no son clases de tipo, son simplemente tipos

No se comportan como tipos para mí, ya que permiten el polimorfismo. El objeto en una matriz polimórfica como Addable[] todavía tiene su tipo real (visible por reflexión en tiempo de ejecución), por lo que se comportan exactamente como clases de tipo de parámetro único. El hecho de que se coloquen en el lugar de un tipo en las firmas de tipo es simplemente una notación abreviada que omite la variable de tipo. No confundas la notación con la semántica.

f(x : Addable) == f<T>(x : T) requires Addable<T>

Por supuesto, esta identidad solo es válida para interfaces de un solo parámetro.

La única diferencia significativa entre las interfaces y las clases de tipo de parámetro único es que las interfaces se definen localmente, pero esto es útil porque evita el problema de coherencia global que tiene Haskell con sus clases de tipo. Creo que este es un punto interesante en el espacio de diseño. Las interfaces de parámetros múltiples le darían todo el poder de las clases de tipos de parámetros múltiples con el beneficio de ser local. No hay necesidad de agregar ninguna herencia o subtipo al lenguaje Go (que son las dos características clave que definen OO, creo).

EN MI HUMILDE OPINIÓN:

Seguir teniendo un tipo predeterminado sería preferible a un DSL dedicado a expresar restricciones de tipo. Como tener una función f(s T fmt.Stringer) que es una función genérica que acepta cualquier tipo que también satisfaga la interfaz fmt.Stringer .

De esta manera es posible tener una función genérica como:

func add(a, b T int) T int {
    return a + b
}

Ahora la función add() funciona con cualquier tipo T que, como int , admita el operador + .

@dc0d Estoy de acuerdo en que parece atractivo mirar la sintaxis actual de Go. Sin embargo, no es 'completo' en el sentido de que no puede representar todas las restricciones necesarias para los genéricos, y todavía habrá un impulso para extenderlo aún más. Esto dará como resultado una proliferación de diferentes sintaxis que veo en conflicto con el objetivo de la simplicidad. Mi opinión es que la simplicidad no es simple, tiene que ser lo más simple pero aún así ofrecer el poder expresivo requerido. Actualmente, veo que la principal limitación de Go en el poder expresivo genérico es la falta de interfaces de parámetros múltiples. Por ejemplo, una interfaz de colección podría definirse como:

type T U Collection interface {
   member(c T, v U) Bool
   insert(c T, v U) T
}

Así que esto tiene sentido, ¿verdad? Nos gustaría escribir interfaces sobre cosas como colecciones. Entonces, la pregunta es cómo usa esta interfaz en una función. Mi sugerencia sería algo como:

func[T, U] f(c T, e U) (Bool, T) requires Collection[T, U] {
   a := member(c, e)
   d := insert(c, e)
   return a, d
}

La sintaxis es solo una sugerencia, sin embargo, realmente no me importa cuál sea la sintaxis, siempre que pueda expresar estos conceptos en el idioma.

@keean No sería exacto si digo que no me importa la sintaxis en absoluto. Pero el punto era enfatizar en tener un tipo predeterminado para cada parámetro genérico. En ese sentido, el ejemplo proporcionado para la interfaz se convertirá en:

type Collection interface (T interface{}, U interface{}) {
   member(c T, v U) Bool
   insert(c T, v U) T
}

Ahora la parte (T interface{}, U interface{}) ayuda a definir restricciones. Por ejemplo, si los miembros están destinados a satisfacer fmt.Stringer , entonces la definición sería:

type Collection interface (T fmt.Stringer, U fmt.Stringer) {
   member(c T, v U) Bool
   insert(c T, v U) T
}

@dc0d Esto nuevamente sería restrictivo en el sentido de que desea restringir por más de un parámetro de tipo, considere:

type OrderedCollection[T, U] interface
   requires Collection[T, U], Ord[U] {...}

Creo que veo de dónde vienes con la ubicación del parámetro, podrías tener:

type OrderedCollection interface(T, U)
   requires Collection(T, U), Ord(U) {...}

Como dije, no estoy demasiado preocupado por la sintaxis, ya que puedo acostumbrarme a la mayoría de las sintaxis. De lo anterior, entiendo que prefiere paréntesis '()' para interfaces de parámetros múltiples.

@keean Consideremos la interfaz heap.Interface . La definición actual en la biblioteca estándar es:

type Interface interface {
    sort.Interface
    Push(x interface{}) // add x as element Len()
    Pop() interface{}   // remove and return element Len() - 1.
}

Ahora reescribámoslo como una interfaz genérica, empleando el tipo predeterminado:

type Interface interface (T interface{}) {
    sort.Interface
    Push(x T) // add x as element Len()
    Pop() T   // remove and return element Len() - 1.
}

Esto no rompe ninguna de las series de códigos Go 1.x que existen. Una implementación sería mi propuesta para Type Alias ​​Rebinding. Pero estoy seguro de que puede haber mejores implementaciones.

Tener tipos predeterminados nos permite escribir código genérico que se puede usar con código de estilo Go 1.x. Y la biblioteca estándar puede convertirse en una genérica, sin romper nada. Esa es una gran victoria en mi opinión.

@dc0d , ¿está sugiriendo una mejora incremental? Lo que está sugiriendo me parece bien como una mejora incremental, sin embargo, todavía tiene un poder expresivo genérico limitado. ¿Cómo implementaría las interfaces "Colección" y "OrderedCollection"?

Considere que varias extensiones de lenguaje parciales pueden conducir a un producto final más complejo (con múltiples sintaxis alternativas) que implementar la solución completa de la manera más simple posible.

@keean No entiendo la parte requires Collection[T, U], Ord[U] . ¿Cómo restringen los parámetros de tipo T y U ?

@dc0d Funcionan de la misma manera que en una función, pero se aplican a todo. Por lo tanto, para cualquier par de tipos TU que sea una OrderedCollection, requerimos que TU sea también una instancia de Collection y que U sea Ord. Entonces, en cualquier lugar que usemos OrderedCollection, podemos usar métodos de Collection y Ord según corresponda.

Si estamos siendo minimalistas, estos no son necesarios, porque podemos incluir las interfaces adicionales en los tipos de funciones donde las necesitamos, por ejemplo:

type OrderedCollection interface(T, U)
{
   first(c T) U
}

func[T] first(c T[]) T requires Collection(T[], T), Ord T
{...}

func[T] f(c T[]) requires OrderedCollection(T[], T), Collection(T[], T), Ord(T)
{...}

Pero esto podría ser más legible:

type OrderedCollection interface(T, U) 
   requires Collection(T, U), Ord(U)
{
   first(c T) U
}

func[T] first(c T[]) T
{...}

func[T] f(c T[]) requires OrderedCollection(T[], T)
{...}

@keean (IMO) Siempre que haya un valor predeterminado obligatorio para los parámetros de tipo, me siento feliz. De esa manera, es posible mantener la compatibilidad con versiones anteriores de la serie de códigos Go 1.x. Ese es el punto principal que traté de hacer.

@keean

Las interfaces de Go no son clases de tipo, son simplemente tipos

No se comportan como tipos para mí, ya que permiten el polimorfismo.

Sí, permiten polimorfismo de subtipo . Go tiene subtipos a través de tipos de interfaz. No tiene jerarquías de subtipo declaradas explícitamente, pero es en gran parte ortogonal. Lo que hace que Go no esté totalmente orientado a objetos es la falta de herencia.

Alternativamente, puede ver las interfaces como aplicaciones cuantificadas existencialmente de clases de tipo. Creo que eso es lo que tienes en mente. Eso es lo que hicimos en Género y Familia.

@andrewcmyers

Sí, permiten polimorfismo de subtipo.

Ir hasta donde sé es invariante, no hay covarianza ni contravarianza, esto habla con fuerza de que esto no es subtipificación. Los sistemas de tipos polimórficos son invariantes, por lo que me parece que Go está más cerca de este modelo, y tratar las interfaces como clases de tipo de parámetro único parece más acorde con la simplicidad de Go. La falta de covarianza y contravarianza es un gran beneficio para los genéricos, solo mire la confusión que estas cosas crean en lenguajes como C#:

https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance

Creo que Go debería evitar por completo este tipo de complejidad. Para mí, esto significa que no queremos genéricos y subtipos en el mismo sistema de tipos.

Alternativamente, puede ver las interfaces como aplicaciones cuantificadas existencialmente de clases de tipo. Creo que eso es lo que tienes en mente. Eso es lo que hicimos en Género y Familia.

Debido a que Go tiene información de tipo en tiempo de ejecución, no hay necesidad de cuantificación existencial. En Haskell, los tipos no están empaquetados (como los tipos 'C' nativos) y esto significa que una vez que hemos puesto algo en una colección existencial, no podemos (fácilmente) recuperar el tipo de los contenidos, todo lo que podemos hacer es usar las interfaces provistas (clases de tipo ). Esto se implementa almacenando un puntero a las interfaces junto con los datos sin procesar. En Go, el tipo de datos se almacena en su lugar, los datos están 'en caja' (como en C# datos en caja y sin caja). Como tal, Go no se limita solo a las interfaces almacenadas con los datos porque es posible (mediante el uso de un caso de tipo) recuperar el tipo de datos en la colección, lo que solo es posible en Haskell mediante la implementación de un 'Reflejo' typeclass (aunque es incómodo sacar los datos, es posible serializar el tipo y los datos, por ejemplo, cadenas, y luego deserializar fuera del cuadro existencial). Entonces, la conclusión que tengo es que las interfaces de Go se comportan exactamente como lo harían las clases de tipos, si Haskell proporcionara la clase de tipos 'Reflection' como una función integrada. Como tal, no hay una caja existencial, y aún podemos escribir mayúsculas y minúsculas en los contenidos de las colecciones, pero las interfaces se comportan exactamente como clases de tipos. La diferencia entre Haskell y Go está en la semántica de los datos en caja y sin caja, y las interfaces son clases de tipo de parámetro único. En efecto, cuando 'Go' trata una interfaz como un tipo, lo que realmente está haciendo es:

Addable[] == exists T . T[] requires Addable[T], Reflection[T]

Probablemente valga la pena señalar que esta es la misma forma en que funcionan los "objetos de rasgos" en Rust.

Go puede evitar totalmente los existenciales (ser visible para el programador), la covarianza y la contravarianza, lo cual es algo bueno, y eso hará que los genéricos sean mucho más simples y poderosos en mi opinión.

Ir hasta donde sé es invariante, no hay covarianza ni contravarianza, esto habla con fuerza de que esto no es subtipificación.

Los sistemas de tipo polimórfico son invariantes, por lo que me parece más cercano a este modelo, y tratar las interfaces como clases de tipo de parámetro único parece más acorde con la simplicidad de Go.

¿Puedo sugerir que ambos tienen razón? En que las interfaces son equivalentes a las clases de tipos, pero las clases de tipos son una forma de subtipado. Las definiciones de subtipos que encontré hasta ahora son bastante vagas e imprecisas y se reducen a "A es un subtipo de B, si uno puede sustituirse por el otro". Lo cual, en mi opinión, se puede argumentar bastante fácilmente para ser satisfecho por las clases de tipo .

Tenga en cuenta que el argumento de la varianza en sí mismo no funciona realmente en mi opinión. La varianza es una propiedad de los constructores de tipos, no un lenguaje. Y es bastante normal que no todos los constructores de tipos en un idioma sean variantes (por ejemplo, muchos idiomas con subtipos tienen matrices mutables, que tienen que ser invariantes para ser seguros). Entonces, no veo por qué no podría tener subtipos sin constructores de tipos variantes.

Además, creo que esta discusión es demasiado amplia para un problema en el repositorio de Go. Esto no debería tratarse de discutir las complejidades de las teorías de tipos, sino de si y cómo agregar genéricos a Go.

@Merovius Variance es una propiedad asociada con la subtipificación. En idiomas sin subtipado, no hay variación. Para que haya varianza, en primer lugar, debe tener subtipado, lo que introduce el problema de covarianza/contravarianza en los constructores de tipos. Sin embargo, tiene razón en que en un lenguaje con subtipos, es posible tener todos los constructores de tipos invariantes.

Las clases de tipos definitivamente no son subtipos, porque una clase de tipos no es un tipo. Sin embargo, podemos ver los 'tipos de interfaz' en Go como lo que Rust llama un 'objeto de rasgo', efectivamente un tipo derivado de la clase de tipo.

La semántica de Go parece encajar con cualquiera de los modelos en este momento, porque no tiene variación y tiene 'objetos de rasgos' implícitos. Entonces, tal vez Go esté en un punto de inflexión, los genéricos y el sistema de tipos podrían desarrollarse a lo largo de las líneas de subtipado, introduciendo variaciones y terminando con algo así como genéricos en C#. Alternativamente, Go podría introducir interfaces multiparámetro, permitiendo interfaces para Colecciones, y esto rompería el vínculo inmediato entre interfaces y 'tipos de interfaz'. Por ejemplo si tienes:

type (T, U) Collection interface {
    member : (c T, e U) Bool
    insert: (c T, e U) T
}

member(c int32[], e int32) Bool {...}
insert(c int32[], e int32) int32[] {...}

member(c float32[], e float32) Bool {...}
insert(c float32[], e float32) float32[] {...}

Ya no existe una relación de subtipo obvia entre los tipos T, U y la colección de interfaz. Por lo tanto, solo puede ver la relación entre el tipo de instancia y los tipos de interfaz como subtipos para el caso especial de las interfaces de un solo parámetro, y no podemos expresar abstracciones de cosas como colecciones con interfaces de un solo parámetro.

Creo que para los genéricos claramente necesitas poder modelar cosas como colecciones, por lo que las interfaces de parámetros múltiples son imprescindibles para mí. Sin embargo, creo que la interacción entre la covarianza y la contravarianza en los genéricos crea un sistema de tipo demasiado complejo, por lo que me gustaría evitar la subtipificación.

@keean Dado que las interfaces se pueden usar como tipos y las clases de tipos no son tipos, la explicación más natural de la semántica de Go es que las interfaces no son clases de tipos. Entiendo que está argumentando para generalizar las interfaces como clases de tipos; Creo que es una dirección razonable tomar el lenguaje y, de hecho, ya hemos explorado ese enfoque extensamente en nuestro trabajo publicado.

En cuanto a si Go tiene subtipos, considere el siguiente código:

package main

type Cloneable interface {
    Clone() Cloneable
}

type CloneableZ interface {
    Clone() Cloneable
    zero() int
}

type S struct {}

func (t S) Clone() Cloneable {
    c := t
    return c
}

func (t S) zero() int {
    return 0
}

var x CloneableZ = S{}
var y Cloneable = x

func main() {
    print("ok\n")
}

La asignación de x a y demuestra que el tipo de y puede usarse donde se espera el tipo de x . Esta es una relación de subtipado, a saber: CloneableZ <: Cloneable , y también S <: CloneableZ . Incluso si explicara las interfaces en términos de clases de tipos, todavía habría una relación de subtipos en juego aquí, algo así como S <: ∃T.CloneableZ[T] <: ∃T.Cloneable[T] .

Tenga en cuenta que sería perfectamente seguro que Go permitiera que la función Clone devolviera un S , pero ocurre que Go aplica reglas innecesariamente restrictivas para el cumplimiento de las interfaces: de hecho, las mismas reglas que Java aplicado originalmente. La creación de subtipos no requiere constructores de tipos no invariantes, como observó @Merovius .

@andrewcmyers ¿Qué sucede con las interfaces de parámetros múltiples, como las necesarias para abstraer colecciones?

Además, la asignación de x a y puede verse como una demostración de la herencia de la interfaz sin ningún subtipo. En Haskell (que claramente no tiene subtipos) escribirías:

class Cloneable t => CloneableZ t where...

Donde tenemos x es un tipo que implementa CloneableZ que por definición también implementa Cloneable , por lo que obviamente se puede asignar a y .

Para tratar de resumir, puede ver una interfaz como un tipo y Go para tener subtipos limitados sin constructores de tipos covariantes o contravariantes, o puede verlo como un "objeto de rasgo", o quizás en Go lo llamaríamos un " objeto de interfaz", que es efectivamente un contenedor polimórfico limitado por una "clase de tipo" de interfaz. En el modelo de clase de tipos no hay subtipos y, por lo tanto, no hay razón para tener que pensar en la covarianza y la contravarianza.

Si nos apegamos al modelo de subtipificación, no podemos tener tipos de colección, por eso C++ tuvo que introducir plantillas, porque la subtipificación orientada a objetos no es suficiente para definir genéricamente conceptos como contenedores. Terminamos con dos mecanismos para la abstracción, objetos y subtipos, y plantillas/características y genéricos, y las interacciones entre los dos se vuelven complejas, observe C++, C# y Scala, por ejemplo. Habrá llamadas continuas para introducir constructores covariantes y contravariantes para aumentar el poder de los genéricos, en línea con esos otros lenguajes.

Si queremos colecciones genéricas sin introducir un sistema genérico separado, entonces deberíamos pensar en interfaces como clases de tipos. Las interfaces multiparámetro significarían dejar de pensar en la creación de subtipos y, en cambio, pensar en la herencia de la interfaz. Si queremos mejorar los genéricos en Go y permitir abstracciones de cosas como colecciones, y no queremos la complejidad de los sistemas de tipos de lenguajes como C++, C#, Scala, etc., entonces las interfaces multiparámetro y la herencia de interfaz son el camino. ir.

@keean

¿Qué sucede con las interfaces multiparámetro, como las necesarias para abstraer colecciones?

Consulte nuestros artículos sobre Género y Familia, que admiten restricciones de tipo multiparámetro. Familia unifica esas restricciones con interfaces y permite que las interfaces restrinjan múltiples tipos.

Si nos apegamos al modelo de subtipos, no podemos tener tipos de colección

No estoy completamente seguro de lo que quiere decir con "el modelo de subtipado", pero está bastante claro que Java y C# tienen tipos de colección, por lo que esta afirmación no tiene mucho sentido para mí.

Donde tenemos x es un tipo que implementa CloneableZ que, por definición, también implementa Cloneable, por lo que obviamente se puede asignar a y.

No, en mi ejemplo, x es una variable e y es otra variable. Si sé que y es del tipo CloneableZ y x es del tipo Cloneable , eso no significa que pueda asignar de y a x. Eso es lo que está haciendo mi ejemplo.

Para aclarar que se necesitan subtipos para modelar Go, a continuación se muestra una versión mejorada del ejemplo cuyo equivalente moral no verifica el tipo en Haskell. El ejemplo muestra que la creación de subtipos permite la creación de colecciones heterogéneas en las que diferentes elementos tienen diferentes implementaciones. Además, el conjunto de posibles implementaciones es abierto.

type Cloneable interface {
    Clone() Cloneable
}

type CloneableZ interface {
    Clone() Cloneable
    zero() int
}

type S struct {}

func (t S) Clone() Cloneable {
    c := t
    return c
}

type T struct { x int }

func (t T) Clone() Cloneable {
    c := t
    return c
}

func (t S) zero() int {
    return 0
}

var x CloneableZ = S{}
var y Cloneable = T{}
var a [2]Cloneable = [2]Cloneable{x, y}

@andrewcmyers

No estoy completamente seguro de lo que quiere decir con "el modelo de subtipado", pero está bastante claro que Java y C# tienen tipos de colección, por lo que esta afirmación no tiene mucho sentido para mí.

Eche un vistazo a por qué C ++ desarrolló plantillas, el modelo de subtipado OO no fue capaz de expresar los conceptos genéricos necesarios para generalizar cosas como colecciones. C# y Java también tuvieron que introducir un sistema genérico completo separado de los objetos, los subtipos y la herencia, y luego tuvieron que limpiar el desorden de las complejas interacciones de los dos sistemas con cosas como constructores de tipo covariante y contravariante. Con el beneficio de la retrospectiva, podemos evitar la subtipificación OO y, en cambio, ver qué sucede si agregamos interfaces (clases de tipo) a un lenguaje de tipeo simple. Esto es lo que ha hecho Rust, por lo que vale la pena echarle un vistazo, pero, por supuesto, es complicado por todo el asunto de la vida útil. Go tiene GC por lo que no tendría esa complejidad. Mi sugerencia es que Go se puede ampliar para permitir interfaces de parámetros múltiples y evitar esta complejidad.

Con respecto a su afirmación de que no puede hacer este ejemplo en Haskell, aquí está el código:

{-# LANGUAGE ExistentialQuantification #-}

class ICloneable t where
    clone :: t -> t

class ICloneable t => ICloneableZ t where
    zero :: t

data S = S deriving Show

instance ICloneable S where
    clone x = x

data T = T Int deriving Show

instance ICloneable T where
    clone x = x

instance ICloneableZ T where
    zero = T 0

data Cloneable = forall a . (ICloneable a, Show a) => ToCloneable a

instance Show Cloneable where
    show (ToCloneable x) = show x

main = do
    x <- return S
    y <- return (T 27)
    a <- return [ToCloneable x, ToCloneable y]
    putStrLn (show a)

Algunas diferencias interesantes, Go deriva automáticamente este tipo data Cloneable = forall a . (ICloneable a, Show a) => ToCloneable a ya que así es como convierte una interfaz (que no tiene almacenamiento) en un tipo (que tiene almacenamiento), Rust también deriva estos tipos y los llama "objetos de rasgos" . En otros lenguajes como Java, C# y Scala, encontramos que no puede crear instancias de interfaces, lo que en realidad es "correcto", las interfaces no son tipos, no tienen almacenamiento, Go deriva automáticamente el tipo de un contenedor existencial para que pueda tratar la interfaz como un tipo, y Go lo oculta al darle al contenedor existencial el mismo nombre que la interfaz de la que se deriva. La otra cosa a tener en cuenta es que este [2]Cloneable{x, y} obliga a todos los miembros a Cloneable , mientras que Haskell no tiene tales coacciones implícitas, y tenemos que obligar explícitamente a los miembros con ToCloneable .

También me han señalado que no debemos considerar los subtipos S y T de Cloneable porque S y T no lo son estructuralmente compatibles. Literalmente, podemos declarar cualquier tipo como una instancia de Cloneable (simplemente declarando la definición relevante de la función clone en Go) y esos tipos no necesitan tener ninguna relación entre sí.

La mayoría de las propuestas de Generics parecen incluir tokens adicionales que, en mi opinión, perjudican la legibilidad y la sensación de simpleza de Go. Me gustaría proponer una sintaxis diferente que creo que podría funcionar bien con la gramática existente de Go (incluso sucede que la sintaxis resalta bastante bien en Github Markdown).

Los puntos principales de la propuesta:

  • La gramática de Go parece tener siempre una manera fácil de determinar cuándo finaliza una declaración de tipo porque hay algún token o palabra clave específica que estamos buscando. Si esto es cierto en todos los casos, los argumentos de tipo simplemente se pueden agregar después de los nombres de tipo.
  • Como la mayoría de las propuestas, el mismo identificador significa el mismo tipo en cualquier declaración de función. Estos identificadores nunca escapan a la declaración.
  • En la mayoría de las propuestas, debe declarar argumentos de tipo genérico, pero en esta propuesta está implícito. Algunas personas afirmarán que esto perjudica la legibilidad o la claridad (la implicidad es mala) o restringe la capacidad de nombrar un tipo, las refutaciones son las siguientes:

    • Cuando se trata de perjudicar la legibilidad, creo que se puede argumentar de cualquier manera, el extrao [T] perjudica la legibilidad al hacer mucho ruido sintáctico.

    • La implicidad cuando se usa correctamente puede ayudar a que un lenguaje sea menos detallado. Omitimos las declaraciones de tipo con := todo el tiempo porque la información oculta por eso simplemente no es lo suficientemente importante como para deletrear cada vez.

    • Nombrar un tipo concreto (no genérico) a o t probablemente sea una mala práctica, por lo que esta propuesta asume que es seguro reservar estos identificadores para que actúen como argumentos de tipo genérico. ¿Aunque esto requeriría una migración de corrección?

package main

import "fmt"

type LinkedList a struct {
  Head *Node a
  Tail *Node a
}

type Node a {
  Next *Node a
  Prev *Node a

  Value a
}

func main() {
  // Not sure about how recursive we could get with the inference
  ll := LinkedList string {
    // The string bit could be inferred
    Head: Node string { Value: "hello world" },
  }
}

func (l *LinkedList a) Append(value a) {
  newNode := &Node{Value: value}

  if l.Tail == nil {
    l.Head = newNode
    l.Tail = l.Head
    return
  }

  l.Tail.Next = newNode
  l.Tail = l.Tail.Next
}

Esto está tomado de Gist que tiene un poco más de detalle, así como los tipos de suma propuestos aquí: https://gist.github.com/aarondl/9b950373642fcf5072942cf0fca2c3a2

Esta no es una propuesta de Genéricos completamente eliminada y no está destinada a serlo, hay muchos problemas que resolver para poder agregar genéricos a Go. Este solo aborda la sintaxis, y espero que podamos tener una conversación sobre si lo que se propone es factible/deseable o no.

@aarondl
Me parece bien, usando esta sintaxis tendríamos:

type Collection a b interface {
   member(c a, e b) Bool
   insert(c a, e b) a
}

func insert(c *LinkedList a, e a) *LinkedList a {
   c.Append(e)
   return c
}

@keean ¿Podría explicar un poco el tipo Collection ? no logro entenderlo:

type Collection a b interface {
   member(c a, e b) Bool
   insert(c a, e b) a
}

@dc0d Collection es una interfaz que abstrae _todas_ las colecciones, es decir, árboles, listas, sectores, etc., por lo que podemos tener operaciones genéricas como member y insert que funcionarán en cualquier colección que contenga cualquier tipo de datos. En lo anterior, di el ejemplo de definir 'insertar' para el tipo LinkedList en el ejemplo anterior:

func insert(c *LinkedList a, e a) *LinkedList a {
   c.Append(e)
   return c
}

También podríamos definirlo para una rebanada

func insert(c []a, e a) []a {
   return append(c, e)
}

Sin embargo, ni siquiera necesitamos el tipo de funciones paramétricas con variables de tipo ilustradas por @aarondl con tipo polimórfico a para que esto funcione, ya que puede definir para tipos concretos:

func insert(c *LinkedList int, e int) *LinkedList int {
   c.Append(e)
   return c
}

func insert(c *LinkedList float, e float) *LinkedList float {
   c.Append(e)
   return c
}

func insert(c int[], e int) int[] {
   return append(c, e)
}

func insert(c float[], e float) float[] {
   return append(c, e)
}

Así que Collection es una interfaz para generalizar tanto el tipo de un contenedor como el tipo de su contenido, lo que permite escribir funciones genéricas que operan en todas las combinaciones de contenedor y contenido.

No hay ninguna razón por la que no pueda tener también una porción de colecciones []Collection donde los contenidos serían todos diferentes tipos de colección con diferentes tipos de valores, siempre que se definieran member y insert para cada combinación .

@aarondl Dado que type LinkedList a ya es una declaración de tipo válida, solo puedo ver dos formas de hacer que esto se pueda analizar sin ambigüedades: hacer que la gramática sea sensible al contexto (meterse en los problemas de analizar C, ugh) o usar una búsqueda anticipada ilimitada ( que la gramática go tiende a evitar, debido a los malos mensajes de error en el caso de falla). Puede que esté malinterpretando algo, pero en mi opinión, eso habla en contra de un enfoque sin fichas.

Las interfaces @keean en Go usan métodos, no funciones. En la sintaxis específica que sugirió, no hay nada que adjunte insert a *LinkedList para el compilador (en Haskell eso se hace a través de declaraciones instance ). También es normal que los métodos cambien el valor en el que están operando. Nada de esto es un Show-Stopper, solo señala que la sintaxis que sugiere no funciona bien con Go. Probablemente más algo como

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

Lo que también demuestra un par de preguntas más con respecto a cómo se analizan los parámetros de tipo y cómo se deben analizar.

@aarondl también tengo más preguntas sobre su propuesta. Por ejemplo, no permite restricciones, por lo que solo obtiene polimorfismo sin restricciones. Lo cual, en general, no es tan útil, ya que no se le permite hacer nada con los valores que obtiene (por ejemplo, no puede implementar Collection con un mapa, ya que no todos los tipos son claves de mapa válidas). ¿Qué debería pasar cuando alguien intenta hacer algo así? Si se trata de un error en tiempo de compilación, ¿se queja de la creación de instancias (mensajes de error de C ++ más adelante) o de la definición (básicamente, no puede hacer nada, porque no hay nada que funcione con todos los tipos)?

@keean Aún no entiendo cómo a está restringido a ser una lista (o rebanada o cualquier otra colección). ¿Es esta una gramática especial dependiente del contexto para colecciones? Si es así, ¿cuál es su valor? No es posible declarar tipos definidos por el usuario de esta manera.

@Merovius ¿Eso significa que Go no puede realizar envíos múltiples y hace que el primer argumento de una 'función' sea especial? Esto sugiere que los tipos asociados encajarían mejor que las interfaces de múltiples parámetros. Algo como esto:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt(c Collection, e Collection.Element) {...}

Sin embargo, esto todavía tiene problemas porque no hay nada que restrinja que las dos colecciones sean del mismo tipo... Terminarías necesitando algo como:

func[A] useIt(c A, e A.Element) requires A:Collection

Para intentar explicar la diferencia, las interfaces multiparámetro tienen tipos de _entrada_ adicionales que participan en la selección de instancias (de ahí la conexión con el envío múltiple), mientras que los tipos asociados son tipos de _salida_, solo el tipo de receptor participa en la selección de instancias, y luego los tipos asociados dependen del tipo de receptor.

@dc0d a y b son parámetros de tipo de la interfaz, al igual que en una clase de tipo Haskell. Para que algo se considere un Collection tiene que definir los métodos que coinciden con los tipos en la interfaz donde a y b pueden ser de cualquier tipo. Sin embargo, como ha señalado @Merovius , las interfaces de Go se basan en métodos y no admiten envíos múltiples, por lo que las interfaces de parámetros múltiples pueden no ser una buena opción. Con el modelo de método de envío único de Go, tener tipos asociados en las interfaces, en lugar de múltiples parámetros, parece ser una mejor opción. Sin embargo, la falta de envío múltiple dificulta la implementación de funciones como unify(x, y) , y tiene que usar el patrón de envío doble, que no es muy bueno.

Para explicar un poco más lo de los parámetros múltiples:

type Cloneable[A] interface {
   clone(x A) A
}

Aquí a representa cualquier tipo, no importa cuál sea, siempre que se definan las funciones correctas, lo consideramos Cloneable . Consideraríamos las interfaces como restricciones sobre los tipos en lugar de los tipos en sí mismos.

func clone(x int) int {...}

entonces, en el caso de 'clonar', sustituimos a por int en la definición de la interfaz, y podemos llamar a clonar si la sustitución tiene éxito. Esto encaja muy bien con esta notación:

func[A] test(x A) A requires Cloneable[A] {...}

Esto es equivalente a:

type Cloneable interface {
   clone() Cloneable
}

pero declara una función, no un método, y puede extenderse con múltiples parámetros. Si tiene un idioma con envío múltiple, no hay nada especial en el primer argumento de una función/método, entonces, ¿por qué escribirlo en un lugar diferente?

Como Go no tiene despacho múltiple, todo esto comienza a parecer demasiado como para cambiarlo todo a la vez. Parece que los tipos asociados encajarían mejor, aunque más limitados. Esto permitiría colecciones abstractas, pero no soluciones elegantes para cosas como la unificación.

@Merovius Gracias por echar un vistazo a la propuesta. Permítame tratar de abordar sus inquietudes. Lamento que hayas rechazado la propuesta antes de que discutiéramos más, espero poder hacerte cambiar de opinión, o tal vez puedas cambiar la mía :)

Anticipación ilimitada:
Entonces, como mencioné en la propuesta, actualmente parece que la gramática Go tiene una buena manera de detectar el "final" de casi todo sintácticamente. Y aún lo haríamos debido a los argumentos genéricos implícitos. La minúscula de una sola letra es la construcción sintáctica que crea ese argumento genérico, o lo que sea que decidamos para hacer ese token en línea, tal vez incluso recurramos a una cosa tokenizada como @a en la propuesta si nos gusta la sintaxis lo suficiente pero no lo es posible dada la dificultad del compilador sin tokens, aunque la propuesta pierde mucho encanto tan pronto como haces eso.

Independientemente, el problema con type LinkedList a bajo esta propuesta no es tan difícil porque sabemos que a es un argumento de tipo genérico y, por lo tanto, fallaría con un error de compilación igual que type LinkedList falla hoy con: prog.go:3:16: expected type, found newline (and 1 more errors) . La publicación original realmente no salió ni lo dijo, pero ya no se le permite nombrar un tipo concreto [a-z]{1} lo que, creo, resuelve este problema y es un sacrificio. Creo que todos estaríamos bien. creación (solo puedo ver perjuicios en la creación de tipos reales con nombres de una sola letra en el código Go hoy).

Es solo polimorfismo sin restricciones
La razón por la que omití cualquier tipo de rasgos o restricciones de argumentos genéricos es porque creo que ese es el papel de las interfaces en Go, si desea hacer algo con un valor, entonces ese valor debe ser un tipo de interfaz y no un tipo totalmente genérico. Creo que esta propuesta también funciona bien con las interfaces.

Según esta propuesta, seguiríamos teniendo el mismo problema que tenemos ahora con operadores como + por lo que no podría crear una función de suma genérica para todos los tipos numéricos, pero podría aceptar una función de suma genérica como argumento. Considera lo siguiente:

func Sort(slice []a, compare func (a, a) bool) { ... }

Preguntas sobre el alcance

Usted dio un ejemplo aquí:

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

El alcance de estos identificadores, como regla, está vinculado a la declaración/definición particular en la que se encuentran. No se comparten en ninguna parte y no veo una razón para que lo sean.

@keean Eso es muy interesante, aunque como otros han señalado, tendría que cambiar lo que ha mostrado allí para poder implementar las interfaces (actualmente en su ejemplo no hay métodos con receptores, solo funciones). Tratando de pensar más en cómo afecta esto a mi propuesta original.

La minúscula de una sola letra es la construcción sintáctica que crea ese argumento genérico

No me siento bien por eso; requiere tener producciones separadas para lo que es un identificador dependiendo del contexto y también significa prohibir arbitrariamente ciertos identificadores para tipos. Pero no es realmente el momento de hablar de estos detalles.

Según esta propuesta, seguiríamos teniendo el mismo problema que tenemos ahora con operadores como +

No entiendo esta frase. Actualmente, el operador + no tiene ninguno de esos problemas porque los tipos de sus operandos se conocen localmente y el mensaje de error es claro e inequívoco y apunta al origen del problema. ¿Tengo razón al suponer que está diciendo que desea prohibir cualquier uso de valores genéricos que no esté permitido para todos los tipos posibles (no se me ocurren muchas operaciones de este tipo)? ¿Y crear un error de compilación para la expresión ofensiva en la función genérica? En mi opinión, eso limitaría demasiado el valor de los genéricos.

si desea hacer algo con un valor, ese valor debe ser un tipo de interfaz y no un tipo completamente genérico.

Las dos razones principales por las que la gente quiere genéricos es el rendimiento (evitar el envoltorio de interfaces) y la seguridad de tipo (asegurarse de que el mismo tipo se use en diferentes lugares, sin importar cuál es). Esto parece ignorar esas razones.

podría aceptar una función de adición genérica como argumento.

Verdadero. Pero bastante poco ergonómico. Considere cuántas quejas hubo sobre la API sort . Para muchos contenedores genéricos, la cantidad de funciones que la persona que llama tendría que implementar y pasar parece ser prohibitiva. Considere, ¿cómo se vería una implementación container/heap bajo esta propuesta y cómo sería mejor que la implementación actual, en términos de ergonomía? Parecería que las ganancias son insignificantes aquí, en el mejor de los casos. Tendría que implementar funciones más triviales (y duplicar / hacer referencia en cada sitio de uso), no menos.

@Merovius

pensando en este punto de @aarondl

podría aceptar una función de adición genérica como argumento.

Sería mejor tener una interfaz Addable para permitir la sobrecarga de sumas, dada alguna sintaxis para definir operadores infijos:

type Addable interface {
   + (x Addable, y Addable) Addable
}

Desafortunadamente esto no funciona, porque no expresa que esperamos que todos los tipos sean iguales. Para definir agregable, necesitaríamos algo como las interfaces multiparámetro:

type Addable[A] interface {
   + (x A, y A) A
}

Luego, también necesitaría Go para realizar envíos múltiples, lo que significaría que todos los argumentos en una función se tratan como un receptor para la coincidencia de interfaz. Entonces, en el ejemplo anterior, cualquier tipo es Addable si hay una función + definida en él que satisface las definiciones de función en la definición de la interfaz.

Pero dados esos cambios, ahora podrías escribir:

type S struct {
   value: int
}

func (+) (x S, y S) S {
   return S {
      value: x.value + y.value
   }
}

func main() {
    println(S {value: 27} + S {value: 5})
}

Por supuesto, la sobrecarga de funciones y el envío múltiple pueden ser algo que la gente nunca quiere en Go, pero cosas como definir la aritmética básica en tipos definidos por el usuario como vectores, matrices, números complejos, etc., siempre serán imposibles. Como dije anteriormente, los 'tipos asociados' en las interfaces permitirían cierto aumento en la capacidad de programación genérica, pero no la generalidad total. ¿Es el envío múltiple (y presumiblemente la sobrecarga de funciones) algo que podría suceder en Go?

cosas como definir aritmética básica en tipos definidos por el usuario como vectores, matrices, números complejos, etc., siempre serán imposibles.

Algunos podrían considerar que es una característica :) AFAIR hay alguna propuesta o hilo flotando en algún lugar discutiendo si debería. FWIW, creo que esto es, nuevamente, desviarse del tema. La sobrecarga de operadores (o las ideas generales de "cómo hacer que Go sea más Haskell") no es realmente el objetivo de este problema :)

¿Es el envío múltiple (y presumiblemente la sobrecarga de funciones) algo que podría suceder en Go?

Nunca digas nunca. Aunque no lo esperaría, personalmente.

@Merovius

Algunos podrían considerar que una característica :)

Claro, y si Go no lo hace, hay otros idiomas que lo harán :-) Go no tiene que ser todo para todos. Solo estaba tratando de establecer un alcance para los genéricos en Go. Mi enfoque es crear lenguajes totalmente genéricos, ya que tengo aversión a repetirme y repetitivo (y no me gustan las macros). Si tuviera un centavo por cada vez que tuve que escribir una lista enlazada o un árbol en 'C' para algún tipo de datos específico. De hecho, hace que algunos proyectos sean imposibles para un equipo pequeño debido al volumen de código que debe tener en la cabeza para comprenderlo y luego mantenerlo mediante cambios. A veces pienso que las personas que no tienen la necesidad de genéricos simplemente no han escrito un programa lo suficientemente grande todavía. Por supuesto, puede tener un gran equipo de desarrolladores trabajando en algo y solo hacer que cada desarrollador sea responsable de una pequeña parte del código total, pero estoy interesado en hacer que un solo desarrollador (o un equipo pequeño) sea lo más efectivo posible.

Dado que la sobrecarga de funciones y el envío múltiple están fuera del alcance, y también dados los problemas de análisis con la sugerencia de @aarondl , parece que agregar tipos asociados a las interfaces y parámetros de tipo a las funciones sería todo lo que desearía. ir con genéricos en Go.

Algo como esto parecería ser el tipo correcto de cosas:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt<T>(c T, e T.Element) requires T:Collection {...}

Entonces habría una decisión en la implementación si usar tipos paramétricos o tipos universalmente cuantificados. Con los tipos paramétricos (como Java), una función 'genérica' no es en realidad una función, sino una especie de plantilla de función de tipo seguro y, como tal, no se puede pasar como argumento a menos que se proporcione su parámetro de tipo, de modo que:

f(useIt) // not okay with parametric types
f(useIt<List>) // okay with parametric types

Con tipos universalmente cuantificados, puede pasar useIt como argumento y luego se le puede proporcionar un parámetro de tipo dentro f . La razón para favorecer los tipos paramétricos es que puede monomorfizar el polimorfismo en tiempo de compilación, lo que significa que no se elaboran funciones polimórficas en tiempo de ejecución. No estoy seguro de que esto sea un problema con Go, porque Go ya está realizando envíos en tiempo de ejecución en las interfaces, por lo que siempre que el parámetro de tipo para useIt implemente Collection, puede enviar al receptor correcto en tiempo de ejecución, por lo que es universal. la cuantificación es probablemente el camino correcto para Go.

Me pregunto, SFINAE mencionado solo por @bcmills. Ni siquiera se menciona en la propuesta (aunque Ordenar está allí como ejemplo).
Entonces, ¿cómo se vería el Sort para el segmento y la lista enlazada?

@keean
No puedo entender cómo se definiría una colección genérica 'Slice' con su sugerencia. Parece que está definiendo un 'IntSlice' que podría estar implementando 'Colección' (aunque Insert devuelve un tipo diferente al que busca la interfaz), pero eso no es un 'segmento' genérico, ya que parece ser solo para ints , y las implementaciones del método son solo para ints. ¿Necesitamos definir una implementación específica por tipo?

A veces pienso que las personas que no tienen la necesidad de genéricos simplemente no han escrito un programa lo suficientemente grande todavía.

Puedo asegurarles que esa impresión es falsa. Y FWIW, ISTM que "el otro lado" está poniendo "no ver la necesidad" en el mismo cubo que "no ver el uso". Veo el uso y no lo refuto. Aunque realmente no veo la necesidad . Me va bien sin, incluso en grandes bases de código.

Y no confundas "querer que se hagan bien y señalar dónde no se hacen las propuestas existentes" con "oponerse fundamentalmente a la idea misma".

también dados los problemas de análisis con la sugerencia de @aarondl .

Como dije, no creo que hablar sobre el problema del análisis sea realmente productivo en este momento. Los problemas de análisis se pueden resolver. El lach del polimorfismo restringido es mucho más serio, semánticamente. En mi opinión, agregar genéricos sin eso realmente no vale la pena.

@urandom

No puedo entender cómo se definiría una colección genérica 'Slice' con su sugerencia.

Como se indicó anteriormente, aún necesitaría definir una implementación separada para cada tipo de segmento, sin embargo, aún se beneficiaría al poder escribir algoritmos en términos de la interfaz genérica. Si quisiera permitir una implementación genérica para todos los segmentos, necesitaría permitir tipos y métodos paramétricos asociados. Tenga en cuenta que moví el parámetro de tipo después de la palabra clave para que ocurra antes del tipo de receptor.

type<T> []T.Element = Int

func<T> ([]T) Member(e T) Bool {...}
func<T> ([]T) Insert(e T) Collection {...}

Sin embargo, ahora también tiene que lidiar con la especialización, porque alguien podría definir el tipo y los métodos asociados para el []int más especializado y tendría que lidiar con cuál usar. Normalmente elegiría la instancia más específica, pero agrega otra capa de complejidad.

No estoy seguro de cuánto esto realmente te hace ganar. Con mi ejemplo original anterior, puede escribir algoritmos genéricos para actuar en colecciones generales usando la interfaz, y solo tendría que proporcionar los métodos y tipos asociados para los tipos que realmente usa. La mayor victoria para mí es poder definir algoritmos como ordenar colecciones arbitrarias y poner esos algoritmos en una biblioteca. Si luego tengo una lista de "formas", solo tengo que definir los métodos de interfaz de colección para mi lista de formas, y luego puedo usar cualquier algoritmo en la biblioteca en ellos. Ser capaz de definir los métodos de interfaz para todos los tipos de corte me interesa menos y podría ser demasiado complejo para Go.

@Merovius

Aunque realmente no veo la necesidad. Me va bien sin, incluso en grandes bases de código.

Si puede hacer frente a un programa de 100 000 líneas, podrá hacer más con 100 000 líneas genéricas que con 100 000 líneas no genéricas (debido a la repetición). Por lo tanto, puede ser un desarrollador superestrella capaz de hacer frente a bases de código muy grandes, pero aun así lograría más con una base de código genérica muy grande, ya que eliminaría la redundancia. Ese programa genérico se expandiría a un programa no genérico aún más grande. Me parece que aún no has alcanzado tu límite de complejidad.

Sin embargo, creo que tiene razón, 'necesitar' es demasiado fuerte, estoy felizmente escribiendo código go, con solo frustración ocasional por la falta de genéricos, y puedo solucionar esto simplemente escribiendo más código, y en Go ese código es agradablemente directo y literal.

La falta de polimorfismo restringido es mucho más grave, semánticamente. En mi opinión, agregar genéricos sin eso realmente no vale la pena.

Estoy de acuerdo con ésto.

podrá hacer más con 100 000 líneas genéricas que con 100 000 líneas no genéricas (debido a la repetición)

Tengo curiosidad, a partir de su ejemplo hipotético, ¿qué porcentaje de esas líneas sería una función genérica?
En mi experiencia, esto es menos del 2% (de una base de código con LOC de 115k), por lo que no creo que sea un buen argumento a menos que escriba una biblioteca para "colecciones".

Desearía que eventualmente obtuviéramos genéricos aunque

@keean

Con respecto a su afirmación de que no puede hacer este ejemplo en Haskell, aquí está el código:

Este código no es moralmente equivalente al código que escribí. Introduce un nuevo tipo de contenedor Cloneable además de la interfaz ICloneable. El código Go no necesitaba un envoltorio; ni lo harían otros idiomas que admitan la subtipificación.

@andrewcmyers

Este código no es moralmente equivalente al código que escribí. Introduce un nuevo tipo de contenedor Cloneable además de la interfaz ICloneable.

¿No es esto lo que hace este código?

type Cloneable interface {...}

Introduce un tipo de datos 'Cloneable' derivado de la interfaz. No ve el 'ICloneable' porque no tiene declaraciones de instancia para las interfaces, solo declara los métodos.

¿Puede considerar subtipificar cuando los tipos que implementan una interfaz no tienen que ser estructuralmente compatibles?

@keean , consideraría que Cloneable es simplemente un tipo, no realmente un "tipo de datos". En un lenguaje como Java, esencialmente no habría costo adicional para la abstracción Cloneable , porque no habría contenedor, a diferencia de su código.

Me parece limitante e indeseable exigir una similitud estructural entre los tipos que implementan una interfaz, por lo que estoy confundido acerca de lo que está pensando aquí.

@andrewcmyers
Estoy usando tipo y tipo de datos indistintamente. Cualquier tipo que pueda contener datos es un tipo de datos.

porque no habría contenedor, a diferencia de su código.

Siempre hay un envoltorio porque los tipos de Go siempre están enmarcados, por lo que el envoltorio existe alrededor de todo. Haskell necesita que el contenedor sea explícito porque tiene tipos sin caja.

similitud estructural entre los tipos que implementan una interfaz, por lo que estoy confundido acerca de lo que está pensando aquí.

La subtipificación estructural requiere que los tipos sean 'estructuralmente compatibles'. Como no existe una jerarquía de tipo explícita como en un lenguaje OO con herencia, la subtipificación no puede ser nominal, por lo que debe ser estructural, si es que existe.

Sin embargo, veo lo que quiere decir, lo que describiría como considerar que una interfaz es una clase base abstracta, no una interfaz, con algún tipo de relación de subtipo nominal implícita con cualquier tipo que implemente los métodos requeridos.

De hecho, creo que Go se adapta a ambos modelos en este momento, y podría ir en cualquier dirección desde aquí, pero sugeriría que llamarlo interfaz, no una clase, sugiere una forma de pensar sin subtipificación.

@keean No entiendo tu comentario. Primero me dices que no estás de acuerdo y que "todavía no he alcanzado mi límite de complejidad" y luego me dices que estás de acuerdo (en esa "necesidad" es una palabra demasiado fuerte). También creo que su argumento es falaz (supone que LOC es la medida principal de complejidad y que todas las líneas de código son iguales). Pero sobre todo, no creo que "quién está escribiendo programas más complicados" sea realmente una línea productiva de discusión. Solo estaba tratando de aclarar que el argumento "si no está de acuerdo conmigo, eso debe significar que no está trabajando en problemas tan difíciles o interesantes" no es convincente y no parece de buena fe. Espero que pueda confiar en que las personas pueden estar en desacuerdo con usted acerca de la importancia de esta función mientras son igualmente competentes y hacen cosas igual de interesantes.

@merovio
Estaba diciendo que es probable que sea un programador más capaz que yo y, por lo tanto, capaz de trabajar con más complejidad. Desde luego, no creo que estés trabajando en problemas menos interesantes o menos complejos, y lamento que te haya parecido así. Pasé ayer tratando de hacer funcionar un escáner, que era un problema muy poco interesante.

Puedo pensar que los genéricos me ayudan a escribir programas más complejos con mi limitada capacidad intelectual y también admitir que no "necesito" genéricos. Es una cuestión de grado. Todavía puedo programar sin genéricos, pero no necesariamente puedo escribir software de la misma complejidad.

Espero que eso le asegure que estoy actuando de buena fe, no tengo una agenda oculta aquí, y si Go no adopta los genéricos, todavía los usaré. Tengo una opinión sobre la mejor manera de hacer genéricos, pero no es la única opinión, solo puedo hablar desde mi propia experiencia. Si no estoy ayudando, hay muchas otras cosas en las que puedo dedicar mi tiempo, así que solo dilo y me reenfocaré en otra parte.

@Merovius Gracias por el diálogo continuo.

| Las dos razones principales por las que la gente quiere genéricos es el rendimiento (evitar el envoltorio de interfaces) y la seguridad de tipo (asegurarse de que el mismo tipo se use en diferentes lugares, sin importar cuál es). Esto parece ignorar esas razones.

Tal vez estamos viendo lo que propuse de manera muy diferente, ya que desde mi perspectiva hace ambas cosas, por lo que puedo decir. En el ejemplo de la lista enlazada, no hay envoltura con interfaces y, por lo tanto, debería tener el mismo rendimiento que si estuviera escrito a mano para un tipo determinado. En el lado tipo-seguridad es lo mismo. ¿Hay algún contraejemplo que puedas dar aquí para ayudarme a entender de dónde vienes?

| Verdadero. Pero bastante poco ergonómico. Considere cuántas quejas hubo sobre la API de clasificación. Para muchos contenedores genéricos, la cantidad de funciones que la persona que llama tendría que implementar y pasar parece ser prohibitiva. Considere, ¿cómo se vería una implementación de contenedor/montón bajo esta propuesta y cómo sería mejor que la implementación actual, en términos de ergonomía? Parecería que las ganancias son insignificantes aquí, en el mejor de los casos. Tendría que implementar funciones más triviales (y duplicar / hacer referencia en cada sitio de uso), no menos.

En realidad, esto no me preocupa en absoluto. No creo que la cantidad de funciones sea prohibitiva, pero definitivamente estoy abierto a ver algunos contraejemplos. Recuerde que la API de la que se quejó la gente no era una para la que tenía que proporcionar una función, sino la original aquí: https://golang.org/pkg/sort/#Interface donde necesitaba crear un nuevo tipo que era simplemente su rebanada + escriba, y luego implemente 3 métodos en él. A la luz de las quejas y el dolor asociado con esta interfaz, se creó lo siguiente: https://golang.org/pkg/sort/#Slice , por mi parte, no tengo ningún problema con esta API y recuperaríamos las penalizaciones de rendimiento de esta bajo la propuesta que estamos discutiendo simplemente modificando la definición a func Slice(slice []a, less func(a, a) bool) .

En términos de la estructura de datos container/heap , no importa qué propuesta genérica acepte que necesita una reescritura completa. container/heap al igual que el paquete sort solo proporciona algoritmos además de su propia estructura de datos, pero ninguno de los paquetes posee la estructura de datos porque de lo contrario tendríamos []interface{} y el costos asociados con eso. Presumiblemente, los cambiaríamos ya que podrías tener un Heap que posee un segmento con un tipo concreto gracias a los genéricos, y esto es cierto en cualquiera de las propuestas que he visto aquí (incluida la mía) .

Estoy tratando de separar las diferencias en nuestras perspectivas sobre lo que he propuesto. Y creo que la raíz del desacuerdo (más allá de cualquier preferencia personal sintácticamente) es que no hay restricciones en los tipos genéricos. Pero todavía estoy tratando de averiguar lo que eso nos gana. Si la respuesta es que nada en lo que respecta al rendimiento puede usar una interfaz, entonces no hay mucho que pueda decir aquí.

Considere la siguiente definición de tabla hash:

// Hasher turns a key into a hash
type Hasher interface {
  func Hash() []byte
}

type HashTable v struct {
   Keys   []Hasher
   Values []v
}

// Note that the generic arguments must be repeated here and immediately
// understood without reading another line of code, which to me
// is a readability win over the sudden appearance of the K and V which are
// defined elsewhere in the code in the example below. This is of course because
// the tokenized type declarations with constraints are fairly painful in general
// and repeating them everywhere is simply too much.
func (h (*HashTable v)) Insert(key Hasher, value v) { ... }

¿Estamos diciendo que el []Hasher no funciona debido a problemas de rendimiento/almacenamiento y que para tener una implementación exitosa de Genéricos en Go debemos tener algo como lo siguiente?

// Without selecting another proposal I have no idea how the constraint might be defined or implemented so let's just pretend
type [K: Hasher, V] HashTable a struct {
   Keys   []K
   Values []V
}

func (h *HashTable) Insert(key K, value V) { ... }

Espero que veas de dónde vengo. Pero definitivamente es posible que no entienda las restricciones que desea imponer a cierto código. Tal vez hay casos de uso que no he considerado, sin embargo, espero llegar a una comprensión más completa de cuáles son los requisitos y cómo la propuesta los está fallando.

Tal vez estamos viendo lo que propuse de manera muy diferente, ya que desde mi perspectiva hace ambas cosas, por lo que puedo decir.

El "esto" en la sección que está citando se refiere al uso de interfaces. El problema no es que su propuesta tampoco lo haga, es que su propuesta no permite el polimorfismo restringido, lo que excluye la mayoría de los usos para ellos. Y la alternativa que sugirió para eso donde las interfaces, que en realidad tampoco abordan el caso de uso central para los genéricos (debido a las dos cosas que mencioné).

Por ejemplo, su propuesta (como se escribió originalmente) en realidad no permitía escribir un mapa genérico de ningún tipo, ya que eso requeriría poder al menos comparar claves usando == (que es una restricción, por lo que implementar un mapa requiere polimorfismo restringido).

A la luz de las quejas y el dolor asociado con esta interfaz, se creó lo siguiente: https://golang.org/pkg/sort/#Slice

Tenga en cuenta que esta interfaz aún no es posible en su propuesta de genéricos, ya que se basa en la reflexión de la longitud y el intercambio (por lo que, nuevamente, tiene una restricción en las operaciones de corte). Incluso si aceptamos esa API como el límite inferior de lo que los genéricos deberían poder lograr (muchas personas no lo harían. Todavía hay muchas quejas sobre la falta de seguridad de tipos en esa API), su propuesta no sería aprobada. esa barra

Pero también, nuevamente, está citando una respuesta a un punto específico que hizo, a saber, que podría obtener polimorfismo restringido al pasar literales de función en la API. Y esa forma específica que sugirió para evitar la falta de polimorfismo restringido requeriría implementar más o menos la API anterior. es decir, está citando mi respuesta a este argumento, que luego simplemente está repitiendo:

recuperaríamos las penalizaciones de rendimiento de esto bajo la propuesta que estamos discutiendo simplemente alterando la definición a func Slice(slice []a, less func(a, a) bool).

Sin embargo, esa es la API anterior. Está diciendo "mi propuesta no permite el polimorfismo restringido, pero eso no es un problema, porque simplemente no podemos usar genéricos y en su lugar usar las soluciones existentes (reflexión/interfaces)". Bueno, responder a "su propuesta no permite los casos de uso más básicos para los que la gente quiere genéricos" con "podemos hacer las cosas que la gente ya está haciendo sin genéricos para los casos de uso más básicos" no parece entendernos. en cualquier lugar, TBH. Una propuesta genérica que no te ayuda a escribir ni siquiera los tipos básicos de contenedores, sort, max... simplemente no parece valer la pena.

esto es cierto en cualquiera de las propuestas que he visto aquí (incluida la mía).

La mayoría de las propuestas de genéricos incluyen alguna forma de restringir los parámetros de tipo. es decir, para expresar "el parámetro de tipo debe tener un método Less", o "el parámetro de tipo debe ser comparable". El tuyo - AFAICT - no lo hace.

Considere la siguiente definición de tabla hash:

Su definición es incompleta. a) El tipo de clave también necesita igualdad yb) no está impidiendo el uso de diferentes tipos de clave. es decir, esto sería legal:

type hasherA uint64

func (a hasherA) Hash() []byte {
    b := make([]byte, 8)
    binary.BigEndian.PutUint64(b, uint64(a))
    return b
}

type hasherB string

func (b hasherB) Hash() []byte {
    return []byte(b)
}

h := new(HashTable int)
h.Insert(hasherA(42), 1)
h.Insert(hasherB("Hello world"), 2)

Sin embargo, no debería ser legal, ya que está utilizando diferentes tipos de claves. es decir, el contenedor no está verificado en la medida en que la gente quiere. Debe parametrizar la tabla hash sobre el tipo de clave y valor

type HashTable k v struct {
    Keys []k
    Values []v
}

func (h *(HashTable k v)) Insert(key k, value v) {
    // You can't actually do anything with k, as it's unconstrained. i.e. you can't hash it, compare it…
    // Implementing this is impossible in your proposal.
}

// If it weren't impossible, you'd get this:
h := new(HashTable hasherA int)
h[hasherA(42)] = 1
h[hasherB("Hello world")] = 2 // compile error - can't use hasherB as hasherA

O, si ayuda, imagine que está tratando de implementar un conjunto hash. Obtendría el mismo problema, pero ahora el contenedor resultante no tiene ninguna verificación de tipo adicional sobre interface{} .

Esta es la razón por la que su propuesta no aborda los casos de uso más básicos: se basa en interfaces para restringir el polimorfismo, pero en realidad no proporciona ninguna forma de verificar la coherencia de esas interfaces. Puede tener una verificación de tipo consistente o polimorfismo restringido, pero no ambos. Pero necesitas ambos.

que para tener una implementación exitosa de Genéricos en Go debemos tener algo como lo siguiente?

Es al menos lo que siento por eso, sí, más o menos. Si una propuesta no permite escribir contenedores con seguridad tipográfica o ordenar o... realmente no agrega nada al lenguaje existente que sea lo suficientemente significativo como para justificar el costo.

@Merovius Está bien. Creo que entiendo lo que quieres. Tenga en cuenta que sus casos de uso están muy lejos de lo que quiero. Realmente no estoy ansioso por tipos de contenedores seguros, aunque sospecho, como dijiste, que puede ser una opinión minoritaria. Algunas de las cosas más importantes que me gustaría ver son tipos de resultados en lugar de errores y una fácil manipulación de cortes sin duplicación o reflexión en todas partes que mi propuesta hace un trabajo razonable al abordar. Sin embargo, puedo ver cómo, desde su perspectiva, "no aborda los casos de uso más básicos" si su caso de uso básico es escribir contenedores genéricos sin el uso de interfaces,

Tenga en cuenta que esta interfaz aún no es posible en su propuesta de genéricos, ya que se basa en la reflexión de la longitud y el intercambio (por lo que, nuevamente, tiene una restricción en las operaciones de corte). Incluso si aceptamos esa API como el límite inferior de lo que los genéricos deberían poder lograr (muchas personas no lo harían. Todavía hay muchas quejas sobre la falta de seguridad de tipos en esa API), su propuesta no sería aprobada. esa barra

Al leer esto, está claro que ha entendido mal por completo la forma en que las porciones genéricas funcionarían/deberían funcionar según esta propuesta. Es a través de este malentendido que ha llegado a la falsa conclusión de que "esta interfaz aún no es posible en su propuesta". Bajo cualquier propuesta debe ser posible una rebanada genérica, eso es lo que pienso. Y len() en el mundo, como vi, se definiría como: func len(slice []a) , que es un argumento de segmento genérico, lo que significa que puede contar la longitud sin reflexión para cualquier segmento. Este es gran parte del objetivo de esta propuesta, como dije anteriormente (manipulación de corte fácil) y lamento no haber podido transmitir eso bien a través de los ejemplos que di y la esencia que hice. Una división genérica debería poder usarse tan fácilmente como lo es hoy []int , repito que cualquier propuesta que no aborde esto (intercambios de división/matriz, asignación, longitud, límite, etc. ) se está quedando corto en mi opinión.

Dicho todo esto, ahora tenemos muy claro cuáles son los objetivos de cada uno. Cuando propuse lo que hice, mucho dije que era simplemente una propuesta sintáctica y que los detalles eran súper confusos. Pero de todos modos entramos en detalles y uno de esos detalles terminó siendo la falta de restricciones, cuando lo escribí simplemente no los tenía en mente porque no son importantes para lo que me gustaría hacer. , no quiere decir que no podamos agregarlos o que no sean deseables. El principal problema de continuar con la sintaxis propuesta y tratar de introducir restricciones con calzador sería que la definición de un argumento genérico actualmente se repite (intencionalmente) por lo que no hay referencia al código en otro lugar para determinar las restricciones, etc. Si tuviéramos que introducir restricciones, yo No veo cómo podríamos mantener esto.

El mejor contraejemplo es la función de clasificación que discutimos antes.

type Sort(slice []a:Lesser, less func(a:Lesser, a:Lesser)) { ... }

Como puede ver, no hay una buena manera de hacer que esto suceda, y los enfoques de token-spam para Generics comienzan a sonar mejor nuevamente. Para definir restricciones sobre estos, necesitamos cambiar dos cosas de la propuesta original:

  • Tiene que haber una manera de señalar un argumento de tipo y darle restricciones.
  • Las restricciones deben durar más que una sola definición, tal vez ese alcance sea un tipo, tal vez ese alcance sea un archivo (el archivo en realidad suena bastante razonable).

Descargo de responsabilidad: la siguiente no es una enmienda real a la propuesta porque solo estoy lanzando símbolos aleatorios, solo estoy usando estas sintaxis como ejemplos para ilustrar lo que podríamos hacer para enmendar la propuesta tal como está originalmente.

// Decorator style, follows the definition of the type thorugh all
// of it's methods.
<strong i="14">@a</strong>: Lesser, Hasher, Equaler
func Sort(slice []a) { ... }
<strong i="15">@k</strong>: Equaler, Hasher
type HashTable k v struct

// Inline, follows the definition of the type through
// all of it's methods.
func [a: Hasher, Equaler] Sort(slice []a) { ... }
type [k: Hasher, Equaler] HashTable k v struct

// File-scope global style, if k appears as a generic argument
// it's constrained by this that appears at the top of the file underneath
// the imports but before any other code.
<strong i="16">@k</strong>: Equaler, Hasher

Una vez más, tenga en cuenta que nada de lo anterior realmente quiero agregar a la propuesta. Solo estoy mostrando qué tipo de construcciones podríamos usar para resolver el problema, y ​​cómo se ven es algo irrelevante en este momento.

La pregunta que debemos responder es: ¿Seguimos obteniendo valor de los argumentos genéricos implícitos? El punto principal de la propuesta era mantener la sensación limpia del lenguaje similar a Go, mantener las cosas simples, mantener las cosas lo suficientemente bajas en ruido eliminando el exceso de tokens. En los muchos casos en los que no se necesitan restricciones, por ejemplo, una función de mapa o la definición de un tipo de resultado, ¿se ve bien, se siente como Go, es útil? Suponiendo que las restricciones también están disponibles de una forma u otra.

func map(slice []a, mapper func(a) b) {
  for i := range slice {
    slice[i] = mapper(slice[i])
  }
}

type Result a b struct {
  Ok  a
  Err b
}

@aarondl intentaré explicarlo. La razón por la que necesita restricciones de tipo es porque esa es la única forma en que puede llamar a funciones o métodos en un tipo. considere el tipo sin restricciones a qué tipo puede ser, bueno, podría ser una cadena o un Int o cualquier cosa. Por lo tanto, no podemos llamar a ninguna función o método porque no conocemos el tipo. Podríamos usar un cambio de tipo y una reflexión en tiempo de ejecución para obtener el tipo y luego llamar a algunas funciones o métodos, pero esto es algo que queremos evitar con los genéricos. Cuando restringe un tipo, por ejemplo a es un Animal, podemos llamar a cualquier método definido para un animal en a .

En su ejemplo, sí, puede pasar una función de mapeador, pero esto dará como resultado que las funciones tomen muchos argumentos, y es básicamente como un lenguaje sin interfaces, solo funciones de primera clase. Para pasar cada función que va a usar en el tipo a obtendrá una lista muy larga de funciones en cualquier programa real, especialmente si está escribiendo principalmente código genérico para la inyección de dependencia, que desea hacer para minimizar el acoplamiento.

Por ejemplo, ¿qué pasa si la función que llama al mapa también es genérica? ¿Qué sucede si la función que llama es genérica, etc.? ¿Cómo definimos el mapeador si aún no conocemos el tipo de a ?

func m(slice []a) []b {
   mapper := func(x a) b {...}
   return map(slice, mapper)
}

¿Qué funciones podemos llamar a x cuando tratamos de definir mapper ?

@keean Entiendo el propósito y la función de las restricciones. Simplemente no los valoro tanto como cosas simples como estructuras de contenedores genéricos (no contenedores genéricos, por así decirlo) y segmentos genéricos y, por lo tanto, ni siquiera los incluí en la propuesta original.

Todavía creo principalmente que las interfaces son la respuesta correcta a problemas como el que estás hablando donde estás haciendo la inyección de dependencia, que simplemente no parece ser el lugar correcto para los genéricos, pero ¿quién soy yo para decirlo? La superposición entre sus responsabilidades es bastante grande en mi opinión, por lo que @Merovius y yo tuvimos que discutir si podíamos vivir sin ellos o no, y él me convenció de que serían útiles en algunos casos de uso, por lo que yo exploré un poco de lo que podríamos hacer para agregar la función a la propuesta que hice originalmente.

En cuanto a su ejemplo, no puede llamar a ninguna función en x. Pero aún puede operar en el segmento como cualquier otro segmento, lo que es tremendamente útil por sí solo. Tampoco estoy seguro de cuál es la función dentro de la función ... ¿quizás quisiste asignar a una var?

@aarondl
Gracias, arreglé la sintaxis, sin embargo, creo que el significado aún estaba claro.

Los ejemplos que di arriba usaban tanto polimorfismo paramétrico como interfaces para lograr cierto nivel de programación genérica, sin embargo, la falta de envío múltiple siempre pondrá un techo en el nivel de generalidad alcanzable. Como tal, parece que Go no proporcionará las funciones que busco en un idioma, eso no significa que no pueda usar Go para algunas tareas y, de hecho, ya lo estoy y funciona bien, incluso si he tenido para cortar y pegar código que realmente solo necesita una definición. Solo espero que en el futuro, si es necesario cambiar ese código, el desarrollador pueda encontrar todas las instancias pegadas.

Entonces tengo dudas sobre si la generalidad limitada posible sin cambios tan grandes en el idioma es una buena idea, considerando la complicidad que agregará. ¿Tal vez sea mejor que Go siga siendo simple, y las personas puedan agregar macros como preprocesamiento u otros lenguajes que se compilen en Go, para proporcionar estas funciones? Por otro lado, agregar polimorfismo paramétrico sería un buen primer paso. Permitir que esos parámetros de tipo estén restringidos sería un buen próximo paso. Luego, podría agregar parámetros de tipo asociados a las interfaces, y tendría algo razonablemente genérico, pero eso es probablemente lo más lejos que puede llegar sin el envío múltiple. Al dividirse en características más pequeñas separadas, supongo que aumentaría la posibilidad de que las acepten.

@keean
¿Es el envío múltiple todo lo necesario? Muy pocos idiomas lo admiten de forma nativa. Incluso C ++ no lo admite. C# lo admite un poco a través dynamic pero nunca lo he usado en la práctica y la palabra clave en general es muy rara en el código real. Los ejemplos que recuerdo tratan con algo como el análisis de JSON, no con la escritura de genéricos.

¿Es el envío múltiple todo lo necesario?

En mi humilde opinión, creo que @keean habla sobre el envío múltiple estático proporcionado por typeclasses/interfaces.
Esto incluso se proporciona en C ++ mediante la sobrecarga de métodos (no sé para C #)

Lo que quiere decir es un envío múltiple dinámico, que es bastante engorroso en lenguajes estáticos sin tipos de unión. Los lenguajes dinámicos evitan este problema al omitir la verificación de tipo estático (inferencia de tipo parcial para lenguajes dinámicos, lo mismo para el tipo "dinámico" de C#).

¿Se podría proporcionar un tipo como "solo" un parámetro?

func Append(t, t2 type, arr []t, value t2) []t {
    v := t(value) // conversion
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

@Inuart escribió:

¿Se podría proporcionar un tipo como "solo" un parámetro?

Cuestionable hasta qué punto esto sería posible o deseable en go

Lo que desea podría lograrse en su lugar si se admiten restricciones genéricas:

func Append(arr []t, value s) []t  requires Convertible<s,t>{
    v := t(value) // conversion
    return append(arr, v)
}

var arr []int64
v := 0.5

arr = Append(arr, v)

También esto debería ser posible con restricciones, también:

func convert(value s) t requires Convertible<s,t>{
    return t(value);
}

f:float64:=2.0

i:int64=convert(f)

Por lo que vale, nuestro lenguaje Genus admite el envío múltiple. Los modelos para una restricción pueden proporcionar múltiples implementaciones a las que se envían.

Entiendo que la notación Convertible<s,t> es necesaria para la seguridad del tiempo de compilación, pero tal vez podría degradarse a una verificación de tiempo de ejecución

func Append(t, t2 type, arr []t, value t2) []t {
    v, ok := t(value) // conversion
    if !ok {
        panic(...) // or return an err
    }
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

Pero esto parece más azúcar de sintaxis para reflect .

@Inuart , el punto es que el compilador puede verificar que el tipo implementa la clase de tipos en el momento de la compilación, por lo que la verificación en tiempo de ejecución es innecesaria. El beneficio es un mejor rendimiento (la llamada abstracción de costo cero). Si se trata de una verificación de tiempo de ejecución, también puede usar reflect .

@creker

¿Es el envío múltiple todo lo necesario?

Estoy demasiado en mente acerca de esto. Por un lado, el envío múltiple (con clases de tipos de parámetros múltiples) no funciona bien con los existenciales, lo que 'Go' llama 'valores de interfaz'.

type Equals<T> interface {eq(right T) bool}
(left I) eq(right I) bool {return left == right}
(left I) eq(right F) bool {return false}
(left F) eq(right I) bool {return false}
(left F) eq(right F) bool {return left == right}

func main() {
    x := []Equals<?>{I{2}, F{4.0}, I{2}, F{4.0}}
}

No podemos definir la porción de Equals porque no tenemos forma de indicar que el parámetro de la derecha es de la misma colección. Ni siquiera podemos hacer esto en Haskell:

data Equals = forall a . IEquals a a => Equals a

Esto no es bueno porque solo permite comparar un tipo consigo mismo.

data Equals = forall a b . IEquals a b => Equals a

Esto no es bueno porque no tenemos forma de restringir b para que sea otro existencial en la misma colección que a (si a incluso está en una colección).

Sin embargo, hace que sea muy fácil de extender con un nuevo tipo:

(left K) eq(right I) bool {return false}
(left K) eq(right F) bool {return false}
(left I) eq(right K) bool {return false}
(left F) eq(right K) bool {return false}
(left K) eq(right K) bool {return left == right}

Y esto sería aún más conciso con instancias predeterminadas o especialización.

Por otro lado, podemos reescribir esto en 'Ir' que funciona ahora mismo:

package main

type I struct {v int}
type F struct {v float32}

type EqualsInt interface {eqInt(left I) bool}
func (right I) eqInt (left I) bool {return left == right}
func (right F) eqInt (left I) bool {return false}

type EqualsFloat interface {eqFloat(left F) bool}
func (right I) eqFloat (left F) bool {return false}
func (right F) eqFloat (left F) bool {return left == right}

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

type EqualsLeft interface {eq(right EqualsRight) bool}
func (left I) eq (right EqualsRight) bool {return right.eqInt(left)}
func (left F) eq (right EqualsRight) bool {return right.eqFloat(left)}

type Equals interface {
    EqualsLeft
    EqualsRight
}

func main() {
    x := []Equals{I{2}, F{4.0}, I{2}, F{4.0}}
    println(x[0].eq(x[1]))
    println(x[1].eq(x[0]))
    println(x[0].eq(x[2]))
    println(x[1].eq(x[3]))
}

Esto funciona bien con el existencial (valor de interfaz), sin embargo, es mucho más complejo, más difícil ver qué está pasando y cómo funciona, y tiene la gran restricción de que necesitamos una interfaz por tipo y necesitamos codificar lo aceptable. tipos del lado derecho como este:

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

Lo que significa que tendríamos que modificar la fuente de la biblioteca para agregar un nuevo tipo porque la interfaz EqualsRight no es extensible.

Entonces, sin interfaces de parámetros múltiples, no podemos definir operadores genéricos extensibles como la igualdad. Con las interfaces de parámetros múltiples, los existenciales (valores de interfaz) se vuelven problemáticos.

Mi problema principal con muchas de las sintaxis propuestas (¿sintaxis?) Blah[E] es que el tipo subyacente no muestra ninguna información sobre la contención de genéricos.

Por ejemplo:

type Comparer[C] interface {
    Compare(other C) bool
}
// or
type Comparer c interface {
    Compare(other c) bool
}
...

Esto significa que estamos declarando un nuevo tipo que agrega más información al tipo subyacente. ¿No es el objetivo de la declaración type definir un nombre basado en otro tipo?

Yo propondría una sintaxis más en la línea de

type Comparer interface[C] {
    Compare(other C) bool
}

Esto significa que realmente Comparer es solo un tipo basado en interface[C] { ... } , y interface[C] { ... } es, por supuesto, su propio tipo separado de interface { ... } . Esto le permite usar una interfaz genérica sin nombrarla, si lo desea (lo cual está permitido con las interfaces normales). Creo que esta solución es un poco más intuitiva y funciona bien con el sistema de tipos de Go, aunque corrígeme si me equivoco.

Nota: la declaración de un tipo genérico solo se permitiría en interfaces, estructuras y funciones con las siguientes sintaxis:
interface[G] { ... }
struct[G] { ... }
func[G] (vars...) { ... }

Luego, "implementar" los genéricos tendría las siguientes sintaxis:
interface[G] { ... }[string]
struct[G] { ... }[string]
func[G] (vars...) { ... }[int](args...)

Y con algunos ejemplos para que quede un poco más claro:

Interfaces

package add

type Adder interface[E] {
    // Adds the element and returns the size
    Add(elem E) int
}

// Adds the integer 5 to any implementation of Adder[int].
func AddFiveTo(a Adder[int]) int {
    return a.Add(5)
}

estructuras

package heap

type List struct[T] {
    slice []T
}

func (l *List) Add(elem T) { // T is a type defined by the receiver
    l.slice = append(l.slice, elem)
}

Funciones

func[A] AddManyTo(a Adder[A], many ...A) {
    for _, each := range a {
        a.Add(each)
    }
}

Esto es en respuesta al borrador de contratos de Go2 y usaré su sintaxis, pero lo estoy publicando aquí ya que se aplica a cualquier propuesta de polimorfismo paramétrico.

No se debe permitir la incrustación de parámetros de tipo.

Considerar

type X(type T C) struct {
  R // A regular type with method Foo()
  T // Some type parameter
}
// X defines some methods other than Foo(),
// some of which invoke Foo.

para algún tipo arbitrario R y algún contrato arbitrario C que no contiene Foo() .

T tendrá todos los selectores requeridos por C pero una instancia particular de T también puede tener otros selectores arbitrarios, incluyendo Foo .

Digamos que Bar es una estructura, admisible bajo C , que tiene un campo llamado Foo .

X(Bar) podría ser una instanciación ilegal. Sin una forma de especificar el contrato de que un tipo no tiene un selector, esto tendría que ser una propiedad inferida.

Los métodos de X(Bar) podrían seguir resolviendo las referencias a Foo como X(Bar).R.Foo . Esto hace que sea posible escribir el tipo genérico, pero podría resultar confuso para un lector que no esté familiarizado con la minuciosidad de las reglas de resolución. Fuera de los métodos de X , el selector permanecería ambiguo, por lo que, mientras que interface { Foo() } no depende de los parámetros de X , algunas instancias de X serían no satisfacerlo.

No permitir la incrustación de un parámetro de tipo es más simple.

(Sin embargo, si se permite esto, el nombre de campo sería T por la misma razón que el nombre de campo de un S incrustado definido como type S = io.Reader es S y no Reader sino también porque el tipo que crea instancias T no necesariamente necesita tener un nombre).

@jimmyfrasche Creo que los campos incrustados con tipos genéricos son lo suficientemente útiles como para permitirlos, incluso si puede haber un poco de incomodidad en algunos lugares. Mi sugerencia sería asumir en todo el código genérico que el tipo incrustado ha definido todos los campos y métodos posibles en todos los niveles posibles, de modo que dentro del código genérico se borren todos los métodos y campos incrustados de tipos no genéricos.

Así dado:

type R struct(type T) {
    io.Reader
    T
}

los métodos en R no podrían invocar Read on R sin indirectamente a través de Reader. Por ejemplo:

func (r R) Do() {
     r.Read(buf)     // Illegal
     r.Reader.Read(buf)  // ok
}

El único inconveniente que puedo ver de esto es que el tipo dinámico puede contener más miembros que el tipo estático. Por ejemplo:

func (r R) Do() {
    var x interface{} = r
    x.(io.Reader)    // Succeeds
}

@rogpeppe

El único inconveniente que puedo ver de esto es que el tipo dinámico puede contener más miembros que el tipo estático.

Este es el caso con los parámetros de tipo directamente, por lo que creo que también debería estar bien con los tipos paramétricos. Creo que la solución al problema que presentó @jimmyfrasche podría ser poner el conjunto de métodos deseado del tipo parametrizado en el contrato.

contract C(t T) {
  interface { Foo() } (X(T){})
  // ...
}

type X(type T C) struct {
  R // A regular type with method Foo()
  T // Some type parameter
}
// X defines some methods other than Foo(),
// some of which invoke Foo.

Esto permitiría llamar a $#$ Foo en X directamente. Por supuesto, esto iría en contra de la regla de "no hay nombres locales en los contratos"...

@stevenblenkinsop Hmm, es posible, aunque incómodo, hacerlo sin referirse a X

contract C(t T) {
  struct{ R; T }{}.Foo
}

C todavía está ligado a la implementación de X aunque un poco más flexible.

Si no haces eso, y escribes

func (x X(T)) Fooer() interface { Foo() } {
  return x
}

¿compila? No estaría bajo la regla de @rogpeppe , que parece que también debería adoptarse cuando no se hace la garantía en el contrato. Pero entonces, ¿se aplica solo cuando incrusta un argumento de tipo sin un contrato suficiente o para todas las incrustaciones?

Sería más fácil rechazarlo.

Empecé a trabajar en esta propuesta antes de que se anunciara el borrador de Go2.

Estaba listo para descartar felizmente el mío cuando vi el anuncio, pero todavía estoy inquieto con la complejidad del borrador, así que terminé el mío. Es menos poderoso pero más simple. Si nada más, puede tener algunos bits que vale la pena robar.

Amplía la sintaxis de las propuestas anteriores de @ianlancetaylor , ya que eso era lo que estaba disponible cuando comencé. Eso no es fundamental. Podría ser reemplazado por una sintaxis de (type T etc. o algo equivalente. Solo necesitaba algo de sintaxis como notación para la semántica.

Se encuentra aquí: https://gist.github.com/jimmyfrasche/656f3f47f2496e6b49e041cd8ac716e4

La regla tendría que ser que cualquier método promovido desde una profundidad mayor que la de un parámetro de tipo incrustado no se puede llamar a menos que (1) se conozca la identidad del argumento de tipo o (2) se afirme que el método se puede llamar en el exterior tipo por el contrato que restringe el parámetro de tipo. El compilador también podría determinar los límites superior e inferior de la profundidad que debe tener un método promocionado dentro del tipo externo O , y usarlos para determinar si se puede llamar al método en un tipo que incrusta O , es decir, si existe potencial de conflicto con otros métodos promovidos o no. Algo similar también se aplicaría a cualquier parámetro de tipo que se afirme que tiene métodos invocables, donde los rangos de profundidad de los métodos dentro del parámetro de tipo serían [0, inf).

Incrustar parámetros de tipo parece demasiado útil para prohibirlo por completo. Por un lado, permite una composición transparente, lo que no permite el patrón de interfaces incrustadas.

También encontré un uso potencial en la definición de contratos. Si desea poder aceptar un valor de tipo T (que podría ser un tipo de puntero) que podría tener métodos definidos en *T , y desea poder poner ese valor en una interfaz, no necesariamente puede poner T en la interfaz, ya que los métodos pueden estar en *T , y no necesariamente puede poner *T en la interfaz porque T podría ser en sí mismo un tipo de puntero (y por lo tanto *T podría tener un conjunto de métodos vacío). Sin embargo, si tuviera un envoltorio como

type Wrapper(type T) { T }

podría poner *Wrapper(T) en la interfaz en todos los casos si su contrato dice que satisface la interfaz.

¿No puedes simplemente hacer

type Interface interface {
  SomeMethod(int) error
}

contract MightBeAPointer(t T) {
  Interface(t)
}

func Example(type T MightBeAPointer)(v T) {
  var i Interface = v
  // ...
}

Estoy tratando de manejar el caso en el que alguien llama

type S struct{}
func (s *S) SomeMethod(int) error { ... }
...
var s S
Example(S)(s)

Esto no funcionará porque S no se puede convertir a Interface , solo *S puede.

Obviamente, la respuesta podría ser "no hagas eso". Sin embargo, la propuesta de contratos describe contratos como:

contract Contract(t T) {
    var _ error = t.SomeMethod(int(0))
}

S cumpliría este contrato debido al direccionamiento automático, al igual que *S . Lo que estoy tratando de abordar es la brecha de capacidad entre las llamadas a métodos y las conversiones de interfaz en los contratos.

De todos modos, esto es un poco tangente, mostrando un uso potencial para incrustar parámetros de tipo.

Volver a incrustar, creo que "puede incrustarse en una estructura" es otra restricción que los contratos tendrían que capturar si se permitieran.

Considerar:

contract Embeddable(type X, Y) {
    type S struct {
        X
        Y
    }
}

type Embedded(type First, Second Embeddable) struct {
        First
        Second
}

// Error: First and Second both provide method Read.
// That must be diagnosed to the Embeddable contract, not the definition of Embedded itself.
type Boom = Embedded(*bytes.Buffer, *strings.Reader)

Se permite la incrustación de tipos de @bcmills con selectores ambiguos, por lo que no estoy seguro de cómo se supone que debe interpretarse ese contrato.

En cualquier caso, si solo está incrustando tipos conocidos, está bien. Si solo está incrustando parámetros de tipo, está bien. El único caso que se vuelve extraño es cuando incrusta uno o más tipos conocidos Y uno o más parámetros de tipo y luego solo cuando los selectores de los tipos conocidos y los argumentos de tipo no son disjuntos

Se permite la incrustación de tipos de @bcmills con selectores ambiguos, por lo que no estoy seguro de cómo se supone que debe interpretarse ese contrato.

Mmm, buen punto. Me falta una restricción más para desencadenar el error.¹

contract Embeddable(type X, Y) {
    type S struct {
        X
        Y
    }
    var _ io.Reader = S{}
}

¹ https://play.golang.org/p/3wSg5aRjcQc

Eso requiere uno de X o Y pero no ambos para ser un io.Reader . Es interesante que el sistema de contratos sea lo suficientemente expresivo como para permitir eso. Me alegro de no tener que descifrar las reglas de inferencia de tipos para semejante bestia.

Pero ese no es realmente el problema.

es cuando lo haces

type S (type T C) struct {
  io.Reader
  T
}
func (s *S(T)) X() io.Reader {
  return s
}

Eso debería fallar al compilar porque T podría tener un selector Read a menos que C lo tenga

struct{ io.Reader; T }.Read

Pero entonces, ¿cuáles son las reglas cuando C no garantiza que los conjuntos de selectores estén separados y S no hace referencia a los selectores? ¿Es posible que cada instanciación S satisfaga una interfaz excepto los tipos que crean un selector ambiguo?

¿Es posible que cada instanciación S satisfaga una interfaz excepto los tipos que crean un selector ambiguo?

Sí, ese parece ser el caso. Me pregunto si eso implica algo más profundo... 🤔

No he podido construir nada irremediablemente desagradable, pero la asimetría es bastante desagradable y me hace sentir incómodo:

type I interface { /* ... */ }
a := G(A) // ok, A satisfies contract
var _ I = a // ok, no selector overlap
b := G(B) // ok, B satisfies contract
var _ = b // error, selector overlap

Me preocupan los mensajes de error cuando G0(B) usa un G1(B) usa un . . . usa un Gn(B) y Gn es el que causa el error. . . .

FTR, no necesita pasar por el problema de los selectores ambiguos para desencadenar errores de tipo con la incrustación.

// Error: Duplicate field name Reader
type Boom = Embedded(*bytes.Reader, *strings.Reader)

Supone que el nombre del campo incrustado se basa en el tipo de argumento, mientras que es más probable que sea el nombre del parámetro de tipo incrustado. Esto es como cuando incrusta un alias de tipo y el nombre del campo es el alias en lugar del nombre del tipo al que alias.

En realidad, esto se especifica en el borrador del diseño en la sección sobre tipos parametrizados :

Cuando un tipo parametrizado es una estructura y el parámetro de tipo está incrustado como un campo en la estructura, el nombre del campo es el nombre del parámetro de tipo, no el nombre del argumento de tipo.

type Lockable(type T) struct {
    T
    mu sync.Mutex
}

func (l *Lockable(T)) Get() T {
    l.mu.Lock()
    defer l.mu.Unlock()
    return l.T
}

(Nota: esto funciona mal si escribe Lockable(X) en la declaración del método: ¿debería el método devolver lT o lX? Tal vez deberíamos simplemente prohibir la incrustación de un parámetro de tipo en una estructura).

Solo estoy sentado aquí al margen y observando. Pero también un poco preocupado.

Una cosa que no me avergüenza decir es que el 90% de esta discusión está por encima de mi cabeza.

Parece que 20 años de ganarme la vida escribiendo software sin saber qué son los genéricos o el polimorfismo paramétrico no me han impedido hacer el trabajo.

Lamentablemente, solo me tomé el tiempo hace aproximadamente un año para aprender Go. Hice la suposición falsa de que era una curva de aprendizaje empinada y que tomaría demasiado tiempo volverse productivo.

No podría haber estado más equivocado.

Pude aprender lo suficiente sobre Go para crear un microservicio que destruyó por completo el servicio node.js con el que estaba teniendo problemas de rendimiento en menos de un fin de semana.

Irónicamente, solo estaba jugando. No era particularmente serio acerca de conquistar el mundo con Go.

Y, sin embargo, en un par de horas, me encontré sentado de mi postura encorvada y derrotada, como si estuviera en el borde de mi asiento viendo un thriller de acción. La API que estaba construyendo se unió muy rápido. Me di cuenta de que este era un lenguaje en el que valía la pena invertir mi precioso tiempo, porque obviamente era muy pragmático en su diseño.

Y eso es lo que me encanta de Go. Es muy rápido..... Para aprender. Todos aquí conocemos sus capacidades de rendimiento. Pero la velocidad a la que se puede aprender no tiene comparación con los otros 8 idiomas que he aprendido a lo largo de los años.

Desde entonces, he estado cantando alabanzas a Go y he conseguido que 4 desarrolladores más se enamoren de él. Simplemente me siento con ellos durante un par de horas y construyo algo. Los resultados hablan por sí solos.

Simplicidad y rapidez para aprender. Estas son las verdaderas características asesinas del lenguaje.

Los lenguajes de programación que requieren meses de arduo aprendizaje a menudo no retienen a los desarrolladores que buscan atraer. Tenemos trabajo que hacer y empleadores que quieren ver el progreso a diario (gracias ágiles, lo aprecio)

Entonces, hay dos cosas que espero que el equipo de Go pueda tener en cuenta:

1) ¿Qué problema del día a día buscamos resolver?

Parece que no puedo encontrar un ejemplo del mundo real, con un problema que se solucione con genéricos, o como se llamen.

Ejemplos estilo libro de cocina de tareas cotidianas que son problemáticas, con un ejemplo de cómo podrían mejorarse con estas propuestas de cambio de idioma.

2) Mantenlo simple, como todas las otras excelentes características de Go

Hay algunos comentarios increíblemente inteligentes aquí. Pero estoy seguro de que la mayoría de los desarrolladores que usan Go en el día a día para la programación general, como yo, están perfectamente contentos y son productivos con las cosas tal como están.

¿Quizás un argumento del compilador para habilitar funciones tan avanzadas? '--duro'

Me entristecería mucho si tuviéramos un impacto negativo en el rendimiento del compilador. solo dilo

Y eso es lo que me encanta de Go. Es muy rápido..... Para aprender. Todos aquí conocemos sus capacidades de rendimiento. Pero la velocidad a la que se puede aprender no tiene comparación con los otros 8 idiomas que he aprendido a lo largo de los años.

Estoy completamente de acuerdo. La combinación de potencia con simplicidad en un lenguaje completamente compilado es algo completamente único. Definitivamente no quiero que Go pierda eso, y por mucho que quiera los genéricos, no creo que valgan la pena a ese costo. Sin embargo, no creo que sea necesario perder eso.

Parece que no puedo encontrar un ejemplo del mundo real, con un problema que se solucione con genéricos, o como se llamen.

Tengo dos casos de uso primarios principales para los genéricos: eliminación repetitiva segura de tipos de estructuras de datos complejas, como árboles binarios, conjuntos y sync.Map , y la capacidad de escribir funciones seguras de tipo _compile-time_ que operan según puramente en la funcionalidad de sus argumentos, en lugar de su diseño en la memoria. Hay algunas cosas más sofisticadas que no me importaría poder hacer, pero no me importaría _no_ poder hacerlas si es imposible agregar soporte para ellas sin romper por completo la simplicidad del lenguaje.

Para ser honesto, ya hay características en el lenguaje de las que se puede abusar bastante. La razón principal por la que _no_ se abusa de ellos con tanta frecuencia, creo, es la cultura Go de escribir código 'idiomático', combinado con la biblioteca estándar que proporciona ejemplos limpios y fáciles de encontrar de dicho código, en su mayor parte. Lograr un buen uso de los genéricos en la biblioteca estándar definitivamente debería ser una prioridad cuando se implementen.

@camstuart

Parece que no puedo encontrar un ejemplo del mundo real, con un problema que se solucione con genéricos, o como se llamen.

Los genéricos son para que no tenga que escribir el código usted mismo. Por lo tanto, nunca más tendrá que implementar otra lista vinculada, árbol binario, deque o cola de prioridad. Nunca necesitará implementar un algoritmo de ordenación, un algoritmo de partición o un algoritmo de rotación, etc. Las estructuras de datos se convierten en colecciones estándar que componen (un Mapa de listas, por ejemplo), y el procesamiento se convierte en algoritmos estándar que componen (necesito ordenar los datos, particionar, y girar). Si puede reutilizar estos componentes, la tasa de error disminuye, porque cada vez que vuelve a implementar una cola de prioridad o un algoritmo de partición, existe la posibilidad de que se equivoque y presente un error.

Los genéricos significan que escribe menos código y reutiliza más. Significan que las funciones de biblioteca estándar y bien mantenidas y los tipos de datos abstractos se pueden usar en más situaciones, por lo que no tiene que escribir los suyos propios.

Aún mejor, técnicamente todo eso se puede hacer en Go ahora mismo, pero solo con una pérdida casi total de la seguridad de tipos en tiempo de compilación _y_ con una sobrecarga de tiempo de ejecución potencialmente importante. Los genéricos te permiten hacerlo sin ninguno de esos inconvenientes.

Implementación de funciones genéricas:

/*

* "generic" is a KIND of types, just like "struct", "map", "interface", etc...
* "T" is a generic type (a type of kind generic).
* var t = T{int} is a value of type T, values of generic types looks like a "normal" type

*/

type T generic {
    int
    float64
    string
}

func Sum(a, b T{}) T{} {
    return a + b
}

Llamador de función:

Sum(1, 1) // 2
// same as:
Sum(T{int}(1), T{int}(1)) // 2

Implementación de estructura genérica:

type ItemT generic {
    interface{}
}

type List struct {
    l []ItemT{}
}

func NewList(t ItemT) *List {
    l := make([]t)
    return &List{l}
}

func (p *List) Push(item ItemT{}) {
    p.l = append(p.l, item)
}

Llamador:

list := NewList(ItemT{int})
list.Push(42)

Como alguien que acaba de aprender Swift y no le gusta, pero con mucha experiencia en otros lenguajes como Go, C, Java, etc.; Realmente creo que los genéricos (o plantillas, o como quieras llamarlo) no son algo bueno para agregar al lenguaje Go.

Tal vez tengo más experiencia con la versión actual de Go, pero para mí esto se siente como una regresión a C++ en el sentido de que es más difícil entender el código que otras personas han escrito. El clásico marcador de posición T para tipos hace que sea muy difícil entender qué intenta hacer una función.

Sé que esta es una solicitud de función popular, por lo que puedo lidiar con ella si aterriza, pero quería agregar mis 2 centavos (opinión).

@jlubawy
¿Conoce otra forma en la que nunca tenga que implementar una lista vinculada o un algoritmo de clasificación rápida? Como señala Alexander Stepanov, la mayoría de los programadores no pueden definir correctamente las funciones "min" y "max", entonces, ¿qué esperanza tenemos de implementar correctamente algoritmos más complejos sin mucho tiempo de depuración? Preferiría sacar versiones estándar de estos algoritmos de una biblioteca y simplemente aplicarlos a los tipos que tengo. ¿Qué alternativa hay?

@jlubawy

o plantillas, o como quieras llamarlo

Todo depende de la implementación. si estamos hablando de plantillas de C++, entonces sí, son difíciles de entender en general. Incluso escribirlos es difícil. Por otro lado, si tomamos los genéricos de C#, eso es completamente diferente. El concepto en sí no es un problema aquí.

Si no lo sabía, el Go Team ha anunciado un borrador de Go 2.0:
https://golang.org/s/go2designs

Hay un borrador del diseño genérico en Go 2.0 (contrato). Es posible que desee echar un vistazo y dar su opinión sobre su Wiki .

Esta es la sección correspondiente:

Genéricos

Después de leer el borrador, pregunto:

Por qué

T:Agregable

significa "un tipo T que implementa el contrato Addable"? ¿Por qué agregar un nuevo
concepto cuando ya tenemos INTERFACES para eso? La asignación de interfaces es
verificado en tiempo de compilación, por lo que ya tenemos los medios para no necesitar ningún
concepto adicional aquí. Podemos usar este término para decir algo como: Cualquier
tipo T implementando la interfaz Addable. Además, T:_ o T:Any
(siendo Any una palabra clave especial o un alias integrado de interfaz{}) serviría
el truco.

Simplemente no sé por qué volver a implementar la mayoría de las cosas así. no hace
sentido y SERÁ redundante (como redundante es el nuevo manejo de errores wrt
el manejo de pánicos).

2018-09-14 6:15 GMT-05:00 Koala Yeung [email protected] :

Si no lo sabía, el Go Team ha anunciado un borrador de Go 2.0:
https://golang.org/s/go2designs

Hay un borrador del diseño genérico en Go 2.0 (contrato). es posible que desee
para echar un vistazo y dar retroalimentación
https://github.com/golang/go/wiki/Go2GenericsFeedback en su wiki
https://github.com/golang/go/wiki/Go2GenericsFeedback .

Esta es la sección correspondiente:

Genéricos


Estás recibiendo esto porque comentaste.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/15292#issuecomment-421326634 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AlhWhS8xmN5Y85_aUKT5VnutoOKUAaLLks5ua4_agaJpZM4IG-xv
.

--
Esta es una prueba para que las firmas de correo se utilicen en TripleMint

Editar: "[...] haría el truco SI NO NECESITA NINGÚN REQUISITO PARTICULAR EN
EL ARGUMENTO DEL TIPO".

2018-09-17 11:10 GMT-05:00 Luis Masuelli [email protected] :

Después de leer el borrador, pregunto:

Por qué

T:Agregable

significa "un tipo T que implementa el contrato Addable"? ¿Por qué agregar un nuevo
concepto cuando ya tenemos INTERFACES para eso? La asignación de interfaces es
verificado en tiempo de compilación, por lo que ya tenemos los medios para no necesitar ningún
concepto adicional aquí. Podemos usar este término para decir algo como: Cualquier
tipo T implementando la interfaz Addable. Además, T:_ o T:Any
(siendo Any una palabra clave especial o un alias integrado de interfaz{}) serviría
el truco.

Simplemente no sé por qué volver a implementar la mayoría de las cosas así. no hace
sentido y SERÁ redundante (como redundante es el nuevo manejo de errores wrt
el manejo de pánicos).

2018-09-14 6:15 GMT-05:00 Koala Yeung [email protected] :

Si no lo sabía, el Go Team ha anunciado un borrador de Go 2.0:
https://golang.org/s/go2designs

Hay un borrador del diseño genérico en Go 2.0 (contrato). Puedes
quiere echar un vistazo y dar su opinión
https://github.com/golang/go/wiki/Go2GenericsFeedback en su wiki
https://github.com/golang/go/wiki/Go2GenericsFeedback .

Esta es la sección correspondiente:

Genéricos


Estás recibiendo esto porque comentaste.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/15292#issuecomment-421326634 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AlhWhS8xmN5Y85_aUKT5VnutoOKUAaLLks5ua4_agaJpZM4IG-xv
.

--
Esta es una prueba para que las firmas de correo se utilicen en TripleMint

--
Esta es una prueba para que las firmas de correo se utilicen en TripleMint

@luismasuelli-jobsity Si leí el historial de implementaciones genéricas en Go correctamente, entonces parece que la razón para introducir Contratos es porque no querían una sobrecarga de operadores en las Interfaces.

Una propuesta anterior que finalmente fue rechazada usaba interfaces para restringir el polimorfismo paramétrico, pero parece haber sido rechazada porque no se podían usar operadores comunes como '+' en tales funciones porque no se puede definir en una interfaz. Los contratos le permiten escribir t == t o t + t para que pueda indicar el tipo que debe admitir la igualdad o la suma, etc.

Editar: Además, Go no admite interfaces de parámetros de tipo múltiple, por lo que, de alguna manera, Go ha separado la clase de tipo en dos cosas separadas, los contratos que relacionan los parámetros de tipo de funciones entre sí y las interfaces que proporcionan métodos. Lo que pierde es la capacidad de seleccionar una implementación de clase de tipo basada en múltiples tipos. Podría decirse que es más simple si solo necesita usar interfaces o contratos, pero más complejo si necesita usar ambos juntos.

¿Por qué T:Addable significa "un tipo T que implementa el contrato Addable"?

En realidad, eso no es lo que significa; solo se ve de esa manera para un argumento de tipo. En otra parte del borrador, se comenta que solo puede tener un contrato por función, y aquí es donde aparece la principal diferencia. Los contratos son en realidad declaraciones sobre los tipos de la función, no solo los tipos de forma independiente. Por ejemplo, si tienes

func Example(type K, V someContract)(k K, v V) V

puedes hacer algo como

contract someContract(k K, v V) {
  k.someMethod(v)
}

Esto simplifica enormemente la coordinación de varios tipos sin tener que especificar de forma redundante los tipos en la firma de la función. Recuerde, están tratando de evitar el 'patrón genérico que se repite curiosamente'. Por ejemplo, la misma función con interfaces parametrizadas usadas para restringir los tipos sería algo como

type someMethoder(V) interface {
  someMethod(V)
}

func Example(type K: someMethoder(V), V)(k K, v V) V

Esto es un poco incómodo. Sin embargo, la sintaxis del contrato le permite hacer esto si lo necesita, porque el compilador completa automáticamente los 'argumentos' del contrato si el contrato tiene la misma cantidad de ellos que la función tiene parámetros de tipo. Sin embargo, puede especificarlos manualmente si lo desea, lo que significa que _podría_ hacer func Example(type K, V someContract(K, V))(k K, v V) V si realmente quisiera, aunque no es particularmente útil en esta situación.

Una forma de aclarar que los contratos se refieren a funciones completas, no a argumentos individuales, sería simplemente asociarlos en función del nombre. Por ejemplo,

contract Example(k K, v V) {
  k.someMethod(v)
}

func Example(type K, V)(k K, v V) V

sería lo mismo que el anterior. Sin embargo, la desventaja es que los contratos no serían reutilizables y perdería la capacidad de especificar los argumentos del contrato manualmente.

Editar: para mostrar más por qué quieren resolver el patrón que se repite curiosamente, considere el problema del camino más corto al que se refirieron. Con interfaces parametrizadas, la definición termina pareciendo

type E(Node) interface {
  Nodes() []Node
}

type N(Edge) interface {
  Edges() (from, to Edge)
}

type Graph(type Node: N(Edge), Edge: E(Node)) struct { ... }
func New(type Node: N(Edge), Edge: E(Node))(nodes []Node) *Graph(Node, Edge) { ... }
func (*Graph(Node, Edge)) ShortestPath(from, to Node) []Edge { ... }

Personalmente, me gusta más la forma en que se especifican los contratos por funciones. No estoy _demasiado_ interesado en tener simplemente cuerpos de funciones 'normales' como la especificación real del contrato, pero creo que muchos de los problemas potenciales podrían resolverse introduciendo algún tipo de simplificador similar a gofmt que simplifique automáticamente los contratos para usted, eliminando partes extrañas. Luego, _podría_ simplemente copiar el cuerpo de una función en él, simplificarlo y modificarlo desde allí. Sin embargo, desafortunadamente, no estoy seguro de cuán posible será implementar esto.

Sin embargo, algunas cosas seguirán siendo un poco incómodas de especificar, y la aparente superposición entre contratos e interfaces todavía parece un poco extraña.

Encuentro la versión "CRTP" mucho más clara, más explícita y más fácil de trabajar (no es necesario crear contratos que solo existen para definir la relación entre contratos preexistentes sobre un conjunto de variables). Es cierto que eso podría ser solo los muchos años de familiaridad con la idea.

aclaraciones. Por el proyecto de diseño , el contrato se puede aplicar tanto a funciones como a tipos .

"""
Podría decirse que es más simple si solo necesita usar interfaces o contratos, pero más complejo si necesita usar ambos juntos.
"""

Siempre que le permitan, dentro de un contrato, hacer referencia a una o más interfaces (en lugar de solo operadores y funciones, lo que permite DRY), este problema (y mi reclamo) se resolverá. Existe la posibilidad de que haya leído mal o no haya leído completamente el contenido de los contratos, y también existe la posibilidad de que dicha función sea compatible y no me di cuenta. Si no lo es, debería serlo.

¿No puedes hacer lo siguiente?

contract Example(t T, v V) {
  t.(interface{
    SomeMethod() V
  })
}

No puede usar una interfaz declarada en otro lugar debido a la restricción de que no puede hacer referencia a identificadores del mismo paquete en el que se declara el contrato, pero puede hacerlo. O simplemente podrían eliminar esa restricción; parece un poco arbitrario.

@DeedleFake No, porque se puede afirmar cualquier tipo de interfaz (y luego entrar en pánico potencialmente en el tiempo de ejecución, pero los contratos no se ejecutan). Pero puedes usar una tarea en su lugar.

t.(someInterface) también significaría que debe ser una interfaz

Buen punto. Ups.

Cuantos más ejemplos de esto veo, más propenso a errores parece ser 'descifrarlo a partir de un cuerpo de función'.

Hay muchos casos en los que es confuso para una persona, la misma sintaxis para diferentes operaciones, matices de implicaciones de diferentes construcciones, etc., pero una herramienta podría tomar eso y reducirlo a una forma normal. Pero luego, el resultado de dicha herramienta se convierte en un sublenguaje de facto para expresar restricciones de tipo que tenemos que aprender de memoria, lo que hace que sea aún más sorprendente cuando alguien se desvía y escribe un contrato a mano.

También notaré que

contract I(t T) {
  var i interface { Foo() }
  i = t
  t.(interface{})
}

expresa que T debe ser una interfaz con al menos Foo() pero también podría tener cualquier otro número de métodos adicionales.

T debe ser una interfaz con al menos Foo() pero también podría tener cualquier otro número de métodos adicionales

¿Es eso un problema, sin embargo? ¿Por lo general, no desea restringir las cosas para que permitan una funcionalidad específica pero no le importan otras funcionalidades? De lo contrario, un contrato como

contract Example(t T) {
  t + t
}

no permitiría la resta, por ejemplo. Pero desde el punto de vista de lo que sea que esté implementando, no me importa si un tipo permite la resta o no. Si impidiera que pudiera realizar restas, entonces la gente arbitrariamente no podría, por ejemplo, pasar nada que haga a una función Sum() o algo así. Eso parece arbitrariamente restrictivo.

No, no es un problema en absoluto. Era solo una propiedad poco intuitiva (para mí), pero tal vez eso se debió a la insuficiencia de café.

Es justo decir que la declaración de contrato actual necesita tener mejores mensajes del compilador para trabajar. Y las reglas para un contrato válido deben ser estrictas.

Hola
Hice una propuesta de restricciones para genéricos que publiqué en este hilo hace aproximadamente medio año.
Ahora he hecho una versión 2 . Los principales cambios son:

  • La sintaxis se ha adaptado a la propuesta por el go-team.
  • Se han omitido las restricciones por campos, lo que permite bastantes simplificaciones.
  • Se han suprimido los párrafos que no se consideraban estrictamente necesarios.

Hace poco pensé en una pregunta interesante (¿pero tal vez más detallada de lo apropiado en esta etapa del diseño?) Con respecto a la identidad del tipo:

func Foo() interface{} {
    type S struct {}
    return S{}
}

func Bar(type T)() interface{} {
    type S struct {}
    return S{}
}

func Baz(type T)() interface{} {
    type S struct{t T}
    return S{}
}

func main() {
    fmt.Println(Foo() == Foo()) // 1
    fmt.Println(Bar(int)() == Bar(string)()) // 2
    fmt.Println(Baz(int)() == Baz(string)()) // 3
}
  1. Imprime true , porque los tipos de los valores devueltos se originan en la misma declaración de tipo.
  2. Huellas dactilares…?
  3. Imprime false , supongo.

es decir, la pregunta es cuándo dos tipos declarados en una función genérica son idénticos y cuándo no lo son. No creo que esto se describa en el diseño de ~spec~. Al menos no puedo encontrarlo ahora :)

@merovius Supongo que se suponía que el caso medio era:

fmt.Println(Bar(int)() == Bar(int)()) // 2

Este es un caso interesante, y depende de si los tipos son "generativos" o "aplicativos". En realidad, hay variantes de ML que adoptan diferentes enfoques. Los tipos aplicativos ven lo genérico como una función de tipo y, por lo tanto, f(int) == f(int). Los tipos generativos ven lo genérico como una plantilla de tipo que crea un nuevo tipo de 'instancia' único cada vez que se usa, así que t<int> != t<int>. Esto debe abordarse a nivel de todo el sistema de tipos, ya que tiene implicaciones sutiles para la unificación, la inferencia y la solidez. Para obtener más detalles y ejemplos de ese tipo de problemas, recomiendo leer el documento "Módulos F-ing" de Andreas Rossberg: https://people.mpi-sws.org/~rossberg/f-ing/ aunque el documento habla de ML " functors" esto se debe a que ML separa su sistema de tipos en dos niveles, y los functors son equivalentes de ML a un genérico y solo están disponibles en el nivel de módulo.

@keean Asumes mal.

@merovius Sí, mi error, veo que la pregunta es porque no se usa el parámetro de tipo (un tipo fantasma).

Con los tipos generativos, cada instancia daría como resultado un tipo único diferente para 'S', por lo que aunque no se use el parámetro, no serían iguales.

Con los tipos aplicativos, la 'S' de cada instanciación sería del mismo tipo, por lo que serían iguales.

Sería extraño si el resultado en el caso 2 cambiara según las optimizaciones del compilador. Suena a UB.

Es gente de 2018, no puedo creer que realmente tenga que escribir esto como en 1982:

función min(x, y int) int {
si x < y {
volver x
}
volver y
}

función max(x, y int) int {
si x > y {
volver x
}
volver y
}

Quiero decir, en serio, amigos MIN (INT, INT) INT, ¿cómo es que NO está en el idioma?
Estoy enojado.

@ dataf3l Si desea que funcionen como se espera con los pedidos anticipados, entonces:

func min(x, y int) int {
   if x <= y {
      return x
   }
   return y
}

Esto es así, el par (min(x, y), max(x, y)) siempre es distinto y es (x, y) o (y, x), y por lo tanto es un tipo estable de dos elementos.

Entonces, otra razón por la que deberían estar en el idioma o en una biblioteca es que la mayoría de las personas se equivocan :-)

Pensé en < vs <=, para números enteros, no estoy seguro de ver la diferencia.
Tal vez solo soy tonto...

No estoy seguro de ver la diferencia.

No hay ninguno en este caso.

@cznic es cierto en este caso, ya que son números enteros, sin embargo, como el hilo era sobre genéricos, asumí que el comentario de la biblioteca era sobre tener definiciones genéricas de mínimo y máximo para que los usuarios no tengan que declararlas ellos mismos. Al volver a leer el OP, puedo ver que solo quieren un mínimo y un máximo simples para los números enteros, por lo que es malo, pero estaban fuera de tema al pedir funciones de integración simples en un hilo sobre genéricos :-)

Los genéricos son una adición crucial a este lenguaje, especialmente dada la falta de estructuras de datos integradas. Hasta ahora, mi experiencia con Go es que es un lenguaje excelente y fácil de aprender. Sin embargo, tiene una gran desventaja, que es que tienes que codificar las mismas cosas una y otra vez.

Tal vez me estoy perdiendo algo, pero esto parece una falla bastante grande en el idioma. En pocas palabras, hay pocas estructuras de datos integradas, y cada vez que creamos una estructura de datos, tenemos que copiar y pegar el código para admitir cada T .

No estoy seguro de cómo contribuir aparte de publicar mi observación aquí como 'usuario'. No soy un programador lo suficientemente experimentado como para contribuir al diseño o la implementación, por lo que solo puedo decir que los genéricos mejorarían en gran medida la productividad en el lenguaje (siempre que el tiempo de construcción y las herramientas sigan siendo increíbles como lo son ahora).

@webern Gracias. Consulte https://go.googlesource.com/proposal/+/master/design/go2draft.md .

@ianlancetaylor , después de publicar, se me ocurrió una idea bastante radical/única que creo que sería 'liviana' en lo que respecta al lenguaje y las herramientas. Todavía no he leído tu enlace por completo, lo haré. Pero si quisiera enviar una idea/propuesta de programación genérica en formato MD, ¿cómo lo haría?

Gracias.

@webern Escríbalo (la mayoría de la gente ha estado usando gists para el formato de descuento) y actualice la wiki aquí https://github.com/golang/go/wiki/Go2GenericsFeedback

Muchos otros ya lo han hecho.

Me fusioné (contra el último consejo) y cargué el CL de nuestra implementación de prototipo anterior a Gophercon de un analizador (e impresora) que implementa el diseño del borrador de contratos. Si está interesado en probar la sintaxis, eche un vistazo: https://golang.org/cl/149638 .

Para jugar con él:

1) Elija el CL en un repositorio que sea reciente:
git fetch https://go.googlesource.com/go refs/changes/38/149638/2 && git cherry-pick FETCH_HEAD

2) Reconstruya e instale el compilador:
ve a instalar cmd/compilar

3) Usa el compilador:
ir herramienta compilar foo.go

Consulte la descripción de CL para obtener más información. ¡Disfrutar!

contract Addable(t T) {
    t + t
}

func Sum(type T Addable)(x []T) T {
    var total T
    for _, v := range x {
        total += v
    }
    return total
}

Este diseño genérico, func Sum(type T Addable)(x []T) T , es MUY MUY MUY FEO!!!

Para compararlo con func Sum(type T Addable)(x []T) T , creo que func Sum<T: Addable> (x []T) T es más claro y no supone una carga para el programador procedente de otros lenguajes de programación.

¿Quiere decir que la sintaxis es más detallada?
Debe haber alguna razón por la que no es func Sum(T Addable)(x []T) T .

sin la palabra clave type no habrá forma de diferenciar entre una función genérica y una que devuelve otra función, que a su vez está siendo llamada.

@urandom Eso es solo un problema en el momento de la creación de instancias y allí no requerimos la palabra clave type , sino que solo vivimos con la ambigüedad AIUI.

El problema es que sin la palabra clave type , func Foo(x T) (y T) podría analizarse declarando una función genérica que toma T y no devuelve nada o una función no genérica que toma T y devolviendo T .

función Suma(x []T) T

Estoy de acuerdo, prefiero algo en este sentido. Dada la expansión del alcance lingüístico que representan los genéricos, creo que sería razonable introducir esta sintaxis para "llamar la atención" sobre una función genérica.

También creo que esto haría que el código fuera un poco más fácil (léase: menos Lisp-y) de analizar para los lectores humanos, así como reduciría las posibilidades de encontrar alguna ambigüedad de análisis oscura más adelante (ver "Most Vexing Parse" de C++, para ayudar a motivar una gran cantidad de precaución).

Es gente de 2018, no puedo creer que realmente tenga que escribir esto como en 1982:

función min(x, y int) int {
si x < y {
volver x
}
volver y
}

función max(x, y int) int {
si x > y {
volver x
}
volver y
}

Quiero decir, en serio, amigos MIN (INT, INT) INT, ¿cómo es que NO está en el idioma?
Estoy enojado.

Hay una razón para ello.
Si no entiendes, puedes aprender o irte.
Tu elección.

Espero sinceramente que lo mejoren.
Pero tu actitud de "puedes aprender o irte" no proporciona un buen ejemplo para que otros lo sigan. se lee innecesariamente abrasivo. No creo que de eso se trate esta comunidad @petar-dambovaliev. sin embargo, no me corresponde a mí decirte qué hacer, o cómo comportarte en línea, ese no es mi lugar.

Sé que hay muchos sentimientos fuertes sobre los genéricos, pero tenga en cuenta nuestros valores Gopher . Mantenga la conversación respetuosa y acogedora en todos los lados.

@bcmills gracias, haces de la comunidad un lugar mejor.

@katzdm estuvo de acuerdo, el idioma ya tiene tantos paréntesis, este nuevo material me parece muy ambiguo

Definir generics parece inevitable introduciendo cosas como type's type , lo que hace que Go sea ​​bastante complicado.

Espero que esto no se desvíe demasiado del tema, pero una función de function overload me parece suficiente.

Por cierto, sé que hubo alguna discusión sobre la sobrecarga .

@xgfone De acuerdo, que el idioma ya tiene tantos paréntesis, lo que hace que el código no sea claro.
func Sum<T: Addable> (x []T) T o func Sum<type T Addable> (x []T) T es mejor y más claro.

Por coherencia (con genéricos incorporados), func Sum[T: Addable] (x []T) T es mejor que func Sum<T: Addable> (x []T) T .

Puede que esté influenciado por trabajos anteriores en otros idiomas, pero Sum<T: Addable> (x []T) T parece más claro y legible a primera vista.

También estoy de acuerdo con @katzdm en que es mejor llamar la atención sobre algo nuevo en el idioma. También es bastante familiar para los desarrolladores que no son de Go que saltan a Go.

FWIW, hay aproximadamente un 0% de posibilidades de que Go use paréntesis angulares para genéricos. La gramática de C++ no se puede analizar porque no se puede distinguir a < b > c (una serie de comparaciones legales pero sin sentido) de una invocación genérica sin comprender los tipos de a, b y c. Otros idiomas evitan el uso de corchetes angulares para los genéricos por este motivo.

func a < b Addable> (...
Supongo que puede hacerlo si se da cuenta de que después func solo puede tener el nombre de la función, ( o < .

@carlmjohnson Espero que tengas razón

f := sum<int>(10)

Pero aquí sabes que sum es un contrato..

La gramática de C++ no se puede analizar porque no se puede distinguir a < b > c (una serie de comparaciones legales pero sin sentido) de una invocación genérica sin comprender los tipos de a, b y c.

Creo que vale la pena señalar que aunque Go, a diferencia de C++, no permite esto en el sistema de tipos, ya que los operadores < y > devuelven bool en Go y < y > no se pueden usar con bool s, _es_ sintácticamente legal, por lo que sigue siendo un problema.

Otro problema con los paréntesis angulares es List<List<int>> , en el que >> se tokeniza como un operador de desplazamiento a la derecha.

¿Cuáles fueron los problemas con el uso [] ? Me parece que la mayoría de los anteriores se resuelven usándolos:

  • Sintácticamente, f := sum[int](10) , para usar el ejemplo anterior, no es ambiguo porque tiene la misma sintaxis que una matriz o un mapa de acceso, y luego el sistema de tipos puede resolverlo más tarde, lo mismo que ya tiene que hacer para la diferencia entre los accesos a matrices y mapas, por ejemplo. Esto es diferente del caso de <> porque un solo < es legal, lo que genera ambigüedad, pero un solo [ no lo es.
  • func Example[T](v T) T tampoco es ambiguo.
  • ]] no es su propio token, por lo que también se evita ese problema.

El borrador del diseño menciona una ambigüedad en las declaraciones de tipo , como en type A [T] int , pero creo que esto podría resolverse con relativa facilidad en un par de formas diferentes. Por ejemplo, la definición genérica podría trasladarse a la palabra clave en sí, en lugar del nombre del tipo, es decir:

  • func[T] Example(v T) T
  • type[T] A int

La complicación aquí podría provenir del uso de bloques de declaración de tipos, como

type (
  A int
)

pero creo que esto es lo suficientemente raro como para decir básicamente que si necesita genéricos, entonces no puede usar uno de esos bloques.

Creo que sería muy desafortunado escribir

type[T] A []T
var s A[int]

porque los corchetes se mueven de un lado de A al otro. Por supuesto que se podría hacer, pero debemos apuntar a algo mejor.

Dicho esto, el uso de la palabra clave type en la sintaxis actual significa que podemos reemplazar los paréntesis con corchetes.

Esto no parece tan diferente del tipo de matriz frente a la sintaxis de expresión que es [N]T frente a arr[i] , en términos de cómo se declara algo que no coincide con la forma en que se usa. Sí, en var arr [N]T , los corchetes terminan en el mismo lado de arr que cuando se usa arr , pero normalmente pensamos en la sintaxis en términos de tipo frente a sintaxis de expresión siendo opuesto.

Extendí y mejoré algunas de mis viejas ideas inmaduras para tratar de unificar los genéricos personalizados y los integrados.

No estoy seguro de si hablar ( frente a < frente a [ y el uso de type es una pérdida de tiempo o si realmente hay un problema con la sintaxis

@ianlancetaylor ... se preguntaba si los comentarios justificaban algún ajuste en el diseño propuesto. Mi propia percepción de los comentarios fue que muchos sintieron que las interfaces y los contratos podían combinarse, al menos inicialmente. Parecía ser un cambio después de un tiempo que los dos conceptos deberían mantenerse separados. Pero podría estar leyendo mal las tendencias. ¡Me encantaría ver una opción experimental en un lanzamiento este año!

Sí, estamos considerando cambios en el diseño del borrador, incluido el análisis de las muchas contrapropuestas que ha hecho la gente. Nada está finalizado.

Solo para agregar un informe de experiencia práctica:
Implementé genéricos como una extensión de idioma en mi intérprete Go https://github.com/cosmos72/gomacro. Curiosamente, tanto la sintaxis

type[T] Pair struct { First T; Second T }
type Pair[T] struct { First T; Second T }

resultó introducir muchas ambigüedades en el analizador: el segundo podría analizarse como una declaración de que Pair es una matriz de estructuras T , donde T es un número entero constante. Cuando se usa Pair , también hay ambigüedades: Pair[int] también podría analizarse como una expresión en lugar de un tipo: podría estar indexando una matriz/rebanada/mapa llamado Pair con la expresión de índice int (nota: int y otros tipos básicos NO son palabras clave reservadas en Go), así que tuve que recurrir a una nueva sintaxis, ciertamente fea, pero hace el trabajo:

template[T] type Pair struct { First T; Second T }
type pairOfInt = Pair#[int]
var p Pair#[int]

y análogamente para las funciones:

template[T] func Sum(args ...T) T { /*...*/ }
Sum#[int] (1,2,3)

Entonces, aunque en teoría estoy de acuerdo en que la sintaxis es un asunto superficial, debo señalar que:
1) por un lado, la sintaxis es a lo que estarán expuestos los programadores de Go, por lo que debe ser expresiva, simple y posiblemente apetecible
2) por otro lado, una mala elección de sintaxis complicará el analizador, el verificador de tipos y el compilador para resolver las ambigüedades introducidas

Pair[int] también podría analizarse como una expresión en lugar de un tipo: podría estar indexando una matriz/rebanada/mapa llamado Pair con la expresión de índice int

Esta no es una ambigüedad de análisis, solo una semántica (hasta después de la resolución del nombre); la estructura sintáctica es la misma en ambos sentidos. Tenga en cuenta que Sum#[int] también podría ser un tipo o una expresión según lo que sea Sum . Lo mismo ocurre con (*T) en el código existente. Siempre que la resolución de nombres no afecte la estructura de lo que se analiza, está bien.

Compare esto con los problemas con <> :

f ( a < b , c < d >> (e) )

Ni siquiera puede tokenizar esto, ya que >> podría ser uno o dos tokens. Entonces, no puede saber si hay uno o dos argumentos para f ... la estructura de la expresión cambia significativamente dependiendo de lo que denota a .

De todos modos, estoy interesado en ver cuál es el pensamiento actual en el equipo sobre los genéricos, en particular, si "las restricciones son solo código" se ha repetido o abandonado. Puedo entender querer evitar definir un lenguaje de restricción distinto, pero resulta que escribir código que restringe lo suficiente los tipos involucrados fuerza un estilo poco natural, y también tiene que poner límites a lo que el compilador realmente puede inferir sobre los tipos basados ​​en el código. porque de lo contrario, estas inferencias pueden volverse arbitrariamente complejas o pueden basarse en hechos sobre el idioma que podrían cambiar en el futuro.

@cosmos72

Tal vez me equivoque, pero además de lo que dijo @stevenblenkinsop , ¿es posible que un término:

a b

también podría implicar que b no es un tipo si se sabe que b es un alfanumérico (sin operador/sin separador) con [identifier] opcional agregado y a no es una palabra clave especial/alfanumérico especial (por ejemplo, sin importación/ paquete/tipo/función)?.

No sé demasiado la gramática de ir.

De alguna manera, los tipos como int y Sum[int] se tratarían de todos modos como expresiones:

type (
    nodeList = []*Node  // nodeList and []*Node are identical types
    Polar    = polar    // Polar and polar denote identical types
)

Si go permitiera funciones infijas, entonces a type tag sería ambiguo ya que type podría ser una función infija o un tipo.

Hoy noté que la descripción general del problema de esta propuesta afirma que Swift:

Declarar que T satisface el protocolo Equatable hace válido el uso de == en el cuerpo de la función. Equatable parece estar integrado en Swift, no es posible definirlo de otra manera.

Esto parece ser más un aparte que algo que está afectando profundamente las decisiones tomadas sobre este tema, pero en caso de que les brinde inspiración a personas mucho más inteligentes que yo, quería señalar que en realidad no hay nada especial. aproximadamente Equatable además de estar predefinido en el idioma (principalmente para que muchos otros tipos incorporados puedan "conforme a"). Es totalmente posible crear protocolos similares:

protocol Equatable2 {
    static func == (lhs: Self, rhs: Self) -> Bool
}

class uniq: Equatable2 {
    static func == (lhs: uniq, rhs: uniq) -> Bool {
        return false
    }
}

let narf = uniq(), poit = uniq()

func !=<T: Equatable2> (lhs: T, rhs: T) -> Bool {
    return !(lhs == rhs)
}

print(narf != poit)

@sighoya
Estaba hablando de las ambigüedades de la sintaxis a[b] propuesta para genéricos, ya que ya se usa para indexar sectores y mapas, no sobre a b .

Mientras tanto, he estado estudiando a Haskell, y aunque sabía de antemano que usaba mucho la inferencia de tipos, la expresividad y la sofisticación de sus genéricos me sorprendieron.

Por desgracia tiene un esquema de nombres bastante peculiar, por lo que no siempre es fácil de entender a primera vista. Por ejemplo, class es en realidad una restricción para los tipos (genéricos o no). La clase Eq es la restricción para los tipos cuyos valores se pueden comparar con '==' y '/=':

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool

significa que un tipo a satisface la restricción Eq si existe una "especialización" (en realidad una "instancia" en la jerga de Haskell) de las funciones infijas == y /= que acepta dos argumentos, cada uno con el tipo a y devuelve un resultado de Bool .

Actualmente estoy tratando de adaptar algunas de las ideas encontradas en los genéricos de Haskell a una propuesta de genéricos de Go y ver qué tan bien encajan. Estoy muy contento de ver que se está investigando con otros lenguajes más allá de C++ y Java:

el ejemplo anterior de Swift y mi ejemplo de Haskell muestran que varios lenguajes de programación ya utilizan restricciones en tipos genéricos en la práctica, y que existe una cantidad no trivial de experiencia en varios enfoques de genéricos y restricciones y está disponible entre los programadores de estos (y otros) idiomas.

En mi opinión, ciertamente vale la pena estudiar dicha experiencia antes de finalizar una propuesta para los genéricos de Go.

Pensamiento extraviado: si la forma de restricción que desea que satisfaga el tipo genérico resulta ser más o menos congruente con una definición de interfaz, puede usar la sintaxis de aserción de tipo existente a la que ya estamos acostumbrados:

type Comparer interface {
  Compare(v interface{}) (*int, error)
}
type PriorityQueue<T.(Comparer)> struct {
  things []T
}

Disculpas si esto ya se ha discutido exhaustivamente en otro lugar; No lo he visto, pero todavía me estoy poniendo al día con la literatura. Lo he estado ignorando por un tiempo porque, bueno, no quiero genéricos en ninguna versión de Go. Pero la idea parece estar ganando impulso y una sensación de inevitabilidad en la comunidad en general.

@jesse-amano Es interesante que no quieras genéricos en ninguna versión de Go. Encuentro esto difícil de entender porque como programador realmente no me gusta repetirme. Cada vez que programo en 'C', tengo que implementar las mismas cosas básicas, como una lista o un árbol, en algún tipo de datos nuevo e, inevitablemente, mis implementaciones están llenas de errores. Con los genéricos solo podemos tener una versión de cualquier algoritmo, y toda la comunidad puede contribuir a hacer que esa versión sea la mejor. ¿Cuál es tu solución para no repetirte?

Con respecto al otro punto, Go parece estar introduciendo una nueva sintaxis para las restricciones genéricas porque las interfaces no permiten sobrecargar a los operadores (como '==' y '+'). Hay dos formas de avanzar a partir de esto, definir un nuevo mecanismo para restricciones genéricas, que es la forma en que Go parece ir, o permitir que las interfaces sobrecarguen a los operadores, que es la forma en que prefiero.

Prefiero la segunda opción porque mantiene la sintaxis del lenguaje más pequeña y simple, y permite declarar nuevos tipos numéricos que pueden usar los operadores habituales, por ejemplo, números complejos que se pueden agregar con '+'. El argumento en contra de esto parece ser que las personas pueden abusar de la sobrecarga del operador para hacer que '+' haga cosas extrañas, pero esto no me parece un argumento porque ya puedo abusar de cualquier nombre de función, por ejemplo, puedo escribir una función llamada 'imprimir ' que borra todos los datos de mi disco duro y finaliza el programa. Me gustaría tener la capacidad de restringir las sobrecargas tanto de los operadores como de las funciones para ajustarse a ciertas propiedades axiomáticas como la conmutatividad o la asociatividad, pero si no se aplica tanto a los operadores como a las funciones, no veo mucho sentido. Un operador es solo una función infija, y una función es solo un operador prefijo después de todo.

Otro punto a mencionar es que las restricciones genéricas que hacen referencia a varios parámetros de tipo son muy útiles, si las restricciones genéricas de un solo parámetro son predicados de los tipos, las restricciones multiparámetro son relaciones de los tipos. Las interfaces de Go no pueden tener más de un parámetro de tipo, por lo que nuevamente se debe introducir una nueva sintaxis o se deben rediseñar las interfaces.

Entonces, en cierto modo, estoy de acuerdo con usted, Go no fue diseñado como un lenguaje genérico, y cualquier intento de agregar genéricos será subóptimo. Tal vez sea mejor mantener Go sin genéricos y diseñar un nuevo lenguaje en torno a los genéricos desde cero para mantener el lenguaje pequeño con una sintaxis simple.

@keean No tengo una aversión tan fuerte a repetirme unas cuantas veces cuando lo necesito, y el enfoque de Go para el manejo de errores, los receptores de métodos, etc. generalmente parece hacer un buen trabajo para mantener a raya a la mayoría de los errores.

En un puñado de casos durante los últimos cuatro años, me he encontrado en situaciones en las que se necesitaba aplicar un algoritmo complejo pero generalizable a más de dos estructuras de datos complejas pero coherentes, y en todos los casos, y digo esto con Con toda seriedad: encontré que la generación de código a través de go:generate es más que suficiente.

A medida que leo los informes de experiencia, en muchos casos creo que go:generate o una herramienta similar podría haber resuelto el problema, y ​​en otros casos siento que tal vez Go1 simplemente no era el lenguaje correcto, y algo más podría haber sido en su lugar (tal vez con un envoltorio de complemento si se necesita algún código Go para usarlo). Pero soy consciente de que es bastante fácil para mí especular sobre lo que _podría haber hecho, lo que _podría haber funcionado; Hasta ahora no he tenido experiencias prácticas que me hicieran desear que Go1 tuviera más formas de expresar tipos genéricos, pero podría ser que tengo una forma extraña de pensar sobre las cosas, o podría ser que he sido extremadamente afortunado de trabajar solo en proyectos que realmente no necesitaban genéricos.

Espero que si Go2 termina admitiendo una sintaxis genérica, tendría un mapeo bastante directo a la lógica que se generará, sin casos extremos extraños que posiblemente surjan del encajonado/desencajonado, "reificación", cadenas de herencia, etc. que otros idiomas tienen que preocuparse.

@ jesse-amano Sin embargo, en mi experiencia, no son solo unas pocas veces, cada programa es una composición de algoritmos bien conocidos. No puedo recordar la última vez que escribí un algoritmo original, tal vez un problema de optimización complejo que necesitaba conocimiento del dominio.

Cuando escribo un programa, lo primero que hago es tratar de dividir el problema en fragmentos bien conocidos que puedo componer, un analizador de argumentos, alguna transmisión de archivos, diseño de interfaz de usuario basado en restricciones. No son solo algoritmos complejos en los que la gente comete errores, casi nadie puede escribir una implementación correcta de "min" y "max" la primera vez (Ver: http://componentsprogramming.com/writing-min-function-part5/ ).

El problema con go:generate es que es básicamente un macroprocesador, no tiene seguridad de tipo, de alguna manera tienes que escribir check y error check el código generado, lo cual no puedes hacer hasta que hayas ejecutado la generación. Este tipo de metaprogramación es muy difícil de depurar. No quiero escribir un programa para escribir el programa, solo quiero escribir el programa :-)

Entonces, la diferencia con los genéricos es que puedo escribir un programa _directo_ simple que puede ser verificado por error y tipo verificado por mi comprensión del significado, sin tener que generar el código, y depurarlo y devolver los errores al generador.

Un ejemplo realmente simple es "intercambiar", solo quiero intercambiar dos valores, no me importa cuáles sean:

swap<A>(x: *A, y: *A) {
   let tmp = *x
   *x = *y
   *y = tmp
}

Ahora creo que es trivial ver si esta función es correcta, y es trivial ver que es genérica y se puede aplicar a cualquier tipo. ¿Por qué querría escribir esta función una y otra vez para cada tipo de puntero a un valor en el que podría querer usar el intercambio? Por supuesto, entonces puedo construir algoritmos genéricos más grandes a partir de esto como una clasificación en el lugar. No creo que el código go:generate incluso para un algoritmo simple sea fácil de ver si es correcto.

Fácilmente podría cometer un error como:

let tmp = *x
*y = *x
*x = tmp

escribiendo esto a mano cada vez que quería intercambiar el contenido de dos punteros.

Entiendo que la forma idiomática de hacer este tipo de cosas en Go es usar una interfaz vacía, pero esto no es seguro y es lento. Sin embargo, me parece que Go no tiene las características correctas para admitir con elegancia este tipo de programación genérica, y las interfaces vacías proporcionan una vía de escape para solucionar los problemas. En lugar de cambiar por completo el estilo de go, parece mejor desarrollar un lenguaje adecuado para este tipo de genéricos desde cero. Curiosamente, 'Rust' hace bien muchas de las cosas genéricas, pero debido a que utiliza la administración de memoria estática en lugar de la recolección de basura, agrega mucha complejidad que no es realmente necesaria para la mayoría de la programación. Creo que entre Haskell, Go y Rust hay probablemente todos los bits necesarios para hacer un lenguaje genérico convencional decente, simplemente mezclados.

Para información: actualmente estoy escribiendo una lista de deseos sobre los genéricos de Go,

con la intención de implementarlo en mi intérprete de Go gomacro , que ya tiene una implementación diferente de los genéricos de Go (modelado a partir de plantillas de C++).

Todavía no está completo, los comentarios son bienvenidos :)

@keean

Leí la publicación de blog que vinculó sobre la función min y las cuatro publicaciones previas. Ni siquiera observé un intento de argumentar que "casi nadie puede escribir una implementación correcta de 'min'...". El escritor en realidad parece reconocer que su primera implementación _es_ correcta... siempre que el dominio esté restringido a números. Es la introducción de objetos y clases, y el requisito de que se comparen a lo largo de una sola dimensión, a menos que los valores en esa dimensión sean los mismos, excepto cuando, etc., lo que crea la complejidad adicional. Los sutiles requisitos ocultos involucrados en la necesidad de definir cuidadosamente el comparador y las funciones de clasificación en un objeto complejo son exactamente la razón por la que no me gustan los genéricos como concepto (al menos en Go; Java con Spring parece que ya es un entorno lo suficientemente bueno para componer reunir un montón de bibliotecas maduras en una aplicación).

Personalmente, no encuentro la necesidad de seguridad de tipos en los generadores de macros; si están generando código legible ( gofmt ayuda a establecer el listón bastante bajo), entonces la verificación de errores en tiempo de compilación debería ser suficiente. De todos modos, no debería importarle al usuario del generador (o el código que lo invoca) para la producción; en el pequeño conjunto de veces que me han pedido que escriba un algoritmo genérico como una macro, un puñado de pruebas unitarias (generalmente flotante, cadena y puntero a estructura, si hay algún tipo codificado que no debería (no está codificado de forma rígida, uno de estos tres será incompatible con él; si alguno de estos tres no se puede usar en el algoritmo genérico, entonces no es un algoritmo genérico) fue suficiente para garantizar que la macro funcionara correctamente.

swap es un mal ejemplo. Lo siento, pero lo es. Ya es una sola línea en Go, no hay necesidad de una función genérica para envolverlo y no hay espacio para que un programador cometa un error no obvio.

*y, *x = *x, *y

También existe un sort en el lugar en la biblioteca estándar . Utiliza interfaces. Para hacer una versión específica para su tipo, defina:

type myslice []mytype
func (s myslice) Len() int { return len(s) }
func (s myslice) Less(i, j int) bool { return s[i].whatWouldAlsoBeNeededInAGenericImpl(s[j]) }
func (s myslice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

Hay que admitir que hay que escribir varios bytes más que SortableList<mytype>(myThings).Sort() , pero es _mucho_ menos denso de leer, no es tan probable que "tartamudee" en el resto de una aplicación, y si surgen errores, es poco probable necesitar algo tan pesado como un rastro de pila para encontrar la causa. El enfoque actual tiene varias ventajas, y me preocupa que las perdamos si nos inclinamos demasiado hacia los genéricos.

@jesse-amano
Los problemas con 'min/max' se aplican incluso si no comprende la necesidad de una ordenación estable. Por ejemplo, un desarrollador implementa min/max para algún tipo de datos en un módulo, y luego otro miembro del equipo lo usa en una ordenación o algún otro algoritmo sin la verificación adecuada de las suposiciones, y conduce a errores extraños porque no es estable.

Creo que la programación consiste principalmente en componer algoritmos estándar, muy rara vez los programadores crean nuevos algoritmos innovadores, por lo que min/max y sort son solo ejemplos. Hacer agujeros en los ejemplos específicos que elegí solo muestra que no elegí muy buenos ejemplos, no aborda el punto real. Elegí "intercambiar" porque es muy simple y rápido de escribir. Podría haber elegido muchos otros, ordenar, rotar, particionar, que son algoritmos muy generales. Cuando estás escribiendo un programa que usa una colección como un árbol rojo/negro, no toma mucho tiempo cansarte de tener que rehacer el árbol para cada tipo de datos diferente del que quieres una colección, porque quieres seguridad de tipo y una interfaz vacía es poco mejor que "void*" en 'C'. Luego, tendría que volver a hacer lo mismo para cada algoritmo que usa cada uno de estos árboles, como pedido previo, en orden, iteración posterior al pedido, búsqueda, y eso es antes de entrar en cosas sofisticadas como los algoritmos de red de Tarjan (disjuntos conjuntos, montones, árboles de expansión mínimos, caminos más cortos, flujos, etc.)

Creo que los generadores de código tienen su lugar, por ejemplo, generar un validador a partir de un esquema json o un analizador a partir de una definición de gramática, pero no creo que sean un reemplazo adecuado para los genéricos. Para la programación genérica, quiero poder escribir cualquier algoritmo una vez y tenerlo claro, simple y directo.

En cualquier caso, estoy de acuerdo con usted acerca de 'Go', no creo que 'Go' se haya diseñado desde el principio para ser un buen lenguaje genérico, y agregar genéricos ahora probablemente no resulte en un buen lenguaje genérico, y va a perder parte de la franqueza y la simplicidad que ya tiene. Personalmente, si tiene que buscar un generador de código (más allá de cosas como generar validadores de json-schema o analizadores de un archivo de gramática), entonces probablemente esté usando el idioma incorrecto de todos modos.

Editar: con respecto a probar genéricos con "flotador" "cadena" "puntero a estructura", no creo que haya muchos algoritmos genéricos que funcionen en un conjunto de tipos tan diverso, excepto tal vez 'intercambio'. Las verdaderas funciones 'genéricas' están realmente limitadas a mezclas y no ocurren muy a menudo. Los genéricos restringidos son mucho más interesantes, donde los tipos genéricos están restringidos por alguna interfaz. Como puede ver, con el ejemplo de clasificación en el lugar de la biblioteca estándar, puede hacer que algunos genéricos restringidos funcionen en 'Ir' en casos limitados. Me gusta la forma en que funcionan las interfaces de Go y puedes hacer mucho con ellas. Me gustan aún más los verdaderos genéricos restringidos. Realmente no me gusta agregar un segundo mecanismo de restricción como lo hace la propuesta actual de genéricos. Un lenguaje donde las interfaces restringen directamente los tipos sería mucho más elegante.

Es interesante que, por lo que puedo decir, la única razón por la que se introdujeron las nuevas restricciones es porque Go no permite definir operadores en las interfaces. Las propuestas genéricas anteriores permitían que los tipos estuvieran restringidos por las interfaces, pero se abandonaron porque no se adaptaban a operadores como '+'.

@keean
Tal vez haya un lugar mejor para una discusión prolongada. (Quizás no; he buscado y este parece ser _el_ lugar para discutir los genéricos en Go2).

¡Ciertamente entiendo la necesidad de una clasificación estable! Sospecho que los autores de la biblioteca estándar original de Go1 también lo entendieron, ya que sort.Stable ha estado allí desde su lanzamiento público.

Creo que lo mejor del paquete sort de la biblioteca estándar es que _no_ solo funciona en segmentos. Ciertamente es más simple cuando el receptor es una porción, pero todo lo que realmente necesita es una forma de saber cuántos valores hay en el contenedor (el método Len() int ), cómo compararlos (el método Less(int, int) bool ) y cómo intercambiarlos (el método Swap(int, int) , por supuesto). ¡Puedes implementar sort.Interface usando canales! Es lento, por supuesto, porque los canales no están diseñados para una indexación eficiente, pero se puede demostrar que es correcto dado un generoso presupuesto de tiempo de ejecución.

No quiero ser quisquilloso, pero el problema con un mal ejemplo es que... es malo. Cosas como sort y min son simplemente _no_ puntos a favor de una función de lenguaje de alto impacto como los genéricos. Creo firmemente que hacer agujeros en estos ejemplos _sí_ aborda el punto real; _mi_ punto es que no hay necesidad de genéricos cuando ya existe una mejor solución en el idioma.

@jesse-amano

Ya existe una solución mejor en el idioma.

¿Cuál? No veo nada mejor que los genéricos restringidos con seguridad de tipos. Los generadores no son Go, simple y llanamente. Las interfaces y la reflexión producen código inseguro, lento y propenso al pánico. Estas soluciones son lo suficientemente buenas porque no hay nada más. Los genéricos resolverían el problema con construcciones repetitivas, construcciones de interfaz vacías e inseguras y, lo peor de todo, eliminarían muchos usos de la reflexión, que es aún más propenso a sufrir pánico en el tiempo de ejecución. Incluso la propuesta del nuevo paquete de errores adolece de la falta de genéricos y su API se beneficiaría enormemente de ellos. Puede ver As como ejemplo: no idiomático, propenso a entrar en pánico, difícil de usar, requiere verificación veterinaria para usarlo correctamente. Todo porque Go carece de cualquier tipo de genéricos.

sort , min y otros algoritmos genéricos son excelentes ejemplos porque muestran el principal beneficio de los genéricos: la composición. Permiten construir una extensa biblioteca de rutinas de transformación genéricas que se pueden encadenar. Y lo que es más importante, sería fácil de usar, seguro, rápido (al menos es posible con los genéricos), sin necesidad de repeticiones, generadores, interfaz{}, reflexión y otras funciones de lenguaje oscuras utilizadas únicamente porque no hay otra manera.

@creker

¿Cuál?

Para ordenar cosas, el paquete sort . Cualquier cosa que implemente sort.Interface se puede ordenar (con un algoritmo estable o inestable de su elección; algunas versiones en el lugar se proporcionan a través del paquete sort , pero puede escribir el suyo propio con un API similar o diferente). Dado que la biblioteca estándar sort.Sort y sort.Stable funcionan con el valor pasado a través de la lista de argumentos, el valor que obtiene es el mismo que el valor con el que comenzó y, por lo tanto, necesariamente, el tipo obtienes es el mismo que el tipo con el que comenzaste. Es perfectamente seguro para tipos, y el compilador hace todo el trabajo de inferir si su tipo implementa la interfaz necesaria y es capaz de _al menos_ tantas optimizaciones en tiempo de compilación como sería posible con una función sort<T> de estilo genérico .

Para intercambiar cosas, el one-liner x, y = y, x . Nuevamente, no se necesitan aserciones de tipo, conversión de interfaz o reflexión. Es solo intercambiar dos valores. El compilador puede asegurarse fácilmente de que sus operaciones sean seguras.

No hay una sola herramienta específica que considere una mejor solución que los genéricos en todos los casos, pero para cualquier problema que se supone que los genéricos deben resolver, creo que hay una mejor solución. Podría estar equivocado aquí; Todavía estoy abierto a ver un ejemplo de algo que los genéricos pueden hacer donde todas las soluciones existentes hubieran sido terribles. Pero si puedo hacerle agujeros, entonces no es uno de esos ejemplos.

Tampoco me gusta mucho el paquete xerrors , pero xerrors.As no me parece que no sea idiomático; después de todo, es una API muy similar a json.Unmarshal . Puede que necesite mejor documentación y/o código de ejemplo, pero por lo demás está bien.

Pero no, sort y min son, por sí mismos, ejemplos bastante terribles. El primero ya existe en Go y es perfectamente componible, todo ello sin necesidad de genéricos. Este último es, en su sentido más amplio, uno de los resultados de sort (que ya resolvimos), y en los casos en que se necesite una solución más especializada u optimizada, escribirá la solución especializada de todos modos en lugar de apoyarse en ella. genéricos. Una vez más, no hay generadores, interfaz{}, reflexión o funciones de lenguaje "oscuras" utilizadas en el paquete sort la biblioteca estándar. Hay interfaces no vacías (que están bien definidas en la API para que obtenga errores en tiempo de compilación si las usa incorrectamente, inferidas para que no necesite conversiones y verificadas en tiempo de compilación para que no necesite afirmaciones). Puede haber algo repetitivo _si_ la colección que está clasificando es una porción, pero si resulta ser una estructura (¿como una que representa el nodo raíz de un árbol de búsqueda binaria?), puede hacer que satisfaga los sort.Interface también, por lo que en realidad es _más_ flexible que una colección genérica.

@jesse-amano

mi punto es que no hay necesidad de genéricos cuando ya existe una mejor solución en el lenguaje

Creo que la mejor solución se basa relativamente en cómo la ves. Si tuviéramos un mejor lenguaje, podríamos tener una mejor solución, por eso queremos mejorar este lenguaje. Por ejemplo, si existe un mejor genérico, podríamos tener un mejor sort en nuestra stdlib, al menos la forma actual de implementar la interfaz de clasificación no es una buena experiencia de usuario para mí, todavía tengo que escribir mucho código similar que creo firmemente que podríamos abstraer.

@jesse-amano

Creo que lo mejor del paquete de clasificación de la biblioteca estándar es que no solo funciona en segmentos.

Estoy de acuerdo, me gusta el tipo estándar.

El primero ya existe en Go y es perfectamente componible, todo ello sin necesidad de genéricos.

Esta es una falsa dicotomía. Las interfaces en Go ya son una forma de genéricos. El mecanismo no es la cosa misma. Mire más allá de la sintaxis y vea el objetivo, que es la capacidad de expresar cualquier algoritmo de forma genérica sin limitaciones. La abstracción de interfaz de 'sort' es genérica, permite ordenar cualquier tipo de datos que pueda implementar los métodos requeridos. La notación es simplemente diferente. Podríamos escribir:

f<T>(x: T) requires Sortable(T)

Lo que significaría que el tipo 'T' debe implementar la interfaz 'Ordenable'. En 'Ir' esto podría escribirse func f(x Sortable) . Entonces, al menos, la aplicación de funciones en Go se puede manejar de manera genérica, pero hay operaciones a las que no les gusta la aritmética o la desreferenciación. Go lo hace bastante bien, ya que las interfaces pueden considerarse predicados de tipos, pero Go no tiene respuesta para las relaciones entre tipos.

Es fácil ver las limitaciones con Go, considere:

func merge(x, y Sortable)

donde vamos a fusionar dos cosas clasificables, sin embargo, Go no nos permite imponer que estas dos cosas deben ser iguales. Contrasta esto con:

merge<T>(x: T, y: T) requires Sortable(T)

Aquí tenemos claro que estamos fusionando dos tipos ordenables que son iguales. 'Ir' descarta la información de tipo subyacente y simplemente trata cualquier cosa "ordenable" como la misma.

Probemos un mejor ejemplo: digamos que quiero escribir un árbol rojo/negro que pueda contener cualquier tipo de datos, como una biblioteca, para que otras personas puedan usarlo.

Las interfaces en Go ya son una forma de genéricos.

Si es así, entonces este problema puede cerrarse como ya resuelto, porque la declaración original era:

Este número propone que Go debería admitir alguna forma de programación genérica.

La ambigüedad perjudica a todas las partes. Las interfaces son de hecho _una_ forma de programación genérica, y de hecho _no_ necesariamente, por sí solas, resuelven hasta el último problema que otras formas de programación genérica pueden resolver. Entonces, para simplificar, permitamos que cualquier problema que pueda resolverse con herramientas fuera del alcance de esta propuesta/tema se considere "resuelto sin genéricos". (Creo que la gran mayoría de los problemas solucionables que se encuentran en el mundo real, si no todos, están en ese conjunto, pero esto es solo para asegurarnos de que todos hablemos el mismo idioma).

Considere: func merge(x, y Sortable)

No me queda claro por qué la fusión de dos cosas ordenables (o cosas que implementan sort.Interface ) sería diferente de la fusión de dos colecciones _en general_. Para rebanadas, eso es append ; para mapas, eso es for k, v := range m { n[k] = v } ; y para estructuras de datos más complejas, necesariamente existen estrategias de fusión más complejas según la estructura (cuyo contenido podría ser necesario para implementar algunos métodos que necesita la estructura). Suponiendo que está hablando de un algoritmo de clasificación más complicado que divide y elige subalgoritmos para las particiones antes de volver a fusionarlas, lo que necesita no es que las particiones sean "ordenables", sino algún tipo de garantía de que sus particiones son ya _ordenado_ antes de fusionarse. Ese es un tipo de problema muy diferente, y no uno que la sintaxis de la plantilla ayude a resolver de manera obvia; naturalmente, querrá algunas pruebas unitarias bastante rigurosas para garantizar la confiabilidad de su(s) algoritmo(s) de ordenación combinada, pero seguramente no querrá exponer una API _exportada_ que agobia al desarrollador con este tipo de cosas.

Plantea un punto interesante acerca de que Go no tiene una buena manera de verificar si dos valores son del mismo tipo sin reflexión, cambios de tipo, etc. Siento que usar interface{} es una solución perfectamente aceptable en el El caso de contenedores de propósito general (por ejemplo, una lista enlazada circular) como el modelo involucrado en el envoltorio de la API para la seguridad de tipo es absolutamente trivial:

type MyStack struct { stack Stack }
func (s *MyStack) Push(v MyType) error { return s.stack.Push(v) }
func (s *MyStack) Pop() (MyType, error) {
  v, err := s.stack.Pop()
  var m MyType
  if v != nil {
    if m, ok := v.(MyType); ok { return m, err; }
    panic("this code should be unreachable from the exported API")
  }
  return nil, err
}

Me cuesta imaginar por qué este modelo sería un problema, pero si lo es, una alternativa razonable podría ser una plantilla (de texto/). Puede anotar los tipos para los que desea definir pilas con un comentario //go:generate stackify MyType github.com/me/myproject/mytype , y dejar que go generate produzca el modelo por usted. Mientras cmd/stackify/stackify_test.go lo pruebe con al menos una estructura y al menos un tipo incorporado, y se compile y apruebe, no veo por qué esto sería un problema, y ​​probablemente sea bastante parecido. a lo que cualquier compilador habría terminado haciendo "debajo del capó" si hubiera definido una plantilla. La única diferencia es que los errores son más útiles porque son menos densos.

(También puede haber casos en los que queramos un _algo_ genérico que se preocupe más por que dos cosas sean del mismo tipo que por su comportamiento, que no entran en la categoría de "contenedores de cosas". Eso sería muy interesante, pero agregar una sintaxis de construcción de plantilla genérica al lenguaje aún podría no ser la única solución posible disponible).

Suponiendo que el repetitivo _no_ sea un problema, estoy interesado en abordar el problema de crear un árbol rojo/negro que sea tan fácil de usar para las personas que llaman como paquetes como sort o encoding/json . Ciertamente fallaré porque... bueno, no soy tan inteligente. Pero estoy emocionado de saber qué tan cerca puedo estar.

Editar: los comienzos de un ejemplo se pueden ver aquí , aunque está lejos de estar completo (lo mejor que pude juntar en un par de horas). Por supuesto, también existen otros intentos de estructuras de datos similares.

@jesse-amano

Si es así, entonces este problema puede cerrarse como ya > resuelto, porque la declaración original era:

No es solo que las interfaces _son_ una forma de genéricos, sino que mejorar el enfoque de las interfaces puede llevarnos hasta el final de los genéricos. Por ejemplo, las interfaces de parámetros múltiples (donde puede tener más de un 'receptor') permitirían relaciones en tipos. Permitir que las interfaces anulen operadores como la suma y la desreferenciación eliminaría la necesidad de cualquier otra forma de restricción en los tipos. Las interfaces _pueden_ ser todas las restricciones de tipo que necesita, si están diseñadas con una comprensión del punto final de los genéricos completamente generales.

Las interfaces son semánticamente similares a las clases de tipos de Haskell y las características de Rust que _sí_ resuelven estos problemas genéricos. Las clases de tipos y los rasgos resuelven los mismos problemas genéricos que las plantillas de C++, pero de una manera segura (pero tal vez no todos los usos de la metaprogramación, lo que creo que es algo bueno).

Me cuesta imaginar por qué este modelo sería un problema, pero si lo es, una alternativa razonable podría ser una plantilla (de texto/).

Personalmente, no tengo ningún problema con tanto repetitivo, pero entiendo el deseo de no tener ningún repetitivo, como programador es aburrido y repetitivo, y es exactamente el tipo de tarea que debemos evitar al escribir programas. Entonces, de nuevo, personalmente, creo que escribir una implementación para una interfaz/clase de tipo 'pila' es exactamente la forma _correcta_ de hacer que su tipo de datos sea 'apilable'.

Hay dos limitaciones con Go que frustran la programación genérica adicional. El problema de equivalencia de 'tipo', por ejemplo, definir funciones matemáticas para que el resultado y todos los argumentos sean iguales. Podríamos imaginar:

mul<T>(x, y T) T requires Addable(T) {
    r := 0
    for i := 0; i < y; ++i  {
        r = r + x
    }
    return r
}

Para satisfacer las restricciones de '+', debemos asegurarnos de que x y y sean numéricos, pero también del mismo tipo subyacente.

El otro es la limitación de las interfaces a un solo tipo de 'receptor'. Esta limitación significa que no solo tiene que escribir el texto modelo anterior una vez (lo que creo que es razonable), sino para cada tipo diferente que quiera poner en MyStack. Lo que queremos es declarar el tipo contenido como parte de la interfaz:

type Stack<T> interface {...}

Esto permitiría, entre otras cosas, declarar una implementación que es paramétrica en T para que podamos poner cualquier T en MyStack usando la interfaz Stack, siempre que todos los usos de Push y Pop en la misma instancia de MyStack opera en el mismo tipo de 'valor'.

Con estos dos cambios deberíamos poder crear un árbol rojo/negro genérico. Debería ser posible sin ellos, pero al igual que la pila, deberá declarar una nueva instancia de la interfaz para cada tipo que desee colocar en el árbol rojo/negro.

Desde mi punto de vista, las dos extensiones anteriores de las interfaces son todo lo que se necesita para que Go sea totalmente compatible con los "genéricos".

@jesse-amano
Mirando el ejemplo del árbol rojo/negro, lo que realmente queremos genéricamente es la definición de un 'Mapa', el árbol rojo/negro es solo una posible implementación. Como tal, podríamos esperar una interfaz como esta:

type Map<Key, Value> interface {
   put(x Key, y Value) 
   get(x Key) Value
}

Luego, el árbol rojo/negro podría proporcionarse como una implementación. Idealmente, queremos escribir código que no dependa de la implementación, por lo que podría proporcionar una tabla hash, un árbol rojo-negro o un BTree. Entonces escribiríamos nuestro código:

f<K, V, T>(index T) T requires Map<K, V> {
   ...
}

Ahora, sea lo que sea f , puede funcionar independientemente de la implementación del Mapa, f puede ser una función de biblioteca escrita por otra persona, que no necesita saber si mi aplicación usa una red/ árbol negro o un mapa hash.

Tal como está ahora, necesitaríamos definir un mapa específico como este:

type MapIntString interface {
   put(x Int, y String)
   get(x Int) String
}

Lo cual no es tan malo, pero significa que la función de 'biblioteca' f debe escribirse para cada combinación posible de tipos de clave y valor si vamos a poder usarla en una aplicación donde no No sabemos los tipos de claves y valores cuando escribimos la biblioteca.

Si bien estoy de acuerdo con el último comentario de @keean , la dificultad es escribir un árbol rojo/negro en Go que implemente una interfaz conocida, como por ejemplo la que se acaba de sugerir.

Sin genéricos, es bien sabido que para implementar contenedores agnósticos de tipo uno tiene que usar interface{} y/o reflexión; desafortunadamente, ambos enfoques son lentos y propensos a errores.

@keean

No es solo que las interfaces sean una forma de genéricos, sino que mejorar el enfoque de las interfaces puede llevarnos hasta el final de los genéricos.

No veo ninguna de las propuestas vinculadas a este tema, hasta la fecha, como una mejora. Parece bastante indiscutible decir que todos tienen fallas de alguna manera. Creo que esas fallas superan con creces cualquier beneficio, y muchos de los beneficios _reclamados_ de hecho ya son compatibles con las funciones existentes. Mi creencia se basa en la experiencia práctica, no en la especulación, pero sigue siendo anecdótica.

Personalmente, no tengo ningún problema con tanto repetitivo, pero entiendo el deseo de no tener ningún repetitivo, como programador es aburrido y repetitivo, y es exactamente el tipo de tarea que debemos evitar al escribir programas.

Yo tampoco estoy de acuerdo con esto. Como profesional remunerado, mi objetivo es reducir los costos de tiempo/esfuerzo _para mí y para los demás_, al mismo tiempo que incremento las ganancias de mi empleador, sin importar cómo se midan. Una tarea que es "aburrida" solo es mala si también requiere mucho tiempo; no puede ser difícil, o no sería aburrido. Si solo requiere un poco de tiempo por adelantado, pero elimina actividades futuras que consumen mucho tiempo y/o hace que el producto se lance antes, entonces aún vale la pena.

Luego, el árbol rojo/negro podría proporcionarse como una implementación.

Creo que he hecho un progreso decente estos últimos días en la implementación de un árbol rojo/negro (no está terminado; le falta incluso un archivo Léame) pero me preocupa que ya no he podido ilustrar mi punto si no es abundante. claro que mi objetivo no es trabajar hacia una interfaz, sino trabajar hacia una implementación. Estoy escribiendo un árbol rojo/negro y, por supuesto, quiero que sea _útil_, pero no me importa para qué cosas _específicas_ otros desarrolladores puedan querer usarlo.

Sé que la interfaz mínima requerida por una biblioteca de árbol rojo/negro es una en la que existe un orden "débil" en sus elementos, por lo que necesito algo _como_ una función llamada Less(v interface{}) bool , pero si la persona que llama tiene un método que hace algo similar pero no se llama Less(v interface{}) bool , depende de ellos escribir los envoltorios/calzas repetitivos para que funcione.

Cuando accede a los elementos contenidos en el árbol rojo/negro, obtiene interface{} , pero si está dispuesto a confiar en mi garantía de que la biblioteca proporcionada _es_ un árbol rojo/negro, no entiendo por qué lo haría. No confíe en que los tipos de elementos que coloque serán exactamente los tipos de elementos que obtendrá. Si _confía_ en ambas garantías, entonces la biblioteca no es propensa a errores en absoluto. Simplemente escriba (o pegue) una docena de líneas de código para cubrir las aserciones de tipo.

Ahora tiene una biblioteca perfectamente segura (nuevamente, suponiendo que no supere el nivel de confianza que tendría que estar dispuesto a dar para descargar la biblioteca en primer lugar) que incluso tiene los nombres de función exactos que desea. Esto es importante. En un ecosistema de estilo Java donde los autores de bibliotecas se esfuerzan al máximo para codificar con una definición de interfaz _exacta_ (casi _tienen_ que hacerlo, porque el lenguaje lo impone a través de la sintaxis class MyClassImpl extends AbstractMyClass implements IMyClass ) y hay un montón de burocracia adicional, tiene que hacer todo lo posible para hacer una fachada para que la biblioteca de terceros se ajuste a los estándares de codificación de su organización (que es la misma cantidad de repetitivo, si no más), o permitir que esto sea una "excepción" a los estándares de codificación de su organización (y eventualmente su organización tiene tantas excepciones en sus estándares como en sus bases de código), o deje de usar una biblioteca perfectamente buena (suponiendo, por el bien del argumento, que la biblioteca es realmente buena).

Idealmente, queremos escribir código que no dependa de la implementación, por lo que podría proporcionar una tabla hash, un árbol rojo-negro o un BTree.

Estoy de acuerdo con este ideal, pero creo que Go ya lo satisface. Con una interfaz como:

type MyStorage interface {
  Get(KeyType) (ValueType, error)
  Put(KeyType, ValueType) error
}

lo único que falta es la capacidad de parametrizar lo que son KeyType y ValueType , y no estoy convencido de que esto sea especialmente importante.

Como mantenedor (hipotético) de una biblioteca de árbol rojo/negro, no me importa cuáles sean sus tipos. Solo usaré interface{} para todas mis funciones principales que manejan "algunos datos", y _tal vez_ proporcione algunas funciones de ejemplo exportadas que le permitan usarlas más fácilmente con tipos comunes como string y int . Pero depende de la persona que llama proporcionar la capa extremadamente delgada alrededor de esta API para que sea segura para cualquier tipo personalizado que pueda terminar definiendo. Pero lo único importante de la API que proporciono es que le permite a la persona que llama hacer todas las cosas que espera que haga un árbol rojo/negro.

Como llamador (hipotético) de una biblioteca de árbol rojo/negro, probablemente solo lo quiero para un almacenamiento rápido y un tiempo de búsqueda. No me importa que sea un árbol rojo/negro. Me importa que pueda Get cosas de él y Put cosas en él y, lo que es más importante, me importa cuáles son esas cosas. Si la librería no ofrece funciones llamadas Get y Put , o no puede interactuar perfectamente con los tipos que he definido, eso no me importa mientras sea fácil para mí para escribir los métodos Get y Put yo mismo, y hacer que mi propio tipo satisfaga la interfaz que necesita la biblioteca mientras lo hago. Si no es fácil, generalmente encuentro que es culpa del autor de la biblioteca, no del idioma, pero una vez más, es posible que haya contraejemplos de los que simplemente no estoy al tanto.

Por cierto, el código podría enredarse mucho más si no fuera así. Como usted dice, hay muchas implementaciones posibles de un almacén de clave/valor. Pasar un "concepto" abstracto de almacenamiento de clave/valor oculta la complejidad de cómo se logra el almacenamiento de clave/valor, y un desarrollador de mi equipo podría elegir el incorrecto para su tarea (incluida una versión futura de mí mismo cuyo conocimiento de la clave ¡La implementación de almacenamiento de valores se ha quedado sin memoria!). La aplicación o sus pruebas unitarias pueden, a pesar de nuestros mejores esfuerzos en la revisión del código, contener código sutil dependiente de la implementación que deja de funcionar de manera confiable cuando algunos almacenes de clave/valor dependen de una conexión a una base de datos y otros no. Es una molestia cuando el informe de error viene con un seguimiento de pila grande, y la única línea en el seguimiento de pila que hace referencia a algo en el código base _real_ apunta a una línea que usa un valor de interfaz, todo porque la implementación de esa interfaz es código generado (que solo puede ver en el tiempo de ejecución) en lugar de una estructura ordinaria, con métodos que devuelven valores de error legibles.

@jesse-amano
Estoy de acuerdo con usted, y me gusta la forma 'Ir' de hacer las cosas donde el código de "usuario" declara una interfaz que abstrae la forma en que funciona, y luego escribe la implementación de esa interfaz para la biblioteca/dependencia. Esto es al revés de la forma en que la mayoría de los otros lenguajes piensan sobre las interfaces. pero una vez que lo consigues es muy potente.

Todavía me gustaría ver las siguientes cosas en un lenguaje genérico:

  • tipos paramétricos, como: RBTree<Int, String> ya que esto haría cumplir la seguridad de tipo de las colecciones de usuarios.
  • escriba variables, como: f<T>(x, y T) T , porque esto es necesario para definir familias de funciones relacionadas como suma, resta, etc. donde la función es polimórfica, pero requerimos que todos los argumentos sean del mismo tipo subyacente.
  • restricciones de tipo, como: f<T: Addable>(x, y T) T , que aplica interfaces a variables de tipo, porque una vez que introducimos variables de tipo, necesitamos una forma de restringir esas variables de tipo en lugar de tratar Addable como un tipo. Si consideramos Addable como un tipo y escribimos f(x, y Addable) Addable , no tenemos forma de saber si los tipos subyacentes originales de x y y son los mismos que entre sí o el tipo devuelto.
  • interfaces de múltiples parámetros, como: type<K, V> Map<K, V> interface {...} , que podrían usarse como merge<K, V, T: Map<K, V>>(x, y T) T que nos permiten declarar interfaces que están parametrizadas no solo por el tipo de contenedor, sino también en este caso por la clave y el valor tipos del mapa.

Creo que cada uno de estos aumentaría el poder de abstracción del lenguaje.

¿Algún progreso o cronograma en esto?

@leaxoy Hay una charla programada sobre "Generics in Go" de @ianlancetaylor en GopherCon . Esperaría escuchar más sobre el estado actual de las cosas en esa charla.

@griesemer Gracias por ese enlace.

@keean Me encantaría ver también la cláusula Where de Rust aquí, que puede ser una mejora para su propuesta de type constraints . Permite usar el sistema de tipos para restringir comportamientos como "iniciar una transacción antes de la consulta" para verificar el tipo sin reflexión en tiempo de ejecución. Mira este video al respecto: https://www.youtube.com/watch?v=jSpio0x7024

@jadbox lo siento si mi explicación no fue clara, pero la cláusula 'dónde' es casi exactamente lo que estaba proponiendo. Las cosas después de 'dónde' en rust son restricciones de tipo, pero creo que usé la palabra clave 'requiere' en una publicación anterior. Todo esto se hizo en Haskell hace al menos una década, excepto que Haskell usa el operador '=>' en las firmas de tipo para indicar restricciones de tipo, pero es el mismo mecanismo subyacente.

Dejé esto fuera de mi publicación de resumen anterior porque quería mantener las cosas simples, pero me gustaría algo como esto:

merge<K, V, T>(x, y T) T requires T: Map<K, V>

Pero en realidad no agrega nada a lo que puede hacer aparte de una sintaxis que puede ser más legible para conjuntos de restricciones largos. Puede representar cualquier cosa que pueda con la cláusula 'where' al poner la restricción después de que escriban variable en la declaración inicial de esta manera:

merge<K, V, T: Map<K, V>>(x, y T) T

Siempre que pueda hacer referencia a las variables de tipo antes de que se declaren, puede poner cualquier restricción allí y usaría una lista separada por comas para aplicar múltiples restricciones a la misma variable de tipo.

Por lo que yo sé, la única ventaja de una cláusula 'where'/'requires' es que todas las variables de tipo ya están declaradas por adelantado, lo que puede hacer que sea más fácil para el analizador y para la inferencia de tipo.

¿Sigue siendo este el hilo adecuado para comentarios/debates sobre la propuesta actual/más reciente de Go 2 Generics que se anunció recientemente?

En resumen, me gusta mucho la dirección que está tomando la propuesta en general y el mecanismo de contratos en particular. Pero me preocupa lo que parece ser una suposición decidida en todo momento de que los parámetros genéricos en tiempo de compilación deben (siempre) ser parámetros de tipo. He escrito algunos comentarios sobre este tema aquí:

¿Solo los parámetros de tipo son lo suficientemente genéricos para los genéricos de Go 2?

Ciertamente, los comentarios aquí están bien, pero en general no creo que los problemas de GitHub sean un buen formato para la discusión, ya que no proporcionan ningún tipo de subprocesamiento. Creo que las listas de correo son mejores.

No creo que esté claro aún con qué frecuencia la gente querrá funciones parametrizadas en valores constantes. El caso más obvio sería para las dimensiones de la matriz, pero ya puede hacerlo pasando el tipo de matriz deseado como argumento de tipo. Aparte de ese caso, ¿qué ganamos realmente al pasar una const como un argumento de tiempo de compilación en lugar de un argumento de tiempo de ejecución?

Go ya ofrece muchas maneras diferentes y excelentes de resolver problemas y nunca deberíamos agregar nada nuevo a menos que esté solucionando un problema y una deficiencia realmente grandes, lo que claramente no está haciendo, e incluso en tales circunstancias, la complejidad adicional que sigue es una muy alto precio a pagar.

Go es único exactamente por la forma en que es. Si no está roto , ¡no intentes arreglarlo!

Las personas que no están contentas con la forma en que se diseñó Go deberían ir y usar uno de la multitud de otros lenguajes que ya poseen esta complejidad añadida y molesta.

Go es único exactamente por la forma en que es. Si no está roto, ¡no intentes arreglarlo!

Está roto, por lo que debe ser reparado.

Está roto, por lo que debe ser reparado.

Puede que no funcione de la manera que crees que debería, pero un idioma nunca puede hacerlo. Ciertamente no está roto de ninguna manera. Teniendo en cuenta la información disponible y el debate, tomarse el tiempo para tomar una decisión informada y sensata es siempre la mejor opción. Muchos otros lenguajes han sufrido, en mi opinión, debido a la adición de más y más funciones para resolver más y más problemas potenciales. Recuerda que el "no" es temporal, el "sí" es para siempre.

Habiendo participado en mega-números anteriores, ¿puedo sugerir que se abra un canal en Gopher Slack para aquellos que quieran discutir esto, el problema se bloquee temporalmente y luego se publiquen las horas en que el problema se descongelará para cualquiera que quiera consolidar el tema? discusión de Slack? Los problemas de Github ya no funcionan como un foro una vez que aparece el temido enlace "478 elementos ocultos Cargar más...".

¿Puedo sugerir que se abra un canal en Gopher Slack para aquellos que quieran discutir esto?
Las listas de correo son mejores porque proporcionan un archivo de búsqueda. Todavía se puede publicar un resumen sobre este tema.

Habiendo participado en mega-números anteriores, ¿puedo sugerir que se abra un canal en Gopher Slack para aquellos que quieran discutir esto?

No mueva la discusión por completo a plataformas cerradas. En cualquier lugar, golang-nuts está disponible para todos (¿o sí? No sé si eso funciona sin una cuenta de Google, pero al menos es un método estándar de comunicación que todos tienen o pueden obtener) y debería moverse allí. . GitHub es lo suficientemente malo, pero acepto a regañadientes que estamos atrapados en él para la comunicación, no todos pueden obtener una cuenta de Slack o pueden usar sus terribles clientes.

no todos pueden obtener una cuenta de Slack o pueden usar sus terribles clientes

¿Qué significa "puede" aquí? ¿Hay restricciones reales en Slack que no conozco o a la gente simplemente no le gusta usarlo? Lo último está bien, supongo, pero algunas personas también boicotean a Github porque no les gusta Microsoft, por lo que pierdes a algunas personas pero ganas a otras.

no todos pueden obtener una cuenta de Slack o pueden usar sus terribles clientes

¿Qué significa "puede" aquí? ¿Hay restricciones reales en Slack que no conozco o a la gente simplemente no le gusta usarlo? Lo último está bien, supongo, pero algunas personas también boicotean a Github porque no les gusta Microsoft, por lo que pierdes a algunas personas pero ganas a otras.

Slack es una empresa estadounidense y, como tal, seguirá cualquier política exterior impuesta por EE. UU.

Github tiene el mismo problema y solo apareció en las noticias por expulsar a los iraníes sin previo aviso. Es desafortunado, pero a menos que usemos Tor o IPFS o algo así, tendremos que respetar la ley estadounidense/europea para cualquier foro de discusión práctico.

Github tiene el mismo problema y solo apareció en las noticias por expulsar a los iraníes sin previo aviso. Es desafortunado, pero a menos que usemos Tor o IPFS o algo así, tendremos que respetar la ley estadounidense/europea para cualquier foro de discusión práctico.

Sí, estamos atascados con GitHub y Google Groups. No agreguemos más servicios problemáticos a la lista. Además, el chat no es un buen archivo; ya es bastante difícil profundizar en estas discusiones cuando están bien hiladas y en las tuercas de golang (donde llegan directamente a su bandeja de entrada). Slack significa que si no estás en la misma zona horaria que los demás, tienes que navegar a través de una gran cantidad de archivos de chat, uno de los que no son sequiters, etc. más tiempo en sus respuestas para que no reciba toneladas de comentarios únicos al azar dejados casualmente. Además, simplemente no tengo una cuenta de Slack y sus estúpidos clientes no funcionarán en ninguna de las máquinas que uso. Mutt, por otro lado (o su cliente de correo electrónico de elección, yay estándares) funciona en todas partes.

Por favor, mantenga este tema sobre los genéricos. Vale la pena discutir el hecho de que el rastreador de problemas de GitHub no es ideal para debates a gran escala como los genéricos, pero no en este tema. He marcado varios comentarios anteriores como "fuera de tema".

Con respecto a la singularidad de Go: Go tiene algunas características interesantes, pero no es tan única como algunos parecen pensar. Como dos ejemplos, CLU y Modula-3 tienen objetivos similares y beneficios similares, y ambos admiten genéricos de alguna forma (¡desde ~ 1975 en el caso de CLU!) No tienen soporte industrial en la actualidad, pero FWIW, es posible obtener un compilador trabajando para ambos.

Un par de consultas sobre la sintaxis, ¿se requiere la palabra clave type en los parámetros de tipo? y ¿tendría más sentido adoptar <> para los parámetros de tipo como otros idiomas? Esto podría hacer que las cosas sean más legibles y familiares...

Aunque no estoy en contra de la forma en que está en la propuesta, solo pongo esto a consideración.

en vez de:

type Vector(type Element) []Element
var v Vector(int)
func (v *Vector(Element)) Push(x Element) { *v = append(*v, x) }
type VectorInt = Vector(int)

nosotros podríamos tener

type Vector<Element> []Element
var v Vector<int>
func (v *Vector<Element>) Push(x Element) { *v = append(*v, x) }
type VectorInt = Vector<int>

La sintaxis <> se menciona en el borrador, @jnericks (su nombre de usuario es perfecto para esta discusión...). El principal argumento en su contra es que aumenta enormemente la complejidad del analizador. En términos más generales, hace que Go sea un lenguaje significativamente más difícil de analizar para obtener pocos beneficios. La mayoría de la gente está de acuerdo en que mejora la legibilidad, pero hay desacuerdo sobre si vale la pena o no la compensación. Personalmente, no creo que lo sea.

El uso de la palabra clave type es necesario para eliminar la ambigüedad. De lo contrario, es difícil saber la diferencia entre func Example(T)(arg int) {} y func Example(arg int) (int) {} .

Leí la última propuesta sobre los genéricos go. todos coinciden con mi gusto, excepto la gramática de declaración de contrato.

como sabemos, en go siempre declaramos struct o interface así:

type MyStruct struct {
        a int
        s string
}

type MyInterface inteface {
    Method1() err
    Method2() string
}

pero la declaración del contrato en la última propuesta es así:

contract Ordered(T) {
    T int, int8
}

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

En mi pensamiento, la gramática del contrato es inconsistente en su forma con el enfoque tradicional. ¿Qué hay de la gramática como a continuación:

type Ordered(T) contract {
    T int, int8
}

if there is only one type parameter, the declaration above can be also wrote like this:

type Ordered contract {
    int , int8
}


if there are more than one type parameter, we have to use named parameter:

type G(Node, Edge) contract {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

ahora la forma de contrato es acorde con la tradicional. podemos declarar un contrato en un bloque de tipo con estructura, interfaz:

type (
        Sequence contract {
                string, []byte
        }

    Stringer(T) contract {
        T String() string
    }

    Stringer contract { // equivalent with the above Stringer(T), single type parameter could be omitted
        String() string
    }

        MyStruct struct {
                a int
                b string
        }

    G(Node, Edge) contract {
        Node Edges() []Edge
        Edge Nodes() (from Node, to Node)
    }
)

Entonces, el "contrato" se convierte en la palabra clave del mismo nivel que struct, interfaz. La diferencia es que el contrato se usa para declarar el tipo meta por tipo.

@bigwhite Todavía estamos discutiendo esta notación. El argumento a favor de la notación sugerida en el borrador de diseño es que un contrato no es un tipo (p. ej., no se puede declarar una variable de un tipo de contrato), por lo que un contrato es un nuevo tipo de entidad en el mismo sentido que una constante. , función, variable o tipo. El argumento a favor de su sugerencia es que un contrato es simplemente un "tipo de tipo" (o un metatipo) y, por lo tanto, debe seguir una notación consistente. Otro argumento a favor de su sugerencia es que permitiría el uso de contratos literales "anónimos" sin la necesidad de declararlos explícitamente. En resumen, en mi humilde opinión, esto aún no está resuelto. Pero también es fácil cambiar en el futuro.

FWIW, CL 187317 admite ambas notaciones en este momento (aunque el parámetro del contrato debe escribirse con el contrato), por ejemplo:

type C contract(X) { ... }

y

contract C (X) { ... }

son aceptados y representados de la misma manera internamente. El enfoque más consistente sería:

type C(type X) contract { ... }

Un contrato no es un tipo. Ni siquiera es un metatipo, ya que los únicos tipos lo
preocupaciones son sus parámetros. No hay un tipo de receptor separado
del cual el contrato podría considerarse el tipo meta.

Go también tiene declaraciones de funciones:

func Name(args) { body }

que la sintaxis del contrato propuesta refleja más directamente.

De todos modos, este tipo de discusiones de sintaxis parecen bajas en la lista de prioridades en
este punto. Es más importante observar la semántica del borrador y
cómo afectan el código, qué tipo de código se puede escribir en función de esos
semántica y qué código no puede.

Editar: con respecto a los contratos en línea, Go tiene literales de función. No veo ninguna razón por la que no pueda haber contratos literales. Simplemente habría un número más limitado de lugares en los que podrían aparecer, ya que no son tipos ni valores.

@stevenblenkinsop No iría tan lejos como para afirmar que un contrato no es un tipo (o metatipo). Creo que hay argumentos muy razonables para ambos puntos de vista. Por ejemplo, un contrato de parámetro único que solo especifica métodos sirve esencialmente como un "límite superior" para un parámetro de tipo: cualquier argumento de tipo válido debe implementar esos métodos. Que es para lo que solemos usar las interfaces. Puede tener mucho sentido permitir interfaces en esos casos en lugar de un contrato, a) porque estos casos pueden ser comunes; yb) porque satisfacer un contrato en este caso simplemente significa satisfacer la interfaz enunciada como contrato. Es decir, dicho contrato actúa de manera muy parecida a un tipo con el que se "compara" otro tipo.

@griesemer considerar los contratos como tipos puede generar problemas con la paradoja de Russel (como en el tipo de todos los tipos que no son 'miembros' de sí mismos). Creo que se consideran mejor 'restricciones en los tipos'. Si consideramos un sistema de tipos como una forma de 'lógica', podemos hacer un prototipo de esto en Prolog. Las variables de tipo se convierten en variables lógicas, los tipos se convierten en átomos y los contratos/restricciones se pueden resolver mediante la programación lógica de restricciones. Todo es muy claro y no paradójico. En términos de sintaxis, podríamos considerar un contrato como una función sobre tipos que devuelve un valor booleano.

@keean Cualquier interfaz ya sirve como una "restricción de tipos", pero son tipos. La gente de la teoría de tipos mira mucho las restricciones de los tipos como tipos, de una manera muy formal. Como mencioné anteriormente , hay argumentos razonables que se pueden hacer para cualquier punto de vista. No hay "paradojas lógicas" aquí; de hecho, el prototipo de trabajo en progreso actual modela un contrato como un tipo internamente, ya que simplifica las cosas en este momento.

Las interfaces de @griesemer en Go son 'subtipos', no restricciones en los tipos. Sin embargo, encuentro que la necesidad de contratos e interfaces es una desventaja para el diseño de Go; sin embargo, puede ser demasiado tarde para cambiar las interfaces en restricciones de tipo en lugar de subtipos. He argumentado anteriormente que las interfaces de Go no necesariamente tienen que ser subtipos, pero no veo mucho apoyo para esa idea. Esto permitiría que las interfaces y los contratos fueran lo mismo, si las interfaces también pudieran declararse para los operadores.

Aquí hay paradojas, así que vaya con cuidado, la paradoja de Girard es la 'codificación' más común de la paradoja de Russel en la teoría de tipos. La teoría de tipos introduce el concepto de universos para evitar estas paradojas, y solo se le permite hacer referencia a tipos en el universo 'U' desde el universo 'U+1'. Internamente, estas teorías de tipos se implementan como lógicas de orden superior (por ejemplo, Elf usa lambda-prolog). Esto, a su vez, se reduce a la resolución de restricciones para el subconjunto decidible de la lógica de orden superior.

Entonces, si bien puede pensar en ellos como tipos, debe agregar un conjunto de restricciones de uso (sintácticas o de otro tipo) que lo lleven de vuelta a las restricciones de tipos. Personalmente, me resulta más fácil trabajar directamente con las restricciones y evitar las dos capas adicionales de abstracción, la lógica de orden superior y los tipos dependientes. Estas abstracciones no añaden nada al poder expresivo del sistema de tipos y requieren más reglas o restricciones para evitar paradojas.

Con respecto al prototipo actual que trata las restricciones como tipos, el peligro surge si puede usar este "tipo de restricción" como un tipo normal y luego construir otro "tipo de restricción" en ese tipo. Necesitará comprobaciones para evitar la autorreferencia (esto suele ser trivial) y los bucles de referencia mutua. Este tipo de prototipo realmente debería escribirse en Prolog, ya que le permite concentrarse en las reglas de implementación. Creo que los desarrolladores de Rust finalmente se dieron cuenta de esto hace un tiempo (ver Chalk).

@griesemer Interesante, re modelar contratos como tipos. Desde mi propio modelo mental, pensaría en las restricciones como metatipos y en los contratos como una especie de estructura de nivel de tipo.

type A int
func (a A) Foo() int {
    return int(a)
}

type C contract(T, U) {
    T int
    U int, uint
    U Foo() int
}

var B (int, uint; Foo() int).type = A
var C1 C = C(A, B)

Esto me sugiere que la sintaxis de estilo de declaración de tipo actual para contratos es la más correcta de las dos. Sin embargo, creo que la sintaxis establecida en el borrador es aún mejor, ya que no requiere abordar la pregunta "si es un tipo, cómo se ven sus valores".

@stevenblenkinsop me perdiste, ¿por qué pasas T a C contract cuando no se usa, y qué intentan hacer las líneas var ?

@griesemer gracias por tu respuesta. Uno de los principios de diseño de Go es "solo proporcionar una forma de hacer algo". Es mejor mantener solo un formulario de declaración de contrato. tipo C (tipo X) contrato { ... } es mejor.

@Goodwine He cambiado el nombre de los tipos para distinguirlos de los parámetros del contrato. ¿Quizás eso ayude? (int, uint; Foo() int).type pretende ser el metatipo de cualquier tipo que tenga un tipo subyacente de int o uint y que implemente Foo() int . var B pretende mostrar el uso de un tipo como valor y asignarlo a una variable cuyo tipo es un metatipo (ya que un metatipo es como un tipo cuyos valores son tipos). var C1 pretende mostrar una variable cuyo tipo es un contrato y mostrar un ejemplo de algo que podría asignarse a dicha variable. Básicamente, tratando de responder a la pregunta "si un contrato es un tipo, ¿cómo son sus valores?". El punto es mostrar que ese valor no parece ser un tipo en sí mismo.

Tengo un problema con los contratos con varios tipos.

Puedes agregarlo o dejarlo por tipo de contrato paremeter, ambos
type Graph (type Node, Edge) struct { ... }
y
type Graph (type Node, Edge G) struct { ... } están bien.

Pero, ¿qué pasa si solo quiero agregar un contrato en uno de los dos parámetros de tipo?

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

contra

contract G(Edge) {
    Edge Nodes() (from Node, to Node)
}

@themez Eso está en el borrador. Puede usar la sintaxis (type T, U comparable(T)) para restringir solo un parámetro de tipo, por ejemplo.

@stevenblenkinsop Ya veo, gracias.

@themez Esto ha surgido un par de veces ahora. Creo que hay cierta confusión por el hecho de que el uso parece un tipo para una definición de variable. Aunque realmente no lo es; un contrato es más un detalle de toda la función que una definición de argumento. Creo que la suposición es que esencialmente escribiría un nuevo contrato, potencialmente compuesto de otros contratos para ayudar con la repetición, básicamente para cada función/tipo genérico que cree. Cosas como las que mencionó @stevenblenkinsop realmente están ahí para detectar los casos extremos en los que esa suposición no tiene sentido.

Al menos, esa es la impresión que tengo, especialmente por el hecho de que se llaman 'contratos'.

@keean Creo que estamos interpretando la palabra "restricción" de manera diferente; Lo estoy usando bastante informalmente. Por definición de interfaces, dada una interfaz I y una variable x de tipo I , solo se pueden asignar valores con tipos que implementen I a x . Por lo tanto I puede verse como una "restricción" en esos tipos (por supuesto, todavía hay infinitos tipos que satisfacen esa "restricción"). De manera similar, uno podría usar I como una restricción para un parámetro de tipo P de una función genérica; solo se permitirían argumentos de tipo real con conjuntos de métodos que implementen I . Por lo tanto I también limita el conjunto de posibles tipos de argumentos reales.

En ambos casos, la razón de esto es describir las operaciones disponibles (métodos) dentro de la función. Si el I se usa como el tipo de un parámetro (valor), sabemos que el parámetro proporciona esos métodos. Si el I se usa como una "restricción" (en lugar de un contrato), sabemos que todos los valores del parámetro de tipo tan restringido proporcionan esos métodos. Obviamente es bastante sencillo.

Me gustaría un ejemplo concreto de por qué esta idea específica de usar interfaces para contratos de un solo parámetro que solo declaran métodos "se rompe" sin algunas restricciones, como aludió en su comentario .

¿Cómo se presentará la propuesta de contratos? ¿Usando el parámetro go módulos go1.14 ? ¿Una variable de entorno GO114CONTRACTS ? ¿Ambos? Algo más..?

Lo siento si esto se ha abordado antes, no dude en redirigirme allí.

Una cosa que me gusta especialmente del diseño de borrador genérico actual es que pone el agua clara entre contracts y interfaces . Siento que esto es importante porque los dos conceptos se confunden fácilmente a pesar de que hay tres diferencias básicas entre ellos:

  1. Contracts describen los requisitos de un _conjunto_ de tipos, mientras que interfaces describen los métodos que debe tener un tipo _único_ para satisfacerlo.

  2. Contracts puede manejar operaciones integradas, conversiones, etc. enumerando los tipos que las admiten; interfaces solo puede tratar con métodos que los tipos incorporados no tienen.

  3. Sean cuales sean en términos teóricos de tipos, contracts no son tipos en el sentido en que normalmente los consideramos en Go, es decir, no se pueden declarar variables de tipos contract y darles algún valor. Por otro lado interfaces son tipos, puede declarar variables de esos tipos y asignarles valores apropiados.

Aunque puedo ver el sentido de un contract , que requiere un solo parámetro de tipo para tener ciertos métodos, para ser representado por un interface (es algo que incluso he defendido en mi propio pasado propuestas), creo que ahora sería un movimiento desafortunado porque volvería a enturbiar las aguas entre contracts y interfaces .

Realmente no se me había ocurrido antes que contracts podría declararse plausiblemente en la forma en que @bigwhite sugirió usar el patrón 'tipo' existente. Sin embargo, de nuevo no me gusta la idea porque siento que comprometería (3) arriba. Además, si es necesario (por razones de análisis) repetir la palabra clave type al declarar una estructura genérica como esta:

type List(type Element) struct {
    next *List(Element)
    val  Element
}

presumiblemente, también sería necesario repetirlo si contracts se declarara de manera similar, lo que es un poco "tartamudo" en comparación con el enfoque de diseño preliminar.

Otra idea que no me gusta es la de los 'contratos literales' que permitirían que contracts se escribieran 'en su lugar' en lugar de construcciones separadas. Esto haría que las funciones genéricas y las definiciones de tipo fueran más difíciles de leer y, como algunas personas piensan que ya lo son, no ayudará a persuadir a esas personas de que los genéricos son algo bueno.

Lamento parecer tan resistente a los cambios propuestos en el borrador de genéricos (que ciertamente tiene algunos problemas), pero, como un entusiasta defensor de los genéricos simples para Go, creo que vale la pena mencionar estos puntos.

Me gustaría sugerir no llamar predicados sobre tipos "contratos". Hay dos razones:

  • El término "contratos" ya se usa en informática de una manera diferente. Por ejemplo, consulte: (https://scholar.google.com/scholar?hl=en&as_sdt=0%2C33&q=contracts+languages&btnG=)
  • Ya hay varios nombres para esta idea en la literatura informática. Conozco al menos ~tres~ cuatro: "composiciones tipográficas", "clases de tipos", "conceptos" y "restricciones". Agregar otro solo confundirá más las cosas.

@griesemer Las "restricciones en los tipos" son puramente una cuestión de tiempo de compilación, porque los tipos se borran antes del tiempo de ejecución. Las restricciones hacen que el código genérico se transforme en código no genérico que se puede ejecutar. Los subtipos existen en tiempo de ejecución y no son restricciones en el sentido de que una restricción sobre los tipos sería como mínimo la igualdad de tipos o la desigualdad de tipos, con restricciones como 'es un subtipo de' opcionalmente disponibles dependiendo del sistema de tipos.

Para mí, la naturaleza del tiempo de ejecución de los subtipos es la diferencia crítica, si X <: Y podemos pasar X donde se espera Y, pero solo conocemos el tipo como Y sin operaciones de tiempo de ejecución inseguras. En este sentido, no restringe el tipo Y, Y es siempre Y. La creación de subtipos también es 'direccional', por lo que puede ser covariante o contravariante dependiendo de si se aplica a un argumento de entrada o de salida.

Con una restricción de tipo 'pred(X)', comenzamos con una X completamente polimórfica y luego restringimos los valores permitidos. Entonces diga solo X que implemente 'imprimir'. Esto no es direccional y, por lo tanto, no tiene covarianza ni contravarianza. De hecho, es invariable en el sentido de que conocemos el tipo básico de X en tiempo de compilación.

Entonces, creo que es peligroso pensar en las interfaces como restricciones en los tipos, ya que ignora diferencias importantes como la covarianza y la contravarianza.

¿Eso responde a su pregunta, o me perdí el punto?

Editar: debo señalar que me refiero a las interfaces 'Ir' específicamente arriba. Los puntos sobre los subtipos se aplican a todos los idiomas que tienen subtipos, pero Go es inusual al hacer que las interfaces sean un tipo y, por lo tanto, tener una relación de subtipos. En otros lenguajes como Java, una interfaz no es explícitamente un tipo (una clase es un tipo), por lo que las interfaces _son_ una restricción sobre los tipos. Entonces, si bien en general es correcto considerar las interfaces como restricciones en los tipos, es incorrecto específicamente para 'Ir'.

@Inuart Es demasiado pronto para decir cómo se agregaría esto a la implementación. Aún no hay propuesta, solo un borrador de diseño. Ciertamente no estará en 1.14.

@andrewcmyers Me gusta la palabra "contrato" porque describe una relación entre el escritor de la función genérica y la persona que llama.

Palabras como "composiciones tipográficas" y "clases de tipo" sugieren que estamos hablando de un metatipo, lo que por supuesto somos, pero los contratos también describen una relación entre múltiples tipos. Sé que las clases de tipo en, por ejemplo, Haskell, pueden tener varios parámetros de tipo, pero me parece que el nombre no encaja bien con la idea que se describe.

Nunca he entendido por qué C++ llama a esto un "concepto". ¿Y eso que significa?

"Restricción" o "restricciones" estaría bien para mí. En este momento pienso en un contrato como si contuviera múltiples restricciones. Pero podríamos cambiar ese pensamiento.

No estoy demasiado preocupado por el hecho de que existe una construcción de lenguaje de programación existente llamada "contrato". Creo que esa idea es relativamente similar a la idea que queremos expresar, en el sentido de que es una relación entre una función y sus llamadores. Entiendo que la forma en que se expresa esa relación es bastante diferente, pero siento que hay una similitud subyacente.

Nunca he entendido por qué C++ llama a esto un "concepto". ¿Y eso que significa?

Un concepto es una abstracción de instanciaciones que comparten algo en común, por ejemplo, firmas.

El término concepto se ajusta mucho mejor a las interfaces, ya que este último también se utiliza para denotar un límite compartido entre dos componentes.

@sighoya También iba a mencionar que los 'conceptos' son conceptuales porque incluyen 'axiomas' que son vitales para evitar el abuso de operadores. Por ejemplo, la suma '+' debe ser asociativa y conmutativa. Estos axiomas no se pueden representar en C++, por lo que existen como ideas abstractas, por lo tanto, "conceptos". Entonces, un concepto es el 'contrato' sintáctico más los axiomas semánticos.

@ianlancetaylor "Restricción" es como lo llamamos en Genus (http://www.cs.cornell.edu/~yizhou/papers/genus-pldi2015.pdf), así que soy partidario de esa terminología. El término "contrato" sería una opción completamente razonable, excepto que se usa de forma muy activa en la comunidad de PL para referirse a la relación entre interfaces e implementaciones, que también tiene un sabor contractual.

@keean Sin ser un experto, no creo que la dicotomía que estás pintando refleje muy bien la realidad. Por ejemplo, si el compilador genera versiones instanciadas de funciones genéricas es completamente una cuestión de implementación, por lo que es perfectamente razonable tener una representación de restricciones en tiempo de ejecución, digamos en forma de una tabla de punteros de función para cada operación requerida. Exactamente como las tablas de métodos de interfaz, de hecho. Del mismo modo, las interfaces en Go no se ajustan a su definición de subtipo, porque puede proyectarlas hacia abajo de manera segura (a través de aserciones de tipo) y porque no tiene co- ni contravarianza para ningún constructor de tipo en Go.

Por último: si la dicotomía que está pintando es o no realista y precisa, no cambia el hecho de que una interfaz es, al final del día, solo una lista de métodos, e incluso en su dicotomía, no hay razón por la que esa lista pueda no se puede reutilizar como una tabla representada en tiempo de ejecución o como una restricción de solo tiempo de compilación, según el contexto en el que se use.

¿Qué tal algo como:

tipoRestricción C(T) {
}

o

tipoContrato C(T) {
}

Es diferente de otras declaraciones de tipo enfatizar que esta no es una construcción de tiempo de ejecución.

Sobre el nuevo diseño del contrato, tengo algunas preguntas.

1.

Cuando un genérico tipo A incorpora otro genérico tipo B,
o una función genérica A llama a otra función genérica B,
¿Necesitamos también especificar los contratos de B sobre A?

Si la respuesta es verdadera, entonces si un tipo genérico incorpora muchos otros tipos genéricos,
o una función genérica llama a muchas otras funciones genéricas,
entonces necesitamos combinar muchos contratos en uno como el contrato del tipo de incrustación o la función de llamada.
Esto puede causar el mismo problema de envenenamiento constante.

  1. Además de las restricciones actuales del conjunto de tipos y métodos, ¿necesitamos otras restricciones?
    Como convertible de un tipo a otro, asignable de un tipo a otro,
    comparable entre dos tipos, es un canal que se puede enviar, es un canal que se puede recibir,
    tiene un conjunto de campos especificado, ...

3.

Si una función genérica usa una línea como la siguiente

v.Foo()

¿Cómo podemos escribir un contrato que permita que Foo sea ​​un método o un campo de un tipo de función?

Las restricciones de tipos de @merovius deben resolverse en el momento de la compilación, o el sistema de tipos puede no ser sólido. Esto se debe a que puede tener un tipo que dependa de otro que no se conoce hasta el tiempo de ejecución. Luego tiene dos opciones, debe implementar un sistema de tipo dependiente completo (que permite que se realice la verificación de tipo en tiempo de ejecución a medida que se conocen los tipos) o debe agregar tipos existenciales al sistema de tipo. Los existenciales codifican la diferencia de fase de tipos conocidos estáticamente y tipos que solo se conocen en tiempo de ejecución (tipos que dependen de la lectura de IO, por ejemplo).

Los subtipos, como se indicó anteriormente, normalmente no se conocen hasta el tiempo de ejecución, aunque muchos lenguajes tienen optimizaciones en el caso de que el tipo se conozca estáticamente.

Si asumimos que uno de los cambios anteriores se introduce en el lenguaje (tipos dependientes o tipos existenciales), entonces todavía necesitamos separar los conceptos de subtipificación y restricciones de tipo. Para Go específicamente, los constrictores de tipo son invariantes, podemos ignorar estas diferencias, y podemos considerar que las interfaces de Go _son_ restricciones en los tipos (estáticamente).

Por lo tanto, podemos considerar una interfaz Go como un contrato de parámetro único donde el parámetro es el receptor de todas las funciones/métodos. Entonces, ¿por qué go tiene interfaces y contratos? Me parece que Go no quiere permitir interfaces para operadores (como '+'), y porque Go no tiene tipos dependientes ni tipos existenciales.

Entonces, hay dos factores que crean una diferencia real entre las restricciones de tipo y la subtipificación. Una es la varianza co/contra, que podemos ignorar en Go debido a la invariancia del constructor de tipos, y la otra es la necesidad de tipos dependientes o tipos existenciales para hacer que un sistema de tipos que tenga restricciones de tipo suene si hay polimorfismo en tiempo de ejecución de los parámetros de tipo para las restricciones de tipo.

@keean Genial, AIUI al menos estamos de acuerdo en que las interfaces en Go pueden considerarse restricciones :)

Con respecto al resto: Arriba afirmaste:

Las "restricciones en los tipos" son puramente una cuestión de tiempo de compilación, porque los tipos se borran antes del tiempo de ejecución. Las restricciones hacen que el código genérico se transforme en código no genérico que se puede ejecutar.

Ese reclamo es más específico que el último, que las restricciones deben resolverse en tiempo de compilación. Todo lo que estaba tratando de decir es que el compilador puede hacer esa resolución (y todas las mismas verificaciones de tipo), pero luego generar código genérico. Seguiría siendo correcto, porque la semántica del sistema de tipos es la misma. Pero las restricciones aún tendrían una representación en tiempo de ejecución. Eso es un poco quisquilloso, pero es por eso que creo que definirlos en función del tiempo de ejecución frente al tiempo de compilación no es la mejor manera de hacerlo. Está mezclando preocupaciones de implementación en una discusión sobre la semántica abstracta de un sistema de tipos.

FWIW, he argumentado antes que preferiría usar interfaces para expresar restricciones, y también llegué a la conclusión de que permitir el uso de operadores en código genérico es el principal obstáculo para hacerlo y, por lo tanto, la razón principal para introducir una separada concepto en forma de contratos.

@keean Gracias, pero no, su respuesta no respondió a mi pregunta. Tenga en cuenta que en mi comentario describí un ejemplo muy simple del uso de una interfaz en lugar de un contrato/"restricción" correspondiente. Pedí un ejemplo _simple_ _concreto_ de por qué este escenario no funcionaría "sin algunas restricciones", como mencionaste en tu comentario anterior. Usted no proporcionó tal ejemplo.

Tenga en cuenta que no mencioné subtipos, covarianza o contravarianza (que de todos modos no permitimos en Go, las firmas siempre deben coincidir), etc. En su lugar, he estado usando terminología Go elemental y establecida (interfaces, implementos, parámetro de tipo, etc.) para explicar lo que quiero decir con "restricción" porque ese es el lenguaje común que todos aquí entienden y todos pueden seguirlo. (Además, contrariamente a su afirmación aquí , en Java, una interfaz me parece un tipo de acuerdo con la especificación de Java : "Una declaración de interfaz especifica un nuevo tipo de referencia con nombre". Si esto no dice que una interfaz es un tipo, entonces la gente de Java Spec tiene trabajo que hacer).

Pero parece que respondió a mi pregunta indirectamente con su último comentario , como @Merovius ya observó, cuando dice: "Por lo tanto, podemos considerar que una interfaz Go es un contrato de un solo parámetro donde el parámetro es el receptor de todas las funciones/métodos .". Este es exactamente el punto que estaba diciendo al principio, así que gracias por confirmar lo que dije todo el tiempo.

@dotaheor

Cuando un tipo genérico A incorpora otro tipo genérico B, o una función genérica A llama a otra función genérica B, ¿necesitamos también especificar los contratos de B sobre A?

Si un tipo genérico A incorpora otro tipo genérico B, entonces los parámetros de tipo pasados ​​a B deben satisfacer cualquier contrato utilizado por B. Para hacerlo, el contrato utilizado por A debe implicar el contrato utilizado por B. Es decir, todas las restricciones Los parámetros de tipo pasados ​​a B deben expresarse en el contrato utilizado por A. Esto también se aplica cuando una función genérica llama a otra función genérica.

Si la respuesta es verdadera, entonces si un tipo genérico incrusta muchos otros tipos genéricos, o una función genérica llama a muchas otras funciones genéricas, entonces necesitamos combinar muchos contratos en uno como el contrato del tipo incrustado o la función de llamador. Esto puede causar el mismo problema de envenenamiento constante.

Creo que lo que dices es cierto, pero no es el problema del envenenamiento constante. El problema del envenenamiento constante es que debe distribuir const todas partes donde se pasa un argumento, y luego, si descubre algún lugar donde el argumento debe cambiarse, debe eliminar const todas partes. El caso con los genéricos es más como "si llama a varias funciones, debe pasar valores del tipo correcto a cada una de esas funciones".

En cualquier caso, me parece extremadamente improbable que las personas escriban funciones genéricas que llamen a muchas otras funciones genéricas que usan contratos diferentes. ¿Cómo sucedería eso naturalmente?

Además de las restricciones actuales del conjunto de tipos y métodos, ¿necesitamos otras restricciones? Como convertible de un tipo a otro, asignable de un tipo a otro, comparable entre dos tipos, es un canal que se puede enviar, es un canal que se puede recibir, tiene un conjunto de campos especificado, ...

Las restricciones como la convertibilidad, la asignabilidad y la comparabilidad se expresan en forma de tipos, como explica el borrador del diseño. Las restricciones, como el canal que se puede enviar o recibir, solo se pueden expresar en forma de chan T donde T es algún parámetro de tipo, como se explica en el borrador del diseño. No hay forma de expresar la restricción de que un tipo tiene un conjunto de campos específico, pero dudo que eso suceda muy a menudo. Tendremos que ver cómo funciona esto escribiendo código real para ver qué sucede.

Si una función genérica usa una línea como la siguiente

v.Foo()
¿Cómo podemos escribir un contrato que permita que Foo sea un método o un campo de un tipo de función?

En el borrador de diseño actual, no se puede. ¿Parece un caso de uso importante? (Sé que el borrador de diseño anterior apoyó esto).

@griesemer te perdiste el punto en el que dije que solo era válido si introduces tipos dependientes o tipos existenciales en el sistema de tipos.

De lo contrario, si usa un contrato como interfaz, puede fallar en el tiempo de ejecución, porque necesita diferir la verificación de tipos hasta que conozca los tipos, y la verificación de tipos puede fallar, lo que, por lo tanto, no es seguro para los tipos.

También he visto interfaces explicadas como subtipos, por lo que debe tener cuidado de que alguien no intente introducir co/contra-varianza en los constructores de tipos en el futuro. Mejor no tener interfaces como tipos, entonces no hay posibilidad de ello, y las intenciones de los diseñadores, que no sean subtipos, son claras.

Para mí, sería un mejor diseño fusionar interfaces y contratos, y hacerlos explícitamente restricciones de tipo (predicados sobre tipos).

@ianlancetaylor

En cualquier caso, me parece extremadamente improbable que las personas escriban funciones genéricas que llamen a muchas otras funciones genéricas que usan contratos diferentes. ¿Cómo sucedería eso naturalmente?

¿Por qué sería eso inusual? Si defino una función en el tipo 'T', querré llamar funciones en 'T'. Por ejemplo, si defino una función de 'suma' sobre 'tipos adicionales' por contrato. Ahora quiero construir una función de multiplicación genérica que llame a sum. Muchas cosas en la programación tienen una estructura de suma/producto (cualquier cosa que sea un 'grupo').

No entiendo cuál será el propósito de la interfaz después de que los contratos estén en el idioma, parece que los contratos servirán para el mismo propósito, para garantizar que un tipo tenga un conjunto de métodos definidos.

@keean El caso inusual son las funciones que llaman a muchas otras funciones genéricas que usan contratos diferentes . Su contraejemplo solo llama a una función. Recuerde que estoy argumentando en contra de la similitud con el envenenamiento constante.

@mrkaspa La forma más sencilla de pensar es que los contratos son como funciones de plantilla de C++ y las interfaces son como métodos virtuales de C++. Hay un uso y un propósito para ambos.

@ianlancetaylor por experiencia, hay dos problemas que ocurren que son similares al envenenamiento constante. Ambos ocurren debido a la naturaleza de árbol de las llamadas a funciones anidadas. La primera es cuando desea agregar la depuración a una función profundamente anidada, debe agregar la imprimible desde la hoja hasta la raíz, lo que podría implicar tocar varias bibliotecas de terceros. La segunda es que puede acumular una gran cantidad de contratos en la raíz, lo que dificulta la lectura de las firmas de funciones. A menudo es mejor que el compilador deduzca las restricciones como lo hace Haskell con las clases de tipos para evitar estos dos problemas.

@ianlancetaylor No sé mucho sobre c ++, ¿cuáles serán los casos de uso para interfaces y contratos en golang? ¿Cuándo debo usar interfaz o contrato?

@keean Este hilo secundario trata sobre un borrador de diseño específico para el lenguaje Go. En Go todos los valores son imprimibles. No es algo que deba expresarse en un contrato. Y aunque estoy dispuesto a ver evidencia de que muchos contratos pueden acumularse para una sola función o tipo genérico, no estoy dispuesto a aceptar la afirmación de que eso sucederá. El objetivo del borrador de diseño es intentar escribir código real que lo use.

El borrador del diseño explica tan claramente como puedo por qué creo que inferir las restricciones es una mala elección para un lenguaje como Go, que está diseñado para programar a escala.

@mrkaspa Por ejemplo, si tiene []io.Reader , quiere un valor de interfaz, no un contrato. Un contrato requeriría que todos los elementos de la porción sean del mismo tipo. Una interfaz les permitirá ser de diferentes tipos, siempre que todos los tipos implementen io.Reader .

@ianlancetaylor , por lo que tengo, la interfaz crea un nuevo tipo, mientras que los contratos restringen un tipo pero no crea uno nuevo, ¿verdad?

@ianlancetaylor :

¿No podrías hacer algo como lo siguiente?

contract Reader(T) {
  T Read([]byte) (int, error)
}

func ReadAll(type T Reader)(readers []T) ([]byte, error) {
  // Use the readers...
}

Ahora ReadAll() debería aceptar un []io.Reader tan bien como aceptaría un []*os.File , ¿no es así? io.Reader parece cumplir el contrato, y no recuerdo nada en el borrador sobre los valores de interfaz que no se pueden usar como argumentos de tipo.

Editar: No importa. Entendí mal. Este sigue siendo un lugar donde estaría usando una interfaz, por lo que es una respuesta a la pregunta de @mrkaspa . Simplemente no está utilizando la interfaz en la firma de la función; solo lo estás usando donde se llama.

@mrkaspa Sí, eso es cierto.

@ianlancetaylor si tuviera una lista de []io.Reader y este contrato:

contract Reader(T) {
  T Read([]byte) (int, error)
}

func ReadAll(type T Reader)(readers []T) ([]byte, error) {
  // Use the readers...
}

¿Podría llamar a ReadAll en cada interfaz porque cumplen el contrato?

@ianlancetaylor seguro que las cosas se pueden imprimir, pero es fácil encontrar otros ejemplos, por ejemplo, iniciar sesión en un archivo o en la red, queremos que el registro sea genérico para poder cambiar el objetivo del registro entre nulo, archivo local, servicio de red, etc. iniciar sesión en una función de hoja requiere agregar las restricciones hasta el total de la raíz, incluida la modificación de las bibliotecas de terceros utilizadas.

El código no es estático, también debe permitir el mantenimiento. De hecho, el código está en 'mantenimiento' durante mucho más tiempo del que se tarda en escribir inicialmente, por lo que hay un buen argumento de que deberíamos diseñar lenguajes para facilitar el mantenimiento, la refactorización, la adición de funciones, etc.

Realmente estos problemas solo se manifestarán en una gran base de código, que se mantiene en el tiempo. No es algo que pueda escribir un pequeño ejemplo rápido para demostrar.

Estos problemas también existen en otros lenguajes genéricos, por ejemplo, Ada. Podría portar alguna aplicación grande de Ada que haga un uso extensivo de genéricos, pero si el problema existe en Ada, no veo nada en Go que pueda mitigar ese problema.

@mrkaspa Sí.

En este punto, sugiero que este hilo de conversación se mueva a golang-nuts. El rastreador de problemas de GitHub es un mal lugar para este tipo de discusión.

@keean Quizás tengas razón. El tiempo dirá. Estamos pidiendo explícitamente a las personas que intenten escribir código en el borrador del diseño. Hay poco valor en discusiones puramente hipotéticas.

@keean No entiendo tu ejemplo de registro. El problema que describe es algo que puede resolver con interfaces en tiempo de ejecución, no con genéricos en tiempo de compilación.

Las interfaces @bserdar solo tienen un parámetro de tipo, por lo que no puede hacer algo donde un parámetro es lo que se va a registrar y un segundo parámetro de tipo es el tipo del registro.

@keean IMO en ese ejemplo, haría lo mismo que está haciendo hoy, sin ningún parámetro de tipo: use la reflexión para inspeccionar lo que se va a registrar y use context.Context para pasar el valor del registro. Sé que estas ideas son repulsivas para los entusiastas de la mecanografía, pero resultan bastante prácticas. Por supuesto, hay valor en los parámetros de tipo restringidos, razón por la cual estamos teniendo esta conversación, pero diría que la razón por la que los casos que le vienen a la mente son los casos que ya funcionan bastante bien en las bases de código actuales de Go a escala . , son que esos no son los casos que realmente se benefician de una verificación de tipo estricta adicional. Lo que vuelve al punto de Ians: queda por ver si este es un problema que se manifiesta en la práctica.

@merovius Si dependiera de mí, se prohibiría toda reflexión en tiempo de ejecución, ya que no quiero que el software enviado genere errores de escritura en tiempo de ejecución que puedan afectar al usuario. Esto permite optimizaciones de compilador más agresivas porque no tiene que preocuparse de que el modelo de tiempo de ejecución se alinee con el modelo estático.

Habiendo lidiado con la migración de proyectos grandes a escala de JavaScript a TypeScript, en mi experiencia, la escritura estricta se vuelve más importante cuanto más grande es el proyecto y más grande es el equipo que trabaja en él. Esto se debe a que necesita confiar en la interfaz/contrato de un bloque de código sin tener que mirar la implementación para mantener la eficiencia cuando trabaja con un equipo grande.

Aparte: por supuesto, depende de cómo logre la escala, en este momento prefiero un enfoque API-First, comenzando con un archivo OpenAPI/Swagger JSON y luego usando la generación de código para construir los stubs del servidor y el SDK del cliente. Como tal, OpenAPI en realidad actúa como su tipo de sistema para microservicios.

@ianlancetaylor

Las restricciones como la convertibilidad, la asignabilidad y la comparabilidad se expresan en forma de tipos

Teniendo en cuenta que hay tantos detalles en las reglas de conversión de tipo Go, es realmente difícil escribir un contrato personalizado C para satisfacer la siguiente función general de conversión de segmentos:

func ConvertSlice(type In, Out C(In, Out)) (x []In) []Out {
    o := make([]Out, len(x))
    for i := range x {
        o[i] = Out(x[i])
    }
    return o
}

Un C perfecto debería permitir conversiones:

  • entre cualquier entero, tipos numéricos de coma flotante
  • entre cualquier tipo numérico complejo
  • entre dos tipos cuyos tipos subyacentes son idénticos
  • de un tipo Out que implementa In
  • de un tipo de canal a un tipo de canal bidireccional y los dos tipos de canal tienen un tipo de elemento idéntico
  • etiqueta de estructura relacionada, ...
  • ...

Según tengo entendido, no puedo escribir tal contrato. Entonces, ¿necesitamos un contrato integrado convertible ?

No hay forma de expresar la restricción de que un tipo tiene un conjunto de campos específico, pero dudo que surja muy a menudo.

Teniendo en cuenta que la incrustación de tipos se usa a menudo en la programación de Go, creo que las necesidades no serían raras.

@keean Esa es una opinión válida, pero obviamente no es la que guía el diseño y desarrollo de Go. Para participar constructivamente, acepte eso y comience a trabajar desde donde estamos y bajo el supuesto de que cualquier desarrollo del lenguaje debe ser un cambio gradual del status quo. Si no puede, entonces hay idiomas que están más alineados con sus preferencias y creo que todos, en particular usted, estarían más felices si contribuyeran con su energía allí.

@merovius Estoy preparado para aceptar que los cambios en Go deben ser graduales y aceptar el statu quo.

Solo estaba respondiendo a su comentario como parte de una conversación, aceptando que soy un entusiasta de la escritura. Expresé una opinión sobre la reflexión en tiempo de ejecución, no sugerí que Go debería abandonar la reflexión en tiempo de ejecución. Trabajo en otros idiomas, uso muchos idiomas en mi trabajo. Estoy desarrollando (lentamente) mi propio idioma, pero siempre tengo la esperanza de que el desarrollo de otros idiomas lo haga innecesario.

@dotaheor Estoy de acuerdo en que no podemos redactar un contrato general de convertibilidad hoy. Tendremos que ver si eso parece ser un problema en la práctica.

Respondiendo a @ianlancetaylor

No creo que esté claro aún con qué frecuencia la gente querrá funciones parametrizadas en valores constantes. El caso más obvio sería para las dimensiones de la matriz, pero ya puede hacerlo pasando el tipo de matriz deseado como argumento de tipo. Aparte de ese caso, ¿qué ganamos realmente al pasar una const como un argumento de tiempo de compilación en lugar de un argumento de tiempo de ejecución?

En el caso de los arreglos, simplemente pasar el tipo de arreglo (completo) como un argumento de tipo parece ser extremadamente limitante, porque el contrato no podría descomponer ni la dimensión del arreglo ni el tipo de elemento e imponerles restricciones. Por ejemplo, ¿podría un contrato que toma un "tipo de matriz completo" requerir el tipo de elemento del tipo de matriz para implementar ciertos métodos?

Pero su pedido de ejemplos más específicos de cómo serían útiles los parámetros genéricos que no son de tipo está bien recibido, por lo que amplié la publicación del blog para incluir una sección que cubre un par de clases significativas de ejemplos de casos de uso y algunos ejemplos específicos de cada uno. Ya que han pasado unos días, nuevamente la publicación del blog está aquí:

¿Solo los parámetros de tipo son lo suficientemente genéricos para los genéricos de Go 2?

La nueva sección se titula "Ejemplos de cómo son útiles los genéricos sobre los no tipos".

Como resumen rápido, los contratos para operaciones matriciales y vectoriales podrían imponer restricciones apropiadas tanto en la dimensionalidad como en los tipos de elementos de las matrices. Por ejemplo, la multiplicación de matrices de una matriz nxm con una matriz mxp, cada una representada como una matriz bidimensional, podría restringir correctamente el número de filas de la primera matriz para igualar el número de columnas de la segunda matriz, etc.

De manera más general, los genéricos podrían usar parámetros que no son de tipo para permitir la configuración y especialización en tiempo de compilación de código y algoritmos de muchas maneras. Por ejemplo, una variante genérica de math/big.Int podría configurarse en tiempo de compilación a un bit en particular con y/o firma, satisfaciendo las demandas de enteros de 128 bits y otros enteros de ancho fijo no nativos con una eficiencia razonable probablemente mucho mejor. que el big.Int existente donde todo es dinámico. Una variante genérica de big.Float podría especializarse de manera similar en tiempo de compilación a una precisión particular y/u otros parámetros de tiempo de compilación, por ejemplo, para proporcionar implementaciones genéricas razonablemente eficientes de los formatos binary16, binary128 y binary256 de IEEE 754-2008 que Go no admite de forma nativa. Muchos algoritmos de biblioteca que pueden optimizar su funcionamiento en función del conocimiento de las necesidades del usuario o aspectos particulares de los datos que se procesan, por ejemplo, optimizaciones de algoritmos gráficos que funcionan solo en pesos de borde no negativos o solo en DAG o árboles, u optimizaciones de procesamiento de matriz que confiar en que las matrices sean triangulares superior o inferior, o la aritmética de enteros grandes para la criptografía que a veces necesita implementarse en tiempo constante y, a veces, no ; podría usar genéricos para configurarse en tiempo de compilación para depender de información declarativa opcional como esto, al tiempo que garantiza que todas las pruebas de estas opciones de tiempo de compilación en la implementación generalmente se compilan a través de una propagación constante.

@bford escribió:

a saber, que los parámetros de los genéricos están vinculados a constantes en tiempo de compilación.

Este es el punto que no entiendo. Por qué requiere esta condición.
Teóricamente, uno podría redefinir variables/parámetros en el cuerpo. No importa.
Intuitivamente, asumo que le gustaría indicar que la primera aplicación de función debe ocurrir en tiempo de compilación.

Pero para este requisito, una palabra clave como comp o comptime sería más adecuada.
Además, si la gramática de golang solo permitiera dos tuplas de parámetros como máximo para una función, entonces esta anotación de palabra clave puede omitirse porque la primera tupla de parámetros de un tipo y de una función (en el caso de dos tuplas de parámetros) siempre se evaluará en tiempo de compilación.

Otro punto: ¿Qué pasa si const se amplía para permitir expresiones de tiempo de ejecución (inicio de sesión único verdadero)?

En los métodos de puntero vs valor :

Si un método aparece en un contrato con T simple en lugar de *T , entonces puede ser un método de puntero o un método de valor de T . Para evitar preocuparse por esta distinción, en un cuerpo de función genérico, todas las llamadas a métodos serán llamadas a métodos de puntero. ...

¿Cómo cuadra esto con la implementación de la interfaz? Si un T tiene algún método de puntero (como el MyInt en el ejemplo), se puede asignar T a la interfaz con ese método ( Stringer en el ejemplo)?

Permitirlo significa tener otra operación de dirección oculta & , no permitirlo significa que los contratos y las interfaces solo pueden interactuar a través de un cambio de tipo explícito. Ninguna solución me parece buena.

(Nota: debemos revisar esta decisión si genera confusión o un código incorrecto).

Veo que el equipo ya tiene algunas reservas sobre esta ambigüedad en la sintaxis del método de puntero. Solo agrego que la ambigüedad también afecta la implementación de la interfaz (e implícitamente también agrego mis reservas al respecto).

@fJavierZunzunegui Tiene razón, el texto actual sí implica que al asignar un valor de un parámetro de tipo a un tipo de interfaz, es posible que se requiera una operación de dirección implícita. Esa puede ser otra razón para no usar direcciones implícitas al invocar métodos. Tendremos que ver.

En tipos parametrizados , en particular con respecto a los parámetros de tipo incrustados como un campo en una estructura:

Considerar

type Lockable(type T) struct {
    T
    sync.Locker
}

¿Qué pasaría si T tuviera un método llamado Lock o Unlock ? La estructura no compilaría. Esto de no tener una condición de método X no es compatible con los contratos, por lo tanto, tenemos un código no válido que no rompe el contrato (anulando todo el propósito de los contratos).

Se vuelve aún más complicado si tiene varios parámetros incrustados (por ejemplo T1 y T2 ), ya que no deben tener métodos comunes (nuevamente, no se aplican mediante contratos). Además, admitir métodos arbitrarios según los tipos incrustados contribuye a restricciones de tiempo de compilación muy limitadas en los cambios de tipo para esas estructuras (muy similar a las aserciones y cambios de tipo ).

Por lo que veo hay 2 buenas alternativas:

  • no permitir la incrustación de parámetros de tipo por completo: simple, pero a un costo pequeño (si se necesita el método, uno debe escribirlo explícitamente en la estructura con el campo).
  • restringir los métodos invocables a los de contrato: de manera similar a la incrustación de una interfaz. Esto se desvía del go normal (un no objetivo) pero sin costo (los métodos no necesitan escribirse explícitamente en la estructura con el campo).

La estructura no compilaría.

Se compilaría. Intentalo. Lo que falla al compilar es una llamada al método ambiguo. Sin embargo, su punto sigue siendo válido.

Su segunda solución, que restringe los métodos invocables a los mencionados en el contrato, no funcionará: incluso si el contrato en T especificaba Lock y Unlock , aún no podría t llamarlos en un Lockable .

@jba gracias por las ideas sobre la compilación.

Por la segunda solución me refiero a tratar los parámetros de tipo incrustado como lo hacemos con las interfaces ahora, de modo que si el método no está en el contrato, no se puede acceder inmediatamente después de la incrustación. En este escenario, dado que T no tiene contrato, se trata efectivamente como interface{} , por lo que no entraría en conflicto con sync.Locker incluso si T se instanciara con un tipo con esos métodos. Esto podría ayudar a explicar mi punto .

De cualquier manera, prefiero la primera solución (prohibir la incrustación por completo), por lo que si esa es su preferencia, ¡no tiene sentido discutir la segunda! :smiley:

El ejemplo proporcionado por @JavierZunzunegui también cubre otro caso. ¿Qué pasa si T es una estructura que tiene un campo noCopy noCopy ? El compilador debería poder manejar ese caso también.

No estoy seguro de si este es exactamente el lugar correcto para esto, pero quería comentar con un caso de uso concreto del mundo real para tipos genéricos que permiten "parametrización en valores que no son de tipo, como constantes", y específicamente para el caso de matrices . Espero que esto sea útil.

En mi mundo sin genéricos, escribo mucho código que se ve así:

import "math/bits"

// SigEl is the element type used in variable length bit vectors, 
// can be any unsigned integer type
type SigEl = uint

// SigElBits is the number of bits storable in each SigEl
const SigElBits = 8 << uint((^SigEl(0)>>32&1)+(^SigEl(0)>>16&1)+(^SigEl(0)>>8&1))

// HammingDist counts the number bitwise differences between two
// bit vectors b1 and b2. I want this to be generic
// Function will panic at runtime if b1 and b2 aren't of equal length.
func HammingDist(b1, b2 []SigEl) (sum int) {
    // Give the compiler a hint so it won't need to bounds check the slices in loops
    _ = b1[len(b2)-1]  
        // This switch is optimized away because SigElBits is const
    switch SigElBits {   // Yay no golang generics!
    case 64:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount64(uint64(b1[x] ^ b2[x]))
        }
    case 32:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount32(uint32(b1[x] ^ b2[x]))
        }
    case 16:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount16(uint16(b1[x] ^ b2[x]))
        }
    case 8:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount8(uint8(b1[x] ^ b2[x]))
        }
    }
    return sum
}

Esto funciona bastante bien, con una arruga. A menudo necesito cientos de millones de []SigEl y su longitud suele ser de 128 a 384 bits en total. Debido a que los segmentos imponen una sobrecarga fija de 192 bits además del tamaño de la matriz subyacente, cuando la matriz en sí es de 384 bits o menos, esto impone una sobrecarga de memoria innecesaria del 50-150%, lo que obviamente es terrible.

Mi solución es asignar una porción de Sig _arrays_, y luego dividirlas sobre la marcha como los parámetros de HammingDist arriba:

const SigBits = 256  // Any multiple of SigElBits is valid

// Sig is the bit vector array type
type Sig [SigBits/SigElBits]SigEl

bitVects := make([]Sig, 100000000)
// stuff happens ... 

// Note slicing below, just to make the arrays "generic" for the call 
dist := HammingDist(bitVects[x][:], bitVects[y][:])

Lo que me _gustaría_ poder hacer en lugar de todo eso es definir un tipo de firma genérico y reescribir todo lo anterior como (algo así):

contract UnsignedInteger(T) {
    T uint, uint8, uint16, uint32, uint64
}

type Signature (type Element UnsignedInteger, n int) [n]Element

// HammingDist counts the number bitwise differences between two bit vectors
func HammingDist(b1, b2 *Signature) (sum int) {
    for x := range *b1 {
        // Assuming the std lib bits.OnesCount becomes generic over 
        // all UnsignedInteger types
        sum += bits.OnesCount(*b1[x] ^ *b2[x])
    }
    return sum
}

Entonces, para usar esta biblioteca:

type sigEl = uint   // Any unsigned int type
const sigElBits = 8 << uint((^SigEl(0)>>32&1)+(^SigEl(0)>>16&1)+(^SigEl(0)>>8&1))
const sigBits = 256  // Any multiple of SigElBits is valid
type sig Signature(sigEl, sigBits/sigElBits)

bitVects := make([]sig, 100000000)
// stuff happens ... 

dist := HammingDist(&bitVects[x], &bitVects[y])

Un ingeniero puede soñar... 🤖

Si sabe qué tan grande puede ser la longitud máxima de bits, puede usar algo como esto en su lugar:

contract uintArrayOfFixedLength(ElemType,ArrayType)
{
    ArrayType [1]ElemType,[2]ElemType,...,[maxBit]ElemType
    ElemType uint8,uint16,uint32,uint64
}

func HammingDist(type ElemType,ArrayType uintArrayOfFixedLength)(t1,t2 ArrayType) (sum int)
{

}

@vsivsi No estoy seguro de entender cómo cree que eso mejorará las cosas. ¿Quizás está suponiendo que el compilador generaría una versión instanciada de esa función para cada longitud de matriz posible? Debido a que ISTM a) eso no es muy probable, entonces b) terminaría con exactamente las mismas características de rendimiento que tiene ahora. La implementación más probable, en mi opinión, aún sería que el compilador pase la longitud y un puntero al primer elemento, por lo que aún pasaría una porción en el código generado (es decir, no pasaría la capacidad, pero no creo que una palabra adicional en la pila realmente importe).

Honestamente, en mi opinión, lo que está diciendo es un buen ejemplo del uso excesivo de genéricos, donde no son necesarios: "una matriz de longitud indeterminada" es exactamente para lo que son los segmentos.

@Merovius Gracias, creo que su comentario revela un par de puntos de discusión interesantes.

"una matriz de longitud indeterminada" es exactamente para lo que son las rebanadas.

Correcto, pero en mi ejemplo no hay matrices de longitud indeterminada. La longitud de la matriz es una constante conocida en _tiempo de compilación_. Esto es precisamente para lo que son las matrices, pero están infrautilizadas en golang IMO porque son muy inflexibles.

Para ser claro, no estoy sugiriendo

type Signature (type Element UnsignedInteger, n int) [n]Element

significa que n es una variable de tiempo de ejecución. Todavía debe ser una constante en el mismo sentido que hoy:

const n = 10
type nArray [n]uint               // works
type nSigInt Signature(uint, n)   // works 

var m = int(n)
type mArray [m]uint               // error
type mSigInt Signature(uint, m)   // error 

Así que veamos el "costo" de la función HammingDist basada en rebanadas. Estoy de acuerdo en que la diferencia entre pasar una matriz como bitVects[x][:] frente a &bitVects[x] es pequeña (-ish, un factor de 3 como máximo). La verdadera diferencia radica en el código y la verificación del tiempo de ejecución que debe realizarse dentro de esa función.

En la versión basada en segmentos, el código de tiempo de ejecución debe verificar los límites de los accesos a los segmentos para garantizar la seguridad de la memoria. Esto significa que esta versión del código puede entrar en pánico (o se necesita un mecanismo explícito de verificación y devolución de errores para evitarlo). Las asignaciones de NOP ( _ = b1[len(b2)-1] ) marcan una diferencia significativa en el rendimiento al darle al optimizador del compilador una pista de que no necesita verificar los límites de cada acceso de segmento en el bucle. Pero estas comprobaciones de límites mínimos siguen siendo necesarias, aunque las matrices subyacentes pasadas siempre tengan la misma longitud. Además, el compilador puede tener dificultades para optimizar de forma rentable el bucle for/range (por ejemplo, a través de unrolling ).

Por el contrario, la versión genérica basada en matrices de la función no puede entrar en pánico en tiempo de ejecución (no requiere manejo de errores) y evita la necesidad de cualquier lógica de verificación de límites condicionales. Dudo mucho que una versión genérica compilada de la función necesite "pasar" la longitud de la matriz como sugiere porque es literalmente un valor constante que forma parte del tipo instanciado en el momento de la compilación.

Además, para dimensiones de matriz pequeñas (importante en mi caso), sería fácil para el compilador desenrollar de manera rentable o incluso optimizar por completo el bucle for/range para obtener una ganancia de rendimiento decente, ya que sabrá en el momento de la compilación cuáles son esas dimensiones .

El otro gran beneficio de la versión genérica del código es que permite al usuario del módulo HammingDist determinar el tipo int sin firmar en su propio código. La versión no genérica requiere que se modifique el propio módulo para cambiar el tipo definido SigEl , ya que no hay forma de "pasar" un tipo a un módulo. Una consecuencia de esta diferencia es que la implementación de la función de distancia se vuelve más simple cuando no hay necesidad de escribir código separado para cada uno de los casos uint de {8,16,32,64} bits.

Los costos de la versión basada en segmentos de la función y la necesidad de modificar el código de la biblioteca para establecer el tipo de elemento son concesiones muy por debajo del nivel óptimo necesarias para evitar tener que implementar y mantener versiones "NxM" de esta función. El soporte genérico para tipos de matriz parametrizados (constantes) resolvería este problema:

// With generics + parameterized constant array lengths:
type Signature (type Element UnsignedInteger, n int) [n]Element
func HammingDist(b1, b2 *Signature) (sum int) { ... }

// Without generics
func HammingDistL1Uint(b1, b2 [1]uint) (sum int) { ... }
func HammingDistL1Uint8(b1, b2 [1]uint8) (sum int) { ... }
func HammingDistL1Uint16(b1, b2 [1]uint16) (sum int) { ... }
func HammingDistL1Uint32(b1, b2 [1]uint32) (sum int) { ... }
func HammingDistL1Uint64(b1, b2 [1]uint64) (sum int) { ... }

func HammingDistL2Uint(b1, b2 [2]uint) (sum int) { ... }
func HammingDistL2Uint8(b1, b2 [2]uint8) (sum int) { ... }
func HammingDistL2Uint16(b1, b2 [2]uint16) (sum int) { ... }
func HammingDistL2Uint32(b1, b2 [2]uint32) (sum int) { ... }
func HammingDistL2Uint64(b1, b2 [2]uint64) (sum int) { ... }

func HammingDistL3Uint(b1, b2 [3]uint) (sum int) { ... }
func HammingDistL3Uint8(b1, b2 [3]uint8) (sum int) { ... }
func HammingDistL3Uint16(b1, b2 [3]uint16) (sum int) { ... }
func HammingDistL3Uint32(b1, b2 [3]uint32) (sum int) { ... }
func HammingDistL3Uint64(b1, b2 [3]uint64) (sum int) { ... }

// and L4, L5, L6 ... ad nauseum

Evitar la pesadilla anterior, o los costos muy reales de las alternativas actuales, me parece lo _opuesto_ al "uso excesivo de genéricos". Estoy de acuerdo con @sighoya en que enumerar todas las longitudes de matriz permitidas en el contrato podría funcionar para un conjunto muy limitado de casos, pero creo que es demasiado limitado incluso para mi caso, ya que incluso si pongo el límite superior de soporte en un bajo 384 bits en total, eso requeriría casi 50 términos en la cláusula ArrayType [1]ElemType,[2]ElemType,...,[maxBit]ElemType del contrato para cubrir el caso uint8 .

Correcto, pero en mi ejemplo no hay matrices de longitud indeterminada. La longitud de la matriz es una constante conocida en tiempo de compilación.

Lo entiendo, pero tenga en cuenta que tampoco dije "en tiempo de ejecución". Desea escribir código que no tenga en cuenta la longitud de la matriz. Slices ya puede hacer eso.

Dudo mucho que una versión genérica compilada de la función necesite "pasar" la longitud de la matriz como sugiere porque es literalmente un valor constante que forma parte del tipo instanciado en el momento de la compilación.

Una versión genérica de la función lo haría, porque cada instancia de ese tipo usa una constante diferente. Es por eso que tengo la impresión de que está asumiendo que el código generado no será genérico, sino que se expandirá para cada tipo. es decir, parece suponer que se generarán varias instancias de esa función, para [1]Element , [2]Element , etc. Estoy diciendo que eso me parece poco probable, que parece más probable que habrá una versión generada, que es esencialmente equivalente a la versión rebanada.

Por supuesto que no tiene por qué ser así. Entonces, sí, tiene razón en que no necesita pasar la longitud de la matriz. Solo estoy prediciendo fuertemente que se implementaría de esa manera y parece una suposición cuestionable que no lo hará. (FWIW, también diría que si está dispuesto a que el compilador genere cuerpos de funciones especializados para longitudes separadas, también podría hacerlo de forma transparente para segmentos, pero esa es una discusión diferente).

El otro gran beneficio de la versión genérica del código

Para aclarar: con "la versión genérica", ¿se refiere a la idea general de los genéricos, tal como se implementa, por ejemplo, en el borrador de diseño de los contratos actuales, o se refiere más específicamente a los genéricos sin parámetros de tipo? Porque las ventajas que mencionas en este párrafo también se aplican al borrador de diseño de los contratos actuales.

No estoy tratando de hacer un caso en contra de los genéricos en general aquí. Solo estoy explicando por qué no creo que su ejemplo sirva para mostrar que necesitamos otros tipos de parámetros además de tipos.

// With generics + parameterized constant array lengths:
// Without generics

Esta es una dicotomía falsa (y tan obvia que estoy un poco frustrado contigo). También hay "con parámetros de tipo, pero sin parámetros enteros":

contract Unsigned(T) {
    T uint, uint8, uint16, uint32, uint64
}
func HammingDist(type T Unsigned) (b1, b2 []T) (sum int) {
    if len(b1) != len(b2) {
        panic("slices of different lengths passed to HammingDist")
    }
    for i := range b1 {
        sum += bits.OnesCount(b1[i]^b2[i]) // Same assumption about OnesCount being generic you made above
    }
    return sum
}

Lo cual me parece bien. Es un poco menos seguro para los tipos, ya que requiere un pánico en el tiempo de ejecución si los tipos no coinciden. Pero, y ese es mi punto, esa es la única ventaja de agregar parámetros genéricos que no son de tipo en su ejemplo (y es una ventaja que ya estaba clara, en mi opinión). Las ganancias de rendimiento que está prediciendo se basan en suposiciones bastante sólidas sobre cómo se implementan los genéricos en general y los genéricos sobre los parámetros que no son de tipo específicamente. Eso, personalmente, no lo considero muy probable según lo que he escuchado del equipo de Go hasta ahora.

Dudo mucho que una versión genérica compilada de la función necesite "pasar" la longitud de la matriz como sugiere porque es literalmente un valor constante que forma parte del tipo instanciado en el momento de la compilación.

Simplemente está asumiendo que los genéricos funcionarían como plantillas de C++ e implementaciones de funciones duplicadas, pero eso no es correcto. La propuesta permite explícitamente implementaciones únicas con parámetros ocultos.

Creo que si realmente necesita un código de plantilla para una pequeña cantidad de tipos numéricos, no es una gran carga usar un generador de código. Los genéricos realmente solo valen la pena por la complejidad del código para cosas como los tipos de contenedores, donde hay un beneficio de rendimiento medible al usar tipos primitivos, pero no puede esperar razonablemente generar solo una pequeña cantidad de plantillas de código por adelantado.

Obviamente, no tengo idea de cómo los mantenedores de golang implementarán algo en última instancia, por lo que me abstendré de especular más y felizmente me remito a aquellos con más conocimiento interno.

Lo que sí sé es que para el ejemplo del problema del mundo real que compartí anteriormente, la diferencia de rendimiento potencial entre la implementación actual basada en segmentos y una basada en matrices genéricas bien optimizada es sustancial.

BenchmarkHD/256-bit_unrolled_array_HD-20            2000000000           1.05 ns/op        0 B/op          0 allocs/op
BenchmarkHD/256-bit_slice_HD-20                     300000000            5.10 ns/op        0 B/op          0 allocs/op

Código en: https://github.com/vsivsi/hdtest

Esa es una diferencia de rendimiento potencial de 5 veces para el caso de 4x64 bits (un punto óptimo en mi trabajo) con solo un poco de desenrollado de bucle (y esencialmente sin código emitido adicional) en el caso de matriz. Estos cálculos están en los bucles internos de mis algoritmos, realizados literalmente muchos billones de veces, por lo que una diferencia de rendimiento de 5x es bastante grande. Pero para obtener estas ganancias de eficiencia hoy, necesito escribir cada versión de la función, para cada tipo de elemento y longitud de matriz necesarios.

Pero sí, si los mantenedores nunca implementan optimizaciones como estas, entonces todo el ejercicio de agregar longitudes de matriz parametrizadas a los genéricos no tendría sentido, al menos en lo que podría beneficiar este caso de ejemplo.

De todos modos, interesante discusión. Sé que estos son temas polémicos, ¡así que gracias por mantener la civilidad!

@vsivsi FWIW, las ganancias que está observando se desvanecen si no está desenrollando manualmente sus bucles (o si también está desenrollando el bucle sobre un segmento), por lo que esto todavía no respalda su argumento de que los parámetros enteros ayudan porque permiten el compilador para hacer el desenrollado por usted. Me parece una mala ciencia, argumentar X sobre Y, basado en que el compilador se vuelve arbitrariamente inteligente para X y permanece arbitrariamente tonto para Y. No me queda claro por qué se desencadenaría una heurística de desenrollado diferente en el caso de recorrer una matriz. , pero no se dispara en el caso de recorrer un segmento con una longitud conocida en tiempo de compilación. No está mostrando los beneficios de un cierto tipo de genéricos sobre otro, está mostrando los beneficios de esa heurística de desarrollo diferente.

Pero en cualquier caso, nadie realmente argumentó que generar código especializado para cada instanciación de una función genérica no sería potencialmente más rápido , solo que hay otras compensaciones a considerar al decidir si desea hacer eso.

@Merovius Creo que el caso más sólido para los genéricos en este tipo de ejemplo es con la elaboración en tiempo de compilación (por lo tanto, emitiendo una función única para cada entero de nivel de tipo) donde el código que se especializará está en una biblioteca. Si el usuario de la biblioteca va a utilizar un número limitado de instancias de la función, obtendrá la ventaja de una versión optimizada. Entonces, si mi código solo usa matrices de longitud 64, puedo usar elaboraciones optimizadas de las funciones de la biblioteca para la longitud 64.

En este caso específico, depende de la distribución de frecuencia de las longitudes de la matriz, porque es posible que no queramos elaborar todas las funciones posibles si hay miles de ellas debido a las limitaciones de memoria y la eliminación de la memoria caché de la página, lo que podría hacer que las cosas sean más lentas. Si, por ejemplo, los tamaños pequeños son comunes, pero son posibles los más grandes (una distribución de tamaño de cola larga), entonces podemos elaborar funciones especializadas para los números enteros pequeños con bucles desenrollados (digamos 1 a 64) y luego proporcionar una única versión generalizada con un oculto -parámetro para el resto.

No me gusta la idea del "compilador arbitrariamente inteligente" y creo que es un mal argumento. ¿Cuánto tiempo tendré que esperar por este compilador arbitrariamente inteligente? En particular, no me gusta la idea de que el compilador cambie los tipos, por ejemplo, optimizando un segmento a un Array haciendo especializaciones ocultas en un lenguaje con reflexión, ya que cuando reflexionas sobre ese segmento, podría suceder algo inesperado.

Con respecto al "dilema genérico", personalmente iría con "hacer que el compilador sea más lento/hacer más trabajo", pero intente hacerlo lo más rápido posible usando una buena implementación y una compilación separada. Rust parece hacerlo bastante bien, y después del reciente anuncio de Intel, parece que eventualmente podría reemplazar a 'C' como el principal lenguaje de programación de sistemas. El tiempo de compilación no pareció ser siquiera un factor en la decisión de Intel, ya que la memoria de tiempo de ejecución y la seguridad de concurrencia con una velocidad similar a 'C' parecían ser los factores clave. Los "rasgos" de Rust son una implementación razonable de clases de tipos genéricas, tienen algunos casos de esquina molestos que creo que provienen de su diseño de sistema de tipos.

Volviendo a nuestra discusión anterior, debo tener cuidado de separar la discusión sobre los genéricos en general y cómo podrían aplicarse específicamente a Go. Como tal, no estoy seguro de que Go deba tener genéricos, ya que complica lo que es un lenguaje simple y elegante, de la misma manera que 'C' no tiene genéricos. Sigo pensando que hay una brecha en el mercado para un lenguaje que tiene implementaciones genéricas como característica principal, pero que sigue siendo simple y elegante.

Me pregunto si ha habido algún progreso en esto.

Cuánto tiempo puedo probar los genéricos. he estado esperando por mucho tiempo

@Nsgj Puede consultar esta CL: https://go-review.googlesource.com/c/go/+/187317/

En la especificación actual, ¿es esto posible?

contract Point(T) {
  T struct { X, Y float64 }
}

En otras palabras, el tipo debe ser una estructura con dos campos, X e Y, de tipo float64.

editar: con uso de ejemplo

func generate(type T Point)() T {
  return T{X: randomFloat64(), Y: randomFloat64()}
}

@ abuchanan-nr Sí, el borrador de diseño actual lo permitiría, aunque es difícil ver cómo sería útil.

Tampoco estoy seguro de que sea útil, pero no vi un ejemplo claro del uso de un tipo de estructura personalizado en una lista de tipos de un contrato. La mayoría de los ejemplos usan tipos incorporados.

FWIW, estaba imaginando una biblioteca de gráficos 2D. Es posible que desee que cada vértice tenga una cantidad de campos específicos de la aplicación, como color, fuerza, etc. Pero también es posible que desee una biblioteca genérica de métodos y algoritmos solo para la parte de la geometría, que realmente solo se basa en las coordenadas X, Y. Sería bueno pasar su tipo de vértice personalizado a esta biblioteca, por ejemplo

type MyVertex struct {
  X, Y float64
  Color color.Color
  OtherAttr int
}
p := geo.RandomPolygon(MyVertex)()

for _, vert := range p.Vertices() {
  p.Color = randColor()
}

Nuevamente, no estoy seguro de que resulte ser un buen diseño en la práctica, pero es donde estaba mi imaginación en ese momento :)

Consulte https://godoc.org/image#Image para ver cómo se hace esto en Go estándar hoy.

Con respecto a los Operadores/Tipos en los contratos :

Esto da como resultado una duplicación de muchos métodos genéricos, ya que los necesitaríamos en formato de operador ( + , == , < , ...) y formato de método ( Plus(T) T , Equal(T) bool , LessThan(T) bool , ...).

Propongo que unifiquemos estos dos enfoques en uno, el formato del método. Para lograr eso, los tipos predeclarados ( int , int64 , string , ...) deberían convertirse en tipos con métodos arbitrarios. Para el caso simple (trivial) que ya es posible ( type MyInt int; func (i MyInt) LessThan(o MyInt) bool {return int(i) < int(o)} ), pero el valor real radica en los tipos compuestos ( []int -> []MyInt , map[int]struct{} -> map[MyInt]struct{} , y así sucesivamente para canal, puntero, ...), lo cual no está permitido (ver Preguntas Frecuentes ). Permitir estas conversiones es un cambio significativo en sí mismo, por lo que he ampliado los tecnicismos en la Propuesta de conversión de tipo relajado . Eso permitiría que las funciones genéricas no traten con operadores y aún admitan todos los tipos, incluidos los predeclarados.

Tenga en cuenta que este cambio también beneficia a los tipos no predeclarados. Según la propuesta actual, dado type X struct{S string} (que proviene de una biblioteca externa, por lo que no puede agregarle métodos), digamos que tiene un []X y desea pasarlo a una función genérica esperando []T , por T satisfaciendo el Stringer . Eso requeriría un type X2 X; func(x X2) String() string {return x.S} y una copia profunda de []X en []X2 . Bajo estos cambios propuestos a esta propuesta, guarde la copia profunda por completo.

NOTA: la propuesta de conversión de tipo relajado mencionada requiere desafío.

@JavierZunzunegui Proporcionar un "formato de método" (o formato de operador) para operadores unarios/binarios básicos no es el problema. Es bastante sencillo introducir métodos como +(x int) int simplemente permitiendo símbolos de operadores como nombres de métodos y extenderlos a tipos integrados (aunque incluso esto se desglosa por turnos ya que el operador de la derecha puede ser un tipo entero arbitrario - no tenemos una manera de expresar esto en este momento). El problema es que eso no es suficiente. Una de las cosas que un contrato debe expresar es si un valor x de tipo X se puede convertir al tipo de un parámetro de tipo T como en T(x) (y viceversa). Es decir, se necesita inventar un "formato de método" para las conversiones permisibles. Además, debe haber una manera de expresar que una constante sin tipo c se puede asignar a (o convertir a) una variable de tipo parámetro tipo T : ¿es legal asignar, digamos, 256 a t de tipo T ? ¿Qué pasa si T es byte ? Hay algunas cosas más como esta. Uno puede inventar la notación de "formato de método" para estas cosas, pero se complica rápidamente y no está claro si es más comprensible o legible.

No digo que no se pueda hacer, pero no hemos encontrado un enfoque satisfactorio y claro. El borrador de diseño actual que simplemente enumera los tipos, por otro lado, es bastante sencillo de entender.

@griesemer Esto puede ser difícil en Go debido a otras prioridades, pero en general es un problema bastante bien resuelto. Es una de las razones por las que veo las conversiones implícitas como malas. Hay otras razones, como que ocurre magia que no es visible para alguien que lee el código.

Si no hay conversiones implícitas en el sistema de tipos, entonces puedo usar la sobrecarga para controlar con precisión el rango de tipos aceptados, y las interfaces controlan la sobrecarga.

Tendería a expresar la similitud entre los tipos usando interfaces, por lo tanto, las operaciones como '+' se expresarían genéricamente como operaciones en una interfaz numérica en lugar de un tipo. Necesita tener variables de tipo así como interfaces para expresar la restricción de que tanto los argumentos como el resultado de la suma deben ser del mismo tipo.

Entonces, aquí se declara que el operador de suma opera sobre tipos con una interfaz numérica. Esto se relaciona muy bien con las matemáticas, donde los 'enteros' y la 'suma' forman un "grupo", por ejemplo.

Terminarías con algo como:

+(T Addable)(x T, y T) T

Si permite la selección de interfaz implícita, entonces el operador '+' puede ser solo un método de la interfaz numérica, pero creo que eso causaría problemas con la selección de método en Go.

@griesemer sobre su punto sobre las conversiones:

Una de las cosas que un contrato debe expresar es si un valor x de tipo X se puede convertir al tipo de un parámetro de tipo T como en T(x) (y viceversa). Es decir, uno necesita inventar un "formato de método" para conversiones permisibles

Puedo ver cómo eso sería una complicación, pero no creo que sea necesario. De la forma en que lo veo, tales conversiones ocurrirían fuera del código genérico, por parte de la persona que llama. Un ejemplo (usando Stringify según el borrador del diseño):

Stringify(int)([]int{1,2}) // does not compile
type MyInt int
func (i MyInt) String() string {...}
Stringify(MyInt)([]MyInt([]int{1,2})) // OK. Generic type MyInt could be inferred

Arriba, en lo que respecta a Stringify , el argumento es tipo []MyInt y cumple con el contrato. El código genérico no puede convertir tipos genéricos en nada más (aparte de las interfaces que implementan, según el contrato), precisamente porque su contrato no establece nada al respecto.

@JavierZunzunegui No veo cómo la persona que llama puede hacer tales conversiones sin exponerlas en la interfaz/contrato. Por ejemplo, podría querer implementar un algoritmo numérico genérico (una función parametrizada) que opere en varios tipos de números enteros o de coma flotante. Como parte de ese algoritmo, el código de función necesita asignar valores constantes c1 , c2 , etc. a valores del tipo de parámetro T . No veo cómo el código puede hacer esto sin saber que está bien asignar estas constantes a una variable de tipo T . (Uno ciertamente no querría tener que pasar esas constantes a la función).

func NumericAlgorithm(type T SomeContract)(vector []T) T {
   ...
   vector[i] = 3.1415  // <<< how do we know this is valid without the contract telling us?
   ...
}

necesita asignar valores constantes c1 , c2 , etc. a los valores del tipo de parámetro T

@griesemer Diría (desde mi punto de vista de cómo son/deberían ser los genéricos) que lo anterior es la declaración incorrecta del problema. Necesita que T se defina como float32 , pero un contrato solo establece qué métodos están disponibles para T , no cómo se define. Si necesita esto, puede mantener vector como []T y requerir un argumento func(float32) T ( vector[i] = f(c1) ), o mucho mejor mantener vector como []float32 y requieren T por contrato para tener un método DoSomething(float32) o DoSomething([]float32) , ya que asumo que T y el los flotadores deben interactuar en algún punto. Eso significa que T puede o no estar definido como type T float32 , todo lo que podemos decir es que tiene los métodos requeridos por el contrato.

@JavierZunzunegui No digo en absoluto que T se defina como float32 - podría ser float32 , float64 , o incluso uno de los tipos complejos. Más generalmente, si la constante fuera un número entero, podría haber una variedad de tipos de números enteros que serían válidos para pasar a esta función, y algunos que no lo son. Ciertamente no es una "declaración de problema incorrecta". El problema es real, ciertamente no es artificial en absoluto querer poder escribir tales funciones, y el problema no desaparece al declararlo "incorrecto".

@griesemer Ya veo, pensé que solo te preocupaba la conversión, no registré el elemento clave que trata con constantes sin tipo.

Puede hacerlo según mi respuesta anterior, con T con un método DoSomething(X) , y la función tomando un argumento adicional func(float64) X , por lo que la forma genérica se define por dos tipos ( T,X ). La forma en que describe el problema X es normalmente float32 o float64 y el argumento de la función es func(f float64) float32 {return float32(f)} o func(f float64) float64 {return f} .

Más significativamente, como usted destaca, para el caso de enteros existe el problema de que los formatos de enteros menos precisos pueden no ser suficientes para una constante determinada. El enfoque más seguro se convierte en mantener privada la función genérica de dos tipos ( T,X ) y exponer públicamente solo MyFunc32 / MyFunc64 /etc.

Concederé que MyFunc32(int32) / MyFunc64(int64) /etc. es menos práctico que un solo MyFunc(type T Numeric) (¡lo contrario es indefendible!). Pero esto es solo para implementaciones genéricas que se basan en una constante, y principalmente una constante entera: ¿cuántas de ellas hay? Por lo demás, obtiene la libertad adicional de no estar restringido a unos pocos tipos integrados por T .

Y, por supuesto, si la función no es costosa, podría estar perfectamente bien haciendo el cálculo como int64 / float64 y exponiéndolo solo, manteniéndolo simple y sin restricciones en T .

Realmente no podemos decirle a la gente "puede escribir funciones genéricas en cualquier tipo T, pero esas funciones genéricas no pueden usar constantes sin tipo". Go es ante todo un lenguaje sencillo. Los idiomas con restricciones extrañas como esa no son simples.

Cada vez que un enfoque propuesto para los genéricos se vuelve difícil de explicar de una manera simple, debemos descartar ese enfoque. Es más importante mantener el lenguaje simple que agregar genéricos al lenguaje.

@JavierZunzunegui Una de las propiedades interesantes del código parametrizado (genérico) es que el compilador puede personalizarlo según los tipos con los que se instancia el código. Por ejemplo, uno podría querer usar un tipo byte en lugar de int porque genera un ahorro de espacio significativo (imagine una función que asigna grandes porciones del tipo genérico). Así que simplemente restringir el código a un tipo "suficientemente grande" es una respuesta insatisfactoria, incluso para un lenguaje "obstinado" como Go.

Además, no se trata solo de algoritmos que usan constantes "grandes" sin tipo que pueden no ser tan comunes: descartar dichos algoritmos con una pregunta "¿cuántos de esos hay de todos modos?" es simplemente agitar la mano para desviar un problema que existe. Solo para su consideración: parece razonable que una gran cantidad de algoritmos usen constantes enteras como -1, 0, 1. Tenga en cuenta que uno no podría usar -1 junto con enteros sin tipo, solo para darle un ejemplo simple. Claramente no podemos simplemente ignorar eso. Necesitamos poder especificar esto en un contrato.

@ianlancetaylor @griesemer gracias por los comentarios. Puedo ver que hay un conflicto significativo en mi cambio propuesto con constantes sin tipo y números enteros negativos, lo dejaré atrás.

¿Puedo llamar su atención sobre el segundo punto en https://github.com/golang/go/issues/15292#issuecomment -546313279:

Tenga en cuenta que este cambio también beneficia a los tipos no predeclarados. Según la propuesta actual, dado el tipo X struct{S string} (que proviene de una biblioteca externa, por lo que no puede agregarle métodos), digamos que tiene una []X y desea pasarla a una función genérica que espera [ ]T, para que T satisfaga el contrato de Stringer. Eso requeriría un tipo X2 X; func(x X2) String() string {return xS}, y una copia profunda de []X en []X2. Bajo estos cambios propuestos a esta propuesta, guarde la copia profunda por completo.

La relajación de las normas de conversión (si es técnicamente factible) seguiría siendo útil.

@JavierZunzunegui Discutir conversiones del tipo []B([]A) si se permite B(a) (con a de tipo A ) parece ser en su mayoría ortogonal a las características genéricas. Creo que no necesitamos traer esto aquí.

@ianlancetaylor No estoy seguro de cuán relevante es esto para Go, pero no creo que las constantes realmente no tengan tipo, deben tener un tipo ya que el compilador debe elegir una representación de máquina. Creo que un mejor término es constantes de tipo indeterminado, ya que la constante puede representarse por varios tipos diferentes. Una solución es usar un tipo de unión, por lo que una constante como 27 tendría un tipo como int16|int32|float16|float32 una unión de todos los tipos posibles. Entonces T en un tipo genérico puede ser este tipo de unión. El único requisito es que en algún momento debemos resolver la unión a un solo tipo. El caso más problemático sería algo así como print(27) porque nunca hay un solo tipo para resolver, en tales casos, cualquier tipo en la unión funcionaría, y podríamos elegir en función de un parámetro de optimización como espacio/velocidad, etc. .

@keean El nombre exacto y el manejo de lo que la especificación llama "constantes sin tipo" está fuera de tema en este tema. Por favor, llevemos esa discusión a otra parte. Gracias.

@ianlancetaylor Estoy feliz de hacerlo, sin embargo, esta es una de las razones por las que creo que Go no puede tener una implementación genérica limpia/simple, todos estos problemas están interconectados y las elecciones originales hechas para Go no se tomaron con la programación genérica en mente. Creo que se necesita otro lenguaje, diseñado para hacer que los genéricos sean simples por diseño, para Go, los genéricos siempre serán algo agregado al lenguaje más adelante, y la mejor opción para mantener el lenguaje limpio y simple puede ser no tenerlos en absoluto.

Si hoy diseñara un lenguaje simple con tiempos de compilación rápidos y una flexibilidad comparable, elegiría la sobrecarga de métodos y el polimorfismo estructural (subtipado) a través de interfaces golang y no genéricos. De hecho, permitiría sobrecargar diferentes interfaces anónimas con diferentes campos.

La elección de genéricos tiene la ventaja de la reutilización del código limpio, pero introduce más ruido, lo que se complica si se agregan restricciones que a veces conducen a un código difícilmente comprensible.
Entonces, si tenemos genéricos, ¿por qué no usar un sistema de restricción avanzado como una cláusula where, tipos de tipos más altos o tal vez tipos de rangos más altos y también tipos dependientes?
Todas estas preguntas eventualmente surgirán si se adoptan genéricos, tarde o temprano.

Dicho claramente, no estoy en contra de los genéricos, pero estoy contemplando si es el camino a seguir para conservar la simplicidad de go.

Si la introducción de genéricos en go es inevitable, entonces sería razonable reflexionar sobre el impacto en los tiempos de compilación al monomorfizar funciones genéricas.
¿No sería un buen valor predeterminado encuadrar genéricos, es decir, generar una copia para todos los tipos de entrada juntos, y solo especializarse si el usuario lo solicita explícitamente con alguna anotación en la definición o en el sitio de llamada?

Con respecto al impacto en el rendimiento del tiempo de ejecución, esto reduciría el rendimiento debido al problema de boxing/unboxing; de lo contrario, hay ingenieros de C++ de nivel experto que recomiendan genéricos de box como lo hace Java para mitigar las fallas de caché.

@ianlancetaylor @griesemer He reconsiderado el problema de las constantes sin tipo y los genéricos 'no operadores' (https://github.com/golang/go/issues/15292#issuecomment-547166519) y he encontrado una mejor manera de tratar con eso.

Dé los tipos numéricos ( type MyInt32 int32 , type MyInt64 int64 , ...), estos tienen muchos métodos que satisfacen el mismo contrato ( Add(T) T , ...) pero críticamente no otros que correría el riesgo de desbordarse func(MyInt64) FromI64(int64) MyInt64 pero no ~ func(MyInt32) FromI64(int64) MyInt32 ~. Esto permite usar constantes numéricas (asignadas explícitamente al valor de precisión más bajo que requieren) de forma segura (1) ya que los tipos numéricos de baja precisión no cumplirán el contrato requerido, pero todos los de mayor precisión sí lo harán. Ver playground , usando interfaces en lugar de genéricos.

Una ventaja de relajar los genéricos numéricos más allá de los tipos incorporados (no es específico de esta última revisión, por lo que debería haberlo compartido la semana pasada) es que permite crear instancias de métodos genéricos con tipos de verificación de desbordamiento; consulte playground . La verificación de desbordamiento es en sí misma una solicitud/propuesta muy popular (https://github.com/golang/go/issues/31500 y problemas relacionados).


(1) : la garantía de tiempo de compilación sin desbordamiento para constantes sin tipo es sólida dentro de la misma 'rama' ( int[8/16/32/64] y uint[8/16/32/64] ). Cruzando ramas, una constante uint[X] solo se instancia de forma segura en int[2X+] y una constante int[X] no se puede instanciar de forma segura en ningún uint[X] . Incluso relajarlos (permitiendo int[X]<->uint[X] ) sería simple y seguro siguiendo algunos estándares mínimos, y críticamente cualquier complejidad recae en el escritor del código genérico, no en el usuario del genérico (que solo se preocupa por el contrato , y puede esperar que cualquier tipo numérico que lo cumpla sea válido).

Métodos genéricos: ¡fue la caída de Java!

@ianlancetaylor Estoy feliz de hacerlo, sin embargo, esta es una de las razones por las que creo que Go no puede tener una implementación genérica limpia/simple, todos estos problemas están interconectados y las elecciones originales hechas para Go no se tomaron con la programación genérica en mente. Creo que se necesita otro lenguaje, diseñado para hacer que los genéricos sean simples por diseño, para Go, los genéricos siempre serán algo agregado al lenguaje más adelante, y la mejor opción para mantener el lenguaje limpio y simple puede ser no tenerlos en absoluto.

Estoy de acuerdo 100%. Aunque me encantaría ver algún tipo de genérico implementado, creo que lo que están cocinando actualmente destruirá la simplicidad del lenguaje Go.

La idea actual de extender las interfaces se ve así:

type I1(type P1) interface {
        m1(x P1)
}

type I2(type P1, P2) interface {
        m2(x P1) P2
        type int, float64
}

func f(type P1 I1(P1), P2 I2(P1, P2)) (x P1, y P2) P2

Lo siento todos, ¡pero por favor no hagan esto! Afea la belleza de Go big time.

Habiendo escrito casi 100 000 líneas de código Go ahora, estoy de acuerdo con no tener genéricos.

Sin embargo, pequeñas cosas como apoyar

// Allow mulitple types in Slices and Maps declarations
func Reverse(s []<int,string>) {
    first := 0
    last := len(s) - 1
    for first < last {
        s[first], s[last] = s[last], s[first]
        first++
        last--
    }
}

//  Allow multiple types in variable declarations
func Index (s <string, []byte>, b byte) int {
    for i := 0; i < len(s); i++ {
        if s[i] == b {
            return i
        }
    }
    return -1
}

// Allow slices and maps declarations with interface values
func ToStrings (s []Stringer) []string {
    r := make([]string, len(s))
    for i, v := range s {
        r[i] = v.String()
    }
    return r
}

ayudaría.

Propuesta de sintaxis para poder separar completamente los genéricos del código Go normal

package graph

// Example how you would define generics completely separat from Go 1 code
contract (Node, Edge)G {
    Node Edges() []Edge
    Edge Nodes() (from, to Node)
}

type (type Node, Edge G) ( Graph )
func (type Node, Edge G) ( New )
const _ = (Node, Edge) Graph

// Unmodified Go 1 code
type Graph struct { ... }
func New(nodes []Node) *Graph { ... }
func (g *Graph) ShortestPath(from, to Node) []Edge { ... }

@martinrode Sin embargo, pequeñas cosas como apoyar
... permitir múltiples tipos en declaraciones de Slices y Maps

Esto no responde a las necesidades de algunas funciones de división genéricas funcionales, por ejemplo head() , tail() , map(slice, func) , filter(slice, func)

Puede escribirlo usted mismo para cada proyecto en el que lo necesite, pero en ese momento existe el riesgo de quedarse obsoleto debido a la repetición de copiar y pegar y fomenta la complejidad del código Go para salvar la simplicidad del lenguaje.

(A nivel personal, también es un poco agotador saber que tengo un conjunto de características que quiero implementar y no tener una forma clara de expresarlas sin responder también a las limitaciones del idioma)

Considere lo siguiente en go actual, no genérico:

Tengo una variable x de tipo externallib.Foo , obtenida de una biblioteca externallib que no controlo.
Quiero pasarlo a una función SomeFunc(fmt.Stringer) , pero externallib.Foo no tiene ningún método String() string . Simplemente puedo hacer:

type MyFoo externallib.Foo
func (mf MyFoo) String() string {...}
// ...
SomeFunc(MyFoo(x))

Considere lo mismo con los genéricos.

Tengo una variable x de tipo []externallib.Foo . Quiero pasarlo a AnotherFunc(type T Stringer)(s []T) . No se puede hacer sin una costosa copia profunda del segmento en un nuevo []MyFoo . Si en lugar de un corte fuera de un tipo más complejo (por ejemplo, un chan o un mapa), o el método modificara el receptor, se vuelve aún más ineficiente y tedioso, si cabe.

Esto puede no ser un problema dentro de la biblioteca estándar, pero eso es solo porque no tiene dependencias externas. Ese es un lujo que prácticamente ningún otro proyecto tendrá.

Mi sugerencia es relajar la conversión para permitir []Foo([]Bar{}) para cualquier Foo definido como type Foo Bar , o viceversa, e igualmente para mapas, matrices, canales y punteros, recursivamente. Tenga en cuenta que estas son todas copias superficiales baratas. Más detalles técnicos en Propuesta de conversión de tipo relajado .


Esto se mencionó por primera vez como una función secundaria en https://github.com/golang/go/issues/15292#issuecomment -546313279.

@JavierZunzunegui No creo que eso tenga nada que ver con los genéricos. Sí, puede proporcionar un ejemplo con genéricos, pero puede proporcionar un ejemplo similar sin utilizar genéricos. Creo que ese tema debe discutirse por separado, no aquí. Consulte también https://golang.org/doc/faq#convert_slice_with_same_underlying_type. Gracias.

Sin genéricos, dicha conversión casi no tiene ningún valor, porque en general []Foo no encontrará ninguna interfaz, o al menos ninguna interfaz que haga uso de ella como una porción. La excepción son las interfaces que tienen un patrón muy específico para hacer uso de él, como sort.Interface , para el cual no necesita convertir el segmento de todos modos.

La versión no genérica de lo anterior ( func AnotherFunc(type T Stringer)(s []T) ) es

type SliceOfStringers interface {
  Len() int
  Get(int) fmt.Stringer
}
func AnotherFunc(s SliceOfStringers) {...}

Puede que sea menos práctico que el enfoque genérico, pero se puede hacer para manejar bien cualquier segmento y hacerlo sin copiarlo, independientemente de que el tipo subyacente sea realmente un fmt.Stringer . Tal como está, los genéricos no pueden, a pesar de que en principio son una herramienta mucho más adecuada para el trabajo. Y seguramente, si agregamos genéricos, es precisamente para hacer que las rebanadas, los mapas, etc. sean más comunes en las API y manipularlos con menos repeticiones. Sin embargo, introducen un nuevo problema, sin equivalencia en un mundo de solo interfaz, que _puede que ni siquiera sea inevitable sino artificialmente impuesto por el lenguaje.

La conversión de tipo que menciona aparece con la suficiente frecuencia en un código no genérico que es una pregunta frecuente. Por favor, traslademos esta discusión a otro lugar. Gracias.

¿Cuál es el estado de esto? ¿Algún borrador ACTUALIZADO? Estoy esperando genéricos desde
hace casi 2 años. ¿Cuándo tendremos genéricos?

El mar., 4 de feb. de 2020 a la(s) 13:28, Ian Lance Taylor (
[email protected]) escribió:

La conversión de tipo que menciona aparece con bastante frecuencia en código no genérico
que es un FAQ. Por favor, traslademos esta discusión a otro lugar. Gracias.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/15292?email_source=notifications&email_token=AJMFNBN3MFHDMENAFXIKBLDRBGXUTA5CNFSM4CA35RX2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEKYV5RI#issuecomment-5820,49477
o darse de baja
https://github.com/notifications/unsubscribe-auth/AJMFNBO5UTKNPL3MSA3NESLRBGXUTANCNFSM4CA35RXQ
.

--
Esta es una prueba para que las firmas de correo se utilicen en TripleMint

Estamos trabajando en ello. Algunas cosas llevan tiempo.

¿El trabajo se realiza fuera de línea? Me encantaría verlo evolucionar con el tiempo, de una manera que el "público en general" como yo no pueda comentar para evitar el ruido.

Aunque desde entonces se cerró para mantener la discusión sobre los genéricos en un solo lugar, consulte #36177 donde @Griesemer se vincula a un prototipo en el que está trabajando y hace algunos comentarios interesantes sobre sus pensamientos sobre el asunto hasta el momento.

Creo que tengo razón al decir que el prototipo solo se ocupa de los aspectos de verificación de tipo del borrador de la propuesta de 'contratos' en este momento, pero el trabajo ciertamente me parece prometedor.

@ianlancetaylor Cada vez que un enfoque propuesto para los genéricos se vuelve difícil de explicar de una manera simple, debemos descartar ese enfoque. Es más importante mantener el lenguaje simple que agregar genéricos al lenguaje.

Ese es un gran ideal por el que luchar, pero en realidad el desarrollo de software a veces no es inherentemente _simple de explicar_.

Cuando el lenguaje se limita a expresar tales ideas _no fáciles de expresar_ los ingenieros de software terminan reinventando esas facilidades una y otra vez, porque estas malditas ideas _difíciles de expresar_ son a veces esenciales para la lógica de los programas.

Mire Istio, Kubernetes, operator-sdk y, hasta cierto punto, Terraform, e incluso la biblioteca protobuf. Todos escapan del sistema de tipos de Go usando la reflexión, implementando un nuevo sistema de tipos encima de Go usando interfaces y generación de código, o una combinación de estos.

@omeid

Mira Istio, Kubernetes

¿Alguna vez se te ocurrió que la razón por la que están haciendo estas cosas absurdas es porque su diseño central no tiene ningún sentido y, como resultado, han tenido que resultar en los juegos de reflect para cumplirlo? ?

Sostengo que mejores diseños para programas golang (tanto en la fase de diseño como en la API) no _requieren_ genéricos.

Por favor, no los agregue a golang.

La programación es difícil. Kubelet es un lugar oscuro. Los genéricos dividen a la gente más que la política estadounidense. Quiero creer.

Cuando el lenguaje se limita a expresar ideas tan difíciles de expresar, los ingenieros de software terminan reinventando esas facilidades una y otra vez, porque estas ideas condenadamente difíciles de expresar son a veces esenciales para la lógica de los programas.

Mire Istio, Kubernetes, operator-sdk y, hasta cierto punto, Terraform, e incluso la biblioteca protobuf. Todos escapan del sistema de tipos de Go usando la reflexión, implementando un nuevo sistema de tipos encima de Go usando interfaces y generación de código, o una combinación de estos.

No me parece que sea un argumento persuasivo. Idealmente, el lenguaje Go debería ser fácil de leer, escribir y comprender, al mismo tiempo que permite realizar operaciones arbitrariamente complejas. Eso es consistente con lo que dices: las herramientas que mencionas necesitan hacer algo complejo, y Go les brinda una manera de hacerlo.

Idealmente, el lenguaje Go debería ser fácil de leer, escribir y comprender, al mismo tiempo que permite realizar operaciones arbitrariamente complejas.

Estoy de acuerdo con esto, pero debido a que esos son objetivos múltiples, a veces estarán en tensión entre sí. El código que naturalmente "quiere" estar escrito en un estilo genérico a menudo se vuelve menos fácil de leer de lo que sería cuando tiene que recurrir a técnicas como la reflexión.

El código que naturalmente "quiere" estar escrito en un estilo genérico a menudo se vuelve menos fácil de leer de lo que sería cuando tiene que recurrir a técnicas como la reflexión.

Es por eso que esta propuesta permanece abierta y por eso tenemos un borrador de diseño para una posible implementación de genéricos (https://blog.golang.org/why-generics).

Mira ... incluso la biblioteca protobuf. Todos escapan del sistema de tipos de Go usando la reflexión, implementando un nuevo sistema de tipos encima de Go usando interfaces y generación de código, o una combinación de estos.

Hablando de la experiencia con protobufs, hay algunos casos en los que los genéricos pueden mejorar la usabilidad y/o la implementación de la API, pero la gran mayoría de la lógica no se beneficiará de los genéricos. Generics supone que la información de tipo concreto se conoce en tiempo de compilación . Para protobufs, la mayoría de las situaciones involucran casos en los que la información de tipo se conoce solo en tiempo de ejecución .

En general, noto que las personas a menudo señalan cualquier uso de la reflexión y lo afirman como evidencia de la necesidad de medicamentos genéricos. No es tan simple. Una distinción crucial es si el tipo de información se conoce en tiempo de compilación o no. En varios casos, fundamentalmente no lo es.

@dsnet Interesante, gracias, nunca pensé que protobuf no fuera compatible con los genéricos. Siempre asumí que cada herramienta que genera código repetitivo, como por ejemplo protoc, basado en un esquema predefinido, sería capaz de generar código genérico sin reflexión utilizando la propuesta genérica actual. ¿Le importaría actualizar esto en la especificación con un ejemplo o en una nueva publicación de blog donde describa este problema con más detalle?

las herramientas que mencionas necesitan hacer algo complejo, y Go les da una forma de hacerlo.

El uso de plantillas de texto para generar código Go no es una instalación por diseño, diría que es una curita ad-hoc, idealmente, al menos los paquetes estándar ast y parser deberían permitir generar código Go arbitrario.

Lo único que puede argumentar que Go le da a uno para lidiar con lógica compleja es quizás Reflection, pero eso muestra rápidamente sus limitaciones, por no hablar del código crítico de rendimiento, incluso cuando se usa en la biblioteca estándar, por ejemplo, el manejo de JSON de Go es primitivo. a lo mejor.

Es difícil argumentar que usar plantillas de texto o reflexión para hacer _algo que ya es complejo_ se ajusta al ideal de:

Cada vez que un enfoque propuesto para ~genéricos~ algo-complejo se vuelve difícil de explicar de una manera simple, debemos descartar ese enfoque.

Creo que la solución a la que han llegado los proyectos mencionados para resolver su problema es demasiado compleja y no es fácil de entender. Entonces, en ese sentido, Go carece de las instalaciones que permiten a los usuarios expresar problemas complejos en términos tan simples y directos como sea posible.

En general, noto que las personas a menudo señalan cualquier uso de la reflexión y lo afirman como evidencia de la necesidad de medicamentos genéricos.

Tal vez haya un concepto erróneo tan general, pero la biblioteca protobuf, especialmente la nueva API, podría ser mucho más simple con _generics_, o algún tipo de _sum type_.

Uno de los autores de esa nueva API de protobuf acaba de decir que "la gran mayoría de la lógica no se beneficiará de los genéricos", por lo que no estoy seguro de dónde está entendiendo que "especialmente la nueva API podría dar pasos agigantados mucho más simple con los genéricos". ¿En qué se basa esto? ¿Puede proporcionar alguna evidencia de que sería mucho más simple?

Hablando como alguien que usó las API de protobuf en un par de lenguajes que incluyen genéricos (Java, C++), no puedo decir que haya notado diferencias significativas de usabilidad con la API de Go y sus API. Si su afirmación fuera cierta, esperaría que hubiera alguna diferencia.

@dsnet También dijo "hay algunos casos en los que los genéricos pueden mejorar la usabilidad y/o la implementación de la API".

Pero si desea un ejemplo de cómo las cosas pueden ser más simples, comience eliminando el tipo Value , ya que es en gran medida un tipo de suma ad-hoc.

@omeid Este problema se trata de genéricos, no de tipos de suma. Así que no estoy seguro de cómo ese ejemplo es relevante.

Específicamente, mi pregunta es: ¿cómo resultaría tener genéricos en una implementación de protobuf o API que es "a pasos agigantados mucho más simple" que la API nueva (o antigua, para el caso)?

Esto no parece estar en línea con mi lectura de lo que @dsnet dijo anteriormente, ni con mi experiencia con las API protobuf de Java y C++.

Además, su comentario sobre el manejo primitivo de JSON en Go también me parece igualmente extraño. ¿Puede explicar cómo cree que los genéricos mejorarían la API encoding/json ?

AFAIK, las implementaciones de análisis JSON en Java usan reflexión (no genéricos). Es cierto que la API de nivel superior en la mayoría de las bibliotecas JSON probablemente usará un método genérico (por ejemplo, Gson ), pero un método que toma un parámetro genérico sin restricciones T y devuelve un valor de tipo T proporciona muy poca verificación de tipo adicional en comparación con json.Unmarshal . De hecho, creo que el único error que el único escenario de error adicional no detecta json.Unmarshal en tiempo de compilación es si pasa un valor que no es de puntero. (Además, tenga en cuenta las advertencias en la documentación de la API de Gson para usar una función diferente para los tipos genéricos frente a los no genéricos. Una vez más, esto argumenta que los genéricos complicaron su API, en lugar de simplificarla; en este caso, es para admitir la serialización/deserialización de genéricos). tipos).

(La compatibilidad con JSON en C++ es AFAICT peor; los diversos enfoques que conozco usan cantidades significativas de macros o implican escribir manualmente funciones de análisis/serialización. Una vez más, esto no es así)

Si espera que los genéricos agreguen mucho al soporte de Go para JSON, me temo que se sentirá decepcionado.


@gertcuykens Todas las implementaciones de protobuf en todos los idiomas que conozco utilizan la generación de código, independientemente de si tienen genéricos o no. Esto incluye Java, C++, Swift, Rust, JS (y TS). No creo que tener genéricos elimine automáticamente todos los usos de la generación de código (como prueba de existencia, he escrito generadores de código que generan código Java y código C++); parece ilógico esperar que cualquier solución para los genéricos cumpla con ese estándar.


Solo para ser absolutamente claro: apoyo agregar genéricos a Go. Pero creo que deberíamos tener los ojos claros sobre lo que vamos a sacar de esto. No creo que obtengamos mejoras significativas en las API de protobuf o JSON.

No creo que protobuf sea un caso particularmente bueno para los genéricos. No necesita genéricos en el idioma de destino, ya que simplemente puede generar código especializado directamente. Esto también se aplicaría a otros sistemas similares como Swagger/OpenAPI.

Donde los genéricos me parecerían útiles, y podrían ofrecer tanto simplificación como seguridad de tipos, sería escribiendo el compilador protobuf.

Lo que necesitaría es un lenguaje que sea capaz de una representación segura de tipos de su propio árbol de sintaxis abstracta. Desde mi propia experiencia, esto requiere al menos genéricos y tipos de datos abstractos generalizados. Luego podría escribir un compilador protobuf con seguridad de tipos para un idioma en el propio idioma.

Donde los genéricos me parecerían útiles, y podrían ofrecer tanto simplificación como seguridad de tipos, sería escribiendo el compilador protobuf.

Realmente no veo cómo. El paquete go/ast ya proporciona una representación del AST de Go. El compilador Go protobuf no lo usa porque trabajar con un AST es mucho más engorroso que simplemente emitir cadenas, incluso si es más seguro para los tipos.

¿Quizás tenga un ejemplo del compilador protobuf para algún otro idioma?

@neild Empecé diciendo que no creía que protobuf fuera un muy buen ejemplo. Se pueden obtener beneficios con los genéricos, pero dependen mucho de la importancia de la seguridad de tipos para usted, y esto se equilibraría con la intrusión de la implementación de los genéricos. Una implementación ideal se saldría de su camino, a menos que cometa un error, y en cuyo caso las ventajas superarían el costo para más casos de uso.

En cuanto al paquete go/ast, no tiene una representación tipificada del AST porque requiere genéricos y GADT. Por ejemplo, un nodo 'agregar' debería ser genérico en el tipo de términos que se agregan. Con un AST sin seguridad de tipo, toda la lógica de verificación de tipo debe codificarse a mano, lo que lo haría engorroso.

Con una buena sintaxis de plantilla y escribir expresiones seguras, podría hacerlo tan fácil como emitir cadenas, pero también escribir con seguridad. Por ejemplo, vea (esto es más sobre el lado del análisis): https://stackoverflow.com/questions/11104536/how-to-parse-strings-to-syntax-tree-using-gadts

Por ejemplo, considere JSX como una sintaxis literal para HTML Dom en JavaScript Vs TSX como una sintaxis literal para Dom en TypeScript.

Podemos escribir expresiones genéricas escritas que se especializan en el código final. Tan fácil de escribir como cadenas, pero con verificación de tipo (en su forma genérica).

Uno de los problemas clave con los generadores de código es que la verificación de tipos solo ocurre en el código emitido, lo que dificulta la escritura de plantillas correctas. Con los genéricos, puede escribir las plantillas como expresiones reales con verificación de tipo, por lo que la verificación se realiza directamente en la plantilla, no en el código emitido, lo que hace que sea mucho más fácil hacerlo bien y mantenerlo.

Los parámetros de tipo variádico faltan en el diseño actual, lo que parece una gran falta de funcionalidad de los genéricos. Un diseño adicional (tal vez) sigue el diseño del contrato actual:

contract Comparables(Ts...) {
    if  len(Ts) > 0 {
        Comparables(Ts[1:]...)
    } else {
        Comparable(Ts[0])
    }
}

contract Comparable(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

type Keys(type Ts ...Comparables) struct {
    fs ...Ts
}

type Metric(type Ts ...Comparables) struct {
    mu sync.Mutex
    m  map[Keys(Ts...)]int
}

func (m *Metric(Ts...)) Add(vs ...Ts) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[Keys(Ts...))]int)
    }
    m[Keys(Ts...){vs...}]++
}


// To use the metric

m := Metric(int, float64, string){m: make(map[Keys(int, float64, string)]int}
m.Add(1, 2.0, "variadic")

Ejemplo inspirado en aquí .

No me queda claro cómo eso agrega seguridad por encima de simplemente usar interface{} . ¿Existe un problema real con las personas que pasan no comparables a una métrica?

No me queda claro cómo eso agrega seguridad por encima de simplemente usar interface{} . ¿Existe un problema real con las personas que pasan no comparables a una métrica?

Comparables en este ejemplo requiere que Keys debe constar de una serie de tipos comparables. La idea clave es mostrar el diseño de los parámetros de tipo variádico, no el significado del tipo en sí.

No quiero obsesionarme demasiado con el ejemplo, pero lo tomo porque creo que muchos ejemplos de "extensión de tipo" simplemente terminan empujando la contabilidad sin agregar ninguna seguridad práctica. En este caso, si ve un tipo incorrecto en tiempo de ejecución o potencialmente con go vet, entonces podría quejarse.

Además, me preocupa un poco que permitir tipos abiertos como este conduzca al problema de las referencias paradójicas, como ocurre en la lógica de segundo orden. ¿Podría definir C como el contrato de todos los tipos que no están en C?

Además, me preocupa un poco que permitir tipos abiertos como este conduzca al problema de las referencias paradójicas, como ocurre en la lógica de segundo orden. ¿Podría definir C como el contrato de todos los tipos que no están en C?

Lo siento, pero no entiendo cómo este ejemplo permite tipos abiertos y se relaciona con la paradoja de Russell, Comparables se define mediante una lista de Comparable .

No me gusta la idea de escribir código Go dentro de un contrato. Si puedo escribir una declaración if , ¿puedo escribir una declaración for ? ¿Puedo llamar a una función? ¿Puedo declarar variables? ¿Por qué no?

También parece innecesario. func F(a ...int) significa que a es []int . Por analogía, func F(type Ts ...comparable) significaría que cada tipo de la lista es comparable .

en estas lineas

type Keys(type Ts ...Comparables) struct {
    fs ...Ts
}

parece que está definiendo una estructura con varios campos, todos llamados fs . No estoy seguro de cómo se supone que funciona. ¿Hay alguna forma de usar referir a los campos en esta estructura que no sea usar la reflexión?

Entonces la pregunta es: ¿qué se puede hacer con los parámetros de tipo variádico? ¿Qué quiere hacer uno?

Aquí creo que está utilizando parámetros de tipo variable para definir un tipo de tupla con un número arbitrario de campos.

¿Qué más podría uno querer hacer?

No me gusta la idea de escribir código Go dentro de un contrato. Si puedo escribir una declaración if , ¿puedo escribir una declaración for ? ¿Puedo llamar a una función? ¿Puedo declarar variables? ¿Por qué no?

También parece innecesario. func F(a ...int) significa que a es []int . Por analogía, func F(type Ts ...comparable) significaría que cada tipo de la lista es comparable .

Después de revisar el ejemplo un día después, creo que tienes toda la razón. El Comparables es una idea tonta. El ejemplo solo quiere transmitir el mensaje de usar len(args) para determinar la cantidad de parámetros. Resulta que para las funciones, func F(type Ts ...Comparable) es lo suficientemente bueno.

El ejemplo recortado:

contract Comparable(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

type Keys(type Ts ...Comparable) struct {
    fs ...Ts
}

type Metric(type Ts ...Comparable) struct {
    mu sync.Mutex
    m  map[Keys(Ts...)]int
}

func (m *Metric(Ts...)) Add(vs ...Ts) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[Keys(Ts...))]int)
    }
    m[Keys(Ts...){vs...}]++
}


// To use the metric

m := Metric(int, float64, string){m: make(map[Keys(int, float64, string)]int}
m.Add(1, 2.0, "variadic")

parece que está definiendo una estructura con múltiples campos, todos llamados fs . No estoy seguro de cómo se supone que funciona. ¿Hay alguna forma de usar referir a los campos en esta estructura que no sea usar la reflexión?

Entonces la pregunta es: ¿qué se puede hacer con los parámetros de tipo variádico? ¿Qué quiere hacer uno?

Aquí creo que está utilizando parámetros de tipo variable para definir un tipo de tupla con un número arbitrario de campos.

¿Qué más podría uno querer hacer?

Los parámetros de tipo variable están destinados a tuplas por su definición si usamos ... para ello, lo que no significa que las tuplas sean el único caso de uso, pero se pueden usar en cualquier estructura y función.

Dado que solo hay dos lugares que aparecen con parámetros de tipo variable: estructura o función, tenemos fácilmente lo que está claro antes para las funciones:

func F(type Ts ...Comparable) (args ...Ts) {
    if len(args) > 1 {
        F(args[1:])
        return
    }
    // ... do stuff with args[0]
}

Por ejemplo, la función variable Min no es posible en el diseño actual, pero es posible con parámetros de tipo variable:

func Min(type T ...Comparable)(p1 T, pn ...T) T {
    switch l := len(pn); {
    case l > 1:
        return Min(pn[0], pn[1:]...)
    case l == 1:
        if p1 >= pn[0] { return pn[0] }
        return p1
    case l < 1:
        return p1
    }
}

Para definir un Tuple con parámetros de tipo variable:

type Tuple(type Ts ...Comparable) struct {
    fs ...Ts
}

Cuando tres parámetros de tipo instanciados por 'Ts', se puede traducir a

type Tuple(type T1, T2, T3 Comparable) struct {
    fs_1 T1
    fs_2 T2
    fs_3 T3
}

como representación intermedia. Para usar el fs , hay varias formas:

  1. parámetros desempaquetar
k := Tuple(int, float64, string){1, 2.0, "variadic"}
fs1, fs2, fs3 := k.fs // translated to fs1, fs2, fs3 := k.fs_1, k.fs_2, k.fs_3
println(fs1) // 1
println(fs2) // 2.0
println(fs3) // variadic
  1. usar for bucle
for idx, f := range k.fs {
    println(idx, ": ", f)
}
// Output:
// 0: 1
// 1: 2.0
// 2: variadic
  1. use el índice (no estoy seguro si las personas ven que esto es una ambigüedad para la matriz/rebanada o el mapa)
k.fs[0] = ... // translated to k.fs_1 = ...
f2 := k.fs[1] // translated to f2 := k.fs_2
  1. use el paquete reflect , básicamente funciona como una matriz
t := Tuple(int, float64, string){1, 2.0, "variadic"}

fs := reflect.ValueOf(t).Elem().FieldByName("fs")
val := reflect.ValueOf(fs)
if val.Kind() == reflect.VariadicTypes {
    for i := 0; i < val.Len(); i++ {
        e := val.Index(i)
        switch e.Kind() {
        case reflect.Int:
            fmt.Printf("%v, ", e.Int())
        case reflect.Float64:
            fmt.Printf("%v, ", e.Float())
        case reflect.String:
            fmt.Printf("%v, ", e.String())
        }
    }
}

Nada realmente nuevo en comparación con el uso de una matriz.

Por ejemplo, la función mínima variable no es posible en el diseño actual, pero es posible con parámetros de tipo variable:

func Min(type T ...Comparable)(p1 T, pn ...T) T {
    switch l := len(pn); {
    case l > 1:
        return Min(pn[0], pn[1:]...)
    case l == 1:
        if p1 >= pn[0] { return pn[0] }
        return p1
    case l < 1:
        return p1
    }
}

Esto no tiene sentido para mí. Los parámetros de tipo variádico solo tienen sentido si los tipos pueden ser tipos diferentes. Pero llamar a Min en una lista de diferentes tipos no tiene sentido. Go no admite el uso >= en valores de diferentes tipos. Incluso si permitiéramos eso de alguna manera, es posible que nos pidan Min(int, string)(1, "a") . Eso no tiene ningún tipo de respuesta.

Si bien es cierto que el diseño actual no permite Min de un número variable de tipos diferentes, admite llamar a Min en un número variable de valores del mismo tipo. Lo cual creo que es la única forma razonable de usar Min todos modos.

func Min(type T comparable)(s ...T) T {
    if len(s) == 0 {
        panic("Min of no elements")
    }
    r := s[0]
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

Para algunos de los otros ejemplos en https://github.com/golang/go/issues/15292#issuecomment -599040081, es importante tener en cuenta que en Go los segmentos y matrices tienen elementos que son todos del mismo tipo. Cuando se utilizan parámetros de tipos variádicos, los elementos son de tipos diferentes. Así que en realidad no es lo mismo que un segmento o una matriz.

Si bien es cierto que el diseño actual no permite Min de un número variable de tipos diferentes, admite llamar a Min en un número variable de valores del mismo tipo. Lo cual creo que es la única forma razonable de usar Min todos modos.

func Min(type T comparable)(s ...T) T {
    if len(s) == 0 {
        panic("Min of no elements")
    }
    r := s[0]
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

Verdadero. Min fue un mal ejemplo. Se agregó tarde y no tenía una idea clara, como puede ver en el historial de edición de comentarios. Un ejemplo real es el Metric que ignoraste.

es importante tener en cuenta que en Go los sectores y las matrices tienen elementos que son todos del mismo tipo. Cuando se utilizan parámetros de tipos variádicos, los elementos son de tipos diferentes. Así que en realidad no es lo mismo que un segmento o una matriz.

¿Ver? Ustedes son esas personas que ven que esto es una ambigüedad para organizar/rebanar o mapear. Como dije en https://github.com/golang/go/issues/15292#issuecomment -599040081, la sintaxis es bastante similar a array/slice y map, pero accede a elementos con diferentes tipos. ¿Realmente importa? ¿O se puede probar que esto es ambigüedad? Lo que es posible en Go 1 es:

m := map[interface{}]int{1: 2, "2": 3, 3.0: 4}
for i, e := range m {
    println(i, e)
}

¿ i se considera del mismo tipo? Aparentemente, decimos que i es interface{} , del mismo tipo. Pero, ¿realmente una interfaz expresa el tipo? Los programadores tienen que comprobar manualmente cuáles son los tipos posibles. Al usar for , [] y desempaquetar, ¿realmente le importa al usuario que no esté accediendo al mismo tipo? ¿Cuáles son los argumentos en contra de esto? Lo mismo para los fs :

for idx, f := range k.fs {
    switch f.(type) { // compare to interface{}, here is zero overhead.
    case int:
        // ...
    case float64:
        // ...
    case string:
        // ...
    }
}

Si tiene que usar un cambio de tipo para acceder a un elemento de un tipo genérico variado, no veo la ventaja. Puedo ver cómo con algunas opciones de técnica de compilación, posiblemente podría ser un poco más eficiente en tiempo de ejecución que usar interface{} . Pero creo que la diferencia sería bastante pequeña, y no veo por qué sería más seguro para los tipos. No es inmediatamente obvio que valga la pena hacer que el lenguaje sea más complejo.

No tenía la intención de ignorar el ejemplo Metric , solo que todavía no veo cómo usar tipos genéricos variados para simplificar la escritura. Si necesito usar un cambio de tipo en el cuerpo de Metric , creo que preferiría escribir Metric2 y Metric3 .

¿Cuál es la definición de "hacer el lenguaje más complejo"? Todos estamos de acuerdo en que los genéricos son algo complejo, y nunca harán que el lenguaje sea más simple que Go 1. Usted ya se ha esforzado mucho en diseñarlo e implementarlo, pero no está muy claro para los usuarios de Go: ¿cuál es la definición de "se siente como escribiendo... Ir"? ¿Existe una métrica cuantificada para medirlo? ¿Cómo podría una propuesta de lenguaje argumentar que no está haciendo que el lenguaje sea más complejo? En la plantilla de propuesta de idioma Go 2, los objetivos son bastante sencillos en su primera impresión:

  1. abordar un tema importante para muchas personas,
  2. tener un impacto mínimo en todos los demás, y
  3. vienen con una solución clara y bien entendida.

Pero, las preguntas podrían ser: ¿Cuántos son "muchos"? ¿Qué significa "importante"? ¿Cómo medir el impacto en una población desconocida? ¿Cuándo se entiende bien un problema? Go está dominando la nube, pero ¿dominar otras áreas como la computación numérica científica (p. ej., aprendizaje automático), la representación gráfica (p. ej., un gran mercado 3D) se convertirá en uno de los objetivos de Go? ¿El problema encaja más en "Prefiero hacer A que B en Go & No hay caso de uso porque podemos hacerlo de otra manera" o "B no se ofrece, por lo tanto no usamos Go & El caso de uso no está allí todavía porque el lenguaje no puede expresarlo fácilmente"? ... Descubrí que esas preguntas son dolorosas e interminables, ya veces ni siquiera vale la pena responderlas.

Volviendo al ejemplo Metric , no muestra ninguna necesidad de acceder a las personas. Desempaquetar el conjunto de parámetros no parece ser una necesidad real aquí, aunque las soluciones que "coinciden" con el lenguaje existente son el uso [ ] indexación y deducción de tipos que pueden resolver el problema de la seguridad de tipos:

f2 := k.fs[1] // f2 is a float64

@changkun Si hubiera métricas claras y objetivas para decidir qué características del idioma son buenas y malas, no necesitaríamos diseñadores de idiomas; simplemente podríamos escribir un programa para diseñar un idioma óptimo para nosotros. Pero no lo hay, siempre se reduce a las preferencias personales de algún grupo de personas. Lo cual es también, por cierto, por qué no tiene sentido discutir si un idioma es "bueno" o no; la única pregunta es si a usted, personalmente, le gusta. En el caso de Go, las personas que deciden sus preferencias son las personas del equipo de Go y las cosas que cita no son métricas, son preguntas guía para ayudarlo a convencerlos.

Personalmente, FWIW, siento que los parámetros de tipo variádico fallan en dos de esos tres. No creo que aborden un problema importante para muchas personas: el ejemplo de métricas podría beneficiarse de ellos, pero en mi opinión solo un poco y es un caso de uso muy especializado. Y no creo que vengan con una solución clara y bien entendida. No conozco ningún idioma que admita algo como esto. Pero puedo estar equivocado. Definitivamente sería útil, si alguien tiene ejemplos de otros idiomas que admitan esto; podría proporcionar información sobre cómo se implementa generalmente y, lo que es más importante, cómo se usa. Tal vez se usa más ampliamente de lo que puedo imaginar.

@Merovius Haskell tiene funciones polivariadas como demostramos en el artículo de HList: http://okmij.org/ftp/Haskell/polyvariadic.html#polyvar -fn
Es claramente complejo hacer esto en Haskell, pero no imposible.

El ejemplo motivador es el acceso seguro a la base de datos donde se pueden hacer cosas como uniones y proyecciones seguras, y el esquema de la base de datos declarado en el lenguaje.

Por ejemplo, una tabla de base de datos se parece mucho a un registro, donde hay nombres y tipos de columnas. La operación de unión relacional toma dos registros arbitrarios y produce un registro con los tipos de ambos. Por supuesto, puede hacerlo a mano, pero es propenso a errores, es muy tedioso, ofusca el significado del código con todos los tipos de registro declarados a mano y, por supuesto, la gran característica de una base de datos SQL es que admite ad-hoc. consultas, por lo que no puede crear previamente todos los tipos de registro posibles, ya que no necesariamente sabe qué consultas desea hasta que las realiza.

Por lo tanto, un operador de unión relacional con seguridad de tipos en registros y tuplas sería un buen caso de uso. Aquí solo estamos pensando en el tipo de función: depende del programador lo que realmente hace la función, ya sea una unión en memoria de dos matrices de tuplas, o si genera SQL para ejecutarse en una base de datos externa y ordenar los resultados. de vuelta de forma segura.

Este tipo de cosas se integran mucho mejor en C# con LINQ. La mayoría de la gente parece pensar que LINQ agrega funciones lambda y mónadas a C#, pero no funcionaría para su caso de uso principal sin polivariadas, ya que simplemente no puede definir un operador de combinación seguro de tipos sin una funcionalidad similar.

Creo que los operadores relacionales son importantes. Después de los operadores básicos en tipos booleanos, binarios, int, flotantes y de cadena, probablemente sigan los conjuntos y luego las relaciones.

Por cierto, C++ también lo ofrece, aunque no queremos discutir si queremos esta función en Go porque XXX la tiene :)

Creo que sería muy extraño si k.fs[0] y k.fs[1] tuvieran tipos diferentes. No es así como funcionan otros valores indexables en Go.

El ejemplo de métricas se basa en https://medium.com/@sameer_74231/go -experience-report-for-generics-google-metrics-api-b019d597aaa4. Creo que el código requiere reflexión para recuperar los valores. Creo que si vamos a agregar genéricos variados a Go, deberíamos obtener algo mejor que la reflexión para recuperar los valores. De lo contrario, simplemente no parece ayudar mucho.

Creo que sería muy extraño si k.fs[0] y k.fs[1] tuvieran tipos diferentes. No es así como funcionan otros valores indexables en Go.

El ejemplo de métricas se basa en https://medium.com/@sameer_74231/go -experience-report-for-generics-google-metrics-api-b019d597aaa4. Creo que el código requiere reflexión para recuperar los valores. Creo que si vamos a agregar genéricos variados a Go, deberíamos obtener algo mejor que la reflexión para recuperar los valores. De lo contrario, simplemente no parece ayudar mucho.

Bien. Estás solicitando algo que no existe. Si no le gusta [``] , quedan dos opciones: ( ) o {``} , y veo que puede argumentar que los paréntesis parecen una llamada de función y las llaves parecen una inicialización variable. A nadie le gusta args.0 args.1 ya que esto no se siente como Go. La sintaxis es trivial.

De hecho, pasé un fin de semana leyendo el libro "el diseño y la evolución de C++", hay muchas ideas interesantes sobre decisiones y lecciones, aunque fue escrito en 1994:

_"[...] En retrospectiva, subestimé la importancia de las restricciones en la legibilidad y la detección temprana de errores"._ ==> Gran diseño de contrato

"_la sintaxis de la función a primera vista también se ve mejor sin una palabra clave adicional:_

T& index<class T>(vector<T>& v, int i) { /*...*/ }
int i = index(v1, 10);

_Parece haber problemas persistentes con esta sintaxis más simple. Es demasiado inteligente. Es relativamente difícil detectar una declaración de plantilla en un programa porque [...] Los corchetes <...> se eligieron con preferencia a los paréntesis porque los usuarios los encontraron más fáciles de leer. [...] Da la casualidad de que Tom Pennello demostró que los paréntesis habrían sido más fáciles de analizar, pero eso no cambia la observación clave de que los lectores (humanos) prefieren <...> _
" ==> ¿No es similar a func F(type T C)(v T) T ?

_"Sin embargo, creo que fui demasiado cauteloso y conservador a la hora de especificar las características de la plantilla. Podría haber incluido características como [...]. Estas características no habrían aumentado mucho la carga de los implementadores, y los usuarios habrían sido ayudados."_

¿Por qué se siente tan familiar?

La indexación de parámetros de tipo variable (o tupla) debe separarse de la indexación en tiempo de ejecución y la indexación en tiempo de compilación. Supongo que puede argumentar que la falta de soporte para la indexación en tiempo de ejecución puede confundir a los usuarios porque no es consistente con la indexación en tiempo de compilación. Incluso para la indexación en tiempo de compilación, también falta un parámetro de "plantilla" que no sea de tipo en el diseño actual.

Con todas las pruebas, la propuesta (excepto el informe de experiencia) trata de evitar discutir esta característica, y empiezo a creer que no se trata de agregar genéricos variados a Go, sino que simplemente se eliminan por diseño.

Estoy de acuerdo en que Design and Evolution of C++ es un buen libro, pero C++ y Go tienen objetivos diferentes. La cita final es buena; Stroustrup ni siquiera menciona el costo de la complejidad del idioma para los usuarios del idioma. En Go siempre intentamos tener en cuenta ese coste. Go pretende ser un lenguaje simple. Si agregáramos todas las funciones que ayudarían a los usuarios, no sería sencillo. Como C++ no es simple.

Con todas las pruebas, la propuesta (excepto el informe de experiencia) trata de evitar discutir esta característica, y empiezo a creer que no se trata de agregar genéricos variados a Go, sino que simplemente se eliminan por diseño.

Lo siento, no sé a qué te refieres aquí.

Personalmente, siempre he considerado la posibilidad de tipos genéricos variados, pero nunca me he tomado el tiempo de averiguar cómo funcionaría. La forma en que funciona en C++ es muy sutil. Me gustaría ver si primero podemos hacer que funcionen los genéricos no variádicos. Ciertamente hay tiempo para agregar genéricos variados, si es posible, más adelante.

Cuando critico los pensamientos anteriores, no estoy diciendo que no se puedan hacer tipos variados. Estoy señalando problemas que creo que necesitan ser resueltos. Si no se pueden resolver, entonces no estoy convencido de que los tipos variádicos valgan la pena.

Stroustrup ni siquiera menciona el costo de la complejidad del idioma para los usuarios del idioma. En Go siempre intentamos tener en cuenta ese coste. Go pretende ser un lenguaje simple. Si agregáramos todas las funciones que ayudarían a los usuarios, no sería sencillo. Como C++ no es simple.

No es cierto en mi opinión. Se debe tener en cuenta que C ++ es el primer practicante que lleva adelante los genéricos (Bueno, ML es el primer idioma). Por lo que leí del libro, recibo el mensaje de que C ++ estaba destinado a ser un lenguaje simple (no ofrecer genéricos al principio, ciclo Experimentar-Simplificar-Enviar para diseño de lenguaje, la misma historia). C ++ también tuvo una fase de congelación de funciones durante varios años, que es lo que tenemos en Go "The Compatability Promise". Pero se sale un poco de control con el tiempo debido a muchas razones razonables, lo que no le queda claro a Go si sigue el antiguo camino de C++ después del lanzamiento de los genéricos.

Ciertamente hay tiempo para agregar genéricos variados, si es posible, más adelante.

El mismo sentimiento para mí. Los genéricos de Variadic también faltan en la primera versión estandarizada de las plantillas.

Estoy señalando problemas que creo que necesitan ser resueltos. Si no se pueden resolver, entonces no estoy convencido de que los tipos variádicos valgan la pena.

Entiendo tus preocupaciones. Pero el problema está básicamente resuelto, pero solo necesita traducirse correctamente a Go (y supongo que a nadie le gusta la palabra "traducir"). Lo que leí de su propuesta histórica de genéricos, básicamente sigue lo que falló en la propuesta inicial de C++ y se comprometió con lo que Stroustrup lamenta. Estoy interesado en sus contraargumentos sobre esto.

Tendremos que estar en desacuerdo sobre los objetivos de C++. Tal vez los objetivos originales eran más similares, pero mirando a C++ hoy, creo que está claro que sus objetivos son muy diferentes a los objetivos de Go, y creo que ha sido así durante al menos 25 años.

Al escribir varias propuestas para agregar genéricos a Go, por supuesto analicé cómo funcionan las plantillas de C++, así como muchos otros lenguajes (después de todo, C++ no inventó los genéricos). No miré de qué se arrepintió Stroustrup, así que si llegamos al mismo lugar, entonces, genial. Mi opinión es que los genéricos en Go se parecen más a los genéricos en Ada o D que en C++. Incluso hoy en día, C ++ no tiene contratos, a los que llaman conceptos pero aún no se han agregado al lenguaje. Además, C ++ permite intencionalmente una programación compleja en el momento de la compilación y, de hecho, las plantillas de C ++ son en sí mismas un lenguaje completo de Turing (aunque no sé si eso fue intencional). Siempre he considerado que es algo que se debe evitar para Go, ya que la complejidad es extrema (aunque es más complejo en C++ de lo que sería en Go debido a la sobrecarga de métodos y la resolución, que Go no tiene).

Después de probar la implementación del contrato actual durante aproximadamente un mes, me pregunto cuál será el destino de las funciones integradas existentes. Todos ellos se pueden implementar de forma genérica:

func Append(type T)(slice []T, elems ...T) []T {...}
func Copy(type T)(dst, src []T) int {...}
func Delete(type K, V)(m map[K]V, k K) {...}
func Make(type T, I Integer(I))(siz ...I) T {...}
func New(type T)() *T {...}
func Close(type T)(c chan<- T) {...}
func Panic(type T)(v T) {...}
func Recover(type T)() T {...}
func Print(type ...T)(args ...T) {...}
func Println(type ...T)(args ...T) {...}

¿Se habrán ido en Go2? ¿Cómo podría Go 2 lidiar con un impacto tan grande en el código base existente de Go 1? Estas parecen ser preguntas abiertas.

Además, estos dos son un poco especiales:

func Len(type T C)(t T) int {...}
func Cap(type T C)(t T) int {...}

Cómo implementar un contrato de este tipo C con el diseño actual, de modo que un parámetro de tipo solo pueda ser un segmento genérico []Ts , mapa map[Tk]Tv y canal chan Tc donde T Ts Tk Tv Tc son diferentes?

@changkun No creo que "se puedan implementar con genéricos" sea una razón convincente para eliminarlos. Y mencionas una razón bastante clara y sólida por la que no deberían eliminarse. Así que no creo que lo sean. Creo que eso hace que el resto de las preguntas queden obsoletas.

@changkun No creo que "se puedan implementar con genéricos" sea una razón convincente para eliminarlos. Y mencionas una razón bastante clara y sólida por la que no deberían eliminarse.

Si, estoy de acuerdo en que no es el convencimiento por quitarlos, por eso lo dije explícitamente. Sin embargo, mantenerlos junto con los genéricos "viola" la filosofía existente de Go, cuyas características del lenguaje son ortogonales. La compatibilidad es la principal preocupación, pero es probable que agregar contratos elimine el gran código actual "desactualizado".

Así que no creo que lo sean. Creo que eso hace que el resto de las preguntas queden obsoletas.

Tratemos de no ignorar la pregunta y considerarla como un caso de uso de contratos en el mundo real. Si uno presenta requisitos similares, ¿cómo podríamos implementarlo con el diseño actual?

Claramente no vamos a deshacernos de las funciones predeclaradas existentes.

Si bien es posible escribir una firma de función parametrizada para delete , close , panic , recover , print y println , no creo que sea posible implementarlos sin depender de las funciones mágicas internas.

Hay versiones parciales de Append y Copy en https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-contracts.md#append. No está completo porque append y copy tienen casos especiales para un segundo argumento de tipo string , que no es compatible con el borrador de diseño actual.

Tenga en cuenta que la firma de Make , arriba, no es válida de acuerdo con el borrador de diseño actual. New no es lo mismo que new , pero lo suficientemente cerca.

Con el borrador de diseño actual, Len y Cap tendrían que tomar un argumento de tipo interface{} y, como tal, no sería seguro en tiempo de compilación.

https://go-review.googlesource.com/c/go/+/187317

No use extensiones de archivo .go2 , ¿tenemos módulos para hacer este tipo de versión? Entiendo si lo está haciendo como una solución temporal para hacer la vida más fácil mientras experimenta con contratos, pero asegúrese de que al final el archivo go.mod se encargará de mezclar paquetes go sin el necesidad de .go2 extensiones de archivo. Sería un golpe contra los desarrolladores de módulos que se esfuerzan por asegurarse de que los módulos funcionen lo mejor posible. Usar extensiones de archivo .go2 es como decir, no, no me importa que las cosas de su módulo lo hagan a mi manera de todos modos porque no quiero que mi compilador go del dinosaurio anterior al módulo de 10 años se rompa .

Los archivos @gertcuykens .go2 son solo para el experimento; no se utilizarán cuando los genéricos lleguen al compilador.

(Ocultaré nuestros comentarios ya que en realidad no se suman a la discusión y es lo suficientemente largo como está).

Recientemente, exploré una nueva sintaxis genérica en el lenguaje K que diseñé, porque K tomó prestada mucha gramática de Go, por lo que esta gramática genérica también puede tener algún valor de referencia para Go.

El problema identifier<T> es que entra en conflicto con los operadores de comparación y también con los operadores de bits, por lo que no estoy de acuerdo con este diseño.

El identifier[T] de Scala tiene una mejor apariencia que el diseño anterior, pero después de resolver el conflicto anterior, tiene un nuevo conflicto con el diseño del índice identifier[index] .
Por esta razón, el diseño del índice de Scala se ha cambiado a identifier(index) . Esto no funciona bien para idiomas que ya usan [] como índice.

En el borrador de Go, se declaró que los genéricos usan (type T) , lo que no causará conflictos, porque type es una palabra clave, pero el compilador aún necesita más juicio cuando se le llama para resolver el identifier(type)(params) . Aunque es mejor que las soluciones anteriores, todavía no me satisface.

Por casualidad, recordé el diseño especial de invocación de métodos en OC, que me sirvió de inspiración para un nuevo diseño.

¿Qué pasa si ponemos el identificador y el genérico como un todo y los ponemos en [] juntos?
Podemos conseguir los [identifier T] . Este diseño no entra en conflicto con el índice, porque debe tener al menos dos elementos, separados por espacios.
Cuando hay varios genéricos, podemos escribir [identifier T V] así, y no entrará en conflicto con el diseño existente.

Sustituyendo este diseño en Go, podemos obtener el siguiente ejemplo.
P.ej

type [Item T] struct {
    Value T
}

func (it [Item T]) Print() {
    println(it.Value)
}

func [TestGenerics T V]() {
    var a = [Item T]{}
    a.Print()
    var b = [Item V]{}
    b.Print()
}

func main() {
    [TestGenerics int string]()
}

Esto se ve muy claro.

Otro beneficio de usar [] es que tiene algo de herencia del diseño original de Slice and Map de Go, y no causará una sensación de fragmentación.

[]int  ->  [slice int]

map[string]int  ->  [map string int]

Podemos hacer un ejemplo más complicado.

var a map[int][]map[string]map[string][]string

var b [map int [slice [map string [map string [slice string]]]]]

Este ejemplo todavía mantiene un efecto relativamente claro y, al mismo tiempo, tiene un pequeño impacto en la compilación.

He implementado y probado este diseño en K y funciona bien.

Creo que este diseño tiene cierto valor de referencia y puede ser digno de discusión.

Recientemente, exploré una nueva sintaxis genérica en el lenguaje K que diseñé, porque K tomó prestada mucha gramática de Go, por lo que esta gramática genérica también puede tener algún valor de referencia para Go.

El problema identifier<T> es que entra en conflicto con los operadores de comparación y también con los operadores de bits, por lo que no estoy de acuerdo con este diseño.

El identifier[T] de Scala tiene una mejor apariencia que el diseño anterior, pero después de resolver el conflicto anterior, tiene un nuevo conflicto con el diseño del índice identifier[index] .
Por esta razón, el diseño del índice de Scala se ha cambiado a identifier(index) . Esto no funciona bien para idiomas que ya usan [] como índice.

En el borrador de Go, se declaró que los genéricos usan (type T) , lo que no causará conflictos, porque type es una palabra clave, pero el compilador aún necesita más juicio cuando se le llama para resolver el identifier(type)(params) . Aunque es mejor que las soluciones anteriores, todavía no me satisface.

Por casualidad, recordé el diseño especial de invocación de métodos en OC, que me sirvió de inspiración para un nuevo diseño.

¿Qué pasa si ponemos el identificador y el genérico como un todo y los ponemos en [] juntos?
Podemos conseguir los [identifier T] . Este diseño no entra en conflicto con el índice, porque debe tener al menos dos elementos, separados por espacios.
Cuando hay varios genéricos, podemos escribir [identifier T V] así, y no entrará en conflicto con el diseño existente.

Sustituyendo este diseño en Go, podemos obtener el siguiente ejemplo.
P.ej

type [Item T] struct {
    Value T
}

func (it [Item T]) Print() {
    println(it.Value)
}

func [TestGenerics T V]() {
    var a = [Item T]{}
    a.Print()
    var b = [Item V]{}
    b.Print()
}

func main() {
    [TestGenerics int string]()
}

Esto se ve muy claro.

Otro beneficio de usar [] es que tiene algo de herencia del diseño original de Slice and Map de Go, y no causará una sensación de fragmentación.

[]int  ->  [slice int]

map[string]int  ->  [map string int]

Podemos hacer un ejemplo más complicado.

var a map[int][]map[string]map[string][]string

var b [map int [slice [map string [map string [slice string]]]]]

Este ejemplo todavía mantiene un efecto relativamente claro y, al mismo tiempo, tiene un pequeño impacto en la compilación.

He implementado y probado este diseño en K y funciona bien.

Creo que este diseño tiene cierto valor de referencia y puede ser digno de discusión.

estupendo

Después de algunas idas y venidas y varias relecturas, en general apoyo el borrador de diseño actual para Contracts in Go. Agradezco la cantidad de tiempo y esfuerzo que se ha invertido en ello. Si bien el alcance, los conceptos, la implementación y la mayoría de las compensaciones parecen sólidos, mi preocupación es que la sintaxis debe revisarse para mejorar la legibilidad.

Escribí una serie de cambios propuestos para abordar esto:

Los puntos clave son:

  • Sintaxis de llamada de método/afirmación de tipo para declaración de contrato
  • El "contrato vacío"
  • Delimitadores sin paréntesis

A riesgo de adelantarme al ensayo, daré algunos fragmentos de sintaxis sin explicación, convertidos a partir de muestras en el borrador de diseño de Contratos actual. Tenga en cuenta que la forma F«T» de los delimitadores es ilustrativa, no prescriptiva; vea el artículo para más detalles.

type List«type Element contract{}» struct {
    next *List«Element»
    val  Element
}

y

contract viaStrings«To, From» {
    To.Set(string)
    From.String() string
}

func SetViaStrings«type To, From viaStrings»(s []From) []To {
    r := make([]To, len(s))
    for i, v := range s {
        r[i].Set(v.String())
    }
    return r
}

y

func Keys«type K comparable, V contract{}»(m map[K]V) []K {
    r := make([]K, 0, len(m))
    for k := range m {
        r = append(r, k)
    }
    return r
}

k := maps.Keys(map[int]int{1:2, 2:4})

y

contract Numeric«T» {
    T.(int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        complex64, complex128)
}

func DotProduct«type T Numeric»(s1, s2 []T) T {
    if len(s1) != len(s2) {
        panic("DotProduct: slices of unequal length")
    }
    var r T
    for i := range s1 {
        r += s1[i] * s2[i]
    }
    return r
}

Sin cambiar realmente los contratos debajo del capó, esto es mucho más legible para mí como desarrollador de Go. También me siento mucho más seguro enseñando esta forma a alguien que está aprendiendo Go (aunque tarde en el plan de estudios).

@ianlancetaylor Basado en su comentario en https://github.com/golang/go/issues/36533#issuecomment -579484523 Estoy publicando en este hilo en lugar de comenzar una nueva edición. También aparece en la página de comentarios sobre genéricos. No estoy seguro de si necesito hacer algo más para que se "considere oficialmente" (es decir, ¿ el grupo de revisión de la propuesta Go 2 ?) o si todavía se están recopilando activamente los comentarios.

Del borrador de diseño de contratos:

¿Por qué no usar la sintaxis F<T> como C++ y Java?
Al analizar el código dentro de una función, como v := F<T> , en el punto de ver < es ambiguo si estamos viendo una instanciación de tipo o una expresión que usa el operador < . Resolver eso requiere una anticipación efectivamente ilimitada. En general, nos esforzamos por mantener el analizador de Go simple.

No particularmente en conflicto con mi última publicación: Delimitadores de abrazadera angular para contratos Go

Solo algunas ideas sobre cómo sortear este punto en el que el analizador se confunde. Pareja de muestras:

// Lifted from the design draft
func New<type K, V>(compare func(K, K) int) *Map<K, V> {
    return &Map{<K, V> compare: compare}
}

// ...

func (m *Map<K, V>) InOrder() *Iterator<K, V> {
    sender, receiver := chans.Ranger(<keyValue<K, V>>)
    var f func(*node<K, V>) bool
    f = func(n *node<K, V>) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue{<K, V> n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Esencialmente, solo una posición diferente para los parámetros de tipo en escenarios donde < podría ser ambiguo.

@toolbox Con respecto a su comentario de soporte angular. Gracias, pero para mí personalmente, esa sintaxis se lee como primero tomar la decisión de que debemos usar corchetes angulares para escribir parámetros y escribir argumentos y luego encontrar una manera de martillarlos. Creo que si agregamos genéricos a Go, debemos apuntar por algo que encaje limpia y fácilmente en el lenguaje existente. No creo que mover corchetes angulares dentro de corchetes logre ese objetivo.

Sí, este es un detalle menor, pero creo que cuando se trata de sintaxis, los detalles menores son muy importantes. Creo que si vamos a agregar parámetros y argumentos de tipo, deben funcionar de manera simple e intuitiva.

Ciertamente no afirmo que la sintaxis en el borrador de diseño actual sea perfecta, pero sí afirmo que encaja fácilmente en el lenguaje existente. Lo que debemos hacer ahora es escribir más código de ejemplo para ver qué tan bien funciona en la práctica. Un punto clave es: ¿con qué frecuencia las personas realmente tienen que escribir argumentos de tipo fuera de las declaraciones de funciones y cuán confusos son esos casos? No creo que lo sepamos.

¿Es una buena idea usar [] para tipos genéricos y usar () para funciones genéricas? Esto sería más coherente con los genéricos básicos actuales.

¿Podría la comunidad votarlo? Personalmente, preferiría _cualquier cosa_ en lugar de agregar más paréntesis, ya es difícil leer algunas definiciones de funciones para cierres, etc., esto agrega más desorden

No creo que votar sea una buena forma de diseñar un idioma. Especialmente con un conjunto muy difícil (probablemente imposible) de determinar e increíblemente grande de votantes elegibles.

Confío en que los diseñadores y la comunidad de Go convergerán en la mejor solución y
así que no he sentido la necesidad de opinar sobre nada en esta conversación.
Sin embargo, solo tenía que decir lo inesperadamente encantada que estaba con la
sugerencia de la sintaxis F«T».

(Otros corchetes Unicode:
https://unicode-search.net/unicode-namesearch.pl?term=BRACKET).

Salud,

  • Beto

El viernes 1 de mayo de 2020 a las 7:43 p. m., Matt Mc [email protected] escribió:

Después de algunas idas y venidas y varias relecturas, en general apoyo la
borrador de diseño actual para Contratos en Go. aprecio la cantidad de tiempo
y el esfuerzo que se ha invertido en ello. Si bien el alcance, los conceptos,
implementación, y la mayoría de las compensaciones parecen sólidas, mi preocupación es que el
la sintaxis necesita ser revisada para mejorar la legibilidad.

Escribí una serie de cambios propuestos para abordar esto:

Los puntos clave son:

  • Sintaxis de llamada de método/afirmación de tipo para declaración de contrato
  • El "contrato vacío"
  • Delimitadores sin paréntesis

A riesgo de adelantarme al ensayo, daré algunos fragmentos sin respaldo
sintaxis, convertida a partir de muestras en el borrador de diseño de Contratos actual. Nota
que la forma F«T» de los delimitadores es ilustrativa, no prescriptiva; ver
el escrito para más detalles.

tipo Lista«tipo Elemento contrato{}» estructura {
siguiente *Lista«Elemento»
valor elemento
}

y

contrato viaStrings«Hasta, Desde» {
Para establecer (cadena)
Desde.String() cadena
}
func SetViaStrings«type To, From viaStrings»(s []From) []To {
r := hacer([]Hasta, largo(s))
para i, v := rango s {
r[i].Set(v.Cadena())
}
volver r
}

y

func Keys«type K comparable, V contract{}»(m map[K]V) []K {
r := hacer([]K, 0, len(m))
para k := rango m {
r = agregar (r, k)
}
volver r
}
k := mapas.Teclas(mapa[int]int{1:2, 2:4})

y

contrato Numérico«T» {
T.(int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
flotar32, flotar64,
complejo64, complejo128)
}
func DotProduct«tipo T Numérico»(s1, s2 []T) T {
si len(s1) != len(s2) {
panic("Producto Punto: rebanadas de longitud desigual")
}
var r T
para i := rango s1 {
r += s1[i] * s2[i]
}
volver r
}

Sin cambiar realmente los Contratos bajo el capó, esto es mucho más
legible para mí como desarrollador de Go. También me siento mucho más seguro
enseñar esta forma a alguien que está aprendiendo Go (aunque tarde en el
plan de estudios).

@ianlancetaylor https://github.com/ianlancetaylor Basado en tu comentario
en #36533 (comentario)
https://github.com/golang/go/issues/36533#issuecomment-579484523 Soy
publicar en este hilo en lugar de comenzar una nueva edición. también está en la lista
en la página de comentarios sobre genéricos
https://github.com/golang/go/wiki/Go2GenericsFeedback . no estoy seguro si yo
necesita hacer algo más para que sea "considerado oficialmente" (es decir, Go 2
grupo de revisión de propuestas https://github.com/golang/go/issues/33892 ?) o si
la retroalimentación aún se está recopilando activamente.


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/15292#issuecomment-622657596 , o
darse de baja
https://github.com/notifications/unsubscribe-auth/AACQ2NJRBNLLDGY2XGCCQCLRPOCEHANCNFSM4CA35RXQ
.

Todos queremos la mejor sintaxis posible para Go. El borrador del diseño usa paréntesis porque funcionó con el resto de Go sin causar ambigüedades de análisis significativas. Nos hemos quedado con ellos porque eran la mejor solución que teníamos en mente en ese momento y porque había peces más grandes para freír. Hasta ahora (paréntesis) se han mantenido bastante bien.

Al final del día, si se encuentra una notación mucho mejor, eso es muy fácil de cambiar siempre que no tengamos una garantía de compatibilidad a la que adherirse (el analizador se ajusta trivialmente y cualquier cuerpo de código se puede convertir fácilmente con gofmt).

@ianlancetaylor Gracias por la respuesta, se agradece.

Tienes razón; esa sintaxis era "no usar paréntesis para argumentos de tipo" y elegir lo que sentí que era el mejor candidato, luego hacer cambios para tratar de aliviar los problemas de implementación con el analizador.

Si la sintaxis es difícil de leer (difícil saber lo que está pasando de un vistazo), ¿realmente encaja fácilmente en el lenguaje existente? Ahí es donde creo que la postura se queda corta.

Es cierto, como mencionas, esa inferencia de tipo podría reducir en gran medida la cantidad de argumentos de tipo que deben pasarse en el código del cliente. Personalmente, creo que el autor de una biblioteca debe esforzarse por exigir que se pasen argumentos de tipo cero al usar su código y, sin embargo, ocurrirá en la práctica.

Anoche, por casualidad, me encontré con la sintaxis de la plantilla para D , que es sorprendentemente similar en algunos aspectos:

template Square(T) {
    T Square(T t) {
        return t * t;
    }
}

writefln("The square of %s is %s", 3, Square!(int)(3));

template TCopy(T) {
    void copy(out T to, T from) {
        to = from;
    }
}

int i;
TCopy!(int).copy(i, 3);

Hay dos diferencias clave que veo:

  1. Tienen ! como operador de creación de instancias para emplear las plantillas.
  2. Su estilo de declaración (sin valores de retorno múltiples, métodos anidados en clases) significa que hay menos paréntesis de forma nativa en el código ordinario, por lo que el uso de paréntesis para los parámetros de tipo no crea la misma ambigüedad visual.

Operador de creación de instancias

Cuando se usan contratos, la principal ambigüedad visual es entre una creación de instancias y una llamada de función (¿o una conversión de tipo, o...?). Parte de por qué esto es problemático es que las instancias son en tiempo de compilación y las llamadas a funciones son en tiempo de ejecución. Go tiene muchas pistas visuales que le dicen al lector a qué campo pertenece cada cláusula, pero la nueva sintaxis las confunde, por lo que no es obvio si está mirando tipos o flujo de programa.

Un ejemplo inventado:

// Instantiation with unexported types and then function call,
// or chained method call?
a := draw(square, ellipse)(canvas, color)

Propuesta: utilice un operador de creación de instancias para especificar parámetros de tipo. El ! que usa D parece perfectamente aceptable. Algunos ejemplos de sintaxis:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map!(K, V) {
    return &Map!(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator!(K, V) {
    sender, receiver := chans.Ranger!(keyValue!(K, V))()
    var f func(*node!(K, V)) bool
    f = func(n *node!(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue!(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Desde mi punto de vista personal, el código anterior es un orden de magnitud más fácil de leer. Creo que eso aclara todas las ambigüedades, tanto visualmente como para el analizador. Además, me pregunto si este puede ser el cambio más importante que se podría hacer en los Contratos.

Estilo de declaración

Al declarar tipos, funciones y métodos, hay menos "tiempo de ejecución o tiempo de compilación". problema. Un Gopher ve una línea que comienza con type o func y sabe que está mirando una declaración, no el comportamiento del programa.

Sin embargo, todavía existen algunas ambigüedades visuales:

// Type-parameterized function,
// or function with multiple return values?
func Draw(cvs canvas, t tool)(canvas, tool) {
    // ...
}
func Draw(type canvas, tool)(cvs canvas, t tool) {
    // ...
}

// Type-parameterized struct, or function call?
func Set(elem constructible) rect {
    // ...
}
type Set(type Elem comparable) struct{
    // ...
}

// Method call, or type-parameterized function?
func Map(type Element)(s []Element, f func(Element) Element) (results []Element) {
    // ...
}
func (t Element) Map(s []Element, f func(Element) Element) (results []Element) {
    // ...
}

Pensamientos:

  • Creo que estos temas son menos importantes que el problema de instanciación.
  • La solución más obvia sería cambiar los delimitadores utilizados para los argumentos de tipo.
  • Posiblemente poner algún otro tipo de operador o carácter allí ( ! podría perderse, ¿qué pasa con # ?) Podría desambiguar las cosas.

EDITAR: @griesemer ¡ gracias por la aclaración adicional!

Gracias. Solo para plantear la pregunta natural: ¿por qué es importante saber si una llamada en particular se evalúa en tiempo de ejecución o en tiempo de compilación? ¿Por qué es esa la pregunta clave?

@cajadeherramientas

// Instantiation with unexported types and then function call,
// or chained method call?
a := draw(square, ellipse)(canvas, color)

¿Por qué importaría de cualquier manera? Para un lector casual, no importaría si se tratara de una pieza de código que se ejecutó durante el tiempo de compilación o el tiempo de ejecución. Para todos los demás, solo pueden echar un vistazo a la definición de la función para saber qué está pasando. Sus ejemplos posteriores no parecen ser ambiguos en absoluto.

De hecho, usar () para parámetros de tipo tiene sentido, ya que parece que está llamando a una función que devuelve una función, y eso es más o menos correcto. La diferencia es que la primera función es aceptar tipos, que normalmente están en mayúsculas o son muy conocidos.

En esta etapa, es mucho más importante determinar las dimensiones del cobertizo, no su color.

No creo que de lo que habla @toolbox sea ​​realmente una diferencia entre el tiempo de compilación y el tiempo de ejecución. Sí, esa es una diferencia, pero no es la importante. La importante es: ¿es esta una llamada de función o una declaración de tipo? Quiere saber porque se comportan de manera diferente y no quiere tener que deducir si alguna expresión está haciendo dos llamadas de función o una, porque esa es una gran diferencia. Es decir, una expresión como a := draw(square, ellipse)(canvas, color) es ambigua sin trabajar para examinar el entorno circundante.

Es importante poder analizar visualmente el flujo de control del programa. Creo que Go ha sido un gran ejemplo de esto.

Gracias. Solo para plantear la pregunta natural: ¿por qué es importante saber si una llamada en particular se evalúa en tiempo de ejecución o en tiempo de compilación? ¿Por qué es esa la pregunta clave?

Lo siento, parece que me equivoqué en mi comunicación. Este es el punto clave que estaba tratando de transmitir:

no es obvio si está mirando tipos o flujo de programa

(Por el momento, uno se soluciona durante la compilación y el otro ocurre en el tiempo de ejecución, pero esas son... características, no el punto clave, que @infogulch tomó correctamente, ¡gracias!)


He visto la opinión en algunos lugares de que los genéricos en el borrador se pueden comparar con llamadas a funciones: es una especie de función en tiempo de compilación que devuelve la función o el tipo real . Si bien eso es útil como modelo mental de lo que ocurre durante la compilación, no se traduce sintácticamente. Sintácticamente, deberían ser nombradas como funciones. Aquí hay un ejemplo:

// Example from the Contracts draft
Print(int)([]int{1, 2, 3})

// New naming that communicates behavior and intent
MakePrintFunc(int)([]int{1, 2, 3}) // Chained function call, great!

Allí, eso en realidad parece una función que devuelve una función; Creo que eso es bastante legible.

Otra forma de hacerlo sería agregarle a todo el sufijo Type , por lo que queda claro por el nombre que cuando "llamas" a la función obtienes un tipo. De lo contrario, no es obvio que (por ejemplo) Pair(...) produzca un tipo de estructura en lugar de una estructura. Pero si esa convención está vigente, este código queda claro: a := drawType(square, ellipse)(canvas, color)

(Me doy cuenta de que un precedente es la convención "-er" para interfaces).

Tenga en cuenta que no apoyo particularmente lo anterior como una solución, solo estoy ilustrando cómo creo que "los genéricos como funciones" no se expresan de manera completa e inequívoca en la sintaxis actual.


De nuevo, @infogulch ha resumido muy bien mi punto. Estoy a favor de diferenciar visualmente los argumentos de tipo para que quede claro que son parte del tipo .

Tal vez la parte visual se vea mejorada por el resaltado de sintaxis del editor.

No sé mucho sobre analizadores y cómo no se puede hacer demasiada anticipación.

Desde la perspectiva de los usuarios, no quiero ver otro carácter en mi código, por lo que «» no obtendría mi soporte (¡no los encontré en mi teclado!).

Sin embargo, ver corchetes seguidos de corchetes tampoco es muy agradable a la vista.

¿Qué tal usar llaves simples?

a := draw{square, ellipse}(canvas, color)

Sin embargo, en Print(int)([]int{1,2,3}) la única diferencia de comportamiento es "tiempo de compilación frente a tiempo de ejecución". Sí, MakePrintFunc en lugar de Print enfatizaría más esta similitud, pero… ¿no es ese un argumento para no usar MakePrintFunc ? Porque en realidad oculta la verdadera diferencia de comportamiento.

FWIW, en todo caso, parece estar argumentando para usar diferentes separadores para funciones paramétricas y tipos paramétricos. Porque Print(int) en realidad se puede considerar como equivalente a una función que devuelve una función (evaluada en el momento de la compilación), mientras que Pair(int, string) no, es una función que devuelve un tipo . Print(int) en realidad es una expresión válida que se evalúa como un func , mientras que Pair(int, string) no es una expresión válida, es una especificación de tipo. Entonces, la diferencia real en el uso no es "funciones genéricas versus no genéricas", sino "funciones genéricas versus tipos genéricos". Y desde ese punto de vista, creo que hay un caso sólido para usar () al menos para funciones paramétricas de todos modos, porque enfatiza la naturaleza de las funciones paramétricas para representar valores, y tal vez deberíamos usar <> para tipos paramétricos.

Creo que el argumento para () para los tipos paramétricos proviene de la programación funcional, donde estos tipos de funciones que regresan son un concepto real llamado constructores de tipos y en realidad se pueden usar y referenciar como funciones. Y FWIW, esa es también la razón por la que no argumentaría que no use () para tipos paramétricos. Personalmente, me siento muy cómodo con este concepto y preferiría la ventaja de tener menos separadores diferentes que la ventaja de eliminar la ambigüedad de las funciones paramétricas de los tipos paramétricos. Después de todo, no tenemos ningún problema con los identificadores puros que se refieren a tipos o valores. .

No creo que de lo que habla @toolbox sea ​​realmente una diferencia entre el tiempo de compilación y el tiempo de ejecución. Sí, esa es una diferencia, pero no es la importante. La importante es: ¿es esta una llamada de función o una declaración de tipo? Usted _quiere_ saber porque se comportan de manera diferente y no quiere tener que deducir si alguna expresión está haciendo dos llamadas de función o una, porque esa es una gran diferencia. Es decir, una expresión como a := draw(square, ellipse)(canvas, color) es ambigua sin trabajar para examinar el entorno circundante.

Es importante poder analizar visualmente el flujo de control del programa. Creo que Go ha sido un gran ejemplo de esto.

Las declaraciones de tipo serían muy fáciles de ver, ya que todas comienzan con la palabra clave type . Su ejemplo obviamente no es uno de ellos.

Tal vez la parte visual se vea mejorada por el resaltado de sintaxis del editor.

Creo que, idealmente, la sintaxis debería ser clara sin importar de qué color sea. Ese ha sido el caso de Go, y no creo que sea bueno abandonar ese estándar.

¿Qué tal usar llaves simples?

Creo que esto, lamentablemente, entra en conflicto con una estructura literal.

Sin embargo, en Print(int)([]int{1,2,3}) la única diferencia de comportamiento es "tiempo de compilación frente a tiempo de ejecución". Sí, MakePrintFunc en lugar de Print enfatizaría más esta similitud, pero… ¿no es ese un argumento para no usar MakePrintFunc ? Porque en realidad oculta la verdadera diferencia de comportamiento.

Bueno, por un lado, esta es la razón por la que apoyaría Print!(int)([]int{1,2,3}) sobre MakePrintFunc(int)([]int{1,2,3}) . Es evidente que algo único está sucediendo.

Pero nuevamente, la pregunta que @ianlancetaylor hizo anteriormente: ¿por qué importa si el tipo instanciación/función-retorno-función es tiempo de compilación versus tiempo de ejecución?

Pensándolo bien, si escribió algunas llamadas a funciones y el compilador pudo optimizarlas y calcular su resultado en tiempo de compilación, ¡estaría feliz por la ganancia de rendimiento! Más bien, el aspecto importante es qué está haciendo el código, ¿cuál es el comportamiento? Eso debería ser obvio de un vistazo.

Cuando veo Print(...) mi primer instinto es "esa es una llamada de función que escribe en alguna parte". No comunica "esto devolverá una función". En mi opinión, cualquiera de estos es mejor porque puede comunicar el comportamiento y la intención:

  • MakePrintFunc(...)
  • Print!(...)
  • Print<...>

En otras palabras, este fragmento de código "hace referencia" o de alguna manera "me da" una función que ahora se puede llamar en el siguiente fragmento de código.

FWIW, en todo caso, parece estar argumentando para usar diferentes separadores para funciones paramétricas y tipos paramétricos. ...

No, sé que los últimos ejemplos han sido sobre funciones, pero recomendaría una sintaxis consistente para funciones paramétricas y tipos paramétricos. No creo que el equipo de Go agregue genéricos a Go a menos que sean un concepto unificado con una sintaxis unificada.

Cuando veo Print(...) mi primer instinto es "esa es una llamada de función que escribe en alguna parte". No comunica "esto devolverá una función".

Tampoco func Print(…) func(…) , cuando se llama como Print(…) . Sin embargo, estamos colectivamente bien con eso. Sin una sintaxis de llamada especial, si una función devuelve func .
La sintaxis Print(…) le dice exactamente lo que hace hoy: que Print es una función que devuelve algún valor, que es lo que se evalúa como Print(…) . Si está interesado en el tipo que devuelve la función, mire su definición.
O, mucho más probablemente, use el hecho de que en realidad es Print(…)(…) como indicador de que devuelve una función.

Pensándolo bien, si escribió algunas llamadas a funciones y el compilador pudo optimizarlas y calcular su resultado en tiempo de compilación, ¡estaría feliz por la ganancia de rendimiento!

Seguro. Ya tenemos eso. Y estoy muy contento de que no necesito anotaciones sintácticas específicas para hacerlos especiales, pero puedo confiar en que el compilador proporcionará heurísticas de mejora continua sobre qué funciones son.

En mi opinión, cualquiera de estos es mejor porque puede comunicar el comportamiento y la intención:

Tenga en cuenta que el primero al menos es 100% compatible con el diseño. No prescribe ninguna forma para los identificadores utilizados y espero que no sugiera que lo prescriba (y si lo hace, me interesaría saber por qué no se aplican las mismas reglas para devolver func ).

No, sé que los últimos ejemplos han sido sobre funciones, pero recomendaría una sintaxis consistente para funciones paramétricas y tipos paramétricos.

Bueno, estoy de acuerdo, como dije :) Solo digo que no entiendo cómo los argumentos que está presentando pueden aplicarse a lo largo del eje "genérico versus no genérico", ya que no hay cambios de comportamiento importantes entre los dos. Tendrían sentido a lo largo del eje "tipo frente a función", porque si algo es una especificación de tipo o una expresión es muy importante para el contexto en el que se puede usar. Todavía no estaría de acuerdo, pero al menos entendería ellos :)

@Merovius gracias por tu comentario.

Tampoco func Print(…) func(…) , cuando se llama como Print(…) . Sin embargo, estamos colectivamente bien con eso. Sin una sintaxis de llamada especial, si una función devuelve un func.
La sintaxis Print(…) le dice exactamente lo que hace hoy: que Print es una función que devuelve algún valor, que es lo que se evalúa como Print(…) . Si está interesado en el tipo que devuelve la función, mire su definición.

Sostengo la opinión de que el nombre de una función debe estar relacionado con lo que hace. Por lo tanto, espero que Print(...) imprima algo, independientemente de lo que devuelva. Creo que esta es una expectativa razonable y que podría cumplirse en la mayoría del código Go existente.

Si veo Print(...)(...) , comunica que el primer () ha impreso algo y que la función ha devuelto una función de algún tipo, y el segundo () está ejecutando ese comportamiento adicional .

(Me sorprendería si esta fuera una opinión inusual o rara, pero no discutiría con algunos resultados de la encuesta).

Tenga en cuenta que el primero al menos es 100% compatible con el diseño. No prescribe ninguna forma para los identificadores utilizados y espero que no sugiera que lo prescriba (y si lo hace, me interesaría saber por qué no se aplican las mismas reglas para devolver una función).

Tienes toda la razón, lo sugerí :)

Mire, enumeré las 3 formas en las que podría pensar para corregir la ambigüedad visual introducida por los parámetros de tipo en funciones y tipos. Si no ve ninguna ambigüedad, entonces no le gustará ninguna de las sugerencias.

Solo digo que no entiendo cómo se pueden aplicar los argumentos que está presentando a lo largo del eje "genérico versus no genérico", ya que no hay cambios de comportamiento importantes entre los dos. Tendrían sentido a lo largo del eje "tipo frente a función", porque si algo es una especificación de tipo o una expresión es muy importante para el contexto en el que se puede usar.

Consulte los puntos anteriores sobre la ambigüedad y las 3 soluciones propuestas.

Los parámetros de tipo son algo nuevo.

  • Si queremos razonar sobre ellos como algo nuevo, propongo cambiar los delimitadores o agregar un operador de creación de instancias para diferenciarlos completamente del código normal: llamadas a funciones, conversiones de tipo, etc.
  • Si queremos razonar sobre ellas como una función más , propongo nombrarlas claramente, de modo que identifier en identifier(...) comunique el comportamiento y el valor de retorno.

Prefiero lo primero. En ambos casos, los cambios serían globales en la sintaxis del parámetro de tipo, como se explicó.

Hay un par de otras maneras de arrojar luz sobre esto:

  1. Encuesta
  2. Tutorial

1. Encuesta

Prefacio: Esto no es una democracia. Creo que las decisiones se basan en datos, y tanto la lógica articulada como los datos de encuestas amplias pueden ayudar en el proceso de decisión.

No tengo los medios para hacer esto, pero me interesaría saber qué sucedería si encuestaras a unos miles de Gophers en "clasificar estos por claridad".

Base:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map(K, V) {
    return &Map(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator(K, V) {
    sender, receiver := chans.Ranger(keyValue(K, V))()
    var f func(*node(K, V)) bool
    f = func(n *node(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Operador de instanciación:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map!(K, V) {
    return &Map!(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator!(K, V) {
    sender, receiver := chans.Ranger!(keyValue!(K, V))()
    var f func(*node!(K, V)) bool
    f = func(n *node!(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue!(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Tirantes de ángulo: (o tirantes de ángulo doble, de cualquier manera)

// Lifted from the design draft
func New<type K, V>(compare func(K, K) int) *Map<K, V> {
    return &Map<K, V>{compare: compare}
}

// ...

func (m *Map<K, V>) InOrder() *Iterator<K, V> {
    sender, receiver := chans.Ranger<keyValue<K, V>>()
    var f func(*node<K, V>) bool
    f = func(n *node<K, V>) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue<K, V>{n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Funciones apropiadamente nombradas:

// Lifted from the design draft
func NewConstructor(type K, V)(compare func(K, K) int) *MapType(K, V) {
    return &MapType(K, V){compare: compare}
}

// ...

func (m *MapType(K, V)) InOrder() *IteratorType(K, V) {
    sender, receiver := chans.RangerType(keyValueType(K, V))()
    var f func(*nodeType(K, V)) bool
    f = func(n *nodeType(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValueType(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

... Gracioso, en realidad me gusta bastante el último.

(¿Cómo crees que les iría a estos en el amplio mundo de Gophers @Merovius ?)

2. Tutoría

Creo que este sería un ejercicio muy útil: escriba un tutorial para principiantes sobre su sintaxis favorita y pídale a algunas personas que lo lean y lo apliquen. ¿Con qué facilidad se comunican los conceptos? ¿Qué son las preguntas frecuentes y cómo las respondes?

El borrador del diseño está destinado a comunicar el concepto a Gophers experimentados. Sigue la cadena de la lógica, sumergiéndote lentamente. ¿Cuál es la versión concisa? ¿Cómo explica las Reglas de oro de los contratos en una publicación de blog fácilmente asimilable?

Esto podría presentar una especie de ángulo o porción de datos diferente a los informes de retroalimentación típicos.

@toolbox Creo que lo que aún no ha respondido es: ¿Por qué es esto un problema para las funciones paramétricas, pero no para las funciones no paramétricas que devuelven un func ? Puedo, hoy, escribir

func Print(a string) func(string) {
    return func(b string) {
        fmt.Println(a+b)
    }
}

func main() {
    Print("foo")("bar")
}

¿Por qué está bien y no te lleva a estar súper confundido por la ambigüedad, pero tan pronto como Print toma un parámetro de tipo en lugar de un parámetro de valor, esto se vuelve insoportable? Y (dejando de lado las preguntas obvias de compatibilidad) ¿también sugeriría que agreguemos una restricción para que funcione correctamente, que esto no debería ser posible, a menos que Print se cambie el nombre a MakeXFunc para algunos X ? ¿Si no, porque no?

@toolbox , ¿sería esto realmente un problema cuando se supone que la inferencia de tipos podría eliminar la necesidad de especificar los tipos paramétricos para las funciones, dejando solo una llamada de función de aspecto simple?

@Merovius No creo que el problema sea con la sintaxis Print("foo")("bar") en sí misma, porque ya es posible en Go 1, precisamente porque tiene una sola interpretación posible . El problema es que con la propuesta sin modificar, la expresión Foo(X)(Y) ahora es ambigua y podría significar que está realizando dos llamadas de función (como en Go 1), o podría significar que está realizando una llamada de función con argumentos de tipo . El problema es poder deducir localmente lo que hace el programa, y ​​esas dos posibles interpretaciones semánticas son muy diferentes .

@urandom Estoy de acuerdo en que la inferencia de tipos puede eliminar la mayor parte de los parámetros de tipo proporcionados explícitamente, pero no creo que sea una buena idea empujar toda la complejidad cognitiva a los rincones oscuros del lenguaje solo porque rara vez se usan. cualquiera. Incluso si es lo suficientemente raro como para que la mayoría de las personas no los encuentren, aún lo encontrarán a veces, y permitir que algún código tenga un flujo de control confuso siempre que no sea "la mayoría" del código deja un mal sabor de boca. Especialmente porque Go es actualmente tan accesible cuando lee código de "plomería", incluido stdlib. Tal vez la inferencia de tipos sea tan buena que "raro" se convierta en "nunca", y los programadores de Go sigan siendo muy disciplinados y nunca diseñen un sistema en el que los parámetros de tipo sean necesarios; entonces todo este asunto es básicamente discutible. Pero yo no apostaría por ello.

Creo que el objetivo principal del argumento de @tooolbox es que no deberíamos sobrecargar alegremente la sintaxis existente con semántica sensible al contexto y, en cambio, deberíamos encontrar otra sintaxis que no sea ambigua (incluso si solo se trata de hacer una pequeña adición como Foo(X)!(Y) .) Creo que esta es una medida importante al considerar las opciones de sintaxis.

Utilicé y leí un poco de código D , en aquellos días (~2008-2009), y debo decir que el ! siempre me hacía tropezar.

déjame pintar este cobertizo con # , $ o @ , en su lugar (ya que no tienen ningún significado en Go o C).
esto podría abrir la posibilidad de usar llaves sin confusión con mapas, cortes o estructuras.

  • Foo@{X}(Y)
  • Foo${X}(Y)
  • Foo#{X}(Y)
    o corchetes.

En discusiones como esta, es esencial observar el código real.

Por ejemplo, considere que pocas personas escriben Foo(X)(Y) . En Go, los nombres de tipo y los nombres de variables y funciones se ven exactamente iguales, pero las personas rara vez se confunden acerca de lo que están viendo. La gente entiende que int64(v) es una conversión de tipo y F(v) es una llamada de función, aunque se ven exactamente iguales.

Necesitamos mirar el código real para ver si los argumentos de tipo son realmente confusos en la práctica. Si lo son, entonces debemos ajustar la sintaxis. En ausencia de código real, simplemente no lo sabemos.

El miércoles 6 de mayo de 2020 a las 13:00, Ian Lance Taylor escribió:

La gente entiende que int64(v) es una conversión de tipo y F(v) es una
llamada de función, aunque se vean exactamente iguales.

No tengo una opinión de una forma u otra en este momento sobre la propuesta.
sintaxis, pero no creo que este ejemplo en particular sea muy bueno. Puede
ser cierto para los tipos incorporados, pero en realidad me he confundido con esto
problema exacto varias veces yo mismo (estaba buscando una función
definición y estar muy confundido acerca de cómo funcionaba el código antes
Me di cuenta de que probablemente era un tipo y no pude encontrar la función porque
no fue una llamada de función en absoluto). No es el fin del mundo, y
probablemente no sea un problema en absoluto para las personas a las que les gustan los IDE sofisticados, pero he
desperdicié 5 minutos más o menos buscando esto varias veces.

—Sam

--
sam blanqueado

@ianlancetaylor una cosa que noté con su ejemplo es que puede escribir una función que toma un tipo y devuelve otro tipo con el mismo significado, por lo que llamar a un tipo como una conversión de tipo básica como int64(v) tiene sentido en el de la misma manera que strconv.Atoi(v) tiene sentido.

Pero si bien puede hacer UseConverter(strconv.Atoi) , UseConverter(int64) no es posible en Go 1. Tener el paréntesis para el parámetro de tipo podría abrir algunas posibilidades si el genérico se puede usar para la conversión como:

func StrToNumber(type K)(s string) K {
  asInt := strconb.Atoi(s)
  return K(asInt)
}

¿Por qué está bien y no te lleva a estar súper confundido por la ambigüedad?

Tu ejemplo no está bien. No me importa si la primera llamada toma argumentos o escribe parámetros. Tiene una función Print que no imprime nada. ¿Te imaginas leer/revisar ese código? Print("foo") con el segundo conjunto de paréntesis omitido se ve bien, pero en secreto no funciona.

Si me envió ese código en un PR, le diría que cambie el nombre a PrintFunc o MakePrintFunc o PrintPlusFunc o algo que comunique su comportamiento.

Utilicé y leí un poco de código D, en aquellos días (~ 2008-2009), ¡y debo decir que ! siempre me estaba haciendo tropezar.

Ja, interesante. No tengo ninguna preferencia particular por un operador de creación de instancias; esas parecen opciones decentes.

En Go, los nombres de tipo y los nombres de variables y funciones se ven exactamente iguales, pero las personas rara vez se confunden acerca de lo que están viendo. La gente entiende que int64(v) es una conversión de tipo y F(v) es una llamada de función, aunque se ven exactamente iguales.

Estoy de acuerdo, las personas generalmente pueden diferenciar rápidamente entre conversiones de tipos y llamadas a funciones. ¿Por qué crees que es?

Mi teoría personal es que los tipos suelen ser sustantivos y las funciones suelen ser verbos. Entonces, cuando ve Noun(...) , es bastante claro que es una conversión de tipo, y cuando ve Verb(...) , es una llamada de función.

Necesitamos mirar el código real para ver si los argumentos de tipo son realmente confusos en la práctica. Si lo son, entonces debemos ajustar la sintaxis. En ausencia de código real, simplemente no lo sabemos.

Eso tiene sentido.

Personalmente, llegué a este hilo porque leí el borrador de Contratos (probablemente 5 veces, cada vez rebotando y luego avanzando cuando volví más tarde) y encontré que la sintaxis era confusa y desconocida. Me gustaron los conceptos cuando finalmente los asimilé, pero había una gran barrera debido a la sintaxis ambigua.

Hay mucho "código real" en la parte inferior del borrador de Contratos, que maneja todos esos casos de uso común, ¡lo cual es genial! Sin embargo, me resulta complicado analizar visualmente; Soy más lento leyendo y entendiendo el código. Me parece que tengo que mirar los argumentos de las cosas y el contexto más amplio para saber qué son las cosas y cuál es el flujo de control, y parece que eso es un paso por debajo del código normal.

Tomemos este código real:

import "container/orderedmap"

var m = orderedmap.New(string, string)(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

Cuando leo orderedmap.New( , espero que lo que sigue sean los argumentos para la función New , esas piezas clave de información que el mapa ordenado necesita para funcionar. Pero esos están en realidad en el segundo paréntesis. Estoy tirado por esto. Hace que el código sea más difícil de asimilar.

(Este es solo un ejemplo, no todo lo que veo es ambiguo, pero es difícil tener una discusión detallada sobre un amplio conjunto de puntos).

Esto es lo que sugeriría:

// Instantiation operator
var m = orderedmap.New!(string, string)(strings.Compare)
// Alternate delimiters -- notice I don't insist on any particular kind
var m = orderedmap.New<|string, string|>(strings.Compare)
// Appropriately named function
var m = orderedmap.MakeConstructor(string, string)(strings.Compare)

En los primeros dos ejemplos, una sintaxis diferente sirve para romper mi suposición de que el primer conjunto de paréntesis contiene los argumentos para New() , por lo que el código es menos sorprendente y el flujo es más observable desde un nivel alto.

La tercera opción usa nombres para que el flujo no sorprenda. Ahora espero que el primer conjunto de paréntesis contenga los argumentos necesarios para crear una función constructora y espero que el valor de retorno sea una función constructora que a su vez se puede llamar para producir un mapa ordenado.


Seguro que puedo leer el código en el estilo actual. Pude leer todo el código en el borrador de Contratos. Simplemente es más lento porque me lleva más tiempo procesarlo. Hice todo lo posible para analizar por qué ocurre esto e informarlo: además del ejemplo orderedmap.New , https://github.com/golang/go/issues/15292#issuecomment -623649521 tiene un buen resumen , aunque probablemente podría pensar en más. El grado de ambigüedad varía entre los diferentes ejemplos.

Reconozco que no obtendré el acuerdo de todos, porque la legibilidad y la claridad son un tanto subjetivas y tal vez influenciadas por los antecedentes y los idiomas favoritos de la persona. Sin embargo, creo que 4 tipos de ambigüedades de análisis son un buen indicador de que tenemos un problema.

import "container/orderedmap"

var m = orderedmap.NewOf(string, string)(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

Creo que NewOf se lee mejor que New porque New generalmente devuelve una instancia, no un genérico que crea una instancia.


Tiene una función Print que no imprime nada.

Para ser claros, dado que hay alguna inferencia de tipo automática, Print(foo) genérico sería una llamada de impresión real a través de una inferencia o un error. En Go hoy, no se permiten identificadores desnudos:

package main

import (
    "fmt"
)

func main() {
    fmt.Println
}

./prog.go:8:5: fmt.Println evaluated but not used

Me pregunto si hay alguna forma de hacer que la inferencia genérica sea menos confusa.

@cajadeherramientas

Tu ejemplo no está bien. No me importa si la primera llamada toma argumentos o escribe parámetros. Tiene una función Imprimir que no imprime nada. ¿Te imaginas leer/revisar ese código?

Ha omitido las preguntas de seguimiento pertinentes aquí. Estoy de acuerdo contigo en que no es realmente legible. Pero está abogando por una aplicación de esta restricción a nivel de idioma. No estaba diciendo "estás de acuerdo con esto" que significa "estás de acuerdo con este código", sino "estás de acuerdo con el idioma que permite ese código".

¿Cuál fue mi pregunta de seguimiento? ¿Crees que Go es un lenguaje peor, porque no puso una restricción de nombre para las funciones que devuelven func ? Si no, ¿por qué sería un lenguaje peor si no impusiéramos esa restricción a tales funciones, cuando toman un argumento de tipo en lugar de un argumento de valor?

@Merovius

Pero está abogando por una aplicación de esta restricción a nivel de idioma.

No, está argumentando que confiar en los estándares de nombres es una posible solución válida al problema. Una regla informal como "se alienta a los autores de tipos a nombrar sus tipos genéricos de una manera que sea menos fácil de confundir con el nombre de una función" es una solución válida al problema de la ambigüedad, ya que literalmente resolvería el problema en casos individuales.

No insinúa en ninguna parte que esta solución deba ser aplicada por el lenguaje, dice que si los mantenedores deciden mantener la propuesta actual tal como está, incluso entonces existen posibles soluciones prácticas para el problema de la ambigüedad. Y afirma que el problema de la ambigüedad es real e importante de considerar.

Editar: Creo que nos estamos desviando un poco del rumbo. Creo que más código de ejemplo "real" sería muy beneficioso para la conversación en este punto.

No, está argumentando que confiar en los estándares de nombres es una posible solución válida al problema.

¿Son ellos? Traté de preguntar específicamente:

Tenga en cuenta que el primero al menos es 100% compatible con el diseño. No prescribe ninguna forma para los identificadores utilizados y espero que no sugiera que lo prescriba (y si lo hace, me interesaría saber por qué no se aplican las mismas reglas para devolver una función).

Tienes toda la razón, lo sugerí :)

Estoy de acuerdo en que "prescribir" no es extremadamente específico aquí, pero esa es al menos la pregunta que pretendía. Si de hecho no están argumentando a favor de un requisito de nivel de idioma integrado en el diseño, me disculpo por el malentendido, por supuesto. Pero me siento justificado al suponer que "prescribir" es al menos más fuerte que "una regla informal", al menos. Especialmente si se colocan en el contexto de las otras dos sugerencias que presentan (en la misma base) que son construcciones a nivel de idioma, ya que ni siquiera usan identificadores válidos actualmente.

¿Habrá un plan similar a vgo para permitir que la comunidad pruebe la última propuesta genérica?

Después de jugar un poco con el patio de juegos habilitado por contrato, realmente no veo de qué se trata todo este alboroto sobre la necesidad de diferenciar entre los argumentos de tipo y los regulares.

Considere este ejemplo . Dejé los inicializadores de tipo en todas las funciones, aunque podía omitirlos todos y aun así compilaría bien. Esto parece indicar que la gran mayoría de dicho código potencial ni siquiera los incluiría, lo que a su vez no causaría ninguna confusión.

En caso de que se incluyan estos parámetros de tipo, sin embargo, se pueden hacer ciertas observaciones:
a) los tipos son los incorporados, que todos conocen y pueden identificar de inmediato
b) los tipos son de terceros, y en ese caso serán TitleCased, lo que los haría destacar bastante. Sí, sería posible, aunque poco probable, que pudiera ser una función que devuelve otra función, y la primera llamada consume variables exportadas de terceros, pero creo que esto es extremadamente raro.
c) los tipos son algunos tipos privados. En este caso, se verían más como identificadores de variables regulares. Sin embargo, dado que no se exportan, esto significaría que el código que está viendo el lector no es parte de alguna documentación que está tratando de descifrar y, lo que es más importante, ya está leyendo el código. Por lo tanto, pueden hacer el paso adicional y simplemente saltar a la definición de la función para eliminar cualquier ambigüedad.

El alboroto es sobre cómo se ve sin genéricos https://play.golang.org/p/7BRdM2S5dwQ y para alguien que es nuevo en la programación de una pila separada para cada tipo como StackString, StackInt, ... es mucho más fácil de programar luego un Stack(T) en la propuesta de sintaxis genérica actual. No tengo ninguna duda de que la propuesta actual está bien pensada, como lo muestra su ejemplo, pero el valor de la simplicidad y la claridad disminuye mucho. Entiendo que la primera prioridad es averiguar si funciona mediante pruebas, pero una vez que acordamos que la propuesta actual cubre la mayoría de los casos y no hay dificultades técnicas del compilador, una prioridad aún mayor es hacerlo comprensible para todos, que siempre fue la razón número uno de Vaya éxito desde el principio.

@Merovius No, es como dijo @infogulch , me refiero a crear una convención al estilo -er en las interfaces. Lo mencioné arriba, perdón por la confusión. (Soy un "él" por cierto.)

Considere este ejemplo. Dejé los inicializadores de tipo en todas las funciones, aunque podía omitirlos todos y aun así compilaría bien. Esto parece indicar que la gran mayoría de dicho código potencial ni siquiera los incluiría, lo que a su vez no causaría ninguna confusión.

¿Qué tal el mismo ejemplo en una versión bifurcada del patio de recreo genérico?

Usé ::<> para la cláusula de parámetro de tipo, y si hay un solo tipo, puede omitir <> . No debería haber ninguna ambigüedad en el analizador en las llaves angulares, y me facilita leer el código, tanto los genéricos como el código que usa los genéricos. (Y si se infieren los parámetros de tipo, tanto mejor).

Como dije antes, no estaba atascado en ! para la creación de instancias de tipo (y creo que :: se ve mejor después de la revisión). Y solo ayuda donde se usan los genéricos, no tanto en las declaraciones. Así que esto combina un poco los dos, omitiendo <> cuando no es necesario, algo así como omitir encerrar () para los parámetros de retorno de la función si solo hay uno.

Extracto de muestra:

type Stack::<type E> []E

func (s Stack::E) Peek() E {
    return s[len(s)-1]
}

func (s *Stack::E) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack::E) Push(value E) {
    *s = append(*s, value)
}

type StackIterator::<type E> struct{
    stack Stack::E
    current int
}

func (s *Stack::E) Iter() Iterator::E {
    it := StackIterator::E{stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator::E) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator::E) Value() E { 
    if i.current < 0 {
        var zero E
        return zero
    }

    return i.stack[i.current]
}

// ...

var it Iterator::string = stack.Iter()

it = Filter::string(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::<string, string>(it, func(s string) string {
    return s + ":1"
})

it = Distinct::string(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

Para este ejemplo, también ajusté los nombres de las variables, creo que E para "Elemento" es más legible que T para "Tipo".

Como dije, al hacer que las cosas genéricas se vean diferentes, el código Go subyacente se vuelve visible. Sabes lo que estás viendo, el flujo de control es obvio, no hay ambigüedad, etc.

También está bien con más inferencia de tipos:

var it Iterator::string = stack.Iter()

it = Filter(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::<string, string>(it, func(s string) string {
    return s + ":1"
})

it = Distinct(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

@tooolbox Disculpas, entonces, estábamos hablando entre nosotros :)

alguien que es nuevo en la programación de una pila separada para cada tipo, como StackString, StackInt, ... es mucho más fácil de programar que una pila (T)

Realmente me sorprendería si ese fuera el caso. Nadie es infalible, y el primer error que se cuele incluso en una simple pieza de código, a la larga, demostrará cuán incorrecta es esa declaración.

El punto de mi ejemplo era ilustrar el uso de funciones paramétricas y su creación de instancias con tipos concretos, que es el quid de esta discusión, no si la implementación Stack muestra era buena o no.

El objetivo de mi ejemplo era ilustrar el uso de funciones paramétricas y su creación de instancias con tipos concretos, que es el quid de esta discusión, no si la implementación de Stack de muestra era buena o no.

No creo que @gertcuykens tuviera la intención de arruinar su implementación de Stack, parece que sintió que la sintaxis de los genéricos no es familiar y es difícil de entender.

En caso de que se incluyan estos parámetros de tipo, sin embargo, se pueden hacer ciertas observaciones:
(a B C D)...

Veo todos sus puntos, aprecio su análisis y no están equivocados. Tiene razón en que, en la mayoría de los casos, al examinar el código de cerca, puede determinar qué está haciendo. No creo que eso refute los informes de los desarrolladores de Go que dicen que la sintaxis es confusa, ambigua o que les lleva más tiempo leerla, incluso si finalmente pueden leerla.

En general, la sintaxis se encuentra en un valle inquietante. El código está haciendo algo diferente, pero se parece lo suficiente a las construcciones existentes como para que sus expectativas se desvanezcan y la visibilidad disminuya. Tampoco puede establecer nuevas expectativas porque (apropiadamente) estos elementos son opcionales, tanto en su conjunto como en partes.

Para esos casos patológicos más específicos, @infogulch lo expresó bien:

Tampoco creo que empujar toda la complejidad cognitiva a los rincones oscuros del idioma solo porque rara vez se usan sea una buena idea. Incluso si es lo suficientemente raro como para que la mayoría de las personas no los encuentren, lo encontrarán a veces, y permitir que algún código tenga un flujo de control confuso siempre que no sea "la mayoría" del código deja un mal sabor de boca.

Creo que, en este punto, estamos alcanzando la saturación de la articulación en esta porción particular del tema. No importa cuánto hablemos al respecto, la prueba de fuego será qué tan rápido y qué tan bien los desarrolladores de Go pueden aprenderlo, leerlo y escribirlo.

(Y sí, antes de que se señale, la carga debe recaer en el autor de la biblioteca, no en el desarrollador del cliente, pero no creo que queramos el efecto Boost donde las bibliotecas genéricas son ininteligibles para el hombre de la calle. Yo tampoco. No quiero que Go se convierta en un Jamboree genérico, pero en parte confío en que las omisiones del diseño limitarán la omnipresencia ).

Tenemos un patio de recreo y podemos hacer bifurcaciones para otras sintaxis , lo cual es fantástico. ¡Tal vez necesitemos aún más herramientas!

La gente ha dado su opinión . Estoy seguro de que se necesitan más comentarios, y tal vez necesitemos sistemas de comentarios mejores o más optimizados.

@toolbox ¿Crees que es posible analizar el código cuando siempre omites <> y type así? Tal vez requiera una propuesta más estricta en lo que se puede hacer, pero ¿tal vez valga la pena el intercambio?

type Stack::E []E

func (s Stack::E) Peek() E {
    return s[len(s)-1]
}

func (s *Stack::E) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack::E) Push(value E) {
    *s = append(*s, value)
}

type StackIterator::E struct{
    stack Stack::E
    current int
}

func (s *Stack::E) Iter() Iterator::E {
    it := StackIterator::E{stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator::E) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator::E) Value() E { 
    if i.current < 0 {
        var zero E
        return zero
    }

    return i.stack[i.current]
}

// ...

var it Iterator::string = stack.Iter()

it = Filter::string(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::string, string (it, func(s string) string {
    return s + ":1"
})

it = Distinct::string(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

No sé por qué, pero este Map::string, string (... se siente raro. Parece que esto crea 2 tokens, un Map::string y una llamada de función string .

Además, aunque esto no se usa en Go, usar "Identificador :: Identificador" podría dar una impresión equivocada a los usuarios primerizos, pensando que hay una clase/espacio de nombres Filter con un string Función

¿Crees que es posible analizar el código cuando siempre omites <> y escribes así? Tal vez requiera una propuesta más estricta en lo que se puede hacer, pero ¿tal vez valga la pena el intercambio?

No, no lo creo. Estoy de acuerdo con @urandom en que el carácter de espacio, sin nada que lo encierre, hace que parezcan dos tokens. Personalmente, también me gusta el alcance de los Contratos y no estoy interesado en cambiar sus capacidades.

Además, aunque esto no se usa en Go, el uso de "Identificador::Identificador" podría dar una impresión equivocada a los usuarios primerizos, pensando que hay una clase de filtro/espacio de nombres con una función de cadena en él. Reutilizar tokens de otros lenguajes ampliamente adoptados para algo completamente diferente causará mucha confusión.

En realidad, no he usado un idioma con :: pero lo he visto por ahí. Tal vez ! sea ​​mejor porque coincidiría con D, aunque encuentro que :: se ve mejor visualmente.

Si tuviéramos que seguir este camino, puede haber mucha discusión sobre qué personajes usar específicamente. Aquí hay un intento de reducir lo que estamos buscando:

  • Algo más que solo identifier() para que no parezca una llamada de función.
  • Algo que pueda encerrar varios parámetros de tipo, para unirlos visualmente de la forma en que lo hacen los paréntesis.
  • Algo que parece conectado al identificador, por lo que parece una unidad.
  • Algo que no sea ambiguo para el analizador.
  • Algo que no entre en conflicto con un concepto diferente que tenga una sólida mentalidad de desarrollador.
  • Si es posible, algo que afectará tanto las definiciones como los usos de los genéricos, para que también sean más fáciles de leer.

Hay muchas cosas que podrían encajar.

  • identifier!(a, b) ( parque infantil )
  • identifier@(a, b)
  • identifier#(a, b)
  • identifier$(a, b)
  • identifier<:a, b:>
  • identifier.<a, b> ¡es como una afirmación de tipo!
  • identifier:<a, b>
  • etc

¿Alguien tiene alguna idea sobre cómo reducir aún más el conjunto de potenciales?

Solo una nota rápida de que hemos considerado todas esas ideas, y también hemos considerado ideas como

func F(T : a, b T) { }
func G() { F(int : 1, 2) }

Pero, de nuevo, la prueba del budín está en comerlo. Vale la pena tener discusiones abstractas en ausencia de código, pero no conducen a conclusiones definitivas.

(No estoy seguro de si se ha hablado de esto antes) Veo que en los casos en que recibimos una estructura, no podremos "extender" una API existente para manejar tipos genéricos sin romper el código de llamada existente.

Por ejemplo, dada esta función no genérica

func Repeat(v, n int) []int {
    var r []int
    for i := n; i > 0; i-- {
        r = append(r, v)
    }
    return r
}

Repeat(4, 4)

Podemos hacerlo genérico sin romper la compatibilidad con versiones anteriores.

func Repeat(type T)(v T, n int) []T {
    var r []T
    for i := n; i > 0; i-- {
        r = append(r, v)
    }
    return r
}

Repeat("a", 5)

Pero si queremos hacer lo mismo con una función que recibe un struct genérico

type XY struct {
    X, Y int
}

func RangeRepeat(arr []XY) []int {
    var r []int
    for _, n := range arr {
        for i := n.Y; i > 0; i-- {
            r = append(r, n.X)
        }
    }
    return r
}

RangeRepeat([]XY{{1, 1}, {2, 2}, {3, 3}})

parece que el código de llamada necesita ser actualizado

type XY(type T) struct {
    X T
    Y int
}

func RangeRepeat(type T)(arr []XY(T)) []T {
    var r []T
    for _, n := range arr {
        for i := n.Y; i > 0; i-- {
            r = append(r, n.X)
        }
    }
    return r
}

// error: cannot use generic type XY(type T any) without instantiation
// RangeRepeat([]XY{{1, 1}, {2, 2}, {3, 3}}) // error in old code
RangeRepeat([](XY(int)){{1, 1}, {2, 2}, {3, 3}}) // API changed
// RangeRepeat([]XY{{"1", 1}, {"2", 2}, {"3", 3}}) // error
RangeRepeat([](XY(string)){{"1", 1}, {"2", 2}, {"3", 3}}) // ok

Sería increíble poder derivar tipos de estructuras también.

@ianlancetaylor

El borrador del contrato menciona que methods may not take additional type arguments . Sin embargo, no se menciona la sustitución del contrato por métodos particulares. Esta función sería muy útil para implementar interfaces según el contrato al que esté vinculado un tipo paramétrico.

¿Ha discutido tal posibilidad?

Otra pregunta para el borrador del contrato. ¿Se restringirán las disyunciones de tipos a los tipos integrados? Si no, ¿sería posible usar tipos parametrizados, especialmente interfaces en la lista de disyunción?

Algo como

type Getter(T) interface {
    Get() T
}

contract(G, T) {
    G Getter(T)
}

sería bastante útil, no solo para evitar duplicar el conjunto de métodos desde la interfaz hasta el contrato, sino también para crear instancias de un tipo parametrizado cuando falla la inferencia de tipos y no tiene acceso al tipo concreto (por ejemplo, no se exporta)

@ianlancetaylor No estoy seguro de si esto se ha discutido antes, pero con respecto a la sintaxis de los argumentos de tipo para una función, ¿es posible concatenar la lista de argumentos con la lista de argumentos de tipo? Entonces, para el ejemplo del gráfico, en lugar de

var g = graph.New(*Vertex, *FromTo)([]*Vertex{ ... })

usarías

var g = graph.New(*Vertex, *FromTo, []*Vertex{ ... })

Esencialmente, los primeros K argumentos de la lista de argumentos corresponden a una lista de argumentos de tipo de longitud K. El resto de la lista de argumentos corresponde a los argumentos regulares de la función. Esto tiene la ventaja de reflejar la sintaxis de

make(Type, size)

que toma un Tipo como primer argumento.

Esto simplificaría la gramática, pero necesita información de tipo para saber dónde terminan los argumentos de tipo y comienzan los argumentos regulares.

@ smasher164 Dijo algunos comentarios que lo consideraron (lo que implica que lo descartaron, aunque tengo curiosidad por qué).

func F(T : a, b T) { }
func G() { F(int : 1, 2) }

Eso es lo que está sugiriendo, pero con dos puntos para separar los dos tipos de argumentos. Personalmente me gusta moderadamente, aunque es un cuadro incompleto; ¿Qué pasa con la declaración de tipos, métodos, creación de instancias, etc.

Quiero volver a algo que dijo @Inuart :

Podemos hacerlo genérico sin romper la compatibilidad con versiones anteriores.

¿Consideraría el equipo de Go cambiar la biblioteca estándar de esta manera para ser coherente con la garantía de compatibilidad de Go 1? Por ejemplo, ¿qué sucede si strings.Repeat(s string, count int) string se reemplazó con Repeat(type S stringlike)(s S, count int) S ? También puede agregar un comentario //Deprecated a bytes.Repeat pero déjelo allí para que lo use el código heredado. ¿Es eso algo que el equipo de Go consideraría?

Editar: para ser claros, quiero decir, ¿se consideraría esto dentro de Go1Compat en general? Ignora el ejemplo específico si no te gusta.

@carlmjohnson No. Este código se rompería: f := strings.Repeat , ya que no se puede hacer referencia a las funciones polimórficas sin instanciarlas primero.

Y partiendo de ahí, creo que la concatenación de argumentos de tipo y argumentos de valor sería un error, ya que impide una sintaxis natural para referirse a una versión instanciada de una función. Sería más natural si ya tuviera curry, pero no lo tiene. Parece extraño que foo(int, 42) y foo(int) sean expresiones y que ambas tengan tipos muy diferentes.

@urandom Sí, hemos discutido la posibilidad de agregar restricciones adicionales en los parámetros de tipo de un método individual. Eso haría que el conjunto de métodos del tipo parametrizado variara según los argumentos de tipo. Esto puede ser útil o confuso, pero una cosa parece segura: podemos agregarlo más tarde sin romper nada. Así que hemos pospuesto la idea para más adelante. Gracias por sacar el tema.

Exactamente lo que se puede enumerar en la lista de tipos permitidos no está tan claro como podría ser. Creo que tenemos más trabajo que hacer allí. Tenga en cuenta que, al menos en el borrador de diseño actual, enumerar un tipo de interfaz en la lista de tipos actualmente significa que el argumento de tipo puede ser ese tipo de interfaz. No significa que el argumento de tipo pueda ser un tipo que implemente ese tipo de interfaz. Creo que actualmente no está claro si puede ser una instancia instanciada de un tipo parametrizado. Sin embargo, es una buena pregunta.

@smasher164 @tooolbox Los casos a tener en cuenta al combinar parámetros de tipo y parámetros regulares en una sola lista son cómo separarlos (si están separados) y cómo manejar el caso en el que no hay parámetros regulares (presumiblemente podemos excluir el caso de parámetros sin tipo). Por ejemplo, si no hay parámetros regulares, ¿cómo se distingue entre instanciar la función pero no llamarla e instanciar la función y llamarla? Aunque claramente el último es el caso más común, es razonable que la gente quiera poder escribir el primer caso.

Si los parámetros de tipo se colocaran dentro de los mismos paréntesis que los parámetros regulares, entonces @griesemer dijo en #36177 (su segunda publicación) que le gustaba bastante el uso de un punto y coma en lugar de dos puntos como separador porque (como resultado de inserción automática de punto y coma) permitía repartir los parámetros en varias líneas de una manera agradable.

Personalmente, también me gusta el uso de barras verticales ( |..| ) para encerrar los parámetros de tipo, ya que a veces se ven en otros lenguajes (Ruby, Crystal, etc.) para encerrar un bloque de parámetros. Entonces tendríamos cosas como:

func F(|T| a, b T) { }
func G() { F(|int| 1, 2) }

Las ventajas incluyen:

  • Proporcionan una buena distinción visual (al menos a mis ojos) entre el tipo y los parámetros regulares.
  • No necesitaría usar la palabra clave type .
  • No tener parámetros regulares no es un problema.
  • El carácter de barra vertical está, por supuesto, en el conjunto ASCII y, por lo tanto, debería estar disponible en la mayoría de los teclados.

Incluso podría usarlo fuera de los paréntesis, pero presumiblemente tendría las mismas dificultades de análisis que con <...> o [...] , ya que podría confundirse con el operador bit a bit 'o' aunque posiblemente las dificultades serían menos agudas.

No entiendo cómo las barras verticales ayudan en el caso de que no haya parámetros regulares. No entiendo cómo se puede distinguir una instanciación de función de una llamada de función.

Una forma de distinguir entre esos dos casos sería requerir la palabra clave type si estuviera instanciando la función, pero no si la estuviera llamando, que, como dijo anteriormente, es el caso más común.

Estoy de acuerdo en que eso podría funcionar, pero parece muy sutil. No creo que sea obvio para el lector lo que está sucediendo.

Creo que en Go necesitamos apuntar más alto que simplemente tener una manera de hacer algo. Necesitamos apuntar a enfoques que sean sencillos, intuitivos y que encajen bien con el resto del lenguaje. La persona que lee el código debe poder entender fácilmente lo que está sucediendo. Por supuesto que no siempre podemos alcanzar esos objetivos, pero debemos hacerlo lo mejor que podamos.

@ianlancetaylor además de debatir sobre la sintaxis, que es interesante por derecho propio, me pregunto si hay algo que nosotros como comunidad podamos hacer para ayudarlo a usted y al equipo en este tema.

Por ejemplo, tengo la idea de que le gustaría escribir más código en el estilo de la propuesta, para evaluar mejor la propuesta, tanto sintácticamente como de otra manera. ¿Y/u otras cosas?

@toolbox Sí. Estamos trabajando en una herramienta para hacerlo más fácil, pero aún no está lista. Muy pronto ahora.

¿Puedes decir algo más sobre la herramienta? ¿Permitiría ejecutar código?

¿Es este problema la ubicación preferida para los comentarios genéricos? Parece más activo que el wiki. Una observación es que la propuesta tiene muchos aspectos, pero el problema de GitHub colapsa la discusión en un formato lineal.

La sintaxis F(T:) / G() { F(T:)} me parece bien. No creo que la creación de instancias que parece una llamada de función sea intuitiva para los lectores sin experiencia.

No entiendo exactamente cuáles son las preocupaciones sobre la compatibilidad con versiones anteriores. Creo que hay una limitación en el borrador contra la declaración de un contrato, excepto en el nivel superior. Podría valer la pena sopesar (y medir) cuánto código se rompería realmente si esto estuviera permitido. Mi entendimiento es solo el código que usa la palabra clave contract , que parece no ser mucho código (que podría admitirse de todos modos especificando go1 en la parte superior de los archivos antiguos). Compare eso con décadas de más poder para los programadores. En general, parece bastante simple proteger el código antiguo con tales mecanismos, especialmente con el uso generalizado de las famosas herramientas de go.

Además de esa restricción, sospecho que la prohibición de declarar métodos dentro de los cuerpos de funciones es una de las razones por las que las interfaces no se usan más: son mucho más engorrosas que pasar funciones individuales. Es difícil decir si la restricción de nivel superior de los contratos sería tan irritante como la restricción de los métodos, probablemente no lo sería, pero no use la restricción de los métodos como precedente. Para mí eso es un defecto de lenguaje.

También me gustaría ver ejemplos de cómo los contratos podrían ayudar a reducir la verbosidad de if err != nil y, lo que es más importante, dónde serían insuficientes. ¿Es posible algo como F() (X, error) {return IfError(foo(), func(i, j int) X { return X(i*j}), Identity )} ?

También me pregunto si el equipo de go anticipa que las firmas de funciones implícitas se sentirán como una característica faltante una vez que el Mapa, el Filtro y los amigos estén disponibles. ¿Es esto algo que debe tenerse en cuenta mientras se agregan nuevas funciones de escritura implícita al lenguaje de los contratos? O se puede agregar mas tarde? ¿O nunca será parte del lenguaje?

Con ganas de probar la propuesta. Perdón por tantos temas.

Personalmente, soy bastante escéptico de que a muchas personas les gustaría escribir métodos dentro de cuerpos de funciones. Hoy en día es muy raro definir tipos dentro de cuerpos de funciones; declarar métodos sería aún más raro. Dicho esto, consulte el n.º 25860 (no relacionado con los genéricos).

No veo cómo los genéricos ayudan con el manejo de errores (ya es un tema muy detallado en sí mismo). No entiendo tu ejemplo, lo siento.

Una sintaxis literal de función más corta, que tampoco está relacionada con los genéricos, es #21498.

Cuando publiqué anoche no me di cuenta de que es posible jugar con el borrador
implementación (!!). Guau, es genial poder finalmente escribir código más abstracto. No tengo ningún problema con la sintaxis del borrador.

Continuando con la discusión anterior...


Parte de la razón por la que la gente no escribe tipos en cuerpos de funciones es porque
no puedo escribir métodos para ellos. Esta restricción puede atrapar el tipo dentro del
bloque donde se definió, ya que no se puede transformar de manera concisa en un
interfaz para su uso en otros lugares. Java permite que las clases anónimas satisfagan su versión.
de interfaces, y se utilizan bastante.

Podemos tener la discusión de la interfaz en #25860. Solo diría que en la era
de los contratos, los métodos se volverán más importantes, por lo que sugiero errar en el
lado de empoderar a los tipos locales y a las personas a las que les gusta escribir cierres, no
debilitándolos.

(Y para reiterar, no utilice la compatibilidad estricta con go1 [vs virtualmente
99.999% de compatibilidad, según tengo entendido] como un factor para decidir sobre esto
característica.)


Con respecto al manejo de errores, sospeché que los genéricos podrían permitir la abstracción
patrones comunes para tratar con tuplas de retorno (T1, T2, ..., error) . Yo no
tener nada detallado en mente. Algo como type ErrPair(type T) struct{T T; Err Error} podría ser útil para encadenar acciones, como Promise en
Java/TypeScript. Quizás alguien haya pensado más en esto. un intento de
escribir una biblioteca auxiliar y el código que usa la biblioteca podría valer la pena mirar
en si está buscando un uso real.

Con un poco de experimentación terminé con lo siguiente. me gustaria probar esto
técnica en un ejemplo más grande para ver si usar ErrPair(T) realmente ayuda.

type result struct {min, max point}

// with a generic ErrPair type and generic function errMap2 (like Java's Optional#map() function).
func minMax2(msg *inputTimeSeries) (result, error) {
    return errMap2(
        MakeErrPair(time.Parse(layout, msg.start)).withMessage("bad start"),
        MakeErrPair(time.Parse(layout, msg.end)).withMessage("bad end"),
        func(start, end time.Time) (result, error) {
            min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
                return float64(p.value)
            })
            mkPoint := func(ip inputPoint) point {
                return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
            }
            return result{mkPoint(*min), mkPoint(*max)}, nil
        }).tuple()
}

// without generics, lots of if err != nil 
func minMax(msg *inputTimeSeries) (result, error) { 
    start, err := time.Parse(layout, msg.start)
    if err != nil {
        return result{}, fmt.Errorf("bad start: %w", err)
    }
    end, err := time.Parse(layout, msg.end)
    if err != nil {
        return result{}, fmt.Errorf("bad end: %w", err)
    }
    min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
        return float64(p.value)
    })
    mkPoint := func(ip inputPoint) point {
        return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
    }
    return result{mkPoint(*min), mkPoint(*max)}, nil
}

// Most languages look more like this.
func minMaxWithThrowing(msg *inputTimeSeries) result {
    start := time.Parse(layout, msg.start)) // might throw
    end := time.Parse(layout, msg.end)) // might throw
    min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
        return float64(p.value)
    })
    mkPoint := func(ip inputPoint) point {
        return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
    }
    return result{mkPoint(*min), mkPoint(*max)}
}

(Código de ejemplo completo disponible aquí )


Para la experimentación general, intenté escribir un paquete S-Expression
aquí
Experimenté algunos pánicos en la implementación experimental mientras intentaba
trabajar con tipos compuestos como Form([]*Form(T)) . Puedo proporcionar más comentarios
después de trabajar alrededor de eso, si sería útil.

Tampoco estaba muy seguro de cómo escribir un tipo primitivo -> función de cadena:

contract PrimitiveType(T) {
    T bool, int, int8, int16, int32, int64, string, uint, uint8, uint16, uint32, uint64, float32, float64, complex64, complex128
    // string(T) is not a contract
}

func primitiveString(type T PrimitiveType(T))(t T) string  {
    // I'm not sure if this is an artifact of the experimental implementation or not.
    return string(t) // error: `cannot convert t (variable of type T) to string`
}

La función real que estaba tratando de escribir era esta:

// basicFormAdapter implements FormAdapter() for the primitive types.
type basicFormAdapter(type T PrimitiveType) struct{}


func (a *basicFormAdapter(T)) Format(e T, fc *FormatContext) error {
    //This doesn't work: fc.Print(string(e)) -- cannot convert e (variable of type T) to string
    // This also doesn't work: cannot type switch on non-interface value e (type int)
    // switch ee := e.(type) {
    // case int: fc.Print(string(ee))
    // default: fc.Print(fmt.Sprintf("!!! unsupported type %v", e))
    // }
    // IMO, the proposal to allow switching on T is most natural:
    // switch T.(type) {
    //  case int: fc.Print(string(e))
    //  default: fc.Print(fmt.Sprintf("!!! unsupported type %v", e))
    // }

    // This can't be the only way, right?
    rv := reflect.ValueOf(e)
    switch rv.Kind() {
    case reflect.Bool: fc.Print(fmt.Sprintf("%v", e))
    case reflect.Int:fc.Print(fmt.Sprintf("%v", e))
    case reflect.Int8: fc.Print(fmt.Sprintf("int8:%v", e))
    case reflect.Int16: fc.Print(fmt.Sprintf("int16:%v", e))
    case reflect.Int32: fc.Print(fmt.Sprintf("int32:%v", e))
    case reflect.Int64: fc.Print(fmt.Sprintf("int64:%v", e))
    case reflect.Uint: fc.Print(fmt.Sprintf("uint:%v", e))
    case reflect.Uint8: fc.Print(fmt.Sprintf("uint8:%v", e))
    case reflect.Uint16: fc.Print(fmt.Sprintf("uint16:%v", e))
    case reflect.Uint32: fc.Print(fmt.Sprintf("uint32:%v", e))
    case reflect.Uint64: fc.Print(fmt.Sprintf("uint64:%v", e))
    case reflect.Uintptr: fc.Print(fmt.Sprintf("uintptr:%v", e))
    case reflect.Float32: fc.Print(fmt.Sprintf("float32:%v", e))
    case reflect.Float64: fc.Print(fmt.Sprintf("float64:%v", e))
    case reflect.Complex64: fc.Print(fmt.Sprintf("(complex64 %f %f)", real(rv.Complex()), imag(rv.Complex())))
    case reflect.Complex128:
         fc.Print(fmt.Sprintf("(complex128 %f %f)", real(rv.Complex()), imag(rv.Complex())))
    case reflect.String:
        fc.Print(fmt.Sprintf("%q", rv.String()))
    }
    return nil
}

También intenté crear una especie de tipo 'Resultado'

type Result(type T) struct {
    Value T
    Err error
}

func NewResult(type T)(value T, err error) Result(T) {
    return Result(T){
        Value: value,
        Err: err,
    }
}

func then(type T, R)(r Result(T), f func(T) R) Result(R) {
    if r.Err != nil {
        return Result(R){Err: r.Err}
    }

    v := f(r.Value)
    return  Result(R){
        Value: v,
        Err: nil,
    }
}

func thenTry(type T, R)(r Result(T), f func(T)(R, error)) Result(R) {
    if r.Err != nil {
        return Result(R){Err: r.Err}
    }

    v, err := f(r.Value)
    return  Result(R){
        Value: v,
        Err: err,
    }
}

p.ej

    r := NewResult(GetInput())
    r2 := thenTry(r, UppercaseAndErr)
    r3 := thenTry(r2, strconv.Atoi)
    r4 := then(r3, Add5)
    if r4.Err != nil {
        // handle err
    }
    return r4.Value, nil

Lo ideal sería que las funciones then fueran métodos en el tipo de resultado.

Además, el ejemplo de diferencia absoluta en el borrador no parece compilarse.
Pienso lo siguiente:

func (a ComplexAbs(T)) Abs() T {
    r := float64(real(a))
    i := float64(imag(a))
    d := math.Sqrt(r * r + i * i)
    return T(complex(d, 0))
}

debiera ser:

func (a ComplexAbs(T)) Abs() ComplexAbs(T) {
    r := float64(real(a))
    i := float64(imag(a))
    d := math.Sqrt(r * r + i * i)
    return ComplexAbs(T)(complex(d, 0))
}

Me preocupa un poco la capacidad de usar múltiples contract para vincular un parámetro de tipo.

En Scala, es común definir una función como:

def compute[A: PointLike: HasTime: IsWGS](points: Vector[A]): Map[Int, A] = ???

PointLike , HasTime y IsWGS son algunos contract pequeños (Scala los llama type class ).

Rust también tiene un mecanismo similar:

fn f<F: A + B>(a F) {}

Y podemos usar una interfaz anónima al definir una función.

type I1 interface {
    A()
}
type I2 interface {
    B()
}
func f(a interface{
    I1
    I2
})

En mi opinión, la interfaz anónima es una mala práctica, porque un interface es un tipo real , la persona que llama a esta función puede tener que declarar una variable con este tipo. Pero contract solo una restricción en el parámetro de tipo, la persona que llama siempre juega con algún tipo real o simplemente con otro parámetro de tipo, creo que es seguro permitir contratos anónimos en una definición de función.

Para los desarrolladores de bibliotecas, es un inconveniente definir un nuevo contract si la combinación de algunos contratos solo se usa en algunos lugares, estropeará la base de código. Para el usuario de bibliotecas, necesitan profundizar en las definiciones para conocer los requisitos reales de la misma. Si el usuario define muchas funciones para llamar a la función en la biblioteca, puede definir un contrato con nombre para facilitar su uso, e incluso puede agregar más contrato a este nuevo contrato si lo necesita porque esto es válido.

contract C1(T) {
    T A()
}
contract C2(T) {
    T B()
}
contract C3(T) {
    T C()
}

contract PART(T) {
    C1(T)
    C2(T)
}

contract ALL(T) {
    C1(T)
    C2(T)
    C3(T)
}

func f1(type A PART) (a A) {}

func f2(type A ALL) (a A) {
    f1(a)
}

Los probé en el borrador del compilador, no se puede verificar el tipo de todos ellos.

func f(type A C1, C2)(x A)

func f1(type A contract C(A1) {
    C1(A)
    C2(A)
}) (x A)

func f2(type A ((type A1) interface {
    I1(A1)
    I2(A1)
})(A)) (x A)

Según las notas en CL

Un parámetro de tipo que está restringido por varios contratos no obtendrá el límite de tipo correcto.

Creo que este fragmento extraño es válido después de que se resolvió este problema

func f1(type A C1, _ C2(A)) (x A)

Aquí están algunos de mis pensamientos:

  • Si tratamos contract como el tipo de un parámetro de tipo, type a A <=> var a A , podemos agregar un azúcar de sintaxis como type a { A1(a); A2(a) } para definir un anónimo contrato rápidamente.
  • De lo contrario, podemos tratar la última parte de la lista de tipos como una lista de requisitos, type a, b, A1(a), A2(a), A3(a, b) , este estilo es como usar interface para restringir los parámetros de tipo.

@bobotu Es común en Go para componer la funcionalidad mediante incrustación. Parece natural redactar contratos de la misma manera que lo haría con estructuras o interfaces.

@azunymous Personalmente, no sé cómo me siento acerca de que toda la comunidad de Go cambie de retornos múltiples a Result , aunque parece que la propuesta de Contratos permitiría esto hasta cierto punto. El equipo de Go parece rehuir los cambios de idioma que comprometen la "sensación" del idioma, con lo que estoy de acuerdo, pero ese parece ser uno de esos cambios.

Solo un pensamiento; Me pregunto si hay alguna opinión sobre este punto.

@toolbox No creo que sea realmente posible usar algo como un solo tipo Result fuera del caso en el que solo está pasando valores, a menos que tenga una gran cantidad de Result genéricos s y funciones de cada combinación de recuentos de parámetros y tipos de devolución. Con muchas funciones numeradas o usando cierres, perdería legibilidad.

Creo que sería más probable que vieras algo equivalente a un errWriter donde usarías algo así de vez en cuando cuando encaje, nombrado para el caso de uso.

Personalmente, no sé cómo me siento acerca de que toda la comunidad de Go cambie de múltiples devoluciones a Result.

No creo que esto suceda. Como dijo @azunymous , muchas funciones tienen múltiples tipos de devolución y un error, pero un resultado no puede contener todos esos otros valores devueltos al mismo tiempo. El polimorfismo paramétrico no es la única característica necesaria para hacer algo como esto; también necesitarías tuplas y desestructuración.

¡Gracias! Como dije, no es algo en lo que haya pensado profundamente, pero es bueno saber que mi preocupación estaba fuera de lugar.

@toolbox No pretendo introducir una sintaxis nueva, el problema clave aquí es la falta de capacidad para usar un contrato anónimo como una interfaz anónima.

En el borrador del compilador, parece imposible escribir algo como esto. Podemos usar una interfaz anónima en la definición de la función, pero no podemos hacer lo mismo para el contrato, incluso en el estilo detallado.

func f1(type A, B, C, D contract {
    C1(A)
    C2(A, B)
    C3(A, C)
}) (a A, b B, c C, d D)

// Or a more verbose style

func f2(type A, B, C, D (contract (_A, _B, _C) {
    C1(_A)
    C2(_A, _B)
    C3(_A, _C)
})(A, B, C)) (a A, b B, c C, d D)

En mi opinión, esta es una extensión natural de la sintaxis existente. Este sigue siendo un contrato al final de la lista de parámetros de tipo, y todavía usamos la incrustación para componer la funcionalidad. Si Go puede proporcionar algo de azúcar para generar los parámetros de tipo de contrato automáticamente como el primer fragmento, el código será más fácil de leer y escribir.

func fff(type A C1(A), B C2(B, A), C C3(B, C, A)) (a A, b B, c C)

// is more verbose than

func fff(type A, B, C contract {
    C1(A)
    C2(B, A)
    C3(B, C, A)
}) (a A, b B, c C)

Encuentro algunos problemas cuando trato de implementar un iterador perezoso sin invocar el método dinámico, al igual que el iterador de Rust.

Quiero definir un contrato simple Iterator

contract Iterator(T, E) {
    T Next() (E, bool)
}

Debido a que Go no tiene el concepto de type member , necesito declarar E como el parámetro de tipo de entrada.

Una función para recoger los resultados.

func Collect(type I, E Iterator) (input I) []E {
    var results []E
    for {
        e, ok := input.Next()
        if !ok {
            return results
        }
        results = append(results, e)
    }
}

Una función para mapear elementos.

contract MapIO(I, E, O, R) {
    Iterator(I, E)
    Iterator(O, R)
}

func Map(type I, E, O, R MapIO) (input I, f func (e E) R) O {
    return &lazyIterator(I, E, R){
        parent: input,
        f:      f,
    }
}

Tengo dos problemas aquí:

  1. No puedo devolver lazyIterator aquí, el compilador dice cannot convert &(lazyIterator(I, E, R) literal) (value of type *lazyIterator(I, E, R)) to O .
  2. Necesito declarar un nuevo contrato llamado MapIO que necesita 4 líneas mientras que Map solo necesita 6 líneas. Es difícil para los usuarios leer el código.

Supongamos que se puede verificar Map , espero poder escribir algo como

type staticIterator(type E) struct {
    elem []E
}

func (it *(staticIterator(E))) Next() (E, bool) { panic("todo") }

func main() {
    inpuit := &staticIterator{
        elem: []int{1, 2, 3, 4},
    }
    mapped := Map(input, func (i int) float32 { return float32(i + 1) })
    fmt.Printf("%v\n", Collect(mapped))
}

Desafortunadamente, el compilador se queja de que no puede inferir tipos. Deja de quejarse de esto después de que cambio el código a

func main() {
    input := &staticIterator(int){
        elem: []int{1, 2, 3, 4},
    }
    mapped := Map(*staticIterator(int), int, *lazyIterator(*staticIterator(int), int, float32), float32)(input, func (i int) float32 { return float32(i + 1) })
    result := Collect(*lazyIterator(*staticIterator(int), int, float32), float32)(mapped)
    fmt.Printf("%v\n", result)
}

El código es muy difícil de leer y escribir, y hay demasiadas sugerencias de tipo duplicadas.

Por cierto, el compilador entrará en pánico con:

panic: interface conversion: ast.Expr is *ast.ParenExpr, not *ast.CallExpr

goroutine 1 [running]:
go/go2go.(*translator).instantiateTypeDecl(0xc000251950, 0x0, 0xc0001af860, 0xc0001a5dd0, 0xc00018ac90, 0x1, 0x1, 0xc00018bca0, 0x1, 0x1, ...)
        /home/tuzi/go-tip/src/go/go2go/instantiate.go:191 +0xd49
go/go2go.(*translator).translateTypeInstantiation(0xc000251950, 0xc000189380)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:671 +0x3f3
go/go2go.(*translator).translateExpr(0xc000251950, 0xc000189380)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:518 +0x501
go/go2go.(*translator).translateExpr(0xc000251950, 0xc0001af990)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:496 +0xe3
go/go2go.(*translator).translateExpr(0xc000251950, 0xc00018ace0)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:524 +0x1c3
go/go2go.(*translator).translateExprList(0xc000251950, 0xc00018ace0, 0x1, 0x1)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:593 +0x45
go/go2go.(*translator).translateStmt(0xc000251950, 0xc000189840)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:419 +0x26a
go/go2go.(*translator).translateBlockStmt(0xc000251950, 0xc00018d830)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:380 +0x52
go/go2go.(*translator).translateFuncDecl(0xc000251950, 0xc0001c0390)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:373 +0xbc
go/go2go.(*translator).translate(0xc000251950, 0xc0001b0400)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:301 +0x35c
go/go2go.rewriteAST(0xc000188280, 0xc000188240, 0x0, 0x0, 0xc0001f6280, 0xc0001b0400, 0x1, 0xc000195360, 0xc0001f6280)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:122 +0x101
go/go2go.RewriteBuffer(0xc000188240, 0x7ffe07d6c027, 0xa, 0xc0001ec000, 0x4fe, 0x6fe, 0x0, 0xc00011ed58, 0x40d288, 0x30, ...)
        /home/tuzi/go-tip/src/go/go2go/go2go.go:132 +0x2c6
main.translateFile(0xc000188240, 0x7ffe07d6c027, 0xa)
        /home/tuzi/go-tip/src/cmd/go2go/translate.go:26 +0xa9
main.main()
        /home/tuzi/go-tip/src/cmd/go2go/main.go:64 +0x434

También encuentro que es imposible definir una función que funcione con un retorno Iterator un tipo en particular.

type User struct {}

func UpdateUsers(type A Iterator(A, User)) (it A) bool { 
    // Access `User`'s field.
}

// And I found this may be possible

contract checkInts(A, B) {
    Iterator(A, B)
    B int
}

func CheckInts(type A, B checkInts) (it A) bool { panic("todo") }

El segundo fragmento puede funcionar en algunos escenarios, pero es difícil de entender y el tipo B sin usar parece raro.

De hecho, podemos usar una interfaz para completar esta tarea.

type Iterator(type E) interface {
    Next() (E, bool)
}

Solo estoy tratando de explorar qué tan expresivo es el diseño del Go.

Por cierto, el código de Rust al que me refiero es

fn main() {
    let input = vec![1, 2, 3, 4];
    let mapped = input.iter().map(|x| x * 3);
    let result = f(mapped);
    println!("{:?}", result.collect::<Vec<_>>());
}

fn f<I: Iterator<Item = i32>>(it: I) -> impl Iterator<Item = f32> {
    it.map(|i| i as f32 * 2.0)
}

// The definition of `map` in stdlib is
pub struct Map<I, F> {
    iter: I,
    f: F,
}

fn map<B, F: FnMut(Self::Item) -> B>(self, f: F) -> Map<Self, F>

Aquí hay un resumen de https://github.com/golang/go/issues/15292#issuecomment -633233479

  1. Es posible que necesitemos algo para expresar existential type por func Collect(type I, E Iterator) (input I) []E

    • El tipo real de parámetro cuantificado universal E no se puede inferir, porque solo apareció en la lista de retorno. Debido a la falta de type member para hacer que E sean existenciales de forma predeterminada, creo que podemos encontrarnos con este problema en muchos lugares.

    • Tal vez podamos usar el existential type más simple como el comodín de Java ? para resolver la inferencia de tipo de func Consume(type I, E Iterator) (input I) . Podemos usar _ para reemplazar E , func Consume(type I Iterator(I, _)) (input I) .

    • Pero aún no puede ayudar con el problema de inferencia de tipos para Collect , no sé si es difícil inferir E , pero parece que Rust puede hacer esto.

    • O podemos usar _ como marcador de posición para los tipos que el compilador puede inferir y llenar los tipos que faltan manualmente, como Collect(_, float32) (...) para recopilar en un iterador de float32.

  1. Debido a la falta de capacidad para devolver un existential type , también tenemos problemas para cosas como func Map(type I, E, O, R MapIO) (input I, f func (e E) R) O

    • Rust soporta esto usando impl Iterator<E> . Si Go puede proporcionar algo como esto, podemos devolver un nuevo iterador sin boxing, puede ser útil para algunos códigos críticos para el rendimiento.

    • O simplemente podemos devolver un objeto en caja, así es como Rust resuelve este problema antes de que admita existential type en la posición de retorno. Pero la pregunta es la relación entre contract y interface , tal vez necesitemos definir algunas reglas de conversión y dejar que el compilador las convierta automáticamente. De lo contrario, es posible que necesitemos definir un contract y un interface con métodos idénticos para este caso.

    • De lo contrario, solo podemos usar CPS para mover el parámetro de tipo de la posición de retorno a la lista de entrada. por ejemplo, func Map(type I, E, O, R MapIO) (input I, f func (e E) R, f1 func (outout O)) . Pero esto es inútil en la práctica, simplemente porque debemos escribir el tipo real de O cuando pasamos una función a Map .

Acabo de ponerme al día con esta discusión un poco, y parece bastante claro que las dificultades sintácticas con los parámetros de tipo siguen siendo una dificultad importante con el borrador de propuesta. Hay una manera de evitar los parámetros de tipo por completo y lograr la mayor parte de la funcionalidad genérica: #32863 -- ¿tal vez este podría ser un buen momento para considerar esa alternativa a la luz de parte de esta discusión adicional? Si hubiera alguna posibilidad de que se adoptara algo como este diseño, me complacería intentar modificar el patio de juegos del ensamblado web para permitir probarlo.

Mi sensación es que el enfoque actual es clavar la corrección de la semántica de la propuesta actual, independientemente de la sintaxis, porque la semántica es muy difícil de cambiar.

Acabo de ver que se publicó un artículo sobre Featherweight Go en Arxiv y es una colaboración entre el equipo de Go y varios expertos en teoría de tipos. Parece que hay más artículos planeados en este sentido.

Para dar seguimiento a mi comentario anterior, Phil Wadler de Haskell Fame y uno de los autores del artículo tiene una charla programada sobre "Peatherweight Go" el lunes 8 de junio a las 7 a. m. PDT / 10 a. m. EDT: http://chalmersfp.org/ . enlace de youtube

@rcoreilly Creo que solo sabremos si las "dificultades sintácticas" son un problema importante cuando las personas tengan más experiencia escribiendo y, lo que es más importante, leyendo código escrito de acuerdo con el borrador de diseño. Estamos trabajando en formas para que las personas prueben eso.

En ausencia de eso, creo que la sintaxis es simplemente lo que la gente ve primero y comenta primero. Puede ser un problema importante, puede que no. Aún no lo sabemos.

Para continuar con mi comentario anterior, Phil Wadler, famoso por Haskell y uno de los autores del artículo, tiene una charla programada sobre "Peso pluma Go" el lunes.

La charla de Phil Wadler fue muy accesible e interesante. Estaba molesto por el límite de tiempo aparentemente inútil de una hora que le impedía entrar en monomorfización.

Notable que Pike le pidió a Wadler que apareciera; aparentemente se conocen de Bell Labs. Para mí, Haskell tiene un conjunto muy diferente de valores y paradigmas, y es interesante ver cómo su (¿creador? ¿diseñador principal?) piensa sobre Go y los genéricos en Go.

La propuesta en sí tiene una sintaxis muy cercana a los contratos, pero omite los contratos en sí mismos, solo usa parámetros de tipo e interfaces. Una diferencia clave que se menciona es la capacidad de tomar un tipo genérico y definir métodos en él que tengan restricciones más específicas que el tipo en sí.

¡Aparentemente, el equipo Go está trabajando o tiene un prototipo de esto! Eso será interesante. Mientras tanto, ¿cómo se vería esto?

package graph

type Node(type e) interface{
    Edges() []e
}

type Edge(type n) interface{
    Nodes() (from n, to n)
}

type Graph(type n Node(e), e Edge(n)) struct { ... }
func New(type n Node(e), e Edge(n))(nodes []n) *Graph(n, e) { ... }
func (g *Graph(type n Node(e), e Edge(n))) ShortestPath(from, to n) []e { ... }

¿Tengo ese derecho? Creo que sí. Si lo hago... no está mal, en realidad. No resuelve del todo el problema de los paréntesis entrecortados, pero parece mejorado de alguna manera. Cierta agitación sin nombre dentro de mí se calma.

¿Qué pasa con el ejemplo de pila de @urandom ? (Aliasing interface{} a Any y usando una cierta cantidad de inferencia de tipos).

package main

type Any interface{}

type Stack(type t Any) []t

func (s Stack(type t Any)) Peek() t {
    return s[len(s)-1]
}

func (s *Stack(type t Any)) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack(type t Any)) Push(value t) {
    *s = append(*s, value)
}

type StackIterator(type t Any) struct{
    stack Stack(t)
    current int
}

func (s *Stack(type t Any)) Iter() *StackIterator(t) {
    it := StackIterator(t){stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator(type t Any)) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator(type t Any)) Value() t {
    if i.current < 0 {
        var zero t
        return zero
    }

    return i.stack[i.current]
}

type Iterator(type t Any) interface {
    Next() bool
    Value() t
}

func Map(type t Any, u Any)(it Iterator(t), mapF func(t) u) Iterator(u) {
    return mapIt(t, u){it, mapF}
}

type mapIt(type t Any, u Any) struct {
    parent Iterator(t)
    mapF func(t) u
}

func (i mapIt(type t Any, u Any)) Next() bool {
    return i.parent.Next()
}

func (i mapIt(type t Any, u Any)) Value() u {
    return i.mapF(i.parent.Value())
}

func Filter(type t Any)(it Iterator(t), predicate func(t) bool) Iterator(t) {
    return filter(t){it, predicate}
}

type filter(type t Any) struct {
    parent Iterator(t)
    predicateF func(t) bool
}

func (i filter(type t Any)) Next() bool {
    if !i.parent.Next() {
        return false
    }

    n := true
    for n && !i.predicateF(i.parent.Value()) {
        n = i.parent.Next()
    }

    return n
}

func (i filter(type t Any)) Value() t {
    return i.parent.Value()
}

func Distinct(type t comparable)(it Iterator(t)) Iterator(t) {
    return distinct(t){it, map[t]struct{}{}}
}

type distinct(type t comparable) struct {
    parent Iterator(t)
    set map[t]struct{}
}

func (i distinct(type t Any)) Next() bool {
    if !i.parent.Next() {
        return false
    }

    n := true
    for n {
        _, ok := i.set[i.parent.Value()]
        if !ok {
            i.set[i.parent.Value()] = struct{}{}
            break
        }
        n = i.parent.Next()
    }


    return n
}

func (i distinct(type t Any)) Value() t {
    return i.parent.Value()
}

func ToSlice(type t Any)(it Iterator(t)) []t {
    var res []t

    for it.Next() {
        res = append(res, it.Value())
    }

    return res
}

func ToSet(type t comparable)(it Iterator(t)) map[t]struct{} {
    var res map[t]struct{}

    for it.Next() {
        res[it.Value()] = struct{}{}
    }

    return res
}

func Reduce(type t Any)(it Iterator(t), id t, acc func(a, b t) t) t {
    for it.Next() {
        id = acc(id, it.Value())
    }

    return id
}

func main() {
    var stack Stack(string)
    stack.Push("foo")
    stack.Push("bar")
    stack.Pop()
    stack.Push("alpha")
    stack.Push("beta")
    stack.Push("foo")
    stack.Push("gamma")
    stack.Push("beta")
    stack.Push("delta")


    var it Iterator(string) = stack.Iter()

    it = Filter(string)(it, func(s string) bool {
        return s == "foo" || s == "beta" || s == "delta"
    })

    it = Map(string, string)(it, func(s string) string {
        return s + ":1"
    })

    it = Distinct(string)(it)

    println(Reduce(it, "", func(a, b string) string {
        if a == "" {
            return b
        }
        return a + ":" + b
    }))


}

Algo así, supongo. Me doy cuenta de que en realidad no hay contratos en ese código, por lo que no es una buena representación de cómo se maneja eso al estilo FGG, pero puedo abordarlo en un momento.

Impresiones:

  • Me gusta que el estilo de los parámetros de tipo en los métodos coincida con el de las declaraciones de tipo. Es decir, decir "tipo" e indicar explícitamente los tipos, ("type" param paramType, param paramType...) en lugar de (param, param) . Lo hace visualmente consistente, por lo que el código es más visible.
  • Me gusta tener los parámetros de tipo en minúsculas. Las variables de una sola letra en Go indican un uso extremadamente local, pero las mayúsculas significan que se exportan y parecen contrarias cuando se juntan. Las minúsculas se sienten mejor ya que los parámetros de tipo se limitan a la función/tipo.

Vale, ¿y los contratos?

Bueno, una cosa que me gusta es que Stringer está intacto; no vas a tener una interfaz Stringer y un Stringer .

type Stringer interface {
    String() string
}

func Stringify(type t Stringer)(s []t) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}

También tenemos el ejemplo viaStrings :

type ToString interface {
    Set(string)
}

type FromString interface {
    String() string
}

func SetViaStrings(type to ToString, from FromString)(s []from) []to {
    r := make([]to, len(s))
    for i, v := range s {
        r[i].Set(v.String())
    }
    return r
}

Interesante. En realidad, no estoy 100% seguro de lo que nos ganó el contrato en ese caso. Quizás parte de esto era la regla de que una función podía tener múltiples parámetros de tipo pero solo un contrato.

Equal está cubierto en el artículo/charla:

contract equal(T) {
    T Equal(T) bool
}

// becomes

type equal(type t equal(t)) interface{
    Equal(t) bool
}

Y así. Estoy bastante enganchado con la semántica. Los parámetros de tipo son interfaces, por lo que se aplican las mismas reglas sobre la implementación de una interfaz a lo que se puede usar como parámetro de tipo. Simplemente no está "encajonado" en tiempo de ejecución, a menos que le pases explícitamente una interfaz, supongo, que eres libre de hacerlo.

Lo más importante que observo como no cubierto es un reemplazo de la capacidad de los Contratos para especificar un rango de tipos primitivos. Bueno, seguro que vendrá una estrategia para eso, y muchas cosas más:

8 - CONCLUSIÓN

Este es el comienzo de la historia, no el final. En el trabajo futuro, planeamos buscar otros métodos de implementación además de la monomorfización y, en particular, considerar una implementación basada en pasar representaciones de tipos en tiempo de ejecución, similar a la que se usa para los genéricos de .NET. Un enfoque mixto que usa monomorfización a veces y pasa representaciones en tiempo de ejecución a veces podría ser mejor, nuevamente similar al que se usa para los genéricos de .NET.

Featherweight Go está restringido a un pequeño subconjunto de Go. Planeamos un modelo de otras funciones importantes, como asignaciones, arreglos, segmentos y paquetes, que llamaremos Bantamweight Go; y un modelo del innovador mecanismo de concurrencia de Go basado en "goroutines" y paso de mensajes, que llamaremos Cruiserweight Go.

Featherweight Go se ve muy bien para mí. Excelente idea para involucrar a algunos expertos en teoría de tipos. Esto se parece mucho más al tipo de cosas que defendía más adelante en este tema.

¡Es bueno escuchar que los expertos en teoría de tipos están trabajando activamente en esto!

Incluso parece similar (excepto por la sintaxis ligeramente diferente) a mi antigua propuesta "los contratos son interfaces" https://github.com/cosmos72/gomacro/blob/master/doc/generics-cti.md

@cajadeherramientas
Al permitir métodos con restricciones diferentes al tipo real (así como tipos diferentes en total), FGG abre bastantes posibilidades que no eran factibles con el borrador de contratos actual. Como ejemplo, con FGG, uno debería poder definir tanto un Iterador como un ReversibleIterator, y hacer que los iteradores intermedios y de terminación (mapa, reducción de filtro) admitan ambos (p. ej., con Next() y NextFromBack() para reversibles) , dependiendo de cuál sea el iterador padre.

Creo que es importante tener en cuenta que FGG no es definitivamente donde terminarán los genéricos en Go. Es una toma de ellos, desde el exterior. E ignora explícitamente un montón de cosas que terminan complicando el producto final. Además, no he leído el periódico, solo he visto la charla. Con eso en mente: por lo que puedo decir, hay dos formas significativas en las que FGG agrega poder expresivo sobre el borrador de contratos:

  1. Permite agregar nuevos parámetros de tipo a los métodos (como se muestra en el ejemplo de "Lista y mapas" en la charla). AFAICT esto permitiría implementar Functor (de hecho, ese es su ejemplo de Lista, si no me equivoco), Monad y sus amigos. No creo que esos tipos específicos sean interesantes para Gophers, pero hay casos de uso interesantes para esto (por ejemplo, un puerto Go de Flume o conceptos similares probablemente se beneficiarían). Personalmente, siento que es un cambio positivo, aunque todavía no veo cuáles son las implicaciones para la reflexión y cosas por el estilo. Siento que las declaraciones de métodos que usan esto están comenzando a ser difíciles de leer, especialmente si los parámetros de tipo de un tipo genérico también se deben enumerar en el receptor.
  2. Permite que los parámetros de tipo tengan límites más estrictos en los métodos de tipos genéricos que en el tipo mismo. Como lo mencionaron otros, esto le permite tener el mismo tipo genérico implementando diferentes métodos, dependiendo de los tipos con los que fue instanciado. Personalmente, no estoy seguro de que sea un buen cambio. Parece una receta para la confusión, que Map(int, T) termine con métodos que Map(string, T) no tiene. Como mínimo, el compilador debe proporcionar excelentes mensajes de error, si sucede algo como esto. Mientras tanto, el beneficio parece comparativamente pequeño, especialmente dado que el factor motivador de la charla (compilación separada) no es muy relevante para Go: como los métodos deben declararse en el mismo paquete que su tipo de receptor y dado que los paquetes son la unidad de compilación, realmente no puedes extender el tipo por separado. Sé que hablar de compilación es más bien una forma concreta de hablar de un beneficio más abstracto, pero aún así, no creo que ese beneficio ayude mucho a Go.

Espero con ansias los próximos pasos, en cualquier caso :)

Creo que es importante tener en cuenta que FGG no es definitivamente donde terminarán los genéricos en Go.

@Merovius ¿por qué dices eso?

@arl
FG es más un trabajo de investigación sobre lo que _podría_ hacerse. Nadie ha dicho explícitamente que así es como funcionará el polimorfismo en Go en el futuro. Aunque los desarrolladores principales de 2 Go aparecen como autores en el documento, eso no significa que esto se implementará en Go.

Creo que es importante tener en cuenta que FGG no es definitivamente donde terminarán los genéricos en Go. Es una toma de ellos, desde el exterior. E ignora explícitamente un montón de cosas que terminan complicando el producto final.

Sí, muy buen punto.

Además, señalaré que Wadler está trabajando como parte de un equipo, y el producto resultante se basa en la propuesta de Contratos y está muy cerca de ella, que es el resultado de años de trabajo de los desarrolladores principales.

Al permitir métodos con restricciones diferentes al tipo real (así como tipos diferentes en total), FGG abre bastantes posibilidades que no eran factibles con el borrador de contratos actual. ...

@urandom Tengo curiosidad sobre cómo se ve ese ejemplo de Iterator; ¿te importaría juntar algo?

Por separado, estoy interesado en lo que los genéricos pueden hacer más allá de los mapas, filtros y cosas funcionales, y tengo más curiosidad por saber cómo podrían beneficiar un proyecto como k8s. (No es que vayan y refactoricen en este punto, pero he escuchado anecdóticamente que la falta de genéricos ha requerido un trabajo de pies elegante, creo que con recursos personalizados. Alguien más familiarizado con el proyecto puede corregirme).

Siento que las declaraciones de métodos que usan esto están comenzando a ser difíciles de leer, especialmente si los parámetros de tipo de un tipo genérico también se deben enumerar en el receptor.

¿Quizás gofmt podría ayudar de alguna manera? Tal vez tenemos que ir multi-línea. Vale la pena jugar, tal vez.

Como lo mencionaron otros, esto le permite tener el mismo tipo genérico implementando diferentes métodos, dependiendo de los tipos con los que fue instanciado.

Veo lo que estás diciendo @Merovius

Wadler lo mencionó como una diferencia, y le permite resolver su problema de expresión, pero usted hace un buen punto de que los paquetes herméticos de Go parecen limitar lo que podría/debería hacer con esto. ¿Puedes pensar en algún caso real en el que quieras hacer eso?

Como lo mencionaron otros, esto le permite tener el mismo tipo genérico implementando diferentes métodos, dependiendo de los tipos con los que fue instanciado.

Veo lo que estás diciendo @Merovius

Wadler lo mencionó como una diferencia, y le permite resolver su problema de expresión, pero usted hace un buen punto de que los paquetes herméticos de Go parecen limitar lo que podría/debería hacer con esto. ¿Puedes pensar en algún caso real en el que quieras hacer eso?

Irónicamente, mi primer pensamiento fue que podría usarse para resolver algunos de los desafíos descritos en este artículo: https://blog.merovius.de/2017/07/30/the-trouble-with-opcional-interfaces.html

@caja de herramientas

Por separado, estoy interesado en lo que los genéricos pueden hacer más allá de los mapas, los filtros y las cosas funcionales.

FWIW, debe aclararse que esto es una especie de venta corta de "mapas y filtros y cosas funcionales". Personalmente, no quiero map y filter sobre estructuras de datos integradas en mi código, por ejemplo (prefiero for-loops). Pero también puede significar

  1. Proporcionar acceso generalizado a cualquier estructura de datos de terceros. es decir, se puede hacer que map y filter funcionen sobre árboles genéricos, o mapas ordenados, o… también. Por lo tanto, puede intercambiar lo que está mapeado para obtener más potencia. Y más importante
  2. Puede intercambiar cómo se mapea. Por ejemplo, podría crear una versión de Compose que pueda generar varias rutinas para cada función y ejecutarlas simultáneamente mediante canales. Esto facilitaría la ejecución de canalizaciones de procesamiento de datos simultáneas y la ampliación automática del cuello de botella, mientras que solo se necesita escribir func(A) B s. O podría poner las mismas funciones en un marco que ejecuta miles de copias del programa en un clúster, programando lotes de datos entre ellos (eso es a lo que me referí cuando me vinculé a Flume arriba).

Por lo tanto, si bien poder escribir Map y Filter y Reduce puede parecer aburrido en la superficie, las mismas técnicas abren algunas posibilidades realmente emocionantes para facilitar la computación escalable.

@ChrisHines

Irónicamente, mi primer pensamiento fue que podría usarse para resolver algunos de los desafíos descritos en este artículo: https://blog.merovius.de/2017/07/30/the-trouble-with-opcional-interfaces.html

Es un pensamiento interesante y ciertamente se siente como debería. Pero no veo cómo, todavía. Si toma el ejemplo ResponseWriter , parece que esto podría permitirle escribir envoltorios genéricos con seguridad de tipos, con diferentes métodos dependiendo de lo que admita el ResponseWriter envuelto. Pero, incluso si puede usar diferentes límites en diferentes métodos, aún debe escribirlos. Entonces, si bien puede hacer que la situación sea segura para el tipo en el sentido de que no agrega métodos que no admite, aún debe enumerar todos los métodos que podría admitir, por lo que el middleware aún puede enmascarar algunas interfaces opcionales simplemente por no saber acerca de ellos. Mientras tanto, también puede (incluso sin esta característica) hacer

type Middleware (type RW http.ResponseWriter) struct {
    RW
}

y sobrescriba los métodos selectivos que le interesan, y promueva todos los demás métodos de RW . Por lo tanto, ni siquiera tiene que escribir envoltorios y, de manera transparente, incluso obtener esos métodos que no conocía.

Entonces, suponiendo que obtengamos métodos promovidos para parámetros de tipo incrustados en estructuras genéricas (y espero que lo hagamos), los problemas parecen resolverse mejor con ese método.

Creo que la solución específica para http.ResponseWriter es algo así como errors.Is/As . No es necesario que haya un cambio de idioma, solo una biblioteca adicional para crear un método estándar de ajuste de ResponseWriter y una forma de consultar si alguno de los ResponseWriters en una cadena puede manejar, por ejemplo, wPush. Soy escéptico de que los genéricos sean una buena opción para algo como esto porque el objetivo principal es tener opciones de tiempo de ejecución entre interfaces opcionales, por ejemplo, Push solo está disponible en http2 y no si estoy activando un servidor de desarrollo local http1.

Mirando a través de Github, no creo que haya creado un problema para esta idea, así que tal vez lo haga ahora.

Editar: #39558.

@cajadeherramientas
Supongo que se vería así, junto con su código interno de monomorfización:

package iter

type Any interface{}

type Iterator(type T Any) interface {
    Next() bool
    Value() T
}

type ReversibleIterator(type T Any) interface {
    Iterator(T)
    NextBack() bool
}

type mapIt(type I Iterator(T), T Any, U Any) struct {
    parent I
    mapF func(T) U
}

func (i mapIt(type I Iterator(T))) Next() bool {
    return i.parent.Next()
}

func (i mapIt(type I Iterator(T), T Any, U Any)) Value() U { 
    return i.mapF(i.parent.Value())
}

func (i mapIt(type I ReversibleIterator(T))) NextBack() bool { 
    return i.parent.NextBack()
}

// Monomorphisation
type mapIt<OnlyForward, int, float64> struct {
    parent OnlyForward,
    mapF func(int) float64
}

func (i mapIt<OnlyForward, int, float64>) Next() bool {
    return i.parent.Next()
}

func (i mapIt<OnlyForward, int, float64>) Value() float64 {
    return i.mapF(i.parent.Value())
}

type mapIt<Slice, int, string> struct {
    parent Slice,
    mapF func(int) string
}

func (i mapIt<Slice, int, string>) Next() bool {
    return i.parent.Next()
}

func (i mapIt<Slice, int, string>) Value() string {
    return i.mapF(i.parent.Value())
}

func (i mapIt<Slice, int, string>) NextBack() bool {
    return i.parent.NextBack()
}



Supongo que se vería así, junto con su código interno de monomorfización:

FWIW, aquí hay un tweet mío de hace algunos años que explora cómo podrían funcionar los iteradores en Go with generics. Si hace una sustitución global para reemplazar <T> con (type T) , tiene algo no muy lejos de la propuesta actual: https://twitter.com/rogpeppe/status/425035488425037824

FWIW, debe aclararse que esto es una especie de venta corta de "mapas y filtros y cosas funcionales". Personalmente, no quiero mapear y filtrar sobre estructuras de datos integradas en mi código, por ejemplo (prefiero for-loops). Pero también puede significar...

Veo su punto y no estoy en desacuerdo, y sí, nos beneficiaremos de las cosas que cubren sus ejemplos.
Pero todavía me pregunto cómo se vería afectado algo como k8s u otra base de código con tipos de datos "genéricos" donde los tipos de acciones que se realizan no son mapas o filtros, o al menos van más allá. Me pregunto qué tan efectivos son los Contratos o FGG para aumentar la seguridad de tipo y el rendimiento en ese tipo de contextos.

¿Se pregunta si alguien puede señalar una base de código, con suerte más simple que k8s, que encaja en este tipo de categoría?

@urandom vaya . Entonces, si instancias un mapIt con un parent que implementa ReversibleIterator entonces mapIt tiene un método NextBack() y, si no, no lo tiene. t. ¿Estoy leyendo eso bien?

Pensándolo bien, parece que es útil desde la perspectiva de una biblioteca. Tiene algunos tipos de estructuras genéricas que son bastante abiertas (parámetros de tipo Any ) y tienen muchos métodos, limitados por varias interfaces. Entonces, cuando usa la biblioteca en su propio código, el tipo que incrusta en la estructura le brinda la capacidad de llamar a un determinado conjunto de métodos, por lo que obtiene un determinado conjunto de la funcionalidad de la biblioteca. Cuál es ese conjunto de funcionalidades, se determina en el momento de la compilación en función de los métodos que tiene su tipo.

... Se parece un poco a lo que mencionó @ChrisHines en el sentido de que podría escribir código que tenga más o menos funcionalidad en función de lo que implemente su tipo, pero, de nuevo, es realmente una cuestión de que el conjunto de métodos disponibles aumente o disminuya, no el comportamiento de un solo método, así que sí, no veo cómo se ayuda con esto el secuestrador http2.

De todos modos, muy interesante.

No es que yo haría esto, pero supongo que esto sería posible:

type OverrideX interface {
    GetX() int
}

type OverrideY interface {
    GetY() int
}

type Inheritor(type child Any) struct {
    Parent
    c child
}

func (i Inheritor(type child OverrideX)) GetX() int {
    return i.c.GetX()
}

func (i Inheritor(type child OverrideY)) GetY() int {
    return i.c.GetY()
}

type Parent struct {
    x, y int
}

func (p Parent) GetX() int {
    return p.x
}

func (p Parent) GetY() int {
    return p.y
}

type Child struct {
    x int
}

func (c Child) GetX() int {
    return c.x
}

func main() {
    i := Inheritor(Child){Parent{5, 6}, Child{3}}
    x, y := i.GetX(), i.GetY() // 3, 6
}

De nuevo, más que nada es una broma, pero creo que es bueno explorar los límites de lo que es posible.

Editar: Hm, muestra cómo puede tener diferentes conjuntos de métodos según el tipo de parámetro, pero produce exactamente el mismo efecto que simplemente incrustar Parent en Child . De nuevo, ejemplo tonto;)

No soy un gran fanático de tener métodos que solo se pueden llamar dado un cierto tipo. Dado el ejemplo de @tooolbox , probablemente sería difícil probarlo debido al hecho de que algunos métodos solo se pueden llamar dado un niño específico: es probable que el evaluador se pierda algún caso. Tampoco está muy claro qué métodos están disponibles y requerir un IDE para proporcionar sugerencias no es lo que debería requerir Go. Sin embargo, puede implementar esto usando solo el tipo dado por la estructura haciendo una aserción de tipo en el método.

func (i Inheritor(type child Any)) GetX() int {
    if c, ok := i.c.(OverrideX); ok {
        return c.GetX()
    }
    return i.Parent.GetX()
}

func (i Inheritor(type child Any)) GetY() int {
    if c, ok := i.c.(OverrideY); ok {
        return c.GetY()
    }
    return i.Parent.GetY()
} 

Este código también es de tipo seguro, claro, fácil de probar y probablemente se ejecuta de manera idéntica al original sin confusión.

@TotallyGamerJet
Ese ejemplo en particular es de tipo seguro, sin embargo, otros no lo son, y requerirá pánicos de tiempo de ejecución con tipos incompatibles.

Además, no estoy seguro de cómo el probador podría pasar por alto algún caso, dado que lo más probable es que sean los que escribieron el código genérico en primer lugar. Además, si está claro o no es un poco subjetivo, aunque definitivamente no requiere un IDE para deducirlo. Tenga en cuenta que esto no es una sobrecarga de funciones, el método puede llamarse o no, por lo que no es como si algún caso pudiera omitirse por accidente. Cualquiera puede ver que este método existe para un determinado tipo, y es posible que necesite leerlo nuevamente para comprender qué tipo se requiere, pero eso es todo.

@urandom No quise decir necesariamente con ese ejemplo específico que alguien se perdería un caso, es muy breve. Quise decir que cuando tienes toneladas de métodos solo se pueden llamar determinados tipos. Así que me mantengo firme en no usar subtipos (como me gusta llamarlo). Incluso es posible resolver el "Problema de la expresión" sin usar aserciones de tipo o subtipado. Así es cómo:

type Any interface {}

type Evaler(type t Any) interface {
    Eval() t
}

type Num struct {
    value int
}

func (n Num) Eval() int {
    return n.value
}

type Plus(type a Evaler(type t Any)) struct {
    left a
    right a
}

func (p Plus(type a Evaler(type t Any)) Eval() t {
    return p.left.Eval() + p.right.Eval()
}

func (p Plus(type a Evaler(type t Any)) String() string {
    return fmt.Sprintf("(%s+%s)", p.left, p.right)
}

type Expr interface {
    Evaler
    fmt.Stringer
}

func main() {
    var e Expr = Plus(Num){Num{1}, Num{2}}
    var v int = e.Eval() // 3
    var s string = e.String() // "(1+2)"
}

Cualquier uso indebido del método Eval debe detectarse en el momento de la compilación debido al hecho de que no está permitido llamar a Eval en Plus con un tipo que no implementa la adición. Aunque es posible usar incorrectamente String() (posiblemente agregando estructuras), una buena prueba debería detectar esos casos. Y Go generalmente abraza la simplicidad sobre la "corrección". Lo único que se gana con los subtipos es más confusión en los documentos y en el uso. Si puede proporcionar un ejemplo que requiera subtipificación, podría estar más inclinado a pensar que es una buena idea, pero actualmente no estoy convencido.
EDITAR: error corregido y mejorado

@TotallyGamerJet en su ejemplo, el método String debería llamar a String recursivamente, no a Eval

@TotallyGamerJet en su ejemplo, el método String debería llamar a String recursivamente, no a Eval

@mágico
No estoy seguro que quieres decir. El tipo de estructura Plus es un Evaler que no garantiza que fmt.Stringer esté satisfecho. Llamar a String() en ambos Evalers requeriría una aserción de tipo y, por lo tanto, no sería seguro para tipos.

@TotallyGamerJet
Desafortunadamente, esa es la idea del método String. Debería llamar recursivamente a cualquier método String en sus miembros, de lo contrario no tiene sentido. Pero ya ve que requeriría una afirmación de tipo y pánico si no puede asegurarse de que el método en el tipo Plug requiere un tipo a que tiene un método String

@urandom
¡Estás en lo correcto! Sorprendentemente, Sprintf hará ese tipo de afirmación por usted. Por lo tanto, puede enviar los campos izquierdo y derecho. Aunque todavía puede generar pánico si los tipos en Plus no implementan Stringer, estoy de acuerdo con eso porque es posible evitar pánicos usando el verbo %v para imprimir la estructura (llamará a String( ) si está disponible). Creo que esta solución es clara y cualquier otra incertidumbre debe documentarse en el código. Así que todavía no estoy convencido de por qué es necesario subtipificar.

@TotallyGamerJet
Personalmente, todavía no veo qué problemas pueden surgir si se permite tener métodos con diferentes restricciones. El método todavía está allí, y el código describe claramente qué argumentos (y receptor, en el caso especial) se requieren.
Así como tener un método, aceptar un argumento string , o un receptor MyType , es claramente legible y sin ambigüedades, así sería también la siguiente definición:

func (rec MyType(type T SomeInterface(T)) Foo() T

Los requisitos están claramente marcados en la propia firma. IE es de MyType(type T SomeInterface(T)) y nada más.

Cambio https://golang.org/cl/238003 menciona este problema: design: add go2draft-type-parameters.md

Cambio https://golang.org/cl/238241 menciona este problema: content: add generics-next-step article

¡La Navidad es temprana!

  • Puedo ver que se hizo un gran esfuerzo para hacer que el documento de diseño fuera accesible, se nota y es genial y muy apreciado.
  • Esta iteración es una mejora importante en mi opinión y pude ver que esto se implementa tal como está.
  • De acuerdo con casi todo el razonamiento y la lógica.
  • Así, si especifica una restricción para un solo parámetro de tipo, debe hacerlo para todos.
  • Comparable suena bien.
  • Las listas de tipos en las interfaces no son malas; estoy de acuerdo en que es mejor que los métodos de operador, pero en mi opinión, es probablemente el área más importante para una mayor discusión.
  • La inferencia de tipos es (todavía) genial.
  • La inferencia para restricciones parametrizadas de tipo de argumento único parece más inteligente que claridad.
  • Me gusta "No afirmamos que esto sea simple" en el ejemplo del gráfico. Esta bien.
  • (type *T constraint) parece una buena solución para el problema del puntero.
  • Totalmente de acuerdo con el cambio de func(x(T)) .
  • Creo que queremos inferencia de tipos para literales compuestos desde el principio. 😄

¡Gracias al equipo Go! 🎉

https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#comparable-types-in-constraints

Creo que comparable es más como un tipo incorporado que una interfaz. Creo que es un pequeño error en el borrador de la propuesta.

type ComparableHasher interface {
    comparable
    Hash() uintptr
}

necesitan ser

type ComparableHasher interface {
    type comparable
    Hash() uintptr
}

El patio de recreo también parece indicar que debe ser type comparable
https://go2goplay.golang.org/p/mhrl0xYsMyj

EDITAR: Ian Lance Taylor y Robert Griesemer están arreglando la herramienta go2go (era un pequeño error en el traductor go2go, no en el borrador. El borrador del diseño era correcto)

¿Se ha pensado en permitir que las personas escriban sus propias tablas hash genéricas y cosas por el estilo? ISTM que actualmente es muy limitado (especialmente en comparación con el mapa incorporado). Básicamente, el mapa integrado tiene comparable como restricción clave, pero, por supuesto, == y != no son suficientes para implementar una tabla hash. Una interfaz como ComparableHasher solo transfiere la responsabilidad de escribir una función hash a la persona que llama, no responde a la pregunta de cómo se vería realmente (además, la persona que llama probablemente no debería ser responsable de esto; escribir buenas funciones hash es difícil). Por último, el uso de punteros como claves puede ser fundamentalmente imposible: convertir un puntero en un uintptr para usarlo como índice podría hacer que el GC mueva el pointee y, por lo tanto, el cubo cambie (salvo este problema, exponiendo un func hash(type T comparable)(v T) uintptr predeclarado).

Bien puedo aceptar "no es realmente factible" como respuesta, solo tengo curiosidad por saber si lo pensaste :)

@gertcuykens Comprometí una solución a la herramienta go2go para manejar comparable según lo previsto.

@Merovius Esperamos que las personas que escriben una tabla hash genérica proporcionen su propia función hash y posiblemente su propia función de comparación. Al escribir su propia función hash, el paquete https://golang.org/pkg/hash/maphash/ puede ser útil. Tiene razón en que el hash de un valor de puntero debe depender del valor al que apunta ese puntero; no puede depender del valor del puntero convertido a uintptr .

No estoy seguro si esto es una limitación de la implementación actual de la herramienta, pero un intento de devolver un tipo genérico restringido por una interfaz devuelve un error:
https://go2goplay.golang.org/p/KYRFL-vrcUF

Ayer implementé un caso de uso del mundo real que tenía para genéricos . Es una abstracción de tubería genérica que permite escalar las etapas de la tubería de forma independiente y admite la cancelación y el manejo de errores (no se ejecuta en el patio de recreo, porque depende de errgroup , pero ejecutarlo con la herramienta go2go parece trabajo). Algunas observaciones:

  • Fue bastante divertido. Tener un verificador de tipos en funcionamiento en realidad ayudó mucho al iterar en el diseño, al traducir las fallas de diseño en errores de tipo. El resultado final es ~100 LOC, incluidos los comentarios. Entonces, en general, la experiencia de escribir código genérico es agradable, en mi opinión.
  • Este caso de uso al menos funciona sin problemas con la inferencia de tipo, no se necesitan instancias explícitas. Creo que es un buen augurio para el diseño de inferencia.
  • Creo que este ejemplo se beneficiaría de la capacidad de tener métodos con parámetros de tipo adicionales. Necesitar una función de nivel superior para Compose significa que la construcción de la canalización ocurre a la inversa: las últimas etapas de la canalización deben construirse para pasarla a las funciones que construyen las etapas anteriores. Si los métodos pudieran tener parámetros de tipo, podría tener Stage como un tipo concreto y hacer func (s *Stage(A, B)) Compose(type C)(n int, f func(B) C) *Stage(A, C) . Y la construcción de la tubería sería en el mismo orden en que se conecta (vea el comentario en el patio de recreo). Por supuesto, también podría haber una API más elegante en el borrador existente que no veo; es difícil demostrar que es negativo. Me interesaría ver un ejemplo de trabajo de eso.

En general, me gusta el nuevo borrador, FWIW :) La eliminación de contratos de la OMI es una mejora, al igual que la nueva forma de especificar los operadores requeridos a través de listas de tipos.

[editar: se corrigió un error en mi código en el que podía ocurrir un interbloqueo si fallaba una etapa de canalización. La concurrencia es difícil]

Una pregunta para la rama de herramientas: ¿se mantendrá al día con la última versión (es decir, v1.15, v1.15.1, ...)?

@urandom : tenga en cuenta que el valor que está devolviendo en su código es del tipo Foo (T). Cada
dicha instanciación de tipo produce un nuevo tipo definido, en este caso Foo(T).
(Por supuesto, si tiene varios Foo(T) en el código, todos son iguales
tipo definido).

Pero el tipo de resultado de su función es V, que es un parámetro de tipo. Nota
que el parámetro de tipo está limitado por la interfaz de Valuer, pero es
_no_ una interfaz (o incluso esa interfaz). V es un parámetro de tipo que es
un nuevo tipo de tipo sobre el cual sabemos cosas descritas por su restricción.
Con respecto a la asignabilidad, actúa como un tipo definido llamado V.

Así que estás tratando de asignar un valor de tipo Foo(T) a una variable de tipo V
(que no es ni Foo(T) ni Valuer(T), solo tiene propiedades descritas por
Tasador (T)). Por lo tanto, la asignación falla.

(Aparte, todavía estamos perfeccionando nuestra comprensión de los parámetros de tipo
y eventualmente necesitaremos deletrearlo con suficiente precisión para que podamos escribir un
Especificaciones. Pero tenga en cuenta que cada parámetro de tipo es efectivamente un nuevo
tipo definido sobre el que sabemos solo lo que especifica su restricción de tipo).

Quizás quisiste escribir esto: https://go2goplay.golang.org/p/8Hz6eWSn8Ek?

@Inuart Si por rama de herramienta se refiere a la rama dev.go2go: este es un prototipo, se ha construido teniendo en cuenta la conveniencia y con fines de experimentación. Queremos que la gente juegue con él y trate de escribir código, pero no es una buena idea _confiar_ en el traductor para el software de producción. Muchas cosas pueden cambiar (incluso la sintaxis, si es necesario). Vamos a corregir errores y ajustar el diseño a medida que aprendamos de los comentarios. Mantenerse al día con los últimos lanzamientos de Go parece menos importante.

Ayer implementé un caso de uso del mundo real que tenía para genéricos. Es una abstracción de tubería genérica que permite escalar las etapas de la tubería de forma independiente y admite la cancelación y el manejo de errores (no se ejecuta en el patio de recreo, porque depende de errgroup, pero ejecutarlo con la herramienta go2go parece funcionar).

me gusta el ejemplo Acabo de leerlo completo y lo que más me hizo tropezar (ni siquiera vale la pena explicarlo) no tenía nada que ver con los genéricos involucrados. Creo que la misma construcción sin genéricos no sería mucho más fácil de entender. También es definitivamente una de esas cosas que quieres escribir una vez, con pruebas, y no tener que volver a jugar más tarde.

Una cosa que podría ayudar a la legibilidad y la revisión es si la herramienta Go tuviera una forma de mostrar la versión monomorfizada del código genérico, para que pueda ver cómo resultan las cosas. Podría ser inviable, en parte porque es posible que las funciones ni siquiera se monomorficen en la implementación final del compilador, pero creo que sería valioso si fuera posible.

Creo que este ejemplo se beneficiaría de la capacidad de tener métodos con parámetros de tipo adicionales.

También vi ese comentario en tu patio de recreo; definitivamente, la sintaxis de llamada alternativa parece más legible y directa. ¿Podría explicar esto con más detalle? Apenas entendí tu código de ejemplo, tengo problemas para dar el salto :)

Así que estás tratando de asignar un valor de tipo Foo(T) a una variable de tipo V
(que no es ni Foo(T) ni Valuer(T), solo tiene propiedades descritas por
Tasador (T)). Por lo tanto, la asignación falla.

Gran explicación.

... De lo contrario, es triste ver que la publicación de HN fue secuestrada por la multitud de Rust. Hubiera sido bueno recibir más comentarios de Gophers sobre la propuesta.

Dos preguntas para el equipo Go:

¿Hay alguna diferencia entre estos dos, o es un error en el patio de recreo de go2? El primero compila, el segundo da error.

type Addable interface {
    type int, float64
}

func Add(type T Addable)(a, b T) T {
  return a + b
}
type Addable interface {
    type int, float64, string
}

func Add(type T Addable)(a, b T) T {
  return a + b
}

Falla con: invalid operation: operator + not defined for a (variable of type T)

Bueno, esta fue una sorpresa muy inesperada y agradable. Esperaba encontrar una manera de probar esto en algún momento, pero no lo esperaba pronto.

En primer lugar, encontré un error: https://go2goplay.golang.org/p/1r0NQnJE-NZ

En segundo lugar, construí un ejemplo de iterador y me sorprendió un poco descubrir que ese tipo de inferencia no funciona. Puedo hacer que devuelva un tipo de interfaz directamente, pero no pensé que no sería capaz de inferir ese tipo, ya que toda la información de tipo que necesita proviene del argumento.

Editar: Además, como han dicho varias personas, creo que permitir que se agreguen nuevos tipos durante las declaraciones de métodos sería bastante útil. En lo que respecta a la implementación de la interfaz, podría simplemente no permitir la implementación de la interfaz, solo permitir la implementación si la interfaz también requiere genéricos allí ( type Example interface { Method(type T someConstraint)(v T) bool } ), o, posiblemente, podría hacer que implemente la interfaz si _cualquiera_ es posible una variante implementa la interfaz, y luego hace que la llamada se restrinja a lo que la interfaz quiere si se llama a través de la interfaz. Por ejemplo,

```ir
tipo Interfaz interfaz {
Obtener (cadena) cadena
}

tipo Ejemplo(tipo T) struct {
v T
}

// Esto solo funcionará porque Interface.Get es más específico que Example.Get.
func (e Ejemplo(T)) Get(tipo R)(v R) T {
return fmt.Sprintf("%v: %v", v, ev)
}

func DoSomething(inter interfaz) {
// El subyacente es Example(string) y Example(string).Get(string) se asume porque es requerido.
fmt.Println(inter.Get("ejemplo"))
}

función principal() {
// Permitido porque Example(string).Get(string) es posible.
HazAlgo(Ejemplo(cadena){v: "Un ejemplo."})
}

@DeedleFake Lo primero que informa no es un error. Deberá escribir https://go2goplay.golang.org/p/qo3hnviiN4k en este momento. Esto está documentado en el borrador de diseño. En una lista de parámetros, escribir a(b) se interpreta como a (b) ( a del tipo entre paréntesis b ) por compatibilidad con versiones anteriores. Podemos cambiar eso en el futuro.

El ejemplo de Iterator es interesante: parece un error a primera vista. Presente un error (instrucciones en la publicación del blog) y asígnemelo. Gracias.

@Kashomon La publicación del blog (https://blog.golang.org/generics-next-step) sugiere la lista de correo para la discusión y la presentación de problemas separados para errores. Gracias.

Creo que el problema con + ya se solucionó.

@cajadeherramientas

Una cosa que podría ayudar a la legibilidad y la revisión es si la herramienta Go tuviera una forma de mostrar la versión monomorfizada del código genérico, para que pueda ver cómo resultan las cosas. Podría ser inviable, en parte porque es posible que las funciones ni siquiera se monomorficen en la implementación final del compilador, pero creo que sería valioso si fuera posible.

La herramienta go2go puede hacer esto. En lugar de usar go tool go2go run x.go2 , escriba go tool go2go translate x.go2 . Eso producirá un archivo x.go con el código traducido.

Dicho esto, tengo que decir que es bastante difícil de leer. No imposible, pero no fácil.

@griesemer

Entiendo que el argumento de devolución puede ser una interfaz, pero realmente no entiendo por qué no puede ser el tipo genérico en sí.

Puede, por ejemplo, usar ese mismo tipo genérico como parámetro de entrada, y eso funciona bien:
https://go2goplay.golang.org/p/LuDrlT3zLRb
¿Funciona esto porque el tipo ya ha sido instanciado?

@urandom escribió:

Entiendo que el argumento de devolución puede ser una interfaz, pero realmente no entiendo por qué no puede ser el tipo genérico en sí.

Teóricamente, podría, pero no tiene sentido hacer un tipo de retorno genérico cuando el tipo de retorno no es genérico porque está determinado por el bloque de funciones, es decir, por el valor de retorno.

Normalmente, los parámetros genéricos están totalmente determinados por la tupla del valor del parámetro o por el tipo de aplicación de la función en el sitio de la llamada (determina la creación de instancias del tipo de retorno genérico).

Teóricamente, también podría permitir parámetros de tipo genérico que no están determinados por la tupla del valor del parámetro y deben proporcionarse explícitamente, por ejemplo:

func f(type S)(i int) int
{
    s S =...
    return 2
}

No sé cuánto sentido tiene esto.

@urandom No quise decir necesariamente con ese ejemplo específico que alguien se perdería un caso, es muy breve. Quise decir que cuando tienes toneladas de métodos solo se pueden llamar determinados tipos. Así que me mantengo firme en no usar subtipos (como me gusta llamarlo). Incluso es posible resolver el "Problema de la expresión" sin usar aserciones de tipo o subtipado. Así es cómo:

type Any interface {}

type Evaler(type t Any) interface {
  Eval() t
}

type Num struct {
  value int
}

func (n Num) Eval() int {
  return n.value
}

type Plus(type a Evaler(type t Any)) struct {
  left a
  right a
}

func (p Plus(type a Evaler(type t Any)) Eval() t {
  return p.left.Eval() + p.right.Eval()
}

func (p Plus(type a Evaler(type t Any)) String() string {
  return fmt.Sprintf("(%s+%s)", p.left, p.right)
}

type Expr interface {
  Evaler
  fmt.Stringer
}

func main() {
  var e Expr = Plus(Num){Num{1}, Num{2}}
  var v int = e.Eval() // 3
  var s string = e.String() // "(1+2)"
}

Cualquier uso indebido del método Eval debe detectarse en el momento de la compilación debido al hecho de que no está permitido llamar a Eval en Plus con un tipo que no implementa la adición. Aunque es posible usar incorrectamente String() (posiblemente agregando estructuras), una buena prueba debería detectar esos casos. Y Go generalmente abraza la simplicidad sobre la "corrección". Lo único que se gana con los subtipos es más confusión en los documentos y en el uso. Si puede proporcionar un ejemplo que requiera subtipificación, podría estar más inclinado a pensar que es una buena idea, pero actualmente no estoy convencido.
EDITAR: error corregido y mejorado

No sé, ¿por qué no usas '<> '?

@99yun
Consulte las preguntas frecuentes incluidas con el borrador actualizado.

¿Por qué no usar la sintaxis F\?como C++ y Java?
Al analizar código dentro de una función, como v := F\, en el punto de ver el < es ambiguo si estamos viendo una instanciación de tipo o una expresión usando el operador <. Resolver eso requiere una anticipación efectivamente ilimitada. En general, nos esforzamos por mantener la eficiencia del analizador Go.

@urandom El cuerpo de una función genérica siempre se verifica sin instanciación (*); en general (si se exporta, por ejemplo) no podemos saber cómo se instanciará. Al realizar la verificación de tipo, solo puede confiar en la información disponible. Si el tipo de resultado es un parámetro de tipo y la expresión de retorno es de un tipo diferente que no es compatible con la asignación, el retorno no puede funcionar. O, en otras palabras, si se invoca una función genérica con argumentos de tipo (posiblemente inferidos), el cuerpo de la función no se verifica de nuevo con esos argumentos de tipo. Solo verifica que los argumentos de tipo satisfagan las restricciones de la función genérica (después de instanciar la firma de la función con esos argumentos de tipo). Espero que ayude.

(*) Más precisamente, la función genérica tiene verificación de tipo como si fuera instanciada con sus propios parámetros de tipo; los parámetros de tipo son tipos reales; solo sabemos sobre ellos tanto como nos dicen sus limitaciones.

Por favor, continuemos esta discusión en otro lugar. Si tiene más preguntas con un fragmento de código que cree que debería estar funcionando, presente un problema para que podamos discutirlo allí. Gracias.

No parece haber una forma de usar una función para crear un valor cero de una estructura genérica. Tomemos por ejemplo esta función:

func zero(type T)() T {
    var zero T
    return zero
}

Parece funcionar para los tipos básicos (int, float32, etc.). Sin embargo, cuando tienes una estructura que tiene un campo genérico, las cosas se vuelven extrañas. Toma por ejemplo:

type Opt(type T) struct {
    val T
}

func (o Opt(T)) Do() { /*stuff*/ }

Todo parece bien. Sin embargo, al hacer:

opt := zero(Opt(int))
opt.Do() 

no se compila dando el error: opt.Do undefined (type func() Opt(int) has no field or method Do) Puedo entender si no es posible hacer esto, pero es extraño pensar que es una función cuando se supone que int es parte del tipo Opt. Pero lo que es más extraño es que es posible hacer esto:

opt := zero(Opt)      //  But somehow this line compiles
opt(int).Do()         // This will panic

No estoy seguro de qué parte es un error y qué parte está destinada.
Código: https://go2goplay.golang.org/p/M0VvyEYwbQU

@TotallyGamerJet

Su función zero() no tiene argumentos, por lo que no se está realizando ninguna inferencia de tipo. Tienes que instanciar la función zero y luego llamarla.

opt := zero(Opt(int))()
opt.Do()

https://go2goplay.golang.org/p/N6ip-nm1BP-

@cajadeherramientas
Ah, sí. Pensé que estaba proporcionando el tipo, pero olvidé el segundo paréntesis para llamar a la función. Todavía me estoy acostumbrando a estos genéricos.

Siempre he entendido que no tener genéricos en Go era una decisión de diseño, no un descuido. Ha hecho que Go sea mucho más simple y no puedo comprender la paranoia exagerada contra una simple copia duplicada. En nuestra empresa, hemos creado toneladas de código Go y nunca hemos encontrado una sola instancia en la que preferiríamos los genéricos.

Para nosotros, definitivamente hará que Go se sienta menos Go y parece que la multitud exagerada finalmente logró afectar el desarrollo de Go en una dirección equivocada. No podían simplemente dejar Go en su belleza simplista, no, tenían que seguir quejándose y quejándose hasta que finalmente se salían con la suya.

Lo siento, no es para degradar a nadie, pero así es como comienza la destrucción de un lenguaje bellamente diseñado. ¿Que sigue? Si seguimos cambiando cosas, como le gustaría a mucha gente, terminaremos con "C++" o "JavaScript".

¡Solo deja Go de la forma en que estaba destinado a ser!

@ iio7 Soy el IQ más bajo de todos aquí, mi futuro depende de asegurarme de que puedo leer el código de otras personas. La exageración acaba de comenzar no solo por los genéricos, sino porque el nuevo diseño no requiere un cambio de idioma en la propuesta actual, por lo que todos estamos entusiasmados de que haya una ventana para mantener las cosas simples y aún tener algunas ventajas genéricas y funcionales. No me malinterpreten, sé que siempre habrá alguna persona en el equipo que escriba código como un científico espacial y yo, el mono, ¿se supone que debo entenderlo así? Entonces, los ejemplos que ves ahora son los del científico espacial y, para ser honesto, sí, me tomó un tiempo leerlo, pero al final, con un poco de prueba y error, sé lo que están tratando de programar. Todo lo que digo es que confíes en Ian, Robert y los demás, aún no han terminado con el diseño. No me sorprendería que en un año más o menos haya herramientas que ayuden al compilador a hablar un lenguaje mono simple y perfecto, sin importar cuán difícil sea el código genérico de cohetes que le lances. El mejor comentario que puede dar es reescribir algunos ejemplos y señalar si algo está demasiado diseñado para que puedan asegurarse de que el compilador se queje o sea reescrito por algo como la herramienta veterinaria automáticamente.

Leí las preguntas frecuentes sobre <> pero para una persona estúpida como yo, ¿cómo es más difícil para el analizador determinar si es una llamada genérica si se ve así v := F<T> en lugar de v := F(T) ?

Además de eso, creo que el analizador, por supuesto, debe mantenerse rápido, pero no olvidemos también cuál es más fácil de leer para el programador, que en mi opinión es igualmente importante. ¿Es más fácil entender lo que hace v := F(T) de inmediato? ¿O es v := F<T> más fácil? También es importante tener en cuenta :)

Sin argumentar a favor ni en contra v := F<T> , solo planteando algunas ideas que podrían valer la pena considerar.

Esto es legal Vaya hoy :

    f, c, d, e := 1, 2, 3, 4
    a, b := f < c, d > (e)
    fmt.Println(a, b) // true false

No tiene sentido discutir los paréntesis angulares a menos que proporcione una propuesta sobre qué hacer al respecto (¿romper la compatibilidad?). Es para todos los efectos y propósitos una cuestión muerta. Efectivamente, no hay posibilidad de que el equipo de Go adopte los corchetes angulares. Por favor discuta cualquier otra cosa.

Editar para agregar: Lo siento si este comentario fue demasiado breve. Hay mucha discusión sobre los paréntesis angulares en Reddit y HN, lo cual es muy frustrante para mí porque el problema de la retrocompatibilidad es bien conocido desde hace mucho tiempo por las personas que se preocupan por los genéricos. Entiendo por qué la gente prefiere los corchetes angulares, pero no es posible sin un cambio radical.

Gracias por tu comentario @iio7. Siempre existe un riesgo distinto de cero de que las cosas se salgan de control. Es por eso que hemos estado usando la máxima precaución en el camino. Creo que lo que tenemos ahora es un diseño mucho más limpio y más ortogonal que el que teníamos el año pasado; y personalmente espero que podamos hacerlo aún más simple, especialmente cuando se trata de listas de tipos, pero lo descubriremos a medida que aprendamos más. (Irónicamente, cuanto más ortogonal y limpio se vuelve el diseño, más poderoso será y más complejo se podrá escribir el código). Las palabras finales aún no se han pronunciado. El año pasado, cuando tuvimos el primer diseño potencialmente viable, la reacción de mucha gente fue similar a la tuya: "¿Realmente queremos esto?" Esta es una excelente pregunta y debemos tratar de responderla lo mejor que podamos.

La observación de @gertcuykens también es correcta: naturalmente, las personas que juegan con el prototipo go2go están explorando sus límites tanto como sea posible (que es lo que queremos), pero en el proceso también producen código que probablemente no pasaría en una producción adecuada. ajuste. Por ahora he visto un montón de código genérico que es muy difícil de descifrar.

Hay situaciones en las que el código genérico sería claramente una victoria; Estoy pensando en algoritmos concurrentes genéricos que nos permitirían poner un código algo sutil en una biblioteca. Por supuesto, hay varias estructuras de datos de contenedores y cosas como clasificación, etc. Probablemente, la gran mayoría del código no necesita genéricos en absoluto. A diferencia de otros lenguajes, donde las características genéricas son centrales para mucho de lo que se hace en el idioma, en Go, las características genéricas son solo otra herramienta en el conjunto de herramientas de Go; no es el bloque de construcción fundamental sobre el cual se construye todo lo demás.

A modo de comparación: en los primeros días de Go, todos tendíamos a abusar de las rutinas y los canales. Me tomó un tiempo aprender cuándo eran apropiados y cuándo no. Ahora tenemos unas pautas más o menos establecidas y las usamos sólo cuando es realmente apropiado. Espero que suceda lo mismo si tuviéramos genéricos.

Gracias.

De la sección del borrador del diseño sobre sintaxis basadas en [T] :

El lenguaje generalmente permite una coma final en una lista separada por comas, por lo que se debe permitir A[T,] si A es un tipo genérico, pero normalmente no se permitiría para una expresión de índice. Sin embargo, el analizador no puede saber si A es un tipo genérico o un valor de tipo de división, matriz o mapa, por lo que este error de análisis no se puede informar hasta que se complete la verificación de tipo. De nuevo, solucionable pero complicado.

¿No se podría resolver esto con bastante facilidad simplemente haciendo que la coma final sea completamente legal en las expresiones de índice y luego haciendo que gofmt la elimine?

@DeedleFake Posiblemente. Esa sería sin duda una salida fácil; pero también parece un poco feo, sintácticamente. No recuerdo todos los detalles, pero una versión anterior admitía parámetros de tipo de estilo [tipo T]. Consulte la rama dev.go2go, confirme 3d4810b5ba donde se eliminó el soporte. Uno podría desenterrar eso de nuevo e investigar.

¿Se puede limitar la longitud de los argumentos genéricos en cada lista [] a la mayoría para evitar este problema, al igual que los tipos genéricos incorporados?

  • [NUEVO TESTAMENTO
  • []T
  • mapa[K]T
  • chan t

Tenga en cuenta que los últimos argumentos en los tipos genéricos integrados no están incluidos en [] .
La sintaxis de declaración genérica es como: https://github.com/dotaheor/unify-Go-builtin-and-custom-generics#the -generic-declaration-syntax

@dotaheor No estoy seguro exactamente de lo que está preguntando, pero es claramente necesario admitir múltiples argumentos de tipo para un tipo genérico. Por ejemplo, https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#containers .

@ianlancetaylor
Lo que quiero decir es que cada parámetro de tipo está encerrado en [] , por lo que el tipo en su enlace se puede declarar como:

type Map[type K][type V] struct

Cuando se usa, es como:

var m Map[string]int

Un argumento de tipo no encerrado por [] indica el final del uso de un tipo genérico.

Mientras pensaba en ordenar arreglos #39355 junto con genéricos, descubrí que "comparable" se maneja de manera especial en el borrador de genéricos actual (presumiblemente debido a que no se pueden enumerar fácilmente todos los tipos comparables en una lista de tipos) como una restricción de tipo predeclarada .

Sería bueno si el borrador de genéricos se cambiara para definir también "ordenado"/"ordenable" similar a cómo se predefine "comparable". Es una relación relacionada de uso común en valores del mismo tipo y esto permitiría futuras extensiones del lenguaje go para definir el orden en más tipos (matrices, estructuras, segmentos, tipos de suma, enumeraciones comprobadas, ...) sin encontrarse con la complicación que no todos los tipos ordenados serían enumerables en una lista de tipos como "comparable".

No estoy sugiriendo que, para decidirse, se deban ordenar más tipos en la especificación del lenguaje, pero este cambio a los genéricos lo deja más compatible con dicho cambio (una restricción. El código ordenado no tendría que ser algo mágico generado por el compilador más adelante o quedaría en desuso si se usa una lista de tipos). La clasificación de paquetes podría comenzar con la restricción de tipo predeclarada "ordenada" y luego podría "simplemente" funcionar con, por ejemplo, matrices si alguna vez se cambiaron y no se solucionó la restricción utilizada.

@martisch Creo que esto solo debería suceder una vez que se extiendan los tipos ordenados. Actualmente, constraints.Ordered podría enumerar todos los tipos (eso no funciona para comparable , debido a que los punteros, las estructuras, las matrices,... son comparables, por lo que tiene que ser mágico. Pero ordered actualmente está limitado a un conjunto finito de tipos subyacentes integrados) y los usuarios pueden confiar en eso. Si extendemos los pedidos a las matrices (por ejemplo), aún podemos agregar nuevas restricciones mágicas ordered e incrustarlas en constraints.Ordered . Esto significa que todos los usuarios de constraints.Ordered se beneficiarían automáticamente de la nueva restricción. Por supuesto, los usuarios que escriben su propia lista de tipos explícita no se beneficiarán, pero es lo mismo si agregamos ordered ahora, para los usuarios que no incrustan eso .

Entonces, en mi opinión, no se pierde nada en retrasar eso hasta que sea realmente significativo. No deberíamos agregar ningún posible conjunto de restricciones como un identificador predeclarado, y mucho menos cualquier posible conjunto de restricciones futuras :)

Si extendemos los pedidos a las matrices (por ejemplo), aún podemos agregar nuevas restricciones mágicas ordered e incrustarlas en constraints.Ordered .

@Merovius Ese es un buen punto en el que no había pensado. Esto permite extender constraints.Ordered en el futuro de manera consistente. Si también habrá un constraints.Comparable entonces encaja muy bien en la estructura general.

@martisch , tenga en cuenta que ordered , a diferencia comparable , no es coherente como tipo de interfaz a menos que también definamos un orden total (global) entre tipos concretos, o prohibamos el uso de código no genérico < en variables de tipo ordered , o prohibir el uso de comparable como un tipo de interfaz de tiempo de ejecución general.

De lo contrario, la transitividad de los "implementos" se rompe. Considere este fragmento de programa:

    var x constraints.Ordered = int(0)
    var y constraints.Ordered = string("0")
    fmt.Println(x < y)

¿Qué debería generar? (¿La respuesta es intuitiva o arbitraria?)

@bcmills
¿Qué pasa con fun (<)(type T Ordered)(t1 T,t2 T) Bool?

Para comparar tipos aritméticos de diferente tipo:

Si alguna aritmética S implementa solo Ordered(T) para S<:T , entonces:

//Isn't possible I think
interface SorT(S,T)
{ 
type S,T
}

fun (<)(type R SorT(S,T), S Ordered(R), T Ordered(R))(s S, t T) Bool

debería ser único.

Para el polimorfismo en tiempo de ejecución, necesitaría que Ordered sea parametrizable.
O:
Divide Ordenado en tipos de tupla y luego vuelve a escribir (<) para que sea:

//but isn't supported that either
fun(<)(type R Ordered)(s R.0,t R.1)

¡Hola!
Tengo una pregunta.

¿Hay alguna manera de hacer una restricción de tipo que pase solo tipos genéricos con un parámetro de tipo?
Algo que pasa solo Result(T) / Option(T) /etc pero no solo T .
Lo intenté

type Box(type T) interface {
    Val() (T, bool)
}

pero requiere el método Val()

type Box(type T) interface{}

es similar a interface{} , es decir, Any

también probé https://go2goplay.golang.org/p/lkbTI7yppmh -> la compilación falla

type Box(type T) interface {
       type Box(T)
}

https://go2goplay.golang.org/p/5NsKWNa3E1k -> la compilación falla

type Box(type T) interface{}

type Generic(type T) interface {
    type Box(T)
}

https://go2goplay.golang.org/p/CKzE2J-YOpD -> no funciona

type Box(type T) interface{}

type Generic(type T Box(T)) interface {}

¿Se espera este comportamiento o es solo un error de verificación de tipo?

@tdakkota Las restricciones se aplican a los argumentos de tipo y se aplican a la forma completamente instanciada de los argumentos de tipo. No hay forma de escribir una restricción de tipo que imponga requisitos en la forma no instanciada de un argumento de tipo.

Consulte las preguntas frecuentes incluidas con el borrador actualizado.

¿Por qué no usar la sintaxis F?como C++ y Java?
Al analizar código dentro de una función, como v := F, en el punto de ver el < es ambiguo si estamos viendo una instanciación de tipo o una expresión usando el operador <. Resolver eso requiere una anticipación efectivamente ilimitada. En general, nos esforzamos por mantener la eficiencia del analizador Go.

@TotallyGamerJet ¡Lo que sea!

¿Cómo negociar el valor cero del tipo genérico? Sin enumeración, ¿cómo podemos lidiar con el valor opcional?
Por ejemplo: la versión genérica de vector , y una función llamada First devuelven el primer elemento si su longitud es > 0 o el valor cero del tipo genérico.
¿Cómo escribimos dicho código? Como no sabemos qué tipo de vector, si chan/slice/map , podemos return (nil, false) , si struct o primitive type como string , int , bool , ¿cómo tratarlo?

@leaxoy

var zero T debería ser suficiente

@leaxoy

var zero T debería ser suficiente

¿Una variable mágica global como nil ?

@leaxoy
var zero T debería ser suficiente

¿Una variable mágica global como nil ?

Hay una propuesta en discusión para este tema; consulte la propuesta: Ir 2: valor cero universal con inferencia de tipo #35966 .

Examina varias sintaxis alternativas nuevas para una expresión (no una declaración como var zero T ) que siempre devolverá el valor cero de un tipo.

El valor cero parece factible actualmente, pero ¿puede ocupar espacio en la pila o en el montón? ¿Deberíamos considerar usar enum Option para completar esto en un solo paso?
De lo contrario, si el valor cero no ocupa espacio, sería mejor y no es necesario agregar una enumeración.

El valor cero parece factible actualmente, pero ¿puede ocupar espacio en la pila o en el montón?

Históricamente, creo, el compilador Go ha optimizado ese tipo de casos. No estoy demasiado preocupado.

Se puede especificar un valor de tipo predeterminado en las plantillas de C++. ¿Se ha considerado una construcción similar para los parámetros de tipo genérico go? Potencialmente, esto haría posible adaptar los tipos existentes sin romper el código existente.

Por ejemplo, considere el tipo asn1.ObjectIdentifier existente que es un []int . Un problema con este tipo es que no cumple con la especificación ASN.1, que establece que cada sub-oid puede ser un INTEGER de longitud arbitraria (por ejemplo *big.Int ). Potencialmente ObjectIdentifier podría modificarse para aceptar un parámetro genérico, pero eso rompería una gran cantidad de código existente. Si hubiera una manera de especificar que int es el valor del parámetro predeterminado, tal vez eso permitiría adaptar el código existente.

type SignedInteger interface {
    type int, int32, int64, *big.Int
}
type ObjectIdentifier(type T SignedInteger) []T
// type ObjectIdentifier(type T SignedInteger=int) []T  // `int` would be the default instantiation type.

// New code with generic awareness would compile in go2.
var oid1 ObjectIdentifier(int) = ObjectIdentifier(int){1, 2, 3}

// But existing code would fail to compile:
var oid1 ObjectIdentifier = ObjectIdentifier{1, 2, 3}

Para que quede claro, los asn1.ObjectIdentifier anteriores son solo un ejemplo. No estoy diciendo que usar genéricos sea la única o la mejor manera de resolver el problema de cumplimiento de ASN.1.

Además, ¿existen planes para permitir límites de interfaz finitos parametrizables?:

type Ordable(type T, S) interface {
    type S, type T
}

Cómo admitir la condición where en el parámetro de tipo.
¿Podemos escribir tal código:

type Vector(type T) struct {
    vec []T
}

func (v Vector(T)) Sum() T where T: Summable {
      //
}

func (v Vector(T)) First()  (T, bool) {
     //
}

El método Sum solo funciona cuando los parámetros de tipo T son Summable , de lo contrario no podemos llamar a Sum en Vector.

Hola @leaxoy

Puedes escribir algo como https://go2goplay.golang.org/p/pRznN30Qu8V

type Addable interface {
    type int, uint
}

type SummableVector(type T Addable) Vector(T)

func (v SummableVector(T)) Sum() T {
    var r T
    for _, i := range v.vec {
        r = r + i
    }
    return r
}

Creo que la cláusula where no parece similar a Go y sería difícil analizarla, debería ser algo como

type Vector(type T) struct {
    vec []T
}

func (v Vector(T Summable)) Sum() T {
      //
}

func (v Vector(T)) First()  (T, bool) {
     //
}

pero parece una especialización de métodos.

@sebastien-rosset No hemos considerado los tipos predeterminados para los parámetros de tipo genérico. El lenguaje no tiene valores predeterminados para los argumentos de función y no es obvio por qué los genéricos serían diferentes. En mi opinión, la capacidad de hacer compatible el código existente con un paquete que agrega genéricos no es una prioridad. Si un paquete se reescribe para usar genéricos, está bien requerir que se cambie el código existente, o simplemente introducir el código genérico usando nuevos nombres.

@sighoya

Además, ¿existen planes para permitir límites de interfaz finitos parametrizables?

Lo siento, no entiendo la pregunta.

Me gustaría recordarle a la gente que la publicación del blog (https://blog.golang.org/generics-next-step) sugiere que la discusión sobre los genéricos se lleve a cabo en la lista de correo de golang-nuts, no en el rastreador de problemas. Seguiré leyendo este problema, pero tiene casi 800 comentarios y es completamente difícil de manejar, además de las otras dificultades del rastreador de problemas, como no tener subprocesos de comentarios. Gracias.

Comentarios: escuché el podcast Go Time más reciente y debo decir que la explicación de @griesemer sobre el problema con los corchetes angulares fue la primera vez que realmente lo entendí, es decir, qué significa realmente "mirada hacia adelante ilimitada en el analizador". para ir? Muchas gracias por el detalle adicional allí.

Además, estoy a favor de los corchetes. 😄

@ianlancetaylor

la publicación del blog sugiere que la discusión sobre los genéricos se lleve a cabo en la lista de correo de golang-nuts, no en el rastreador de problemas

En una publicación de blog reciente [1], @ddevault señala que Google Group (donde está esa lista de correo) requiere una cuenta de Google. Necesita uno para publicar, y aparentemente algunos grupos incluso requieren una cuenta para leer. Tengo una cuenta de Google, así que esto no es un problema para mí (y tampoco digo que esté de acuerdo con todo lo que se dice en esa publicación de blog), pero sí estoy de acuerdo en que si queremos tener una comunidad de golang más justa, y si queremos evitar una cámara de eco, sería mejor no tener este tipo de requisito.

No sabía esto sobre los grupos de Google, y si hay alguna excepción para golang-nuts, acepte mis disculpas e ignore esto. Por si sirve de algo, he aprendido mucho al leer este hilo, y también estoy bastante convencido (después de usar golang durante más de seis años) de que los genéricos son el enfoque incorrecto para el lenguaje. Sin embargo, solo es mi opinión personal, y gracias por traernos el idioma que disfruto mucho.

¡Salud!

[1] https://drewdevault.com/2020/08/01/pkg-go-dev-sucks.html

@purpleidea Se puede usar cualquier grupo de Google como lista de correo. Puedes unirte y participar sin tener una cuenta de Google.

@ianlancetaylor

Cualquier grupo de Google se puede utilizar como lista de correo. Puedes unirte y participar sin tener una cuenta de Google.

Cuando voy a:

https://groups.google.com/forum/#!forum/golang-nueces

en una ventana privada del navegador (para ocultar mi cuenta de Google en la que he iniciado sesión) y hacer clic en "nuevo tema", me redirige a una página de inicio de sesión de Google. ¿Cómo lo uso sin una cuenta de Google?

@purpleidea Escribiendo un correo electrónico a [email protected] . Es una lista de correo. Solo la interfaz web necesita una cuenta de Google. Lo cual parece justo: dado que es una lista de correo, necesita una dirección de correo electrónico y, obviamente, los grupos solo pueden enviar correos desde una cuenta de Gmail.

Creo que la mayoría de la gente no entiende lo que es una lista de correo.

De todos modos, también puede usar cualquier espejo de lista de correo público, por ejemplo https://www.mail-archive.com/[email protected]/

Todo esto es genial, pero no lo hace más fácil cuando las personas se vinculan a
hilos en Grupos de Google (lo que sucede con frecuencia). es increíblemente
irritante tratar de encontrar un mensaje de la ID en una URL.

—Sam

El domingo 2 de agosto de 2020 a las 19:24, Ahmed W. escribió:
>
>

Creo que la mayoría de la gente no entiende lo que es una lista de correo.

De todos modos, también puede usar cualquier espejo de lista de correo público, por ejemplo
https://www.mail-archive.com/[email protected]/

— 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/15292#issuecomment-667738419 , o
darse de baja
https://github.com/notifications/unsubscribe-auth/AAD5EPNQTEUF5SPT6GMM4JLR6XYUBANCNFSM4CA35RXQ
.

--
sam blanqueado

Este no es realmente el lugar para tener esta discusión.

¿Alguna actualización sobre esto? 🤔

@Imperatorn ha habido, simplemente no se han discutido aquí. Se decidió que los corchetes [ ] serían la sintaxis elegida y la palabra "tipo" no sería necesaria al escribir funciones/tipos genéricos. También hay un nuevo alias "cualquiera" para la interfaz vacía.

El último borrador de diseño genérico está aquí .
Ver también este comentario re: discusiones sobre este tema. Gracias.

Me gustaría recordarle a la gente que la publicación del blog (https://blog.golang.org/generics-next-step) sugiere que la discusión sobre los genéricos se lleve a cabo en la lista de correo de golang-nuts, no en el rastreador de problemas. Seguiré leyendo este problema, pero tiene casi 800 comentarios y es completamente difícil de manejar, además de las otras dificultades del rastreador de problemas, como no tener subprocesos de comentarios. Gracias.

Sobre esto, si bien respeto que al equipo de Go le gustaría sacar tales discusiones de un problema por razones prácticas, parece que hay muchos miembros de la comunidad en GitHub que no están enloquecidos. Me pregunto si la nueva función de debates de GitHub encajaría bien. 🤔 Tiene rosca, al parecer.

@toolbox El argumento también se puede hacer en la otra dirección: hay personas que no tienen una cuenta de github (y se niegan a obtener una). Tampoco tienes que estar suscrito a golang-nuts para poder publicar y participar allí.

@Merovius Una de las características que realmente me gustan de los problemas de GitHub es que puedo suscribirme a notificaciones solo para los problemas que me interesan. No estoy seguro de cómo hacerlo con Grupos de Google.

Estoy seguro de que hay buenas razones para preferir uno u otro. Ciertamente puede haber una discusión sobre cuál debería ser el foro preferido. Sin embargo, de nuevo, no creo que esa discusión deba estar aquí. Este problema ya es bastante ruidoso.

@toolbox El argumento también se puede hacer en la otra dirección: hay personas que no tienen una cuenta de github (y se niegan a obtener una). Tampoco tienes que estar suscrito a golang-nuts para poder publicar y participar allí.

Entiendo lo que dices, y es verdad, pero te estás equivocando. No estoy diciendo que a los usuarios de golang-nuts se les deba decir que vayan a GitHub (como está sucediendo ahora al revés). Estoy diciendo que sería bueno para los usuarios de GitHub tener un foro de discusión.

Estoy seguro de que hay buenas razones para preferir uno u otro. Ciertamente puede haber una discusión sobre cuál debería ser el foro preferido. Sin embargo, de nuevo, no creo que esa discusión deba estar aquí. Este problema ya es bastante ruidoso.

Estoy de acuerdo en que esto está completamente fuera de tema para este problema, y ​​me disculpo por haberlo sacado a colación, pero espero que vean la ironía.

@keean @Merovius @toolbox y amigos en el futuro.

FYI: Hay un tema abierto para este tipo de discusión, vea #37469.

Hola,

En primer lugar, gracias por Go. El lenguaje es absolutamente brillante. Una de las cosas más sorprendentes de Go, para mí, ha sido la legibilidad. Soy nuevo en el idioma, por lo que todavía estoy en las primeras etapas de descubrimiento, pero hasta ahora, se ha mostrado increíblemente claro, nítido y directo.

El único comentario que me gustaría presentar es que desde mi escaneo inicial de la propuesta de genéricos, [T Constraint] no es fácil de analizar rápidamente para mí, al menos no tan fácil como un conjunto de caracteres designado para genéricos. . Entiendo que el estilo C++ F<T Constraint> no es factible debido a la naturaleza del paradigma de retorno múltiple de go. Cualquier carácter que no sea ascii sería una tarea absoluta, así que estoy muy agradecido de que rechazaras esa idea.

Considere usar una combinación de caracteres. No estoy seguro de si las operaciones bit a bit podrían malinterpretarse o enturbiar las aguas del análisis, pero F<<T Constraint>> estaría bien, en mi opinión. Sin embargo, cualquier combinación de símbolos sería suficiente. Si bien puede agregar un impuesto inicial de escaneo ocular, creo que esto se puede remediar fácilmente con ligaduras de fuentes como FireCoda e Iosevka . No se puede hacer mucho para distinguir clara y fácilmente la diferencia entre Map[T Constraint] y map[string]T .

No tengo ninguna duda de que la gente entrenará su mente para distinguir entre las dos aplicaciones de [] según el contexto. Solo sospecho que aumentará la curva de aprendizaje.

Gracias por la nota. No se pierda lo obvio, pero map[T1]T2 y Map[T1 Constraint] se pueden distinguir porque el primero no tiene restricción y el segundo tiene una restricción requerida.

La sintaxis se ha discutido extensamente en golang-nuts y creo que está resuelta. Nos complace escuchar comentarios basados ​​en datos reales, como el análisis de ambigüedades. Para comentarios basados ​​en sentimientos y preferencias creo que es momento de discrepar y comprometerse.

Gracias de nuevo.

@ianlancetaylor Bastante justo. Estoy seguro de que estás cansado de escuchar quisquillosos :) Por lo que vale, me refiero a diferenciar fácilmente el escaneo inteligente.

De todos modos, estoy deseando usarlo. Gracias.

Una alternativa genérica a reflect.MakeFunc sería una gran ganancia de rendimiento para la instrumentación Go. Pero no veo forma de descomponer un tipo de función con la propuesta actual.

@ Julio-Guerra No estoy seguro de lo que quiere decir con "descomponer un tipo de función". Puede, hasta cierto punto, parametrizar sobre argumentos y tipos de devolución: https://go2goplay.golang.org/p/RwU11S4gC59

package main

import (
    "fmt"
)

func Call[In, Out any](f func(In) Out, v In) Out {
    return f(v)
}

func main() {
    triple := func(i int) int {
        return 3 * i
    }
    fmt.Println(Call(triple, 23))
}

Sin embargo, esto solo funciona si el número de ambos es constante.

@ Julio-Guerra No estoy seguro de lo que quiere decir con "descomponer un tipo de función". Puede, hasta cierto punto, parametrizar sobre argumentos y tipos de devolución: https://go2goplay.golang.org/p/RwU11S4gC59

De hecho, me refiero a lo que hiciste, pero generalizado a cualquier parámetro de función y lista de tipos de retorno (de manera similar a la matriz de parámetros y tipos de retorno de reflect.MakeFunc). Eso permitiría tener contenedores de funciones generalizados (en lugar de usar la generación de código con herramientas).

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