Go: propuesta: especificación: agregar compatibilidad con enumeración escrita

Creado en 1 abr. 2017  ·  180Comentarios  ·  Fuente: golang/go

Me gustaría proponer que se agregue enumeración a Go como un tipo especial de type . Los siguientes ejemplos están tomados del ejemplo de protobuf.

Enumeraciones en Go hoy

type SearchRequest int
var (
    SearchRequestUNIVERSAL SearchRequest = 0 // UNIVERSAL
    SearchRequestWEB       SearchRequest = 1 // WEB
    SearchRequestIMAGES    SearchRequest = 2 // IMAGES
    SearchRequestLOCAL     SearchRequest = 3 // LOCAL
    SearchRequestNEWS      SearchRequest = 4 // NEWS
    SearchRequestPRODUCTS  SearchRequest = 5 // PRODUCTS
    SearchRequestVIDEO     SearchRequest = 6 // VIDEO
)

type SearchRequest string
var (
    SearchRequestUNIVERSAL SearchRequest = "UNIVERSAL"
    SearchRequestWEB       SearchRequest = "WEB"
    SearchRequestIMAGES    SearchRequest = "IMAGES"
    SearchRequestLOCAL     SearchRequest = "LOCAL"
    SearchRequestNEWS      SearchRequest = "NEWS"
    SearchRequestPRODUCTS  SearchRequest = "PRODUCTS"
    SearchRequestVIDEO     SearchRequest = "VIDEO"
)

// IsValid has to be called everywhere input happens, or you risk bad data - no guarantees
func (sr SearchRequest) IsValid() bool {
    switch sr {
        case SearchRequestUNIVERSAL, SearchRequestWEB...:
            return true
    }
    return false
}

Cómo podría verse con soporte de idiomas

enum SearchRequest int {
    0 // UNIVERSAL
    1 // WEB
    2 // IMAGES
    3 // LOCAL
    4 // NEWS
    5 // PRODUCTS
    6 // VIDEO
}

enum SearchRequest string {
    "UNIVERSAL"
    "WEB"
    "IMAGES"
    "LOCAL"
    "NEWS"
    "PRODUCTS"
    "VIDEO"
}

El patrón es lo suficientemente común que creo que justifica una carcasa especial, y creo que hace que el código sea más legible. En la capa de implementación, me imagino que la mayoría de los casos se pueden verificar en tiempo de compilación, algunos de los cuales ya ocurren hoy, mientras que otros son casi imposibles o requieren compensaciones significativas.

  • Seguridad para los tipos exportados : nada impide que alguien haga SearchRequest(99) o SearchRequest("MOBILEAPP") . Las soluciones actuales incluyen hacer un tipo no exportado con opciones, pero eso a menudo hace que el código resultante sea más difícil de usar/documentar.
  • Seguridad en el tiempo de ejecución : al igual que protobuf verificará la validez mientras se desarma, esto proporciona una validación en todo el idioma, cada vez que se crea una enumeración.
  • Herramientas/Documentación : muchos paquetes de hoy incluyen opciones válidas en los comentarios de campo, pero no todos lo hacen y no hay garantía de que los comentarios no estén desactualizados.

Cosas para considerar

  • Nil : al implementar enum sobre el sistema de tipos, no creo que esto deba requerir una carcasa especial. Si alguien quiere que nil sea válido, entonces la enumeración debe definirse como un puntero.
  • Asignaciones de valor predeterminado/tiempo de ejecución : esta es una de las decisiones más difíciles de tomar. ¿Qué sucede si el valor predeterminado de Go no está definido como una enumeración válida? El análisis estático puede mitigar algo de esto en el momento de la compilación, pero debería haber una forma de manejar la entrada externa.

No tengo opiniones firmes sobre la sintaxis. Creo que esto podría hacerse bien y tendría un impacto positivo en el ecosistema.

Go2 LanguageChange NeedsInvestigation Proposal

Comentario más útil

@ md2perpe que no son enumeraciones.

  1. No se pueden enumerar, iterar.
  2. No tienen una representación de cadena útil.
  3. No tienen identidad:

```ir
paquete principal

importar (
"fmt"
)

función principal() {
escriba Solicitud de búsqueda int
constante (
Solicitud de búsqueda universal = iota
Web
)

const (
    Another SearchRequest = iota
    Foo
)

fmt.Println("Should be false: ", (Web == Foo))
    // Prints: "Should be false:  true"

}
````

Estoy totalmente de acuerdo con @derekperkins en que Go necesita una enumeración como ciudadano de primera clase. Cómo se vería eso, no estoy seguro, pero sospecho que podría hacerse sin romper la casa de cristal Go 1.

Todos 180 comentarios

@derekparker hay una discusión para hacer una propuesta de Go2 en #19412

Lo leí más temprano hoy, pero parecía más enfocado en tipos válidos, donde esto se enfoca en valores de tipo válidos. Tal vez este sea un subconjunto de esa propuesta, pero también es un cambio de menor alcance en el sistema de tipos que podría implementarse en Go hoy.

Las enumeraciones son un caso especial de tipos de suma donde todos los tipos son iguales y hay un valor asociado a cada uno por un método. Más a escribir, seguramente, pero mismo efecto. Independientemente, sería uno u otro, los tipos de suma cubren más terreno e incluso los tipos de suma son poco probables. No pasa nada hasta Go2 debido al acuerdo de compatibilidad con Go1, en cualquier caso, ya que estas propuestas requerirían, como mínimo, una nueva palabra clave, en caso de que alguna de ellas fuera aceptada.

Está bien, pero ninguna de estas propuestas está rompiendo el acuerdo de compatibilidad. Se expresó la opinión de que los tipos de suma eran "demasiado grandes" para agregarlos a Go1. Si ese es el caso, entonces esta propuesta es un término medio valioso que podría ser un trampolín para los tipos de suma total en Go2.

Ambos requieren una nueva palabra clave que rompería el código Go1 válido usándolo como identificador

Creo que podría solucionarse

Una nueva función de lenguaje necesita casos de uso convincentes. Todas las características del lenguaje son útiles, o nadie las propondría; la pregunta es: ¿son lo suficientemente útiles como para justificar complicar el lenguaje y exigir que todos aprendan los nuevos conceptos? ¿Cuáles son los casos de uso convincentes aquí? ¿Cómo los usará la gente? Por ejemplo, ¿la gente esperaría poder iterar sobre el conjunto de valores de enumeración válidos y, de ser así, cómo lo harían? ¿Esta propuesta hace más que evitar agregar casos predeterminados a algunos conmutadores?

Esta es la forma idiomática de escribir enumeraciones en Go actual:

type SearchRequest int

const (
    Universal SearchRequest = iota
    Web
    Images
    Local
    News
    Products
    Video
)

Esto tiene la ventaja de que es fácil crear banderas que pueden ser OR:ed (usando el operador | ):

type SearchRequest int

const (
    Universal SearchRequest = 1 << iota
    Web
    Images
    Local
    News
    Products
    Video
)

No puedo ver que la introducción de una palabra clave enum lo haría mucho más corto.

@ md2perpe que no son enumeraciones.

  1. No se pueden enumerar, iterar.
  2. No tienen una representación de cadena útil.
  3. No tienen identidad:

```ir
paquete principal

importar (
"fmt"
)

función principal() {
escriba Solicitud de búsqueda int
constante (
Solicitud de búsqueda universal = iota
Web
)

const (
    Another SearchRequest = iota
    Foo
)

fmt.Println("Should be false: ", (Web == Foo))
    // Prints: "Should be false:  true"

}
````

Estoy totalmente de acuerdo con @derekperkins en que Go necesita una enumeración como ciudadano de primera clase. Cómo se vería eso, no estoy seguro, pero sospecho que podría hacerse sin romper la casa de cristal Go 1.

@md2perpe iota es una forma muy limitada de abordar las enumeraciones, lo que funciona muy bien para un conjunto limitado de circunstancias.

  1. Necesitas int
  2. Solo necesita ser consistente dentro de su paquete, no representar un estado externo

Tan pronto como necesite representar una cadena u otro tipo, lo cual es muy común para las banderas externas, iota no funciona para usted. Si desea hacer coincidir con una representación externa/de base de datos, no usaría iota , porque entonces ordenar el código fuente importa y reordenar causaría problemas de integridad de datos.

Este no es solo un problema de conveniencia para acortar el código. Esta es una propuesta que permitirá la integridad de los datos de una manera que el lenguaje actual no exige.

@ianlancetaylor

Por ejemplo, ¿la gente esperaría poder iterar sobre el conjunto de valores de enumeración válidos y, de ser así, cómo lo harían?

Creo que es un caso de uso sólido, como lo menciona @bep. Creo que la iteración se vería como un bucle Go estándar, y creo que se repetirían en el orden en que se definieron.

for i, val := range SearchRequest {
...
}

Si Go agregara algo más que iota, en ese momento, ¿por qué no elegir tipos de datos algebraicos?

Por extensión de ordenar según el orden de definición, y siguiendo el ejemplo de protobuf, creo que el valor por defecto del campo sería el primer campo definido.

@bep No es tan conveniente, pero puede obtener todas estas propiedades:

package main

var SearchRequests []SearchRequest
type SearchRequest struct{ name string }
func (req SearchRequest) String() string { return req.name }

func Request(name string) SearchRequest {
    req := SearchRequest{name}
    SearchRequests = append(SearchRequests, req)
    return req
}

var (
    Universal = Request("Universal")
    Web       = Request("Web")

    Another = Request("Another")
    Foo     = Request("Foo")
)

func main() {
    fmt.Println("Should be false: ", (Web == Foo))
    fmt.Println("Should be true: ", (Web == Web))
    for i, req := range SearchRequests {
        fmt.Println(i, req)
    }
}

No creo que las enumeraciones comprobadas en tiempo de compilación sean una buena idea. Creo que go tiene esto en este momento . mi razonamiento es

  • Las enumeraciones verificadas en tiempo de compilación no son compatibles con versiones anteriores ni posteriores para el caso de adiciones o eliminaciones. #18130 dedica un esfuerzo significativo a avanzar hacia la habilitación de la reparación gradual del código; las enumeraciones destruirían ese esfuerzo; cualquier paquete que alguna vez quiera cambiar un conjunto de enumeraciones, automáticamente y por la fuerza rompería todos sus importadores.
  • Al contrario de lo que afirma el comentario original, protobuf (por esa razón específica) en realidad no verifica la validez de los campos de enumeración. proto2 especifica que un valor desconocido para una enumeración debe tratarse como un campo desconocido y proto3 incluso especifica que el código generado debe tener una forma de representarlos con el valor codificado (exactamente como lo hace go actualmente con las enumeraciones falsas)
  • Al final, en realidad no agrega mucho. Puede obtener stringificación utilizando la herramienta stringer. Puede obtener una iteración agregando una const Sentinel MaxValidFoo (pero vea la advertencia anterior. Ni siquiera debería tener el requisito). Simplemente no debería tener las dos const-decls en primer lugar. Simplemente integre una herramienta en su CI que verifique eso.
  • No creo que otros tipos además de ints sean realmente necesarios. La herramienta de stringer ya debería cubrir la conversión a y desde strings; al final, el código generado sería equivalente a lo que generaría un compilador de todos modos (a menos que sugiera seriamente que cualquier comparación en "string-eums" iteraría los bytes...)

En general, solo un gran -1 para mí. No solo no agrega nada; duele activamente.

Creo que la implementación de enumeración actual en Go es muy sencilla y proporciona suficientes verificaciones de tiempo de compilación. De hecho, espero algún tipo de enumeraciones de Rust con coincidencia de patrones básicos, pero posiblemente rompa las garantías de Go1.

Dado que las enumeraciones son un caso especial de tipos de suma y la sabiduría común es que debemos usar interfaces para simular tipos de suma, la respuesta es claramente https://play.golang.org/p/1BvOakvbj2

(si no está claro: sí, eso es una broma, al estilo clásico de los programadores, me equivoco).

Con toda seriedad, para las características discutidas en este hilo, algunas herramientas adicionales serían útiles.

Al igual que la herramienta stringer, una herramienta "ranger" podría generar el equivalente de la función Iter en el código que vinculé anteriormente.

Algo podría generar implementaciones {Binary,Text}{Marshaler,Unmarshaler} para que sea más fácil enviarlas por cable.

Estoy seguro de que hay un montón de cositas como esta que serían bastante útiles en alguna ocasión.

Existen algunas herramientas de verificación/intercambio para la verificación exhaustiva de los tipos de suma simulados con interfaces. No hay razón para que no haya enumeraciones de iota que le indiquen cuándo se pierden los casos o si se usan constantes sin tipo no válidas (¿tal vez debería informar algo que no sea 0?).

Ciertamente hay espacio para mejorar en ese frente, incluso sin cambios de idioma.

Las enumeraciones complementarían el sistema de tipos ya establecido. Como han demostrado los numerosos ejemplos de este número, los componentes básicos de las enumeraciones ya están presentes. Así como los canales son abstracciones de alto nivel basadas en tipos más primitivos, las enumeraciones deben construirse de la misma manera. Los humanos son arrogantes, torpes y olvidadizos, mecanismos como las enumeraciones ayudan a los programadores humanos a cometer menos errores de programación.

@bep Tengo que estar en desacuerdo con los tres puntos. Las enumeraciones idiomáticas de Go se parecen mucho a las enumeraciones de C, que no tienen ninguna iteración de valores válidos, no tienen ninguna conversión automática a cadenas y no tienen necesariamente una identidad distinta.

Es bueno tener iteración, pero en la mayoría de los casos, si desea iteración, está bien definir constantes para el primer y último valor. Incluso puede hacerlo de una manera que no requiera actualización cuando agregue nuevos valores, ya que iota automáticamente lo hará uno más allá del final. La situación en la que el soporte de idioma marcaría una diferencia significativa es cuando los valores de la enumeración no son contiguos.

La conversión automática a cadena es solo un valor pequeño: especialmente en esta propuesta, los valores de cadena deben escribirse para que se correspondan con los valores int, por lo que hay poco que ganar escribiendo explícitamente una matriz de valores de cadena usted mismo. En una propuesta alternativa, podría valer más, pero también hay inconvenientes en obligar a que los nombres de las variables se correspondan con las representaciones de cadenas.

Finalmente, la identidad distinta, ni siquiera estoy seguro, es una característica útil en absoluto. Las enumeraciones no son tipos de suma como, por ejemplo, Haskell. Son números con nombre. El uso de enumeraciones como valores de marca, por ejemplo, es común. Por ejemplo, puede tener ReadWriteMode = ReadMode | WriteMode y esto es algo útil. Es muy posible tener también otros valores, por ejemplo, podría tener DefaultMode = ReadMode . No es como si algún método pudiera evitar que alguien escribiera const DefaultMode = ReadMode en cualquier caso; ¿Para qué sirve exigir que suceda en una declaración separada?

@bep Tengo que estar en desacuerdo con los tres puntos. Las enumeraciones idiomáticas de Go se parecen mucho a las enumeraciones de C, que no tienen ninguna iteración de valores válidos, no tienen ninguna conversión automática a cadenas y no tienen necesariamente una identidad distinta.

@alercah , por favor no incluya este idomatic Go en ninguna discusión como un supuesto "argumento ganador"; Go no tiene enumeraciones integradas, por lo que hablar de algunos idoms que no existen tiene poco sentido.

Go se creó para ser un mejor C/C++ o un Java menos detallado , por lo que compararlo con este último tendría más sentido. Y Java tiene un Enum type incorporado ("Los tipos de enumeración del lenguaje de programación Java son mucho más poderosos que sus contrapartes en otros lenguajes"): https://docs.oracle.com/javase/tutorial/java /javaOO/enum.html

Y, aunque no esté de acuerdo con la "parte mucho más poderosa", el tipo Java Enum tiene las tres características que mencioné.

Puedo apreciar el argumento de que Go es más ágil, más simple, etc., y que se debe hacer algún compromiso para mantenerlo de esta manera, y he visto algunas soluciones alternativas en este hilo que funcionan, pero un conjunto de iota ints solos no hacen una enumeración.

Las enumeraciones y las conversiones automáticas de cadenas son buenas candidatas para la función 'ir a generar'. Ya tenemos algunas soluciones. Las enumeraciones de Java están en el medio de las enumeraciones clásicas y los tipos de suma. Así que es un mal diseño de lenguaje en mi opinión.
Lo que pasa con el Go idiomático es la clave, y no veo razones sólidas para copiar todas las características del idioma X al idioma Y, solo porque alguien está familiarizado con él.

Los tipos de enumeración del lenguaje de programación Java son mucho más poderosos que sus contrapartes en otros lenguajes

Eso era cierto hace una década. Vea la implementación moderna de costo cero de Option en Rust impulsada por tipos de suma y coincidencia de patrones.

Lo que pasa con el Go idiomático es la clave, y no veo razones sólidas para copiar todas las características del idioma X al idioma Y, solo porque alguien está familiarizado con él.

Tenga en cuenta que no estoy demasiado en desacuerdo con las conclusiones dadas aquí, pero el uso de _Go idiomático_ está poniendo a Go en un pedestal artístico. La mayoría de la programación de software es bastante aburrida y práctica. Y, a menudo, solo necesita completar un cuadro desplegable con una enumeración ...

//go:generate enumerator Foo,Bar
Escrito una vez, disponible en todas partes. Tenga en cuenta que el ejemplo es abstracto.

@bep Creo que leíste mal el comentario original. Se suponía que "Ir a las enumeraciones idiomáticas" se refería a la construcción actual de usar el tipo Foo int + const-decl + iota, creo, para no decir "lo que sea que propongas no es idiomático".

@rsc Con respecto a la etiqueta Go2 , eso va en contra de mi razonamiento para enviar esta propuesta. #19412 es una propuesta de tipos de suma completa, que es un superconjunto más poderoso que mi propuesta de enumeración simple aquí, y preferiría ver eso en Go2. Desde mi perspectiva, la probabilidad de que Go2 suceda en los próximos 5 años es pequeña y prefiero ver que algo suceda en un período de tiempo más corto.

Si mi propuesta de una nueva palabra clave reservada enum es imposible para BC, todavía hay otras formas de implementarla, ya sea una integración completa del idioma o herramientas integradas en go vet . Como dije originalmente, no soy particular con la sintaxis, pero creo firmemente que sería una valiosa adición a Go hoy sin agregar una carga cognitiva significativa para los nuevos usuarios.

Una nueva palabra clave no es posible antes de Go 2. Sería una clara violación de la garantía de compatibilidad de Go 1.

Personalmente, todavía no veo los argumentos convincentes para enum o, para el caso, para los tipos de suma, incluso para Go 2. No digo que no puedan suceder. Pero uno de los objetivos del lenguaje Go es la simplicidad del lenguaje. No es suficiente que una función de idioma sea útil; todas las características del lenguaje son útiles; si no fueran útiles, nadie las propondría. Para agregar una función a Go, la función debe tener suficientes casos de uso convincentes para que valga la pena complicar el lenguaje. Los casos de uso más convincentes son el código que no se puede escribir sin la característica, al menos ahora sin una gran incomodidad.

Me encantaría ver enumeraciones en Go. Constantemente me encuentro queriendo restringir mi API expuesta (o trabajando con una API restringida fuera de mi aplicación) en la que hay un número limitado de entradas válidas. Para mí, este es el lugar perfecto para una enumeración.

Por ejemplo, podría estar creando una aplicación de cliente que se conecta a algún tipo de API de estilo RPC y tiene un conjunto específico de acciones/códigos de operación. Puedo usar const s para esto, pero no hay nada que impida que nadie (¡incluido yo mismo!) envíe un código no válido.

Por otro lado, si estoy escribiendo el lado del servidor para esa misma API, sería bueno poder escribir una declaración de cambio en la enumeración, eso generaría un error de compilación (o al menos algo go vet advertencias) si no se comprueban todos los valores posibles de la enumeración (o al menos existe un default: ).

Creo que esta (enumeraciones) es un área en la que Swift realmente acertó .

Podría estar creando una aplicación de cliente que se conecta a algún tipo de API de estilo RPC y tiene un conjunto específico de acciones/códigos de operación. Puedo usar constantes para esto, pero no hay nada que impida que nadie (¡incluido yo mismo!) envíe un código no válido.

Esta es una idea horrible para resolver con enumeraciones. Esto significaría que ahora nunca puede agregar un nuevo valor de enumeración, porque de repente los RPC pueden estar fallando o sus datos se volverán ilegibles al revertirlos. La razón por la que proto3 requiere que el código de enumeración generado admita un valor de "código desconocido" es que esta es una lección aprendida por el dolor (compárelo con cómo proto2 resolvió esto, que es mejor, pero sigue siendo muy malo). Desea que la aplicación pueda manejar este caso correctamente.

@Merovius Respeto tu opinión, pero cortésmente no estoy de acuerdo. Asegurarse de que solo se usen valores válidos es uno de los principales usos de las enumeraciones.

Las enumeraciones no son adecuadas para todas las situaciones, ¡pero son excelentes para algunas! El control de versiones y el manejo de errores adecuados deberían poder manejar nuevos valores en la mayoría de las situaciones.

