Go: todo: admite la reparación gradual de código mientras se mueve un tipo entre paquetes

Creado en 1 dic. 2016  ·  225Comentarios  ·  Fuente: golang/go

Título original: propuesta: admite la reparación gradual de código mientras se mueve un tipo entre paquetes

Go debería agregar la capacidad de crear nombres equivalentes alternativos para los tipos, con el fin de permitir la reparación gradual del código durante la refactorización de la base de código. Este fue el objetivo de la función de alias de Go 1.8, propuesta en el n. ° 16339 pero que se retuvo de Go 1.8. Debido a que no resolvimos el problema para Go 1.8, sigue siendo un problema y espero que podamos resolverlo para Go 1.9.

En la discusión de la propuesta de alias, hubo muchas preguntas sobre por qué es importante esta capacidad de crear nombres alternativos para tipos en particular. Como un nuevo intento de responder a esas preguntas, escribí y publiqué un artículo, " Refactorización de Codebase (con la ayuda de Go) ". Lea ese artículo si tiene preguntas sobre la motivación. (Para una presentación alternativa más corta, vea la charla relámpago de Gophercon de Robert. Desafortunadamente, ese video no estuvo disponible en línea hasta el 9 de octubre. Actualización, 16 de diciembre: aquí está mi charla de GothamGo , que fue esencialmente el primer borrador del artículo).

Este problema _no_ propone una solución específica. En cambio, quiero recopilar comentarios de la comunidad de Go sobre el espacio de posibles soluciones. Una posible vía es limitar los alias a los tipos, como se menciona al final del artículo. Puede haber otros que también deberíamos considerar.

Publique sus pensamientos sobre los alias de tipo u otras soluciones como comentarios aquí.

Gracias.

Actualización, 16 de diciembre : Documento de diseño para alias de tipo publicado .
Actualización, 9 de enero : propuesta aceptada, repositorio dev.typealias creado, implementación prevista para el inicio del ciclo de Go 1.9 para la experimentación.


Resumen de la discusión (última actualización 02/02/2017)

¿Esperamos necesitar una solución general que funcione para todas las declaraciones?

Si los alias de tipo son 100% necesarios, entonces los alias var son quizás un 10% necesarios, los alias func son un 1% necesarios y los alias const son necesarios un 0%. Debido a que const ya tiene = y func también podría usar =, la pregunta clave es si los alias de var son lo suficientemente importantes como para planificarlos o implementarlos.