Para tratar con procesos externos, tener un estado oh-oh es imprescindible, sin duda.

Con las enumeraciones (o los tipos de suma más generales y útiles) puede agregar un código "desconocido" explícito a la suma/enumeración que el compilador le obliga a manejar (o simplemente manejar esa situación por completo en el punto final si todo lo que puede hacer es regístrelo y pase a la siguiente solicitud).

Considero que los tipos de suma son más útiles para el interior de un proceso cuando sé que tengo X casos con los que sé que debo lidiar. Para X pequeño no es difícil de manejar, pero, para X grande, agradezco que el compilador me grite, especialmente al refactorizar.

A través de los límites de la API, los casos de uso son menos, y uno siempre debe errar por el lado de la extensibilidad, pero a veces tienes algo que realmente solo puede ser una de X cosas, como con un AST o ejemplos más triviales como un "día de el valor de la semana" en el que el rango está prácticamente establecido en este punto (hasta la elección del sistema calendárico).

@jimmyfrasche Podría darte el Día de la semana, pero no el AST. Las gramáticas evolucionan. Lo que podría no ser válido hoy, podría ser totalmente válido mañana y eso podría implicar agregar nuevos tipos de nodos al AST. Con los tipos de suma verificados por el compilador, esto no sería posible sin roturas.

Y no veo por qué esto no puede resolverse simplemente con una revisión veterinaria; brindándole una verificación estática perfectamente adecuada de casos exhaustivos y brindándome la posibilidad de reparaciones graduales.

Estoy jugando con la implementación de un cliente para una API de servidor. Algunos de los argumentos y valores devueltos son enumeraciones en la API. Hay 45 tipos de enumeración en total.

En mi caso, no es factible usar constantes enumeradas en Go, ya que algunos de los valores para diferentes tipos de enumeración comparten el mismo nombre. En el siguiente ejemplo, Destroy aparece dos veces, por lo que el compilador emitirá el error Destroy redeclared in this block .

type TaskAllowedOperations int
const (
    _ TaskAllowedOperations = iota
    Cancel
    Destroy
)

type OnNormalExit int
const (
    _ OnNormalExit = iota
    Destroy
    Restart
)

Por lo tanto, necesitaré encontrar una representación diferente. Idealmente, uno que permita que un IDE muestre los valores posibles para un tipo dado para que los usuarios del cliente tengan más facilidad para usarlo. Tener enum como ciudadano de primera clase en Go satisfaría eso.

@kongslund Sé que no es una implementación perfecta, pero acabo de crear un generador de código que podría interesarle. Solo requiere que declare su enumeración en un comentario sobre la declaración de tipo y generará el resto para usted.

// ENUM(_, Cancel, Destroy)
type TaskAllowedOperations int

// ENUM(_, Destroy, Restart)
type OnNormalExit int

generaría

const(
  _ TaskAllowedOperations = iota
  TaskAllowedOperationsCancel
  TaskAllowedOperationsDestroy
)

const(
  _ OnNormalExit = iota
  OnNormalExitDestroy
  OnNormalExitRestart
)

La mejor parte es que generaría métodos String() que excluyen el prefijo en ellos, permitiéndole analizar "Destroy" como TaskAllowedOperations o OnNormalExit .

https://github.com/abice/go-enum

Ahora que el enchufe está fuera del camino...

Personalmente, no me importa que las enumeraciones no se incluyan como parte del lenguaje go, que no era mi opinión original sobre el asunto. Cuando venía por primera vez, a menudo tenía una reacción confusa sobre por qué se tomaron tantas decisiones. Pero después de usar el lenguaje, es bueno tener la simplicidad a la que se adhiere, y si se necesita algo adicional, es muy probable que alguien más lo haya necesitado también y haya creado un paquete increíble para ayudar con ese problema en particular. Manteniendo la cantidad de cruft a mi discreción.

Se han planteado muchos puntos válidos en esta discusión, algunos a favor del soporte de enumeración y también muchos en contra (al menos en la medida en que la propuesta decía algo sobre lo que son "enumeraciones" en primer lugar). Algunas cosas que me llamaron la atención:

  • El ejemplo introductorio (Enumeraciones en Go hoy) es engañoso: ese código se genera y casi nadie escribiría un código Go así a mano. De hecho, la sugerencia (cómo se vería con soporte de idiomas) está mucho más cerca de lo que ya hacemos en Go.

  • @jediorange menciona que Swift "realmente hizo (enumeraciones) bien": sea como sea, pero las enumeraciones de Swift son una bestia sorprendentemente complicada, que mezcla todo tipo de conceptos. En Go evitamos deliberadamente mecanismos que se superpusieran con otras características del lenguaje y, a cambio, obtuviéramos más ortogonalidad. La consecuencia para un programador es que no tiene que tomar una decisión sobre qué característica usar: una enumeración o una clase, o un tipo de suma (si los tuviéramos), o una interfaz.

  • El punto de @ianlancetaylor sobre la utilidad de las características del lenguaje no debe tomarse a la ligera. Hay un montón de características útiles; la pregunta es cuáles son realmente convincentes y valen su costo (de complejidad adicional del lenguaje y, por lo tanto, de legibilidad y de implementación).

  • Como punto menor, las constantes definidas por iota en Go, por supuesto, no están restringidas a enteros. Siempre que sean constantes, están restringidas a tipos básicos (posiblemente nombrados) (incluidos flotantes, booleanos, cadenas: https://play.golang.org/p/lhd3jqqg5z).

  • @merovius destaca las limitaciones de las comprobaciones en tiempo de compilación (¡estáticas!). Tengo muchas dudas de que las enumeraciones que no se pueden extender sean adecuadas en situaciones donde la extensión es deseable o esperada (cualquier superficie API de larga duración evoluciona con el tiempo).

Lo que me lleva a algunas preguntas sobre esta propuesta que creo que deben responderse antes de que pueda haber un progreso significativo:

1) ¿Cuáles son las expectativas reales para las enumeraciones propuestas? @bep menciona enumerabilidad, iterabilidad, representaciones de cadenas, identidad. ¿Hay más? hay menos?

2) Asumiendo la lista en 1), ¿se pueden extender las enumeraciones? ¿Si es así, cómo? (¿en el mismo paquete? ¿en otro paquete?) Si no se pueden extender, ¿por qué no? ¿Por qué eso no es un problema en la práctica?

3) Espacio de nombres: en Swift, un tipo de enumeración introduce un nuevo espacio de nombres. Existe una maquinaria significativa (azúcar sintáctico, deducción de tipos) de modo que el nombre del espacio de nombres no tiene que repetirse en todas partes. Por ejemplo, para los valores de enumeración de un mes de enumeración, en el contexto correcto, se puede escribir .Enero en lugar de Mes.Enero (o peor, MiPaquete.Mes.Enero). ¿Se necesita un espacio de nombres de enumeración? Si es así, ¿cómo se extiende un espacio de nombres de enumeración? ¿Qué tipo de azúcar sintáctico se requiere para que esto funcione en la práctica?

4) ¿Son constantes los valores enumerados? ¿Valores inmutables?

5) ¿Qué tipo de operaciones son posibles en los valores de enumeración (por ejemplo, además de la iteración): ¿Puedo mover uno hacia adelante, uno hacia atrás? ¿Requiere funciones u operadores incorporados adicionales? (No todas las iteraciones pueden estar en orden). ¿Qué sucede si uno avanza más allá del último valor enumerado? ¿Es eso un error de tiempo de ejecución?

(He corregido mi redacción del siguiente párrafo en https://github.com/golang/go/issues/19814#issuecomment-322771922. Disculpas por la elección descuidada de las palabras a continuación).

Sin tratar de responder realmente a estas preguntas, esta propuesta no tiene sentido ("Quiero enumeraciones que hagan lo que quiero" no es una propuesta).

Sin tratar de responder realmente a estas preguntas, esta propuesta no tiene sentido.

@griesemer Tiene un gran conjunto de puntos/preguntas, pero etiquetar esta propuesta como insignificante por no responder estas preguntas tiene poco sentido. El listón de la contribución está muy alto en este proyecto, pero debería permitirse _proponer algo_ sin tener un doctorado en compiladores, y no debería ser necesario que una propuesta sea un diseño _listo para implementar_.

  • Go necesitaba esta propuesta ya que inició una discusión muy necesaria => valor y significado
  • Si también lleva a la propuesta #21473 => valor y significado

El ejemplo introductorio (Enumeraciones en Go hoy) es engañoso: ese código se genera y casi nadie escribiría un código Go así a mano. De hecho, la sugerencia (cómo se vería con soporte de idiomas) está mucho más cerca de lo que ya hacemos en Go.

@griesemer Tengo que estar en desacuerdo. No debería haber dejado las mayúsculas completas en el nombre de la variable de Go, pero hay muchos lugares donde el código escrito a mano se ve casi idéntico a mi sugerencia, escrito por Googlers a quienes respeto en la comunidad de Go. Seguimos el mismo patrón en nuestra base de código con bastante frecuencia. Este es un ejemplo extraído de la biblioteca de Google Cloud Go.

// ACLRole is the level of access to grant.
type ACLRole string

const (
    RoleOwner  ACLRole = "OWNER"
    RoleReader ACLRole = "READER"
    RoleWriter ACLRole = "WRITER"
)

Usan la misma construcción en múltiples lugares.
https://github.com/GoogleCloudPlatform/google-cloud-go/blob/master/bigquery/table.go#L78 -L116
https://github.com/GoogleCloudPlatform/google-cloud-go/blob/master/storage/acl.go#L27 -L49

Hubo una discusión más tarde sobre cómo puede hacer las cosas más concisas si está de acuerdo con iota , que puede ser útil por derecho propio, pero para un caso de uso limitado. Ver mi comentario anterior para más detalles. https://github.com/golang/go/issues/19814#issuecomment-290948187

@bep Punto justo; Me disculpo por mi elección descuidada de palabras. Permítanme intentarlo de nuevo, con suerte redactando mi último párrafo anterior de manera más respetuosa y clara esta vez:

Para poder lograr un progreso significativo, creo que los proponentes de esta propuesta deberían tratar de ser un poco más precisos sobre lo que creen que son características importantes de las enumeraciones (por ejemplo, respondiendo algunas de las preguntas en https://github. com/golang/go/issues/19814#issuecomment-322752526). De la discusión hasta ahora, las características deseadas solo se describen de manera bastante vaga.

Tal vez como primer paso, sería realmente útil tener estudios de casos que muestren cómo Go existente se queda (significativamente) corto y cómo las enumeraciones resolverían un problema mejor/más rápido/más claro, etc. Vea también la excelente charla de @rsc en Gophercon con respecto a los cambios de idioma de Go2.

@derekperkins Llamaría a esas definiciones constantes (escritas), no enumeraciones. Supongo que nuestro desacuerdo se debe a una comprensión diferente de lo que se supone que es una "enumeración", de ahí mis preguntas anteriores.

(Mi https://github.com/golang/go/issues/19814#issuecomment-322774830 anterior debería haber ido a @derekperkins , por supuesto, no a @ derekparker. Autocompletar me derrotó).

A juzgar por el comentario de @derekperkins , y respondiendo parcialmente a mis propias preguntas, deduzco que una "enumeración" de Go debe tener al menos las siguientes cualidades:

  • capacidad de agrupar un conjunto de valores bajo un (nuevo) tipo
  • facilitar la declaración de nombres (y los valores correspondientes, si los hay) con ese tipo, con una mínima sobrecarga sintáctica o repetitivo
  • capacidad de iterar a través de estos valores en orden de declaración ascendente

¿Eso suena bien? Si es así, ¿qué más se necesita agregar a esta lista?

Tus preguntas son todas buenas.

¿Cuáles son las expectativas reales para las enumeraciones propuestas? @bep menciona enumerabilidad, iterabilidad, representaciones de cadenas, identidad. ¿Hay más? hay menos?

Asumiendo la lista en 1), ¿se pueden extender las enumeraciones? ¿Si es así, cómo? (¿en el mismo paquete? ¿en otro paquete?) Si no se pueden extender, ¿por qué no? ¿Por qué eso no es un problema en la práctica?

No creo que las enumeraciones se puedan extender por dos razones:

  1. Las enumeraciones deben representar el rango completo de valores aceptables, por lo que extenderlas no tiene sentido.
  2. Al igual que los tipos de Go normales no se pueden ampliar en paquetes externos, esto mantiene la misma mecánica y expectativas de los desarrolladores.

Espacio de nombres: en Swift, un tipo de enumeración introduce un nuevo espacio de nombres. Existe una maquinaria significativa (azúcar sintáctico, deducción de tipos) de modo que el nombre del espacio de nombres no tiene que repetirse en todas partes. Por ejemplo, para los valores de enumeración de un mes de enumeración, en el contexto correcto, se puede escribir .Enero en lugar de Mes.Enero (o peor, MiPaquete.Mes.Enero). ¿Se necesita un espacio de nombres de enumeración? Si es así, ¿cómo se extiende un espacio de nombres de enumeración? ¿Qué tipo de azúcar sintáctico se requiere para que esto funcione en la práctica?

Entiendo cómo surgió el espacio de nombres, ya que todos los ejemplos que mencioné prefijan el nombre del tipo. Si bien no me opondría si alguien se sintiera convencido de agregar espacios de nombres, creo que está fuera del alcance de esta propuesta. Los prefijos encajan perfectamente en el sistema actual.

¿Los valores de enumeración son constantes? ¿Valores inmutables?

Yo pensaría constantes.

¿Qué tipo de operaciones son posibles en los valores de enumeración (por ejemplo, además de la iteración): ¿Puedo mover uno hacia adelante, uno hacia atrás? ¿Requiere funciones u operadores incorporados adicionales? (No todas las iteraciones pueden estar en orden). ¿Qué sucede si uno avanza más allá del último valor enumerado? ¿Es eso un error de tiempo de ejecución?

Predeterminaría las prácticas estándar de Go para cortes/matrices (no mapas). Los valores de enumeración serían iterables según el orden de declaración. Como mínimo, habría soporte de rango. Me inclino por dejar que se acceda a las enumeraciones a través del índice, pero no me siento muy convencido al respecto. No admitir eso debería eliminar el posible error de tiempo de ejecución.

Habría un nuevo error de tiempo de ejecución (¿pánico?) causado por la asignación de un valor no válido a una enumeración, ya sea mediante asignación directa o conversión de tipos.

Si resumo esto correctamente, los valores de enumeración que usted propone son como constantes escritas (y como constantes, pueden tener valores constantes definidos por el usuario), pero:

  • también definen el tipo de enumeración asociado con los valores de enumeración (de lo contrario, son solo constantes) en la misma declaración
  • es imposible convertir/crear un valor de enumeración de un tipo de enumeración existente fuera de su declaración
  • es posible iterar sobre ellos

¿Eso suena bien? (Esto coincidiría con el enfoque clásico que los lenguajes han adoptado hacia las enumeraciones, iniciado hace unos 45 años por Pascal).

Sí, eso es exactamente lo que estoy proponiendo.

¿Qué pasa con las declaraciones de cambio? AIUI que es uno de los principales impulsores de la propuesta.

Creo que poder activar una enumeración está implícito, ya que puedes activar básicamente cualquier cosa. Me gusta que Swift tenga errores si no ha satisfecho completamente la enumeración en su cambio, pero eso podría ser manejado por un veterinario

@jediorange Me refería específicamente a la cuestión de la última parte, de si debería haber o no una verificación exhaustiva (en aras de mantener la propuesta completa). "No" es, por supuesto, una respuesta perfectamente correcta.

El mensaje original de este número menciona protobufs como motivador. Me gustaría mencionar explícitamente que con la semántica dada ahora, el compilador protobuf necesitaría crear un caso adicional "no reconocido" para cualquier enumeración (lo que implica algún esquema de cambio de nombre para evitar colisiones). También necesitaría agregar un campo adicional a cualquier estructura generada usando enumeraciones (nuevamente, alterando los nombres de alguna manera), en caso de que el valor de enumeración decodificado no esté en el rango compilado. Al igual que se hace actualmente para Java . O, probablemente más probable, continúe usando int s.

@Merovius Mi propuesta original mencionaba protobufs como ejemplo, no como el principal motivador de la propuesta. Mencionas un buen punto sobre esa integración. Creo que probablemente debería tratarse como una preocupación ortogonal. La mayoría del código que he visto se convierte de los tipos de protobuf generados en estructuras de nivel de aplicación, prefiriendo usarlas internamente. Para mí, tendría sentido que protobuf pudiera continuar sin cambios, y si los creadores de la aplicación quieren convertirlos en una enumeración de Go, podrían manejar los casos extremos que usted menciona en el proceso de conversión.

@derekperkins Algunas preguntas más:

  • ¿Cuál es el valor cero para una variable de tipo enumeración que no se inicializa explícitamente? Supongo que no puede ser cero en general (lo que complica la asignación/inicialización de memoria).

  • ¿Podemos hacer aritmética limitada con valores enumerados? Por ejemplo, en Pascal (en el que programé una vez, hace mucho tiempo), sorprendentemente, a menudo era necesario iterar en pasos> 1. Y a veces uno quería calcular el valor de enumeración.

  • Con respecto a la iteración, ¿por qué no es lo suficientemente bueno un soporte de iteración producida (y stringify)?

¿Cuál es el valor cero para una variable de tipo enumeración que no se inicializa explícitamente? Supongo que no puede ser cero en general (lo que complica la asignación/inicialización de memoria).

Como mencioné en la propuesta inicial, esta es una de las decisiones más difíciles de tomar. Si el orden de definición es importante para la iteración, creo que también tendría sentido que el primer valor definido sea el predeterminado.

¿Podemos hacer aritmética limitada con valores enumerados? Por ejemplo, en Pascal (en el que programé una vez, hace mucho tiempo), sorprendentemente, a menudo era necesario iterar en pasos> 1. Y a veces uno quería calcular el valor de enumeración.

Ya sea que esté utilizando enumeraciones numéricas o basadas en cadenas, ¿eso significa que todas las enumeraciones tienen un índice implícito basado en cero? La razón por la que mencioné antes que me inclino por iteraciones range compatibles únicamente y no basadas en índices, es que no expone la implementación subyacente, que podría usar una matriz o un mapa o lo que sea debajo. No anticipo la necesidad de acceder a las enumeraciones a través del índice, pero si tiene razones por las que eso sería beneficioso, no creo que haya una razón para no permitirlo.

Con respecto a la iteración, ¿por qué no es lo suficientemente bueno un soporte de iteración producida (y stringify)?

Personalmente, la iteración no es mi principal caso de uso, aunque sí creo que agrega valor a la propuesta. Si ese fuera el factor determinante, tal vez go generate sería suficiente. Eso no ayuda a garantizar la seguridad del valor. El argumento Stringer() asume que el valor bruto será iota o int o algún otro tipo que represente el valor "real". También tendría que generar (Un)MarshalJSON , (Un)MarshalBinary , Scanner/Valuer y cualquier otro método de serialización que pueda usar para asegurarse de que el Stringer se usó para comunicarse frente a lo que sea que Go use internamente.

@griesemer Creo que es posible que no haya respondido completamente a su pregunta sobre la extensibilidad de las enumeraciones, al menos en lo que respecta a agregar/eliminar valores. Tener la capacidad de editarlos es parte esencial de esta propuesta.

De @Merovius https://github.com/golang/go/issues/19814#issuecomment -290969864

cualquier paquete que alguna vez quiera cambiar un conjunto de enumeraciones, automáticamente y por la fuerza rompería todos sus importadores

No veo cómo esto es diferente de cualquier otro cambio de API importante. Depende del creador del paquete manejar respetuosamente BC, al igual que si los tipos, las funciones o las firmas de funciones cambiaran.

Desde una perspectiva de implementación, sería bastante complejo admitir tipos cuyo valor predeterminado no fuera todos los bits cero. Hoy en día no existen tales tipos. Requerir tal característica tendría que contar como una marca en contra de esta idea.

La única razón por la que el lenguaje requiere make para crear un canal es para preservar esta función para los tipos de canales. De lo contrario make podría ser opcional, solo se usa para establecer el tamaño del búfer del canal o para asignar un nuevo canal a una variable existente.

@derekperkins La mayoría de los demás cambios de API se pueden orquestar para una reparación gradual. Realmente recomiendo leer la descripción de Russ Cox, deja muchas cosas muy claras.

Las enumeraciones abiertas (como la construcción actual const+iota) permiten la reparación gradual, (por ejemplo) a) definiendo el nuevo valor sin usarlo, b) actualizando las dependencias inversas para manejar el nuevo valor, c) comenzando a usar el valor. O, si desea eliminar un valor, a) deje de usar el valor, b) actualice las dependencias inversas para no mencionar el valor que se eliminará, c) elimine el valor.

Con enumeraciones cerradas (verificadas por el compilador para que sean exhaustivas), esto no es posible. Si elimina el manejo de un valor o define uno nuevo, el compilador se quejará inmediatamente de que falta un caso de cambio. Y no puede agregar el manejo de un valor antes de definir uno.

La pregunta no es si los cambios individuales se pueden considerar de ruptura (pueden, de forma aislada), sino si hay una secuencia de confirmaciones de no ruptura sobre el código base distribuido que no se rompe.

Desde una perspectiva de implementación, sería bastante complejo admitir tipos cuyo valor predeterminado no fuera todos los bits cero. Hoy en día no existen tales tipos. Requerir tal característica tendría que contar como una marca en contra de esta idea.

@ianlancetaylor Definitivamente no voy a poder hablar sobre la implementación completa, pero si las enumeraciones se implementaron como una matriz basada en 0 (que es lo que parece que @griesemer está a favor), entonces 0 como el índice parece podría duplicarse como "todos los bits cero".

Con enumeraciones cerradas (verificadas por el compilador para que sean exhaustivas), esto no es posible.

@Merovius Si la exhaustividad fuera verificada por go vet o herramientas similares como las sugeridas por @jediorange frente a las impuestas por el compilador, ¿eso aliviaría sus preocupaciones?

@derekperkins Sobre su nocividad, sí. No sobre su falta de utilidad. Los mismos problemas de sesgo de versión también ocurren en la mayoría de los casos de uso para los que generalmente se consideran (llamadas al sistema, protocolos de red, formatos de archivo, objetos compartidos...). Hay una razón por la que proto3 requiere enumeraciones abiertas y proto2 no: es una lección aprendida de muchas interrupciones e incidentes de corrupción de datos. Aunque Google ya es bastante cuidadoso para evitar el sesgo de versión. Desde mi perspectiva, las enumeraciones abiertas con casos predeterminados son solo la solución correcta. Y aparte de la supuesta seguridad contra valores inválidos, en realidad no aportan mucho a la mesa, por lo que puedo decir.

Habiendo dicho todo eso, no soy un decisor.

@derekperkins En https://github.com/golang/go/issues/19814#issuecomment -322818206 está confirmando que (desde su punto de vista):

  • una declaración de enumeración declara un tipo de enumeración junto con valores de enumeración con nombre (constantes)
  • es posible iterar sobre ellos
  • no se pueden agregar valores a la enumeración fuera de su declaración
    Y más adelante: un cambio en las enumeraciones debe ser (o quizás no ser) exhaustivo (parece menos importante)

En https://github.com/golang/go/issues/19814#issuecomment -322895247 estás diciendo que:

  • el primer valor definido probablemente debería ser el valor predeterminado (cero) (tenga en cuenta que esto no importa para la iteración, importa para la inicialización de la variable de enumeración)
  • la iteración no es su principal motivación

Y en https://github.com/golang/go/issues/19814#issuecomment -322903714 está diciendo que "la capacidad de editarlos es una parte importante de esta propuesta".

Estoy confundido: entonces la iteración no es un motivador principal, está bien. Eso deja como mínimo una declaración de enumeración de valores de enumeración que son constantes, y no se pueden extender fuera de la declaración. Pero ahora estás diciendo que la capacidad de editarlos es importante. ¿Que significa eso? Seguramente no que se puedan extender (eso sería una contradicción). ¿Son variables? (Pero entonces no son constantes).

En https://github.com/golang/go/issues/19814#issuecomment -322903714 está diciendo que las enumeraciones podrían implementarse como una matriz basada en 0. Esto sugiere que una declaración de enumeración introduce un nuevo tipo junto con una lista ordenada de nombres de enumeración que son índices constantes basados ​​en 0 en una matriz de valores de enumeración (para los cuales el espacio se reserva automáticamente). ¿Es eso lo que quieres decir? Si es así, ¿por qué no sería suficiente declarar una matriz de tamaño fijo y una lista de índices constantes para acompañarla? La verificación de los límites de la matriz garantizaría automáticamente que no pueda "extender" el rango de enumeración, y la iteración ya sería posible.

¿Qué me estoy perdiendo?

Estoy confundido: entonces la iteración no es un motivador principal, está bien.

Tengo mis propias razones por las que quiero enumeraciones, al mismo tiempo que trato de tener en cuenta lo que otros en este hilo, incluido @bep y otros, han expresado como partes necesarias de la propuesta.

Eso deja como mínimo una declaración de enumeración de valores de enumeración que son constantes, y no se pueden extender fuera de la declaración. Pero ahora estás diciendo que la capacidad de editarlos es importante. ¿Que significa eso? Seguramente no que se puedan extender (eso sería una contradicción). ¿Son variables? (Pero entonces no son constantes).

Cuando digo que los edite, es para el punto de @Merovius que son enumeraciones abiertas. Constantes en el momento de la compilación, pero no bloqueadas para siempre.

En # 19814 (comentario) está diciendo que las enumeraciones podrían implementarse como una matriz basada en 0.

Solo soy yo especulando más allá de mi nivel de pago sobre cómo imagino que podría implementarse tras bambalinas, según su https://github.com/golang/go/issues/19814#issuecomment -322884746 y el https de @ianlancetaylor : //github.com/golang/go/issues/19814#issuecomment -322899668

"¿Podemos hacer aritmética limitada con valores de enumeración? Por ejemplo, en Pascal (en el que programé una vez, mucho tiempo atrás), sorprendentemente, a menudo era necesario iterar en pasos> 1. Y a veces uno quería calcular el valor de enumeración".

No sé cómo planearía hacer eso para cualquier enumeración no entera, de ahí mi pregunta sobre si esa aritmética requeriría que a cada miembro de la enumeración se le asigne implícitamente un índice basado en el orden de declaración.

Desde una perspectiva de implementación, sería bastante complejo admitir tipos cuyo valor predeterminado no fuera todos los bits cero. Hoy en día no existen tales tipos. Requerir tal característica tendría que contar como una marca en contra de esta idea.

Nuevamente, no sé cómo funciona el compilador, así que solo estaba tratando de continuar la conversación. Al final del día, no estoy tratando de proponer nada radical. Como mencionaste antes, "Esto coincidiría con el enfoque clásico que los lenguajes han adoptado hacia las enumeraciones, iniciado hace unos 45 años por Pascal", y eso encaja a la perfección.

Para cualquier otra persona que haya expresado interés, no dude en contribuir.

Otra pregunta es si se pueden usar estas enumeraciones para indexar matrices o segmentos. Un segmento es a menudo una forma muy eficiente y compacta de representar un mapeo de enumeración-> valor y creo que requerir un mapa sería desafortunado.

@derekperkins Ok, me preocupa que eso nos devuelva (o al menos a mí) al punto de partida: ¿Cuál es el problema que estás tratando de resolver? ¿Simplemente quiere una forma más agradable de escribir lo que hacemos actualmente con constantes y tal vez iota (y para lo cual usamos go generar para obtener representaciones de cadenas)? Es decir, ¿algo de azúcar sintáctico para una notación que (quizás) encuentre demasiado pesada? (Esa es una buena respuesta, solo trato de entender).

Mencionaste que tienes tus propias razones para quererlas, tal vez puedas explicar un poco más cuáles son esas razones. El ejemplo que diste al principio no tiene mucho sentido para mí, pero probablemente me estoy perdiendo algo.

Tal como está, todos tienen una comprensión un poco diferente de lo que implica esta propuesta ("enumeraciones"), como quedó claro a partir de las diversas respuestas: hay una gran variedad de posibilidades entre las enumeraciones de Pascal y las enumeraciones de Swift. A menos que usted (o alguien más) describa muy claramente lo que se propone (no estoy pidiendo una implementación, tenga en cuenta), será difícil lograr un progreso significativo o incluso debatir los méritos de esta propuesta.

¿Tiene sentido?

@griesemer Tiene mucho sentido y entiendo que se debe pasar el listón del que @rsc habló en Gophercon. Obviamente tienes una comprensión mucho más profunda de lo que yo jamás tendré. En #21473, mencionó que iota para vars no se implementó porque no había un caso de uso convincente en ese momento. ¿Es esa la misma razón por la que enum no se incluyó desde el principio? Me interesaría mucho saber su opinión sobre si agregaría o no valor a Go, y si lo hiciera, ¿dónde comenzaría el proceso?

@derekperkins Con respecto a su pregunta en https://github.com/golang/go/issues/19814#issuecomment -323144075: En ese momento (en el diseño de Go) solo estábamos considerando enumeraciones relativamente simples (digamos Pascal o estilo C). No recuerdo todos los detalles, pero ciertamente existía la sensación de que no había suficiente beneficio para la maquinaria adicional requerida para las enumeraciones. Sentimos que eran esencialmente declaraciones constantes glorificadas.

También hay problemas con estas enumeraciones tradicionales: es posible hacer aritmética con ellas (son solo números enteros), pero ¿qué significa si van "fuera del rango (enumeración)"? En Go son solo constantes y "fuera de rango" no existe. Otra es la iteración: en Pascal había funciones integradas especiales (creo que SUCC y PRED) para avanzar el valor de una variable de tipo enumeración hacia adelante y hacia atrás (en C uno solo hace ++ o --). Pero el mismo problema también aparece aquí: ¿Qué sucede si uno va más allá del final (problema muy común en un ciclo for que varía sobre valores de enumeración usando ++ o el equivalente de Pascal SUCC). Finalmente, una declaración de enumeración introduce un nuevo tipo, cuyos elementos son los valores de enumeración. Estos valores tienen nombres (los definidos en la declaración de enumeración), pero esos nombres están (en Pascal, C) en el mismo ámbito que el tipo. Esto es un poco insatisfactorio: al declarar dos enumeraciones diferentes, uno esperaría poder usar el mismo nombre de valor de enumeración para cada tipo de enumeración sin conflicto, lo cual no es posible. Por supuesto, Go tampoco resuelve eso, pero una declaración constante tampoco parece estar introduciendo un nuevo espacio de nombres. Una mejor solución es introducir un espacio de nombres con cada enumeración, pero luego, cada vez que se usa un valor de enumeración, debe calificarse con el nombre del tipo de enumeración, lo cual es molesto. Swift resuelve esto deduciendo el tipo de enumeración donde sea posible y luego se puede usar el nombre del valor de enumeración con el prefijo de un punto. Pero eso es bastante maquinaria. Y finalmente, a veces (a menudo, en API públicas), uno necesita extender una declaración de enumeración. Si eso no es posible (no posee el código), hay un problema. Con constantes, esos problemas no existen.

Probablemente haya más; esto es justo lo que me viene a la mente. Al final, decidimos que era mejor emular enumeraciones en Go usando las herramientas ortogonales que ya teníamos: tipos de enteros personalizados que hacen que las asignaciones erróneas sean menos probables, y el mecanismo iota (y la capacidad de dejar de lado las expresiones de inicialización repetidas) para el azúcar sintáctico.

De ahí mis preguntas: ¿Qué busca obtener de las declaraciones de enumeración especializadas que no podemos emular adecuadamente en Go con poca sobrecarga sintáctica? Puedo pensar en la enumeración y un tipo de enumeración que no se puede extender fuera de la declaración. Puedo pensar en más habilidades para los valores de enumeración, como en Swift.

La enumeración podría resolverse fácilmente con un generador de go en Go. Ya tenemos un larguero. Restringir la extensión es problemático a través de los límites de la API. Más habilidades para los valores de enumeración (digamos como en Swift) parece muy diferente a Go porque mezcla muchos conceptos ortogonales. En Go, probablemente lograríamos eso usando bloques de construcción elementales.

@griesemer Gracias por su atenta respuesta. No estoy en desacuerdo en que básicamente son declaraciones constantes glorificadas. Tener seguridad de tipo en Go es excelente, y el valor principal que enum me proporcionaría personalmente es seguridad de valor. La forma de imitar eso en Go hoy es ejecutar funciones de validación en cada punto de entrada para esa variable. Es detallado y hace que sea fácil cometer errores, pero es posible con el lenguaje actual. Ya utilicé el espacio de nombres anteponiendo el nombre del tipo delante de la enumeración, que si bien es detallado, no es gran cosa.

Personalmente, no me gusta la mayoría de los usos de iota . Si bien es genial, la mayoría de las veces mis valores similares a enum se asignan a recursos externos como una base de datos o una API externa, y prefiero ser más explícito en cuanto a que el valor no debe cambiarse si vuelve a ordenar. iota tampoco ayuda en la mayoría de los lugares en los que usaría un enum porque estaría usando una lista de valores de cadena.

Al fin y al cabo, no sé cuánto más pueda aclarar esta propuesta. Me encantaría que fueran apoyados de la manera que tuviera sentido para Go. Independientemente de la implementación exacta, aún podría usarlos y harían que mi código fuera más seguro.