Como argumentan @rogpeppe (https://github.com/golang/go/issues/16339#issuecomment-258771806) y @ianlancetaylor (https://github.com/golang/go/issues/16339#issuecomment-233644777) en la propuesta de alias original y como se menciona en el artículo, una var global mutante suele ser un error. Probablemente no tenga sentido complicar la solución para adaptarse a lo que suele ser un error. (De hecho, si podemos descubrir cómo, no me sorprendería si, a largo plazo, Go se mueve hacia la exigencia de que las variables globales sean inmutables).

Debido a que es probable que los alias var más ricos no sean lo suficientemente importantes como para planificarlos, parece que la elección correcta aquí es centrarse solo en los alias de tipo. La mayoría de los comentarios aquí parecen estar de acuerdo. No enumeraré a todos.

¿Necesitamos una nueva sintaxis (= vs => vs exportar)?

El argumento más fuerte para la nueva sintaxis es la necesidad de admitir alias var, ya sea ahora o en el futuro (https://github.com/golang/go/issues/18130#issuecomment-264232763 por @Merovius). Parece correcto planificar no tener alias var (consulte la sección anterior).

Sin los alias de var, reutilizar = es más simple que introducir una nueva sintaxis, ya sea => como en la propuesta de alias, ~ (https://github.com/golang/go/issues/18130#issuecomment-264185142 por @joegrasse), o exportar (https://github.com/golang/go/issues/18130#issuecomment-264152427 por @cznic).

El uso de = in también coincidiría exactamente con la sintaxis de los alias de tipo en Pascal y Rust. En la medida en que otros lenguajes tengan los mismos conceptos, es bueno usar la misma sintaxis.

De cara al futuro, podría haber un Go futuro en el que también existan alias de funciones (consulte https://github.com/golang/go/issues/18130#issuecomment-264324306 por @nigeltao), y luego todas las declaraciones permitirían la misma forma :

const C2 = C1
func F2 = F1
type T2 = T1
var V2 = V1

El único de estos que no establecería un alias verdadero sería la declaración var, porque V2 y V1 se pueden redefinir de forma independiente a medida que se ejecuta el programa (a diferencia de las declaraciones const, func y type que son inmutables). Dado que una de las principales razones de las variables es permitir que varíen, esa excepción sería al menos fácil de explicar. Si Go se mueve hacia variables globales inmutables, incluso esa excepción desaparecería.

Para ser claros, no estoy sugiriendo alias de funciones o variables globales inmutables aquí, solo estoy trabajando en las implicaciones de tales adiciones futuras.

@jimmyfrasche sugirió (https://github.com/golang/go/issues/18130#issuecomment-264278398) alias para todo excepto consts, por lo que const sería la excepción en lugar de var:

const C2 = C1 // no => form
func F2 => F1
type T2 => T1
var V2 => V1
var V2 = V1 // different from => form

Tener inconsistencias tanto con const como con var parece más difícil de explicar que tener solo una inconsistencia para var.

¿Puede ser esto un cambio de herramientas o solo del compilador en lugar de un cambio de idioma?

Ciertamente, vale la pena preguntarse si la reparación gradual de código se puede habilitar únicamente mediante la información complementaria proporcionada al compilador (por ejemplo, https://github.com/golang/go/issues/18130#issuecomment-264205929 por @btracey).

O tal vez si el compilador puede aplicar algún tipo de preprocesamiento basado en reglas para transformar los archivos de entrada antes de la compilación (por ejemplo, https://github.com/golang/go/issues/18130#issuecomment-264329924 por @ tux21b).

Desafortunadamente, no, el cambio realmente no se puede limitar de esa manera. Hay al menos dos compiladores (gc y gccgo) que deberían coordinarse, pero también lo haría cualquier otra herramienta que analice programas, como go vet, guru, goimports, gocode (finalización de código) y otros.

Como dijo @bcmills (https://github.com/golang/go/issues/18130#issuecomment-264275574), “un mecanismo de 'no cambio de idioma' que debe ser compatible con todas las implementaciones es un cambio de idioma de facto - es solo uno con la documentación más deficiente ".

¿Qué otros usos pueden tener los alias?

Sabemos de lo siguiente. Dado que los alias de tipo en particular se consideraron lo suficientemente importantes para su inclusión en Pascal y Rust, es probable que haya otros.

  1. Los alias (o simplemente los alias de tipo) permitirían crear reemplazos directos que expandan otros paquetes. Por ejemplo, consulte https://go-review.googlesource.com/#/c/32145/ , especialmente la explicación en el mensaje de confirmación.

  2. Los alias (o simplemente los alias de tipo) permitirían estructurar un paquete con una pequeña superficie de API pero una implementación grande como una colección de paquetes para una mejor estructura interna, pero aún presentando un solo paquete para ser importado y utilizado por los clientes. Hay un ejemplo algo abstracto descrito en https://github.com/golang/go/issues/16339#issuecomment -232813695.

  3. Los búferes de protocolo tienen una característica de "importación pública" cuya semántica es trivial de implementar en el código C ++ generado pero imposible de implementar en el código Go generado. Esto causa frustración a los autores de definiciones de búfer de protocolo compartidas entre clientes C ++ y Go. Los alias de tipo proporcionarían una forma para que Go implemente esta función. De hecho, el caso de uso original para el público de importación fue la reparación gradual del código . Pueden surgir problemas similares en otros tipos de generadores de código.

  4. Abreviando nombres largos. Los alias locales (no exportados o sin alcance de paquete) pueden ser útiles para abreviar un nombre de tipo largo sin introducir la sobrecarga de un tipo completamente nuevo. Al igual que con todos estos usos, la claridad del código final influiría mucho en si se trata de un uso sugerido.

¿Qué otras cuestiones debe abordar una propuesta de alias de tipo?

Enumerarlos como referencia. No intentar resolverlos o discutirlos en esta sección, aunque algunos se discutieron más adelante y se resumen en secciones separadas a continuación.

  1. Manejo en godoc. (https://github.com/golang/go/issues/18130#issuecomment-264323137 por @nigeltao y https://github.com/golang/go/issues/18130#issuecomment-264326437 por @jimmyfrasche)

  2. ¿Se pueden definir métodos en tipos nombrados por alias? (https://github.com/golang/go/issues/18130#issuecomment-265077877 por @ulikunitz)

  3. Si se permiten alias a alias, ¿cómo manejamos los ciclos de alias? (https://github.com/golang/go/issues/18130#issuecomment-264494658 por @thwd)

  4. ¿Los alias deberían poder exportar identificadores no exportados? (https://github.com/golang/go/issues/18130#issuecomment-264494658 por @thwd)

  5. ¿Qué sucede cuando incrusta un alias (cómo accedes al campo incrustado)? (https://github.com/golang/go/issues/18130#issuecomment-264494658 por @thwd , también # 17746)

  6. ¿Los alias están disponibles como símbolos en el programa construido? (https://github.com/golang/go/issues/18130#issuecomment-264494658 por @thwd)

  7. Inyección de cadenas de LDFlags: ¿y si nos referimos a un alias? (https://github.com/golang/go/issues/18130#issuecomment-264494658 por @thwd; esto solo surge si hay alias de var).

¿Es el control de versiones una solución en sí mismo?

"En ese caso, tal vez el control de versiones sea la respuesta completa, no los alias de tipo".
(https://github.com/golang/go/issues/18130#issuecomment-264573088 por @iainmerrick)

Como se señaló en el artículo , creo que el control de versiones es una preocupación complementaria. El soporte para la reparación gradual de código, como con los alias de tipo, le da a un sistema de control de versiones más flexibilidad en la forma en que construye un programa grande, lo que puede ser la diferencia entre poder construir el programa y no hacerlo.

¿Se puede resolver el problema de la refactorización más grande en su lugar?

En https://github.com/golang/go/issues/18130#issuecomment -265052639, @niemeyer señala que en realidad hubo dos cambios para mover os.Error a error: el nombre cambió pero también lo hizo la definición (el actual El método de error solía ser un método String).

@niemeyer sugiere que quizás podamos encontrar una solución al problema de refactorización más amplio que corrige los tipos que se mueven entre paquetes como un caso especial, pero también maneja cosas como el cambio de nombres de métodos, y propone una solución construida alrededor de "adaptadores".

Hay una gran cantidad de discusión en los comentarios que no puedo resumir fácilmente aquí. La discusión no ha terminado, pero hasta ahora no está claro si los "adaptadores" pueden encajar en el lenguaje o implementarse en la práctica. Parece claro que los adaptadores son al menos un orden de magnitud más complejos que los alias de tipo.

Los adaptadores también necesitan una solución coherente a los problemas de subtipificación que se indican a continuación.

¿Se pueden declarar métodos en tipos de alias?

Ciertamente, los alias no permiten eludir las restricciones habituales de definición de métodos: si un paquete define el tipo T1 = otherpkg.T2, no puede definir métodos en T1, al igual que no puede definir métodos directamente en otherpkg.T2. Es decir, si escribe T1 = otherpkg.T2, entonces func (T1) M () es equivalente a func (otherpkg.T2) M (), que no es válido hoy y sigue siendo inválido. Sin embargo, si un paquete define el tipo T1 = T2 (ambos en el mismo paquete), la respuesta es menos clara. En este caso, func (T1) M () sería equivalente a func (T2) M (); dado que lo último está permitido, existe un argumento para permitir lo primero. El documento de diseño actual no impone una restricción aquí (de acuerdo con la evitación general de restricciones), por lo que func (T1) M () es válida en esta situación.

En https://github.com/golang/go/issues/18130#issuecomment -267694112, @jimmyfrasche sugiere que, en cambio, definir "no usar alias en las definiciones de métodos" sería una regla clara y evitaría la necesidad de saber qué se define T para saber si func (T) M () es válido. En https://github.com/golang/go/issues/18130#issuecomment -267997124, @rsc señala que aún hoy existen ciertas T para las que func (T) M () no es válido: https: // play .golang.org / p / bci2qnldej. En la práctica, esto no surge porque la gente escriba un código razonable.

Tendremos en cuenta esta posible restricción, pero esperaremos hasta que haya pruebas sólidas de que es necesaria antes de introducirla.

¿Existe una forma más limpia de manejar la incrustación y, de manera más general, los cambios de nombre de los campos?

En https://github.com/golang/go/issues/18130#issuecomment -267691816, @Merovius señala que un tipo incrustado que cambia su nombre durante el movimiento de un paquete causará problemas cuando ese nuevo nombre deba adoptarse finalmente en el sitios de uso. Por ejemplo, si el tipo de usuario U tiene un io.ByteBuffer incrustado que se mueve a bytes.Buffer, mientras U incrusta io.ByteBuffer el nombre del campo es U.ByteBuffer, pero cuando U se actualiza para referirse a bytes.Buffer, el nombre del campo necesariamente cambia a U.Buffer.

En https://github.com/golang/go/issues/18130#issuecomment -267710478, @neild señala que hay al menos una solución si las referencias a io.ByteBuffer deben eliminarse: el paquete P que define U también puede defina 'type ByteBuffer = bytes.Buffer' e incruste ese tipo en U. Entonces U todavía tiene un U.ByteBuffer, incluso después de que io.ByteBuffer desaparezca por completo.

En https://github.com/golang/go/issues/18130#issuecomment -267703067, @bcmills sugiere la idea de los alias de campo, para permitir que un campo tenga varios nombres durante una reparación gradual. Los alias de campo permitirían definir algo como type U struct { bytes.Buffer; ByteBuffer = Buffer } lugar de tener que crear el alias de tipo de nivel superior.

En https://github.com/golang/go/issues/18130#issuecomment -268001111, @rsc plantea otra posibilidad más: alguna sintaxis para 'incrustar este tipo con este nombre', de modo que sea posible incrustar bytes. Búfer como el nombre de campo ByteBuffer, sin necesidad de un tipo de nivel superior o un nombre alternativo. Si eso existiera, entonces el nombre del tipo podría actualizarse de io.ByteBuffer a bytes.Buffer mientras se conserva el nombre original (y no se introduce un segundo tipo exportado ni torpe).

Todos estos parecen valer la pena explorar una vez que tengamos más evidencia de refactorizaciones a gran escala bloqueadas por problemas con campos que cambian de nombre. Como escribió @rsc , "Si los alias de tipo nos ayudan a llegar al punto en que la falta de alias de campo es el próximo gran obstáculo para las refactorizaciones a gran escala, ¡eso será un progreso!"

Hubo una sugerencia de restringir el uso de alias en campos incrustados o cambiar el nombre incrustado para usar el nombre del tipo de destino, pero esos hacen que la introducción del alias rompa las definiciones existentes que luego deben arreglarse atómicamente, esencialmente evitando cualquier reparación gradual. @rsc : "Discutimos esto con cierto detalle en el # 17746. Originalmente estaba del lado del nombre de un alias de io.ByteBuffer incrustado que era Buffer, pero el argumento anterior me convenció de que estaba equivocado. @jimmyfrasche en particular hizo algo bueno argumentos acerca de que el código no cambia dependiendo de la definición del elemento incrustado. No creo que sea sostenible rechazar completamente los alias incrustados ".

¿Cuál es el efecto en los programas que utilizan la reflexión?

Los programas que utilizan la reflexión ven a través de alias. En https://github.com/golang/go/issues/18130#issuecomment -267903649, @atdiar señala que si un programa está usando la reflexión para, por ejemplo, encontrar el paquete en el que se define un tipo o incluso el nombre de un tipo, observará el cambio cuando se mueva el tipo, incluso si se deja un alias de reenvío. En https://github.com/golang/go/issues/18130#issuecomment -268001410, @rsc confirmó esto y escribió "Como la situación con la incrustación, no es perfecta. A diferencia de la situación con la incrustación, no tengo ninguna las respuestas excepto tal vez el código no debería escribirse usando reflect para ser tan sensible a esos detalles ".

El uso de paquetes vendidos en la actualidad también cambia las rutas de importación de paquetes vistas por reflect, y no se nos ha informado de los problemas importantes causados ​​por esa ambigüedad. Esto sugiere que los programas no suelen inspeccionar reflect.Type.PkgPath de formas que se romperían con el uso de alias. Aun así, es una brecha potencial, al igual que la incrustación.

¿Cuál es el efecto en la compilación separada de programas y complementos?

En https://github.com/golang/go/issues/18130#issuecomment -268524504, @atdiar plantea la cuestión del efecto en los archivos objeto y la compilación separada. En https://github.com/golang/go/issues/18130#issuecomment -268560180, @rsc responde que no debería ser necesario realizar cambios aquí: si X importa los cambios Y e Y y se vuelve a compilar, entonces X necesita ser recompilado también. Eso es cierto hoy sin alias, y seguirá siendo cierto con los alias. La compilación separada significa poder compilar X e Y en pasos distintos (el compilador no tiene que procesarlos en la misma invocación), aunque no es posible cambiar Y sin volver a compilar X.

¿Los tipos de suma o algún tipo de subtipo serían una solución alternativa?

En https://github.com/golang/go/issues/18130#issuecomment -264413439, @iand sugiere "tipos sustituibles", "una lista de tipos que pueden ser sustituidos por el tipo nombrado en argumentos de función, valores de retorno, etc. ". En https://github.com/golang/go/issues/18130#issuecomment -268072274, @ j7b sugiere usar tipos algebraicos "por lo que también obtenemos una interfaz vacía equivalente con verificación de tipo en tiempo de compilación como un bono". Otros nombres para este concepto son tipos de suma y tipos de variantes.

En general, esto no es suficiente para permitir tipos móviles con reparación de código gradual. Hay dos formas de pensar en esto.

En https://github.com/golang/go/issues/18130#issuecomment -268075680, @bcmills toma el camino concreto, señalando que los tipos algebraicos tienen una representación diferente a la original, lo que hace que no sea posible tratar la suma y el original como intercambiable: este último tiene etiquetas de tipo.

En https://github.com/golang/go/issues/18130#issuecomment -268585497, @rsc toma el camino teórico, ampliando https://github.com/golang/go/issues/18130#issuecomment -265211655 por @gri señala que en una reparación de código gradual, a veces necesita que T1 sea un subtipo de T2 y, a veces, viceversa. La única forma de que ambos sean subtipos el uno del otro es que sean del mismo tipo, lo que no es casualidad es lo que hacen los alias de tipo.

Como tangente lateral, además de no resolver el problema de reparación gradual del código, los tipos algebraicos / tipos de suma / tipos de unión / tipos de variantes son por sí mismos difíciles de agregar a Go. Ver
la respuesta a las preguntas frecuentes y la discusión de Go 1.6 AMA para obtener más información.

En https://github.com/golang/go/issues/18130#issuecomment -265206780, @thwd sugiere que, dado que Go tiene una relación de subtipo entre tipos e interfaces concretos (bytes.Buffer puede verse como un subtipo de io.Reader ) y entre interfaces (io.ReadWriter es un subtipo de io.Reader de la misma manera), hacer interfaces "recursivamente covariantes (de acuerdo con las reglas de varianza actuales) hasta sus argumentos de método" resolvería el problema siempre que todos los paquetes futuros solo utilice interfaces, nunca tipos concretos como estructuras ("también fomenta el buen diseño").

Hay tres problemas con eso como solución. Primero, tiene los problemas de subtipificación anteriores, por lo que no resuelve la reparación gradual del código. En segundo lugar, no se aplica al código existente, como señaló @thwd en esta sugerencia. En tercer lugar, forzar el uso de interfaces en todas partes puede no ser un buen diseño e introduce gastos generales de rendimiento (consulte, por ejemplo, https://github.com/golang/go/issues/18130#issuecomment-265211726 por @Merovius y https: // github .com / golang / go / issues / 18130 # issuecomment-265224652 por @zombiezen).

Restricciones

Esta sección recopila las restricciones propuestas como referencia, pero tenga en cuenta que las restricciones agregan complejidad. Como escribí en https://github.com/golang/go/issues/18130#issuecomment -264195616, "probablemente solo deberíamos implementar esas restricciones después de que la experiencia real con el diseño sin restricciones y más simple nos ayude a comprender si la restricción traerá suficiente beneficios para pagar su costo ".

Dicho de otra manera, cualquier restricción debería estar justificada con pruebas de que evitaría un uso indebido o confusión graves. Dado que aún no hemos implementado una solución, no existe tal evidencia. Si la experiencia proporcionó esa evidencia, valdrá la pena volver a ella.

¿Restricción? Los alias de los tipos de biblioteca estándar solo se pueden declarar en la biblioteca estándar.

(https://github.com/golang/go/issues/18130#issuecomment-264165833 y https://github.com/golang/go/issues/18130#issuecomment-264171370 por @iand)

La preocupación es "el código que ha cambiado el nombre de los conceptos de biblioteca estándar para ajustarse a una convención de nomenclatura personalizada", o "largas cadenas espaguetis de alias en varios paquetes que terminan en la biblioteca estándar", o "alias cosas como interfaz {} y error". .

Como se indicó, la restricción no permitiría el caso de "paquete de extensión" descrito anteriormente que involucra x / image / draw.

No está claro por qué la biblioteca estándar debería ser especial: los problemas existirían con cualquier código. Además, ni la interfaz {} ni el error son un tipo de la biblioteca estándar. Reformular la restricción como "tipos predefinidos de aliasing" no permitiría el error de aliasing, pero la necesidad del error de alias fue uno de los ejemplos motivadores del artículo.

¿Restricción? El destino de alias debe ser un identificador calificado por paquete.

(https://github.com/golang/go/issues/18130#issuecomment-264188282 por @jba)

Esto haría imposible crear un alias al cambiar el nombre de un tipo dentro de un paquete, que puede usarse lo suficiente como para necesitar una reparación gradual (https://github.com/golang/go/issues/18130#issuecomment-264274714 por @ bcmills).

También no permitiría el error de alias como en el artículo.

¿Restricción? El destino de alias debe ser un identificador calificado por paquete con el mismo nombre que el alias.

(propuesto durante la discusión de alias en Go 1.8)

Además de los problemas de la sección anterior con la limitación a identificadores calificados por paquetes, forzar que el nombre permanezca igual no permitiría la conversión de io.ByteBuffer a bytes.Buffer en el artículo.

¿Restricción? Los alias deben desalentarse de alguna manera.

"¿Qué tal ocultar alias detrás de una importación, como para" C "e" inseguro ", para desalentar aún más su uso? En la misma línea, me gustaría que la sintaxis de los alias sea detallada y se destaque como un andamio para la refactorización continua . " - https://github.com/golang/go/issues/18130#issuecomment -264289940 por @xiegeo

"¿Deberíamos también inferir automáticamente que un tipo con alias es heredado y debería ser reemplazado por el nuevo tipo? Si aplicamos golint, godoc y herramientas similares para visualizar el tipo antiguo como obsoleto, limitaría el abuso del alias de tipo de manera muy significativa. Y la preocupación final de que se abuse de la función de aliasing se resolverá ". - https://github.com/golang/go/issues/18130#issuecomment -265062154 por @rakyll

Hasta que sepamos que se utilizarán incorrectamente, parece prematuro desalentar su uso. Puede haber buenos usos no temporales (ver arriba).

Incluso en el caso de la reparación de código, el tipo antiguo o nuevo puede ser el alias durante la transición, dependiendo de las restricciones impuestas por el gráfico de importación. Ser un alias no significa que el nombre esté desaprobado.

Ya existe un mecanismo para marcar ciertas declaraciones como obsoletas (consulte https://github.com/golang/go/issues/18130#issuecomment-265294564 por @jimmyfrasche).

¿Restricción? Los alias deben apuntar a tipos con nombre.

"Los alias no deberían aplicarse a tipos sin nombre. No hay una historia de" reparación de código "al pasar de un tipo sin nombre a otro. Permitir alias en tipos sin nombre significa que ya no puedo enseñar Go como tipos con nombre y sin nombre". - https://github.com/golang/go/issues/18130#issuecomment -276864903 por @davecheney

Hasta que sepamos que se utilizarán incorrectamente, parece prematuro desalentar su uso. Puede haber buenos usos con objetivos sin nombre (ver arriba).

Como se indica en el documento de diseño, esperamos cambiar la terminología para aclarar la situación.

FrozenDueToAge Proposal Proposal-Accepted

Comentario más útil

@cznic , @iand , otros: tenga en cuenta que las _restricciones añaden complejidad_. Complican la explicación de la función y agregan carga cognitiva para cualquier usuario de la función: si te olvidas de una restricción, tienes que averiguar por qué algo que creías que debería funcionar no funciona.

A menudo es un error implementar restricciones en una prueba de un diseño debido únicamente a un hipotético mal uso. Eso sucedió en las discusiones de la propuesta de alias, e hizo que los alias en la prueba no pudieran manejar la conversión io.ByteBuffer => bytes.Buffer del artículo. Parte del objetivo de escribir el artículo es definir algunos casos que sabemos que queremos poder manejar, para no restringirlos inadvertidamente.

Como otro ejemplo, sería fácil hacer un argumento de uso indebido para no permitir receptores que no sean de puntero, o para no permitir métodos en tipos que no sean de estructura. Si hubiéramos hecho cualquiera de esos, no podría crear enumeraciones con métodos String () para imprimirse a sí mismos, y no podría hacer que http.Headers sean un mapa simple y proporcionen métodos auxiliares. A menudo es fácil imaginar usos indebidos; Los usos positivos convincentes pueden tardar más en aparecer, y es importante crear un espacio para la experimentación.

Como otro ejemplo más, el diseño y la implementación originales de los métodos de puntero vs valor no distinguían entre los conjuntos de métodos en T y * T: si tuvieras un * T, podrías llamar a los métodos de valor (receptor T), y si tuvieras una T, podría llamar a los métodos de puntero (receptor * T). Esto fue simple, sin restricciones para explicar. Pero luego, la experiencia real nos mostró que permitir llamadas a métodos de puntero en valores conducía a una clase específica de errores confusos y sorprendentes. Por ejemplo, podrías escribir:

var buf bytes.Buffer
io.Copy(buf, reader)

y io.Copy tendría éxito, pero buf no tendría nada. Tuvimos que elegir entre explicar por qué ese programa se ejecutó incorrectamente o explicar por qué ese programa no se compilaba. De cualquier manera, habría preguntas, pero decidimos evitar una ejecución incorrecta. Aun así, todavía teníamos que escribir una entrada de preguntas frecuentes sobre por qué el diseño tiene un agujero cortado.

Nuevamente, recuerde que las restricciones agregan complejidad. Como toda complejidad, las restricciones necesitan una justificación significativa. En esta etapa del proceso de diseño, es bueno pensar en las restricciones que podrían ser apropiadas para un diseño en particular, pero probablemente solo deberíamos implementar esas restricciones después de que la experiencia real con el diseño sin restricciones y más simple nos ayude a comprender si la restricción traerá suficientes beneficios. para pagar su costo.

Todos 225 comentarios

Me gusta lo visualmente uniforme que se ve esto.

const OldAPI => NewPackage.API
func  OldAPI => NewPackage.API
var   OldAPI => NewPackage.API
type  OldAPI => NewPackage.API

Pero dado que podemos mover casi gradualmente la mayoría de los elementos, tal vez el más simple
La solución _es_ solo para permitir un = para tipos.

const OldAPI = NewPackage.API
func  OldAPI() { NewPackage.API() }
var   OldAPI = NewPackage.API
type  OldAPI = NewPackage.API

Primero, solo quería agradecerles por ese excelente artículo. Creo que la mejor solución es introducir alias de tipo con un operador de asignación. Esto no requiere nuevas palabras clave / operadores, utiliza una sintaxis familiar y debería resolver el problema de refactorización para bases de código grandes.

Como señala el artículo de Russ, cualquier solución similar a un alias debe resolver con elegancia https://github.com/golang/go/issues/17746 y https://github.com/golang/go/issues/17784

Gracias por la redacción de ese artículo.

Encuentro que los alias de solo tipo que usan el operador de asignación son los mejores:

type OldAPI = NewPackage.API

Mis razones:

  • Es mas simple.
    La solución alternativa => tener un significado sutilmente diferente basado en su operando se siente fuera de lugar para Go.
  • Es concentrado y conservador.
    El problema en cuestión con los tipos está resuelto y no necesita preocuparse por imaginar las complicaciones de la solución generalizada.
  • Es estético.
    Creo que parece más agradable.

Todo lo anterior: el resultado es simple, enfocado, conservador y estético, lo que me facilita imaginarlo como parte de Go.

Si la solución se limitaría solo a tipos, entonces la sintaxis

type NewFoo = old.Foo

ya considerado antes, como se comenta en el artículo de @rsc , me parece muy bueno.

Si quisiéramos poder hacer lo mismo para las constantes, variables y funciones, mi sintaxis preferida sería (como se propuso anteriormente)

package newfmt

import (
    "fmt"
)

// No renaming.
export fmt.Printf // Note: Same as `export Printf fmt.Printf`.

export (
        fmt.Sprintf
        fmt.Formatter
)

// Renaming.
export Foo fmt.Errorf // Foo must be exported, ie. `export foo fmt.Errorf` would be invalid.

export (
    Bar fmt.Fprintf
    Qux fmt.State
)

Como se mencionó anteriormente, la desventaja es que se introduce una nueva palabra clave exclusiva de nivel superior, lo cual es ciertamente incómodo, aunque técnicamente factible y totalmente compatible con versiones anteriores. Me gusta esta sintaxis porque refleja el patrón de importaciones. Me parecería natural que las exportaciones solo se permitirían en la misma sección donde se permiten las importaciones, es decir. entre la cláusula del paquete y cualquier TLD var, tipo, constante o función.

Los identificadores de cambio de nombre se declararían en el alcance del paquete, sin embargo, los nuevos nombres no son visibles en el paquete que los declara (newfmt en el ejemplo anterior) con respecto a la redeclaración, que no está permitida como de costumbre. Dado el ejemplo anterior, TLD

var v = Printf // undefined: Printf.
var Printf int // Printf redeclared, previous declaration at newfmt.go:8.

En el paquete de importación, los identificadores de cambio de nombre son visibles normalmente, como cualquier otro identificador exportado del bloque de paquete (newftm).

package foo

import "newfmt"

type bar interface {
    baz(qux newfmt.Qux) // qux type is identical to fmt.State.
}

En conclusión, este enfoque no introduce ningún enlace de nombre local nuevo en newfmt, lo que creo que evita al menos algunos de los problemas discutidos en # 17746 y resuelve # 17784 por completo.

Mi primera preferencia es una type NewFoo = old.Foo .

Si se desea una solución más general, estoy de acuerdo con @cznic en que una palabra clave dedicada es mejor que un operador nuevo (especialmente un operador asimétrico con direccionalidad confusa [1]). Dicho esto, no creo que la palabra clave export transmita el significado correcto. Ni la sintaxis ni la semántica reflejan import . ¿Qué pasa con alias ?

Entiendo por qué @cznic no quiere que los nuevos nombres sean accesibles en el paquete que los declara, pero, al menos para mí, esa restricción se siente inesperada y artificial (aunque entiendo perfectamente la razón detrás de ella).

[1] He estado usando Unix durante casi 20 años y todavía no puedo crear un enlace simbólico en el primer intento. Y normalmente fallo incluso en el segundo intento, después de haber leído el manual.

Me gustaría proponer una restricción adicional: los alias de tipo para los tipos de biblioteca estándar solo se pueden declarar en la biblioteca estándar.

Mi razonamiento es que no quiero trabajar con código que ha cambiado el nombre de los conceptos de biblioteca estándar para ajustarse a una convención de nomenclatura personalizada. Tampoco quiero lidiar con largas cadenas de espaguetis de alias en múltiples paquetes que terminan en la biblioteca estándar.

@iand : Esa restricción bloquearía el uso de esta función para migrar cualquier cosa a la biblioteca estándar. Por ejemplo, la migración actual de Context a la biblioteca estándar. La antigua casa de Context debería convertirse en un alias para Context en la biblioteca estándar.

@quentinmit, lamentablemente eso es cierto. También limita el caso de uso de golang.org/x/image/draw en este CL https://go-review.googlesource.com/#/c/32145/

Mi verdadera preocupación es que las personas usen alias para cosas como interface{} y error

Si se decide introducir un nuevo operador, me gustaría proponer ~ . En el idioma inglés, generalmente se entiende que significa "similar a", "aproximadamente", "sobre" o "alrededor". Como se indicó anteriormente en => es un operador asimétrico con direccionalidad confusa.

Por ejemplo:

const OldAPI ~ NewPackage.API
func  OldAPI ~ NewPackage.API
var   OldAPI ~ NewPackage.API
type  OldAPI ~ NewPackage.API

@i y si limitamos el lado derecho a un identificador calificado por paquete, eso eliminaría su preocupación específica.

También significaría que no podría tener alias para ningún tipo en el paquete actual, o para expresiones de tipo largo como map[string]map[int]interface{} . Pero esos usos no tienen nada que ver con el objetivo principal de la reparación gradual del código, por lo que tal vez no sean una gran pérdida.

@cznic , @iand , otros: tenga en cuenta que las _restricciones añaden complejidad_. Complican la explicación de la función y agregan carga cognitiva para cualquier usuario de la función: si te olvidas de una restricción, tienes que averiguar por qué algo que creías que debería funcionar no funciona.

A menudo es un error implementar restricciones en una prueba de un diseño debido únicamente a un hipotético mal uso. Eso sucedió en las discusiones de la propuesta de alias, e hizo que los alias en la prueba no pudieran manejar la conversión io.ByteBuffer => bytes.Buffer del artículo. Parte del objetivo de escribir el artículo es definir algunos casos que sabemos que queremos poder manejar, para no restringirlos inadvertidamente.

Como otro ejemplo, sería fácil hacer un argumento de uso indebido para no permitir receptores que no sean de puntero, o para no permitir métodos en tipos que no sean de estructura. Si hubiéramos hecho cualquiera de esos, no podría crear enumeraciones con métodos String () para imprimirse a sí mismos, y no podría hacer que http.Headers sean un mapa simple y proporcionen métodos auxiliares. A menudo es fácil imaginar usos indebidos; Los usos positivos convincentes pueden tardar más en aparecer, y es importante crear un espacio para la experimentación.

Como otro ejemplo más, el diseño y la implementación originales de los métodos de puntero vs valor no distinguían entre los conjuntos de métodos en T y * T: si tuvieras un * T, podrías llamar a los métodos de valor (receptor T), y si tuvieras una T, podría llamar a los métodos de puntero (receptor * T). Esto fue simple, sin restricciones para explicar. Pero luego, la experiencia real nos mostró que permitir llamadas a métodos de puntero en valores conducía a una clase específica de errores confusos y sorprendentes. Por ejemplo, podrías escribir:

var buf bytes.Buffer
io.Copy(buf, reader)

y io.Copy tendría éxito, pero buf no tendría nada. Tuvimos que elegir entre explicar por qué ese programa se ejecutó incorrectamente o explicar por qué ese programa no se compilaba. De cualquier manera, habría preguntas, pero decidimos evitar una ejecución incorrecta. Aun así, todavía teníamos que escribir una entrada de preguntas frecuentes sobre por qué el diseño tiene un agujero cortado.

Nuevamente, recuerde que las restricciones agregan complejidad. Como toda complejidad, las restricciones necesitan una justificación significativa. En esta etapa del proceso de diseño, es bueno pensar en las restricciones que podrían ser apropiadas para un diseño en particular, pero probablemente solo deberíamos implementar esas restricciones después de que la experiencia real con el diseño sin restricciones y más simple nos ayude a comprender si la restricción traerá suficientes beneficios. para pagar su costo.

Además, mi esperanza es que podamos tomar una decisión tentativa sobre qué probar y luego tener algo listo para experimentar al comienzo del ciclo de Go 1.9 (idealmente el día en que se abre el ciclo). Tener más tiempo para experimentar tendrá muchos beneficios, entre ellos la oportunidad de saber si una restricción en particular es convincente. Un error con el alias fue no realizar una implementación completa hasta cerca del final del ciclo de Go 1.8.

Una cosa acerca de la propuesta de alias original es que en el caso de uso previsto (habilitar la refactorización), el uso real del tipo de alias solo debe ser temporal. En el ejemplo del protobuffer, el stub io.BytesBuffer se eliminó una vez concluida la reparación gradual.

Si el mecanismo de alias solo debe verse temporalmente, ¿realmente requiere un cambio de idioma? Quizás en su lugar podría haber un mecanismo para proporcionar gc con una lista de "alias". gc podría realizar temporalmente las sustituciones, y el autor del código base descendente podría eliminar gradualmente los elementos de este archivo a medida que se fusionan las correcciones. Me doy cuenta de que esta sugerencia también tiene consecuencias complicadas, pero al menos fomenta un mecanismo temporal.

No participaré en el bikeshedding sobre sintaxis (básicamente no me importa), con una excepción: si se decide agregar alias y si se decide restringirlos a tipos, use una sintaxis que sea consistentemente extensible a al menos var , si no también func y const (todas las construcciones sintácticas propuestas permiten todos, excepto type Foo = pkg.Bar ). La razón es que, aunque estoy de acuerdo en que los casos en los que los alias para var marcan la diferencia pueden ser raros, no creo que sean inexistentes y, como tal, creo que en algún momento podríamos decidir agregar ellos tambien. En ese punto, definitivamente querremos que todas las declaraciones de alias sean consistentes, sería malo si es type Foo = pkg.Bar y var Foo => pkg.Bar .

También abogaría un poco por tener los cuatro. Las razones son

1) existe una diferencia de var y, a veces hago uso de ella. Por ejemplo, a menudo expongo un var Debug *log.Logger global, o reasigno singleton globales como http.DefaultServeMux para interceptar / eliminar registros de paquetes que le agregan controladores.

2) También creo que, si bien func Foo() { pkg.Bar() } hace lo mismo que func Foo => pkg.Bar , la intención de este último es mucho más clara (especialmente si ya conoces los alias). Dice claramente que "esto no está realmente destinado a estar aquí". Entonces, si bien es técnicamente idéntico, la sintaxis del alias podría servir como documentación.

Sin embargo, no es la colina en la que moriría; Los alias de tipo solos por ahora estarían bien para mí, siempre que exista la opción de extenderlos más tarde.

También estoy muy contento de que esto se haya escrito como estaba. Resume un montón de opiniones que tuve sobre el diseño y la estabilidad de la API durante un tiempo y, en el futuro, servirá como una simple referencia para vincular a las personas también :)

Sin embargo, también quiero enfatizar que hubo casos de uso adicionales cubiertos por alias que son diferentes del documento (y AIUI es la intención más general de este problema, que es encontrar alguna solución para resolver la reparación gradual). Estoy muy contento si la comunidad puede ponerse de acuerdo sobre el concepto de habilitar la reparación gradual, pero si se decide llegar a ella con una decisión diferente a los alias, también creo que en ese caso se debería hablar simultáneamente sobre si y cómo apoyar cosas como las importaciones públicas de protobuf o el caso de uso x/image/draw de paquetes de reemplazo directos (ambos algo cercanos a mi corazón también) con una solución diferente. La propuesta de @btracey de un indicador go-tool / gc para alias es un ejemplo en el que creo que, si bien cubre la reparación gradual relativamente bien, no es realmente aceptable para esos otros casos de uso. Realmente no puede esperar que todos los que quieran compilar algo que use x/image/draw pasen esas banderas, solo deberían poder go get .

@jba

@i y si limitamos el lado derecho a un identificador calificado por paquete, eso eliminaría su preocupación específica.

También significaría que no podría tener alias para ningún tipo en el paquete actual, […]. Pero esos usos no tienen nada que ver con el objetivo principal de la reparación gradual del código, por lo que tal vez no sean una gran pérdida.

El cambio de nombre dentro de un paquete (por ejemplo, a un nombre más idiomático o coherente) es ciertamente un tipo de refactorización que uno podría desear razonablemente hacer, y si el paquete se usa ampliamente, entonces eso requiere una reparación gradual.

Creo que una restricción a solo nombres calificados por paquetes sería un error. (Una restricción a solo los nombres exportados podría ser más tolerable).

@btracey

Quizás, en cambio, podría haber un mecanismo para proporcionar a gc una lista de "alias". gc podría realizar temporalmente las sustituciones, y el autor del código base descendente podría eliminar gradualmente los elementos de este archivo a medida que se fusionan las correcciones.

Un mecanismo para gc significaría que el código solo se puede compilar con gc durante el proceso de reparación, o que el mecanismo tendría que ser compatible con otros compiladores (por ejemplo, gccgo y llgo ) también. Un mecanismo de "no cambio de idioma" que debe ser compatible con todas las implementaciones es un cambio de idioma de facto, es solo uno con documentación más pobre.

@btracey y @bcmills , y no solo los compiladores: cualquier herramienta que analice el código fuente, como guru o cualquier otra cosa que la gente haya construido. Ciertamente es un cambio de idioma sin importar cómo lo mire.

Bien gracias.

Otra posibilidad son los alias para todo excepto consts (y @rsc , ¡perdóname por proponer una restricción!)

Para los concursos, => es en realidad una forma más larga de escribir = . No hay una nueva semántica, como ocurre con los tipos y las vars. No hay pulsaciones de teclas guardadas como ocurre con las funciones.

Eso resolvería al menos # 17784.

El contraargumento sería que las herramientas podrían tratar los casos de manera diferente y que podría ser un indicador de intención. Ese es un buen contraargumento, pero no creo que supere el hecho de que son básicamente dos formas de hacer exactamente lo mismo.

Dicho esto, estoy bien con solo escribir alias por ahora, sin duda son los más importantes. Definitivamente estoy de acuerdo con @Merovius en que deberíamos considerar seriamente retener la opción de agregar alias var y func en el futuro, incluso si eso no sucede por algún tiempo.

¿Qué tal ocultar alias detrás de una importación, como para "C" e "inseguro", para desalentar aún más su uso? En la misma línea, me gustaría que la sintaxis de los alias sea detallada y se destaque como un andamio para la refactorización continua.

Como un intento de abrir un poco el espacio de diseño, aquí hay algunas ideas. No están desarrollados. Probablemente sean malos y / o imposibles; la esperanza es principalmente desencadenar nuevas / mejores ideas en otros. Y si hay algún interés, podemos explorar más.

La idea motivadora para (1) y (2) es utilizar de alguna manera la conversión en lugar de alias. En # 17746, los alias se encontraron con problemas relacionados con tener varios nombres para el mismo tipo (o varias formas de deletrear el mismo nombre, dependiendo de si piensa en los alias como #define o como enlaces físicos). El uso de la conversión evita eso al mantener los tipos distintos.

  1. Agregue más conversión automática.

Cuando llamas a fmt.Println("abc") o escribes var e interface{} = "abc" , "abc" se convierte automáticamente en interface{} . Podríamos cambiar el idioma para que cuando haya declarado type T struct { S } , y T no tenga métodos no promocionados, el compilador se convertirá automáticamente entre S y T según sea necesario, incluso de forma recursiva dentro de otras estructuras. Entonces, T podría servir como un alias de facto de S (o viceversa) para propósitos de refactorización gradual.

  1. Agregue un nuevo tipo de tipo "apariencia".

Deje que type T ~S declare un nuevo tipo T que es un tipo que "se parece a S". Más precisamente, T es "cualquier tipo convertible hacia y desde el tipo S". (Como siempre, la sintaxis podría discutirse más adelante). Como los tipos de interfaz, T no puede tener métodos; para hacer básicamente cualquier cosa con T, necesita convertirlo a S (o un tipo convertible a / desde S). A diferencia de los tipos de interfaz, no existe un "tipo concreto", la conversión entre S a T y T a S no implica cambios de representación. Para una refactorización gradual, estos tipos de "apariencia" permitirían a los autores escribir API que acepten tanto tipos nuevos como antiguos. (Los tipos "Parece que" son básicamente un tipo de unión simplificado y muy restringido).

  1. Etiquetas de tipo

Bonificación idea súper espantosa. (Por favor, no se moleste en decirme que esto es horrible, lo sé. Solo estoy tratando de generar nuevas ideas en otros). ¿Qué pasa si introdujimos etiquetas de tipo (como etiquetas de estructura) y usamos etiquetas de tipo especiales para configurar y controlar los alias, como por ejemplo type T S "alias:\"T\"" . Las etiquetas de tipo también tendrán otros usos y proporcionan un margen para más especificaciones de alias por parte del autor del paquete que simplemente "este tipo es un alias"; por ejemplo, el autor del código podría especificar el comportamiento de incrustación.

Si probamos los alias de nuevo, podría valer la pena pensar en "qué hace godoc", similar a los problemas de "qué hace iota" y "qué hace la incrustación".

Específicamente, si tenemos

type  OldAPI => NewPackage.API

y NewPackage.API tiene un comentario de documento, ¿se espera que copiemos / peguemos ese comentario junto a "type OldAPI"? ¿Se espera que lo dejemos sin comentarios (con godoc proporcionando automáticamente un enlace o copiando / pegando automáticamente), o ¿Hay alguna otra convención?

Algo tangencial, mientras que la motivación principal es y debería apoyar la reparación gradual del código, un caso de uso menor (volviendo a la propuesta de alias, ya que es una propuesta concreta) podría ser evitar una sobrecarga de llamada de función doble al presentar una sola función respaldado por múltiples implementaciones dependientes de etiquetas de compilación. Solo estoy agitando la mano en este momento, pero siento que los alias podrían haber sido útiles en el reciente https://groups.google.com/d/topic/golang-nuts/wb5I2tjrwoc/discussion "Evitar la sobrecarga de llamadas a funciones en los paquetes con el debate sobre las implementaciones de go + asm ".

@nigeltao re godoc, creo:

Siempre debe vincularse al original, independientemente.

Si hay documentos en el alias, deben mostrarse independientemente.

Si no hay documentos en el alias, es tentador que godoc muestre los documentos originales, pero el nombre del tipo sería incorrecto si el alias también cambiara el nombre, los documentos podrían hacer referencia a elementos que no están en el paquete actual y, si se está utilizando para una refactorización gradual, podría aparecer un mensaje que diga "En desuso: use X" cuando esté mirando X.

Sin embargo, tal vez eso no importe para la mayoría de los casos de uso. Esas son cosas que podrían salir mal, no cosas que saldrán mal. Y algunos de ellos podrían detectarse mediante el uso de pelusas, como los alias renombrados y la copia accidental de advertencias de obsolescencia.

No estoy seguro de si la siguiente idea se había publicado antes, pero ¿qué pasa con un enfoque similar a "gofix" / "gorename" basado principalmente en herramientas? Elaborar:

  • cualquier paquete puede contener un conjunto de reglas de reescritura (por ejemplo, mapeo pkg.Ident => otherpkg.Ident )
  • esas reglas de reescritura se pueden especificar con etiquetas //+rewrite ... dentro de archivos go arbitrarios
  • esas reglas de reescritura no se limitan a cambios compatibles con ABI, también es posible hacer otras cosas (por ejemplo, pkg.MyFunc(a) => pkg.MyFunc(context.Contex(), a) )
  • Se puede usar una herramienta similar a gofix para aplicar todas las transformaciones al repositorio actual. Esto facilita a los usuarios de un paquete actualizar su código.
  • no es necesario llamar a la herramienta gofix para compilar correctamente. Una biblioteca que todavía quiera usar la API antigua de una dependencia X (para seguir siendo compatible con las versiones antiguas y nuevas de X) todavía puede hacerlo. El comando go build debería aplicar las transformaciones (especificadas en las etiquetas de reescritura del paquete X) sobre la marcha sin cambiar los archivos en el disco.

Los últimos pasos pueden complicar / ralentizar un poco el compilador, pero básicamente es solo un preprocesador y la cantidad de reglas de reescritura debe mantenerse pequeña de todos modos. Entonces, suficiente lluvia de ideas por hoy :)

El uso de alias para evitar la sobrecarga de llamadas a funciones parece un truco para evitar la incapacidad del compilador de integrar funciones que no son hojas. No creo que las deficiencias de implementación deban influir en la especificación del idioma.

@josharian Si bien no pretendías que fueran propuestas completas, déjame responder (aunque sea solo, para que quien se inspire en ti pueda tener en cuenta la crítica inmediata):

  1. Realmente no resuelve el problema, porque las conversiones no son realmente el problema. x/net/context.Context es asignable / convertible / lo que sea posible a context.Context . El problema son los tipos de orden superior; es decir, los tipos func (ctx x/net/context.Context) y func (ctx context.Context) no son los mismos, aunque los argumentos son asignables. Entonces, para que 1 resuelva el problema, type T struct { S } tendría que significar que T y S son tipos idénticos. Lo que significa que, después de todo, simplemente está usando una sintaxis diferente para los alias (solo que esta sintaxis ya tiene un significado diferente).

  2. Nuevamente tiene un problema con los tipos de orden superior, porque los tipos asignables / convertibles no tienen necesariamente la misma representación de memoria (y si la tienen, la interpretación puede cambiar significativamente). Por ejemplo, un uint8 se puede convertir en un uint64 y viceversa. Pero eso significaría que, por ejemplo, con type T ~uint8 , el compilador no puede saber cómo llamar a func(T) ; ¿Necesita insertar 1, 2,4 u 8 bytes en la pila? Puede haber formas de solucionar este problema, pero me parece bastante complicado (y más difícil de entender que los alias).

Gracias, @Merovius.

  1. Sí, me perdí la satisfacción de la interfaz aquí. Tienes razón, esto no funciona.

  2. Tenía en mente "tener la misma representación de la memoria". El convertible de ida y vuelta claramente no es la explicación correcta de eso, gracias.

@uluyol sí, se trata en gran medida de la incapacidad del compilador para

En cualquier caso, como dije, es una tangente menor.

@josharian Problema similar: [2]uintptr y interface{} tienen la misma representación de memoria; por lo que solo confiar en la representación de la memoria permitirá eludir la seguridad de los tipos. uint64 y float64 tener ambos la misma representación de la memoria y son convertibles de ida y vuelta, pero aún así conducir a resultados muy extraños por lo menos, si no sabe cuál es cuál.

Sin embargo, es posible que se salga con la suya con "el mismo tipo subyacente". No estoy seguro de cuáles serían las implicaciones de eso. De la parte superior de mi sombrero, eso podría conducir a un error si se usa un tipo en los campos, por ejemplo. Si tiene type S1 struct { T1 } y type S2 struct { T2 } (con T1 y T2 el mismo tipo subyacente), entonces bajo type L1 ~T1 ambos podrían funcionar como type S struct { L1 } , pero como T1 y T2 todavía tienen un tipo subyacente diferente (aunque similar), con type L2 ~S1 no tendrá S2 parece S1 y no se puede usar como L2 .

Por lo tanto, tendría que, en varios lugares de la especificación, reemplazar o enmendar "tipos idénticos" con "el mismo tipo subyacente" para que esto funcione, lo que parece difícil de manejar y probablemente tendrá consecuencias imprevistas para la seguridad de los tipos. Los tipos "parecidos" también parecen tener un potencial de abuso y confusión aún mayor que los alias, en mi humilde opinión, que parecen ser los principales argumentos en contra de los alias.

Sin embargo, si alguien puede proponer una regla simple que no tenga estos problemas, definitivamente debería considerarse como una alternativa :)

Siguiendo la idea de

Permitir la especificación de "tipos sustituibles". Esta es una lista de tipos que pueden ser sustituidos por el tipo nombrado en argumentos de función, valores de retorno, etc. El compilador permitiría llamar a una función con un argumento del tipo nombrado o cualquiera de sus sustitutos. Los tipos sustitutos deben tener una definición compatible con el tipo nombrado. Compatible aquí significa representaciones de memoria idénticas y declaraciones idénticas después de permitir otros tipos sustitutos en la declaración.

Un problema inmediato es que la direccionalidad de esta relación es opuesta a la propuesta de alias que invierte el gráfico de dependencia. Esto por sí solo podría hacerlo inviable, pero lo propongo aquí porque otros podrían pensar en una forma de evitarlo. Una forma podría ser declarar los sustitutos como comentarios // go en lugar de hacerlo mediante el gráfico de importación. De esta forma, tal vez se parezcan más a macros.

A la inversa, esta inversión de direccionalidad tiene algunas ventajas:

  • el conjunto de tipos sustituibles lo controla el autor del nuevo paquete, que está en mejor posición para garantizar la semántica
  • no se requieren cambios de código en el paquete original para que los clientes no tengan que actualizar hasta que comiencen a usar el nuevo paquete

Aplicando esto a la refactorización de contexto: el paquete de contexto de biblioteca estándar declararía que context.Context puede ser sustituido por golang.org/x/net/context.Context . Esto significa cualquier uso que acepte el contexto. El contexto también puede aceptar un golang.org/x/net/context.Context en su lugar. Sin embargo, las funciones del paquete de contexto que devuelven un contexto siempre devolverán un context.Context .

Esta propuesta elude el problema de incrustación (# 17746) porque el nombre del tipo incrustado nunca cambia. Sin embargo, un tipo incrustado podría inicializarse utilizando un valor de un tipo sustituto.

@iand @josharian estás pidiendo una determinada variante de tipos covariantes.

@josharian , gracias por las sugerencias.

Re type T struct { S } , que parece una sintaxis diferente para el alias, y no necesariamente más clara.

Re type T ~S , no estoy seguro de en qué se diferencia del alias o no estoy seguro de cómo ayuda a refactorizar. Supongo que en una refactorización (digamos, io.ByteBuffer -> bytes.Buffer), escribirías:

package io
type ByteBuffer ~bytes.Buffer

pero luego, si, como dices, "para hacer básicamente cualquier cosa con T, necesitas convertirlo a S", entonces todo el código que hace cualquier cosa con io.ByteBuffer todavía se rompe.

Re type T S "alias" : Un punto clave que @bcmills hizo anteriormente es que tener varios nombres equivalentes para tipos es un cambio de idioma, sin importar cómo se deletree. Todos los compiladores necesitan saber que, digamos, io.ByteBuffer y bytes.Buffer son lo mismo, al igual que cualquier herramienta que analice o incluso verifique el código. La parte clave de su sugerencia me parece algo así como "tal vez deberíamos planificar con anticipación otras adiciones". Tal vez, pero no está claro que una cadena sea la mejor manera de describirlos, y tampoco está claro que queremos diseñar una sintaxis (como anotaciones generalizadas de Java) sin una necesidad clara. Incluso si tuviéramos una forma general, todavía tendríamos que considerar cuidadosamente todas las implicaciones de cualquier nueva semántica que introdujimos, y la mayoría seguirían siendo cambios de lenguaje que requerirían actualizar todas las herramientas (excepto gofmt, es cierto). A fin de cuentas, parece más sencillo seguir encontrando la forma más clara de escribir los formularios que necesitamos uno por uno en lugar de crear un metalenguaje de un tipo u otro.

@Merovius FWIW, yo diría que [2] uintptr y la interfaz {} no tienen la misma representación de memoria. Una interfaz {} es un [2] inseguro. El puntero no es un [2] uintptr. Un uintptr y un puntero son representaciones diferentes. Pero creo que su punto general es correcto, que no queremos permitir necesariamente la conversión directa de ese tipo de cosas. Quiero decir, ¿puedes convertir de interfaz {} a [2] * bytes también? Es mucho más de lo que se necesita aquí.

@jimmyfrasche y @nigeltao , re godoc: Estoy de acuerdo en que también necesitamos trabajar temprano. Estoy de acuerdo en que no deberíamos codificar la suposición de que "la nueva característica, sea lo que sea, solo se utilizará para la refactorización de la base de código". Puede tener otros usos importantes, como los que encontró Nigel para ayudar a escribir un paquete de extensión de dibujo con alias. Espero que las cosas desaprobadas se marquen explícitamente en sus comentarios de documentos, como dijo Jimmy. Pensé en generar un comentario de documento automáticamente si uno no está allí, pero no hay nada obvio que decir que no debería estar claro en la sintaxis (hablando en general). Para hacer un ejemplo específico, considere los antiguos alias de Go 1.8. Dado

type ByteBuffer => bytes.Buffer

podríamos sintetizar un comentario de documento que diga "ByteBuffer es un alias para bytes.Buffer", pero eso parece redundante al mostrar la definición. Si alguien escribe "tipo X estructura {}" hoy, no sintetizamos "X es un tipo con nombre para una estructura {}".

@iand , gracias. Parece que su propuesta requiere que el autor del nuevo paquete escriba la definición exacta del paquete anterior y luego también una declaración que vincule los dos, como (sintaxis de creación):

package old
type T { x int }

package new
import "old"
type T1 { x int }
substitutable T1 <- old.T

Estoy de acuerdo en que la inversión de la importación es problemática y puede ser un obstáculo por sí misma, pero saltemos eso. En este punto, el código base parece estar en un estado frágil: ahora el paquete nuevo puede romperse mediante un cambio para agregar un campo de estructura en el paquete antiguo. Dada la línea sustituible, solo hay una definición posible para T1: exactamente la misma que la antigua. Si los dos tipos todavía tienen definiciones distintas, entonces también debe preocuparse por los métodos: ¿las implementaciones de métodos también deben coincidir? Si no es así, ¿qué sucede cuando pones una T en una interfaz {} y luego la sacas usando una aserción de tipo como T1 y llamas a M ()? ¿Obtienes T1.M? ¿Qué pasa si lo extrae como una interfaz {M ()}, sin nombrar T1 directamente, y llama a M ()? ¿Obtienes TM? Hay mucha complejidad causada por la ambigüedad de tener ambas definiciones en el árbol de origen.

Por supuesto, se podría decir que la línea sustituible hace que el resto sea redundante y no requiere una definición para el tipo T1 ni ningún método. Pero eso es básicamente lo mismo que escribir (en la antigua sintaxis de alias) type T1 => old.T .

Volviendo al problema del gráfico de importación, aunque todos los ejemplos del artículo hicieron que el código antiguo se definiera en términos del nuevo código, si el gráfico del paquete fuera tal que nuevo tuviera que importar el antiguo en su lugar, es igualmente efectivo poner la redirección en el nuevo paquete durante la transición.

Creo que esto muestra que en cualquier transición como esta, probablemente no haya una distinción útil entre el autor del nuevo paquete y el autor del paquete anterior. Al final, el objetivo es que el código se haya agregado al nuevo y eliminado del antiguo, por lo que ambos autores (si son diferentes) deben participar en ese momento. Y los dos también necesitan algún tipo de compatibilidad coordinada durante el medio, ya sea explícito (algún tipo de redireccionamiento) o implícito (las definiciones de tipo deben coincidir exactamente, como en el requisito de sustituibilidad).

@rsc ese escenario de rotura sugiere que cualquier tipo de alias debe ser bidireccional. Incluso bajo la propuesta de alias anterior, cualquier cambio en el nuevo paquete podría potencialmente romper cualquier número de paquetes que tengan el alias del tipo.

@iand Si solo hay una definición (porque la otra dice "igual que _ esa_ una"), entonces no hay que preocuparse de que no estén sincronizadas.

En # 13467, @joegrasse señala que sería bueno si esta propuesta proporcionara un mecanismo para permitir que tipos C idénticos se conviertan en tipos Go idénticos cuando se usa cgo en varios paquetes. Ese no es en absoluto el mismo problema que para el que es este problema, pero ambos problemas están relacionados con el alias de tipos.

¿Hay algún resumen de las restricciones / limitaciones propuestas / aceptadas / rechazadas de los alias? Algunas preguntas que me vienen a la mente son:

  • ¿El RHS siempre está completamente calificado?
  • Si se permiten alias a alias, ¿cómo manejamos los ciclos de alias?
  • ¿Los alias deberían poder exportar identificadores no exportados?
  • ¿Qué sucede cuando incrusta un alias? (cómo se accede al campo incrustado)
  • ¿Los alias están disponibles como símbolos en el programa construido?
  • Inyección de cadenas de ldflags: ¿y si nos referimos a un alias?

@rsc No quiero desviar demasiado la conversación, pero bajo la propuesta de alias, si "nuevo" elimina un campo en el que "antiguo" dependía, significa que los clientes "antiguos" ahora no pueden compilar.

Sin embargo, según la propuesta de sustitución, creo que podría arreglarse que solo los clientes que usan tanto lo antiguo como lo nuevo juntos se rompan. Para que eso sea posible, la directiva de sustitución debería validarse sólo cuando el compilador detecte un uso de tipos "antiguos" en el paquete "nuevo".

@thwd No creo que haya una buena reseña todavía. Mis notas:

  • Los ciclos de alias no son un problema. En el caso de alias de cruce de paquetes, un ciclo ya está prohibido debido a un ciclo de importación. En el caso de los alias que no cruzan paquetes, obviamente deben rechazarse, lo que es muy similar a los ciclos en el orden de inicialización. Personalmente, me gustaría tener alias a alias, porque no creo que deban restringirse a casos de uso de reparación gradual (vea mi comentario arriba) y sería triste si el paquete A pudiera romperse si alguien moviera un tipo en paquete B con un alias (imagina x/image/draw.Image alias draw.Image y luego alguien decide mover draw.Image a image.Draw través de un alias, asumiendo que es seguro. De repente x/image/draw saltos, porque no se permiten alias a alias).
  • Creo que anteriormente los defensores de los alias han acordado que la exportación de alias identificadores no exportados es probablemente una mala idea debido a la rareza que puede causar. Efectivamente, eso significa que los alias a identificadores no exportados son inútiles y pueden rechazarse por completo.
  • La pregunta de incrustación, AFAIK, aún no se ha resuelto. Hay una discusión completa en # 17746, espero que esta discusión continúe si / cuando / antes de que se decida seguir adelante con alias (pero todavía existe la posibilidad de una solución alternativa o la decisión de no hacer reparaciones graduales como un objetivo en absoluto)

@iand , re "solo los clientes que usan tanto lo antiguo como lo nuevo juntos se romperían", ese es el único caso interesante. Son los clientes mixtos los que lo convierten en una reparación de código gradual. Los clientes que usan solo el código nuevo o solo el código anterior funcionarán hoy.

Hay algo más a considerar, que aún no he visto mencionado en otra parte:

Dado que un objetivo explícito aquí es permitir una refactorización grande y gradual en grandes bases de código descentralizadas, habrá situaciones en las que el propietario de una biblioteca quiera hacer algún tipo de limpieza que requerirá que un número desconocido de clientes cambien su código (al final " Retirar el paso de la antigua API "). Una forma común de hacerlo es agregar una advertencia de obsolescencia, pero el compilador Go no tiene advertencias.

Sin ningún tipo de advertencia del compilador, ¿cómo puede el propietario de una biblioteca estar seguro de que es seguro completar la refactorización?

Una respuesta podría ser algún tipo de esquema de control de versiones: es una nueva versión de la biblioteca con una nueva API incompatible. En ese caso, tal vez el control de versiones sea la respuesta completa, no los alias de tipo.

Alternativamente, ¿qué tal si se permite que el autor de la biblioteca agregue una "advertencia de obsolescencia" que en realidad causa un _error_ de compilación para los clientes, pero con un algoritmo explícito para la refactorización que necesitan realizar? Me estoy imaginando algo como:

Error: os.time is obsolete, use time.time instead. Run "go upgrade" to fix this.

Para los alias de tipo, supongo que el algoritmo de refactorización sería simplemente "reemplazar todas las instancias de OldType con NewType", pero podría haber sutilezas, no estoy seguro.

De todos modos, eso permitiría al autor de la biblioteca hacer el mejor esfuerzo posible para advertir a todos los clientes que su código está a punto de romperse y darles una manera fácil de solucionarlo, antes de eliminar la API anterior por completo.

@iainmerrick Hay errores abiertos para estos: golang / lint # 238 y golang / gddo # 456

Resolver el problema de reparación gradual de código, como se describe en el artículo de @rsc , se reduce a requerir una forma para que dos tipos sean intercambiables (ya que existen soluciones para vars, funcs y consts).

Esto necesita una herramienta o un cambio de idioma.

Dado que hacer dos tipos intercambiables es, por definición, cambiar la forma en que funciona el lenguaje, cualquier herramienta sería un mecanismo para simular la equivalencia fuera del compilador, probablemente reescribiendo todas las instancias del tipo antiguo en el nuevo tipo. Pero esto significa que dicha herramienta tendría que reescribir el código que no posee, como un paquete vendido que usa golang.org/x/net/context en lugar del paquete de contexto stdlib. La especificación para el cambio tendría que estar en un archivo de manifiesto separado o en un comentario legible por máquina. Si no ejecuta la herramienta, obtendrá errores de compilación. Todo eso se vuelve complicado de tratar. Parece que una herramienta crearía tantos problemas como resuelve. Todavía sería un problema con el que todos los que usan estos paquetes tienen que lidiar, aunque algo más agradable ya que una parte está automatizada.

Si se cambia el idioma, el código solo necesita ser modificado por quienes lo mantienen y, para la mayoría de las personas, las cosas simplemente funcionan. Las herramientas para ayudar a los mantenedores siguen siendo una opción, pero sería mucho más simple ya que la fuente es la especificación, y solo los mantenedores de un paquete necesitarían invocarlo.

Como señaló @griesemer (no recuerdo dónde, ha habido tantos hilos sobre esto) Go ya tiene alias, para cosas como byteuint8 , y cuando importas un paquete dos veces, con diferentes nombres locales, en el mismo archivo fuente.

Agregar una forma de tipos de alias explícitamente en el lenguaje solo nos permite usar la semántica que ya existe. Hacerlo resuelve un problema real de una manera manejable.

Un cambio de idioma sigue siendo un gran problema y hay muchas cosas por resolver, pero creo que, en última instancia, es lo correcto aquí.

Hasta donde yo sé, un "elefante en la habitación" es el hecho de que para los alias de tipo, introducirlos permitirá usos no temporales (es decir, "no refactorizados"). He visto los mencionados de pasada (por ejemplo, "reexportar identificadores de tipo en un paquete diferente para simplificar la API"). Siguiendo la buena tradición de propuestas anteriores, enumere todos los usos alternativos conocidos de los alias de tipo en la subsección "impacto" . Esto también debería traer el beneficio de alimentar la imaginación de las personas para inventar posibles usos alternativos adicionales y sacarlos a la luz en la discusión actual. Como está ahora, la propuesta parece pretender que los autores desconocen por completo otros usos posibles de los alias de tipo. Además, en cuanto a la reexportación, Rust / OCaml puede tener alguna experiencia sobre cómo funcionan para ellos.

Pregunta adicional: aclare si los alias de tipo permitirían agregar métodos al tipo en el nuevo paquete (posiblemente rompiendo la encapsulación) o no. Además, ¿el nuevo paquete obtendría acceso a campos privados de estructuras antiguas o no?

Pregunta adicional: aclare si los alias de tipo permitirían agregar métodos al tipo en el nuevo paquete (posiblemente rompiendo la encapsulación) o no. Además, ¿el nuevo paquete obtendría acceso a campos privados de estructuras antiguas o no?

Un alias es solo otro nombre para un tipo. No cambia el paquete del tipo. Así que no a sus dos preguntas (a menos que el paquete nuevo == paquete antiguo).

@akavel A partir de ahora, no hay ninguna propuesta. Pero sí conocemos dos posibilidades interesantes que surgieron durante las pruebas de alias de Go 1.8.

  1. Los alias (o simplemente los alias de tipo) permitirían crear reemplazos directos que expandan otros paquetes. Por ejemplo, consulte https://go-review.googlesource.com/#/c/32145/ , especialmente la explicación en el mensaje de confirmación.

  2. Los alias (o simplemente los alias de tipo) permitirían estructurar un paquete con una pequeña superficie de API pero una implementación grande como una colección de paquetes para una mejor estructura interna, pero aún presentando un solo paquete para ser importado y utilizado por los clientes. Hay un ejemplo algo abstracto descrito en https://github.com/golang/go/issues/16339#issuecomment -232813695.

El objetivo subyacente de los alias es excelente, pero aún parece que no estamos siendo muy honestos con el objetivo de refactorizar el código, a pesar de ser el motivador número uno para la función. Algunas de las propuestas sugieren bloquear el nombre, y aún no lo he visto mencionado que los tipos generalmente cambian su superficie con tales refactorizaciones también. Incluso el ejemplo de os.Error => error se menciona a menudo en torno a los alias ignora el hecho de que os.Error tenía un método String y no Error . Si simplemente moviéramos el tipo y le cambiamos el nombre, todo el código de manejo de errores se rompería independientemente. Ese es un lugar común durante las refactorizaciones ... los métodos antiguos se renombran, mueven, eliminan y no los queremos en el nuevo tipo, ya que preservarían la incompatibilidad con el nuevo código.

En aras de ayudar, aquí hay una idea inicial: ¿qué pasaría si miramos el problema en términos de adaptadores, en lugar de alias? Un adaptador le daría a un tipo existente un nombre alternativo _y una interfaz_, y se puede usar sin adornos en lugares donde antes se veía el tipo original. El adaptador necesitaría definir explícitamente los métodos que admite, en lugar de asumir que está presente la misma interfaz del tipo adaptado subyacente. Esto sería muy parecido al comportamiento actual de type foo bar , pero con algo de semántica adicional.

io.ByteBuffer

Por ejemplo, aquí hay un esqueleto de ejemplo que aborda el caso io.ByteBuffer , utilizando la palabra clave temporal "adapta" por el momento:

type ByteBuffer adapts bytes.Buffer

func (old *ByteBuffer) Write(b []byte) (n int, err error) {
        buf := (*bytes.Buffer)(old)
        return buf.Write(b)
}

(... etc ...)

Entonces, con ese adaptador en su lugar, este código sería válido:

func newfunc(b *bytes.Buffer) { ... }
func oldfunc(b *io.ByteBuffer) { ... }

func main() {
        var newvar bytes.Buffer
        var oldvar io.BytesBuffer

        // New code using the new type obviously just works.
        newfunc(&newvar)

        // New code using the old type receive the underlying value that was adapted.
        newfunc(&oldvar)

        // Old code using the old type receive the adapted value unchanged.
        oldfunc(&oldvar)

        // Old code gets new variable adapted on the way in. 
        oldfunc(&newvar)
}

Las interfaces de newfunc y oldfunc son compatibles. Ambos realmente aceptan *bytes.Buffer , con oldfunc adaptándolo a *io.BytesBuffer al entrar. El mismo concepto funciona para asignaciones, resultados, etc.

os.Error

La misma lógica probablemente también funcione en la interfaz, aunque la implementación del compilador es un poco más complicada. Aquí hay un ejemplo de os.Error => error , que maneja el hecho de que se cambió el nombre del método:

package os

type Error adapts error

func (e Error) String() string { return error(e).Error() }

Sin embargo, este caso necesita más reflexión porque métodos como:

func (v *T) Read(b []byte) (int, os.Error) { ... }`

Devolverá un tipo que tiene un método String , por lo que normalmente querríamos adaptarnos en la dirección opuesta para que el código se pueda arreglar gradualmente.

_ ACTUALIZADO: Necesita más reflexión.

Problema de incrustación

En términos del error de incrustación que arrastró la función de 1.8, el resultado es un poco más claro con los adaptadores, ya que no son solo nombres nuevos para lo mismo: si el adaptador está incrustado, el nombre de campo utilizado es el de el adaptador por lo que la lógica antigua sigue funcionando, y el acceso al campo utilizará la interfaz del adaptador a menos que se entregue explícitamente en un contexto que tome el tipo subyacente. Si se incrusta el tipo no adaptado, ocurre lo habitual.

kubernetes, estibador

Los problemas planteados en la publicación parecen variaciones de los problemas anteriores y resueltos por la propuesta.

vars, consts

No tendría mucho sentido adaptar variables o constantes en ese escenario, ya que realmente no podemos asociar métodos con ellas directamente. Son sus tipos los que se adaptarían o no.

godoc

Seríamos explícitos sobre el hecho de que la cosa es un adaptador y mostraremos la documentación como de costumbre, ya que contiene una interfaz independiente de la cosa adaptada.

sintaxis

Por favor, elija algo agradable. ;)

@iainmerrick @zombiezen

¿Deberíamos también inferir automáticamente que un tipo con alias es heredado y debería ser reemplazado por el nuevo tipo? Si aplicamos golint, godoc y herramientas similares para visualizar el tipo antiguo como obsoleto, limitaría el abuso del alias de tipos de manera muy significativa. Y se resolvería la preocupación final de que se abuse de la función de aliasing.

Dos observaciones:

1. La semántica de las referencias de tipo depende del caso de uso de refactorización admitido

La propuesta de Gustavo muestra que es necesario trabajar más en el caso de uso de las referencias de tipo y la semántica resultante.

La nueva propuesta de Ross incluye una nueva sintaxis type OldAPI = newpkg.newAPI . Pero, ¿qué es la semántica? ¿Es imposible extender OldAPI con métodos o campos públicos heredados? Suponiendo que sí como respuesta que requiere que newAPI admita todos los métodos y campos públicos de OldAPI para mantener la compatibilidad. Tenga en cuenta que cualquier código en el paquete con OldAPI que se base en campos y métodos privados debe reescribirse para usar solo la nueva API pública, asumiendo que la modificación de las restricciones de visibilidad de los paquetes está fuera de la mesa.

La ruta alternativa sería permitir que se definan métodos adicionales para OldAPI. Eso podría aliviar la carga de NewAPI para proporcionar todos los métodos públicos antiguos. Pero eso haría que OldAPI sea un tipo diferente de NewAPI. Debe mantenerse alguna forma de asignabilidad entre valores de los dos tipos, pero las reglas se volverían complejas. Permitir la adición de campos resultaría en una mayor complejidad.

2. El paquete con NewAPI no puede importar el paquete con OldAPI

La redefinición de OldAPI requiere que el paquete O que contenga la definición de OldAPI importe el paquete N con NewAPI. Eso implica que el paquete N no puede importar O. Tal vez sea tan obvio que no se ha mencionado, pero me parece una restricción importante para el caso de uso de refactorización.

Actualización: el paquete N no puede tener ninguna dependencia del paquete O. Por ejemplo, no puede importar un paquete que importe O.

@niemeyer Los cambios como cambiar el nombre de un método ya son posibles gradualmente: a) Agregar el nuevo método, llamar al antiguo bajo el capó (o viceversa), b) cambiar gradualmente a todos los usuarios al nuevo método, c) eliminar el método anterior. Puede combinar eso con un alias de tipo. La razón por la que esto se centra en el movimiento de tipos es que esto es lo único identificado, que aún no es posible. Todos los demás cambios identificados son posibles, incluso si pueden utilizar varios pasos (por ejemplo, cambiar el conjunto de argumentos de un método sin cambiarle el nombre). Creo que es preferible elegir una solución con menos superficie (menos cosas que entender).

@rakyll Personalmente, si considerara que los alias son útiles para algo que no sea refactorial (como paquetes de envoltura, que encuentro un caso de uso excelente) simplemente los usaría, las advertencias de depreciación al diablo. Me enojaría con quienquiera que los mutilara artificialmente y los hiciera confusos para mis usuarios, pero no me desanimaría.

Creo que en algún momento debe debatirse si realmente consideramos los paquetes de envoltura, las importaciones públicas de protobuf o exponer las API de paquetes internos como algo tan malo (y no sé cómo debatir mejor algo tan subjetivo sin que un lado repita una y otra vez que son ilegibles y el otro dice "no, no lo son". No hay mucho argumento objetivo aquí, me parece).

Al menos (obviamente) creo que son algo bueno y también soy de la opinión de que agregar una función de lenguaje y restringirlo artificialmente a un solo caso de uso es algo malo; un lenguaje ortogonal y bien diseñado le permite hacer todo lo posible con la menor cantidad de funciones posibles. Quiere que sus características amplíen el "espacio vectorial extendido de posibles programas" tanto como sea posible, por lo que agregar una característica que solo agrega un punto al espacio me parece extraño.

Me gustaría que se tuviera en cuenta otro caso de uso ligeramente diferente a medida que se desarrolla cualquier propuesta de alias de tipo.

Aunque el caso de uso principal que estamos discutiendo en este tema es el tipo _replacement_, los alias de tipos también serían muy útiles para eliminar un cuerpo de código de una dependencia de un tipo.

Por ejemplo, supongamos que un tipo resulta ser "inestable" (es decir, se sigue modificando, quizás de forma incompatible). Entonces, algunos de sus usuarios podrían querer migrar a un tipo de reemplazo "estable". Estoy pensando en el desarrollo en github, etc., donde los propietarios de un tipo y sus usuarios no necesariamente trabajan en estrecha colaboración ni están de acuerdo con el objetivo de estabilidad.

Otros ejemplos serían cuando un solo tipo es lo único que impide que se elimine una dependencia de un paquete grande o problemático, por ejemplo, cuando se ha descubierto una incompatibilidad de licencia.

Entonces el proceso aquí sería:

  1. Definir el alias de tipo
  2. Cambiar el cuerpo de código relevante para usar el alias de tipo
  3. Reemplace el alias de tipo con una definición de tipo.

Al final de este proceso, habría dos tipos independientes que serían libres de evolucionar en sus propias direcciones.

Tenga en cuenta que en este caso de uso:

  • no hay opción de cambiar el paquete que contiene la definición de tipo original para agregar un alias de tipo allí (ya que es poco probable que los propietarios estén de acuerdo con esto)
  • el tipo original no está en desuso (aunque podría considerarse como tal en el cuerpo del código en el proceso de "destete" del tipo).

@Merovius En el momento en que elimina o cambia el nombre del método anterior, mata a todos los clientes que lo estaban usando, a la vez. Si está dispuesto a hacer eso, todo el ejercicio no trivial de agregar una función de idioma para evitar que se rompa todo a la vez es discutible. También podríamos decir exactamente lo mismo para mover el código: simplemente cambie el nombre del tipo en cada sitio de llamada a la vez. Hecho. Ambas acciones son simplemente cambios de nombre atómicos, que tienen en común el hecho de que asumen el acceso completo a cada línea de código en los sitios de llamada. Este podría ser el caso de Google, pero como mantenedor de grandes aplicaciones y bibliotecas de código abierto, no es el mundo en el que vivo.

En la mayoría de los casos, considero que las críticas son injustas, ya que el equipo de Go hace todo lo posible para que el proyecto sea inclusivo para las partes externas, pero en el momento en que asume que tiene acceso a cada línea de código que llama a un paquete determinado, eso es un jardín que no coincide con el contexto de una comunidad de código abierto. Agregar una función de refactorización a nivel de idioma que solo funciona dentro de jardines amurallados sería atípico, por decir lo menos.

@niemeyer Al parecer, no me

  1. Agregue una nueva API, intercambiable con la antigua API
  2. Cambie gradualmente a los consumidores a una nueva API
    3a. Una vez que se haya migrado todo o se agote el período de desactivación, elimine la API anterior
    3b. Proporcione estabilidad indefinida manteniendo ambas API para siempre (consulte, por ejemplo, esta parte del artículo )

Parece estar discutiendo acerca de hacer 3a frente a 3b. Pero lo que estaba señalando, que 1. ya es posible para nombres de métodos pero no es posible para tipos, que es de lo que se trata.

Sin embargo, ahora me doy cuenta de que creo que no te entendí :) Es posible que hayas estado señalando que os.Error son definiciones de interfaz diferentes, por lo que el movimiento realmente no funciona. Creo que eso es cierto; Si prohíbe la eliminación de API, los alias de tipos no permitirían cambiar el nombre de los métodos de los tipos de interfaz.

Sin embargo, tal vez pueda aclararme algo sobre su idea de adaptador: ¿No permitiría eso también usar (por ejemplo, en el caso os.Error) cualquier fmt.Stringer como os.Error?

En cualquier caso, la idea del adaptador parece que vale la pena seguir desarrollándose, incluso si soy un poco escéptico al respecto. Pero tener una forma de refactorizar gradualmente las interfaces sin romper a los posibles implementadores y / o consumidores es un buen objetivo.

@niemeyer Sí, mencionas un buen punto sobre el cambio de nombre del método por error también. Eso presenta muchas complicaciones y no es algo que esté tratando de abordar aquí. Debido a que solo una fracción del código que menciona error / os.Error realmente llama al método, el movimiento fue la parte más dolorosa que el cambio de método. Creo que podemos tratar el cambio de nombre de métodos como un problema independiente de cambiar la ubicación del código. Si el movimiento estuviera sucediendo hoy y pudiéramos hacer la reorganización del paquete sin problemas, pero nos quedamos con el nombre del método anterior, eso aún sería un progreso significativo. Centrar este problema en la ubicación del código pretende simplificarlo.

Estoy de acuerdo en que si hubiera alguna solución general que manejara ambos tipos de cambios, sería genial. No veo cuál es esa solución. En particular, no entiendo cómo funcionan los interruptores de tipo con los adaptadores que describió: ¿el valor de alguna manera se convierte automáticamente durante el cambio de tipo? ¿Y la reflexión? Tener solo un tipo con dos nombres evita muchos problemas que surgen al tener dos tipos que se convierten automáticamente de un lado a otro.

@rsc Sí, el adaptador se convertiría automáticamente de forma consistente en cada situación, por lo que los interruptores de tipo no serían diferentes. Prohibiríamos los conmutadores de tipo que contengan tanto el adaptador como su tipo subyacente, ya que sería ambiguo. Es posible que me esté perdiendo algo, pero todavía no puedo ver un problema con la reflexión, ya que cada contexto de código necesita necesariamente usar el tipo adaptado o su tipo subyacente, explícitamente. Al igual que hoy, no podemos entrar en un interface{} sin saber cómo llegamos allí, si eso tiene sentido.

@Merovius Mis dos comentarios anteriores abordan precisamente los puntos que todavía está haciendo. Si mueve un tipo hoy, rompe el código que necesita ser arreglado. Si cambia el nombre de un método, rompe el código que necesita ser arreglado. Si elimina un método, cambia sus argumentos, rompe el código que necesita ser arreglado. Al refactorizar el código en cualquiera de estos casos, las correcciones deben hacerse de forma atómica con la rotura en cada sitio de llamada para que las cosas sigan funcionando. Permitir que el tipo se mueva pero que no se modifique es un caso muy limitado de refactorización, que en mi opinión no justifica una función de idioma.

@niemeyer Eso manejaría los tipos concretos. ¿Qué pasa con una afirmación de tipo para .(interface{String() string}) vs .(interface{Error() string}) o cualquier parte específica de la interfaz que haya cambiado? ¿El cheque tiene que considerar ambos posibles tipos subyacentes de alguna manera?

@niemeyer No. Cambiar el nombre de un método es posible de forma no atómica. por ejemplo, para mover un método de A.Foo a A.Bar , hacer

  1. Agregue el método A.Bar como envoltorio alrededor de A.Foo
  2. Migre a los usuarios para que solo llamen a A.Bar mediante arbitrariamente muchas confirmaciones
  3. Elimine A.Foo , o no lo haga, dependiendo de si está dispuesto a hacer cumplir una desaprobación.

Los argumentos de las funciones de cambio son posibles de forma no atómica. por ejemplo, para agregar un parámetro x int a func Foo() , haz

  1. Agregar func FooWithInt(x int) { Foo(); // use x somehow; }
  2. Migre usuarios para agregar el parámetro a través de muchas confirmaciones arbitrariamente
  3. Si no está dispuesto a hacer cumplir una desaprobación (o no le molesta tener el WithInt), ha terminado. De lo contrario, modifique Foo a func Foo(x int) { FooWithInt(x) } .
  4. Migre usuarios con s/FooWithInt/Foo/g través de muchas confirmaciones arbitrariamente.
  5. Eliminar FooWithInt .

Lo mismo funciona para casi todos los casos excepto los tipos móviles (y, estrictamente hablando, vars). no se requiere atomicidad. O rompe la compatibilidad al hacer cumplir la depreciación, o no lo hace, pero eso es completamente ortogonal a la atomicidad. La capacidad de usar dos nombres diferentes para referirse a lo mismo es lo que le permite eludir la atomicidad al hacer cambios básicamente arbitrarios y tiene esa capacidad para todos los casos excepto los tipos. Sí, para hacer un movimiento real, en lugar de una enmienda, debe estar dispuesto a hacer cumplir la desaprobación (por lo tanto, romper la compilación de un código potencialmente desconocido, lo que significa que esto necesita un anuncio amplio y oportuno). Pero incluso si no es así, la capacidad de aumentar las API con un nombre más conveniente u otro envoltorio útil (ver x / image / draw) también depende de la capacidad de referirse a lo antiguo por el nuevo nombre y viceversa.

La diferencia entre mover tipos hoy y cambiar el nombre de una función hoy es que, en el primer caso, realmente necesita un cambio atómico, mientras que para el segundo, puede realizar el cambio gradualmente, sobre repositorios y confirmaciones independientes. No como un "haré una confirmación que haga s / Foo / Bar /", pero hay un proceso para hacerlo.

De todas formas. No sé dónde estamos, aparentemente, hablando entre nosotros. Encuentro el documento de @rsc bastante claro para transmitir mi punto de vista y realmente no entiendo el tuyo :)

@rsc Puedo ver dos respuestas razonables. El simple de que la interfaz lleva el tipo que entró, adaptador o de otro tipo, y la semántica habitual se aplica cuando se afirma la interfaz. La otra es que el valor puede no estar adaptado si no satisface la interfaz, pero el valor subyacente sí. El primero es más simple y quizás suficiente para los casos de uso de refactorización que tenemos en mente, mientras que el segundo es quizás más consistente con la idea de que también podemos afirmarlo en el tipo subyacente.

@Merovius Claro, es posible cambiar el nombre de un método siempre que _no lo cambie realmente_ y obligue a los sitios de llamadas a usar una nueva API en su lugar. Del mismo modo, es posible mover un tipo siempre que _no lo mueva realmente _ y obligue a los sitios de llamadas a usar una nueva API en su lugar. Todos hemos estado haciendo ambas cosas durante años para preservar el funcionamiento del código antiguo.

@niemeyer Pero de nuevo: para los tipos, ni siquiera puedes agregar cosas de una manera decente. Ver x / imagen / dibujar. Y no todo el mundo puede tener una visión tan absoluta de la estabilidad; Yo mismo estoy de acuerdo con decir "en 6,12, ... meses $ function, $ type, ... va a desaparecer, asegúrese de que haya migrado lejos de él en ese momento" y luego simplemente rompa el código no mantenido que no se las arregla para seguir ese aviso de desaprobación (si alguien piensa que necesita soporte a largo plazo para las API, seguramente puede encontrar a alguien a quien pagar para que se lo proporcione). Incluso diría que la mayoría de la gente no tiene esa visión absoluta de la estabilidad; vea el reciente impulso de versiones semánticas, que solo tiene sentido si desea tener la opción de romper la compatibilidad. Y el documento argumenta muy bien cómo, incluso en ese caso, todavía se beneficiaría de la capacidad de tener reparaciones graduales y cómo puede aliviar, si no esencialmente resolver, el problema de la dependencia de los diamantes.

Puede descartar la mayoría de los casos de uso de alias para reparaciones graduales porque su postura sobre la estabilidad es absoluta. Pero diría que para la mayoría de la comunidad de go, eso es diferente, que hay un deseo de roturas y un uso para hacerlas lo más suavemente posible cuando suceden.

@niemeyer @rsc @Merovius He estado siguiendo su discusión (y toda la discusión) y me gustaría golpear descaradamente esta publicación justo en el medio.

Cuanto más iteramos sobre el tema, más nos acercamos a alguna forma de semántica de covarianza extendida. Entonces, aquí va un pensamiento: ya tenemos semántica de subtipos ("es-a") definida desde tipos concretos hasta interfaces y entre interfaces. Mi propuesta es hacer que las interfaces sean recursivamente covariantes (de acuerdo con las reglas de varianza actuales) hasta los argumentos de sus métodos.

Esto no resuelve el problema para todos los paquetes actuales. Pero puede resolver el problema de todos los paquetes futuros, aún por escribir, ya que las "partes móviles" de la API pueden ser interfaces (también fomenta un buen diseño).

Creo que podemos resolver todos los requisitos (ab) usando interfaces de esta manera. ¿Estamos rompiendo Go 1.0? No lo sé, pero creo que no lo somos.

@thwd Creo que necesitas definir con más precisión lo que quieres decir con "hacer interfaces recursivamente covariantes". Por lo general, en la subtipificación, los argumentos del método deben cambiar de forma contravariable y los resultados de forma covariante. Además, por lo que está diciendo, esto no resolvería ningún problema existente con tipos concretos (sin interfaz).

@thwd No estoy de acuerdo, que las interfaces (incluso las covariantes) son una buena solución para cualquiera de estos problemas (solo para instancias muy específicas). Para convertirlos en uno, necesitaría hacer que todo en su API sea una interfaz (porque nunca se sabe qué es posible que desee mover / cambiar en algún momento), incluidas vars / consts / funcs / ... y no creo que en todo, que eso es buen diseño (lo he visto en java. Me irrita). Si algo es una estructura, simplemente conviértalo en una estructura. Todo lo demás solo agrega una sobrecarga sintáctica extraña en su paquete y cada dependencia inversa para prácticamente ningún beneficio. También es la única forma de mantenerse cuerdo cuando comienza; comience de manera simple y luego pase a algo más general. Muchas de las complicaciones en API que he visto hasta ahora provienen de personas que piensan demasiado en el diseño y la planificación de API para una mayor generalización de la que nunca se necesitará. Y luego, en el 80% (ese número es una mentira obvia) de los casos, no sucede nada en absoluto, porque no hay un "diseño limpio de API".

(para ser claros: no estoy diciendo que las interfaces covariantes no sean una buena idea. Solo digo que no son una buena solución para estos problemas)

Para agregar al punto de

package foo

type Authority struct {
  Host string
  Port int
}

Con el tiempo, el paquete foo crece y termina ganando más responsabilidad (y tamaño de código) de lo que realmente quiere alguien que solo necesita el tipo Authority . Por lo tanto, tener una forma de crear un paquete fooauthority que solo contenga Authority y que los usuarios existentes de foo.Authority sigan funcionando es un caso de uso deseable. Tenga en cuenta que cualquier solución que solo considere los tipos de interfaz no ayudaría aquí.

@Merovius Tu último comentario ha sido completamente subjetivo y se dirige a mí personalmente en lugar de a mi propuesta. Esto no terminará bien, así que detendré esa línea de discusión aquí.

@griesemer @Merovius Estoy de acuerdo con los dos. Entonces, para cerrar el ciclo, podemos estar de acuerdo en que la discusión hasta ahora nos ha llevado a alguna noción de subtipos / covarianza. Además, cualquier implementación no debería incurrir en una indirección en tiempo de ejecución. Eso es lo que proponía @niemeyer (si lo entendí bien). Pero me encantaría leer más ideas. Yo también estaré pensando en el problema.

@niemeyer No había nada _ad hominem_ en los comentarios de @Merovius . Su afirmación de que "su postura sobre la estabilidad es absoluta" es una observación sobre su posición, no sobre usted, y es una inferencia razonable de algunas de sus declaraciones, como

En el momento en que elimina o cambia el nombre del método anterior, mata a todos los clientes que lo estaban usando, a la vez.

y

Por supuesto, es posible cambiar el nombre de un método siempre que no lo cambie y obligue a los sitios de llamadas a usar una nueva API en su lugar. Del mismo modo, es posible mover un tipo siempre que no lo mueva y obligue a los sitios de llamadas a usar una nueva API en su lugar. Todos hemos estado haciendo ambas cosas durante años para preservar el funcionamiento del código antiguo.

Tuve la misma impresión que Merovius por esas declaraciones: que no simpatizas con desaprobar algo por un tiempo y luego finalmente eliminarlo; que está comprometido a mantener el código en funcionamiento indefinidamente; que "su postura sobre la estabilidad es absoluta". (Y para evitar más malentendidos, estoy usando "usted" para referirme a sus ideas, no a su personalidad).

@niemeyer La adapts que estás sugiriendo parece estar estrechamente relacionada con instance de las clases de tipos de Haskell. Traduciendo libremente eso a Go, podría verse algo como:

package os

type Error interface {
  String() string
}

instance error Error (
  func (e error) String() string { return e.Error() }
)

Desafortunadamente (como señala @zombiezen ), no está claro cómo esto ayudaría para los tipos que no son de interfaz.

Tampoco me resulta obvio cómo interactuaría con los tipos de funciones (argumentos y valores de retorno); por ejemplo, ¿cómo ayudaría la semántica de adapts con la migración de Context a la biblioteca estándar?

Tengo la misma impresión que Merovius por esas declaraciones: que no simpatizas con desaprobar algo por un tiempo.

@jba Estos son hechos absolutos, no opiniones absolutas. Si elimina un método o un tipo, el código de Go que lo usa se rompe, por lo que estos cambios deben realizarse de forma atómica. Mi propuesta, sin embargo, es sobre la refactorización gradual del código, que es el tema aquí e implica desaprobación. Sin embargo, ese proceso de desaprobación no es una cuestión de simpatía. Tengo varios paquetes Go públicos con miles de dependencias en estado salvaje cada uno, y varias API independientes debido a esa evolución gradual. Cuando rompemos una API, es bueno hacer esas fallas en lotes, en lugar de transmitirlas, si esperamos que la gente no se vuelva loca. A menos que, por supuesto, viva en un jardín amurallado y pueda comunicarse con todos los sitios de llamadas para solucionarlo. Pero me repito ... todo eso se puede leer en la propuesta original anterior de una manera más articulada.

@Merovio

Personalmente, si considero que los alias son útiles para algo que no sea refactorial (como paquetes de envoltura, que encuentro un caso de uso excelente), simplemente los usaría, las advertencias de depreciación al diablo.

Mantenemos paquetes con una gran cantidad de API nuevas y obsoletas, y tener alias sin una explicación clara del estado del tipo antiguo (con alias) no ayudará a la reparación gradual del código y solo contribuirá a la abrumadora superficie de la API. Estoy de acuerdo con @niemeyer en que nuestra solución debe abordar los requisitos de una comunidad de desarrolladores distribuida que actualmente no tiene más señales que el texto godoc de forma libre que dice que una API está "obsoleta". Agregar una función de lenguaje para ayudar a desaprobar los tipos antiguos es el tema de este hilo, por lo que, naturalmente, lleva a la pregunta de cuál es el estado del tipo antiguo (con alias).

Me encantaría hablar sobre el alias de tipos bajo un tema diferente, como proporcionar extensión a un tipo o paquetes parciales, pero no en este hilo. Ese tema en sí tiene varios problemas específicos de encapsulación que deben abordarse antes de cualquier consideración.

Un operador específico o insinuar que el alias escrito se reemplaza de alguna manera podría ser saludable para comunicar a los usuarios que necesitan cambiar. Tal diferenciación permitiría a las herramientas informar automáticamente las API reemplazadas.

Para ser claros, la política de obsolescencia no es técnicamente posible para tipos fuera de la biblioteca estándar. Un tipo solo es antiguo desde la perspectiva de un paquete de alias. Dado que nunca podemos hacer cumplir esto en el ecosistema, aún me gustaría ver que los alias de biblioteca estándar implican la desaprobación estrictamente (insinuado por los avisos de desaprobación adecuados).

También estoy sugiriendo que estandaricemos la noción de desaprobación en una discusión paralela y que los apoyemos en nuestras herramientas centrales (golint, godoc, etc.). La falta de avisos de depreciación es el mayor problema en el ecosistema de Go y está más extendido que el problema de la reparación gradual del código.

@rakyll Soy comprensivo con el caso de uso de tener avisos de obsolescencia legibles por computadora; Solo me opongo a la noción de a) que los alias sean eso yb) que los emita como advertencias del compilador.

Para a), aparte del hecho de que me gustaría usar alias de manera productiva para otras cosas que no sean movimientos, también se aplicaría solo para un conjunto muy pequeño de depreciaciones. Por ejemplo, digamos que quisiera eliminar algunos parámetros de una función en un par de versiones; No puedo usar alias, de verdad, porque la firma de la nueva API será diferente, pero aún así me gustaría anunciarlo. Para b), las advertencias del compilador en mi humilde opinión son universalmente malas. Creo que esto está en línea con lo que go ya está haciendo, así que no creo que requiera justificación.

Estoy de acuerdo con todo lo que dice acerca de los avisos de baja. Aparentemente, ya existe una sintaxis para esto: # 10909, por lo que el siguiente paso para hacerlo más útil sería mejorar el soporte de herramientas resaltándolos en godoc y tener una marca que advierta sobre su uso (digamos, vet, golint o una herramienta separada en conjunto).

@rakyll Estoy de acuerdo en que stdlib debería comenzar con un uso conservador de los alias de tipo, en caso de que se introduzcan.


Barra lateral:

Antecedentes para aquellos que desconocen el estado de los comentarios en desuso en Go y las herramientas relacionadas, ya que está bastante extendido:

Como @Merovius menciona anteriormente, existe una convención estándar para marcar elementos como obsoletos, # 10909, consulte https://blog.golang.org/godoc-documenting-go-code

TL; DR: haga un párrafo en los documentos del artículo obsoleto que comience con "Deprecated:" y explique cuál es el reemplazo.

Hay una propuesta aceptada para que godoc muestre los elementos obsoletos de una manera más útil: # 17056.

@rakyll propuso que golint advierta cuando se utilicen elementos obsoletos: golang / lint # 238.


Incluso si stdlib adopta una postura conservadora sobre el uso de alias dentro de stdlib, no creo que la existencia de un alias de tipo deba implicar (de alguna manera que se detecte mecánicamente o se denote visualmente) que el tipo antiguo está en desuso, incluso si siempre significa eso en la práctica.

Hacerlo significaría uno de:

  • escanear otros paquetes stdlib para ver si algún tipo, no marcado explícitamente como obsoleto, tiene un alias en otro lugar
  • codificar todos los alias de stdlib en herramientas automatizadas
  • solo informa que el tipo anterior está en desuso cuando ya está buscando su reemplazo, lo que no ayuda al descubrimiento

Cuando se introduce un alias de tipo porque el tipo antiguo ha quedado obsoleto, debe manejarse marcando el tipo antiguo como obsoleto, con una referencia al nuevo tipo, independientemente.

Esto permite que existan mejores herramientas al permitir que sea más simple y más general: no necesita nada en casos especiales o incluso saber sobre alias de tipo: solo necesita coincidir con "Deprecated:" en los comentarios del documento.

Una política oficial, aunque quizás temporal, de que un alias en stdlib es solo para desaprobación es buena, pero solo debe aplicarse con los comentarios de desaprobación estándar y al rechazar otros usos para que pase la revisión del código.

@niemeyer Mi respuesta anterior se perdió debido a la pérdida de energía :( fuera de servicio:

Pero me estoy repitiendo ...

FWIW, encontré su última respuesta bastante útil. Me convenció de que estamos más de acuerdo de lo que parecía anteriormente (y de lo que todavía le puede parecer). Sin embargo, todavía parece haber falta de comunicación en alguna parte.

Mi propuesta, sin embargo, es sobre la refactorización gradual del código

Esto no es contencioso, creo. :) Estuve de acuerdo, desde el principio, en que su propuesta es una alternativa interesante a considerar para abordar el problema. Lo que me confunde son declaraciones como esta:

Si elimina un método o un tipo, el código de Go que lo usa se rompe, por lo que estos cambios deben realizarse de forma atómica.

Todavía me pregunto cuál es tu razonamiento aquí. Entiendo que la unidad de atomicidad es una única confirmación. Con esa suposición, simplemente no entiendo por qué está convencido de que la eliminación de un método o tipo no puede ocurrir primero por separado, arbitrariamente numerosas confirmaciones en los repositorios dependientes y luego, una vez que ya no hay un usuario aparente (y una amplia depreciación el intervalo ha pasado) el método o tipo se elimina en una confirmación en sentido ascendente (sin romper nada, ya que ya nadie depende). Estoy de acuerdo en que existe un cierto factor de confusión en torno a las dependencias inversas que no se adhieren a la desaprobación o que no puedes encontrar (o solucionar razonablemente), pero que, para mí, parece en gran medida independiente del asunto en cuestión; tendrá ese problema cada vez que aplique un cambio importante y sin importar cómo intente orquestarlo.

Y, para ser justos: la confusión no ayuda realmente con oraciones como

A menos que, por supuesto, viva en un jardín amurallado y pueda comunicarse con todos los sitios de llamadas para solucionarlo.

Si algo de lo que dije le dio la impresión de que este es el punto desde el que estoy discutiendo, espero que pueda dar un paso atrás y tal vez volver a leerlo asumiendo que estoy discutiendo completamente desde la posición del código abierto. comunidad (si no me cree, no dude en consultar mis contribuciones anteriores a este tema; siempre soy el primero en señalar que esto es más un problema de la comunidad que un problema de monorepo. , como señaló).

De todas formas. Encuentro esto tan agotador como tú. Sin embargo, espero entender su posición en algún momento.

simultáneamente hablar sobre si y cómo apoyar cosas como las importaciones públicas de protobuf ...
Creo que en algún momento debe debatirse si realmente consideramos que los paquetes de envoltura, las importaciones públicas de protobuf o la exposición de las API de paquetes internos son algo tan malo

nit: No creo que las importaciones públicas de protobuf deban mencionarse como un caso de uso secundario especial. Fueron diseñados para la reparación gradual del código, como se menciona explícitamente tanto en el documento de diseño interno como en la documentación pública , por lo que ya caen bajo el paraguas de los problemas descritos en este número. Además, creo que los alias de tipo serían suficientes para implementar las importaciones públicas de protobuf. (El compilador de proto genera vars, pero son lógicamente constantes, por lo que "var Enum_name = importado.Enum_name" debería ser suficiente).

@Merovius Gracias por la respuesta productiva. Permítanme intentar proporcionar algo de contexto:

Todavía me pregunto cuál es tu razonamiento aquí. Entiendo que la unidad de atomicidad es una única confirmación. Con esa suposición, simplemente no entiendo por qué está convencido de que la eliminación de un método o tipo no puede ocurrir primero por separado,

Nunca dije que no puede suceder. Déjame dar un paso atrás y repetirlo con más claridad.

Probablemente todos estemos de acuerdo en que el objetivo final es doble: queremos un software que funcione y queremos mejorar el software para poder seguir trabajando en él de una manera sana. Algunos de estos últimos están rompiendo cambios, lo que lo pone en desacuerdo con el objetivo anterior. Entonces hay tensión, lo que significa que hay algo de subjetividad sobre dónde se encuentra el punto óptimo. La parte interesante de nuestro debate radica aquí.

Una forma útil de buscar ese punto óptimo es pensar en las intervenciones humanas. Es decir, una vez que hace algo que requiere que las personas modifiquen manualmente el código para mantenerlo funcionando, se produce la inercia. Se necesita mucho tiempo para que la parte relevante de todas las bases de códigos dependientes pasen por este proceso. Pedimos a las personas ocupadas que hagan cosas que en la mayoría de los casos prefieren no molestar.

Otra forma de ver ese punto óptimo es la probabilidad de que el software funcione. No importa cuánto le pidamos a la gente que no utilice un método obsoleto. Si es de fácil acceso y resuelve su problema aquí y ahora, la mayoría de los desarrolladores simplemente lo usarán. El contraargumento común aquí es: _oh, ¡pero entonces es su problema cuando se rompe! _ Pero eso va en contra del objetivo establecido: queremos software que funcione, no ser correcto.

Por lo tanto, es de esperar que esto proporcione más información sobre por qué simplemente mover un tipo no parece útil. Para que la gente realmente use ese nuevo tipo en su nuevo hogar, necesitamos la intervención humana. Cuando las personas se toman la molestia de cambiar manualmente su código, es mejor tener una intervención que _utilice el nuevo tipo_ en lugar de algo que pronto volverá a cambiar bajo sus pies en el futuro próximo. Si nos tomamos la molestia de agregar una función de idioma para ayudar con las refactorizaciones, lo ideal sería que las personas movieran gradualmente su código _a ese nuevo tipo, _ no simplemente a un nuevo hogar, por las razones anteriores.

Gracias por la explicación. Creo que ahora entiendo mejor su posición y estoy de acuerdo con sus suposiciones (es decir, que la gente usará material obsoleto pase lo que pase, por lo que brindar cualquier ayuda posible para guiarlos hacia el reemplazo es primordial). FWIW, mi plan ingenuo para lidiar con este problema (sin importar la solución de reparación gradual con la que vayamos) es una herramienta tipo go-fix para migrar automáticamente el código paquete por paquete en el período de desaprobación, pero admito libremente que Todavía no he probado cómo y si eso funciona en la práctica.

@niemeyer No creo que su sugerencia sea viable sin una interrupción grave del sistema de tipos Go.

Considere el dilema presentado por este código:

package old
import "new"
type A adapts new.A
func (a A) NewA() {}

package new
type A struct{}
func (a A) OldA() {}

package main
import (
    "new"
    "old"
    "reflect"
)
func main() {
    oldv := reflect.ValueOf(old.A{})
    newv := reflect.ValueOf(new.A{})
    if oldv.Type() == newv.Type() {
        // The two types are equal, therefore they must
        // have exactly the same method set, so either
        // oldv doesn't have the OldA method or newv doesn't
        // have the NewA method - both of which imply a contradiction
        // in the type system.
    } else {
         // The two types are not equal, which means that the
         // old adapted type is not fully compatible with the old
         // one. Any type that includes either new.A or new.B will
         // be incompatible as one of its components will likewise be
         // unequal, so any code that relies on dynamic type checking
         // will fail when presented with the type that's not using the
         // expected version.
    }
 }

Uno de los axiomas actuales del paquete reflect es que si dos tipos son iguales, sus valores reflect.Type son iguales. Esta es una de las bases de la eficiencia de la conversión de tipos en tiempo de ejecución de Go. Por lo que puedo ver, no hay forma de implementar la palabra clave "adapta" sin romper esto.

@rogpeppe Vea la conversación con @rsc sobre la reflexión arriba. Los dos tipos no son iguales, por lo que reflect solo diría la verdad y proporcionaría detalles sobre el adaptador cuando se le preguntara al respecto.

@niemeyer Si los dos tipos no son iguales, no creo que podamos admitir la reparación gradual de código mientras se mueve un tipo entre paquetes. Por ejemplo, digamos que queremos crear un nuevo paquete de imágenes que mantenga la compatibilidad de tipos.

Podríamos hacer:

package newimage
import "image"
type RGBA adapts image.RGB
func (r *RGBA) At(x, y) color.Color {
    return (*image.Buffer)(r).At(x, y)
}
etc for all the methods

Dado el objetivo de la reparación gradual del código, creo que es razonable esperar que
una imagen creada en el nuevo paquete es compatible con las funciones existentes
que utilizan el tipo de imagen antiguo.

Supongamos por el bien del argumento que el paquete image / png tiene
se ha convertido para usar newimage pero image / jpeg no.

Creo que deberíamos esperar que este código funcione:

img, err := png.Decode(r)
if err != nil { ... }
err = jpeg.Encode(w, img, nil)

pero, dado que hace una afirmación de tipo contra * image.RGBA no * newimage.RGBA,
fallará AFAICS, porque los tipos son diferentes.

Digamos que hicimos que la afirmación de tipo anterior tuviera éxito, ya sea que el tipo sea * image.RGBA
o no. Eso rompería el invariante actual que:

reflect.TypeOf (x) == reflect.TypeOf (x. (anyStaticType))

Es decir, usar una aserción de tipo estático no solo afirmaría el tipo estático de un
valor, pero a veces realmente lo cambiaría.

Digamos que decidimos que estaba bien, entonces presumiblemente también necesitaríamos
para permitir convertir un tipo adaptado a cualquier interfaz que cualquiera de sus compatibles
soporte de tipos adaptados, de lo contrario, el código nuevo o antiguo se detendría
trabajando al convertir a tipos de interfaz que son compatibles con el
tipo que están usando.

Esto conduce a otra situación contradictoria:

// oldInterface is some interface with methods that
// are only supported by the old type.
type oldInterface interface {
    OldMethod()
}
var x = interface{} = newpackage.Type{}
switch x.(type) {
case oldInterface:
    // This would fail because the newpackage.Type
    // does not implement OldMethod, even though we
    // we just supposedly checked that x implements OldMethod.
    reflect.TypeOf(x).Method("OldMethod")
}

En general, creo que tener dos tipos iguales pero diferentes
conduciría a un sistema de tipos muy difícil de explicar e incompatibilidades inesperadas
en código que usa tipos dinámicos.

Apoyo la propuesta de "tipo X = Y". Es simple de explicar y no
interrumpir demasiado el sistema de tipos.

@rogpeppe : Creo que la sugerencia de @niemeyer es convertir implícitamente un tipo adaptado a su tipo base, similar a las sugerencias anteriores de @josharian .

Para que eso funcione para la refactorización gradual, también tendría que convertir implícitamente funciones con argumentos de tipos adaptados; en esencia, requeriría agregar covarianza al lenguaje. Ciertamente, esa no es una tarea imposible, muchos lenguajes permiten la covarianza, particularmente para tipos con la misma estructura subyacente, pero agrega mucha complejidad al sistema de tipos, particularmente para los tipos de interfaz .

Eso conduce a algunos casos extremos interesantes, como ha notado, pero no son necesariamente "contradictorios" per se:

type oldInterface interface {
    OldMethod()
}
var x = interface{} = newpackage.Type{}
switch y := x.(type) {
case oldInterface:
    reflect.TypeOf(y).Method("OldMethod")  // ok
    reflect.TypeOf(x).Method("NewMethod")  // ok

    // This would fail because y has been implicitly converted to oldInterface.
    reflect.TypeOf(y).Method("NewMethod")

    // This would fail because accessing OldMethod on newpackage.Type requires
    // a conversion to oldInterface.
    reflect.TypeOf(x).Method("OldMethod")
}
// This would fail because accessing OldMethod on newpackage.Type requires
// a conversion to oldInterface.

Esto todavía me parece contradictorio. El modelo actual es muy simple: un valor de interfaz tiene un tipo estático subyacente bien definido. En el código anterior inferimos algo sobre ese tipo subyacente, pero cuando miramos el valor, no se parece a lo que hemos inferido. En mi opinión, este es un cambio serio (y difícil de explicar) en el idioma.

La discusión aquí parece estar llegando a su fin. Basado en una sugerencia de @egonelbre en https://github.com/golang/go/issues/16339#issuecomment -247536289, actualicé el comentario del problema original (en la parte superior) para incluir un resumen vinculado de la discusión. lejos. Publicaré un nuevo comentario, como este, cada vez que actualice el resumen.

En general, parece que el sentimiento aquí es para los alias de tipo en lugar de los alias generalizados. Posiblemente la idea del adaptador de Gustavo desplazará los alias de tipo, pero posiblemente no. Parece un poco complejo por el momento, aunque quizás al final de la discusión se llegue a una forma más sencilla. Sugiero que la discusión continúe un poco más.

Todavía no estoy convencido de que las vars globales mutables sean "generalmente un error" (y en los casos en los que son un error, el detector de carreras es la herramienta elegida para encontrar ese tipo de error). Solicitaría que, si ese argumento se usa para justificar la falta de una sintaxis extensible, se implemente una verificación de vet que, digamos, verifique las asignaciones a las variables globales en el código no accesible exclusivamente por init () o sus declaraciones. Ingenuamente, pensaría que esto no es particularmente difícil de implementar y no debería ser mucho trabajo ejecutarlo, digamos, todos los paquetes registrados en godoc.org para ver cuáles son los casos de uso de las variables globales mutables y si lo hacemos. considérelos todos errores.

(También me gustaría creer que, si go crece variables globales inmutables, deberían ser parte de las declaraciones const, porque eso es lo que son conceptualmente y porque serían compatibles con versiones anteriores, pero reconozco que esto probablemente conducirá a complicaciones sobre qué tipo de expresiones se pueden usar en tipos de matriz, por ejemplo, y necesitarían más pensamiento)

Re "¿Restricción? Los alias de los tipos de biblioteca estándar solo se pueden declarar en la biblioteca estándar". - en particular, eso evitaría el caso de uso directo de x/image/draw , un paquete existente que ha expresado interés en usar alias. También podría imaginarme muy bien, por ejemplo, paquetes de enrutadores o similares usando alias en net/http de manera similar ( saluda con la mano ).

También estoy de acuerdo con los argumentos en contra de todas las restricciones, es decir, estoy a favor de no tener ninguna de esas.

@Merovius , ¿qué pasa con las variables globales _exportadas_ mutables? Es cierto que un global no exportado podría estar bien, ya que todo el código del paquete sabe cómo manejarlo correctamente. Es menos obvio que los globales mutables exportados alguna vez tengan sentido. Nosotros mismos cometimos este error varias veces en la biblioteca estándar. Por ejemplo, no existe una forma completamente segura de actualizar runtime.MemProfileRate. Lo mejor que puede hacer es configurarlo al principio de su programa y esperar que ningún paquete que haya importado inicie una rutina de inicialización que pueda estar asignando memoria. Puede que tengas razón sobre var vs const, pero podemos dejar eso para otro día.

Buen punto sobre x / imagen / dibujo. Se agregará al resumen en la próxima actualización.

Me gustaría mucho armar un corpus representativo de código Go que pudiéramos analizar para responder preguntas como las que usted plantea. Empecé a intentar hacer esto hace unas semanas y encontré algunos problemas. Es un poco más trabajo de lo que parece que debería ser, pero es muy importante tener ese conjunto de datos, y espero que lleguemos allí.

@rsc, tu presentación de GothamGo sobre este tema se ha publicado en youtube https://www.youtube.com/watch?v=h6Cw9iCDVcU y sería una buena adición a la primera publicación.

En la sección "¿Qué otras cuestiones debe abordar una propuesta de alias de tipo?" sección sería útil especificar que la respuesta a "¿Se pueden definir métodos en tipos nombrados por alias?" es un no duro. Me doy cuenta de que va en contra del espíritu decretado de la sección, pero he notado que, en muchas conversaciones sobre alias, aquí y en otros lugares, hay personas que rechazan inmediatamente el concepto porque creen que los alias necesariamente permitirían esto y por lo tanto causarían problemas de los que resuelve. Está implícito en la definición, pero mencionarlo explícitamente provocaría un cortocircuito innecesario de ida y vuelta. Aunque tal vez eso pertenezca a una FAQ de alias en la nueva propuesta de alias, debería ser el resultado de este hilo.

@Merovius cualquier variable mutable global de paquete exportada se puede simular mediante funciones getter y setter a nivel de paquete.

Dada la versión n de un paquete p ,

package p
var Global = 0

en la versión n + 1 se pueden introducir getters y setters y la variable en desuso

package p
//Deprecated: use GetGlobal and SetGlobal.
var Global = 0
func GetGlobal() int {
    return Global
}
func SetGlobal(n int) {
   Global = n
}

y la versión n + 2 podría no exportarse Global

package p
var global = 0
func GetGlobal() int {
    return global
}
func SetGlobal(n int) {
   global = n
}

(Ejercicio para el lector: también podría ajustar el acceso a global en un mutex en n + 2 y desaprobar GetGlobal() a favor del más idiomático Global() .)

Esa no es una solución rápida, pero reduce el problema de modo que solo los alias de función (o su solución actual) son estrictamente necesarios para la reparación gradual del código.

@rsc Un uso trivial para los alias que dejó fuera de su resumen: abreviar nombres largos. (Probablemente la única motivación para Pascal, que inicialmente no tenía características de programación en grande como paquetes). Aunque es trivial, es el único caso de uso donde los alias no exportados tienen sentido, por lo que tal vez valga la pena mencionarlos por esa razón.

@jimmyfrasche Tienes razón. No me gusta la idea de usar getters y setters (al igual que no me gusta tenerlos para campos de estructura) pero su análisis es, por supuesto, correcto.

Hay que destacar los usos de los alias que no son de reparación (por ejemplo, hacer paquetes de reemplazo directos), pero reconozco que debilita el caso de los alias de var.

@Merovius estuvo de acuerdo en todos los puntos. No estoy contento con eso tampoco, pero tengo que seguir la lógica v☹v

@niemeyer, ¿ puede aclarar cómo los adaptadores ayudarían a migrar tipos en los que tanto el antiguo como el nuevo tienen un método con el mismo nombre pero con firmas diferentes? Agregar un argumento a un método o cambiar el tipo de un argumento parece que serían evoluciones comunes de una base de código.

@rogpeppe Tenga en cuenta que así es exactamente como sucede hoy:

type two one

Esto hace que one y two tipos independientes, y ya sea que reflejen o estén debajo de un interface{} , eso es lo que ves. También puede convertir entre one y two . La propuesta de adaptador anterior solo hace que ese último paso sea automático para los adaptadores. Puede que no le guste la propuesta por múltiples razones, pero no hay nada contradictorio en eso.

@iand Como en el caso de type two one , los dos tipos tienen conjuntos de métodos completamente independientes, por lo que no hay nada especial en la coincidencia de nombres. Antes de que se migren las bases de código antiguas, seguirían utilizando la firma antigua con el tipo anterior (ahora un adaptador). El nuevo código que usa el nuevo tipo usaría la nueva firma. Pasar un valor del nuevo tipo al código antiguo lo adapta automáticamente porque el compilador sabe que el último es un adaptador del primero y, por lo tanto, utiliza el conjunto de métodos correspondiente.

@niemeyer Parece que hay mucha complejidad escondida detrás de estos adaptadores que no está completamente especificada. En este punto, creo que la simplicidad de los alias de tipo pesa mucho a su favor. Me senté a enumerar todas las cosas que necesitarán actualizarse solo para los alias de tipo, y es una lista muy larga. La lista ciertamente sería más larga para los adaptadores, y todavía no entiendo completamente todos los detalles. Me gustaría sugerir que escribamos alias por ahora y dejemos una decisión sobre los adaptadores relativamente más pesados ​​para más adelante, si desea elaborar una propuesta completa (pero nuevamente soy escéptico de que no hay dragones acechando allí) .

@jimmyfrasche Con respecto a los métodos en los alias, ciertamente los alias no permiten eludir las restricciones habituales de definición de métodos: si un paquete define el tipo T1 = otherpkg.T2, no puede definir métodos en T1, al igual que no puede definir métodos directamente en otherpkg.T2. Sin embargo, si un paquete define el tipo T1 = T2 (ambos en el mismo paquete), la respuesta es menos clara. Podríamos introducir una restricción, pero (todavía) no existe una necesidad obvia para ello.

Se actualizó el resumen de la discusión de nivel superior . Cambios:

  • Enlace agregado al video de GothamGo
  • Se agregó "abreviar nombres largos" como posible uso, por @jba.
  • Se agregó x / image / draw como argumento contra la restricción de biblioteca estándar, según @Merovius.
  • Se agregó más texto sobre métodos en alias, por @jimmyfrasche.

Documento de diseño agregado:

Como fue el caso hace una semana, todavía parece haber un consenso general para los alias de tipos. Robert y yo redactamos un documento de diseño formal, que acabo de registrar (enlace de arriba).

Después del proceso de propuesta , publique comentarios sustantivos sobre la propuesta _aquí_ sobre este tema. Ortografía / gramática / etc puede ir a la página de revisión de código de Gerrit https://go-review.googlesource.com/#/c/34592/. Gracias.

Me gustaría que se reconsiderara el "Efecto en la incrustación". Limita la usabilidad de los alias de tipo para la reparación gradual del código. Es decir, si p1 quiere cambiar el nombre de un tipo type T1 = T2 y el paquete p2 incrusta p1.T2 en una estructura, nunca podrán actualizar esa definición a p1.T1 , porque un importador p3 puede referirse a la estructura incrustada por su nombre. p2 entonces no puede cambiar a p1.T1 sin romper p3 ; p3 no puede actualizar el nombre a p1.T1 , sin romper con el actual p2 .

Una forma de salir de esto sería, a) en general limitar cualquier promesa de compatibilidad / período de desaprobación al código que no se refiera a campos incrustados por su nombre, ob) agregar una etapa de desaprobación separada, por lo que p1 agrega type T1 = T2 y deprecia T2 , luego p2 deprecia refiriéndose a (digamos) s2.T2 por su nombre, todos los importadores de p2 serán reparados para no hacer eso, entonces p2 hace el cambio.

Ahora, en teoría, el problema puede repetirse indefinidamente; p4 podría importar p3 , que a su vez incorpora el tipo de p2 ; Me parece que p3 también necesita tener un período de desaprobación, para referirse al campo dos veces incrustado por su nombre. En ese caso, el período de desaprobación más interno se vuelve infinitesimal o el más externo se vuelve infinito. Pero incluso sin considerar el problema como recursivo, me parecería que b) sería bastante difícil de cronometrar (el período de depreciación de p2 debería estar completamente contenido en el período de depreciación de p1 . Por lo tanto, si T es un "período de desactivación estándar", tendría que elegir al menos 2T al cambiar el nombre de los tipos, para que las versiones se alineen).

a) también me parece poco práctico; por ejemplo, si un tipo incrusta un *byte.Buffer y quiero establecer ese campo (o pasar ese búfer a alguna otra función), simplemente no hay forma de hacerlo, sin referirse a él por su nombre (excepto mediante el uso de inicializadores de estructura sin nombres, lo que también pierde las garantías de compatibilidad :)).

Entiendo el atractivo de ser compatible con byte y rune como alias. Pero, personalmente, lo colocaría en segundo plano para preservar la utilidad de los alias de tipo para reparaciones graduales. Un ejemplo (probablemente malo) de una idea para obtener ambos sería, para, los nombres exportados permiten usar cualquier alias para referirse a un campo incrustado y para nombres no exportados (inherentemente restringidos al mismo paquete, por lo tanto, bajo más control del autor ) mantener la semántica propuesta actualmente? Sí, a mí tampoco me gusta esta distinción. Quizás alguien tenga una mejor idea.

@rsc re métodos en un alias

Si tiene un tipo S que es un alias para el tipo T, ambos definidos en el mismo paquete, y permite definir métodos en S, ¿qué pasa si T es un alias para pF definido en un paquete diferente? Si bien eso obviamente también debería fallar, hay sutilezas en la aplicación, implementación y legibilidad de la fuente a considerar (si T está en un archivo diferente de S, no está claro de inmediato si puede definir un método en T mirando el definición de T).

La regla, si tiene type T = S , entonces no puede declarar métodos en T - es absoluta y está claro a partir de esa única línea en la fuente que se aplica, sin tener que investigar la fuente de S, como lo haría en la situación de alias de alias.

Además, permitir métodos en un alias de tipo local enturbia la distinción entre un alias de tipo y una definición de tipo. Dado que los métodos se definirían tanto en S como en T de todos modos, la restricción de que solo se pueden escribir en uno no restringe lo que se puede expresar. Simplemente mantiene las cosas más simples y uniformes.

@jimmyfrasche Si estamos escribiendo type T1 = T2 y T2 está en el mismo paquete, entonces probablemente estemos desaprobando el nombre T2. En ese caso, queremos la menor cantidad posible de apariciones de T2 en el godoc. Así que nos gustaría declarar todos los métodos como func (T1) M() .

@jba un cambio de godoc para informar que los métodos de un alias se declaran en ese alias cumpliría ese requisito sin cambiar la legibilidad de la fuente. En general, sería bueno que godoc mostrara el conjunto de métodos completo de un tipo cuando se trata de aliasing y / o incrustación, especialmente cuando el tipo proviene de otro paquete. El problema debe resolverse con herramientas más inteligentes, no más semántica del lenguaje.

@jba En ese caso, ¿por qué no invertir la dirección del alias? type T2 = T1 ya le permite definir métodos en T1 con la misma estructura de paquete; la única diferencia es el nombre del tipo informado por el paquete reflect , y puede iniciar la migración arreglando los sitios de llamadas sensibles al nombre para que no sean sensibles al nombre antes de agregar el alias.

@jimmyfrasche Del documento de propuesta :

"Dado que T1 es solo otra forma de escribir T2, no tiene su propio conjunto de declaraciones de métodos. En cambio, el conjunto de métodos de T1 es el mismo que el de T2. Al menos para la prueba inicial, no hay restricciones contra las declaraciones de métodos que usan T1 como un tipo de receptor, siempre que utilice T2 en la misma declaración, sería válido "

El uso de pF como tipo de receptor de método nunca es válido.

@mdempsky No estaba muy claro, pero dije que no era válido.

Mi punto es que es menos obvio si es válido o no con solo mirar esa línea de código específica.

Dado type S = T , también debe mirar T para asegurarse de que no sea también un alias que alias a un tipo en otro paquete. La única ganancia es la complejidad.

Siempre rechazar métodos en un alias es más simple y más fácil de leer y no pierde nada. No creo que surja un caso confuso con mucha frecuencia, pero no es necesario introducir la posibilidad cuando no se obtiene nada que no pueda manejarse mejor en otro lugar o con un enfoque diferente pero equivalente.

@Merovio

Si p1 quiere cambiar el nombre de un tipo de tipo T1 = T2 y el paquete p2 incrusta p1.T2 en una estructura, nunca podrán actualizar esa definición a p1.T1, porque un importador p3 podría referirse a la estructura incrustada por su nombre.

Hoy en día, es posible solucionar este problema en muchos casos cambiando el campo anónimo a un campo con nombre y reenviando explícitamente los métodos. Sin embargo, eso no funcionaría para métodos no exportados.

Otra opción podría ser agregar una segunda característica para compensar. Si pudiera adoptar el conjunto de métodos de un campo sin convertirlo en anónimo (o con un cambio de nombre explícito), eso permitiría que el nombre del campo permaneciera sin cambios incluso cuando se cambia el tipo subyacente.

Considerando la declaración de su ejemplo:

package p2

type S struct {
  p1.T2
}

Una característica de compensación podría ser "alias de campo", que seguiría una sintaxis similar a la de escribir alias:

package p2

type S struct {
  p1.T1
  T2 = T1  // field T2 is an alias for field T1.
}

var s S  // &s.T2 == &s.T1

Otra característica de compensación podría ser la "delegación", que adoptaría explícitamente el conjunto de métodos de un campo anónimo:

package p2

type S struct {
  T2 p1.T1 delegated  // T2 is a field of type T1.
  // The method set of S includes the method set of T1 and forwards those calls to field T2.
}

Creo que prefiero los alias de campo, porque también permitirían otro tipo de reparación gradual: cambiar el nombre de los campos de una estructura sin introducir errores de consistencia o alias de puntero.

@Merovius El problema principal es cuando se cambia el nombre del tipo por un alias.

No he considerado esto en su totalidad, apenas de pasada, solo un pensamiento al azar:

¿Qué pasa si introduce un alias en su paquete que le devuelve el nombre y lo incrusta?

No sé si eso soluciona algo, pero ¿tal vez da algo de tiempo para romper el ciclo?

@bcmills No pensé en esa solución, gracias. Creo que la advertencia sobre los métodos no declarados me parecería (a mí) que surgiría tan raramente en la práctica que no influiría en mi opinión en general (a menos que no la entienda completamente. Siéntase libre de aclarar, si cree que es útil ). No creo que acumular más cambios esté justificado (o sea una buena idea).

@Merovius Cuanto más lo pienso, más me gusta la idea de los alias de campo.

Reenviar los métodos explícitamente es tedioso incluso si se exportan y rompe otros tipos de refactorización (por ejemplo, agregar métodos al tipo incrustado y esperar que el tipo que lo incrusta continúe satisfaciendo la misma interfaz). Y el cambio de nombre de los campos de estructura también se incluye en el marco general de permitir la reparación gradual del código.

@Merovio

Si p1 quiere cambiar el nombre de un tipo de tipo T1 = T2 y el paquete p2 incrusta p1.T2 en una estructura, nunca podrán actualizar esa definición a p1.T1, porque un importador p3 podría referirse a la estructura incrustada por su nombre. p2 entonces no puede cambiar a p1.T1 sin romper p3; p3 no puede actualizar el nombre a p1.T1, sin romper con el p2 actual.

Si entiendo tu ejemplo, tenemos:

package p1

type T2 struct {}
type T1 = T2
package p2

import "p1"

type S struct {
  p1.T2
  F2 string // see below
}

Creo que este es solo un ejemplo específico del caso general en el que deseamos cambiar el nombre de un campo de estructura; el mismo problema se aplica si queremos cambiar el nombre de S.F2 a S.F1.

En este caso específico, podemos actualizar el paquete p2 para usar la nueva API de p1 con un alias de tipo local:

package p2

import "p1"

type T2 = p1.T1

type S struct {
  T2
}

Por supuesto, esta no es una buena solución a largo plazo. Sin embargo, no creo que haya ninguna forma de evitar el hecho de que p2 deba cambiar su API exportada para eliminar el nombre T2, lo que procederá de la misma manera que cualquier cambio de nombre de campo.

Solo una nota sobre "mover tipos entre paquetes". ¿No es esa formulación un poco problemática?

Por lo que tengo entendido, la propuesta permite "hacer referencia" a una definición de objeto que se encuentra en otro paquete a través de un nuevo nombre.

No mueve la definición del objeto, ¿verdad? (a menos que uno escriba código usando alias en primer lugar, en cuyo caso, el usuario es libre de cambiar el lugar al que se refiere el alias, como en el paquete de dibujo).

@atdiar Hacer referencia a un tipo en un paquete diferente puede usarse como un paso para mover el tipo. Sí, un alias no mueve el tipo, pero se puede usar como herramienta para hacerlo.

@Merovius Hacer eso probablemente romperá la reflexión y los complementos.

@atdiar Lo siento, pero no entiendo lo que estás tratando de decir. ¿Ha leído el comentario original de este hilo, el artículo sobre reparaciones graduales vinculado allí y la discusión hasta ahora? Si está tratando de agregar un argumento no considerado hasta ahora a la discusión, creo que debe ser más claro.

Finalmente, una propuesta útil y bien redactada. Necesitamos un alias de tipo, tengo grandes problemas para crear una única API sin un alias de tipo, hasta ahora, tengo que escribir mi código de una manera que no me gusta tanto para lograr eso. Esto debería incluirse en go v1.8, pero nunca es demasiado tarde, así que adelante con la versión 1.9.

@Merovio
Estoy hablando explícitamente de "tipos de movimiento" entre paquetes. Cambia la definición del objeto. Por ejemplo, en pkg reflect, cierta información está vinculada al paquete en el que se definió un objeto.
Si mueve la definición, puede romperse.

@kataras no se trata realmente de buenos documentos y comentarios, es simplemente que las definiciones de tipo no deben moverse. Por mucho que aprecio la propuesta de alias, desconfío de que la gente piense que pueden hacer eso.

@atdiar nuevamente, lea el artículo del comentario original y la discusión hasta ahora. Los tipos de mudanzas y cómo abordar sus inquietudes son la principal preocupación de este hilo. Si cree que el artículo de Russ no aborda adecuadamente sus inquietudes, especifique por qué su explicación no es satisfactoria. :)

@kataras Si bien yo, personalmente, estoy de acuerdo, no creo que sea particularmente útil, simplemente afirmar lo importante que encontramos esta característica. Es necesario que exista un argumento constructivo para abordar las preocupaciones de la gente. :)

@Merovius He leído el documento. No responde a mi pregunta. Creo que he sido lo suficientemente explícito. Está relacionado con el mismo problema que nos disuadió de implementar la propuesta de alias anterior.

@atdiar Yo, al menos, no entiendo. Estás diciendo que mover un tipo rompería cosas; la propuesta trata sobre cómo evitar tales roturas con una reparación gradual, mediante el uso de un alias, luego actualice cada dependencia inversa hasta que ningún código use el tipo anterior y luego elimine el tipo anterior. No veo cómo su afirmación de que "la reflexión y los complementos" se rompen se sostiene bajo estas suposiciones. Si desea cuestionar las suposiciones, eso ya se ha discutido.

Tampoco veo cómo ninguno de los problemas que impiden que los alias ingresen 1.8 se conecte con lo que dijo. Los temas respectivos, a mi leal saber y entender, son # 17746 y # 17784. Si se refiere al problema de la incrustación (que podría interpretarse como relacionado con roturas o reflejos, aunque no estoy de acuerdo), entonces eso se aborda en la propuesta formal (aunque, vea arriba, creo que la solución propuesta merece más discusión) y debe ser específico sobre por qué no cree que lo sea.

Entonces, lo siento, pero no, no fuiste lo suficientemente específico. ¿Tiene un número de problema para "el mismo problema que nos disuadió de implementar la propuesta de alias anterior" al que se refiere, que se relaciona con lo que mencionó hasta ahora, para ayudar a comprender? ¿Puede dar un ejemplo específico de las roturas de las que está hablando (consulte los ejemplos de este hilo ascendente; proporcione una secuencia de paquetes, definiciones de tipos y algo de código y describa cómo se rompe cuando se transforma como se propone)? Si desea que se aborden sus inquietudes, primero debe ayudar a los demás a comprenderlas.

@Merovius Entonces, en el caso de dependencias transitivas donde una de estas dependencias está mirando reflect.Type.PkgPath (), ¿qué sucede?
Ese es el mismo problema que ocurre en el tema de la incrustación.

@atdiar Lo siento, no veo cómo esto es de alguna manera una preocupación comprensible, a la luz de la discusión en este hilo hasta ahora y de qué se trata esta propuesta. Saldré de este subproceso particular ahora y daré a otros, que podrían entender mejor su objeción, la oportunidad de abordarla.

Déjame reformularlo de forma concisa:

El problema es sobre la igualdad de tipos dado el hecho de que la definición de tipos codifica su propia ubicación.
Dado que la igualdad de tipos se puede probar y se prueba en tiempo de ejecución, no veo cómo mover tipos es tan fácil de hacer.

Simplemente estoy haciendo una advertencia de que este caso de uso de "tipos en movimiento" puede potencialmente romper muchos paquetes en estado salvaje, a distancia. Preocupación similar con los complementos.

(de la misma manera que cambiar el tipo de puntero en un paquete rompería muchos otros paquetes, si ese paralelo puede aclarar las cosas).

@atdiar Una

@niemeyer

Esto crea uno y dos tipos independientes, y ya sea que reflejen o estén debajo de una interfaz {}, eso es lo que
verás. También puede convertir entre uno y dos. La propuesta de adaptador anterior solo hace que eso dure
paso automático para adaptadores. Puede que no le guste la propuesta por varias razones, pero no hay nada
contradictorio sobre eso.

No puedes convertir entre

 func() one

y

func() two

@Merovius No es posible que considere cambiar todos los importadores de un paquete con código reparado que existen en la naturaleza. Y no estoy muy interesado en comenzar a profundizar en el control de versiones de paquetes aquí.

Para ser claros, no estoy en contra de la propuesta de alias, sino de la formulación de "tipos de movimiento entre paquetes" que implica un caso de uso que aún no es seguro.

@jimmyfrasche re predecibilidad de la validez del método en alias:

Ya se da el caso de que func (t T) M() veces es válido, a veces no es válido. No surge mucho porque la gente no empuja estos límites muy a menudo. Es decir, funciona bien en la práctica. https://play.golang.org/p/bci2qnldej. En cualquier caso, esto está en la lista de _posibles_ restricciones. Como todas las restricciones posibles, agrega complejidad y queremos ver evidencia concreta del mundo real antes de agregar esa complejidad.

@Merovius , volviendo a incrustar nombres:

Estoy de acuerdo en que la situación no es perfecta. Sin embargo, si tengo una base de código llena de referencias a io.ByteBuffer y quiero moverla a bytes.Buffer, entonces quiero poder introducir

package io
type ByteBuffer = bytes.Buffer

_sin_ actualizar ninguna de las referencias existentes a io.ByteBuffer. Si todos los lugares donde io.ByteBuffer están incrustados cambian automáticamente el nombre del campo a Buffer como resultado de reemplazar una definición de tipo con un alias, entonces he roto el mundo y no hay una reparación gradual. Por el contrario, si el nombre de un io.ByteBuffer incrustado sigue siendo ByteBuffer, los usos se pueden actualizar uno a la vez en sus propias reparaciones graduales (posiblemente teniendo que realizar varios pasos; de nuevo, no es lo ideal).

Discutimos esto con cierto detalle en el n. ° 17746. Originalmente estaba del lado del nombre de un alias io.ByteBuffer incrustado que era Buffer, pero el argumento anterior me convenció de que estaba equivocado. @jimmyfrasche en particular hizo algunos buenos argumentos sobre que el código no cambia dependiendo de la definición de la cosa incrustada. No creo que sea viable rechazar por completo los alias incrustados.

Tenga en cuenta que hay una solución en p2 en su ejemplo. Si p2 realmente quiere un campo incrustado llamado ByteBuffer sin hacer referencia a io.ByteBuffer, puede definir:

type ByteBuffer = bytes.Buffer

y luego incrustar un ByteBuffer (es decir, un p2.ByteBuffer) en lugar de un io.ByteBuffer. Eso tampoco es perfecto, pero significa que las reparaciones pueden continuar.

Definitivamente, esto no es perfecto y que los cambios de nombre de los campos en general no se tratan en esta propuesta. Podría ser que la incrustación no debería ser sensible al nombre subyacente, que debería haber algún tipo de sintaxis para 'incrustar X como nombre N'. También podría ser que debamos agregar alias de campo más adelante. Ambas parecen ideas razonables a priori y probablemente ambas deberían ser propuestas posteriores por separado, evaluadas sobre la base de la evidencia real de una necesidad. Si los alias de tipo nos ayudan a llegar al punto en que la falta de alias de campo es el próximo gran obstáculo para las refactorizaciones a gran escala, ¡eso será un progreso!

(/ cc @neild y @bcmills)

@atdiar , sí, es cierto que

@rsc Lo que tenía en mente era a) prohibir incrustar tanto un alias como su tipo de definición en la misma estructura (para evitar la ambigüedad de b), b) permitir hacer referencia a un campo por cualquier nombre en el código fuente, c) elegir uno o el otro en el tipo generado información / reflexión y similares (no importa cuál).

Afirmaría agitando la mano que esto ayuda a evitar el tipo de roturas que traté de describir, al tiempo que hago una elección clara para el caso en que se requiere una elección; y, personalmente, me importa menos no romper el código que se basa en la reflexión, que el código que no lo hace.

No estoy seguro en este momento de entender su argumento ByteBuffer, pero también estoy al final de un largo día de trabajo, por lo que no es necesario que explique más, si lo encuentro poco convincente, responderé eventualmente :)

@Merovius Creo que tiene sentido probar las reglas simples y ver hasta dónde llegamos antes de introducir reglas más complejas. Podemos agregar (a) y (b) más adelante si es necesario; (c) es un hecho sin importar qué.

Estoy de acuerdo en que tal vez (b) sea una buena idea en ciertas circunstancias, pero tal vez no en otras. Si está utilizando alias de tipo para el caso de uso de "estructurar una API de un solo paquete en múltiples paquetes de implementación" mencionado anteriormente, entonces tal vez no desee incrustar el alias para exponer el otro nombre (que puede estar en un paquete interno y de lo contrario inaccesible para la mayoría de los usuarios). Espero que podamos acumular más experiencia.

@rsc

Quizás podría ayudar agregar información a nivel de paquete sobre la aliabilidad a los archivos de objeto.
(Teniendo en cuenta si los complementos de go deben seguir funcionando correctamente o no).

@Merovio @rsc

a) prohibir incrustar tanto un alias como su tipo definitorio en la misma estructura

Tenga en cuenta que en muchos casos esto ya está prohibido como consecuencia de la forma en que la incrustación interactúa con los conjuntos de métodos. (Si el tipo incrustado tiene un conjunto de métodos no vacío y se llama a uno de esos métodos, el programa no podrá compilar: https://play.golang.org/p/XkaB2a0_RK).

Entonces, agregar una regla explícita que prohíba la doble incrustación parece que solo haría una diferencia en un pequeño subconjunto de casos; no me parece que valga la pena la complejidad.

¿Por qué no abordar los alias de tipo como tipos algebraicos en su lugar y admitir los alias a un conjunto de tipos para que también obtengamos una interfaz vacía equivalente con verificación de tipo en tiempo de compilación como un bono, a la

type Stringeroonie = {string,fmt.Stringer}

@ j7b

¿Por qué no abordar los alias de tipo como tipos algebraicos y admitir los alias en un conjunto de tipos?

Los alias son semántica y estructuralmente equivalentes al tipo original. Los tipos de datos algebraicos no lo son: en el caso general, requieren almacenamiento adicional para las etiquetas de tipo. (Los tipos de interfaz Go ya llevan ese tipo de información, pero las estructuras y otros tipos que no son de interfaz no).

@bcmills

Esto podría ser un razonamiento erróneo, pero pensé que el problema podría abordarse como alias A de tipo T es equivalente a declarar A como interfaz {} y dejar que el compilador convierta de forma transparente variables de tipo A a T en ámbitos donde se declaran variables de tipo A , que pensé que sería principalmente un costo de tiempo de compilación lineal, inequívoco, y crearía una base para pseudotipos administrados por el compilador, incluidos los algebraicos usando la sintaxis type T = , y posiblemente también permitiría implementar tipos como referencias inmutables en tiempo de compilación que, como en lo que respecta al código de usuario, sería simplemente interfaz {} s "bajo el capó".

Las deficiencias en ese hilo de pensamiento probablemente serían producto de la ignorancia, y como no estoy en condiciones de ofrecer una prueba práctica de concepto, estoy feliz de aceptar que es deficiente y diferido.

@ j7b Incluso si ADT fuera una solución a un problema de reparación gradual, entonces crean el suyo propio; es imposible agregar o eliminar miembros de un ADT sin romper las dependencias. Entonces, en esencia, crearías más problemas de los que resolverías.

Su idea de traducir de forma transparente hacia y desde la interfaz {} tampoco funciona para tipos de orden superior como []interface{} . Y, finalmente, terminará perdiendo una de las fortalezas de go, que es dar a los usuarios el control sobre el diseño de los datos y, en su lugar, hacer la cosa de Java de envolver todo.

ADT no son la solución aquí.

@Merovius Estoy bastante seguro de que si una construcción de tipo algebraico incluye un cambio de nombre (que sería consistente con una definición razonable de la misma) es una solución, esa interfaz {} puede servir como un proxy para la forma de proyección y selección administrada por el compilador descrito, y no estoy seguro de cómo el diseño de datos es relevante ni cómo está definiendo tipos de "orden superior", un tipo es solo un tipo si se puede declarar y [] interfaz {} es solo un tipo.

Aparte de todo eso, estoy seguro de que type T = tiene el potencial de sobrecargarse de formas intuitivas y útiles más allá del cambio de nombre, los tipos algebraicos y las referencias públicamente inmutables parecen las aplicaciones más obvias, así que espero que la especificación termine indicando esa sintaxis indica un meta o pseudo tipo administrado por el compilador y se tienen en cuenta todas las formas en que un tipo administrado por el compilador podría ser útil y la sintaxis que mejor expresa esos usos. Dado que una nueva sintaxis no necesita preocuparse por el conjunto de palabras reservadas globalmente cuando se usa como calificadores, algo como type A = alias Type sería claro y extensible.

@ j7b

Aparte de todo eso, estoy seguro de que el tipo T = tiene el potencial de sobrecargarse de formas intuitivas y útiles más allá del cambio de nombre,

Seguramente espero que no. Go es (en su mayoría) muy bien ortogonal hoy en día, y mantener esa ortogonalidad es algo bueno.

La forma en que hoy se declara un nuevo tipo T en Go es type T def , donde def es la definición del nuevo tipo. Si uno fuera a implementar tipos de datos algebraicos (también conocidos como uniones etiquetadas), esperaría que siguieran esa sintaxis en lugar de la sintaxis para los alias de tipos.

Me gusta incluir un punto de vista diferente (en apoyo) de los alias de tipo, lo que puede proporcionar información sobre casos de uso alternativos además de la refactorización:

Retrocedamos un momento y supongamos que no teníamos declaraciones de tipo Go antiguas y regulares de la forma type T <a type> , sino solo declaraciones de alias de tipo type A = <a type> .

(Para completar la imagen, supongamos también que los métodos se declaran de alguna manera de manera diferente, no a través de la asociación al tipo nombrado utilizado como receptor, porque no podemos. Por ejemplo, uno podría imaginar la noción de un tipo de clase con los métodos literalmente dentro y, por lo tanto, no necesitamos depender de un tipo con nombre para declarar métodos. Dos de esos tipos que son estructuralmente idénticos pero tienen métodos diferentes serían tipos diferentes. Los detalles no son importantes aquí para este experimento mental).

Afirmo que en un mundo así podríamos escribir prácticamente el mismo código que escribimos ahora: usamos los nombres de tipo (alias) para no tener que repetirnos, y los tipos mismos se aseguran de que usemos datos en un tipo -camino seguro.

En otras palabras, si Go hubiera sido diseñado de esa manera, probablemente también hubiéramos estado bien, en general.

Más aún, en un mundo así, debido a que los tipos son idénticos si son estructuralmente idénticos (sin importar el nombre), los problemas que tenemos con la refactorización ahora no habrían aparecido en primer lugar, y no habría necesidad de ningún cambios en el idioma.

Pero no tendríamos un mecanismo de seguridad que tenemos en Go actual: no podríamos introducir un nombre para un tipo y afirmar que ahora debería ser un tipo nuevo y diferente. (Aún así, es importante tener en cuenta que, en esencia, es un mecanismo de seguridad).

En otros lenguajes de programación, la noción de crear un tipo nuevo y diferente de un tipo existente se denomina "marca": un tipo es una marca adjunta que lo hace diferente de todos los demás tipos. Por ejemplo, en Modula-3, había una palabra clave especial BRANDED para que eso suceda (por ejemplo, TYPE T = BRANDED REF T0 crearía una referencia nueva y diferente a T0). En Haskell, la palabra new antes de un tipo tiene un efecto similar.

Volviendo a nuestro mundo alternativo de Go, podríamos encontrarnos en la posición en la que no tenemos problemas con la refactorización, pero en la que queríamos mejorar la seguridad de nuestro código para que type MyBuffer = []byte y type YourBuffer = []byte denoten diferentes tipos para que no usemos accidentalmente el incorrecto. Podríamos proponer introducir una forma de marca tipográfica exactamente para ese propósito. Por ejemplo, podríamos querer escribir type MyBuffer = new []byte , o incluso type MyBuffer = new YourBuffer con el efecto de que MyBuffer ahora es un tipo diferente de YourBuffer.

Este es, en esencia, el problema dual de lo que tenemos ahora. Sucede que en Go, desde el primer día, siempre trabajamos con tipos "de marca" tan pronto como obtuvieron un nombre. En otras palabras, type T <a type> es efectivamente type T = new <a type> .

Para resumir: en Go existente, los tipos con nombre son siempre tipos "de marca", y carecemos de la noción de solo un nombre para un tipo (que ahora llamamos alias de tipo). En varios otros lenguajes, los alias de tipos son la norma, y ​​uno tiene que usar un mecanismo de "marca" para crear un tipo explícitamente nuevo y diferente.

El punto es que ambos mecanismos son inherentemente útiles, y con los alias de tipo finalmente conseguimos admitirlos a ambos.

@griesemer La extensión de esa característica es la propuesta de alias inicial que idealmente debería limpiar la refactorización. Me temo que solo los alias de tipo crearían casos de borde de refactorización difíciles debido a su alcance restringido.

En ambas propuestas, me pregunto si la colaboración del vinculador no debería ser necesaria porque el nombre es parte de la definición de tipo en Go, como ha explicado.

No estoy familiarizado en absoluto con el código objeto, por lo que es solo una idea, pero parece que es posible agregar secciones personalizadas a los archivos objeto. Si por casualidad, fuera posible mantener una especie de lista enlazada desenrollada, rellenada en el momento del enlace con los nombres de los tipos y sus alias, tal vez eso podría ayudar. El tiempo de ejecución tendría toda la información que necesita sin sacrificar la compilación por separado.

La idea es que el tiempo de ejecución debería poder devolver dinámicamente los diferentes alias para un tipo dado para que los mensajes de error permanezcan claros (ya que el aliasing introduce una discrepancia de nombres entre el código en ejecución y el código escrito).

Una alternativa al uso de alias de rastreo sería tener una historia de versiones concreta en general, para poder "mover" las definiciones de objetos entre paquetes como se hizo para el paquete de contexto. Pero ese es un tema completamente diferente.

Al final, sigue siendo una buena idea dejar la equivalencia estructural a las interfaces y la equivalencia de nombres a los tipos.
Dado el hecho de que un tipo puede considerarse una interfaz con más restricciones, parece que la declaración de un alias debería / podría implementarse manteniendo un segmento por paquete de segmentos de cadenas de nombre de tipo.

@atdiar No estoy seguro de que se

@ j7d , a nivel de sistema de tipos, los tipos de suma o cualquier tipo de subtipo (como sugirieron otros anteriormente en la discusión) solo ayudan con ciertos tipos de usos. Es cierto que podemos pensar en bytes.Buffer como un subtipo de io.Reader ("un búfer es un lector", o en su ejemplo "una cadena es un Stringeroonie"). Los problemas surgen al construir tipos más complejos utilizando esos. El resto de este comentario habla de los tipos Go, pero habla de sus relaciones fundamentales en un nivel de subtipificación, no de lo que Go implementa en realidad el lenguaje. Sin embargo, Go debe implementar reglas consistentes con las relaciones fundamentales.

Un constructor de tipos (una forma elegante de decir "una forma de usar un tipo") es covariante si conserva la relación de subtipo, contravariante si invierte la relación.

El uso de un tipo en el resultado de una función es covariante. Un búfer func () "es un lector" func (), porque devolver un búfer significa que ha devuelto un lector. Usar un tipo en un argumento de función es _no_ covariante. Un func (Buffer) no es un func (Reader), porque el func necesita un Buffer y algunos lectores no son Buffers.

Usar un tipo en un argumento de función es contravariante. Un func (Reader) es un func (Buffer), porque el func solo necesita un Reader, y un Buffer es un Reader. Usar un tipo en el resultado de una función es _no_ contravariante. Un lector func () no es un búfer func (), porque la función devuelve un lector y algunos lectores no son búfer.

Combinando los dos, un Func (Reader) Reader no es un Func (Buffer) Buffer, ni viceversa, porque o los argumentos no funcionan o los resultados no funcionan. (La única combinación a lo largo de estas líneas que funciona sería que un búfer de función (lector) es un lector de función (búfer)).

En general, si func (X1) X2 es un (subtipo de) func (X3) X4, entonces debe ser que X3 es un (subtipo de) X1 y, de manera similar, X2 es un (subtipo de) X4. En el caso del uso de alias donde queremos que T1 y T2 sean intercambiables, una función (T1) T1 es un subtipo de func (T2) T2 solo si T1 es un subtipo de T2 _y_ T2 es un subtipo de T1. Eso básicamente significa que T1 es el mismo tipo que T2, no un tipo más general.

Usé argumentos de función y resultados porque ese es el ejemplo canónico (y uno bueno), pero lo mismo sucede con otras formas de generar resultados complejos. En general, obtienes covarianza para las salidas (como func () T, o <-chan T, o map [...] T) y contravarianza para las entradas (como func (T), o chan <- T, o map [T ] ...) y la igualdad de tipo forzada para entrada + salida (como func (T) T, o chan T, o * T, o [10] T, o [] T, o struct {Field T}, o una variable de tipo T). De hecho, el caso más común en Go, como puede ver en los ejemplos, es entrada + salida.

Concretamente, un búfer [] no es un lector [] (porque puede almacenar un archivo en un lector [] pero no en un búfer []), ni un lector [] es un búfer [] (porque se obtiene de un búfer [] El lector puede devolver un archivo, mientras que la recuperación de un búfer [] debe devolver un búfer).

Una conclusión de todo esto es que, si desea resolver el problema general de reparación del código para que el código pueda usar T1 o T2, no puede hacerlo con ningún esquema que haga que T1 sea solo un subtipo de T2 (o viceversa). Cada uno debe ser un subtipo del otro, es decir, deben ser del mismo tipo, o de lo contrario algunos de estos usos enumerados no serán válidos.

Es decir, el subtipo no es suficiente para resolver el problema de reparación gradual del código. Esta es la razón por la que los alias de tipo introducen un nuevo nombre para el mismo tipo, de modo que T1 = T2, en lugar de intentar el subtipo.

Este comentario también se aplica a la sugerencia de @iand de hace dos semanas de algún tipo de "tipos sustituibles" y básicamente una expansión de la respuesta de

Se actualizó el resumen de la discusión de nivel superior. Cambios:

  • Se eliminó TODO para actualizar el resumen de la discusión del adaptador, que parece haberse desvanecido.
  • Se agregó un resumen de la discusión sobre la incorporación y los cambios de nombre de los campos.
  • Se movió el resumen de 'métodos sobre alias' a su propia sección fuera de la lista de preguntas de diseño, ampliado para incluir comentarios recientes.
  • Se agregó un resumen de la discusión del efecto en los programas que utilizan la reflexión.
  • Se agregó un resumen de la discusión de la compilación separada.
  • Se agregó un resumen de la discusión de varios enfoques basados ​​en subtipos.

@rsc con respecto a la compilación separada, mi comentario es relativo a si las definiciones de tipo necesitan mantener una lista de sus alias (que no es manejable a gran escala, debido al requisito de compilación por separado) o cada alias implica la construcción iterativa de una lista de nombres de alias siguientes el gráfico de importación, todo relacionado con el nombre de tipo inicial proporcionado en la definición de tipo. (y cómo y dónde guardar esa información para que el tiempo de ejecución tenga acceso a ella).

@atdiar No existe tal lista de nombres de alias en ninguna parte del sistema. El tiempo de ejecución no tiene acceso a él. Los alias no existen en tiempo de ejecución.

@rsc Eh, lo siento. Estoy atascado con la propuesta de alias inicial en la cabeza y estaba pensando en alias para func (mientras discutía el alias para tipos). En ese caso, habría una discrepancia entre los nombres en el código y los nombres en tiempo de ejecución.
El uso de la información en runtime.Frame para el registro necesitaría replantearse un poco en ese caso.
No me importa.

@rsc gracias por volver a resumir. El nombre del campo incrustado todavía me molesta; todas las soluciones alternativas propuestas se basan en cambios permanentes para mantener los nombres antiguos. Aunque el punto más importante de este comentario , a saber, que se trata de un caso especial de cambio de nombre de campos, que tampoco es posible, me convence de que esto debería verse (y resolverse) como un problema separado. ¿Tendría sentido abrir un problema separado para una solicitud / propuesta / discusión para admitir cambios de nombre de campo para una reparación gradual (posiblemente abordado en la misma versión de lanzamiento)?

@Merovius , estoy de acuerdo en que la reparación gradual del código para el cambio de nombre del campo parece el siguiente problema en la secuencia. Para comenzar esa discusión, creo que alguien necesitaría recopilar un conjunto de ejemplos del mundo real, tanto para que tengamos alguna evidencia de que es un problema generalizado como para contrastar posibles soluciones. Siendo realistas, no veo que eso suceda para el mismo lanzamiento.

De regreso de dos semanas fuera. La discusión parece haber convergido. Incluso la actualización de la discusión hace dos semanas fue bastante menor.

Sugiero que nosotros:

  • aceptar la propuesta de alias de tipo como la solución tentativa para el problema expuesto anteriormente,
    siempre que una implementación pueda estar lista para que la gente la pruebe al comienzo de Go 1.9 (1 de febrero).
  • cree una rama dev.typealias dev para que los CL puedan revisarse ahora (enero) y fusionarse en master al comienzo de Go 1.9.
  • tome una decisión final sobre mantener los alias de tipo cerca del comienzo de la congelación de Go 1.9 (como hicimos para los alias generalizados en el ciclo de Go 1.8).

+1

Aprecio la historia de la discusión detrás de este cambio. Digamos que está implementado. Sin duda, se convertirá en un detalle marginal del idioma, más que en una característica central. Como tal, agrega complejidad al lenguaje y herramientas desproporcionadamente a su frecuencia de uso real. También agrega más área de superficie en la que el idioma podría ser abusado inadvertidamente. Por esa razón, ser demasiado cauteloso es algo bueno, y me alegra que haya habido una gran cantidad de discusiones hasta ahora.

@Merovius : ¡Perdón por editar mi publicación! Pensé que nadie estaba leyendo. Inicialmente en este comentario, expresé cierto escepticismo de que este cambio de idioma es necesario cuando ya existen herramientas como la herramienta gorename .

@ jcao219 Esto se ha discutido antes, pero sorprendentemente, parece que no puedo encontrar esto rápidamente aquí. Se discute en detalle en el hilo original para los alias generales # 16339 y los hilos golang-nuts asociados. En resumen: este tipo de herramientas solo aborda cómo preparar los compromisos de reparación, no cómo secuenciar los cambios para evitar roturas. Si los cambios son realizados por una herramienta o por un humano es irrelevante para el problema, que actualmente no hay una secuencia de confirmaciones que no rompa algún código u otro (el comentario original de este problema y el documento asociado justifican esta declaración más en -profundidad).

Para herramientas más automatizadas (por ejemplo, integradas en la herramienta Go o similar), el comentario original aborda esto bajo el título "¿Puede ser un cambio de herramientas o compilador solo en lugar de un cambio de idioma?".

En conclusión, digamos que se implementa el cambio. Sin duda, se convertirá en un detalle marginal del idioma, más que en una característica central.

Me gustaría expresar dudas. :) No considero que esto sea una conclusión inevitable.

@Merovio

Me gustaría expresar dudas. :) No considero que esto sea una conclusión inevitable.

Supongo que quise decir que las personas que usarían esta función serán principalmente los mantenedores de paquetes importantes de Go con muchos clientes dependientes. En otras palabras, beneficia a aquellos que ya son expertos en Go. Al mismo tiempo, presenta una forma tentadora de hacer que el código sea menos legible para los nuevos programadores de Go. La excepción es el caso de uso de cambiar el nombre de nombres largos, pero los nombres de tipo Go naturales generalmente no son demasiado largos ni complejos.

Al igual que en el caso de la función de importación de puntos, sería prudente que los tutoriales y los documentos acompañen sus menciones de esta función con una declaración sobre las pautas de uso.

Por ejemplo, digamos que quería usar "github.com/gonum/graph/simple".DirectedGraph , y quería alias con digraph para evitar escribir simple.DirectedGraph , ¿sería una buena idea? caso de uso? ¿O debería restringirse este tipo de cambio de nombre a nombres excesivamente largos generados por cosas como protobuf?

@ jcao219 , el resumen de la discusión en la parte superior de esta página responde a sus preguntas. En particular, consulte estas secciones:

  • ¿Puede ser esto un cambio de herramientas o solo del compilador en lugar de un cambio de idioma?
  • ¿Qué otros usos pueden tener los alias?
  • Restricciones (las notas generales que comienzan esa sección)

Para su punto más general sobre los expertos de Go frente a los nuevos programadores de Go, un objetivo explícito de Go es facilitar la programación en grandes bases de código. El hecho de que sea un experto no tiene nada que ver con el tamaño de la base de código en la que está trabajando. (Tal vez recién esté comenzando en un nuevo proyecto que alguien más comenzó. Es posible que aún deba hacer este tipo de trabajo).

De acuerdo, según la unanimidad / silencio aquí, marcaré esta propuesta (como sugerí la semana pasada en https://github.com/golang/go/issues/18130#issuecomment-268614964) aprobada y crearé una rama dev.typealias .

El excelente resumen tiene una sección "¿Qué otras cuestiones debe abordar una propuesta de alias de tipo?" ¿Cuáles son los planes para abordar esos problemas después de que la propuesta haya sido declarada aceptada?

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

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

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

@ulikunitz con respecto a los problemas (todas estas citas del documento de diseño asumen 'tipo T1 = T2'):

  1. Manejo en godoc. El documento de diseño especifica cambios mínimos en godoc. Una vez que esté dentro, podemos ver si se necesita soporte adicional. Quizás, pero quizás no.
  2. ¿Se pueden definir métodos en tipos nombrados por alias? Si. Documento de diseño: "Dado que T1 es solo otra forma de escribir T2, no tiene su propio conjunto de declaraciones de métodos. En cambio, el conjunto de métodos de T1 es el mismo que el de T2. Al menos para la prueba inicial, no hay restricciones contra las declaraciones de métodos utilizando T1 como tipo de receptor, siempre que utilice T2 en la misma declaración sería válido ".
  3. Si se permiten alias a alias, ¿cómo manejamos los ciclos de alias? Sin ciclos. Documento de diseño: "En una declaración de alias de tipo, a diferencia de una declaración de tipo, T2 nunca debe referirse, directa o indirectamente, a T1".
  4. ¿Los alias deberían poder exportar identificadores no exportados? Si. Documento de diseño: "No hay restricciones en la forma de T2: puede ser de cualquier tipo, incluidos, entre otros, los tipos importados de otros paquetes".
  5. ¿Qué sucede cuando incrusta un alias (cómo accedes al campo incrustado)? El nombre se toma del alias (el nombre visible en el programa). Documento de diseño: https://golang.org/design/18130-type-alias#effect -on-embedding.
  6. ¿Los alias están disponibles como símbolos en el programa construido? No. Documento de diseño: "Los alias de tipo son en su mayoría invisibles en tiempo de ejecución". (La respuesta se deriva de esto, pero no se menciona explícitamente).
  7. Inyección de cadenas de ldflags: ¿y si nos referimos a un alias? No hay alias de var, por lo que esto no surge.

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

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

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

@rsc Muchas gracias por las aclaraciones.

Asumamos:

package a

import "b"

type T1 = b.T2

Por lo que tengo entendido, T1 es esencialmente idéntico a b.T2 y, por lo tanto, es un tipo no local y no se pueden definir nuevos métodos. Sin embargo, el identificador T1 se reexporta en el paquete a. ¿Es esta una interpretación correcta?

@ulikunitz eso es correcto

T1 denota exactamente el mismo tipo que b.T2. Es simplemente un nombre diferente. Si algo se exporta o no se basa solo en su nombre (no tiene nada que ver con el tipo que denota).

Para hacer explícita la respuesta de @griesemer : sí, T1 se exporta desde el paquete a (porque es T1, no t1).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Esto ahora está en master, antes de la apertura de Go 1.9. Siéntase libre de sincronizar en master y probar cosas. Gracias.

Redirigido desde # 18893

package main

import (
        "fmt"
        "q"
)

func main() {
        var a q.A
        var b q.B // i'm a named unnamed type !!!

        fmt.Printf("%T\t%T\n", a, b)
}

¿Qué esperabas ver?

deadwood(~/src) % go run main.go
q.A     q.B

¿Qué viste en su lugar?

deadwood(~/src) % go run main.go
q.A     []int

Discusión

Los alias no deberían aplicarse a tipos sin nombre. No hay una historia de "reparación de código" al pasar de un tipo sin nombre a otro. Permitir alias en tipos sin nombre significa que ya no puedo enseñar Go como tipos simplemente nombrados y sin nombre. En cambio, tengo que decir

oh, a menos que sea un alias, en cuyo caso debe recordar que _ podría ser_ un tipo sin nombre, incluso cuando lo importe desde otro paquete.

Y lo que es peor, permitirá a las personas promulgar patrones anti legibilidad como

type Any = interface{}

No permita que los tipos sin nombre tengan un alias.

@davecheney

No hay una historia de "reparación de código" al pasar de un tipo sin nombre a otro.

No es verdad. ¿Qué sucede si desea cambiar el tipo de parámetro de un método de un tipo con nombre a un tipo sin nombre o viceversa? El paso 1 es agregar el alias; el paso 2 es actualizar los tipos que implementan ese método para usar el nuevo tipo; el paso 3 es eliminar el alias.

(Es cierto que puede hacerlo hoy cambiando el nombre del método dos veces. El doble cambio de nombre es tedioso en el mejor de los casos).

Y lo que es peor, permitirá a las personas promulgar patrones anti legibilidad como
type Any = interface{}

La gente ya puede escribir type Any interface{} hoy. ¿Qué daño adicional introducen los alias en este caso?

La gente ya puede escribir el tipo Cualquier interfaz {} hoy. ¿Qué daño adicional introducen los alias en este caso?

Lo llamé anti patrón porque eso es precisamente lo que es. type Any interface{} , porque la persona _escribiendo_ el código escribe algo un poco más corto, que tiene un poco más de sentido para ellos.

Por otro lado, _todos_ los lectores, que tienen experiencia en la lectura de código Go y reconocen interface{} tan instintivamente como su cara en un espejo, tienen que aprender y volver a aprender cada variante de Any , Object , T , y asignarlos a cosas como type Any interface{} , type Any map[interface{}]interface{} , type Any struct{} por paquete.

¿Seguramente estás de acuerdo en que los nombres específicos de los paquetes para los modismos comunes de Go son un negativo neto para la legibilidad?

¿Seguramente estás de acuerdo en que los nombres específicos de los paquetes para los modismos comunes de Go son un negativo neto para la legibilidad?

Estoy de acuerdo, pero dado que el ejemplo en cuestión (con mucho, la ocurrencia más común de ese antipatrón que he encontrado) se puede hacer sin alias, no entiendo cómo ese ejemplo se relaciona con la propuesta de alias de tipo.

El hecho de que el antipatrón sea posible sin alias de tipo significa que ya debemos educar a los programadores de Go para evitarlo, independientemente de si pueden existir alias para tipos sin nombre.

Y, de hecho, los alias de tipo permiten la _ eliminación gradual_ de ese antipatrón de las bases de código en las que ya existe.

Considerar:

package antipattern

type Any interface{}  // not an alias

type Widget interface{
  Frozzle(Any) error
}

func Bozzle(w Widget) error {
  …
}

Hoy en día, los usuarios de antipattern.Bozzle estarían atascados usando antipattern.Any en sus implementaciones Widget , y no hay forma de eliminar antipattern.Any con reparaciones graduales. Pero con los alias de tipo, el propietario del paquete antipattern podría redefinirlo así:

// Any is deprecated; please use interface{} directly.
type Any = interface{}

Y ahora las personas que llaman pueden migrar de Any a interface{} gradualmente, permitiendo que el mantenedor de antipattern eventualmente lo elimine.

Mi punto es que no hay justificación para aplicar un alias a tipos sin nombre, por lo que
rechazar esta opción seguiría subrayando la inadecuación de
la práctica.

Lo contrario, permitir el alias de tipos sin nombre habilita no uno, sino dos
formas de este anti patrón.

El jueves 2 de febrero de 2017 a las 16:34, Bryan C. Mills [email protected] escribió:

Seguramente estás de acuerdo en que los nombres específicos de los paquetes de modismos comunes de Go son
¿Neto negativo para la legibilidad?

Estoy de acuerdo, pero dado que el ejemplo en cuestión (con mucho el más común
ocurrencia de ese antipatrón que he encontrado) se puede hacer sin
alias, no entiendo cómo se relaciona ese ejemplo con la propuesta de
alias de tipo.

El hecho de que el anti-patrón sea posible sin alias de tipo significa que
ya debemos educar a los programadores de Go para evitarlo, independientemente de si
pueden existir alias a tipos sin nombre.

-
Recibes esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/18130#issuecomment-276872714 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AAAcA6BGrFjjTi7eW1BPp7o81XIekbGXks5rYWr-gaJpZM4LBBEL
.

@davecheney No creo que tengamos ninguna evidencia todavía de que poder darle un nombre a un literal de tipo arbitrario sea perjudicial. Esta tampoco es una característica "sorpresa" inesperada - se ha discutido en detalle en el documento de diseño . En este punto, tiene sentido usar esto durante algún tiempo y ver a dónde nos lleva.

Como contraejemplo, hay API públicas que usan literales de tipo solo porque la API no quiere restringir a un cliente a un tipo específico (consulte https://golang.org/pkg/go/types/#Info por ejemplo ). Tener ese literal de tipo explícito puede ser documentación útil. Pero al mismo tiempo, puede resultar bastante molesto tener que repetir ese mismo tipo literal por todas partes; y de hecho ser un impedimento para la legibilidad. Ser capaz de hablar convenientemente sobre una IntSet lugar de una map[int]struct{} sin estar encerrado en esa única y única definición de IntSet es una ventaja en mi mente. Ahí es donde type IntSet = map[int]struct{} es exactamente correcto.

Finalmente, me gusta volver a consultar https://github.com/golang/go/issues/18130#issuecomment -268411811 en caso de que se lo haya perdido. Las declaraciones de tipo sin restricciones que utilizan = son realmente la declaración de tipo "elemental", y estoy feliz de que finalmente las tengamos en Go.

Quizás type intSet = map[int]struct{} (no exportado) sería una mejor manera de usar alias de tipo sin nombre, pero esto suena como el dominio de CodeReviewComments y las prácticas de programación recomendadas, en lugar de limitar la función.

Dicho esto, %T es una herramienta útil para ver los tipos al depurar o explorar el sistema de tipos. Me pregunto si debería haber un verbo de formato similar que incluya el alias. q.B = []int en el ejemplo de @davecheney .

@nathany ¿Cómo implementas ese verbo? La información de alias no está presente en tiempo de ejecución. (En lo que respecta al paquete reflect , el alias es _del mismo tipo_ que el alias).

@bcmills pensé que ese podría ser el caso ... 😞

Me imagino que las herramientas de análisis estático y los complementos del editor todavía están en la imagen para ayudar a trabajar con alias, así que está bien.

El 2 de febrero de 2017 a las 5:01 p.m., "Nathan Youngman" [email protected] escribió:

Dicho esto,% T es una herramienta útil para ver los tipos al depurar o explorar el
sistema de tipo. Me pregunto si debería haber un verbo de formato similar que
incluye el alias? qB = [] int en @davecheney
Ejemplo de https://github.com/davecheney .

Creo que una mejor solución es agregar un modo de consulta a guru para responder a esto
pregunta:

que son los alias declarados en todo GOPATH (o un paquete dado) para
este tipo dado en la línea de comando?

No me preocupa el abuso de los tipos sin nombre de alias, pero el potencial
alias duplicados al mismo tipo sin nombre.

@davecheney Agregué su sugerencia a la sección "Restricciones" del resumen de la discusión en la parte superior. Como todas las restricciones, nuestra posición general es que las restricciones agregan complejidad (ver notas arriba) y probablemente necesitaríamos ver evidencia real de daño generalizado para poder introducir una restricción. Tener que cambiar la forma en que enseña Go no es suficiente: cualquier cambio que hagamos en el idioma requerirá cambiar la forma en que enseña Go.

Como se indica en el documento de diseño y en la lista de correo, estamos trabajando en una mejor terminología para facilitar las explicaciones.

@minux , como señaló @bcmills , la información de alias no existe en tiempo de ejecución (completamente fundamental para el diseño). No hay forma de implementar un "% T que incluya el alias".

El 2 de febrero de 2017 a las 8:33 p.m., "Russ Cox" [email protected] escribió:

@minux https://github.com/minux , como @bcmills
https://github.com/bcmills señaló, la información de alias no existe
en tiempo de ejecución (completamente fundamental para el diseño). No hay manera de
implementar un "% T que incluya el alias".

Sugiero un modo de consulta Go guru (https://golang.org/x/tools/cmd/guru)
para el mapeo de alias inverso, que se basa en el análisis de código estático. Eso
no importa si la información de alias está disponible en tiempo de ejecución o no.

@minux , ya veo, estás respondiendo por correo electrónico y Github hace que el texto citado parezca texto que escribiste tú mismo. Estaba respondiendo al texto que citó de Nathan Youngman, pensando que era suyo. Perdón por la confusion.

Con respecto a la terminología y la enseñanza, encontré que los antecedentes de los tipos de marca publicados por @griesemer son bastante informativos. Gracias por eso.

Al explicar los tipos y las conversiones de tipos, las tuzas bebé inicialmente piensan que estoy hablando de un alias de tipo, probablemente debido a su familiaridad con otros idiomas.

Cualquiera que sea la terminología final, podría imaginar la introducción de alias de tipos antes de los tipos con nombre (de marca), especialmente porque es probable que la declaración de nuevos tipos con nombre se produzca después de introducir byte y rune en cualquier libro o plan de estudios. Sin embargo, quiero ser consciente de la preocupación de @davecheney de no fomentar los antipatrones.

Para type intSet map[int]struct{} decimos que map[int]struct{} es el tipo _ subyacente_. ¿Cómo llamamos a cada lado de type intSet = map[int]struct{} ? ¿Alias ​​y tipo de alias?

En cuanto a %T , ya necesito explicar que byte y rune dan como resultado uint8 y int32 , así que esto no es diferente.

En todo caso, creo que los alias de tipo harán que byte y rune más fáciles de explicar. En mi opinión, el desafío será saber cuándo usar tipos con nombre frente a alias de tipos, y luego poder comunicar eso.

@nathany Creo que tiene mucho sentido introducir "tipos de alias" primero, aunque no usaría el término necesariamente. Las declaraciones de "alias" recién introducidas son simplemente declaraciones regulares que no hacen nada especial. El identificador de la izquierda y el tipo de la derecha son uno y el mismo, denotan tipos idénticos. Ni siquiera estoy seguro de que necesitemos los términos alias o tipo alias (no llamamos alias a un nombre constante, y al valor constante la constante alias).

La declaración de tipo tradicional (sin alias) hace más trabajo: primero crea un nuevo tipo a partir del tipo de la derecha antes de vincularlo con el identificador de la izquierda. Por lo tanto, el identificador y el tipo de la derecha no son iguales (solo comparten el mismo tipo subyacente). Este es claramente el concepto más complicado.

Necesitamos un nuevo término para estos tipos recién creados porque cualquier tipo ahora puede tener un nombre. Y necesitamos poder referirnos a ellos, ya que existen reglas de especificación que se refieren a ellos (identidad de tipo, asignabilidad, tipos de base de receptor).

Aquí hay otra forma de describir esto, que puede ser útil en un entorno de enseñanza: un tipo puede tener color o no. Todos los tipos predeclarados y todos los literales de tipo no están coloreados. La única forma de crear un nuevo tipo de color es a través de una declaración de tipo tradicional (sin alias) que primero pinta (una copia) del tipo de la derecha con un color nuevo, nunca antes usado (quitando el color anterior, si lo hay, completamente en el proceso) antes de vincular el identificador de la izquierda. Nuevamente, el identificador y el tipo de color (creado de forma implícita e invisible) son idénticos, pero son diferentes del tipo (de diferente color o sin color) escrito a la derecha.

Usando esta analogía, también podemos reformular varias otras reglas existentes:

  • Un tipo de color siempre es diferente de cualquier otro tipo (porque cada declaración de tipo usa un color nuevo, nunca antes usado).
  • Los métodos solo pueden asociarse con tipos de base de receptor que están coloreados.
  • El tipo subyacente de un tipo es ese tipo despojado de todo su color.
    etc.

no llamamos alias a un nombre constante, y al valor constante la constante con alias

buen punto 👍

No estoy seguro de si la analogía entre colores y no coloreados es más fácil de entender, pero demuestra que hay más de una forma de explicar los conceptos.

Los tipos tradicionales con nombre / marca / color ciertamente requieren más explicación. Especialmente cuando un tipo con nombre se puede declarar utilizando un tipo con nombre existente. Hay diferencias bastante sutiles a tener en cuenta.

type intSet map[int]struct{} // a new type with an underlying type map[int]struct{}

type myIntSet intSet // a new type with an underlying type map[int]struct{}

type otherIntSet = intSet // just another name (alias) for intSet, add methods to intSet (only in the same package)

type literalIntSet = map[int]struct{} // just another name for map[int]struct{}, no adding methods

Sin embargo, no es insuperable. Suponiendo que esto aterrice en Go 1.9, sospecho que veremos segundas ediciones de varios libros de Go. 😉

Regularmente me refiero a las especificaciones de Go para conocer la terminología aceptada, por lo que tengo mucha curiosidad por saber qué términos se eligen al final.

Necesitamos un nuevo término para estos tipos recién creados porque cualquier tipo ahora puede tener un nombre.

Algunas ideas:

  • "distinguido" o "distinto" (como en, se puede distinguir de otros tipos)
  • "único" (como en, es un tipo diferente de todos los demás tipos)
  • "concreto" (como en, es una entidad que existe en el tiempo de ejecución)
  • "identificable" (como en, el tipo tiene una identidad)

@bcmills Hemos estado pensando en tipos distinguidos, únicos, distintos, de marca, coloreados, definidos, sin alias, etc. "Concreto" es engañoso porque una interfaz también se puede colorear, y una interfaz es la encarnación de un tipo abstracto. "Identificable" también parece engañoso porque una "estructura {int}" es tan identificable como cualquier tipo con nombre explícito (sin alias).

Recomendaría no:

  • "de color" (en contextos que no son de programación, la frase "tipos de color" tiene fuertes connotaciones de prejuicio racial)
  • "sin alias" (es confuso, ya que el destino del alias puede ser o no lo que antes se llamaba un "tipo con nombre")
  • "definido" (los alias también se definen, simplemente se definen como alias)

"de marca" podría funcionar: tiene una connotación de "tipos como ganado", pero eso no me parece intrínsecamente malo.

Único y distinto parecen las opciones destacadas hasta ahora.

Son simples y comprensibles sin mucho contexto o conocimiento adicional. Si no conociera la distinción, creo que al menos tendría una idea general de lo que implican. No puedo decir eso de las otras opciones.

Una vez que aprende el término, no importa, pero un nombre connotativo evita barreras innecesarias para internalizar la distinción.

Esta es la definición de un argumento en bicicleta. Robert tiene una CL pendiente en https://go-review.googlesource.com/#/c/36213/ que parece estar perfectamente bien.

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

Quiero volver a plantear el problema de go fix .

Para que quede claro que no estoy sugiriendo "eliminar" el alias. Quizás sea algo útil y adecuado para otros trabajos, esa es otra historia.

En mi opinión, es algo muy importante que el título se trate de tipos móviles. No tengo ningún deseo de dejar perplejo el tema. Nuestro objetivo es hacer frente a una especie de cambios de interfaz en un proyecto. Cuando llegamos a un cambio en la interfaz, no es cierto que esperemos que todos los usuarios usen estas dos interfaces (antigua y nueva) como la misma eventualmente , y es por eso que decimos 'reparación gradual de código'. Esperamos que los usuarios eliminen / cambien el uso del anterior.

Todavía considero la herramienta como el mejor método para reparar el código, algo así como la idea que sugirió @ tux21b . Por ejemplo:

$ cat "$GOROOT"/RENAME
# This file could be used for `go fix`
[package]
x/net/context=context
[type]
io.ByteBuffer=bytes.Buffer

$ go fix -rename "$GOROOT"/RENAME [packages]
# -- or --
# use a standard libraries rename table as default
$ go fix -rename [packages]
# -- or --
# include this fix as default
$ go fix [packages]

La única razón por la que @rsc dice que no aquí es que los cambios afectarán a otras herramientas. Pero creo que no es cierto en este flujo de trabajo : si hay un paquete desactualizado (por ejemplo, una dependencia) usa el nombre / ruta obsoleto del paquete, por ejemplo, x/net/context , podemos arreglar el código al principio , al igual que el documento dice cómo migrar el código a la nueva versión, pero no la codificación, a través de una tabla configurable en formato de texto. Luego, puede usar cualquier herramienta cuando lo desee, al igual que Go de la nueva versión. Hay un efecto secundario: modificará el código.

@LionNatsu , creo que tiene razón, pero creo que ese es un tema aparte: ¿deberíamos adoptar convenciones para los paquetes para explicar a los clientes potenciales cómo actualizar su código en respuesta a los cambios de API de una manera mecánica? Quizás, pero tendríamos que averiguar cuáles son esas convenciones. ¿Puede abrir un número separado para este tema, señalando esta conversación? Gracias.

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

Con esta propuesta en la punta, ahora puedo crear este paquete:

package safe

import "unsafe"

type Pointer = unsafe.Pointer

que permite a los programas crear valores unsafe.Pointer sin importar unsafe directamente:

package main

import "safe"

func main() {
    x := []int{4, 9}
    y := *(*int)(safe.Pointer(uintptr(safe.Pointer(&x[0])) + 8))
    println(y)
}

El documento de diseño de declaraciones de alias original indica que esto se admite explícitamente. No es explícito en esta propuesta de alias de tipo más reciente, pero funciona.

En el tema de la declaración de alias, la razón para esto es: _ "La razón por la que permitimos el uso de alias para inseguro. Puntero es que ya es posible definir un tipo que tiene inseguro.Pointer como tipo subyacente". _ Https://github.com/ golang / go / issues / 16339 # issuecomment -232435361

Si bien eso es cierto, creo que permitir un alias de unsafe.Pointer introduce algo nuevo: los programas ahora pueden crear valores unsafe.Pointer sin importar explícitamente inseguros.

Para escribir el programa anterior antes de esta propuesta, tendría que mover la conversión safe.Pointer a un paquete que se importe inseguro. Esto puede dificultar un poco la auditoría de los programas por su uso de inseguros.

@crawshaw , ¿no podrías haber hecho esto antes?

package safe

import (
  "reflect"
  "unsafe"
)

func Pointer(p interface {}) unsafe.Pointer {
  switch v := reflect.ValueOf(p); v.Kind() {
  case reflect.Uintptr:
    return unsafe.Pointer(uintptr(v.Uint()))
  default:
    return unsafe.Pointer(v.Pointer())
  }
}

Creo que eso permitiría compilar exactamente el mismo programa, con la misma falta de importación en el paquete main .

(No sería necesariamente un programa válido: la conversión uintptr -to- Pointer incluye una llamada de función, por lo que no cumple con la restricción del paquete unsafe que " ambas conversiones deben aparecer en la misma expresión, con solo la aritmética intermedia entre ellas ". Sin embargo, sospecho que sería posible construir un programa válido equivalente sin importar unsafe de main haciendo uso de cosas como reflect.SliceHeader .)

Parece que exportar un tipo inseguro oculto es solo otra regla para agregar a la auditoría.

Sí, quería señalar que la creación de alias directamente no es segura. El puntero hace que el código sea más difícil de auditar, lo suficiente como para que espero que nadie termine haciéndolo.

@crawshaw Según mi comentario, esto también era cierto antes de que tuviéramos un alias de tipo. Lo siguiente es válido:

package a

import "unsafe"

type P unsafe.Pointer
package main

import "./a"
import "fmt"

var x uint64 = 0xfedcba9876543210
var h = *(*uint32)(a.P(uintptr(a.P(&x)) + 4))

func main() {
    fmt.Printf("%x\n", h)
}

Es decir, en el paquete principal, puedo hacer operaciones aritméticas inseguras usando a.P aunque no haya unsafe paquete a.P no sea un alias. Esto siempre fue posible.

¿Hay algo más a lo que te refieres?

Mi error. Pensé que eso no funcionó. (Tenía la impresión de que las reglas especiales se aplicaban a inseguro. El puntero no se propagaría a los nuevos tipos definidos a partir de él).

La especificación en realidad no es clara sobre esto. Al observar la implementación de go / types, resulta que mi implementación inicial requería unsafe.Pointer exactamente, no solo un tipo que tenía un tipo subyacente de unsafe.Pointer . Acabo de encontrar el # 6326, que es cuando cambié go / types para que sea compatible con gc.

Quizás deberíamos rechazar esto para las definiciones de tipos regulares y también rechazar los alias de unsafe.Pointer . No veo ninguna buena razón para permitirlo y compromete lo explícito de tener que importar unsafe por código inseguro.

Esto ocurrió. No creo que quede nada aquí.

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