Creo que la forma canónica en que Go hace las enumeraciones hoy (como se ve en https://github.com/golang/go/issues/19814#issuecomment-290909885) está bastante cerca de ser correcta.
Hay algunos inconvenientes:

  1. No se pueden repetir
  2. No tienen representación de cadena.
  3. Los clientes pueden introducir valores de enumeración falsos

Estoy bien sin el #1.
go:generate + stringer se puede usar para #2. Si eso no maneja su caso de uso, haga que el tipo base de su "enumeración" sea una cadena en lugar de un int, y use valores constantes de cadena.

3 es el más difícil de manejar con el Go actual. Tengo una propuesta tonta que podría manejar esto bien.

Agregue una palabra clave explicit a una definición de tipo. Esta palabra clave prohíbe las conversiones a este tipo excepto las conversiones en bloques const en el paquete en el que se define ese tipo. (¿O restricted ? ¿O tal vez enum significa explicit type ?)

Reutilizando el ejemplo que mencioné anteriormente,

//go:generate stringer -type=SearchRequest
explicit type SearchRequest int

const (
    Universal SearchRequest = iota
    Web
    Images
    Local
    News
    Products
    Video
)

Hay conversiones de int a SearchRequest dentro del bloque const . Pero solo el autor del paquete puede introducir un nuevo valor de SearchRequest, y es poco probable que introduzca uno accidentalmente (pasando int a una función que espera SearchRequest , por ejemplo).

Realmente no estoy proponiendo activamente esta solución, pero creo que no construir accidentalmente una no válida es la propiedad más destacada de las enumeraciones que no se pueden capturar en Go hoy (a menos que vaya a la estructura con un ruta de campo no exportada).

Creo que el riesgo interesante con las enumeraciones es con constantes sin tipo. Las personas que escriben una conversión de tipo explícita saben lo que están haciendo. Estaría dispuesto a considerar una forma para que Go prohíba las conversiones de tipos explícitas en ciertas circunstancias, pero creo que es completamente ortogonal a la noción de tipos de enumeración. Es una idea que se aplica a cualquier tipo de tipo.

Pero las constantes sin tipo pueden dar lugar a la creación accidental e inesperada de un valor del tipo, de una manera que no ocurre con las conversiones de tipo explícitas. Así que creo que la sugerencia de @ randall77 de explicit se puede simplificar para que simplemente signifique que las constantes sin tipo no se pueden convertir implícitamente al tipo. Siempre se requiere una conversión de tipo explícita.

Estaría dispuesto a considerar una forma de que Go prohíba las conversiones de tipos explícitos en determinadas circunstancias.

@ianlancetaylor Opcionalmente, rechazar las conversiones de tipos, ya sean explícitas o implícitas, resolvería los problemas que me llevaron a crear esta propuesta en primer lugar. Solo el paquete de origen podría crear y, por lo tanto, satisfacer cualquier tipo. Eso es incluso mejor que la solución enum en algunos aspectos porque no solo admite declaraciones const , sino cualquier tipo.

@randall77 , @ianlancetaylor ¿Cómo haría que la sugerencia explicit funcionara junto con el valor cero de ese tipo?

@derekperkins No permitir las conversiones de tipos por completo hará que sea imposible usar estos tipos en codificadores/descodificadores genéricos, como los paquetes encoding/* .

@Merovius Creo que @ianlancetaylor sugiere la restricción solo para conversiones implícitas (por ejemplo, asignación de una constante sin tipo a un tipo restringido). La conversión explícita aún sería posible.

@griesemer Lo sé :) Pero entendí que @derekperkins sugiriera algo diferente.

¿Permitir conversiones explícitas no socava la razón por la que estamos pensando en este calificador "explícito"? Si alguien puede decidir convertir un valor arbitrario en un tipo "explícito", entonces no tenemos más garantías de que un valor dado es una de las constantes enumeradas que las que tenemos ahora.

Supongo que ayuda para el uso casual o no intencionado de constantes sin tipo, que es quizás lo más importante.

Supongo que me estoy preguntando si rechazar conversiones explícitas está en "el espíritu de Go". No permitir conversiones explícitas es dar un gran paso hacia la programación basada en tipos en lugar de la programación basada en la escritura de código. Creo que Go está tomando una posición clara a favor de este último.

@griesemer @Merovius Volveré a publicar la cita de @ianlancetaylor , ya que fue su sugerencia, no la mía.

Estaría dispuesto a considerar una forma de que Go prohíba las conversiones de tipos explícitos en determinadas circunstancias.

Tanto @rogpeppe como @Merovius mencionan buenos puntos sobre las ramificaciones. Permitir conversiones explícitas pero no conversiones implícitas no resuelve el problema de garantizar tipos válidos, pero perder la codificación genérica sería una desventaja bastante grande.

Ha habido mucho de ida y vuelta aquí, pero creo que ha habido algunas buenas ideas. Aquí hay un resumen de lo que me gustaría ver (o algo similar), que parece alinearse con lo que otros han dicho. Admito abiertamente que no soy diseñador de lenguajes ni programador de compiladores, así que no sé qué tan bien funcionaría.

  1. Enumeraciones enraizadas solo en tipos base (cadena, uint, int, runa, etc.). Si no es necesario un tipo base, ¿podría ser uint por defecto?
  2. Todos los valores válidos de la enumeración deben declararse con la declaración de tipo -- Constants. Los valores no válidos (no declarados en la declaración de tipo) no se pueden convertir al tipo de enumeración.
  3. Representación automática de cadenas para la depuración (agradable tener).
  4. Comprueba en tiempo de compilación la exhaustividad de las declaraciones de cambio en la enumeración. Opcionalmente, recomiende (a través go vet ?) un caso default , incluso cuando ya sea exhaustivo (probablemente un error) para cambios futuros.
  5. El valor cero debería ser esencialmente inválido (no algo en la declaración de enumeración). Personalmente, me gustaría que fuera nil , como lo hace una rebanada.

Ese último _puede_ ser un poco controvertido. Y no sé con certeza si eso funcionaría, pero creo que encajaría semánticamente, al igual que uno buscaría una porción nil , uno podría poner cheques por nil valor de enumeración.

En cuanto a la iteración, realmente no creo que la use, pero no veo el daño en ella.

Como ejemplo de cómo se podría declarar:

type MetadataBlockType enum[uint] {
    StreamInfo:    0
    Padding:       1
    Application:   2
    SeekTable:     3
    VorbisComment: 4
    CueSheet:      5
    Picture:       6
}

Además, el estilo Swift de inferir el tipo y usar la "sintaxis de puntos" sería _bueno_, pero definitivamente no es necesario.

escriba EnumA int
constante (
Desconocido EnumA = iota
AAA
)


escriba EnumB int
constante (
Desconocido EnumB = iota
BBB
)

No pueden existir 2 piezas de código en un solo archivo Go, ni el mismo paquete, ni siquiera uno se importa de otro paquete.

Simplemente implemente la forma C# de implementar Enum:
type Days enum {Sábado, Domingo, Lunes, Martes, Miércoles, Jueves, Viernes}
type Days enum[int] { Sat:1 , Sun, Tue, Wed, Thu, Vie}
type Days enum[string] { Sáb: "Saturay", Dom: "Domingo", etc.}

@KamyarM ¿Cómo es eso mejor que

type Days int
const (
  Sat Days = 1+iota
  Sun
  ...
)

y

type Days string
const (
  Sat Days = "Saturday"
  Sun      = "Sunday"
  ...
)

Me gustaría solicitar amablemente restringir los comentarios a nuevos enfoques/argumentos. Muchas personas están suscritas a este hilo y agregar ruido/repetición puede percibirse como una falta de respeto por su tiempo y atención. Hay mucha discusión allí arriba ↑, incluidas respuestas detalladas a los dos comentarios anteriores. No estará de acuerdo con todo lo dicho y es posible que a ninguna de las partes le gusten los resultados de esa discusión hasta el momento, pero simplemente ignorarla tampoco ayudará a avanzar en una dirección productiva.

Es mejor porque no tiene problemas de conflicto de nombres. También admite la verificación de tipos de compilador. El enfoque que mencionó lo organizó mejor que nada, pero el compilador no lo restringe en lo que puede asignarle. Puede asignar un número entero que no sea ninguno de los días a un objeto de ese tipo:
var a Días
un = 10
el compilador en realidad no hace nada al respecto. Así que no tiene mucho sentido este tipo de enumeración. aparte de que está mejor organizado en IDE como GoLand

me gustaria ver algo asi

type WeekDay enum string {
  Monday "mon"
  Tuesday "tue"
  // etc...
}

O con uso automático iota :

// this assumes that iota automatically assigned to the first declared enum key
// and values have unsigned integer type
// value is positional, so if you decide to rename your key 
// you don't have to change everything in db
// also it is easy to grow your lists
type WeekDay enum {
  Monday
  Tuesday
}

Esto proporcionará simplicidad y facilidad de uso:

func makeItWorkOn(day WeekDay) {
  // your implementation
}

Además, enum debe tener un método incorporado para validar el valor para que podamos validar algo de la entrada del usuario:

if day in WeekDay {
  makeItWorkOn(day)
}

Y cosas simples como:

if day == WeekDay.Monday {
 // whatever
}

Para ser honesto, mi sintaxis favorita sería así (KISS):

// type automatically inferred from values or `iota`
enum WeekDay {
  Monday "mon"
  Tuesday "tue"
}

@zoonman El último ejemplo no sigue el siguiente principio de Go: una declaración de función comienza con func , una declaración de tipo comienza con type , una declaración de variable comienza con var , ...

@ md2perpe No estoy tratando de seguir los principios de "tipo" de Go, estoy escribiendo código todos los días y el único principio que sigo es mantener las cosas simples.
Si tiene que escribir más código para seguir los principios, se pierde más tiempo.
Para ser sincero, soy novato en Go, pero hay muchas cosas que puedo criticar.
Por ejemplo:

struct User {
  Id uint
  Email string
}

Es más fácil de escribir y entender que

type User struct {
  Id uint
  Email string
}

Puedo darte un ejemplo donde se debe usar el tipo:

// this is terrible because it blows your mind off
// especially if you Go newbie
// this should not be allowed by compiler/linter:
w := map[string]interface{}{"type": 0, "connected": true}

// it has to be splitted into type definition
type WeirdJSON map[string]interface{}

// and used like
w := WeirdJSON{"type": 0, "connected": true}

Solía ​​escribir código en Asm, C, C++, Pascal, Perl, PHP, Ruby, Python, JavaScript, TypeScript, ahora Go. Vi todo eso. Esta experiencia me dice que el código debe ser lacónico, fácil de leer y entender .

Hago un proyecto de aprendizaje automático y necesito analizar el archivo MIDI.
Allí necesito analizar el código de tiempo SMPTE. Encuentro bastante difícil usar la forma idiomática con iota, pero no me detiene)

const (
        SMTPE0 int8 = ((-24 - (1 + (iota - 1) * 3) % 6 * (iota - 1) / ((iota - 1) | 0x01)) - 10 * ((iota - 1) % 2) - 5 * (iota / 3 - iota / 4) ) * iota / (iota | 0x01)
    SMTPE24 
    SMTPE25
    SMTPE29
    SMTPE30
)
const (
   _SMTPE0 int8 = 0 
   _SMTPE24 int8 = -24
   _SMTPE25 int8 = -25
   _SMTPE29 int8 = -29
   _SMTPE30 int8 = -30
)

Por supuesto, es posible que necesite una verificación de tiempo de ejecución con programación defensiva ...

func IsSMTPE(status int8) bool {
    j := 4
    for i:= 0; i >= -30; i -= j % 6{
        if i == int(status){ 
            return true
        }
        j+=3
    }

    return status == 0
}

PlayGroundRef

Las enumeraciones hacen que la vida de los programadores sea, en algunos casos, más simple. Enums es solo un instrumento, entonces si lo usa correctamente, puede ahorrar tiempo y aumentar la productividad. Creo que no hay problemas para implementar esto en Go 2 como en C++, C# u otros idiomas. Este ejemplo es solo una broma, pero muestra claramente el problema.

@ streeter12 No veo cómo su ejemplo "muestra claramente el problema". ¿Cómo harían las enumeraciones que este código fuera mejor o más seguro?

Hay una clase C# con implementación de la misma lógica de enumeración.

 public enum SMTPE : sbyte
   {
        SMTPE0 = 0,
        SMTPE24 = -24,
        SMTPE25 = -25,
        SMTPE29 = -29,
        SMTPE30 = -30
   }

   public class TestClass
   {
        public readonly SMTPE smtpe;

        public TestClass(SMTPE smtpe)
        {
            this.smtpe = smtpe;
        }
   } 

Con enumeraciones de tiempo de compilación puedo:

  1. No tener comprobaciones de tiempo de ejecución.
  2. Reducción significativa de la posibilidad de error por parte del equipo (no puede pasar un valor incorrecto en el tiempo de compilación).
  3. No se contradice con el concepto iota.
  4. Es más fácil de entender la lógica que tener un nombre para las constantes (es importante que las constantes representen algunos valores de protocolo de bajo nivel).
  5. Puede hacer que el método ToString() sea analógico para hacer una representación simple de valores. (CONNECTION_ERROR.NO_INTERNET es mejor que 0x12). Sé sobre stringer, pero no hay generación de código explícito con enumeraciones.
  6. En algunos idiomas, puede obtener una matriz de valores, rango, etc.
  7. Es fácil de entender mientras lee el código (no necesita cálculos en la cabeza).

Después de todo, es solo una herramienta para prevenir algunos errores humanos comunes y ahorrar rendimiento.

@ streeter12 Gracias por aclarar lo que querías decir. La única ventaja sobre las constantes Go aquí es que no se puede introducir un valor no válido porque el sistema de tipo no aceptará ningún otro valor que no sea uno de los valores enumerados. Ciertamente es bueno tenerlo, pero también tiene un precio: no hay forma de extender esta enumeración fuera de ese código. La extensión de enumeración externa es una de las razones clave por las que en Go nos decidimos por las enumeraciones estándar.

Responda simplemente la necesidad de hacer que algunas extensiones no usen enumeraciones.
FE necesita hacer que la máquina de estado use un patrón de estado en lugar de enumeraciones.

Las enumeraciones tienen su propio alcance. Completo algunos proyectos grandes sin ninguna enumeración. Creo que es una decisión de arquitectura terrible extender la enumeración fuera del código de definición. No tiene control sobre lo que hace su colega y comete algunos errores divertidos)

Y olvidó las enumeraciones de factores humanos en muchos casos, lo que reduce significativamente los errores en proyectos grandes.

@ streeter12 Desafortunadamente, la realidad es que a menudo las enumeraciones deben extenderse.

@griesemer que extiende un tipo de enumeración/suma crea un tipo separado y, a veces, incompatible.

Esto sigue siendo cierto en Go a pesar de que no hay tipos explícitos para enumeraciones/sumas. Si tiene un "tipo de enumeración" en un paquete que espera valores en {1, 2, 3} y le pasa un 4 de su "tipo de enumeración extendida", aún ha violado el contrato del "tipo" implícito.

Si necesita extender una enumeración/suma, también necesita crear funciones de conversión explícitas a/desde que manejen explícitamente los casos a veces incompatibles.

Creo que la desconexión entre ese argumento y las personas para esta propuesta o propuestas similares como # 19412 es que creemos que es extraño que la compensación sea "siempre escriba el código de validación básico que el compilador podría manejar" en lugar de "a veces escriba funciones de conversión que usted' probablemente también tenga que escribir de todos modos".

Eso no quiere decir que ninguna de las partes esté bien o mal o que esa sea la única compensación a considerar, pero quería identificar un cuello de botella en la comunicación entre las partes que he notado.

Creo que la desconexión entre ese argumento y las personas para esta propuesta o propuestas similares como # 19412 es que creemos que es extraño que la compensación sea "siempre escriba el código de validación básico que el compilador podría manejar" en lugar de "a veces escriba funciones de conversión que usted' probablemente también tenga que escribir de todos modos".

muy bien dicho

@jimmyfrasche No es así como yo personalmente describiría la compensación. Yo diría que es "siempre escriba el código de validación básico que el compilador podría manejar" frente a "agregar un concepto completamente nuevo al sistema de tipos que todos los que usan Go deben aprender y comprender".

O, déjame ponerlo de otra manera. Por lo que puedo decir, las únicas características significativas que faltan en la versión de tipos enumerados de Go es que no hay validación de asignación de constantes sin tipo, no hay verificación de conversiones explícitas y no hay verificación de que todos los valores se manejaron en un cambiar. Me parece que esas características son todas independientes de la noción de tipos enumerados. No debemos permitir que el hecho de que otros lenguajes tengan tipos enumerados nos lleve a la conclusión de que Go también necesita tipos enumerados. Sí, los tipos enumerados nos darían esas características que faltan. Pero, ¿es realmente necesario agregar un tipo de tipo completamente nuevo para obtenerlos? ¿Vale la pena el aumento de la complejidad del lenguaje por los beneficios?

@ianlancetaylor Agregar complejidad al idioma es ciertamente algo válido a considerar, y "porque otro idioma lo tiene" ciertamente no es un argumento. Yo, personalmente, no creo que los tipos de enumeración valgan la pena por sí solos. (Sin embargo, su generalización, tipos de suma, seguro marcan muchas casillas para mí).

Una forma general para que un tipo opte por la asignabilidad sería bueno, aunque no estoy seguro de cuán útil sería eso fuera de las primitivas.

No estoy seguro de cuán generalizable es el concepto de "verificar todos los valores manejados en un interruptor", sin alguna forma de informar al compilador la lista completa de valores legales. Además de los tipos de enumeración y suma, lo único que se me ocurre es algo así como los tipos de rango de Ada, pero esos no son naturalmente compatibles con los valores cero a menos que 0 deba estar en el rango o el código se genere para manejar las compensaciones cada vez que se convierten o reflejan sobre. (Otros idiomas han tenido familias de tipos similares, algunos en la familia pascal, pero el de Ada es el único que me viene a la mente en este momento)

De todos modos, me refería específicamente a:

La única ventaja sobre las constantes Go aquí es que no se puede introducir un valor no válido porque el sistema de tipo no aceptará ningún otro valor que no sea uno de los valores enumerados. Ciertamente es bueno tenerlo, pero también tiene un precio: no hay forma de extender esta enumeración fuera de ese código. La extensión de enumeración externa es una de las razones clave por las que en Go nos decidimos por las enumeraciones estándar.

y

Desafortunadamente, la realidad es que a menudo es necesario ampliar las enumeraciones.

Ese argumento no funciona para mí por las razones que expongo.

@jimmyfrasche Entendido; es un problema dificil Es por eso que en Go no intentamos resolverlo, sino que solo brindamos un mecanismo para crear fácilmente secuencias de constantes sin la necesidad de repetir el valor constante.

(Enviado retrasado: se pensó como respuesta a https://github.com/golang/go/issues/19814#issuecomment-349158748)

De hecho, @griesemer y definitivamente fue la decisión correcta para Go 1, pero vale la pena reevaluar parte de eso para Go 2.

Hay suficiente en el lenguaje para obtener _casi_ todo lo que uno quisiera de los tipos de enumeración. Requiere más código que una definición de tipo, pero un generador podría manejar la mayor parte y le permite definir tanto o tan poco como se ajuste a la situación en lugar de simplemente obtener los poderes que vienen con un tipo de enumeración.

Este enfoque https://play.golang.org/p/7ud_3lrGfx te ofrece todo excepto

  1. seguridad dentro del paquete definitorio
  2. la capacidad de pelusa un interruptor para la integridad

Ese enfoque también se puede usar para tipos de suma pequeños y simples†, pero es más complicado de usar, por lo que creo que algo como https://github.com/golang/go/issues/19412#issuecomment -323208336 se agregaría a el lenguaje y podría ser utilizado por un generador de código para crear tipos de enumeración que eviten los problemas 1 y 2.

† consulte https://play.golang.org/p/YFffpsvx5e para obtener un boceto de json.Token con esta construcción

Creemos que es extraño que la compensación sea "siempre escriba el código de validación básico que el compilador podría manejar" en lugar de "a veces escriba funciones de conversión que probablemente también tendrá que escribir de todos modos".

Para mí, un representante del campo de los defensores feroces de la reparación gradual , esto parece la compensación correcta (más o menos). Honestamente, incluso si no estamos hablando de una reparación gradual, lo consideraría un mejor modelo mental.

Por un lado, la enumeración con verificación de tipo solo podrá verificar los valores insertados en el código fuente de todos modos. Si la enumeración viaja a través de una red, se conserva en el disco o se intercambia entre procesos, todas las apuestas están canceladas (y la mayoría de los usos propuestos de las enumeraciones entran en esta categoría). Por lo tanto, no evitará el problema de manejar las incompatibilidades en tiempo de ejecución de todos modos. Y no existe un comportamiento predeterminado general de talla única para cuando encuentre un valor de enumeración no válido. A menudo, es posible que desee salir por error. A veces, es posible que desee convertirlo en un valor predeterminado. La mayoría de las veces desea conservarlo y pasarlo, para que no se pierda en la re-serialización.

Por supuesto, podría argumentar que todavía debe haber un límite de confianza, donde se verifica la validez y se implementa el comportamiento requerido, y todo dentro de ese límite debería poder confiar en ese comportamiento. Y el modelo mental parece ser que este límite de confianza debe ser un proceso. Porque todo el código en un binario se cambiará atómicamente y permanecerá internamente consistente. Pero ese modelo mental se ve erosionado por la idea de reparación gradual; De repente, el límite de confianza natural se convierte en un paquete (o tal vez un depósito) como las unidades a las que aplica sus reparaciones atómicas y la unidad en la que confía para que sea autoconsistente.

Y, personalmente, encuentro que es una unidad muy natural y excelente de autoconsistencia. Un paquete debe ser lo suficientemente grande como para mantener su semántica, reglas y convenciones en su cabeza. Por eso también las exportaciones funcionan a nivel de paquete, no a nivel de tipo, y por qué las declaraciones de nivel superior tienen un alcance a nivel de paquete, no a nivel de programa. Me parece bien y ahorra lo suficiente para decidir el manejo correcto de valores de enumeración desconocidos también a nivel de paquete. Tener una función no exportada, que la verifique y mantenga el comportamiento deseado internamente.

Estaría mucho más de acuerdo con una propuesta de que cada cambio necesita un caso predeterminado, que con una propuesta de tener enumeraciones con verificación de tipos que incluyan verificaciones exhaustivas.

@Merovius El proceso del sistema operativo y el paquete son límites de confianza, como usted dice.

La información que proviene del proceso debe validarse en su ingreso y descomponerse en una representación adecuada para el proceso y debe tomarse el cuidado apropiado cuando falla. Eso nunca desaparece. Realmente no veo nada específico para los tipos de suma/enumeración allí. Podría decir lo mismo sobre las estructuras: a veces obtiene campos adicionales o muy pocos campos. Las estructuras siguen siendo útiles.

Dicho esto, con el tipo de enumeración, por supuesto, puede incluir casos específicos para modelar estos errores. Por ejemplo

type FromTheNetwork enum {
  // pretend all the "valid" values are listed here
  Missing // the value was not included in the message
  Unknown // the value was not in the set of the valid values
  Error // there was an error attempting to read the value
}

y con los tipos de suma puedes ir más allá:

type FromTheNetwork pick {
  Missing struct{} // Not included in message
  Valid somepkg.TheSumBeingReceived // Include valid states with composition
  Unknown []byte // Raw bytes of unknown value received
  Error error // The error from attempting to read the value
}

(El primero no es tan útil a menos que se mantenga en una estructura con campos específicos para los casos de error, pero entonces la validez de esos campos depende del valor de la enumeración. El tipo de suma se encarga de eso, ya que es esencialmente un estructura que solo puede tener un campo establecido a la vez).

En el nivel del paquete, aún necesita manejar la validación de alto nivel, pero la validación de bajo nivel viene con el tipo. Diría que reducir el dominio del tipo ayuda a mantener el paquete pequeño y en tu cabeza. También hace que la intención sea más clara para las herramientas, de modo que su editor pueda escribir todas las líneas case X: y dejar que usted complete el código real o se puede usar un linter para asegurarse de que todo el código esté verificando todos los casos (usted me disuadió de tener la exhaustividad en el compilador anterior).

Realmente no veo nada específico para los tipos de suma/enumeración allí. Podría decir lo mismo sobre las estructuras: a veces obtiene campos adicionales o muy pocos campos. Las estructuras siguen siendo útiles.

Si estamos hablando de enumeraciones abiertas (como las que actualmente construye iota), entonces, seguro. Si estamos hablando de enumeraciones cerradas (que es de lo que la gente suele hablar cuando habla de enumeraciones) o enumeraciones con controles exhaustivos, entonces ciertamente son especiales. Porque no son extensibles.

La analogía con las estructuras explica esto bastante perfectamente: la promesa de compatibilidad de Go 1 excluye los literales de estructura sin clave de cualquier promesa y, por lo tanto, el uso de literales de estructura con clave ha sido una práctica considerada tan fuertemente como "mejor", que go vet tiene una verificación para ello. La razón es exactamente la misma: si está utilizando literales de estructura sin clave, las estructuras ya no son extensibles.

Entonces sí. Las estructuras son exactamente como enumeraciones en este sentido. Y hemos acordado como comunidad que es preferible usarlos de manera extensible.

Dicho esto, con el tipo de enumeración, por supuesto, puede incluir casos específicos para modelar estos errores.

Su ejemplo solo cubre el límite del proceso (al hablar de errores de red), no el límite del paquete. ¿Cómo se comportarán los paquetes si agrego un "Estado interno no válido" (para inventar algo) a FromTheNetwork ? ¿Tengo que arreglar sus interruptores antes de que compilen de nuevo? Entonces no es extensible en el modelo de reparación gradual. ¿Requieren un caso predeterminado para compilar en primer lugar? Entonces no parece haber ningún punto para las enumeraciones.

Nuevamente, tener enumeraciones abiertas es una cuestión diferente. Estaría de acuerdo con cosas como

Diría que reducir el dominio del tipo ayuda a mantener el paquete pequeño y en tu cabeza. También hace que la intención sea más clara para las herramientas, de modo que su editor pueda escribir todo el caso X: líneas y dejar que complete el código real o se puede usar un linter para asegurarse de que todo el código esté verificando todos los casos.

Pero para eso no necesitamos enumeraciones reales, como tipos. Tal herramienta de pelusa también podría verificar heurísticamente const -declaraciones usando iota , donde cada caso es de un tipo determinado y considerar que "una enumeración" y realizar las comprobaciones que desee. Estaría completamente de acuerdo con una herramienta que use estas "enumeraciones por convención" para ayudar a completar automáticamente o aclarar que cada cambio debe tener un valor predeterminado o incluso que cada caso (conocido) debe verificarse. Incluso no me opondría a agregar una palabra clave de enumeración que se comporta principalmente así; es decir, una enumeración está abierta (puede tomar cualquier valor entero), le brinda un alcance adicional y requiere tener un valor predeterminado en cualquier interruptor (no creo que agreguen suficientes enumeraciones de iota para los costos adicionales, pero al menos no dañarían mi agenda). Si eso es lo que se propone, bien. Pero no parece ser lo que la mayoría de los partidarios de esta propuesta (ciertamente no el texto inicial) quieren decir.

No podemos estar de acuerdo sobre la importancia de mantener posible la reparación gradual y la extensibilidad; por ejemplo, mucha gente cree que el control de versiones semántico es una mejor solución para los problemas que resuelve. Pero si los encuentra importantes, es perfectamente válido y razonable considerar las enumeraciones como dañinas o sin sentido. Y esa era la pregunta a la que estaba respondiendo: cómo las personas pueden razonablemente hacer el compromiso de requerir un cheque en todas partes, en lugar de tenerlo en el compilador. Respuesta: Valorando la extensibilidad y la evolución de las API, lo que hace que estas comprobaciones sean necesarias en el sitio de uso de todos modos.

De vez en cuando, los oponentes de enum dijeron que no son expandibles, todavía necesitamos controles después de la serialización/transición, podemos romper la compatibilidad, etc.

El principal problema de que esto no es un problema de enumeración, es su desarrollo y problemas de arquitectura.
Intenta dar un ejemplo en el que el uso de enumeraciones es ridículo, pero consideremos algunas situaciones con más detalle.

Ejemplo 1. Soy un desarrollador de bajo nivel y necesito const para algunas direcciones de registros, establecí valores de protocolo de bajo nivel, etc. Ahora en Go solo tengo una solución: es usar const sin iota, porque en muchos casos sería feo . Puedo obtener varios bloques de constantes para un paquete y luego presionar . Obtuve las 20 constantes y si tienen el mismo tipo y nombres similares, puedo cometer errores. Si el proyecto es grande, obtendrá este error. Para evitar esto con la programación defensiva, TDD, debemos ofender el código de verificación duplicado (código duplicado = errores/pruebas duplicados en cualquier caso). Con el uso de transferencias no tenemos problemas similares y los valores nunca cambiarán en este caso (trate de encontrar una situación en la que las direcciones de los registros cambien en producción :)). Todavía a veces verificamos si el valor que obtenemos del archivo/red de etc. está dentro del rango, pero no hay problemas para hacer que esto esté centralizado (ver c# Enum.TryParsepor ejemplo). Con las enumeraciones, ahorro tiempo de desarrollo y rendimiento en este caso.

Ejemplo 2. Estoy desarrollando un pequeño módulo con lógica de estado/errores. Si hago una enumeración privada, nadie sabrá acerca de esta enumeración, y puede cambiarla/ampliarla sin problemas con todos los beneficios de 1. Si basó su código en alguna lógica privada, algo salió completamente mal en su desarrollo.

Ejemplo 3. Desarrollo un módulo ampliable y modificado a menudo para una amplia gama de aplicaciones. Sería una solución extraña usar enumeraciones o cualquier otra constante para determinar la lógica/interfaz pública. Si agrega un nuevo número de enumeración en la arquitectura cliente-servidor, puede bloquearse, pero con las constantes puede obtener un estado impredecible del modelo e incluso guardarlo en el disco. A menudo prefiero un accidente a un estado impredecible. Esto nos muestra que el problema de copiabilidad/extensión posterior es un problema de nuestros desarrollos, no de enumeraciones. Si entiende qué enumeraciones no son adecuadas en este caso, simplemente no las use. Creo que tenemos suficiente competencia para elegir.

En mi opinión, la principal diferencia entre las constantes y la enumeración de tiempo de compilación es que las enumeraciones tienen dos contratos principales.

  1. Contrato de denominación.
  2. Contrato de valores.
    Todos los argumentos a favor y en contra de este párrafo fueron considerados antes.
    Si utiliza la programación por contrato, puede comprender fácilmente los beneficios de esto.

Enumera cuántas otras cosas tienen sus desventajas.
Fe no se puede cambiar sin copabilidad de frenos. Pero si conoce O de los principios SOLID, esto se aplica no solo a la enumeración sino también al desarrollo en general. Alguien puede decir, hago mi programa con lógica paralela y estructuras mutables feas. ¿Prohibamos las estructuras mutables? En lugar de esto, podemos agregar estructuras mutables/no mutables y dejar que los desarrolladores elijan.

Después de todo lo dicho, quiero señalar que Iota también tiene sus desventajas.

  1. Siempre tiene tipo int,
  2. Necesita calcular los valores en la cabeza. Puede perder mucho tiempo tratando de calcular los valores y comprobar que está bien.
    Con enumeraciones/const solo puedo presionar F12 y ver todos los valores.
  3. La expresión Iota es una expresión de código, también necesita probar esto.
    En algunos proyectos, me negué por completo a usar iota por estas razones.

Intenta dar un ejemplo en el que el uso de enumeraciones es ridículo

Disculpe mi franqueza, pero después de este comentario no creo que tenga mucho terreno para pararse aquí.

Y ni siquiera estaba haciendo lo que dices, es decir, dar un ejemplo de dónde usar enumeraciones es ridículo. Tomé un ejemplo que se suponía que mostraría cómo son necesarios e ilustraría cómo duelen.

Podemos discrepar razonablemente, pero al menos todos debemos argumentar de buena fe.

Ejemplo 1

Podría darle "nombres de registro" como algo que realmente no se puede cambiar, pero en lo que respecta a los valores de protocolo, estoy convencido de que la posición de hacer que tomen valores arbitrarios para la extensibilidad y la compatibilidad es razonable. Nuevamente, proto2 -> proto3 contenía exactamente ese cambio y lo hizo a partir de la experiencia adquirida.

Y de cualquier manera, no veo por qué un linter no podría atrapar esto.

Obtuve las 20 constantes y si tienen el mismo tipo y nombres similares, puedo cometer errores. Si el proyecto es grande, obtendrá este error.

Si está escribiendo mal los nombres, tener enumeraciones cerradas no lo ayudará. Solo si no usa los nombres simbólicos y usa int/string-literals en su lugar.

Ejemplo 2

Personalmente, tiendo a poner "paquete único" firmemente en la línea de "no es un proyecto grande". Por lo tanto, considero que es mucho menos probable que olvide un caso o cambie la ubicación de un código al extender una enumeración.

Y de cualquier manera, no veo por qué un linter no podría atrapar esto.

Ejemplo 3

Sin embargo, ese es el caso de uso más común que se presenta para las enumeraciones. Caso en cuestión: este problema específico los usa como justificación. Otro caso que se menciona a menudo son las llamadas al sistema, una arquitectura cliente-servidor disfrazada. La generalización de este ejemplo es "cualquier código en el que dos o más componentes desarrollados de forma independiente intercambien tales valores", lo cual es increíblemente amplio, cubre la gran mayoría de los casos de uso para ellos y, bajo el modelo de reparación gradual, también cualquier API exportada. .

FTR, todavía no estoy tratando de convencer a nadie de que las enumeraciones son dañinas (estoy seguro de que no lo haré). Solo para explicar cómo llegué a la conclusión de que lo son y por qué los argumentos a su favor no me convencen.

Siempre tiene tipo int,

iota puede (no necesariamente, pero lo que sea), pero los bloques const no, pueden tener una variedad de tipos constantes; de hecho, un superconjunto de las implementaciones de enumeración más comúnmente propuestas.

Necesita calcular los valores en la cabeza.

Nuevamente, no puede usar esto como un argumento a favor de las enumeraciones; puede escribir las constantes tal como puede hacerlo en una declaración de enumeración.

La expresión Iota es una expresión de código, también necesita probar esto.

No todas las expresiones tienen que ser probadas. Si es inmediatamente obvio, la prueba es una exageración. Si no es así, anote las constantes, lo haría de todos modos en una prueba.

iota no es la forma recomendada actualmente de hacer enumeraciones en Go; las declaraciones const sí lo son. iota solo sirve como una forma más general de ahorrar tipeo al escribir declaraciones de const consecutivas o formulaicas.

Y sí, las enumeraciones abiertas de Go tienen inconvenientes, obviamente. Se han mencionado anteriormente, extensamente: puede olvidar un caso en un interruptor, lo que genera errores. No tienen espacio de nombres. Puede usar accidentalmente una constante no simbólica que termina siendo un valor no válido (lo que genera errores).
Pero me parece más productivo hablar sobre estas desventajas y compararlas con las desventajas de cualquier solución propuesta, que tomar una solución fija (tipo enumeración) y discutir sobre sus compensaciones específicas para resolver los problemas.

Para mí, la mayoría de las desventajas se pueden resolver de manera pragmática en el idioma actual, con una herramienta de linter que detecta declaraciones constantes de cierto tipo y verifica sus usos. El espacio de nombres no se puede resolver de esta manera, lo cual no es bueno. Pero también puede haber una solución diferente a ese problema, además de las enumeraciones.

Podría darle "nombres de registro" como algo que realmente no se puede cambiar, pero en lo que respecta a los valores de protocolo, estoy convencido de que la posición de hacer que tomen valores arbitrarios para la extensibilidad y la compatibilidad es razonable. Nuevamente, proto2 -> proto3 contenía exactamente ese cambio y lo hizo a partir de la experiencia adquirida.

Por eso dije valores establecidos. La base de formato fe wav no ha cambiado durante muchos años y obtiene una gran capacidad de respaldo. Si hay nuevos valores, puede quedarse, use enumeraciones y agregue algunos valores.

Si está escribiendo mal los nombres, tener enumeraciones cerradas no lo ayudará. Solo si no usa los nombres simbólicos y usa int/string-literals en su lugar.

Sí, no me ayuda a hacer buenos nombres, pero pueden ayudar a organizar algunos valores con un nombre. Hace que el proceso de desarrollo sea más rápido en algunos casos. Puede reducir el número de variantes con escritura automática a una.

Sin embargo, ese es el caso de uso más común que se presenta para las enumeraciones. Caso en cuestión: este problema específico los usa como justificación. Otro caso que se menciona a menudo son las llamadas al sistema, una arquitectura cliente-servidor disfrazada. La generalización de este ejemplo es "cualquier código en el que dos o más componentes desarrollados de forma independiente intercambien tales valores", lo cual es increíblemente amplio, cubre la gran mayoría de los casos de uso para ellos y, bajo el modelo de reparación gradual, también cualquier API exportada. .

Pero usar/no usar constantes/enumeraciones no elimina el núcleo del problema, aún necesita pensar en la copiabilidad posterior. Quiero decir que el problema no está en las enumeraciones/constituciones sino en nuestros casos de uso.

Personalmente, tiendo a poner "paquete único" firmemente en la línea de "no es un proyecto grande". Por lo tanto, considero que es mucho menos probable que olvide un caso o cambie la ubicación de un código al extender una enumeración.

En este caso, aún tiene los beneficios de la convención de nombres y la verificación del tiempo de compilación,

No todas las expresiones tienen que ser probadas. Si es inmediatamente obvio, la prueba es una exageración. Si no es así, anote las constantes, lo haría de todos modos en una prueba.

Por supuesto, entiendo que no se debe probar toda la línea de código, pero si tiene un precedente, debe probar esto o reescribir. Sé cómo hacer esto sin iota, pero mi viejo ejemplo es solo una broma.

Nuevamente, no puede usar esto como un argumento a favor de las enumeraciones; puede escribir las constantes tal como puede hacerlo en una declaración de enumeración.

No es argumento para enumeraciones.

@Merovius

Si estamos hablando de enumeraciones cerradas (que es de lo que la gente suele hablar cuando habla de enumeraciones) o enumeraciones con controles exhaustivos, entonces ciertamente son especiales. Porque no son extensibles.

Tampoco son extensibles con seguridad.

Si usted tiene

package p
type Enum int
const (
  A Enum = iota
  B
  C
)
func Make() Enum {...}
func Take(Enum) {...}

y

package q
import "p"
const D enum = p.C + 1

dentro q es seguro usar D (a menos que la próxima versión de p agregue su propia etiqueta para Enum(3) ), pero siempre y cuando nunca páselo de vuelta a p : puede tomar el resultado de p.Make y cambiar su estado a D pero si llama a p.Take debe asegurarse de que sea no se pasa q.D Y tiene que asegurarse de que solo recibe uno de A , B , C o tiene un error. Puede solucionar esto haciendo

package q
import "p"
type Enum int
const (
    A = p.A
    B = p.B
    C = p.C
    D = C + 1
)
// needs to return an error if passed D or an unknown state of Enum
func To(Enum) (p.Enum, error) {...}
// needs to return an error if p.Enum has a value not known to the author
// at the time this package was written.
func From(p.Enum) (Enum, error) {...}

Con o sin un tipo cerrado en el idioma, tiene todos los problemas de tener un tipo cerrado pero sin que el compilador lo cuide.

Su ejemplo solo cubre el límite del proceso (al hablar de errores de red), no el límite del paquete. ¿Cómo se comportarán los paquetes si agrego un "Estado interno no válido" (para inventar algo) a FromTheNetwork? ¿Tengo que arreglar sus interruptores antes de que compilen de nuevo? Entonces no es extensible en el modelo de reparación gradual. ¿Requieren un caso predeterminado para compilar en primer lugar? Entonces no parece haber ningún punto para las enumeraciones.

Con solo los tipos de enumeración, aún tendría que hacer lo anterior y definir su propia versión con el estado adicional y las funciones de conversión de escritura.

Sin embargo, los tipos de suma se pueden componer incluso cuando se usan como enumeraciones, por lo que puede "extender" uno de forma bastante natural y segura de esta manera. Di un ejemplo, pero para ser más explícito, dado

package p
type Enum pick {
  A, B, C struct{}
}

Enum se puede "extender" con

package q
import "p"
type Enum pick {
  P p.Enum
  D struct{}
}

y esta vez es completamente seguro que una nueva versión de p agregue D . El único inconveniente es que tiene que cambiar dos veces para llegar al estado de un p.Enum desde dentro de un q.Enum pero es explícito y claro y, como mencioné, su editor podría escupir el esqueleto de los interruptores automáticamente.

Pero para eso no necesitamos enumeraciones reales, como tipos. Dicha herramienta de pelusa también podría verificar heurísticamente las declaraciones de const usando iota, donde cada caso es de un tipo determinado y considerar que "una enumeración" y realizar las comprobaciones que desee. Estaría completamente de acuerdo con una herramienta que use estas "enumeraciones por convención" para ayudar a completar automáticamente o aclarar que cada cambio debe tener un valor predeterminado o incluso que cada caso (conocido) debe verificarse.

Hay dos problemas con esto.

Si tiene un tipo integral definido con etiquetas dadas a un subconjunto de su dominio a través de const/iota:

Uno, puede representar una enumeración cerrada o abierta. Si bien se usa principalmente para simular un tipo cerrado, también podría usarse simplemente para dar nombres a valores de uso común. Considere una enumeración abierta para un formato de archivo imaginario:

const (
  //Name is the ID of a record field
  Name Record = iota
  //EmpID is the ID of an employee ID field
  EmpID

  //Intermediate values are reserved for future versions

  //Custom is the base of custom fields. Any custom field must have a unique ID greater than Custom.
  Custom Record = 42
)

Esto no quiere decir que 0, 1 y 42 sean el dominio del tipo Record. El contrato es mucho más sutil y requeriría tipos dependientes para modelar. (¡Eso definitivamente sería ir demasiado lejos!)

Dos, podríamos asumir heurísticamente que un tipo integral definido con etiquetas constantes significa que el dominio está restringido. Obtendría un falso positivo de lo anterior, pero nada es perfecto. Podríamos usar go/types para extraer este pseudotipo de las definiciones y luego revisar y encontrar todos los cambios sobre valores de ese tipo y asegurarnos de que todos contengan las etiquetas necesarias. Esto puede ser útil, pero no hemos mostrado exhaustividad en este punto. Hemos asegurado la cobertura de todos los valores válidos, pero no hemos probado que no se hayan creado valores no válidos. No es posible hacerlo. Incluso si pudiéramos encontrar todas las fuentes, sumideros y transformaciones de valores e interpretarlos de manera abstracta para garantizar de forma estática que no se creó ningún valor no válido, aún no podríamos decir nada sobre el valor en tiempo de ejecución, ya que el reflejo desconoce el verdadero dominio del tipo ya que no está codificado en el sistema de tipos.

Aquí hay una alternativa a los tipos de enumeración y suma que soluciona esto, aunque tiene sus propios problemas.

Digamos que el tipo literal range m n crea un tipo integral que es al menos m y como máximo n (para todo v, m ≤ v ≤ n). Con eso podríamos limitar el dominio de la enumeración, como

package p
type Enum range 0 2
const (
  A Enum = iota
  B
  C
)

Dado que el tamaño del dominio = el número de etiquetas, es posible determinar con un 100 % de confianza si una declaración de cambio agota todas las posibilidades. Para extender esa enumeración externamente, absolutamente necesitaría crear funciones de conversión de tipos para manejar el mapeo, pero sigo manteniendo que debe hacerlo de todos modos.

Por supuesto, esa es en realidad una familia de tipos sorprendentemente sutil para implementar y no funcionaría tan bien con el resto de Go. Tampoco tiene muchos usos fuera de este y algunos casos de uso de nicho.

No podemos estar de acuerdo sobre la importancia de mantener posible la reparación gradual y la extensibilidad; por ejemplo, mucha gente cree que el control de versiones semántico es una mejor solución para los problemas que resuelve. Pero si los encuentra importantes, es perfectamente válido y razonable considerar las enumeraciones como dañinas o sin sentido. Y esa era la pregunta a la que estaba respondiendo: cómo las personas pueden razonablemente hacer el compromiso de requerir un cheque en todas partes, en lugar de tenerlo en el compilador. Respuesta: Valorando la extensibilidad y la evolución de las API, lo que hace que estas comprobaciones sean necesarias en el sitio de uso de todos modos.

Para los tipos básicos de enumeración, estoy de acuerdo. Al comienzo de esta discusión, simplemente no me hubiera gustado que se eligieran en lugar de los tipos de suma, pero ahora entiendo por qué serían dañinos. Gracias a ti y a @griesemer por aclararme eso.

Para los tipos de suma, creo que lo que dijiste es una razón válida para no requerir que los cambios sean exhaustivos en el momento de la compilación. Sigo pensando que los tipos cerrados tienen varios beneficios y que, de los tres examinados aquí, los tipos de suma son los más flexibles sin las desventajas de los demás. Permiten cerrar un tipo sin inhibir la extensibilidad o la reparación gradual mientras evitan errores causados ​​por valores ilegales, como lo hace cualquier tipo bueno.

La razón principal por la que uso golang sobre python y javascript y otros lenguajes comunes sin tipos es la seguridad de tipos. Hice mucho con Java y una cosa que extraño en golang que proporciona Java son las enumeraciones seguras.

No estaría de acuerdo con poder diferenciar los tipos con enumeraciones. Si necesita ints, simplemente adhiérase a ints, como lo hace Java. Si necesita enumeraciones seguras, sugeriría seguir la sintaxis.

type enums enum { foo, bar, baz }

@rudolfschmidt , estoy de acuerdo contigo, también podría verse así:

type DaysOfTheWeek enum {
  Monday
  Tuesday
}

Pero tiene un pequeño escollo: tenemos que poder tomar el control de enum en los casos en que tenemos que validar datos, transformarlos en JSON o interactuar con o FS.
Si asumimos ciegamente que enum es un conjunto de enteros sin signo, podemos terminar con una iota.
Si queremos innovar tenemos que pensar en la comodidad de uso.
Por ejemplo, ¿con qué facilidad puedo validar que el valor dentro de JSON entrante es un elemento válido de enumeración?
¿Qué pasa si te digo que el software está cambiando?

Digamos que tenemos una lista de criptomonedas:

type CryptoCurrency enum {
  BTC
  ETH
  XMR
}

Intercambiamos datos con múltiples sistemas de terceros. Digamos que tienes miles de ellos.
Tiene una larga historia, cantidad de datos almacenados. El tiempo pasa, digamos, BitCoin eventualmente muere. Nadie lo está usando.
Entonces decides eliminarlo de la estructura:

type CryptoCurrency enum {
  ETH
  XMR
}

Esto está causando cambios en los datos. Porque todos los valores de enumeración cambiaron. Esta bien para ti. Puede ejecutar la migración de sus datos. ¿Qué pasa con sus socios? Algunos de ellos no se están moviendo tan rápido, algunos no tienen recursos o simplemente no pueden hacerlo por varias razones.
Pero tienes datos de ingesta de ellos. Entonces terminará teniendo 2 enumeraciones: antigua y nueva; y un mapeador de datos usando ambos.
Eso nos dice que brindemos flexibilidad de definición para enumeraciones y capacidades para validar y clasificar/desclasificar ese tipo de datos.

type CryptoCurrency enum {
  ETH = 1, // reminds const?
  XMR = 2
}
// this is real life case 
v := 3
if v is CryptoCurrency {
 // right?
} else {
 // nope, provide default value
 v = CryptoCurrency.ETH
}

Tenemos que pensar en la aplicabilidad de las enumeraciones y los casos de uso.

Podemos aprender 2 nuevas palabras clave si nos puede ahorrar miles de líneas de código repetitivo.

El término medio es, de hecho, tener la capacidad de validar los valores de enumeración sin restringirlos en lo que pueden ser esos valores. El tipo de enumeración permanece prácticamente igual: es un montón de constantes con nombre. La variable del tipo de enumeración puede ser igual a cualquier valor del tipo subyacente de la enumeración. Lo que agrega además de eso es la capacidad de validar un valor para ver si contiene un valor de enumeración válido o no. Y podría haber otras bonificaciones como la stringificación.

Muy a menudo me encuentro en una situación en la que tengo un protocolo (protobuf o ahorro) con un montón de enumeraciones por todas partes. Tengo que validar cada uno de ellos y, si el valor de enumeración es desconocido para mí, tirar ese mensaje e informar un error. No hay otra forma en que pueda manejar ese tipo de mensaje. Con lenguajes donde enum es solo un montón de constantes, no tengo otra manera que escribir grandes cantidades de declaraciones de cambio verificando todas las combinaciones posibles. Esa es una gran cantidad de código y es probable que haya errores. Con algo como C#, puedo usar el soporte incorporado para validar enumeraciones, lo que ahorra mucho tiempo. Algunas implementaciones de protobuf en realidad lo están haciendo internamente y lanzan una excepción si ese es el caso. Sin mencionar lo fácil que se vuelve el registro: obtienes la encadenación lista para usar. Es bueno que protobuf genere la implementación de Stringer para usted, pero no todo en su código es protobuf.

Pero la capacidad de almacenar cualquier valor es útil en otros casos en los que no desea desechar mensajes, sino hacer algo con ellos, incluso si no es válido. Por lo general, el cliente puede desechar el mensaje, pero en el lado del servidor a menudo necesita almacenar todo en la base de datos. Tirar contenido no es una opción allí.

Entonces, para mí, hay un valor real en la capacidad de validar los valores de enumeración. Me ahorraría miles de líneas de código repetitivo que no hace más que validar.

Me parece bastante simple proporcionar esta funcionalidad como una herramienta. Parte de él ya existe en la herramienta stringer. Si hay una herramienta a la que llamaría como enumer Foo , que generaría métodos fmt.Stringer para Foo y (digamos) un método Known() bool que verifica si el el valor almacenado está en el rango de valores conocidos, ¿eso aliviaría sus problemas?

@Merovius en ausencia de cualquier otra cosa, sería útil. Pero generalmente estoy en contra de la autogen. El único caso en el que es útil y simplemente funciona es en cosas como protobuf, donde tiene un protocolo bastante estable que se puede compilar una vez. Usarlo para enumeraciones en general se siente como una muleta para un sistema de tipos simplista. Y viendo cómo Go tiene que ver con la seguridad tipográfica, eso se siente en contra de la filosofía del lenguaje en sí. En lugar de ayudar con el idioma, comienzas a desarrollar esta infraestructura encima que no es realmente parte del ecosistema del idioma. Deje herramientas externas para examinar, no para implementar lo que falta en el lenguaje.

. Usarlo para enumeraciones en general se siente como una muleta para un sistema de tipos simplista.

Porque lo es: el sistema de tipos de Go es famoso e intencionalmente simplista. Pero esa no era la cuestión, la cuestión era si aliviaría tus problemas. Aparte de "No me gusta", realmente no veo cómo no (si asume enumeraciones abiertas de todos modos).

Y viendo cómo Go tiene que ver con la seguridad tipográfica, eso se siente en contra de la filosofía del lenguaje en sí.

Go no es "todo sobre seguridad tipográfica". Los lenguajes como Idris tienen que ver con la seguridad de tipos. Go se trata de problemas de ingeniería a gran escala y, como tal, su diseño está impulsado por los problemas que está tratando de resolver. Por ejemplo, su sistema de tipos permite detectar una amplia gama de errores debido a cambios en la API y permite algunas refactorizaciones a gran escala. Pero también se mantiene intencionalmente simple, para facilitar el aprendizaje, reducir la divergencia de las bases de código y aumentar la legibilidad del código de terceros.

Como tal, si el caso de uso que le interesa (enumeraciones abiertas) se puede resolver sin un cambio de idioma, mediante una herramienta que genera un código legible con la misma facilidad, eso parece estar muy en línea con la filosofía de Go. En particular, agregar una nueva función de idioma que es un subconjunto de la funcionalidad de una existente no parece estar en línea con el diseño de Go.

Por lo tanto, para reiterar: sería útil si pudiera ampliar cómo el uso de una herramienta que genera el texto modelo que le preocupa no resuelve el problema real; al menos, porque comprender eso es necesario para informar el diseño de la función de todos modos. .

He combinado algunas de las ideas de la discusión, ¿qué piensas al respecto?

Algunas informaciones básicas:

  1. Puede extender una enumeración como cualquier otro tipo.
  2. Se almacenan como una constante, pero con el nombre del tipo como prefijo. Motivo: cuando use enumeraciones iota actuales, probablemente escribirá el nombre de la enumeración como prefijo de cada constante. Con esta función puedes evitarlo.
  3. Son inmutables y se tratan como cualquier otra constante.
  4. Puede iterar sobre enumeraciones. Cuando haces esto, se comportan como un mapa. La clave es el nombre de enumeración, el valor es el valor de enumeración.
  5. Puede agregar métodos a una enumeración, como lo hace con cualquier otro tipo.
  6. Cada valor de enumeración tiene métodos generados automáticamente:
  7. Name() devolverá el nombre de la variable de enumeración
  8. Index() devolverá el índice de enumeración, que aumenta automáticamente. Comienza donde comienza una matriz.

Código:
```ir
paquete principal

//Ejemplo A
type Country enum[struct] { //las enumeraciones pueden extender otros tipos (mira el ejemplo B)
Austria("AT", "Austria", false) //Será accesible como una constante, pero con el tipo
Alemania("DE", "Alemania", verdadero) //prefijo (por ejemplo, País.Austria)

//The struct will automatically begin when it doesn't match the format EnumName(...) anymore
Code, CountryName string
HasMerkel bool //Totally awesome

}

// Las enumeraciones pueden tener métodos como cualquier otro tipo
func (c País) test() {}

función principal() {
println(País.Austria.NombrePaís) //Austria
println(País.Alemania.Código) //DE

/* Prints:
Austria
0
Germany
1
 */
for name, country := range Country {
    println(name) //Austria
    println(name == country.Name()) //true ; also autogenerated 
    println(country.Index()) //Auto generated increasing index
}

}

//Ejemplo B
escriba Genialidad enum[int] {
muy guay(10)
Genial(5)
No genial(0)
}```

@sinnlosername Creo que las enumeraciones deberían ser algo que sea muy fácil de entender. La combinación de algunas de las ideas presentadas en la discusión anterior puede no conducir necesariamente a la mejor idea para una enumeración.

Creo que lo siguiente sería fácil de entender:

Declaración

type Day enum {
    Monday
    Tuesday
    ...
    Sunday
}

Conversión de cadenas (usando la interfaz Stringer ):

func (d Day) String() string {
    switch d {
    case Monday:
        return "mon"
    case Tuesday:
        return "tues"
    ...
    case Sunday:
        return "sun"
    }
}

Es así de simple. El beneficio de esto es permitir una mayor seguridad de tipo al pasar enumeraciones.

Ejemplo de uso

func IsWeekday(d Day) bool {
    return d != Saturday && d != Sunday
}

Si tuviera que usar las constantes string aquí para representar un Day , IsWeekday diría que cualquier cadena que no sea "sat" o "sun" es un día de semana (es decir, ¿qué devolvería/debería devolver IsWeekday("abc") ?). Por el contrario, el dominio de la función que se muestra arriba está restringido, lo que permite que la función tenga más sentido con respecto a su entrada.

@ljeabmreosn

probablemente debería ser

func IsWeekday(d Day) bool {
    return d != Day.Saturday && d != Day.Sunday
}

Dejé de esperar que el equipo de golang mejorara el idioma de manera necesaria. Puedo recomendar a todos que echen un vistazo a rust lang, ya tiene todas las características deseadas como enumeración y genéricos y más.

Estamos en 14. May 2018 y todavía discutimos sobre el soporte de enumeración. ¿Quiero decir, qué demonios? Personalmente estoy decepcionado de golang.

Puedo entender que puede ser frustrante esperar una función. Pero publicar comentarios no constructivos como este no ayuda. Por favor mantenga sus comentarios respetuosos. Consulte https://golang.org/conduct.

Gracias.

@agnivade Tengo que estar de acuerdo con @rudolfschmidt. GoLang definitivamente tampoco es mi idioma favorito debido a esta falta de funciones, API y demasiada resistencia al cambio o a aceptar los errores del pasado por parte de los creadores de Go. Pero no tengo otra opción en este momento porque no fui yo quien tomó la decisión sobre qué idioma elegir para mi último proyecto en mi lugar de trabajo. Así que tengo que trabajar con todas sus deficiencias. Pero para ser honesto, es como torturar escribir códigos en GoLang ;-)

Dejé de esperar que el equipo de golang mejorara el idioma de manera necesaria.

  • La palabra necesario no significa "lo que quiero".

En realidad, las características básicas de cada idioma moderno son necesarias. GoLang tiene algunas buenas características, pero no sobrevivirá si el proyecto sigue siendo conservador. Las funciones como enumeraciones o genéricos no tienen desventajas para las personas a las que no les gustan, pero tienen muchas ventajas para las personas que quieren usarlas.

Y no me digas "pero go quiere seguir siendo simple". Hay una gran diferencia entre "simple" y "sin características reales". Java es muy simple, pero faltan muchas funciones. Entonces, los desarrolladores de Java son magos o este argumento es simplemente malo.

En realidad, las características básicas de cada idioma moderno son necesarias.

Por supuesto. Esas características centrales se denominan integridad de Turing. _Todo_ lo demás es una elección de diseño. Hay mucho espacio entre la completitud de Turing y C++ (por ejemplo) y en ese espacio puedes encontrar muchos lenguajes. La distribución sugiere que no existe un óptimo global.

GoLang tiene algunas buenas características, pero no sobrevivirá si el proyecto sigue siendo conservador.

Posiblemente. Hasta ahora sigue creciendo. En mi opinión, no seguiría creciendo si no hubiera sido conservador. Nuestras dos opiniones son subjetivas y técnicamente no valen mucho. Es la experiencia y el gusto de los diseñadores lo que manda. Está bien tener una opinión diferente, pero eso no garantiza que los diseñadores la compartan.

Por cierto, si me imagino lo que sería Go hoy si se adoptara el 10% de las características que la gente demanda, probablemente ya no usaría Go.

En realidad, te perdiste el argumento más importante de mi respuesta. Tal vez porque ya es un contraataque a algunas de las cosas que dijiste.

"Características como enumeraciones o genéricos no tienen desventajas para las personas a las que no les gustan, pero tienen muchas ventajas para las personas que quieren usarlas".

¿Y por qué cree que este conservadurismo es una razón para el crecimiento de golang? Creo que es más probable que esto esté relacionado con la eficiencia de golang y el gran conjunto de bibliotecas estándar.

También java experimentó una especie de "bloqueo" cuando intentaron cambiar cosas importantes en java 9, lo que probablemente hizo que muchas personas buscaran una alternativa. Pero mire Java antes de este accidente. Crecía constantemente porque tenía más y más funciones que facilitaban la vida de los desarrolladores.

"Características como enumeraciones o genéricos no tienen desventajas para las personas a las que no les gustan, pero tienen muchas ventajas para las personas que quieren usarlas".

Eso es muy claramente no es cierto. Eventualmente, todas las características llegarán a la biblioteca estándar y/o a los paquetes que quiero importar. _Todos_ tendrán que lidiar con las nuevas características sin importar si les gustan o no.

Hasta ahora sigue creciendo. En mi opinión, no seguiría creciendo si no hubiera sido conservador

No creo que su lento crecimiento (si lo hay) se deba a la prudencia, sino a una biblioteca estándar, un conjunto ya existente de funciones de lenguaje, herramientas. Eso es lo que me trajo aquí. Agregar funciones de idioma no cambiaría nada para mí en ese sentido.

Si miramos C# y Typescript o incluso Rust/Swift. Están agregando nuevas características como locos. C# todavía está en los principales idiomas, fluctuando hacia arriba y hacia abajo. Typescript está creciendo muy rápido. Lo mismo para Rust/Swift. Go, por otro lado, explotó en popularidad en 2009 y 2016. Pero entre eso no creció en absoluto y en realidad perdió. Go no tiene nada que dar a los nuevos desarrolladores si ya lo sabían y no lo eligieron antes por alguna razón. Exactamente porque Go se está estancando en su diseño. Otros idiomas agregan funciones no porque no tengan nada más que hacer, sino porque la base de usuarios real lo exige. Las personas necesitan nuevas funciones para que su base de código siga siendo relevante en dominios de problemas en constante cambio. Como asíncrono/espera. Era necesario para resolver un problema real. No es de extrañar que ahora puedas verlo en muchos idiomas.

Eventualmente habrá Go 2 y puede estar absolutamente seguro de que traerá muchos desarrolladores nuevos. No porque sea nuevo y brillante, sino porque las nuevas características podrían convencer a alguien de cambiarlo o probarlo. Si el conservadurismo fuera tan importante, incluso tendríamos estas propuestas.

No creo que su lento crecimiento (si lo hay) se deba a la prudencia, sino a una biblioteca estándar, un conjunto ya existente de funciones de lenguaje, herramientas. Eso es lo que me trajo aquí.

Y ese es el resultado de ser conservador. Si el idioma rompe algo/todo cada [medio] año más o menos, no haría nada de lo que dice que valora sobre Go porque habrá mucha menos gente brindándole eso.

Agregar funciones de idioma no cambiaría nada para mí en ese sentido.

¿Está usted seguro de eso? Véase más arriba.


Por cierto, ¿has visto los resultados de la encuesta de 2017 ?

Si el idioma rompe algo/todo cada [medio] año más o menos

Entonces no rompas nada. C# agregó un montón de funciones y nunca violó la compatibilidad con versiones anteriores. Esa tampoco es una opción para ellos. Lo mismo para C++, creo. Si Go no puede agregar funciones sin romper algo, entonces es un problema con Go y, posiblemente, con la forma en que se implementa.

Por cierto, ¿has visto los resultados de la encuesta de 2017?

Mi comentario se basa en las encuestas de 2017/2018, el índice TIOBE y mis observaciones generales sobre lo que sucede con varios idiomas.

@cznic
Todos tienen que lidiar con ellos, pero no es necesario que los uses. Si prefiere escribir su código con enumeraciones y mapas de iota, aún puede hacerlo. Y si no le gustan los genéricos, use bibliotecas sin ellos. Java prueba que es posible tener ambos.

Entonces no rompas nada.

Buena idea. Sin embargo, muchos, si no la mayoría de los cambios propuestos al lenguaje, _incluido este mismo_, son cambios importantes.

C# agregó un montón de funciones y nunca violó la compatibilidad con versiones anteriores.

Verifique sus datos: Visual C# 2010 Breaking Changes . (primer resultado de búsqueda web, solo puedo adivinar si es o no es el único ejemplo).

Mi comentario se basa en las encuestas de 2017/2018, el índice TIOBE y mis observaciones generales sobre lo que sucede con varios idiomas.

Bueno, ¿cómo puede ver que el idioma no crece mientras los resultados de la encuesta muestran un crecimiento del 70 % de año a año en el número de encuestados?

¿Cómo defines "cambios radicales"? Cada línea de código go seguiría funcionando después de agregar enumeraciones o genéricos.

Todos tienen que lidiar con ellos, pero no es necesario que los uses. Si prefiere escribir su código con enumeraciones y mapas de iota, aún puede hacerlo. Y si no le gustan los genéricos, use bibliotecas sin ellos. Java prueba que es posible tener ambos.

no puedo estar de acuerdo Una vez que el idioma se vuelve genérico, por ejemplo, aunque no los use, se usarán en todas partes. Incluso cuando internamente sin cambiar la API. El resultado es que estoy muy afectado por ellos, porque seguro que no hay forma de agregar genéricos al lenguaje sin ralentizar la construcción de cualquier programa que los use. También conocido como "No hay almuerzo gratis".

¿Cómo defines "cambios radicales"? Cada línea de código go seguiría funcionando después de agregar enumeraciones o genéricos.

Por supuesto que no. Este código ya no compilaría con esta propuesta:

package foo

var enum = 42

La palabra necesario no significa "lo que quiero".

claro, no significa, y nunca lo he querido decir. Por supuesto, puede responder que tales características no son necesarias, pero luego puedo responder lo que es necesario en general. Nada es necesario y podemos volver al lápiz y papel.

Golang dice ser un lenguaje para equipos grandes. No estoy seguro si puede usar golang para desarrollar grandes bases de código. Para eso, necesita compilación estática y verificación de tipos para evitar errores de tiempo de ejecución tanto como sea posible. ¿Cómo puede hacerlo sin enumeraciones y genéricos? Esas características ni siquiera son elegantes o agradables, pero son absolutamente esenciales para un desarrollo serio. Si no los tiene, termina usando interfaces{} en todas partes. ¿Cuál es el punto de tener tipos de datos si se ve obligado a usar interfaces{} en su código?

Claro, si no tiene otra opción, también lo hará, pero ¿por qué debería hacerlo si tiene alternativas como rust que ya ofrecen todo eso y es incluso más rápido en ejecución que golang? Realmente me pregunto si go tiene futuro con esa mentalidad de:

La palabra necesario no significa "lo que quiero".

Respeto todas las contribuciones al código abierto y si golang es un proyecto de pasatiempo está bien como es, pero golang quiere que se lo tome en serio y por el momento es más un juguete para algunos desarrolladores aburridos y no veo la voluntad de cambiar eso.

No es necesario cambiar la API, solo las partes nuevas de la API pueden usar genéricos, pero probablemente siempre haya alternativas sin genéricos en Internet.

Y tanto la compilación un poco más lenta como las variables llamadas "enum" son efectos mínimos. En realidad, el 99% de las personas ni siquiera lo notarán y el otro 1% solo necesitará agregar algunos pequeños cambios que sean tolerables. Esto no es comparable con, por ejemplo, el rompecabezas de Java que lo jodió todo.

Y tanto la compilación un poco más lenta como las variables llamadas "enum" son efectos mínimos. En realidad, el 99% de las personas ni siquiera lo notarán y el otro 1% solo necesitará agregar algunos pequeños cambios que sean tolerables.

Todos estarían felices si alguien pudiera venir con un diseño e implementación que tuviera un rendimiento tan maravilloso. Por favor contribuya a #15292.

Sin embargo, si este es un juego llamado "sacar números a mi favor sin ningún tipo de datos de respaldo", lo siento, pero no participo.

¿Tiene algún número de la diferencia de velocidad con los genéricos?

Y sí, esos números no están respaldados por ningún dato, porque solo dirán que la probabilidad de tener variables llamadas "enum" no es muy alta.

Me gustaría recordarles a todos que hay muchas personas suscritas a este número para la pregunta específica de si se pueden agregar enumeraciones a Go y cómo. Las preguntas generales de "¿es Go un buen idioma?" y "¿debería Go centrarse más en ofrecer funciones?" probablemente se discutan mejor en un foro diferente.

¿Tiene algún número de la diferencia de velocidad con los genéricos?

No, por eso no publiqué ninguno. Solo publiqué que el costo no puede ser cero.

Y sí, esos números no están respaldados por ningún dato, porque solo dirán que la probabilidad de tener variables llamadas "enum" no es muy alta.

Eso está mezclado. La desaceleración se debió a los genéricos. "enum" se trataba de compatibilidad con versiones anteriores y su código falso "_Every_ line of go seguiría funcionando después de agregar enumeraciones o genéricos". afirmar. (enfatiza el mio)

@Merovius Tienes razón, ahora me estoy callando.

Trayendo esto de vuelta a los tipos de enumeración, que es de lo que trata este problema, entiendo completamente el argumento de por qué Go necesita genéricos, pero estoy mucho más inestable en el argumento de por qué Go necesita tipos de enumeración. De hecho, pregunté esto arriba en https://github.com/golang/go/issues/19814#issuecomment -290878151 y todavía estoy inestable. Si había una buena respuesta a eso, me la perdí. ¿Alguien podría repetirlo o señalarlo? Gracias.

@ianlancetaylor No creo que el caso de uso sea complicado, queriendo una forma segura de escribir para garantizar que un valor pertenezca a un conjunto predefinido de valores, lo que no es posible hoy en Go. La única solución es validar manualmente en cada punto de entrada posible en su código, incluidos los RPC y las llamadas a funciones, lo que es intrínsecamente poco confiable. Las otras sutilezas sintácticas para iterar facilitan muchos casos de uso comunes. Ya sea que encuentre valioso o no eso es subjetivo, y obviamente ninguno de los argumentos anteriores ha sido convincente para los poderes fácticos, por lo que esencialmente he renunciado a que esto se aborde a nivel de lenguaje.

@ianlancetaylor : todo tiene que ver con la seguridad tipográfica. usa tipos para minimizar el riesgo de errores de tiempo de ejecución debido a un error tipográfico o al uso de tipos incompatibles. Por el momento puedes escribir en go

if enumReference == 1

porque por el momento las enumeraciones son solo números u otros tipos de datos primitivos.

Ese código no debería ser posible en absoluto y debería evitarse. La misma discusión que tuvo en la comunidad de Java hace años, esa es la razón por la que introdujeron las enumeraciones porque entendieron la importancia.

Solo deberías poder escribir

if enumReference == enumType

no necesita demasiada fantasía para imaginar en qué escenarios if enumReference == 1 pueden ocurrir de una manera más oculta y generar problemas adicionales que solo verá en tiempo de ejecución.

Solo quiero mencionar: Go tiene su potencial, pero es extraño que las cosas y los conceptos que se han probado y entendido durante años se discutan aquí como se discuten nuevos conceptos o paradigmas de programación. Si tiene una forma alternativa de garantizar la seguridad de los tipos, tal vez haya algo mejor que las enumeraciones, pero no lo veo.

Go tiene su potencial, pero es extraño que aquí se discutan cosas y conceptos que han sido probados y entendidos durante años, como se discuten nuevos conceptos o paradigmas de programación.

Afais, especialmente al seguir las otras discusiones sobre genéricos, tipos de suma, etc., no se trata tanto de tenerlo, sino de cómo implementarlo. El sistema de tipos de Java es extremadamente extensible y está bien especificado. Es una diferencia enorme.

En Go, la gente está tratando de encontrar formas de agregar funciones al lenguaje, sin aumentar la complejidad de los compiladores. Eso no suele funcionar demasiado bien y les hace abandonar esas ideas iniciales.

Si bien yo también creo que esas prioridades son bastante absurdas en su forma y calidad actuales, su mejor oportunidad es encontrar la implementación más simple posible y menos disruptiva . Cualquier otra cosa no te llevará más lejos, en mi opinión.

@derekperkins @rudolfschmidt Gracias. Quiero dejar en claro que aunque C++ tiene tipos de enumeración, las características que sugiere no están en C++. Así que no hay nada obvio en esto.

En general, si una variable de un tipo de enumeración solo puede aceptar valores de esa enumeración, sería inútil. En particular, debe haber una conversión de un entero arbitrario al tipo de enumeración; de lo contrario, no puede enviar enumeraciones a través de una conexión de red. Bueno, puedes, pero tienes que escribir un interruptor con un caso para cada valor de enumeración, lo que parece realmente tedioso. Entonces, al hacer una conversión, ¿el compilador genera una verificación de que el valor es un valor de enumeración válido durante la conversión de tipo? ¿Y entra en pánico si el valor no es válido?

¿Se requiere que los valores de enumeración sean secuenciales, o pueden tomar cualquier valor como en C++?

En Go, las constantes no tienen tipo, por lo que si permitimos conversiones de números enteros a un tipo de enumeración, sería extraño prohibir if enumVal == 1 . Pero supongo que podríamos.

Uno de los principios generales de diseño de Go es que las personas que escriben Go escriben código, no tipos. Todavía no veo ninguna ventaja proveniente de los tipos de enumeración que nos ayuden a la hora de escribir código. Parecen agregar un conjunto de restricciones de tipo de un tipo que generalmente no tenemos en Go. Para bien o para mal, Go no proporciona mecanismos para controlar el valor de los tipos. Entonces, debo decir que, para mí, el argumento a favor de agregar enumeraciones a Go aún no parece convincente.

Me repetiré, pero estoy a favor de mantener las enumeraciones como están hoy y agregar funciones encima de ellas:

  • el tipo de enumeración tiene un tipo de valor subyacente y un par de constantes con nombre asociadas a él
  • el compilador debe permitir la conversión de un valor arbitrario a un valor de enumeración siempre que sus tipos subyacentes sean compatibles. Cualquier valor de tipo int debe poder convertirse a cualquier tipo de enumeración entera.
  • Se permite la conversión que conduce a un valor de enumeración no válido. El tipo de enumeración no debe imponer ninguna restricción sobre los valores que puede tomar una variable.

Lo que proporciona además de eso es:

  • cadena de valores de enumeración. Desde mi experiencia, muy útil para la interfaz de usuario y el registro. Si el valor de enumeración es válido, la clasificación devuelve el nombre de la constante. Si no es válido, devuelve una representación de cadena del valor subyacente. Si -1 no es un valor de enumeración válido de algún tipo de enumeración Foo , la cadena debería devolver -1 .
  • permita que el desarrollador determine si el valor es un valor de enumeración válido en tiempo de ejecución. Muy útil a la hora de trabajar con cualquier tipo de protocolo. A medida que evolucionan los protocolos, se podrían introducir nuevos valores de enumeración que el programa no conoce. O podría ser un simple error. En este momento, debe asegurarse de que los valores de enumeración sean estrictamente secuenciales (algo que no siempre puede hacer cumplir) o verificar manualmente todos los valores posibles. Ese tipo de código se vuelve muy grande muy rápido y es probable que ocurran errores.
  • posiblemente permita que el desarrollador enumere todos los valores posibles de un tipo de enumeración. Vi a personas que pedían esto aquí, otros idiomas también tienen esto, pero en realidad no recuerdo haberlo necesitado nunca, así que no tengo experiencia personal a favor de esto.

Mi justificación tiene que ver con escribir código y evitar errores. Todas estas tareas son tediosas e innecesarias para que el desarrollador las haga a mano o incluso introduzca herramientas externas que compliquen el código y la creación de scripts. Estas características cubren todo lo que necesito de las enumeraciones sin complicarlas ni restringirlas demasiado. No creo que Go necesite nada como enumeraciones en Swift o incluso en Java.


Hubo discusiones sobre la validación en tiempo de compilación de que la declaración de cambio cubre todos los valores de enumeración posibles. Con mi propuesta será inútil. Hacer verificaciones de agotamiento no cubrirá los valores de enumeración no válidos, por lo que aún debe tener un caso predeterminado para manejarlos. Eso es necesario para admitir la reparación gradual del código. Lo único que podemos hacer aquí, creo, es producir una advertencia si la declaración de cambio no tiene un caso predeterminado. Pero eso se puede hacer incluso sin cambiar el idioma.

@ianlancetaylor Creo que tu argumento tiene algunos defectos.

En general, si una variable de un tipo de enumeración solo puede aceptar valores de esa enumeración, sería inútil. En particular, debe haber una conversión de un entero arbitrario al tipo de enumeración; de lo contrario, no puede enviar enumeraciones a través de una conexión de red.

La abstracción para el programador está bien; Go proporciona muchas abstracciones. Por ejemplo, el siguiente código no se compila:

package main

import "fmt"

const NULL = 0x0

func main() {
    str := "hello"
    if &str == NULL {
        fmt.Println("str is null")
    }
}

pero en C , se compilaría un programa de este estilo. Esto se debe a que Go está fuertemente tipado y C no.

Los índices de las enumeraciones pueden almacenarse internamente pero estar ocultos para el usuario como una abstracción, similar a las direcciones de las variables.

@zerkms Sí, esa es una posibilidad, pero dado el tipo de d , la inferencia de tipo debería ser posible; sin embargo, el uso calificado de enumeraciones (como en su ejemplo) es un poco más fácil de leer.

@ianlancetaylor esa es una versión muy C de las enumeraciones de las que estás hablando. Estoy seguro de que a mucha gente le gustaría eso, pero, en mi opinión:

Los valores de enumeración no deben tener propiedades numéricas. Los valores de cada tipo de enumeración deben ser su propio universo finito de etiquetas discretas, aplicadas en el momento de la compilación, sin relación con ningún número u otro tipo de enumeración. Lo único que puede hacer con un par de estos valores es == o != . Otras operaciones se pueden definir como métodos o con funciones.

La implementación compilará esos valores hasta números enteros, pero eso no es algo fundamental con ninguna razón legítima para exponerse directamente al programador, excepto por inseguridad o reflexión. Por la misma razón que no puedes hacer bool(0) para obtener false .

Si desea convertir una enumeración en o desde un número o cualquier otro tipo, escriba todos los casos e incluya el manejo de errores apropiado para la situación. Si eso es tedioso, use un generador de código como stringer o al menos algo para completar los casos en la declaración de cambio.

Si está enviando el valor fuera del proceso, un int es bueno si está siguiendo un estándar bien definido o si sabe que está hablando con otra instancia de su programa que se compiló desde la fuente o si necesita hacer cosas. encajar en el espacio más pequeño posible incluso si eso puede causar problemas, pero generalmente ninguno de estos se mantiene y es mejor usar una representación de cadena para que el valor no se vea afectado por el orden de origen de la definición de tipo. No desea que el proceso A's Green se convierta en el proceso B's Blue porque alguien más decidió que se debe agregar Blue antes de Green para mantener las cosas en orden alfabético en la definición: desea unrecognized color "Blue" .

Es una forma buena y segura de representar varios estados de forma abstracta. Deja que el programa defina qué significan esos estados.

(Por supuesto, a menudo desea asociar datos con esos estados y el tipo de esos datos varía de un estado a otro...)

@ljeabmreosn Mi punto era que si Go permite la conversión de enteros a tipos de enumeración, entonces sería natural que una constante sin tipo se convierta automáticamente a un tipo de enumeración. Su contraejemplo es diferente, ya que Go no permite convertir números enteros a tipos de puntero.

@jimmyfrasche Si tiene que escribir un cambio para convertir entre enteros y tipos de enumeración, estoy de acuerdo en que funcionaría limpiamente en Go, pero, francamente, no parece lo suficientemente útil como para agregarlo al idioma por sí mismo. Se convierte en un caso especial de tipos de suma, para los cuales ver #19412.

Aquí hay muchas propuestas.

Un comentario general: para cualquier propuesta que no exponga un valor subyacente (por ejemplo, un int) que pueda convertir hacia y desde una enumeración, aquí hay algunas preguntas para responder.

¿Cuál es el valor cero de un tipo de enumeración?

¿Cómo se pasa de una enumeración a otra? Sospecho que, para muchas personas, los días de la semana son un ejemplo canónico de una enumeración, pero uno podría desear razonablemente "incrementar" de miércoles a jueves. No me gustaría tener que escribir una gran declaración de cambio para eso.

(Además, con respecto a la "stringificación", la cadena correcta para un día de la semana depende del idioma y la configuración regional).

@josharian stringification generalmente significa convertir nombres de valores de enumeración en cadenas automáticamente por parte del compilador. Sin localización ni nada. Si desea construir algo además de eso, como la localización, entonces hágalo por otros medios y otros lenguajes brindan un lenguaje completo y herramientas de marco para hacerlo.

Por ejemplo, algunos tipos de C# tienen una anulación ToString que también toma información cultural. O puede usar el objeto DateTime en sí mismo y usar su método ToString que acepta tanto el formato como la información cultural. Pero estas anulaciones no son estándar, la clase object de la que todos heredan tiene solo ToString() . Más o menos como la interfaz de stringer en Go.

Así que creo que la localización debería estar fuera de esta propuesta y enumeraciones en general. Si desea implementarlo, hágalo de otra manera. Como la interfaz de stringer personalizada, por ejemplo.

@josharian Dado que, en cuanto a la implementación, seguiría siendo un int y los valores cero son todos bits cero, el valor cero sería el primer valor en el orden de origen. Eso es una especie de filtración de la intuición, pero en realidad es bastante agradable porque puede elegir el valor cero, decidiendo si una semana comienza el lunes o el domingo, por ejemplo. Por supuesto, es menos agradable que el orden de los términos restantes no tenga tanto impacto y que reordenar los valores pueda tener impactos no triviales si cambia el primer elemento. Sin embargo, esto no es realmente diferente de const/iota.

Re encordando lo que dijo @creker . Para ampliar, sin embargo, esperaría

var e enum {
  Sunday
  Monday
  //etc.
}
fmt.Println(reflect.ValueOf(e))

para imprimir domingo no 0. La etiqueta es el valor, no su representación.

Para que quede claro, no digo que deba tener un método String implícito, solo que las etiquetas se almacenen como parte del tipo y sean accesibles por reflexión. (¿Tal vez Println llama a Label() en un reflect.Value de una enumeración o algo así? No he investigado en profundidad cómo fmt hace su vudú).

¿Cómo se pasa de una enumeración a otra? Sospecho que, para muchas personas, los días de la semana son un ejemplo canónico de una enumeración, pero uno podría desear razonablemente "incrementar" de miércoles a jueves. No me gustaría tener que escribir una gran declaración de cambio para eso.

Creo que la reflexión o un gran cambio es lo correcto. Los patrones comunes se pueden completar fácilmente con go generar para crear métodos en el tipo o funciones de fábrica de ese tipo (y tal vez incluso reconocidos por el compilador para reducirlo a aritmética en la representación).

No tiene sentido para mí suponer que todas las enumeraciones tienen un orden total o que son cíclicas. Dado type failure enum { none; input; file; network } , ¿realmente tiene sentido imponer que la entrada no válida sea menor que una falla de archivo o que incrementar una falla de archivo resulte en una falla de red o que incrementar una falla de red resulte en éxito?

Suponiendo que el uso principal es para valores ordenados cíclicos, otra forma de manejar esto sería crear una nueva clase de tipos enteros parametrizados. Esta es una mala sintaxis, pero, para discusión, digamos que es I%N donde I es un tipo entero y N es una constante entera. Toda la aritmética con un valor de este tipo es implícitamente mod N. Entonces podrías hacer

type Weekday uint%7
const (
  Sunday Weekday = iota
  //etc.

entonces Sábado + 1 == Domingo y Día de la semana (456) == Lunes. Es imposible construir un día de la semana inválido. Sin embargo, podría ser útil fuera de const/iota.

Para cuando no quieres que sea un número y en absoluto, como señaló @ianlancetaylor , lo que realmente quiero son tipos de suma.

Introducir un tipo aritmético modular arbitrario es una sugerencia interesante. Luego, las enumeraciones podrían tener esta forma, lo que le da un método de cadena trivial:

var Weekdays = [...]string{"Sunday", ..., "Saturday"}

type Weekday = uint % len(Weekdays)

Combinado con ints de tamaño arbitrario, esto también te da int128, int256, etc.

También podría definir algunos incorporados:

type uint8 = uint%(1<<8)
// etc

El compilador puede probar más límites que antes. Y las API pueden proporcionar aserciones más precisas a través de tipos, por ejemplo, la función Len64 en math/bits ahora puede devolver uint % 64 .

Cuando trabajaba en un puerto RISC-V, quería un tipo uint12 , ya que mis componentes de codificación de instrucciones son de 12 bits; eso podría haber sido uint % (1<<12) . Mucha manipulación de bits, particularmente protocolos, podría beneficiarse de esto.

Las desventajas son significativas, por supuesto. Go tiende a favorecer el código sobre los tipos, y esto tiene muchos tipos. Operaciones como + y - pueden volverse repentinamente tan costosas como %. Sin parametricidad de tipo de algún tipo, probablemente tendrá que convertir al canónico uint8 , uint16 , etc. para interoperar con casi cualquier función de biblioteca, y la conversión hacia atrás puede ocultar límites fallas (a menos que tengamos una forma de hacer pánico en la conversión fuera de rango, lo que introduce su propia complejidad). Y puedo ver que se usa en exceso, por ejemplo, usando uint % 1000 para códigos de estado HTTP.

Sin embargo, una idea interesante. :)


Otras respuestas menores:

Eso es como filtrar la int-idad

Esto me hace pensar que realmente son ints. :)

Los patrones comunes se pueden completar fácilmente con go generar

Si tiene que generar código con enumeraciones de todos modos, entonces me parece que también puede generar funciones de cadena y verificaciones de límites y similares y hacer enumeraciones con generación de código en lugar del peso del soporte de lenguaje.

No tiene sentido para mí suponer que todas las enumeraciones tienen un orden total o que son cíclicas.

Lo suficientemente justo. Esto me hace pensar que tener un puñado de casos de uso concretos ayudaría a aclarar exactamente lo que queremos de las enumeraciones. Sospecho que no habrá un conjunto claro de requisitos, y que emular enumeraciones usando otras construcciones de lenguaje (es decir, el statu quo) terminará teniendo más sentido. Pero eso es sólo una hipótesis.

Re encordando lo que dijo @creker .

Lo suficientemente justo. Pero me pregunto cuántos casos terminan siendo como los días de la semana. Cualquier cosa orientada al usuario, seguro. Y la encadenación parece ser una de las principales solicitudes de enumeraciones.

Sin embargo, las enumeraciones de @josharian que son realmente ints probablemente necesitarían un mecanismo similar. De lo contrario, ¿cuánto es enum { A; B; C}(42) ?

Puede decir que es un error del compilador, pero eso no funciona en un código más complicado, ya que puede convertir desde y hacia ints en tiempo de ejecución.

Es A o pánico en tiempo de ejecución. En cualquier caso, está agregando un tipo integral con un dominio limitado. Si se trata de un pánico en tiempo de ejecución, está agregando un tipo integral que entra en pánico en el desbordamiento cuando los demás terminan. Si es A, ha agregado uint%N con alguna ceremonia.

La otra opción es dejar que no sea A, B o C, pero eso es lo que tenemos hoy con const/iota, por lo que no hay ganancias.

Todas las razones por las que dice int%N no se incluirán en el idioma parecen aplicarse igualmente a las enumeraciones que son un poco ints. (Aunque no estaría enojado de ninguna manera si se incluyera algo como ellos).

Quitar la int-idad elimina ese enigma. Requiere la generación de código para los casos en los que desea volver a agregar algo de esa intidad, pero también le da la opción de no hacerlo, lo que le permite controlar la cantidad de intness que desea introducir y de qué tipo: puede agregar no " next", un método next cíclico o un método next que devuelve un error si se cae por el borde. (Tampoco terminas con cosas como Monday*Sunday - Thursday siendo legales). La rigidez adicional lo convierte en un material de construcción más maleable. Una unión discriminada modela muy bien la variedad non-int-y: pick { A, B, C struct{} } , entre otras cosas.

Los principales beneficios de tener información como esta en el idioma son que

  1. Los valores ilegales son ilegales.
  2. la información está disponible para reflejar e ir/escribir permitiendo que los programas actúen sobre ella sin necesidad de hacer suposiciones o anotaciones (que actualmente no están disponibles para reflejar).

Los principales beneficios de tener información como esta en el idioma son los siguientes: Los valores ilegales son ilegales.

Creo que es importante enfatizar que no todos ven esto como un beneficio. Ciertamente no. A menudo lo hace más fácil al consumir valores, a menudo lo hace más difícil al producirlos. Cuál pesas más parece, hasta ahora, depende de tus preferencias personales. Por lo tanto, también lo es la cuestión de si se trata de un beneficio neto general.

Tampoco veo el sentido de prohibir los valores ilegales. Si ya tiene los medios para verificar la validez usted mismo (como en mi propuesta anterior), ¿qué beneficio brinda esa limitación? Para mí, solo complica las cosas. En mis aplicaciones, las enumeraciones para la mayoría de los casos podrían contener valores no válidos/desconocidos y tenía que solucionarlo dependiendo de la aplicación: desechar por completo, degradar a algún valor predeterminado o guardar como está.

Me imagino que las enumeraciones estrictas que no permiten valores no válidos podrían ser útiles en casos muy limitados en los que su aplicación está aislada del mundo exterior y no tiene forma de recibir una entrada no válida. Como enumeraciones internas que solo usted puede ver y usar.

const con iota no es seguro en tiempo de compilación, la verificación se retrasaría hasta el tiempo de ejecución y la verificación segura no está en el nivel de tipo. Así que creo que iota no puede reemplazar a enum literalmente, prefiero enum porque es más poderoso.

Los valores ilegales son ilegales.
Creo que es importante enfatizar que no todos ven esto como un beneficio.

No entiendo esta lógica. Los tipos son conjuntos de valores. No puede asignar un tipo a una variable cuyo valor no está en ese tipo. ¿Estoy malinterpretando algo?

PD: Estoy de acuerdo en que las enumeraciones son un caso especial de tipos de suma y ese problema debe tener prioridad sobre este.

Permítanme reformular / ser más preciso: no todos ven como un beneficio que se cierren las enumeraciones.

Si quiere ser estricto de esa manera, entonces a) "Los valores ilegales son ilegales" es una tautología yb) por lo tanto, no puede contarse como un beneficio. Con enumeraciones basadas en const, según su interpretación, los valores ilegales también son ilegales. El tipo solo permite muchos más valores.

Si las enumeraciones son enteros y cualquier entero es válido (desde el punto de vista del sistema de tipos), la única ventaja es que los valores con nombre del tipo están reflejados.

Eso es básicamente const/iota pero no tienes que ejecutar stringer ya que el paquete fmt puede obtener los nombres usando la reflexión. (Todavía tendría que ejecutar stringer si quisiera que las cadenas fueran diferentes de los nombres en la fuente).

@jimmyfrasche stringification es solo una buena ventaja. La característica principal para mí, como puede leer en mi propuesta anterior, es la capacidad de verificar si el valor dado es un valor válido del tipo de enumeración dado en tiempo de ejecución.

Por ejemplo, dado algo como esto

type Foo enum {
    Val1 = 1
    Val2 = 2
}

Y método de reflexión como

func IsValidEnum(v {}interface) bool

Podríamos hacer algo como esto

a := Foo.Val1
b := Foo(-1)
reflection.IsValidEnum(a) //returns true
reflection.IsValidEnum(b)  //returns false

Para un ejemplo del mundo real, puede mirar las enumeraciones en C# que, en mi opinión, capturaron perfectamente este término medio en lugar de seguir ciegamente lo que hizo Java. Para comprobar la validez en C#, utiliza el método estático Enum.IsDefined .

@crecker La única diferencia entre eso y const/iota es el
información almacenada en el reflejo. Eso no es mucha ganancia para un todo.
nuevo tipo de tipo.

Idea un poco loca:

Almacene los nombres y valores de todas las constantes declaradas en el mismo paquete
como su tipo definido de una manera a la que puede llegar el reflejo. Podría ser
Sin embargo, es extraño destacar esa clase estrecha de uso constante.

La característica principal para mí, como puede leer en mi propuesta anterior

En mi opinión, esto ilustra una de las cosas principales que arrastra esta discusión: la falta de claridad de cuáles son el conjunto de "características principales". Todo el mundo parece tener ideas ligeramente diferentes al respecto.
Personalmente, me sigue gustando el formato de los informes de experiencia para descubrir ese conjunto. Incluso hay uno en la lista (aunque, personalmente, aún me gustaría comentar el hecho de que la sección "Lo que salió mal" solo menciona lo que podría salir mal, no lo que realmente salió ). Tal vez sería útil agregar un par, que ilustre dónde la falta de verificación de tipo conduce a interrupciones/errores o, por ejemplo, falla en hacer refactorizaciones a gran escala.

@jimmyfrasche pero eso resuelve un gran problema en muchas aplicaciones: validar los datos de entrada. Sin ninguna ayuda del sistema de tipos, debe hacerlo a mano y eso no es algo que pueda hacer en un par de líneas de código. Tener algún tipo de validación asistida por tipos resolvería eso. Agregar cadenas además de eso simplificaría el registro, ya que tendría nombres formateados correctamente y no los valores de tipo subyacentes.

Por otro lado, hacer enumeraciones estrictas limitaría severamente los posibles casos de uso. Ahora no puedes usarlos en protocolos fácilmente, por ejemplo. Para conservar incluso los valores no válidos, tendría que descartar las enumeraciones y usar tipos de valores sin formato, posiblemente convirtiéndolos en enumeraciones más adelante si es necesario. En algunos casos, podría descartar el valor no válido y arrojar un error. En otros, puede degradar a algún valor predeterminado. De cualquier manera, está luchando con restricciones de su tipo de sistema en lugar de ayudarlo a evitar errores.

Solo mire qué protobuf para Java tiene que generar para evitar las enumeraciones de Java.

@Merovius con respecto a la validación, creo que ya lo cubrí varias veces. No sé qué más se podría agregar aparte de: sin validación, debe escribir grandes cantidades de código de copiar y pegar para validar su entrada. El problema es obvio, así como también cómo la solución propuesta podría ayudar con eso. No trabajo en una aplicación a gran escala que todos conocen, pero los errores en ese código de validación me molestaron suficientes veces en varios idiomas con el mismo concepto de enumeraciones que quiero ver que se haga algo al respecto.

Por otro lado, no veo (disculpas si me perdí algo) ningún argumento a favor de implementar enumeraciones que no permitan valores inválidos. Es bueno y ordenado en teoría, pero simplemente no veo que me ayude en aplicaciones reales.

No hay tantas características que la gente quiera de las enumeraciones. Cadena, validación, estricto/laxo en términos de valores no válidos, enumeración: eso es todo por lo que puedo ver. Todos (incluyéndome a mí, por supuesto) simplemente los mezclan en este punto. estricto/laxo parece ser el principal punto de discusión debido a su naturaleza conflictiva. No creo que todo el mundo esté de acuerdo con uno u otro. Tal vez la solución podría ser incorporar ambos de alguna manera y dejar que el programador elija, pero no conozco ningún lenguaje que tenga eso para ver cómo podría funcionar en el mundo real.

@crecker mi sugerencia para almacenar las constantes en los datos de exportación en lo anterior
las circunstancias permitirían el tipo de cosas que usted está pidiendo
sin la introducción de un nuevo tipo de tipo.

No estoy seguro de que esta sea la forma idiomática, y también soy bastante nuevo en el idioma, pero lo siguiente funciona y es conciso

type Day struct {
    value string
}

// optional, if you need string representation
func (d Day) String() string { return d.value }

var (
    Monday = Day{"Monday"}
    Tuesday = Day{"Tuesday"}
)

func main() {
    getTask(Monday)
}

func getTask(d Day) string {
    if d == Monday {
        fmt.Println("today is ", d, "!”) // today is Monday !
        return "running"
    }

    return "nothing to do"
}

ventajas :

Desventajas :

¿Realmente necesitamos enumeraciones?

¿Qué impide que alguien haga algo como esto?

NotADay := Day{"NotADay"}
getTask(NotADay)

El consumidor de una variable de este tipo puede o no captarlo con la verificación adecuada de los valores esperados (suponiendo que no se produzcan suposiciones deficientes en las declaraciones de cambio, como cualquier cosa que no sea sábado o domingo es un día de la semana, por ejemplo), pero no sería hasta el tiempo de ejecución. Creo que uno preferiría que este tipo de error se detecte en tiempo de compilación, no en tiempo de ejecución.

@bkroth
Al tener Day en su propio paquete y exponer solo los campos y métodos seleccionados, no puedo crear nuevos valores de tipo Day fuera de package day
Además, de esta manera no puedo pasar estructuras anónimas a getTask

./día/día.ir

package day

type Day struct {
    value string
}

func (d Day) String() string { return d.value }

var (
    Monday  = Day{"Monday"}
    Tuesday = Day{"Tuesday"}
    Days    = []Day{Monday, Tuesday}
)

./main.ir

package main

import (
    "fmt"
    "github.com/somePath/day"
)

func main() {
    january := day.Day{"january"} // implicit assignment of unexported field 'value' in day.Day literal

    var march struct {
        value string
    }
    march.value = "march"
    getTask(march) // cannot use march (type struct { value string }) as type day.Day in argument to getTask

    getTask(day.Monday)
}

func getTask(d day.Day) string {
    if d == day.Monday {
        fmt.Println("today is ", d, "!") // today is Monday !
        return "running"
    }

    return "nothing to do"
}

func iterateDays() {
    for _, d := range day.Days {
        fmt.Println(d)
    }
}

Nunca he visto en toda mi vida ningún otro lenguaje que insista en no agregar las funciones más simples y útiles como enumeraciones, operadores ternarios, compilación con variables no utilizadas, tipos de suma, genéricos, parámetros predeterminados, etc.

¿Es Golang un experimento social para ver cuán estúpidos pueden ser los desarrolladores?

@gh67uyyghj ¡ Alguien marcó tu comentario como fuera de tema! y supongo que alguien hará lo mismo con mi respuesta. pero supongo que la respuesta a tu pregunta es SÍ. ¡En GoLang, no tener características significa tener características, por lo que cualquier cosa que GoLang no tenga es en realidad una característica que GoLang tiene y que otros lenguajes de programación no tienen!

@ L-oris Esta es una forma muy interesante de implementar enumeraciones con tipos. Pero se siente incómodo, y tener una palabra clave enum (que necesariamente complica un poco más el lenguaje) facilitaría lo siguiente:

  • escribe
  • leer
  • razonar sobre

En su ejemplo (que es excelente porque funciona hoy), tener enumeraciones (de alguna forma) implica la necesidad de:

  • Crear un tipo de estructura
  • Crear un método
  • Crear variables (ni siquiera constantes, aunque un usuario de la biblioteca no puede cambiar esos valores)

Esto toma más tiempo (aunque no tanto) para leer, escribir y razonar (discernir que representa y debe usarse como enumeraciones).

Por lo tanto, creo que la propuesta de sintaxis da en el clavo en cuanto a sencillez y valor añadido al lenguaje.

Gracias @andradei
Sí, es una solución alternativa, pero siento que el objetivo del lenguaje es mantenerlo pequeño y simple.
También podríamos argumentar que extrañamos las clases, pero luego pasemos a Java :)

Preferiría centrarme en las propuestas de Go 2, un mejor manejo de errores, por ejemplo. me proporcionaría mucho más valor que estas enumeraciones

Volviendo a tus puntos:

  • no es tanto repetitivo; en el peor de los casos, podemos tener algunos generadores (pero, ¿realmente es tanto código?)
  • ¿Cuánta "simplicidad" estamos logrando al agregar una nueva palabra clave y todo el conjunto específico de comportamientos que probablemente tendrá?
  • ser un poco creativo con los métodos también puede agregar capacidades interesantes a esas enumeraciones
  • para la legibilidad, se trata más de acostumbrarse; tal vez agregue un comentario encima o prefije sus variables
package day

// Day Enum
type Day struct {
    value string
}

@ L-oris Ya veo. También estoy entusiasmado con las propuestas de Go 2. Yo diría que los genéricos aumentarán la complejidad del lenguaje más de lo que lo harían las enumeraciones. Pero para apegarse a sus puntos:

  • De hecho, no es tanto repetitivo
  • Tendríamos que comprobar qué tan conocido es el concepto de enumeración para estar seguros, diría que la mayoría de la gente sabe lo que es (pero no puedo probarlo). La complejidad del lenguaje estaría a un buen "precio" a pagar por sus beneficios.
  • Eso es cierto, no tener enumeraciones son problemas que solo me surgieron al revisar el código generetad protobuf y al intentar hacer un modelo de base de datos que imite una enumeración, por ejemplo.
  • Eso también es cierto.

He estado pensando mucho en esta propuesta, y puedo ver el gran valor que tiene la simplicidad en la productividad, y por qué te inclinas por mantenerla a menos que sea claramente necesario un cambio. Las enumeraciones también podrían cambiar el idioma tan drásticamente que ya no es Go, y evaluar los pros y los contras parece llevar mucho tiempo. Así que he estado pensando que las soluciones simples como la suya, donde el código aún es fácil de leer, son una buena solución al menos por ahora.

¡Chicos, realmente quieren esta función para el futuro!. Los punteros y la forma de definir "enumeraciones" en _hoy en día_ no se llevan muy bien. Por ejemplo: https://play.golang.org/p/A7rjgAMjfCx

Mi propuesta para enum es la siguiente. Deberíamos considerar esto como un nuevo tipo. Por ejemplo, me gustaría usar el tipo de enumeración con una estructura arbitraria y la siguiente implementación:

package application

type Status struct {
Name string
isFinal bool
}

enum Status {
     Started = &Status{"Started",false}
     Stopped = &Status{"Stopped",true}
     Canceled = &Status{"Canceled",true}
}

// application.Status.Start - to use

Es comprensible cómo ordenar esta estructura y cómo trabajar y cómo cambiar a cadena y así sucesivamente.
Y, por supuesto, si pudiera anular la función "Siguiente", sería genial.

Para eso, Go primero tendría que admitir estructuras inmutables profundas. Sin tipos inmutables, puedo imaginar que podría hacer esto con enumeraciones para tener lo mismo:

type Status enum {
  Started
  Stopped
}

func isFinal(s Status) bool {
  exhaustive switch(s) {
    case Started: return false;
    case Stopped: return true;
  }
}

Creo que debería verse más simple.

func isFinal(s Status) bool {
  return s == Status.Stopped
}

Propuesta para Go2

Lógicamente, se supone que las enumeraciones proporcionan una interfaz de tipo.
Indiqué que las enumeraciones anteriores debían estar separadas.
Se nombran explícitamente constantes vinculadas a un espacio de nombres específico.

enum Status uint8 {
  Started  // Status.Started == 0
  Stopped // Status.Stopped == 1, etc, like we have used iota
}
// or 
enum Status string  {
  Started // Status.Started == "Started", like it works with JSON
  Stopped // Status.Stopped == "Stopped", etc
}
// unless you wanna define its values explicitly
enum Status {
  Started "started"  // compiler can infer underlying type
  Stopped "finished"
}
// and enums are type extensions and should be used like this
type MyStatus Status

MyStatus validatedStatus // holds a nil until initialized

// for status value validation we can use map pattern
if validatedStatus, ok := MyStatus[s]; ok {
  // this value is a valid status
  // and we can use it later as regular read-only string
  // or like this
  if validatedStatus == MyStatus.Started {
     fmt.Printf("Hey, my status is %s", validatedStatus)
  }
}

Las enumeraciones son extensiones de tipo, "contenedores de constantes".

Para los amantes de la tipografía

Alternativas de sintaxis para aquellos que quieren verlo como tipo

type Status uint8 enum {
  Started  // Status.Started == 0
  Stopped // Status.Stopped == 1, etc, like we have used iota
}

Pero también podemos evitar esas declaraciones explícitas de alto nivel

type Status enum {
  Started  // Status.Started == 0
  Stopped // Status.Stopped == 1, etc, like we have used iota
}

El ejemplo de validación sigue siendo el mismo.

pero en caso

type Status1 uint8 enum {
  Started  // Status1.Started == 0
  Stopped // Status1.Stopped == 1, etc, like we have used iota
}

type Status2 uint8 enum {
  Started  // Status1.Started == 0
  Stopped // Status1.Stopped == 1, etc, like we have used iota
}

¿Qué hay de Status1.Started == Status2.Started?
sobre Mariscal?

¿Si cambio de puesto?

type Status uint8 enum {
  Started  // Status.Started == 0
  InProcess
  Stopped // Status.Stopped == 1, etc, like we have used iota
}

Estoy de acuerdo con @Goodwine sobre los tipos inmutables.

La clasificación es una pregunta interesante.
Todo esto depende de cómo vamos a tratar el valor subyacente. Si vamos a usar valores reales, entonces Status1.Started sería igual a Status2.Started .
Si vamos con la interpretación simbólica, esos serían considerados como valores diferentes.

Insertar algo provocará un cambio en los valores (exactamente de la misma manera que sucede con iota ).
Para evitar esto, el desarrollador tiene que especificar valores junto con las declaraciones.

type Status uint8 enum {
  Started  0
  InProcess 2
  Stopped 1
}

Esto es algo obvio.
Si queremos evitar tales problemas, debemos proporcionar una salida de compilador predecible basada en la interpretación léxica de los valores de enumeración. Supongo que es la forma más sencilla: crear una tabla hash o adherirse a nombres simbólicos (cadenas) a menos que se defina una conversión de tipo personalizada.

Me gusta cómo se implementa Rust Enums.

Predeterminado sin tipo especificado

enum IpAddr {
    V4,
    V6,
}

tipo personalizado

enum IpAddr {
    V4(string),
    V6(string),
}

home := IpAddr.V4("127.0.0.1");
loopback := IpAddr.V6("::1");

tipos complejos

enum Message {
    Quit,
    Move { x: int32, y: int32 },
    Write(String),
    ChangeColor(int32, int32, int32),
}

Seguro que incluso tener enumeraciones simples como en C# que se almacenan como tipos integrales sería genial.

Lo anterior va más allá enum s, esas son _uniones discriminadas_, que de hecho son más poderosas, especialmente con _coincidencia de patrones_, que podría ser una extensión menor de switch , algo así como:

switch something.(type) {
case Quit:
        ...
case ChangeColor; r, g, b := something:
        ...
case Write: // Here `something` is known to be a string
        ...
// Ideally Go would warn here about the missing case for "Move"
}

No necesito verificaciones de tiempo de compilación de enumeraciones, ya que eso podría ser peligroso como se mencionó

Lo que necesitaba varias veces habría sido iterar sobre todas las constantes de un tipo determinado:

  • ya sea para validación (si estamos muy seguros de que solo queremos aceptar esto o simplemente ignorar opciones desconocidas)

    • o para una lista de posibles constantes (piense en menús desplegables).

Podríamos hacer la validación con iota y especificando el final de la lista. Sin embargo, usar iota para cualquier otra cosa que no sea solo dentro del código sería bastante peligroso porque las cosas se romperán al insertar una constante en la línea incorrecta (sé que debemos ser conscientes de dónde ponemos las cosas en la programación, pero un error como ese es mucho más difícil de encontrar que otras cosas). Además, no tenemos una descripción de lo que realmente significa la constante cuando es un número. Eso lleva al siguiente punto:

Un buen extra sería especificar nombres de cadenas para ello.

¿Qué impide que alguien haga algo como esto?

NotADay := Day{"NotADay"}
getTask(NotADay)

El consumidor de una variable de este tipo puede o no captarlo con la verificación adecuada de los valores esperados (suponiendo que no se produzcan suposiciones deficientes en las declaraciones de cambio, como cualquier cosa que no sea sábado o domingo es un día de la semana, por ejemplo), pero no sería hasta el tiempo de ejecución. Creo que uno preferiría que este tipo de error se detecte en tiempo de compilación, no en tiempo de ejecución.

@ L-oris Entonces, ¿qué pasa con esto?

package main
import "yet/it/is/not/a/good/practice/in/Go/enum/example/day"

func main()
{
  // var foo day.Day
  foo := day.Day{}
  bar(foo)
}

func bar(day day.Day)
{
  // xxxxxxxxxx
}

¡Lo que queremos NO es SILENCIO DE TIEMPO DE EJECUCIÓN y ERROR extraño causado por [devolver "nada que hacer"] sino un INFORME DE ERROR en tiempo de compilación / tiempo de codificación !
¿COMPRENDER?

  1. enum es de hecho un tipo nuevo, que es lo que hace type State string , no hay necesidad idiomática de introducir una nueva palabra clave. Go no se trata de ahorrar espacio en su código fuente, se trata de legibilidad, claridad de propósito.

  2. La falta de seguridad de tipos, confundir los nuevos tipos basados ​​en string - o int con cadenas/ints reales es el principal obstáculo. Todas las cláusulas de enumeración se declaran como const , lo que crea un conjunto de valores conocidos que el compilador puede comparar.

  3. La interfaz Stringer es el idioma para representar cualquier tipo como texto legible por humanos. Sin personalización, las enumeraciones type ContextKey string son el valor de la cadena, y para las enumeraciones generadas por iota es el número entero, al igual que los códigos XHR ReadyState (0: sin enviar, 4: terminado) en JavaScript.

    Más bien, el problema radica en la falibilidad de la implementación personalizada de func (k ContextKey) String() string , que generalmente se realiza mediante un modificador que debe contener todas las constantes de cláusula de enumeración conocidas.

  4. En un lenguaje como Swift, existe la noción de _un cambio exhaustivo_. Este es un buen enfoque tanto para la verificación de tipos contra un conjunto de const como para crear una forma idiomática de invocar esa verificación. La función String() , siendo una necesidad común, es un gran caso para la implementación.

Propuesta

package main

import (
    "context"
    "strconv"
    "fmt"
    "os"
)

// State is an enum of known system states.
type DeepThoughtState int

// One of known system states.
const (
    Unknown DeepThoughtState = iota
    Init
    Working
    Paused
    ShutDown
)

// String returns a human-readable description of the State.
//
// It switches over const State values and if called on
// variable of type State it will fall through to a default
// system representation of State as a string (string of integer
// will be just digits).
func (s DeepThoughtState) String() string {
    // NEW: Switch only over const values for State
    switch s.(const) {
    case Unknown:
        return fmt.Printf("%d - the state of the system is not yet known", Unknown)
    case Init:
        return fmt.Printf("%d - the system is initializing", Init)
    } // ERR: const switch must be exhaustive; add all cases or `default` clause

    // ERR: no return at the end of the function (switch is not exhaustive)
}

// RegisterState allows changing the state
func RegisterState(ctx context.Context, state string) (interface{}, error) {
    next, err := strconv.ParseInt(state, 10, 32)
    if err != nil {
        return nil, err
    }
    nextState := DeepThoughtState(next)

    fmt.Printf("RegisterState=%s\n", nextState) // naive logging

        // NEW: Check dynamically if variable is a known constant
    if st, ok := nextState.(const); ok {
        // TODO: Persist new state
        return st, nil
    } else {
        return nil, fmt.Errorf("unknown state %d, new state must be one of known integers", nextState)
    }
}

func main() {
    _, err := RegisterState(context.Background(), "42")
    if err != nil {
        fmt.Println("error", err)
        os.Exit(1)
    }
    os.Exit(0)
    return
}

Los valores asociados de PS en las enumeraciones de Swift son uno de mis trucos favoritos. En Go no hay lugar para ellos. Si desea tener un valor al lado de sus datos de enumeración, use un struct fuertemente tipado que envuelva los dos.

Hace unos meses escribí una prueba de concepto para un linter que verifica que los tipos enumerados se manejen correctamente. https://github.com/loov/enumcheck

Actualmente usa comentarios para marcar cosas como enumeraciones:

type Letter byte // enumcheck

const (
    Alpha Letter = iota
    Beta
    Gamma
)

func Switch(x Letter) {
    switch x { // error: "missing cases Beta and Gamma"
    case Alpha:
        fmt.Println("alpha")
    case 4: // error: "implicit conversion of 4 to Letter"
        fmt.Println("beta")
    default: // error: "Letter shouldn't have a default case"
        fmt.Println("default")
    }
}

Me quedé atascado en descubrir cómo manejar todas las conversiones implícitas, pero funciona decentemente para casos básicos.

Tenga en cuenta que actualmente todavía es un trabajo en progreso, por lo que las cosas pueden cambiar. por ejemplo, en lugar de comentarios, podría usar algún paquete auxiliar para anotar los tipos, pero los comentarios son lo suficientemente buenos en este momento.

La implementación actual de enumeraciones en Go1 es la implementación de enumeración más extraña e inobvia en cualquier idioma que yo sepa. Incluso C los implementa mejor. Lo de iota parece un truco. ¿Y qué diablos significa iota de todos modos? ¿Cómo se supone que voy a memorizar esa palabra clave? Go se supone que es fácil de aprender. Pero eso es sólo qiurky.

@pofl :
Si bien estoy de acuerdo en que las enumeraciones de Go son bastante incómodas, iota es en realidad solo una palabra normal en inglés:

iota
_sustantivo_

  1. una cantidad muy pequeña; jota; ápice.
  2. la novena letra del alfabeto griego (I, ι).
  3. el sonido de la vocal representado por esta letra.

Presumiblemente, iban por la definición uno en términos del uso en el idioma.

En una nota al margen en respuesta a un comentario anterior aquí:
Si bien también me gustarían las uniones discriminadas en Go, creo que deberían estar separadas de las enumeraciones reales. Con la forma en que funcionan actualmente los genéricos, en realidad puede obtener algo muy similar a las uniones discriminadas a través de listas de tipos en las interfaces. Ver #41716.

El uso de iota en Go se basa libremente en su uso en APL. Citando https://en.wikipedia.org/wiki/Iota :

En algunos lenguajes de programación (p. ej., A+, APL, C++[6], Go[7]), iota (ya sea como el símbolo en minúscula ⍳ o el identificador iota) se usa para representar y generar una matriz de enteros consecutivos. Por ejemplo, en APL ⍳4 da 1 2 3 4.

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