Go: all: suporta reparo de código gradual enquanto move um tipo entre pacotes

Criado em 1 dez. 2016  ·  225Comentários  ·  Fonte: golang/go

Título original: proposta: suporte ao reparo gradual do código ao mover um tipo entre os pacotes

Go deve adicionar a capacidade de criar nomes equivalentes alternativos para tipos, a fim de permitir o reparo gradual do código durante a refatoração da base de código. Esse era o objetivo do recurso alias do Go 1.8, proposto em # 16339, mas evitado do Go 1.8. Como não resolvemos o problema para o Go 1.8, ele continua sendo um problema e espero que possamos resolvê-lo para o Go 1.9.

Na discussão da proposta de alias, houve muitas perguntas sobre por que essa capacidade de criar nomes alternativos para tipos em particular é importante. Como uma nova tentativa de responder a essas perguntas, escrevi e postei um artigo, “ Refatoração de base de código (com ajuda de Go) ”. Leia esse artigo se tiver dúvidas sobre a motivação. (Para uma apresentação alternativa mais curta, veja a palestra relâmpago de Gophercon de Robert. Infelizmente, esse vídeo não estava disponível online até 9 de outubro. Atualização, 16 de dezembro: aqui está minha palestra GothamGo , que foi essencialmente o primeiro rascunho do artigo.)

Este problema _não_ está propondo uma solução específica. Em vez disso, quero obter feedback da comunidade Go sobre o espaço de soluções possíveis. Uma maneira possível é limitar os aliases aos tipos, conforme mencionado no final do artigo. Pode haver outros que devemos considerar também.

Por favor, poste ideias sobre apelidos de tipo ou outras soluções como comentários aqui.

Obrigada.

Atualização, 16 de dezembro : Documento de design para aliases de tipo publicado .
Atualização, 9 de janeiro : Proposta aceita, repositório dev.typealias criado, implementação prevista para o início do ciclo do Go 1.9 para experimentação.


Resumo da discussão (última atualização 02/02/2017)

Esperamos precisar de uma solução geral que funcione para todas as declarações?

Se os aliases de tipo são 100% necessários, então os aliases var são talvez 10% necessários, os aliases funcionais são 1% necessários e os aliases constantes são 0% necessários. Como const já tem = e func poderia plausivelmente usar = também, a questão chave é se os aliases de var são importantes o suficiente para planejar ou implementar.

Conforme argumentado por @rogpeppe (https://github.com/golang/go/issues/16339#issuecomment-258771806) e @ianlancetaylor (https://github.com/golang/go/issues/16339#issuecomment-233644777) na proposta de alias original e conforme mencionado no artigo, uma var global mutante geralmente é um erro. Provavelmente não faz sentido complicar a solução para acomodar o que geralmente é um bug. (Na verdade, se pudermos descobrir como, não me surpreenderia se, a longo prazo, Go tomasse a direção de exigir que os vars globais fossem imutáveis.)

Como os aliases de var mais ricos provavelmente não são importantes o suficiente para planejar, parece que a escolha certa aqui é focar apenas nos aliases de tipo. A maioria dos comentários aqui parecem concordar. Não vou listar todos.

Precisamos de uma nova sintaxe (= vs => vs export)?

O argumento mais forte para a nova sintaxe é a necessidade de oferecer suporte a aliases de var, agora ou no futuro (https://github.com/golang/go/issues/18130#issuecomment-264232763 por @Merovius). Parece correto planejar não ter vários apelidos (consulte a seção anterior).

Sem var aliases, reutilizar = é mais simples do que introduzir uma nova sintaxe, seja => como na proposta de alias, ~ (https://github.com/golang/go/issues/18130#issuecomment-264185142 por @joegrasse) ou exportar (https://github.com/golang/go/issues/18130#issuecomment-264152427 por @cznic).

Usar = in também corresponderia exatamente à sintaxe dos aliases de tipo em Pascal e Rust. Na medida em que outras linguagens têm os mesmos conceitos, é bom usar a mesma sintaxe.

Olhando para o futuro, pode haver um Go futuro no qual também existam aliases de funções (consulte https://github.com/golang/go/issues/18130#issuecomment-264324306 por @nigeltao), e então todas as declarações permitiriam a mesma forma :

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

O único que não estabeleceria um alias verdadeiro seria a declaração var, porque V2 e V1 podem ser redefinidas independentemente à medida que o programa é executado (ao contrário das declarações const, func e type, que são imutáveis). Uma vez que uma das principais razões para as variáveis ​​é permitir que elas variem, essa exceção seria pelo menos fácil de explicar. Se Go se mover em direção a vars globais imutáveis, até mesmo essa exceção desapareceria.

Para ser claro, não estou sugerindo aliases de funções ou vars globais imutáveis ​​aqui, apenas trabalhando nas implicações de tais adições futuras.

@jimmyfrasche sugeriu (https://github.com/golang/go/issues/18130#issuecomment-264278398) aliases para tudo, exceto consts, de modo que const seria a exceção em vez de var:

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

Ter inconsistências com const e var parece mais difícil de explicar do que apenas ter uma inconsistência com var.

Isso pode ser uma mudança de ferramenta ou apenas do compilador em vez de uma mudança de linguagem?

Certamente vale a pena perguntar se o reparo gradual do código pode ser ativado puramente pelas informações paralelas fornecidas ao compilador (por exemplo, https://github.com/golang/go/issues/18130#issuecomment-264205929 por @btracey).

Ou talvez se o compilador puder aplicar algum tipo de pré-processamento baseado em regras para transformar arquivos de entrada antes da compilação (por exemplo, https://github.com/golang/go/issues/18130#issuecomment-264329924 por @ tux21b).

Infelizmente, não, a mudança realmente não pode ser confinada dessa forma. Existem pelo menos dois compiladores (gc e gccgo) que precisam ser coordenados, mas o mesmo aconteceria com outras ferramentas que analisam programas, como go vet, guru, goimports, gocode (conclusão de código) e outros.

Como @bcmills disse (https://github.com/golang/go/issues/18130#issuecomment-264275574), “um mecanismo de 'não mudança de idioma' que deve ser suportado por todas as implementações é uma mudança de idioma de fato - é apenas um com documentação mais pobre. ”

Que outros usos os apelidos podem ter?

Nós sabemos o seguinte. Dado que os aliases de tipo em particular foram considerados importantes o suficiente para inclusão em Pascal e Rust, provavelmente existem outros.

  1. Aliases (ou apenas digitar aliases) permitiriam a criação de substitutos drop-in que expandem outros pacotes. Por exemplo, consulte https://go-review.googlesource.com/#/c/32145/ , especialmente a explicação na mensagem de confirmação.

  2. Aliases (ou apenas aliases de tipo) permitiriam estruturar um pacote com uma pequena superfície de API, mas uma grande implementação como uma coleção de pacotes para melhor estrutura interna, mas ainda apresentaria apenas um pacote a ser importado e usado pelos clientes. Há um exemplo um tanto abstrato descrito em https://github.com/golang/go/issues/16339#issuecomment -232813695.

  3. Os buffers de protocolo têm um recurso de "importação pública" cuja semântica é trivial para implementar no código C ++ gerado, mas impossível de implementar no código Go gerado. Isso causa frustração para os autores de definições de buffer de protocolo compartilhadas entre clientes C ++ e Go. Os apelidos de tipo forneceriam uma maneira de Go implementar esse recurso. Na verdade, o caso de uso original para o público de importação era o reparo gradual do código . Problemas semelhantes podem surgir em outros tipos de geradores de código.

  4. Abreviando nomes longos. Aliases locais (não exportados ou sem escopo de pacote) podem ser úteis para abreviar um nome de tipo longo sem introduzir a sobrecarga de um tipo totalmente novo. Como acontece com todos esses usos, a clareza do código final influenciaria fortemente se este é um uso sugerido.

Que outras questões uma proposta de apelidos de tipo precisa abordar?

Listando-os para referência. Não tentar resolvê-los ou discuti-los nesta seção, embora alguns tenham sido discutidos posteriormente e resumidos em seções separadas abaixo.

  1. Manipulação em godoc. (https://github.com/golang/go/issues/18130#issuecomment-264323137 por @nigeltao e https://github.com/golang/go/issues/18130#issuecomment-264326437 por @jimmyfrasche)

  2. Os métodos podem ser definidos em tipos nomeados por alias? (https://github.com/golang/go/issues/18130#issuecomment-265077877 por @ulikunitz)

  3. Se apelidos para apelidos são permitidos, como tratamos os ciclos de apelidos? (https://github.com/golang/go/issues/18130#issuecomment-264494658 por @thwd)

  4. Os aliases devem ser capazes de exportar identificadores não exportados? (https://github.com/golang/go/issues/18130#issuecomment-264494658 por @thwd)

  5. O que acontece quando você incorpora um alias (como você acessa o campo incorporado)? (https://github.com/golang/go/issues/18130#issuecomment-264494658 por @thwd , também # 17746)

  6. Os apelidos estão disponíveis como símbolos no programa integrado? (https://github.com/golang/go/issues/18130#issuecomment-264494658 por @thwd)

  7. Injeção de string Ldflags: e se nos referirmos a um alias? (https://github.com/golang/go/issues/18130#issuecomment-264494658 por @thwd; isso só surge se houver vários aliases.)

O controle de versão é uma solução por si só?

"Nesse caso, talvez o controle de versão seja a resposta completa, não os apelidos de tipo."
(https://github.com/golang/go/issues/18130#issuecomment-264573088 por @iainmerrick)

Conforme observado no artigo , acho que o versionamento é uma preocupação complementar. O suporte para reparo de código gradual, como com aliases de tipo, dá a um sistema de controle de versão mais flexibilidade em como ele constrói um grande programa, o que pode ser a diferença entre ser capaz de construir o programa ou não.

Em vez disso, o problema maior de refatoração pode ser resolvido?

Em https://github.com/golang/go/issues/18130#issuecomment -265052639, @niemeyer aponta que, na verdade, houve duas mudanças para mover os.Error para erro: o nome mudou, mas também a definição (o atual Método de erro costumava ser um método String).

@niemeyer sugere que talvez possamos encontrar uma solução para o problema de refatoração mais amplo que corrige tipos que se movem entre pacotes como um caso especial, mas também lida com coisas como mudança de nomes de métodos, e ele propõe uma solução construída em torno de "adaptadores".

Há uma boa discussão nos comentários que não posso resumir facilmente aqui. A discussão não acabou, mas até agora não está claro se os "adaptadores" podem caber na linguagem ou ser implementados na prática. Parece claro que os adaptadores são pelo menos uma ordem de magnitude mais complexos do que os aliases de tipo.

Os adaptadores também precisam de uma solução coerente para os problemas de subtipagem observados abaixo.

Os métodos podem ser declarados em tipos de alias?

Certamente os apelidos não permitem contornar as restrições usuais de definição de método: se um pacote define o tipo T1 = otherpkg.T2, ele não pode definir métodos em T1, assim como não pode definir métodos diretamente em otherpkg.T2. Ou seja, se tipo T1 = otherpkg.T2, então func (T1) M () é equivalente a func (otherpkg.T2) M (), que é inválido hoje e permanece inválido. No entanto, se um pacote define o tipo T1 = T2 (ambos no mesmo pacote), a resposta é menos clara. Nesse caso, func (T1) M () seria equivalente a func (T2) M (); uma vez que o último é permitido, há um argumento para permitir o primeiro. O documento de design atual não impõe uma restrição aqui (de acordo com a prevenção geral de restrições), de modo que func (T1) M () é válido nesta situação.

Em https://github.com/golang/go/issues/18130#issuecomment -267694112, @jimmyfrasche sugere que, em vez disso, definir "nenhum uso de apelidos nas definições de método" seria uma regra clara e evitaria a necessidade de saber o que T está definido para saber se func (T) M () é válido. Em https://github.com/golang/go/issues/18130#issuecomment -267997124, @rsc aponta que ainda hoje existem certos T para os quais func (T) M () não é válido: https: // play .golang.org / p / bci2qnldej. Na prática, isso não acontece porque as pessoas escrevem um código razoável.

Manteremos essa possível restrição em mente, mas espere até que haja fortes evidências de que ela é necessária antes de introduzi-la.

Existe uma maneira mais limpa de lidar com a incorporação e, de maneira mais geral, com a renomeação de campos?

Em https://github.com/golang/go/issues/18130#issuecomment -267691816, @Merovius aponta que um tipo incorporado que muda de nome durante a movimentação de um pacote causará problemas quando esse novo nome eventualmente for adotado no usar sites. Por exemplo, se o tipo de usuário U tem um io.ByteBuffer incorporado que se move para bytes.Buffer, enquanto U incorpora io.ByteBuffer, o nome do campo é U.ByteBuffer, mas quando U é atualizado para se referir a bytes.Buffer, o nome do campo necessariamente muda para U.Buffer.

Em https://github.com/golang/go/issues/18130#issuecomment -267710478, @neild aponta que há pelo menos uma solução alternativa se as referências a io.ByteBuffer devem ser removidas: o pacote P que define U também pode defina 'type ByteBuffer = bytes.Buffer' e incorpore esse tipo em U. Então U ainda terá um U.ByteBuffer, mesmo depois que io.ByteBuffer tiver desaparecido completamente.

Em https://github.com/golang/go/issues/18130#issuecomment -267703067, @bcmills sugere a ideia de aliases de campo, para permitir que um campo tenha vários nomes durante um reparo gradual. Os apelidos de campo permitiriam definir algo como type U struct { bytes.Buffer; ByteBuffer = Buffer } vez de criar o apelido de tipo de nível superior.

Em https://github.com/golang/go/issues/18130#issuecomment -268001111, @rsc levanta ainda outra possibilidade: alguma sintaxe para 'embutir este tipo com este nome', de modo que seja possível embutir bytes. Buffer como o nome de campo ByteBuffer, sem a necessidade de um tipo de nível superior ou um nome alternativo. Se isso existisse, o nome do tipo poderia ser atualizado de io.ByteBuffer para bytes.Buffer preservando o nome original (e não introduzindo um segundo, nem um tipo exportado desajeitado).

Tudo isso parece valer a pena explorar, uma vez que tenhamos mais evidências de refatorações em grande escala bloqueadas por problemas com campos que mudam de nome. Como escreveu @rsc , "Se os aliases de tipo nos ajudarem a chegar ao ponto em que a falta de aliases de campo é o próximo grande obstáculo para refatorações em grande escala, isso será um progresso!"

Houve uma sugestão de restringir o uso de apelidos em campos embutidos ou alterar o nome embutido para usar o nome do tipo de destino, mas aqueles fazem a introdução do apelido quebrar as definições existentes que devem ser corrigidas atomicamente, essencialmente evitando qualquer reparo gradual. @rsc : "Discutimos isso detalhadamente no # 17746. Originalmente, eu estava do lado do nome de um io.ByteBuffer alias incorporado sendo Buffer, mas o argumento acima me convenceu de que eu estava errado. @jimmyfrasche em particular fez alguns bons argumentos sobre o código não mudar dependendo da definição da coisa incorporada. Não acho que seja sustentável proibir completamente os aliases incorporados. "

Qual é o efeito em programas que usam reflexão?

Programas que usam reflexão vêem através de apelidos. Em https://github.com/golang/go/issues/18130#issuecomment -267903649, @atdiar aponta que se um programa está usando reflexão para, por exemplo, encontrar o pacote em que um tipo está definido ou mesmo o nome de um tipo, ele observará a mudança quando o tipo for movido, mesmo se um alias de encaminhamento for deixado para trás. Em https://github.com/golang/go/issues/18130#issuecomment -268001410, @rsc confirmou isso e escreveu "Como a situação com incorporação, não é perfeito. Ao contrário da situação com incorporação, não tenho nenhum respostas, exceto que talvez o código não deva ser escrito usando refletir para ser tão sensível a esses detalhes. "

O uso de pacotes vendidos hoje também muda os caminhos de importação de pacotes vistos pelo reflect, e não fomos informados sobre os problemas significativos causados ​​por essa ambigüidade. Isso sugere que os programas não costumam inspecionar o reflect.Type.PkgPath de maneiras que seriam interrompidas pelo uso de apelidos. Mesmo assim, é uma lacuna potencial, assim como a incorporação.

Qual é o efeito na compilação separada de programas e plug-ins?

Em https://github.com/golang/go/issues/18130#issuecomment -268524504, @atdiar levanta a questão do efeito em arquivos de objeto e compilação separada. Em https://github.com/golang/go/issues/18130#issuecomment -268560180, @rsc responde que não deve haver necessidade de fazer alterações aqui: se X importa as alterações de Y e Y e é recompilado, então X precisa ser recompilado também. Isso é verdade hoje sem aliases e permanecerá verdadeiro com aliases. Compilação separada significa ser capaz de compilar X e Y em etapas distintas (o compilador não precisa processá-los na mesma invocação), não que seja possível alterar Y sem recompilar X.

Os sum types ou algum tipo de subtipagem seriam uma solução alternativa?

Em https://github.com/golang/go/issues/18130#issuecomment -264413439, @iand sugere "tipos substituíveis", "uma lista de tipos que podem ser substituídos pelo tipo nomeado em argumentos de função, valores de retorno etc. " Em https://github.com/golang/go/issues/18130#issuecomment -268072274, @ j7b sugere o uso de tipos algébricos "para que também obtenhamos uma interface vazia equivalente com verificação de tipo em tempo de compilação como um bônus". Outros nomes para este conceito são tipos de soma e tipos de variantes.

Em geral, isso não é suficiente para permitir a movimentação de tipos com reparo de código gradual. Existem duas maneiras de pensar sobre isso.

Em https://github.com/golang/go/issues/18130#issuecomment -268075680, @bcmills toma o caminho concreto, apontando que os tipos algébricos têm uma representação diferente da original, o que não permite o tratamento da soma e o original intercambiável: o último possui tags de tipo.

Em https://github.com/golang/go/issues/18130#issuecomment -268585497, @rsc segue o caminho teórico, expandindo em https://github.com/golang/go/issues/18130#issuecomment -265211655 por @gri apontando que em um reparo de código gradual, às vezes você precisa que T1 seja um subtipo de T2 e às vezes vice-versa. A única maneira de ambos serem subtipos um do outro é serem do mesmo tipo, o que, não por acaso, é o que os apelidos de tipo fazem.

Como uma tangente lateral, além de não resolver o problema de reparo gradual do código, os tipos algébricos / tipos de soma / tipos de união / tipos de variantes são por si só difíceis de adicionar ao Go. Ver
a resposta ao FAQ e a discussão do

Em https://github.com/golang/go/issues/18130#issuecomment -265206780, @thwd sugere que, uma vez que Go tem uma relação de subtipagem entre tipos concretos e interfaces (bytes.Buffer pode ser visto como um subtipo de io.Reader ) e entre interfaces (io.ReadWriter é um subtipo de io.Reader da mesma forma), tornar as interfaces "covariáveis ​​recursivamente (de acordo com as regras de variação atuais) até seus argumentos de método" resolveria o problema, desde que todos os pacotes futuros apenas use interfaces, nunca tipos concretos como structs ("também incentiva um bom design").

Existem três problemas com isso como solução. Primeiro, ele tem os problemas de subtipagem acima, portanto, não resolve o reparo gradual do código. Em segundo lugar, não se aplica ao código existente, como @thwd observou nesta sugestão. Terceiro, forçar o uso de interfaces em todos os lugares pode não ser realmente um bom design e introduz sobrecargas de desempenho (consulte por exemplo https://github.com/golang/go/issues/18130#issuecomment-265211726 por @Merovius e https: // github .com / golang / go / issues / 18130 # issuecomment-265224652 por @zombiezen).

Restrições

Esta seção coleta as restrições propostas para referência, mas lembre-se de que as restrições aumentam a complexidade. Como escrevi em https://github.com/golang/go/issues/18130#issuecomment -264195616, "provavelmente só devemos implementar essas restrições após a experiência real com o design irrestrito e mais simples nos ajuda a entender se a restrição traria o suficiente benefícios para pagar seus custos. "

Dito de outra forma, qualquer restrição precisaria ser justificada por evidências de que evitaria algum mau uso ou confusão grave. Como ainda não implementamos uma solução, não há tal evidência. Se a experiência forneceu essa evidência, valerá a pena retornar a ela.

Restrição? Aliases de tipos de biblioteca padrão só podem ser declarados na biblioteca padrão.

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

A preocupação é "código que renomeou conceitos de biblioteca padrão para se ajustar a uma convenção de nomenclatura personalizada" ou "longas cadeias de aliases em vários pacotes que acabam voltando para a biblioteca padrão" ou "aliasing coisas como interface {} e erro" .

Conforme declarado, a restrição não permitiria o caso de "pacote de extensão" descrito acima envolvendo x / image / draw.

Não está claro por que a biblioteca padrão deve ser especial: os problemas existiriam com qualquer código. Além disso, nem a interface {} nem o erro são um tipo da biblioteca padrão. Reformular a restrição como "aliasing de tipos predefinidos" não permitiria o erro de aliasing, mas a necessidade de aliasar o erro foi um dos exemplos motivadores no artigo.

Restrição? O destino do alias deve ser um identificador qualificado do pacote.

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

Isso tornaria impossível criar um alias ao renomear um tipo dentro de um pacote, que pode ser usado amplamente o suficiente para exigir um reparo gradual (https://github.com/golang/go/issues/18130#issuecomment-264274714 por @ bcmills).

Também não permitiria o erro de aliasing como no artigo.

Restrição? O destino do alias deve ser um identificador qualificado do pacote com o mesmo nome do alias.

(proposto durante a discussão de alias no Go 1.8)

Além dos problemas da seção anterior com a limitação de identificadores qualificados de pacote, forçar o nome a permanecer o mesmo não permitiria a conversão de io.ByteBuffer para bytes.Buffer no artigo.

Restrição? Os apelidos devem ser desencorajados de alguma forma.

"Que tal ocultar aliases atrás de uma importação, assim como para" C "e" inseguro ", para desencorajar ainda mais seu uso? Na mesma linha, gostaria que a sintaxe dos aliases fosse prolixa e se destacasse como um suporte para refatoração contínua . " - https://github.com/golang/go/issues/18130#issuecomment -264289940 por @xiegeo

"Devemos também inferir automaticamente que um tipo de alias é legado e deve ser substituído pelo novo tipo? Se aplicarmos golint, godoc e ferramentas semelhantes para visualizar o tipo antigo como obsoleto, isso limitaria o abuso de aliasing de tipo muito significativamente. E a preocupação final de usar o recurso de aliasing seria resolvida. " - https://github.com/golang/go/issues/18130#issuecomment -265062154 por @rakyll

Até que saibamos que eles serão usados ​​incorretamente, parece prematuro desencorajar o uso. Pode haver usos bons e não temporários (veja acima).

Mesmo no caso de reparo de código, o tipo antigo ou novo pode ser o alias durante a transição, dependendo das restrições impostas pelo gráfico de importação. Ser um alias não significa que o nome está obsoleto.

Já existe um mecanismo para marcar certas declarações como obsoletas (consulte https://github.com/golang/go/issues/18130#issuecomment-265294564 por @jimmyfrasche).

Restrição? Os aliases devem ser direcionados a tipos nomeados.

"Os apelidos não devem se aplicar a tipos não nomeados. Não é uma história de" reparo de código "na mudança de um tipo não nomeado para outro. Permitir apelidos em tipos não nomeados significa que não posso mais ensinar Go como tipos simplesmente nomeados e não nomeados." - https://github.com/golang/go/issues/18130#issuecomment -276864903 por @davecheney

Até que saibamos que eles serão usados ​​incorretamente, parece prematuro desencorajar o uso. Pode haver bons usos com destinos não nomeados (veja acima).

Conforme observado no documento de design, esperamos mudar a terminologia para tornar a situação mais clara.

FrozenDueToAge Proposal Proposal-Accepted

Comentários muito úteis

@cznic , @iand , outros: Observe que _restrições adicionam complexidade_. Eles complicam a explicação do recurso e adicionam carga cognitiva para qualquer usuário do recurso: se você esquecer uma restrição, terá que descobrir por que algo que você achava que deveria funcionar não funciona.

Freqüentemente, é um erro implementar restrições em um teste de design apenas devido ao uso incorreto hipotético. Isso aconteceu nas discussões da proposta de alias e fez com que os aliases no teste não conseguissem lidar com a conversão de io.ByteBuffer => bytes.Buffer do artigo. Parte do objetivo de escrever o artigo é definir alguns casos que sabemos que queremos ser capazes de lidar, para que não os restrinjam inadvertidamente.

Como outro exemplo, seria fácil fazer um argumento de uso indevido para proibir receptores não-ponteiro ou para proibir métodos em tipos não-struct. Se tivéssemos feito qualquer um desses, você não poderia criar enums com métodos String () para imprimir eles próprios, e você não poderia ter http.Headers um mapa simples e fornecer métodos auxiliares. Freqüentemente, é fácil imaginar usos indevidos; usos positivos convincentes podem demorar mais para aparecer, e é importante criar espaço para experimentação.

Como outro exemplo, o projeto original e a implementação dos métodos de ponteiro vs valor não distinguiam entre os conjuntos de métodos em T e * T: se você tivesse um * T, poderia chamar os métodos de valor (receptor T), e se tivesse a T, você poderia chamar os métodos de ponteiro (receptor * T). Isso era simples, sem restrições para explicar. Mas a experiência real nos mostrou que permitir chamadas de método de ponteiro em valores levava a uma classe específica de bugs confusos e surpreendentes. Por exemplo, você pode escrever:

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

e io.Copy teria sucesso, mas buf não teria nada nele. Tivemos que escolher entre explicar por que aquele programa foi executado incorretamente ou explicar por que o programa não compilou. De qualquer maneira, haveria perguntas, mas optamos por evitar a execução incorreta. Mesmo assim, ainda tivemos que escrever uma entrada de FAQ sobre por que o design tem um buraco.

Novamente, lembre-se de que as restrições adicionam complexidade. Como toda complexidade, as restrições precisam de justificativas significativas. Neste estágio do processo de design, é bom pensar sobre as restrições que podem ser apropriadas para um determinado design, mas provavelmente só devemos implementar essas restrições após a experiência real com o design irrestrito e mais simples nos ajuda a entender se a restrição traria benefícios suficientes para pagar seu custo.

Todos 225 comentários

Eu gosto de como isso parece visualmente uniforme.

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

Mas como podemos mover quase gradualmente a maioria dos elementos, talvez o mais simples
a solução _é_ permitir apenas = para tipos.

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

Então, primeiro, eu só queria agradecer por esse excelente artigo. Acho que a melhor solução é introduzir aliases de tipo com um operador de atribuição. Isso não requer novas palavras-chave / operadores, usa uma sintaxe familiar e deve resolver o problema de refatoração para grandes bases de código.

Como o artigo de Russ aponta, qualquer solução semelhante a um alias precisa resolver normalmente https://github.com/golang/go/issues/17746 e https://github.com/golang/go/issues/17784

Obrigado por escrever esse artigo.

Acho que os aliases somente de tipo usando o operador de atribuição são os melhores:

type OldAPI = NewPackage.API

Meus motivos:

  • É mais simples.
    A solução alternativa => tendo um significado sutilmente diferente com base em seu operando parece deslocada para Go.
  • É focado e conservador.
    O problema em questão com os tipos está resolvido e você não precisa se preocupar em imaginar as complicações da solução generalizada.
  • É estético.
    Eu acho que parece mais agradável.

Tudo isso acima: o resultado sendo simples, focado, conservador e estético torna mais fácil para mim imaginar que ele faz parte do Go.

Se a solução fosse limitada a apenas tipos, a sintaxe

type NewFoo = old.Foo

já considerado antes, conforme discutido no artigo do @rsc , parece muito bom para mim.

Se quisermos fazer o mesmo para constantes, variáveis ​​e funções, minha sintaxe preferida seria (como proposto antes)

package newfmt

import (
    "fmt"
)

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

export (
        fmt.Sprintf
        fmt.Formatter
)

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

export (
    Bar fmt.Fprintf
    Qux fmt.State
)

Como discutido antes, a desvantagem é que uma nova palavra-chave de nível superior é introduzida, o que é reconhecidamente difícil, embora tecnicamente viável e totalmente compatível com versões anteriores. Gosto dessa sintaxe porque ela reflete o padrão das importações. Parece-me natural que as exportações sejam permitidas apenas na mesma seção em que as importações são permitidas, ou seja, entre a cláusula do pacote e qualquer var, tipo, constante ou TLD de função.

Os identificadores de renomeação seriam declarados no escopo do pacote, entretanto, os novos nomes não são visíveis no pacote que os declara (newfmt no exemplo acima) acima com relação à redeclaração, que é desaprovada normalmente. Dado o exemplo anterior, TLDs

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

No pacote de importação, os identificadores de renomeação são visíveis normalmente, como qualquer outro identificador exportado do bloco de pacote (newftm).

package foo

import "newfmt"

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

Em conclusão, esta abordagem não introduz nenhuma nova vinculação de nome local em newfmt, o que eu acredito que evita pelo menos alguns dos problemas discutidos em # 17746 e resolve # 17784 completamente.

Minha primeira preferência é por um tipo apenas type NewFoo = old.Foo .

Se uma solução mais geral for desejada, concordo com @cznic que uma palavra-chave dedicada é melhor do que um novo operador (especialmente um operador assimétrico com direcionalidade confusa [1]). Dito isso, não acho que a palavra-chave export transmita o significado correto. Nem a sintaxe, nem a semântica espelham import . E quanto a alias ?

Eu entendo porque @cznic não quer que os novos nomes sejam acessíveis no pacote que os declara, mas, pelo menos para mim, essa restrição parece inesperada e artificial (embora eu entenda perfeitamente a razão por trás disso).

[1] Uso Unix há quase 20 anos e ainda não consigo criar um link simbólico na primeira tentativa. E geralmente falho mesmo na segunda tentativa, depois de ler o manual.

Eu gostaria de propor uma restrição adicional: aliases de tipo para tipos de biblioteca padrão só podem ser declarados na biblioteca padrão.

Meu raciocínio é que não quero trabalhar com código que renomeou conceitos de biblioteca padrão para se ajustar a uma convenção de nomenclatura personalizada. Eu também não quero lidar com longas cadeias de aliases em vários pacotes que acabam voltando para a biblioteca padrão.

@iand : Essa restrição bloquearia o uso deste recurso para migrar qualquer coisa para a biblioteca padrão. Caso em questão, a migração atual de Context para a biblioteca padrão. A antiga casa de Context deve se tornar um apelido para Context na biblioteca padrão.

@quentinmit isso infelizmente é verdade. Também limita o caso de uso para golang.org/x/image/draw neste CL https://go-review.googlesource.com/#/c/32145/

Minha verdadeira preocupação é com as pessoas criando nomes como interface{} e error

Se for decidido introduzir um novo operador, gostaria de propor ~ . No idioma inglês, é geralmente entendido como "semelhante a", "aproximadamente", "cerca de" ou "ao redor". Como @ 4ad acima declarado, o => é um operador assimétrico com direcionalidade confusa.

Por exemplo:

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

@iand se limitarmos o lado direito a um identificador qualificado de pacote, isso eliminaria sua preocupação específica.

Também significaria que você não poderia ter apelidos para nenhum tipo no pacote atual ou para expressões de tipo longo como map[string]map[int]interface{} . Mas esses usos não têm nada a ver com o objetivo principal de reparo gradual do código, então talvez não sejam uma grande perda.

@cznic , @iand , outros: Observe que _restrições adicionam complexidade_. Eles complicam a explicação do recurso e adicionam carga cognitiva para qualquer usuário do recurso: se você esquecer uma restrição, terá que descobrir por que algo que você achava que deveria funcionar não funciona.

Freqüentemente, é um erro implementar restrições em um teste de design apenas devido ao uso incorreto hipotético. Isso aconteceu nas discussões da proposta de alias e fez com que os aliases no teste não conseguissem lidar com a conversão de io.ByteBuffer => bytes.Buffer do artigo. Parte do objetivo de escrever o artigo é definir alguns casos que sabemos que queremos ser capazes de lidar, para que não os restrinjam inadvertidamente.

Como outro exemplo, seria fácil fazer um argumento de uso indevido para proibir receptores não-ponteiro ou para proibir métodos em tipos não-struct. Se tivéssemos feito qualquer um desses, você não poderia criar enums com métodos String () para imprimir eles próprios, e você não poderia ter http.Headers um mapa simples e fornecer métodos auxiliares. Freqüentemente, é fácil imaginar usos indevidos; usos positivos convincentes podem demorar mais para aparecer, e é importante criar espaço para experimentação.

Como outro exemplo, o projeto original e a implementação dos métodos de ponteiro vs valor não distinguiam entre os conjuntos de métodos em T e * T: se você tivesse um * T, poderia chamar os métodos de valor (receptor T), e se tivesse a T, você poderia chamar os métodos de ponteiro (receptor * T). Isso era simples, sem restrições para explicar. Mas a experiência real nos mostrou que permitir chamadas de método de ponteiro em valores levava a uma classe específica de bugs confusos e surpreendentes. Por exemplo, você pode escrever:

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

e io.Copy teria sucesso, mas buf não teria nada nele. Tivemos que escolher entre explicar por que aquele programa foi executado incorretamente ou explicar por que o programa não compilou. De qualquer maneira, haveria perguntas, mas optamos por evitar a execução incorreta. Mesmo assim, ainda tivemos que escrever uma entrada de FAQ sobre por que o design tem um buraco.

Novamente, lembre-se de que as restrições adicionam complexidade. Como toda complexidade, as restrições precisam de justificativas significativas. Neste estágio do processo de design, é bom pensar sobre as restrições que podem ser apropriadas para um determinado design, mas provavelmente só devemos implementar essas restrições após a experiência real com o design irrestrito e mais simples nos ajuda a entender se a restrição traria benefícios suficientes para pagar seu custo.

Além disso, minha esperança é que possamos chegar a uma decisão provisória sobre o que tentar e, em seguida, ter algo pronto para experimentação no início do ciclo Go 1.9 (idealmente no dia em que o ciclo se abre). Ter mais tempo para experimentar trará muitos benefícios, entre eles a oportunidade de saber se uma determinada restrição é convincente. Um erro com o alias foi não cometer uma implementação completa até perto do final do ciclo do Go 1.8.

Uma coisa sobre a proposta de alias original é que, no caso de uso pretendido (habilitando a refatoração), o uso real do tipo de alias deve ser apenas temporário. No exemplo do protobuffer, o stub io.BytesBuffer foi excluído assim que o reparo gradual foi concluído.

Se o mecanismo de alias deve ser visto apenas temporariamente, ele realmente requer uma mudança de idioma? Em vez disso, talvez pudesse haver um mecanismo para fornecer gc com uma lista de "apelidos". O gc pode fazer as substituições temporariamente, e o autor da base de código downstream pode remover gradualmente os itens desse arquivo à medida que as correções são mescladas. Sei que essa sugestão também tem consequências complicadas, mas pelo menos incentiva um mecanismo temporário.

Não participarei de bicicletas sobre sintaxe (basicamente não me importo), com uma exceção: se for decidido adicionar aliases e se for decidido restringi-los a tipos, use uma sintaxe que seja consistentemente extensível a pelo menos var , senão também func e const (todas as construções sintáticas propostas permitem todos, exceto type Foo = pkg.Bar ). A razão é que, embora eu concorde que os casos em que os aliases para var façam a diferença podem ser raros, não acho que eles sejam inexistentes e, como tal, acredito que podemos muito bem em algum momento decidir adicionar eles também. Nesse ponto, definitivamente queremos que todas as declarações de alias sejam consistentes, seria ruim se fosse type Foo = pkg.Bar e var Foo => pkg.Bar .

Eu também argumentaria ligeiramente por ter todos os quatro. As razões são

1) uma distinção para var e eu às vezes usá-lo. Por exemplo, frequentemente exponho um var Debug *log.Logger global ou reatribuo singletons globais como http.DefaultServeMux para interceptar / remover registros de pacotes que adicionam manipuladores a ele.

2) Eu também acho que, embora func Foo() { pkg.Bar() } faça a mesma coisa que func Foo => pkg.Bar , a intenção do último é muito mais clara (especialmente se você já conhece pseudônimos). Afirma claramente "isso não é realmente para estar aqui". Portanto, embora seja tecnicamente idêntica, a sintaxe do alias pode servir como documentação.

Não é a colina em que eu morreria, no entanto; Os aliases de tipo sozinhos, por enquanto, estão bem para mim, contanto que haja a opção de estendê-los mais tarde.

Eu também estou muito feliz que isso tenha sido escrito como estava. Ele resume várias opiniões que tive sobre o design e a estabilidade da API por um tempo e, no futuro, servirá como uma referência simples para conectar pessoas também :)

No entanto, também quero enfatizar que há casos de uso adicionais cobertos por aliases que são diferentes do doc (e AIUI a intenção mais geral deste problema, que é encontrar alguma solução para resolver o reparo gradual). Fico muito feliz se a comunidade pode concordar com o conceito de permitir o reparo gradual, mas se uma decisão diferente dos aliases for decidida para alcançá-la, eu também acho que, nesse caso, deveria haver simultaneamente uma conversa sobre se e como apoiar coisas como as importações públicas de protobuf ou o caso de uso x/image/draw de pacotes de substituição instantâneos (ambos um tanto próximos ao meu coração também) com uma solução diferente. A proposta da @btracey de um sinalizador go-tool / gc para aliases é um exemplo em que acredito que, embora cubra o reparo gradual relativamente bem, não é realmente aceitável para esses outros casos de uso. Você realmente não pode esperar que todos que desejam compilar algo que usa x/image/draw passem esses sinalizadores, eles devem apenas ser capazes de go get .

@jba

@iand se limitarmos o lado direito a um identificador qualificado de pacote, isso eliminaria sua preocupação específica.

Também significaria que você não poderia ter apelidos para nenhum tipo no pacote atual, […]. Mas esses usos não têm nada a ver com o objetivo principal de reparo gradual do código, então talvez não sejam uma grande perda.

Renomear dentro de um pacote (por exemplo, para um nome mais idiomático ou consistente) é certamente um tipo de refatoração que alguém pode razoavelmente querer fazer, e se o pacote for amplamente usado, isso requer um reparo gradual.

Eu acho que uma restrição para apenas nomes qualificados de pacote seria um erro. (Uma restrição apenas para nomes exportados pode ser mais tolerável.)

@btracey

Em vez disso, talvez pudesse haver um mecanismo para fornecer ao gc uma lista de "apelidos". O gc pode fazer as substituições temporariamente, e o autor da base de código downstream pode remover gradualmente os itens desse arquivo à medida que as correções são mescladas.

Um mecanismo para gc significaria que o código só pode ser compilado com gc durante o processo de reparo ou que o mecanismo teria que ser suportado por outros compiladores (por exemplo, gccgo e llgo ) também. Um mecanismo de "não mudança de idioma" que deve ser suportado por todas as implementações é uma mudança de idioma de fato - é apenas uma com documentação mais pobre.

@btracey e @bcmills , e não apenas os compiladores: qualquer ferramenta que analise o código-fonte, como o guru ou qualquer outra coisa que as pessoas tenham construído. Certamente é uma mudança de idioma, não importa como você o faça.

Ok, obrigado.

Outra possibilidade são apelidos para tudo, exceto consts (e @rsc, por favor, me perdoe por propor uma restrição!)

Para consts, => é realmente apenas uma maneira mais longa de escrever = . Não há nenhuma nova semântica, como com tipos e vars. Não há pressionamentos de tecla salvos como nas funções.

Isso resolveria pelo menos # 17784.

O contra-argumento seria que o ferramental poderia tratar os casos de maneira diferente e poderia ser um indicador de intenção. Esse é um bom contra-argumento, mas não acho que supere o fato de serem basicamente duas maneiras de fazer exatamente a mesma coisa.

Dito isso, por enquanto, estou bem com apenas aliases de tipo, eles são certamente os mais importantes. Eu definitivamente concordo com @Merovius que devemos considerar fortemente manter a opção de adicionar aliases de var e func no futuro, mesmo que isso não aconteça por algum tempo.

Que tal esconder aliases atrás de uma importação, assim como para "C" e "inseguro", para desencorajar ainda mais seu uso? Na mesma linha, gostaria que a sintaxe dos aliases fosse prolixa e se destacasse como uma estrutura para refatoração contínua.

Na tentativa de abrir um pouco o espaço do design, aqui vão algumas ideias. Eles não estão definidos. Eles provavelmente são ruins e / ou impossíveis; a esperança é principalmente desencadear ideias novas / melhores nos outros. E se houver algum interesse, podemos explorar mais.

A ideia motivadora para (1) e (2) é usar de alguma forma a conversão em vez de apelidos. Em # 17746, apelidos enfrentavam problemas em torno de ter vários nomes para o mesmo tipo (ou várias maneiras de escrever o mesmo nome, dependendo se você pensa em apelidos como #define ou como links físicos). Usar a conversão evita isso, mantendo os tipos distintos.

  1. Adicione mais conversão automática.

Quando você chama fmt.Println("abc") ou escreve var e interface{} = "abc" , "abc" é automaticamente convertido em interface{} . Poderíamos mudar a linguagem de forma que quando você declarar type T struct { S } e T não tiver métodos não promovidos, o compilador irá converter automaticamente entre S e T conforme necessário, incluindo recursivamente dentro de outras estruturas. T poderia então servir como um alias de fato de S (ou vice-versa) para fins de refatoração gradual.

  1. Adicione um novo tipo de tipo de "aparência".

Deixe type T ~S declarar um novo tipo T que é um tipo que "se parece com S". Mais precisamente, T é "qualquer tipo conversível de e para o tipo S". (Como sempre, a sintaxe pode ser discutida mais tarde.) Como os tipos de interface, T não pode ter métodos; para fazer basicamente qualquer coisa com T, você precisa convertê-lo para S (ou um tipo conversível de / para S). Ao contrário dos tipos de interface, não existe um "tipo concreto", a conversão entre S para T e T para S não envolve mudanças de representação. Para refatoração gradual, esses tipos de "aparência" permitiriam aos autores escrever APIs que aceitassem tipos antigos e novos. (Os tipos "parece" são basicamente um tipo de união simplificado e altamente restrito.)

  1. Tags de tipo

Bônus de ideia super-hedionda. (Por favor, não se preocupe em me dizer que isso é horrível - eu sei disso. Estou apenas tentando estimular novas ideias em outras pessoas.) E se introduzíssemos tags de tipo (como tags de estrutura) e usássemos tags de tipo especial para configurar e aliases de controle, como type T S "alias:\"T\"" . As tags de tipo também terão outros usos e fornecem escopo para mais especificações de apelidos pelo autor do pacote do que meramente "este tipo é um apelido"; por exemplo, o autor do código pode especificar o comportamento de incorporação.

Se tentarmos apelidos novamente, pode valer a pena pensar sobre "o que o godoc faz", semelhante aos problemas "o que o iota faz" e "o que a incorporação faz".

Especificamente, se tivermos

type  OldAPI => NewPackage.API

e NewPackage.API tem um comentário de documento, devemos copiar / colar esse comentário ao lado de "digite OldAPI", deixá-lo sem comentários (com o godoc fornecendo automaticamente um link ou copiando / colando automaticamente) ou iremos haverá alguma outra convenção?

Um tanto tangencial, embora a principal motivação seja e deva apoiar o reparo gradual do código, um caso de uso menor (voltando à proposta de alias, uma vez que é uma proposta concreta) poderia ser evitar uma sobrecarga de chamada de função dupla ao apresentar uma única função apoiado por várias implementações dependentes de tag de construção. Estou apenas acenando com a mão no momento, mas sinto que os aliases poderiam ter sido úteis no recente https://groups.google.com/d/topic/golang-nuts/wb5I2tjrwoc/discussion "Como evitar sobrecarga de chamada de função em pacotes com go + asm implementations "discussão.

@nigeltao re godoc, eu acho:

Deve sempre ter um link para o original, independentemente.

Se houver documentos no alias, eles devem ser exibidos de qualquer maneira.

Se não houver documentos no alias, é tentador fazer o godoc exibir os documentos originais, mas o nome do tipo estaria errado se o alias também alterasse o nome, os documentos poderiam se referir a itens que não estão no pacote atual e, se estiver sendo usado para refatoração gradual, pode haver uma mensagem que diz "Obsoleto: use X" quando você estiver olhando para X.

No entanto, talvez isso não importasse para a maioria dos casos de uso. Essas são coisas que podem dar errado, não coisas que podem dar errado. E alguns deles podem ser detectados por linting, como apelidos renomeados e avisos de suspensão de cópia acidentalmente.

Não tenho certeza se a ideia a seguir foi postada antes, mas e quanto a uma abordagem do tipo "gofix" / "gorename" baseada principalmente em ferramentas? Para elaborar:

  • qualquer pacote pode conter um conjunto de regras de reescrita (por exemplo, mapeamento pkg.Ident => otherpkg.Ident )
  • essas regras de reescrita podem ser especificadas com //+rewrite ... tags dentro de arquivos go arbitrários
  • essas regras de reescrita não se limitam a alterações compatíveis com ABI, também é possível fazer outras coisas (por exemplo, pkg.MyFunc(a) => pkg.MyFunc(context.Contex(), a) )
  • uma ferramenta como o gofix pode ser usada para aplicar todas as transformações ao repositório atual. Isso torna mais fácil para os usuários de um pacote atualizarem seu código.
  • não é necessário chamar a ferramenta gofix para compilar com sucesso. Uma biblioteca que ainda deseja usar a API antiga de uma dependência X (para permanecer compatível com as versões antigas e novas do X) ainda pode fazer isso. O comando go build deve aplicar as transformações (especificadas nas tags de reescrita do pacote X) em tempo real, sem alterar os arquivos no disco.

As últimas etapas podem complicar / desacelerar o compilador um pouco, mas é basicamente apenas um pré-processador e a quantidade de regras de reescrita deve ser mantida pequena de qualquer maneira. Então, brainstorming suficiente para hoje :)

Usar apelidos para evitar sobrecarga de chamada de função parece um hack para contornar a incapacidade do compilador de incorporar funções não-folha. Não acho que as deficiências de implementação devam influenciar as especificações da linguagem.

@josharian Embora você não pretendesse que fossem propostas completas, deixe-me responder (mesmo que apenas, para que quem se inspira em você possa levar em consideração a crítica imediata):

  1. Realmente não resolve o problema, porque as conversões não são realmente o problema. x/net/context.Context é atribuível / conversível / qualquer que seja a context.Context . O problema são os tipos de ordem superior; a saber, os tipos func (ctx x/net/context.Context) e func (ctx context.Context) não são os mesmos, embora os argumentos possam ser atribuídos. Portanto, para 1 resolver o problema, type T struct { S } precisaria significar que T e S são tipos idênticos. O que significa que, afinal, você está simplesmente usando uma sintaxe diferente para aliases (só que essa sintaxe já tem um significado diferente).

  2. Mais uma vez, tem um problema com tipos de ordem superior, porque tipos atribuíveis / conversíveis não têm necessariamente a mesma representação de memória (e se tiverem, a interpretação pode mudar significativamente). Por exemplo, um uint8 ser convertido em uint64 e vice-versa. Mas isso significaria que, por exemplo, com type T ~uint8 , o compilador não pode saber como chamar um func(T) ; ele precisa colocar 1, 2,4 ou 8 bytes na pilha? Pode haver maneiras de contornar esse problema, mas parece muito complicado para mim (e mais difícil de entender do que apelidos).

Obrigado, @Merovius.

  1. Sim, perdi a satisfação com a interface aqui. Você está certo, isso não funciona.

  2. Eu tinha em mente "ter a mesma representação de memória". O vaivém conversível claramente não é a elucidação certa disso - obrigado.

@uluyol sim, é em grande parte sobre a incapacidade do compilador de incorporar funções não-folha, mas o aliasing explícito pode ser menos surpreendente com relação a se as chamadas sequenciais para não-folhas devem ou não aparecer em rastreamentos de pilha, tempo de execução. Chamadores, etc.

Em todo caso, como eu disse, é uma tangente menor.

@josharian Problema semelhante: [2]uintptr e interface{} têm a mesma representação de memória; portanto, apenas confiar na representação da memória permitirá contornar a segurança de tipo. uint64 e float64 têm ambos a mesma representação de memória e são conversíveis vai-e-vem, mas ainda levaria a resultados muito estranho, pelo menos, se você não sabe qual é qual.

Você pode se safar com o "mesmo tipo subjacente", no entanto. Não tenho certeza de quais seriam as implicações disso. Em cima da minha cartola, isso pode levar ao erro se um tipo for usado em campos, por exemplo. Se você tiver type S1 struct { T1 } e type S2 struct { T2 } (com T1 e T2 do mesmo tipo subjacente), então em type L1 ~T1 ambos podem funcionar como type S struct { L1 } , mas como T1 e T2 ainda têm um tipo subjacente diferente (embora parecido), com type L2 ~S1 você não terá S2 parecidos com S1 e não podem ser usados ​​como L2 .

Portanto, você teria que, em vários lugares na especificação, substituir ou corrigir "tipos idênticos" por "mesmo tipo subjacente" para fazer este trabalho, o que parece difícil de manejar e provavelmente terá consequências imprevistas para a segurança de tipo. Os tipos "semelhantes" também parecem ter um potencial de abuso e confusão ainda maior do que os apelidos, IMHO, que parecem ser os principais argumentos contra os apelidos.

Se alguém puder propor uma regra simples para isso, no entanto, que não tenha esses problemas, ela definitivamente deve ser considerada como uma alternativa :)

Seguindo a ideia de

Permitir a especificação de "tipos substituíveis". Esta é uma lista de tipos que podem ser substituídos pelo tipo nomeado em argumentos de função, valores de retorno etc. O compilador permitiria a chamada de uma função com um argumento do tipo nomeado ou qualquer um de seus substitutos. Os tipos substitutos devem ter uma definição compatível com o tipo nomeado. Compatível aqui significa representações de memória idênticas e declarações idênticas após permitir outros tipos substitutos na declaração.

Um problema imediato é que a direcionalidade dessa relação é oposta à proposta de alias que inverte o gráfico de dependência. Isso por si só pode torná-lo impraticável, mas eu proponho aqui porque outros podem pensar em uma maneira de contornar isso. Uma maneira pode ser declarar substitutos como comentários // go, em vez de por meio do gráfico de importação. Dessa forma, eles talvez se tornem mais como macros.

Por outro lado, existem algumas vantagens para essa reversão de direcionalidade:

  • o conjunto de tipos substituíveis é controlado pelo autor do novo pacote que está em melhor posição para garantir a semântica
  • nenhuma alteração de código é necessária no pacote original, então os clientes não precisam atualizar até que comecem a usar o novo pacote

Aplicando isso à refatoração de Contexto: o pacote de contexto da biblioteca padrão declararia que context.Context pode ser substituído por golang.org/x/net/context.Context . Isso significa qualquer uso que aceite context.Context também pode aceitar um golang.org/x/net/context.Context em seu lugar. No entanto, as funções no pacote de contexto que retornam um Contexto sempre retornariam um context.Context .

Esta proposta contorna o problema de incorporação (# 17746) porque o nome do tipo incorporado nunca muda. No entanto, um tipo incorporado pode ser inicializado usando um valor de um tipo substituto.

@iand @josharian você está pedindo uma certa variante de tipos covariantes.

@josharian , obrigado pelas sugestões.

Re type T struct { S } , que parece uma sintaxe diferente para alias, e não necessariamente mais clara.

Re type T ~S , não tenho certeza de como ele difere do alias ou não tenho certeza de como ajuda a refatorar. Acho que em uma refatoração (digamos, io.ByteBuffer -> bytes.Buffer), você escreveria:

package io
type ByteBuffer ~bytes.Buffer

mas então se, como você diz, "para fazer basicamente qualquer coisa com T, você precisa convertê-lo para S", então todo o código que faz qualquer coisa com io.ByteBuffer ainda falha.

Re type T S "alias" : Um ponto-chave @bcmills feito acima é que ter vários nomes equivalentes para tipos é uma mudança de idioma, não importa como ele é escrito. Todos os compiladores precisam saber que, digamos, io.ByteBuffer e bytes.Buffer são iguais, assim como qualquer ferramenta que analise ou até mesmo verifique o tipo de código. A parte principal da sua sugestão me parece algo como "talvez devêssemos planejar com antecedência para outras adições". Talvez, mas não está claro se uma string seria a melhor maneira de descrevê-los, e também não está claro se queremos projetar a sintaxe (como anotações generalizadas do Java) sem uma necessidade clara. Mesmo se tivéssemos uma forma geral, ainda precisaríamos considerar cuidadosamente todas as implicações de qualquer nova semântica que introduzíssemos, e a maioria ainda seriam alterações de linguagem que exigiriam a atualização de todas as ferramentas (exceto gofmt, é certo). No geral, parece mais simples continuar a encontrar a maneira mais clara de escrever as formas de que precisamos, uma a uma, em vez de criar uma meta-linguagem de um tipo ou de outro.

@Merovius FWIW, eu diria que [2] uintptr e interface {} não têm a mesma representação de memória. Uma interface {} é um [2] inseguro.Pointer, não um [2] uintptr. Um uintptr e um ponteiro são representações diferentes. Mas acho que seu ponto geral está certo, que não queremos necessariamente permitir a conversão direta desse tipo de coisa. Quer dizer, você pode converter da interface {} para [2] * byte também? É muito mais do que o necessário aqui.

@jimmyfrasche e @nigeltao , re godoc: Concordo que precisamos trabalhar cedo também. Concordo que não devemos embutir no código a suposição de que "o novo recurso - seja ele qual for - será usado apenas para refatoração da base de código". Pode ter outros usos importantes, como Nigel encontrado para ajudar a escrever um pacote de extensão de desenho com apelidos. Espero que itens obsoletos sejam marcados como obsoletos em seus comentários de documentos explicitamente, como Jimmy disse. Eu pensei em gerar um comentário de documento automaticamente se não houver um, mas não há nada óbvio a dizer que já não deva estar claro na sintaxe (em geral). Para dar um exemplo específico, considere os antigos apelidos do Go 1.8. Dado

type ByteBuffer => bytes.Buffer

poderíamos sintetizar um comentário de documento dizendo "ByteBuffer é um apelido para bytes.Buffer", mas isso parece redundante com a exibição da definição. Se alguém escreve "type X struct {}" hoje, não sintetizamos "X é um tipo nomeado para uma struct {}".

@iand , obrigado. Parece que sua proposta requer que o autor do novo pacote escreva a definição exata do pacote antigo e também uma declaração ligando os dois, como (criando a sintaxe):

package old
type T { x int }

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

Concordo que a reversão da importação é problemática e pode ser um empecilho por si só, mas vamos pular isso. Neste ponto, a base de código parece estar em um estado frágil: agora o pacote novo pode ser quebrado por uma alteração para adicionar um campo de estrutura no pacote antigo. Dada a linha substituível, há apenas uma definição possível para T1: exatamente o mesmo que old.T. Se os dois tipos ainda têm definições distintas, você também precisa se preocupar com os métodos: as implementações dos métodos também precisam ser correspondentes? Se não, o que acontece quando você coloca um T em uma interface {} e, em seguida, extrai-o usando uma asserção de tipo como T1 e chama M ()? Você obtém T1.M? E se você puxar para fora como uma interface {M ()}, sem nomear T1 diretamente, e chamar M ()? Você consegue TM? Há muita complexidade causada pela ambigüidade de ter ambas as definições na árvore de origem.

Claro, você poderia dizer que a linha substituível torna o resto redundante e não requer uma definição para o tipo T1 ou quaisquer métodos. Mas isso é basicamente o mesmo que escrever (na sintaxe do apelido antigo) type T1 => old.T .

Voltando ao problema do gráfico de importação, embora os exemplos no artigo tenham feito o código antigo definido em termos do novo código, se o gráfico do pacote fosse tal que o novo tivesse que importar o antigo, é igualmente eficaz colocar o redirecionamento no novo pacote durante a transição.

Acho que isso mostra que em qualquer transição como essa, provavelmente não há uma distinção útil entre o autor do novo pacote e o autor do pacote antigo. No final, o objetivo é que o código tenha sido adicionado ao novo e excluído do antigo, portanto, os dois autores (se forem diferentes) precisam ser envolvidos. E os dois precisam de algum tipo de compatibilidade coordenada durante o meio também, seja explícito (algum tipo de redirecionamento) ou implícito (as definições de tipo devem corresponder exatamente, como no requisito de substituibilidade).

@rsc esse cenário de quebra sugere que qualquer tipo de aliasing precisa ser bidirecional. Mesmo sob a proposta de alias anterior, qualquer alteração no novo pacote poderia quebrar qualquer número de pacotes que tenham o alias do tipo.

@iand Se houver apenas uma definição (porque a outra diz "igual a _tela_"), então não há preocupação sobre eles não estarem em sincronia.

Em # 13467, @joegrasse aponta que seria bom se esta proposta fornecesse um mecanismo para permitir que tipos C idênticos se tornassem tipos Go idênticos ao usar cgo em vários pacotes. Esse não é o mesmo problema que esse problema, mas os dois problemas estão relacionados ao aliasing de tipo.

Existe algum resumo das restrições / limitações propostas / aceitas / rejeitadas nos apelidos? Algumas perguntas que vêm à mente são:

  • O RHS é sempre totalmente qualificado?
  • Se apelidos para apelidos são permitidos, como tratamos os ciclos de apelidos?
  • Os aliases devem ser capazes de exportar identificadores não exportados?
  • O que acontece quando você incorpora um alias? (como você acessa o campo incorporado)
  • Os apelidos estão disponíveis como símbolos no programa integrado?
  • Injeção de string ldflags: e se nos referirmos a um alias?

@rsc Não quero desviar muito a conversa, mas sob a proposta de alias, se "novo" remover um campo que "antigo" dependia dele, significa que os clientes de "antigo" agora não podem compilar.

No entanto, de acordo com a proposta substituta, acho que poderia ser arranjado que apenas clientes que usam o antigo e o novo juntos quebrariam. Para que isso fosse possível, a diretiva de substituição teria que ser validada apenas quando o compilador detectasse o uso de tipos "antigos" no pacote "novo".

@thwd Não acho que haja um bom artigo ainda. Minhas anotações:

  • Os ciclos de alias não são um problema. No caso de aliases de cruzamento de pacote, um ciclo já está desabilitado por causa de um ciclo de importação. No caso de aliases que não cruzam o pacote, eles obviamente precisam ser desabilitados, o que é muito semelhante aos ciclos na ordem de inicialização. Pessoalmente, gostaria de ter apelidos para apelidos, porque não acho que eles devem ser restritos a casos de uso de reparo gradual (veja meu comentário acima) e seria triste se o pacote A pudesse quebrar por alguém movendo um tipo pacote B com um alias (imagine x/image/draw.Image aliasing draw.Image e então alguém decidindo mover draw.Image para image.Draw por meio de um alias, supondo que seja seguro. De repente x/image/draw quebra, porque apelidos para apelidos não são permitidos).
  • Acho que os proponentes anteriores de aliases concordaram que a exportação de identificadores não exportados de alias é provavelmente uma má ideia devido à estranheza que pode causar. Efetivamente, isso significa que apelidos para identificadores não exportados são inúteis e podem ser completamente proibidos.
  • A questão da incorporação, AFAIK, ainda não foi resolvida. Há toda uma discussão em # 17746, espero que essa discussão continue se / quando / antes de ser decidido avançar com aliases (mas ainda há a possibilidade de uma solução alternativa ou a decisão de não fazer reparos graduais como objetivo em absoluto)

@iand , re "apenas clientes que usam o antigo e o novo juntos quebrariam", esse é o único caso interessante. São os clientes mistos que o tornam um reparo de código gradual. Os clientes que usam apenas o código novo ou apenas o código antigo funcionarão hoje.

Há outra coisa a considerar, que eu não vi mencionado em nenhum outro lugar ainda:

Uma vez que um objetivo explícito aqui é permitir a refatoração grande e gradual em grandes bases de código descentralizadas, haverá situações em que o proprietário da biblioteca deseja fazer algum tipo de limpeza que exigirá que um número desconhecido de clientes altere seu código (no final " retirar a etapa da API antiga "). Uma maneira comum de fazer isso é adicionar um aviso de descontinuação, mas o compilador Go não tem nenhum aviso.

Sem qualquer tipo de aviso do compilador, como um proprietário de biblioteca pode ter certeza de que é seguro concluir a refatoração?

Uma resposta pode ser algum tipo de esquema de controle de versão - é um novo lançamento da biblioteca com uma nova API incompatível. Nesse caso, talvez o controle de versão seja a resposta completa, não os apelidos de tipo.

Alternativamente, que tal permitir que o autor da biblioteca adicione um "aviso de depreciação" que realmente causa um _erro_ de compilação para os clientes, mas com um algoritmo explícito para a refatoração que eles precisam executar? Estou imaginando algo como:

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

Para aliases de tipo, acho que o algoritmo de refatoração seria apenas "substituir todas as instâncias de OldType por NewType", mas pode haver sutilezas, não tenho certeza.

De qualquer forma, isso permitiria ao autor da biblioteca fazer o melhor esforço para avisar todos os clientes de que seu código está prestes a quebrar e dar a eles uma maneira fácil de corrigi-lo, antes de excluir completamente a API antiga.

@iainmerrick Existem bugs abertos para estes: golang / lint # 238 e golang / gddo # 456

Resolver o problema de reparo gradual de código, conforme descrito no artigo de @rsc , reduz-se a exigir uma maneira de dois tipos serem intercambiáveis ​​(já que existem soluções alternativas para vars, funcs e consts).

Isso requer uma ferramenta ou uma mudança no idioma.

Já que tornar dois tipos intercambiáveis ​​é, por definição, mudar o modo como a linguagem funciona, qualquer ferramenta seria um mecanismo para simular a equivalência fora do compilador, provavelmente reescrevendo todas as instâncias do tipo antigo para o novo tipo. Mas isso significa que tal ferramenta teria que reescrever o código que você não possui, como um pacote vendido que usa golang.org/x/net/context em vez do pacote de contexto stdlib. A especificação da mudança deve estar em um arquivo de manifesto separado ou em um comentário legível por máquina. Se você não executar a ferramenta, obterá erros de compilação. Isso tudo fica complicado de se lidar. Parece que uma ferramenta criaria tantos problemas quanto resolve. Ainda seria um problema com o qual todos os que usam esses pacotes têm que lidar, embora seja um pouco melhor, já que uma parte é automatizada.

Se a linguagem for alterada, o código só precisa ser modificado por seus mantenedores e, para a maioria das pessoas, as coisas simplesmente funcionam. Ferramentas para ajudar os mantenedores ainda são uma opção, mas seria muito mais simples, já que o código-fonte é a especificação, e apenas os mantenedores de um pacote precisariam invocá-la.

Como @griesemer apontou (não me lembro onde, tem havido tantos tópicos sobre isso) Go já tem aliasing, para coisas como byteuint8 , e quando você importa um pacote duas vezes, com nomes locais diferentes, no mesmo arquivo de origem.

Adicionar uma maneira de explicitamente os tipos de alias na linguagem está apenas nos permitindo usar a semântica que já existe. Fazer isso resolve um problema real de uma forma administrável.

Uma mudança de idioma ainda é um grande negócio e muitas coisas precisam ser resolvidas, mas acho que, no final das contas, é a coisa certa a se fazer aqui.

Até onde eu sei, um "obstáculo na sala" é o fato de que, para apelidos de tipo, introduzi-los permitirá usos não temporários (isto é, "não refatoração"). Eu vi aqueles mencionados de passagem (por exemplo, "reexportando identificadores de tipo em pacotes diferentes para simplificar a API"). Mantendo a boa tradição de propostas anteriores, liste todos os usos alternativos conhecidos de aliases de tipo na subseção "impacto" . Isso também deve trazer o benefício de alimentar a imaginação das pessoas para inventar outros usos alternativos possíveis e trazê-los à luz na discussão atual. Como está agora, a proposta parece fingir que os autores desconhecem completamente outros usos possíveis dos apelidos de tipo. Além disso, quanto à reexportação, Rust / OCaml pode ter alguma experiência em como funcionam para eles.

Pergunta adicional: por favor, esclareça se os aliases de tipo permitiriam adicionar métodos ao tipo no novo pacote (possivelmente quebrando o encapsulamento) ou não? além disso, o novo pacote teria acesso a campos privados de antigas estruturas ou não?

Pergunta adicional: por favor, esclareça se os aliases de tipo permitiriam adicionar métodos ao tipo no novo pacote (possivelmente quebrando o encapsulamento) ou não? além disso, o novo pacote teria acesso a campos privados de antigas estruturas ou não?

Um alias é apenas outro nome para um tipo. Isso não muda o pacote do tipo. Portanto, não para ambas as perguntas (a menos que novo pacote == pacote antigo).

@akavel A partir de agora, não há proposta alguma. Mas sabemos de duas possibilidades interessantes que surgiram durante os testes de alias do Go 1.8.

  1. Aliases (ou apenas digitar aliases) permitiriam a criação de substitutos drop-in que expandem outros pacotes. Por exemplo, consulte https://go-review.googlesource.com/#/c/32145/ , especialmente a explicação na mensagem de confirmação.

  2. Aliases (ou apenas aliases de tipo) permitiriam estruturar um pacote com uma pequena superfície de API, mas uma grande implementação como uma coleção de pacotes para melhor estrutura interna, mas ainda apresentaria apenas um pacote a ser importado e usado pelos clientes. Há um exemplo um tanto abstrato descrito em https://github.com/golang/go/issues/16339#issuecomment -232813695.

O objetivo básico dos aliases é ótimo, mas ainda parece que não estamos sendo muito honestos com o objetivo de refatorar o código, apesar de ser o motivador número um para o recurso. Algumas das propostas sugerem bloquear o nome, e eu não vi isso mencionado ainda que os tipos geralmente mudam sua superfície com essas refatorações também. Mesmo o exemplo de os.Error => error frequentemente mencionado em torno de apelidos ignora o fato de que os.Error tinha um método String e não Error . Se apenas movermos o tipo e o renomearmos, todo o código de tratamento de erros será quebrado de qualquer maneira. Esse é um lugar comum durante as refatorações. Os métodos antigos são renomeados, movidos, descartados e não os queremos no novo tipo, pois isso preservaria a incompatibilidade com o novo código.

No interesse de ajudar, aqui está uma ideia-semente: e se olhássemos para o problema em termos de adaptadores, em vez de apelidos? Um adaptador daria a um tipo existente um nome alternativo _e interface_ e pode ser usado sem adornos em locais onde o tipo original foi visto antes. O adaptador precisaria definir explicitamente os métodos que ele suporta, em vez de assumir que a mesma interface do tipo adaptado subjacente está presente. Isso seria muito parecido com o comportamento type foo bar , mas com algumas semânticas adicionais.

io.ByteBuffer

Por exemplo, aqui está um exemplo de esqueleto abordando o caso io.ByteBuffer , usando a palavra-chave temporária "adapta" por enquanto:

type ByteBuffer adapts bytes.Buffer

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

(... etc ...)

Então, com esse adaptador instalado, todo este código seria válido:

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

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

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

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

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

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

As interfaces de newfunc e oldfunc são compatíveis. Ambos realmente aceitam *bytes.Buffer , com oldfunc adaptando-o a *io.BytesBuffer no caminho. O mesmo conceito funciona para atribuições, resultados, etc.

os.Error

A mesma lógica provavelmente funcionará na interface também, embora a implementação do compilador dela seja um pouco mais complicada. Aqui está um exemplo para os.Error => error , que lida com o fato de o método ter sido renomeado:

package os

type Error adapts error

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

Este caso precisa de mais reflexão, porém, porque métodos como:

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

Estará retornando um tipo que tem um método String , então geralmente queremos adaptar na direção oposta para que o código possa ser corrigido gradualmente.

_ ATUALIZADO: Necessita de mais reflexão._

Problema de incorporação

Em termos do bug de incorporação que tirou o recurso do 1.8, o resultado é um pouco mais claro com os adaptadores, já que eles não são apenas novos nomes para a mesma coisa: se o adaptador for incorporado, o nome do campo usado é o de o adaptador para que a lógica antiga continue funcionando e o acesso ao campo usará a interface do adaptador, a menos que seja explicitamente inserido em um contexto que aceite o tipo subjacente. Se o tipo não adaptado estiver incorporado, o normal acontece.

kubernetes, docker

Os problemas declarados no post parecem variações dos problemas acima, e resolvidos pela proposta.

vars, consts

Não faria muito sentido adaptar variáveis ​​ou constantes nesse cenário, uma vez que não podemos realmente associar métodos diretamente a elas. São os seus tipos que seriam adaptados ou não.

godoc

Seríamos explícitos sobre o fato de que a coisa é um adaptador, e mostraríamos a documentação como de costume, já que contém uma interface independente da coisa adaptada.

sintaxe

Por favor, escolha algo legal. ;)

@iainmerrick @zombiezen

Devemos também inferir automaticamente que um tipo de alias é legado e deve ser substituído pelo novo tipo? Se aplicarmos golint, godoc e ferramentas semelhantes para visualizar o tipo antigo como obsoleto, isso limitaria o abuso de aliasing de tipo muito significativamente. E a preocupação final de usar o recurso de aliasing seria resolvida.

Duas observações:

1. A semântica das referências de tipo depende do caso de uso de refatoração com suporte

A proposta de Gustavo mostra que é necessário mais trabalho no caso de uso para referências de tipo e na semântica resultante.

A nova proposta de Ross inclui uma nova sintaxe type OldAPI = newpkg.newAPI . Mas quais são as semânticas? É impossível estender OldAPI com métodos ou campos públicos legados? Presumindo sim como uma resposta que requer que a newAPI suporte todos os métodos e campos públicos da OldAPI para manter a compatibilidade. Observe que qualquer código no pacote com OldAPI que dependa de métodos e campos privados deve ser reescrito para usar apenas o newAPI público, assumindo que a modificação das restrições de visibilidade dos pacotes está fora de questão.

O caminho alternativo seria permitir que métodos adicionais sejam definidos para OldAPI. Isso poderia aliviar o fardo da NewAPI de fornecer todos os métodos públicos antigos. Mas isso tornaria OldAPI um tipo diferente de NewAPI. Alguma forma de atribuição entre os valores dos dois tipos deve ser mantida, mas as regras se tornariam complexas. Permitir a adição de campos resultaria em mais complexidade.

2. Pacote com NewAPI não pode importar pacote com OldAPI

A redefinição do OldAPI requer que o pacote O contenha a definição do pacote N de importações OldAPI com o NewAPI. Isso implica que o pacote N não pode importar O. Talvez seja tão óbvio que não foi mencionado, mas me parece uma restrição importante para o caso de uso de refatoração.

Atualização: o pacote N não pode ter nenhuma dependência do pacote O. Por exemplo, ele não pode importar um pacote que importa O.

@niemeyer Mudanças como renomear um método já são gradualmente possíveis: a) Adicione o novo método, chame o antigo sob o capô (ou vice-versa), b) altere gradualmente todos os usuários para o novo método, c) exclua o método antigo. Você pode combinar isso com um alias de tipo. A razão pela qual isso se concentra na movimentação de tipos é que essa é a única coisa identificada, que ainda não é possível. Todas as outras alterações identificadas são possíveis, mesmo que possam usar várias etapas (por exemplo, alterar o conjunto de argumentos de um método sem renomeá-lo). Eu acredito que escolher uma solução com menos área de superfície (menos coisas para entender) é preferível.

@rakyll Pessoalmente, se eu considerasse apelidos úteis para algo que não seja refatorado (como pacotes de invólucros, que considero um caso de uso excelente), eu simplesmente os usaria, deprecation-warnings que se dane. Eu ficaria chateado com quem os aleijou artificialmente e os tornou confusos para meus usuários, mas não desanimaria.

Eu acho que em algum ponto precisa ser debatido se nós realmente consideramos pacotes wrapper, importações públicas de protobuf ou expor APIs de pacotes internos uma coisa tão ruim (e eu não sei como debater melhor algo tão subjetivo sem um lado apenas repetir repetidamente que eles são ilegíveis e o outro dizendo "não, eles não são". Não há muitos argumentos objetivos para se ter aqui, parece-me).

Eu, pelo menos (obviamente) acho que eles são uma coisa boa e também sou da opinião que adicionar um recurso de linguagem e restringi-lo artificialmente a apenas um caso de uso é uma coisa ruim; uma linguagem ortogonal e bem projetada permite que você faça o máximo possível com o mínimo de recursos possível. Você deseja que seus recursos expandam o "espaço vetorial estendido de programas possíveis" tanto quanto possível, então adicionar um recurso que adiciona apenas um único ponto ao espaço parece estranho para mim.

Eu gostaria que outro caso de uso ligeiramente diferente fosse levado em consideração quando qualquer proposta de alias de tipo for desenvolvida.

Embora o principal caso de uso que estamos discutindo nesta edição seja o tipo _replacement_, aliases de tipo também seriam muito úteis para livrar um corpo de código de uma dependência de um tipo.

Por exemplo, suponha que um tipo se torne "instável" (ou seja, ele continua sendo alterado, talvez de maneiras incompatíveis). Então, alguns de seus usuários podem querer migrar para um tipo de substituição "estável". Estou pensando no desenvolvimento no github etc., onde os proprietários de um tipo e seus usuários não necessariamente trabalham juntos ou concordam com o objetivo de estabilidade.

Outros exemplos seriam onde um único tipo é a única coisa que impede a exclusão de uma dependência de um pacote grande ou problemático, por exemplo, quando uma incompatibilidade de licença foi descoberta.

Portanto, o processo aqui seria:

  1. Defina o alias do tipo
  2. Altere o corpo de código relevante para usar o alias de tipo
  3. Substitua o alias de tipo por uma definição de tipo.

Ao final desse processo, haveria dois tipos independentes que seriam livres para evoluir em suas próprias direções.

Observe que, neste caso de uso:

  • não há opção de alterar o pacote que contém a definição de tipo original para adicionar um apelido de tipo (uma vez que os proprietários provavelmente não concordarão com isso)
  • o tipo original não está obsoleto (embora possa ser considerado como tal no corpo do código no processo de "desmame" do tipo).

@Merovius No momento em que você deleta ou renomeia o método antigo, você mata todos os clientes que o estavam usando, de uma vez. Se você estiver disposto a fazer isso, todo o exercício não trivial de adicionar um recurso de linguagem para evitar quebras de uma vez é discutível. Podemos também dizer exatamente a mesma coisa para mover o código: basta renomear o tipo em cada site de chamada de uma vez. Feito. Ambas as ações são simplesmente renomeações atômicas, que têm em comum o fato de assumirem o acesso completo a todas as linhas de código nos sites de chamada. Esse pode ser o caso do Google, mas como mantenedor de grandes aplicativos e bibliotecas de código aberto, não é o mundo em que vivo.

Na maioria dos casos, considero essa crítica injusta, já que a equipe de Go faz de tudo para tornar o projeto inclusivo para partes externas, mas no momento em que você assume que tem acesso a cada linha de código que está chamando um determinado pacote, isso é uma barreira jardim que não corresponde ao contexto de uma comunidade de código aberto. Adicionar um recurso de refatoração de nível de linguagem que só funcione dentro de jardins murados seria atípico, para dizer o mínimo.

@niemeyer Eu aparentemente não fui claro. Eu não estava defendendo a exclusão da API antiga em qualquer caso, eu estava apenas apontando que qualquer fluxo de trabalho que desejamos habilitar com aliases de tipo já é possível com métodos de renomeação (seja ao mesmo tempo ou não). Então, não importa o que você queira fazer, para

  1. Adicionar nova API, intercambiável com API antiga
  2. Mude gradualmente os consumidores para a nova API
    3a. Depois que tudo for migrado ou o período de suspensão de uso terminar, exclua a API antiga
    3b. Fornece estabilidade indefinida, mantendo ambas as APIs para sempre (consulte, por exemplo, esta parte do artigo )

Você parece estar discutindo sobre fazer 3a contra 3b. Mas o que eu estava apontando é que 1. já é possível para nomes de métodos, mas não é possível para tipos, e é disso que se trata.

Porém, agora eu percebo que acho que o entendi mal :) Você deve ter apontado que os.Error são definições de interface diferentes, então a mudança realmente não deu certo. Eu acho que isso é verdade; se você proibir a remoção de APIs, os aliases de tipo não permitiriam renomear métodos de tipos de interface.

Talvez você possa esclarecer algo sobre sua ideia de adaptador para mim: Isso também não permitiria usar (por exemplo, no caso os.Error) qualquer fmt.Stringer como um os.Error?

Em qualquer caso, parece que vale a pena desenvolver a ideia do adaptador, mesmo que eu seja um pouco cético a respeito. Mas ter uma maneira de refatorar gradualmente as interfaces sem quebrar possíveis implementadores e / ou consumidores é uma boa meta.

@niemeyer Sim, você

Concordo que, se houvesse alguma correção geral que lidasse com os dois tipos de mudanças, isso seria ótimo. Eu não vejo o que é essa correção. Em particular, não entendo como as opções de tipo funcionam com os adaptadores que você descreveu: o valor é convertido de alguma forma automaticamente durante a troca de tipo? Que tal reflexão? Ter apenas um tipo com dois nomes evita muitos problemas que surgem com dois tipos que se convertem automaticamente de um lado para outro.

@rsc Sim, o adaptador seria consistentemente convertido automaticamente em todas as situações, portanto, as opções de tipo não seriam diferentes. Proibiríamos as opções de tipo contendo o adaptador e seu tipo subjacente, pois isso seria ambíguo. Posso estar faltando alguma coisa, mas ainda não consigo ver um problema com reflexão, uma vez que todo contexto de código precisa necessariamente estar usando o tipo adaptado, ou seu tipo subjacente, explicitamente. Assim como hoje, não podemos entrar em um interface{} sem saber como chegamos lá, se isso faz sentido.

@Merovius Meus dois comentários acima abordam precisamente os pontos que você ainda está fazendo. Se você mover um tipo hoje, você quebrará o código que precisa de conserto. Se você renomear um método, quebrará o código que precisa de conserto. Se você deletar um método, mudar seus argumentos, você quebra o código que precisa de conserto. Ao refatorar o código em qualquer um desses casos, as correções precisam ser feitas atomicamente com a quebra em cada site de chamada para que as coisas continuem funcionando. Permitir que o tipo seja movido, mas completamente intocado, é um caso muito limitado de refatoração, que IMO não justifica um recurso de linguagem.

@niemeyer Isso .(interface{String() string}) vs .(interface{Error() string}) ou quaisquer partes específicas da interface alteradas? A verificação deve considerar os dois tipos subjacentes possíveis de alguma forma?

@niemeyer Não. Renomear um método é possível não atomicamente. por exemplo, para mover um método de A.Foo para A.Bar , faça

  1. Adicione o método A.Bar como um envoltório em torno de A.Foo
  2. Migrar usuários para apenas chamar A.Bar por meio de muitos commits arbitrariamente
  3. Exclua A.Foo ou não, dependendo se você deseja aplicar uma suspensão de uso.

É possível alterar os argumentos das funções de forma não atômica. por exemplo, para adicionar um parâmetro x int a um func Foo() , faça

  1. Adicionar func FooWithInt(x int) { Foo(); // use x somehow; }
  2. Migre usuários para adicionar o parâmetro por meio de muitos commits arbitrariamente
  3. Se você não estiver disposto a impor uma suspensão de uso (ou não se incomodar em ter o WithInt), está feito. Caso contrário, modifique Foo para func Foo(x int) { FooWithInt(x) } .
  4. Migre usuários com s/FooWithInt/Foo/g por meio de muitos commits arbitrariamente.
  5. Exclua FooWithInt .

O mesmo funciona para praticamente todos os casos, exceto os tipos móveis (e, estritamente falando, vars). atomicidade não é necessária. Você pode quebrar a compatibilidade ao impor a depreciação ou não, mas isso é completamente ortogonal à atomicidade. A capacidade de usar dois nomes diferentes para se referir à mesma coisa é o que permite contornar a atomicidade ao fazer alterações basicamente arbitrárias e você tem essa capacidade para todos os casos, exceto os tipos. Sim, para fazer uma mudança real, em vez de uma emenda, você precisa estar disposto a impor a depreciação (quebrando a compilação de código potencialmente desconhecido, o que significa que isso precisa de um anúncio amplo e oportuno). Mas mesmo se você não estiver, a capacidade de aumentar as APIs com um nome mais conveniente ou outro empacotamento útil (consulte x / imagem / desenho) também depende da capacidade de se referir ao antigo pelo novo nome e vice-versa.

A diferença entre mover tipos hoje e renomear uma função hoje é que, no primeiro caso, você realmente precisa de uma mudança atômica, enquanto para o último, você pode fazer a mudança gradualmente, em repos e commits independentes. Não como um "Vou fazer um commit que faça s / Foo / Bar /", mas existe um processo para fazer isso.

Qualquer forma. Não sei onde estamos, aparentemente, falando um atrás do outro. Acho o documento de @rsc bastante claro para transmitir meu ponto de vista e não entendo realmente o seu :)

@rsc posso ver duas respostas razoáveis. O simples fato de que a interface carrega o tipo que entrou, adaptador ou outro, e a semântica usual se aplica quando a interface é declarada. A outra é que o valor pode não ser adaptado se não atender à interface, mas o valor subjacente sim. O primeiro é mais simples e talvez o suficiente para os casos de uso de refatoração que temos em mente, enquanto o último é talvez mais consistente com a ideia de que podemos afirmar o tipo para o tipo subjacente também.

@Merovius Claro, renomear um método é possível desde que _você não o renomeie_ e force os sites de chamada a usar uma nova API. Da mesma forma, mover um tipo é possível, desde que _você não o mova de fato _ e force os sites de chamada a usar uma nova API. Todos nós temos feito essas duas coisas há anos para preservar o funcionamento do código antigo.

@niemeyer Mas, novamente: para tipos, você não pode nem adicionar coisas de uma maneira decente. Veja x / imagem / desenho. E nem todos podem ter uma visão tão absoluta da estabilidade; Eu, pessoalmente, estou bem em dizer "em 6,12, ... meses $ function, $ type, ... está indo embora, certifique-se de que você foi migrado para longe dele nesse ponto" e, em seguida, apenas quebre o código não mantido que não conseguem seguir esse aviso de descontinuação (se alguém pensa que precisa de suporte de longo prazo para APIs, certamente pode encontrar alguém que pague para fornecer isso). Eu até diria que a maioria das pessoas não tem essa visão absoluta sobre estabilidade; veja o recente push para versões semânticas, que realmente só faz sentido se você quiser ter a opção de quebrar a compatibilidade. E o médico argumenta muito bem como, mesmo nesse caso, você ainda lucraria com a capacidade de fazer reparos graduais e como isso pode aliviar, se não resolver essencialmente, o problema da dependência do diamante.

Você pode descartar a maioria dos casos de uso de aliases para reparos graduais porque sua postura quanto à estabilidade é absoluta. Mas eu diria que, para a maior parte da comunidade go, isso é diferente, que há uma necessidade de quebras e a utilidade de fazê-las da maneira mais suave possível quando elas acontecem.

@niemeyer @rsc @Merovius Eu tenho acompanhado sua discussão (e toda a discussão) e gostaria de bater descaradamente este post bem no meio dela.

Quanto mais iteramos sobre o problema, mais perto chegamos de alguma forma de semântica de covariância estendida. Então, aqui vai uma reflexão: já temos semântica de subtipo ("is-a") definida de tipos concretos para interfaces e entre interfaces. Minha proposta é tornar as interfaces recursivamente covariantes (de acordo com as regras de variância atuais) até os argumentos de seus métodos.

Isso não resolve o problema de todos os pacotes atuais. Mas pode resolver o problema para todos os pacotes futuros, ainda a serem escritos, em que as "partes móveis" da API podem ser interfaces (incentiva um bom design também).

Acho que podemos resolver todos os requisitos (ab) usando interfaces dessa maneira. Estamos quebrando o Go 1.0? Não sei, mas acho que não.

@thwd eu acho que você precisa definir mais precisamente o que você quer dizer com "fazer interfaces recursivamente covariantes". Normalmente, na subtipagem, os argumentos do método precisam mudar de maneiras contravariantes e os resultados de maneiras covariantes. Além disso, pelo que você está dizendo, isso não resolveria nenhum problema existente com tipos concretos (sem interface).

@thwd eu discordo, que as interfaces (mesmo as covariantes) são uma boa solução para qualquer um desses problemas (apenas para instâncias muito específicas). Para torná-los um, você precisa fazer de tudo em sua API uma interface (porque você nunca sabe o que pode querer mover / alterar em algum ponto), incluindo vars / consts / funcs / ... e eu não penso em tudo, que é um bom design (eu vi isso em java. Isso me irrita). Se algo for uma estrutura, basta torná-la uma estrutura. Todo o resto apenas adiciona sobrecarga sintática estranha em seu pacote e cada dependência reversa para virtualmente nenhum benefício. É também a única maneira de permanecer são quando você começa; comece simples e vá para algo mais geral mais tarde. Muitas complicações na API que vi até agora vêm de pessoas que pensam demais no design da API e no planejamento de uma maneira mais geral do que será necessário. E então, em 80% (esse número é uma mentira óbvia) dos casos, nada acontece, porque não há um "design de API limpo".

(para ser claro: não estou dizendo que interfaces covariantes não sejam uma boa ideia. Só estou dizendo que não são uma boa solução para esses problemas)

Para adicionar ao ponto de @Merovius , muitos reparos graduais de código que eu vi tomaram a forma de mover um tipo não-interface geralmente útil de um pacote muito maior. Considere o seguinte:

package foo

type Authority struct {
  Host string
  Port int
}

Com o tempo, o pacote foo cresce e acaba ganhando mais responsabilidade (e tamanho do código) do que alguém que precisa apenas do tipo Authority realmente deseja. Portanto, ter uma maneira de criar um pacote fooauthority que contenha apenas Authority e ter usuários existentes de foo.Authority ainda funcionando é um caso de uso desejável. Observe que qualquer solução que considere apenas os tipos de interface não ajudaria aqui.

@Merovius Seu último comentário foi inteiramente subjetivo e se dirige a mim pessoalmente, em vez de à minha proposta. Isso não vai acabar bem, então vou parar essa linha de discussão aqui.

@griesemer @Merovius Concordo com vocês dois. Para fechar o loop, então, podemos concordar que a discussão até agora nos levou a alguma noção de subtipos / covariância. Além disso, qualquer implementação dele não deve incorrer em nenhuma direção indireta do tempo de execução. Isso é mais ou menos o que @niemeyer estava propondo (se eu o entendi direito). Mas adoraria ler mais ideias. Eu estarei pensando sobre o problema também.

@niemeyer Não havia nada _ad hominem_ nos comentários de @Merovius . Sua afirmação de que "sua posição sobre a estabilidade é absoluta" é uma observação sobre sua posição, não você, e é uma inferência razoável de algumas de suas declarações, como

No momento em que você deleta ou renomeia o método antigo, você mata todos os clientes que o estavam usando de uma vez.

e

Claro, renomear um método é possível, desde que você não o renomeie e force os sites de chamada a usar uma nova API. Da mesma forma, mover um tipo é possível, desde que você não o mova de fato e force os sites de chamada a usar uma nova API. Todos nós temos feito essas duas coisas há anos para preservar o funcionamento do código antigo.

Tive a mesma impressão que Merovius nessas declarações - que você não simpatiza em depreciar algo por um tempo e depois removê-lo; que você está comprometido em manter o código funcionando indefinidamente; que "sua postura em relação à estabilidade é absoluta". (E para evitar mais mal-entendidos, estou usando "você" para se referir às suas ideias, não à sua personalidade.)

@niemeyer A adapts que você está sugerindo parece intimamente relacionada a instance das typeclasses Haskell. Traduzindo isso vagamente para Go, pode ser algo como:

package os

type Error interface {
  String() string
}

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

Infelizmente (como observa @zombiezen ), não está claro como isso ajudaria para tipos sem interface.

Também não é óbvio para mim como ele interagiria com os tipos de função (argumentos e valores de retorno); por exemplo, como a semântica de adapts ajudaria na migração de Context para a biblioteca padrão?

Tive a mesma impressão que Merovius a partir dessas declarações - que você não simpatiza em depreciar algo por um tempo

@jba Estes são fatos absolutos, não opiniões absolutas. Se você excluir um método ou tipo, o código Go que o usará será interrompido, portanto, essas alterações precisam ser feitas atomicamente. Minha proposta, porém, é sobre a refatoração gradual do código, que é o assunto aqui e implica em desaprovação. Esse processo de depreciação, porém, não é uma questão de simpatia. Eu tenho vários pacotes Go públicos com milhares de dependências in-the-wild cada, e várias APIs independentes devido a essa evolução gradual. Quando quebramos uma API, é bom fazer essas quebras em lotes, em vez de transmiti-los, se não esperamos enlouquecer as pessoas. A menos, é claro, que você more em um jardim murado e possa entrar em contato com todos os locais de atendimento para consertá-lo. Mas estou me repetindo .. tudo isso pode ser lido na proposta original acima de forma mais articulada.

@Merovius

Pessoalmente, se eu considerasse apelidos úteis para algo que não seja refatorado (como pacotes de invólucros, que considero um excelente caso de uso), eu apenas os usaria, deprecation-warnings que se dane.

Mantemos pacotes com um número absurdamente grande de APIs novas e obsoletas e ter aliases sem uma explicação clara do estado do tipo antigo (alias) não ajudará no reparo gradual do código e apenas contribuirá para o esmagamento da superfície aumentada da API. Concordo com @niemeyer que nossa solução precisa atender aos requisitos de uma comunidade de desenvolvedores distribuída que atualmente não tem nenhum outro sinal além do texto godoc de formato livre dizendo que uma API está "obsoleta". Adicionar um recurso de linguagem para ajudar a descontinuar tipos antigos é o tópico deste tópico, portanto, naturalmente leva à questão de qual é o estado do tipo antigo (alias).

Eu adoraria discutir o alias de tipo em um tema diferente, como fornecer extensão para um tipo ou pacotes parciais, mas não neste tópico. Esse tópico em si tem vários problemas específicos de encapsulamento a serem tratados antes de qualquer consideração.

Um operador específico ou sugerindo que o alias digitado foi substituído de alguma forma pode ser útil para comunicar aos usuários que eles precisam mudar. Essa diferenciabilidade permitiria que as ferramentas relatassem automaticamente as APIs substituídas.

Para ser claro, a política de reprovação não é tecnicamente possível para tipos fora da biblioteca padrão. Um tipo só é antigo da perspectiva de um pacote de aliasing. Dado que nunca poderemos impor isso no ecossistema, eu ainda gostaria de ver os aliases de biblioteca padrão implicarem em depreciação estritamente (sugerido por avisos de descontinuação adequados).

Também estou sugerindo que padronizemos a noção de depreciação em uma discussão paralela e suporte para eles em nossas ferramentas principais (golint, godoc, etc). A falta de avisos de depreciação é o maior problema no ecossistema Go e é mais difundido do que o problema de reparo gradual de código.

@rakyll Eu simpatizo com o caso de uso de avisos de descontinuação legíveis por computador; Eu apenas me oponho à noção de a) aliases sendo que eb) emitindo-os como avisos do compilador.

Para a), além do fato de que eu gostaria de usar aliases de forma produtiva para outras coisas além de movimentos, também só se aplicaria a um conjunto muito pequeno de depreciações. Por exemplo, digamos que eu queira remover alguns parâmetros de uma função em algumas versões; Não posso usar aliases, realmente, porque a assinatura da nova API será diferente, mas ainda quero anunciar isso. Para b), os avisos do compilador IMHO são universalmente ruins. Acho que isso está de acordo com o que go já está fazendo, então não acho que exija justificativa.

Concordo com tudo o que você está dizendo sobre os avisos de suspensão de uso. Já existe uma sintaxe para isso, aparentemente: # 10909, então a próxima etapa para torná-la mais útil seria aprimorar o suporte a ferramentas, destacando-as no godoc e tendo uma verificação que avisa sobre seu uso (digamos go vet, golint ou uma ferramenta separada completamente).

@rakyll Concordo que o stdlib deve começar com um uso conservador de aliases de tipo, caso sejam introduzidos.


Barra Lateral:

Histórico para quem não sabe do status dos comentários de suspensão de uso no Go e nas ferramentas relacionadas, já que é bastante espalhado:

Como @Merovius mencionou acima, há uma convenção padrão para marcar itens como obsoletos, # 10909, consulte https://blog.golang.org/godoc-documenting-go-code

TL; DR: faça um parágrafo nos documentos do item obsoleto que começa com "Obsoleto:" e explica o que é a substituição.

Há uma proposta aceita para godoc para exibir itens obsoletos de uma maneira mais útil: # 17056.

@rakyll propôs que o golint avise quando itens obsoletos forem usados: golang / lint # 238.


Mesmo que o stdlib tenha uma postura conservadora sobre o uso de aliases dentro do stdlib, não acho que a existência de um alias de tipo deva implicar (de qualquer forma que seja detectada mecanicamente ou denotada visualmente) que o tipo antigo está obsoleto, mesmo se isso sempre significa isso na prática.

Fazer isso significaria um dos seguintes:

  • escanear outros pacotes stdlib para ver se algum tipo, não explicitamente marcado como obsoleto, tem um alias em outro lugar
  • codificar todos os aliases stdlib em ferramentas automatizadas
  • informando apenas que o tipo antigo está obsoleto quando você já está analisando sua substituição, o que não ajuda na descoberta

Quando um alias de tipo é introduzido porque o tipo antigo se tornou obsoleto, ele precisa ser tratado marcando o tipo antigo como obsoleto, com uma referência ao novo tipo, independentemente.

Isso possibilita a existência de um melhor conjunto de ferramentas, permitindo que seja mais simples e geral: ele não precisa colocar nada especial ou mesmo saber sobre apelidos de tipo: ele só precisa corresponder a "Obsoleto:" nos comentários de documentos.

Uma política oficial, se talvez temporária, de que um alias no stdlib seja apenas para desaprovação é boa, mas só deve ser aplicada com os comentários de desaprovação padrão e não permitindo que outros usos passem pela revisão do código.

@niemeyer Minha resposta anterior foi perdida devido à perda de energia :( fora de serviço:

Mas estou me repetindo ..

FWIW, achei sua última resposta muito útil. Convenceu-me de que estamos mais de acordo, do que parecia anteriormente (e do que ainda pode parecer para você). No entanto, ainda parece haver falha de comunicação em algum lugar.

Minha proposta, porém, é sobre a refatoração gradual do código

Isso não é contencioso, eu acho. :) Concordei, desde o início, que sua proposta é uma alternativa interessante a ser considerada para enfrentar o problema. O que me confunde são declarações como esta:

Se você excluir um método ou tipo, o código Go que o usará será interrompido, portanto, essas alterações precisam ser feitas atomicamente.

Ainda me pergunto qual é o seu raciocínio aqui. Eu entendo que a unidade de atomicidade é um único commit. Com essa suposição, eu simplesmente não entendo por que você está convencido de que a exclusão de um método ou tipo não pode acontecer primeiro em separado, com numerosos commits nos repositórios dependentes e, em seguida, uma vez que não há mais usuário aparente (e uma ampla depreciação intervalo passou) o método ou tipo é deletado em um commit upstream (sem quebrar nada, já que ninguém depende mais). Concordo que há um certo fator de imprecisão em torno das dependências reversas que não aderem à depreciação ou que você não pode encontrar (ou corrigir razoavelmente), mas que, para mim, parece amplamente independente do assunto em questão; você terá esse problema sempre que aplicar uma alteração significativa e não importa como tente orquestrá-la.

E, para ser justo: a confusão não é ajudada por frases como

A menos, é claro, que você more em um jardim murado e possa entrar em contato com todos os locais de atendimento para consertá-lo.

Se alguma coisa que eu disse deu a você a impressão de que este é o ponto a partir do qual estou argumentando, espero que você possa dar um passo para trás e talvez relê-lo sob a suposição de que estou argumentando completamente a partir da posição do código aberto comunidade (se você não acredita em mim, sinta-se à vontade para pesquisar minhas contribuições anteriores para este tópico; eu sou sempre o primeiro a apontar que isso é mais um problema da comunidade do que um problema de monorepo. Monorepos têm maneiras de contornar isso , como você apontou).

Qualquer forma. Acho isso tão extenuante quanto você. Espero entender sua posição em algum momento.

simultaneamente falar sobre se e como apoiar coisas como as importações públicas protobuf ...
Acho que em algum ponto precisa ser debatido se realmente consideramos pacotes de invólucros, importações públicas de protobuf ou expor APIs de pacotes internos uma coisa tão ruim

nit: Não acho que as importações públicas de protobuf precisem ser mencionadas como um caso de uso secundário especial. Eles foram projetados para reparo de código gradual, conforme mencionado explicitamente no documento de design interno e até mesmo na documentação pública , portanto, eles já estão sob o guarda-chuva dos problemas descritos por esta edição. Além disso, acredito que aliases de tipo seriam suficientes para implementar importações públicas de protobuf. (O proto-compilador gera vars, mas eles são logicamente constantes, então "var Enum_name = importado.Enum_name" deve ser suficiente.)

@Merovius Obrigado pela resposta produtiva. Deixe-me tentar fornecer algum contexto:

Ainda me pergunto qual é o seu raciocínio aqui. Eu entendo que a unidade de atomicidade é um único commit. Com essa suposição, eu simplesmente não entendo por que você está convencido de que a exclusão de um método ou tipo não pode acontecer primeiro separadamente,

Nunca disse que não pode acontecer. Deixe-me dar um passo para trás e reafirmar com mais clareza.

Provavelmente, todos concordamos que o objetivo final é duplo: queremos um software funcional e queremos melhorá-lo para que possamos continuar trabalhando nele de maneira sã. Alguns dos últimos estão quebrando as mudanças, colocando-o em desacordo com o primeiro objetivo. Portanto, há tensão, o que significa que há alguma subjetividade em relação a onde está o ponto ideal. A parte interessante do nosso debate está aqui.

Uma maneira útil de pesquisar esse ponto ideal é pensar sobre as intervenções humanas. Ou seja, uma vez que você faz algo que requer que as pessoas modifiquem manualmente o código para mantê-lo funcionando, ocorre a inércia. Leva muito tempo para que a parte relevante de todas as bases de código dependentes passe por esse processo. Estamos pedindo às pessoas ocupadas que façam coisas que, na maioria dos casos, preferem não se preocupar.

Outra maneira de ver esse ponto ideal é a probabilidade de um software funcional. Não importa o quanto pedimos às pessoas para não usarem um método obsoleto. Se for facilmente acessível e resolver o problema aqui e agora, a maioria dos desenvolvedores simplesmente o usará. O contra-argumento comum aqui é: _oh, mas então é o problema deles quando ele quebra! _ Mas isso vai contra o objetivo declarado: queremos software funcionando, não estar certo.

Portanto, espero que isso forneça mais informações sobre por que simplesmente mover um tipo parece inútil. Para que as pessoas realmente usem esse novo tipo em sua nova casa, precisamos da intervenção humana. Quando as pessoas se deparam com a dificuldade de alterar manualmente seu código, é melhor ter uma intervenção que _use o novo tipo_ em vez de algo que logo mudará novamente sob seus pés no futuro próximo. Se examinarmos o problema de adicionar um recurso de linguagem para ajudar nas refatorações, o ideal seria permitir que as pessoas movessem gradualmente seu código _ para aquele novo tipo, _ não simplesmente para uma nova casa, pelas razões acima.

Obrigada pelo esclarecimento. Acho que entendo sua posição melhor agora e concordo com seus pressupostos (ou seja, que as pessoas usarão coisas obsoletas, não importa o que aconteça, portanto, fornecer qualquer ajuda possível para orientá-los para a substituição é fundamental). FWIW, meu plano ingênuo para lidar com este problema (não importa com qual solução de reparo gradual usaremos) é uma ferramenta do tipo go-fix para migrar automaticamente o código pacote por pacote no período de depreciação, mas eu admito livremente que Ainda não tentei como e se isso funciona na prática.

@niemeyer Não acredito que sua sugestão seja viável sem uma séria interrupção do sistema de tipos Go.

Considere o dilema apresentado por este código:

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

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

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

Um dos axiomas atuais do pacote reflet é que, se dois tipos são iguais, seus valores refletem. Tipo são iguais. Esta é uma das bases da eficiência da conversão de tipo de tempo de execução do Go. Até onde posso ver, não há como implementar a palavra-chave "adapta" sem quebrar isso.

@rogpeppe Veja a conversa com @rsc sobre reflexão acima. Os dois tipos não são iguais, portanto, refletir apenas diria a verdade e forneceria detalhes para o adaptador quando questionado sobre isso.

@niemeyer Se os dois tipos não forem iguais, não acho que possamos suportar o reparo gradual de código ao mover um tipo entre os pacotes. Por exemplo, digamos que queremos fazer um novo pacote de imagens que mantém a compatibilidade de tipos.

Podemos fazer:

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

Dado o objetivo de reparo gradual do código, acho que é razoável esperar que
uma imagem criada no novo pacote é compatível com funções existentes
que usam o tipo de imagem antigo.

Vamos supor, para fins de argumentação, que o pacote image / png tem
foi convertido para usar newimage, mas image / jpeg não.

Acredito que devemos esperar que este código funcione:

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

mas, uma vez que faz um tipo de declaração contra * image.RGBA e não * newimage.RGBA,
ele falhará no AFAICS, porque os tipos são diferentes.

Digamos que fizemos o tipo declarar acima com sucesso, se o tipo é * imagem.RGBA
ou não. Isso quebraria o invariante atual que:

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

Ou seja, usar uma declaração de tipo estático não apenas declararia o tipo estático de um
valor, mas às vezes pode realmente alterá-lo.

Digamos que decidimos que está tudo bem, então presumivelmente também precisaríamos
para tornar possível converter um tipo adaptado para qualquer interface que qualquer um de seus
suporte a tipos adaptados, caso contrário, o código novo ou antigo pararia
trabalhando ao converter para tipos de interface que são compatíveis com o
tipo que eles estão usando.

Isso leva a outra situação contraditória:

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

No geral, acho que ter dois tipos iguais, mas diferentes
levaria a um sistema de tipos muito difícil de explicar e incompatibilidades inesperadas
no código que usa tipos dinâmicos.

Eu apoio a proposta "tipo X = Y". É simples de explicar e não
atrapalhar muito o sistema de tipos.

@rogpeppe : Eu acredito que a sugestão de @niemeyer é converter implicitamente um tipo adaptado em seu tipo base, semelhante às sugestões anteriores de @josharian .

Para fazer esse trabalho para refatoração gradual, ele também teria que converter implicitamente funções com argumentos de tipos adaptados; em essência, exigiria adicionar covariância à linguagem. Essa certamente não é uma tarefa impossível - muitas linguagens permitem covariância, particularmente para tipos com a mesma estrutura subjacente - mas adiciona muita complexidade ao sistema de tipos, particularmente para tipos de interface .

Isso leva a alguns casos interessantes, como você observou, mas eles não são necessariamente "contraditórios" em si:

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

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

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

Isso ainda me parece contraditório. O modelo atual é muito simples: um valor de interface tem um tipo estático subjacente bem definido. No código acima, inferimos algo sobre esse tipo subjacente, mas quando olhamos o valor, ele não se parece com o que inferimos. Esta é uma mudança séria (e difícil de explicar) na linguagem, na minha opinião.

A discussão aqui parece estar terminando. Com base em uma sugestão de @egonelbre em https://github.com/golang/go/issues/16339#issuecomment -247536289, atualizei o comentário do problema original (no topo) para incluir um resumo vinculado da discussão. longe. Vou postar um novo comentário, como este, cada vez que atualizar o resumo.

No geral, parece que a opinião aqui é para apelidos de tipo, em vez de apelidos generalizados. Possivelmente a ideia do adaptador de Gustavo deslocará apelidos de tipo, mas possivelmente não. Parece um pouco complexo no momento, embora talvez ao final da discussão uma forma mais simples seja alcançada. Eu sugiro que a discussão continue um pouco mais.

Ainda não estou convencido de que vars globais mutáveis ​​são "geralmente um bug" (e nos casos em que são um bug, o detector de corrida é a ferramenta escolhida para encontrar esse tipo de bug). Eu solicitaria que, se esse argumento for usado para justificar a falta de uma sintaxe extensível, um vet-check seja implementado que - digamos - verifique as atribuições a variáveis ​​globais no código não exclusivamente acessíveis por init () ou suas declarações. Eu ingenuamente pensaria que isso não é particularmente difícil de implementar e não deveria ser muito trabalhoso executá-lo - digamos - todos os pacotes registrados do godoc.org para ver quais são os casos de uso para vars globais mutáveis ​​e se fazemos considere todos eles bugs.

(Eu também gostaria de acreditar que, se go crescer vars globais imutáveis, eles deveriam fazer parte de const-declarações, porque isso é o que conceitualmente são e porque isso seria compatível com versões anteriores, mas reconheço que isso provavelmente levará a complicações em torno de quais tipos de expressões podem ser usados ​​em tipos de matriz, por exemplo, e precisariam de mais reflexão)

Re "Restrição? Aliases de tipos de biblioteca padrão só podem ser declarados na biblioteca padrão." - notavelmente, isso evitaria a inclusão do caso de uso de x/image/draw , um pacote existente que expressou interesse em usar apelidos. Eu também poderia muito bem imaginar, por exemplo, pacotes de roteador ou similares usando aliases em net/http forma semelhante ( acena com as mãos ).

Também concordo com os contra-argumentos de todas as restrições, ou seja, sou a favor de não haver nenhuma delas.

@Merovius , que tal vars globais _exportados_ mutáveis? É verdade que um global não exportado pode ser adequado, já que todo o código do pacote sabe como tratá-lo corretamente. É menos óbvio que os globais mutáveis ​​exportados façam sentido. Nós mesmos cometemos esse erro várias vezes na biblioteca padrão. Por exemplo, não há uma maneira completamente segura de atualizar o runtime.MemProfileRate. O melhor que você pode fazer é configurá-lo no início de seu programa e esperar que nenhum pacote importado dê início a uma rotina de inicialização que possa estar alocando memória. Você pode estar certo sobre var vs const, mas podemos deixar isso para outro dia.

Bom ponto sobre x / imagem / desenho. Será adicionado ao resumo na próxima atualização.

Eu gostaria muito de montar um corpus representativo de código Go que pudéssemos analisar para responder a perguntas como as que você levantou. Comecei a tentar fazer isso há algumas semanas e tive alguns problemas. É um pouco mais trabalhoso do que parece que deveria ser, mas é muito importante ter esse conjunto de dados e espero chegar lá.

@rsc sua apresentação GothamGo sobre este tópico foi postada no youtube https://www.youtube.com/watch?v=h6Cw9iCDVcU e seria uma boa adição ao primeiro post.

Na seção "Que outros problemas uma proposta de apelidos de tipo precisa abordar?" seção seria útil especificar que a resposta para "Os métodos podem ser definidos em tipos nomeados por alias?" é um não difícil. Sei que vai contra o espírito decretado da seção, mas percebi que, em muitas conversas sobre apelidos, aqui e em outros lugares, há pessoas que rejeitam imediatamente o conceito por acreditarem que os apelidos necessariamente permitiriam isso e, portanto, causariam problemas do que resolve. Está implícito na definição, mas mencioná-lo explicitamente causaria um curto-circuito em muitas idas e vindas desnecessárias. Embora talvez isso pertença a um FAQ de apelidos na nova proposta de apelidos, deve ser esse o resultado deste tópico.

@Merovius qualquer variável mutável global de pacote exportada pode ser simulada por funções getter e setter em nível de pacote.

Dada a versão n de um pacote p ,

package p
var Global = 0

na versão n + 1 getters e setters podem ser introduzidos e a variável obsoleta

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

e a versão n + 2 poderia não exportar Global

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

(Exercício deixado para o leitor: você também pode envolver o acesso a global em um mutex em n + 2 e descontinuar GetGlobal() em favor do mais idiomático Global() .)

Essa não é uma solução rápida, mas reduz o problema de forma que apenas os apelidos de funções (ou sua solução alternativa atual) sejam estritamente necessários para o reparo gradual do código.

@rsc Um uso trivial para apelidos que você deixou de fora de seu resumo: abreviar nomes longos. (Provavelmente a única motivação para Pascal, que inicialmente não tinha recursos de programação ampla, como pacotes.) Embora seja trivial, é o único caso de uso em que apelidos não exportados fazem sentido, então talvez valha a pena mencionar por esse motivo.

@jimmyfrasche Você está correto. Não gosto da ideia de usar getters e setters (assim como não gosto de tê-los para campos de estrutura), mas sua análise está, claro, correta.

Há um ponto a ser feito sobre o uso sem reparo de aliases (por exemplo, criação de pacotes de substituição instantâneos), mas admito que isso enfraquece o caso de aliases alternativos.

@Merovius concordou em todos os pontos. Eu também não estou feliz com isso, mas tenho que seguir a lógica v☹v

@niemeyer pode esclarecer como os adaptadores ajudariam na migração de tipos em que tanto o antigo quanto o novo têm um método com o mesmo nome, mas assinaturas diferentes. Adicionar um argumento a um método ou alterar o tipo de um argumento parece que seriam evoluções comuns de uma base de código.

@rogpeppe Observe que é exatamente assim que acontece hoje:

type two one

Isso torna one e two tipos independentes, e se refletindo ou sob um interface{} , é isso que você vê. Você também pode converter entre one e two . A proposta do adaptador acima apenas torna a última etapa automática para os adaptadores. Você pode não gostar da proposta por vários motivos, mas não há nada de contraditório nisso.

@iand Como no caso de type two one , os dois tipos têm conjuntos de métodos completamente independentes, portanto, não há nada de especial em combinar nomes. Antes de as bases de código antigas serem migradas, elas permaneceriam usando a assinatura antiga sob o tipo anterior (agora um adaptador). O novo código usando o novo tipo usaria a nova assinatura. Passar um valor do novo tipo para o código antigo o adapta automaticamente porque o compilador sabe que o último é um adaptador do primeiro e, portanto, usa o respectivo conjunto de métodos.

@niemeyer Parece que há muita complexidade escondida por trás desses adaptadores que não estão totalmente especificados. Neste ponto, acho que a simplicidade dos apelidos de tipo pesa fortemente a seu favor. Sentei-me para listar todas as coisas que precisarão ser atualizadas apenas para aliases de tipo, e é uma lista muito longa. A lista certamente seria mais longa para adaptadores, e ainda não entendi totalmente todos os detalhes. Eu gostaria de sugerir que digitemos aliases por enquanto e deixarmos uma decisão sobre os adaptadores relativamente mais pesados ​​para um momento posterior, se você quiser trabalhar em uma proposta completa (mas, novamente, sou cético de que não haja dragões espreitando lá) .

@jimmyfrasche Com relação aos métodos em aliases, certamente aliases não permitem contornar as restrições usuais de definição de método: se um pacote define o tipo T1 = otherpkg.T2, ele não pode definir métodos em T1, assim como não pode definir métodos diretamente em otherpkg.T2. No entanto, se um pacote define o tipo T1 = T2 (ambos no mesmo pacote), a resposta é menos clara. Poderíamos introduzir uma restrição, mas não há (ainda) uma necessidade óbvia para isso.

Atualizado o resumo da discussão de nível superior . Alterar:

  • Adicionado link para o vídeo GothamGo
  • Adicionado "abreviar nomes longos" como um uso possível, por @jba.
  • Adicionado x / image / draw como argumento contra a restrição da biblioteca padrão, por @Merovius.
  • Adicionado mais texto sobre métodos em aliases, por @jimmyfrasche.

Documento de design adicionado:

Como acontecia há uma semana, ainda parece haver um consenso geral para apelidos de tipo. Robert e eu redigimos um documento de design formal, que acabei de verificar (link acima).

Seguindo o processo de proposta , poste comentários substantivos sobre a proposta _aqui_ sobre este assunto. A ortografia / gramática / etc pode ir para a página Gerrit codereview https://go-review.googlesource.com/#/c/34592/. Obrigado.

Gostaria de reconsiderar o "Efeito na incorporação". Limita a usabilidade de aliases de tipo para reparo gradual de código. Ou seja, se p1 deseja renomear um tipo type T1 = T2 e o pacote p2 incorpora p1.T2 em uma estrutura, eles nunca serão capazes de atualizar essa definição para p1.T1 , porque um importador p3 pode referir-se à estrutura incorporada pelo nome. p2 então não pode mudar para p1.T1 sem quebrar p3 ; p3 não pode atualizar o nome para p1.T1 , sem romper com o p2 atual.

Uma maneira de sair disso seria a) em geral limitar qualquer promessa de compatibilidade / período de depreciação ao código que não se refira a campos incorporados por nome, ou b) adicionar um estágio de depreciação separado, então p1 adiciona type T1 = T2 e desaprova T2 , então p2 desaprova referindo-se a (digamos) s2.T2 por nome, todos os importadores de p2 serão reparados para não fazer isso, p2 faz a troca.

Agora, em teoria, o problema pode recomeçar indefinidamente; p4 pode importar p3 , que por sua vez incorpora o tipo de p2 ; parece-me que p3 também precisa ter um período de depreciação, para se referir ao campo incorporado duas vezes pelo nome? Nesse caso, o período de depreciação mais interno torna-se infinitesimal ou o mais externo torna-se infinito. Mas mesmo sem considerar o problema como recursivo, parece-me que b) seria muito difícil de calcular (o período de suspensão de p2 precisaria estar totalmente contido no período de suspensão de p1 . Portanto, se T for um "período de depreciação padrão", você terá que escolher pelo menos 2T ao renomear os tipos, para que as versões sejam alinhadas).

a) também me parece pouco prático; por exemplo, se um tipo incorpora um *byte.Buffer e eu quero definir esse campo (ou passar esse buffer para alguma outra função), simplesmente não há maneira de fazer isso, sem referir-se a ele pelo nome (exceto usando inicializadores de estrutura sem nomes, que também perde as garantias de compatibilidade :)).

Eu entendo a atratividade de ser compatível com byte e rune como apelidos. Mas, pessoalmente, eu colocaria isso em segundo plano, preservando a utilidade dos apelidos de tipo para reparos graduais. Um exemplo (provavelmente ruim) de uma ideia para obter os dois seria, para, para nomes exportados permitir o uso de qualquer alias para se referir a um campo incorporado e para nomes não exportados (inerentemente restrito ao mesmo pacote, portanto, sob maior controle do autor ) mantém a semântica proposta atualmente? Sim, também não gosto dessa distinção. Talvez alguém tenha uma ideia melhor.

Métodos @rsc re em um alias

Se você tiver um tipo S que é um apelido para o tipo T, ambos definidos no mesmo pacote, e permitir a definição de métodos em S, e se T for um apelido para pF definido em um pacote diferente? Embora isso obviamente deva falhar também, há sutilezas na aplicação, implementação e legibilidade da fonte a serem consideradas (se T estiver em um arquivo diferente de S, não é imediatamente claro se você pode definir um método em T olhando para o definição de T).

A regra - se você tiver type T = S , então você não pode declarar métodos em T - é absoluta e é claro a partir dessa única linha na fonte que se aplica, sem ter que investigar a fonte de S, como você faria no alias de situação de alias.

Além disso, permitir métodos em um alias de tipo local confunde a distinção entre um alias de tipo e uma definição de tipo. Uma vez que os métodos seriam definidos em S e T de qualquer maneira, a restrição de que eles só podem ser escritos em um não restringe o que pode ser expresso. Isso apenas mantém as coisas mais simples e uniformes.

@jimmyfrasche Se estamos escrevendo type T1 = T2 e T2 está no mesmo pacote, então provavelmente estamos descontinuando o nome T2. Nesse caso, queremos o mínimo possível de ocorrências de T2 no godoc. Portanto, gostaríamos de declarar todos os métodos como func (T1) M() .

@jba uma mudança godoc para relatar os métodos de um alias como sendo declarados naquele alias atenderia a esse requisito sem alterar a legibilidade da fonte. Em geral, seria bom se godoc exibisse o conjunto completo de métodos de um tipo quando o aliasing e / ou incorporação estiver envolvido, especialmente quando o tipo vier de outro pacote. O problema deve ser resolvido com ferramentas mais inteligentes, não mais semântica de linguagem.

@jba Nesse caso, por que você simplesmente não inverter a direção do alias? type T2 = T1 já permite definir métodos em T1 com a mesma estrutura de pacote; a única diferença é o nome do tipo relatado pelo pacote reflect , e você pode iniciar a migração corrigindo os sites de chamada sensíveis ao nome para não serem sensíveis ao nome antes de adicionar o alias.

@jimmyfrasche Do documento de proposta :

"Como T1 é apenas outra maneira de escrever T2, ele não tem seu próprio conjunto de declarações de método. Em vez disso, o conjunto de métodos de T1 é igual ao de T2. Pelo menos para o teste inicial, não há restrição contra declarações de método usando T1 como um tipo de receptor, fornecido usando T2 na mesma declaração seria válido. "

Usar pF como um tipo de receptor de método nunca é válido.

@mdempsky Não fui muito claro, mas disse que era inválido.

Meu ponto é que fica menos claro se é válido ou não apenas olhando para aquela linha de código específica.

Dado type S = T , você também deve olhar T para ter certeza de que não é também um apelido que dá apelido a um tipo em outro pacote. O único ganho é a complexidade.

Sempre desautorizar métodos em um alias é mais simples e fácil de ler e você não perde nada. Não imagino que um caso confuso surja com muita frequência, mas não há necessidade de introduzir a possibilidade quando você não ganha nada que não possa ser tratado melhor em outro lugar ou por uma abordagem diferente, mas equivalente.

@Merovius

se p1 deseja renomear um tipo de tipo T1 = T2 e o pacote p2 incorpora p1.T2 em uma estrutura, eles nunca poderão atualizar essa definição para p1.T1, porque um importador p3 pode se referir à estrutura incorporada pelo nome.

É possível contornar esse problema hoje em muitos casos, alterando o campo anônimo para um campo nomeado e encaminhando explicitamente os métodos. No entanto, isso não funcionaria para métodos não exportados.

Outra opção pode ser adicionar um segundo recurso para compensar. Se você pudesse adotar o conjunto de métodos de um campo sem torná-lo anônimo (ou com renomeação explícita), isso permitiria que o nome do campo permanecesse inalterado mesmo que o tipo subjacente fosse alterado.

Considerando a declaração do seu exemplo:

package p2

type S struct {
  p1.T2
}

Um recurso de compensação pode ser "aliases de campo", que seguiriam uma sintaxe semelhante para digitar aliases:

package p2

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

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

Outro recurso de compensação pode ser "delegação", que adotaria explicitamente o conjunto de métodos de um campo anônimo:

package p2

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

Acho que prefiro aliases de campo, porque eles também permitiriam outro tipo de reparo gradual: renomear os campos de uma estrutura sem introduzir aliases de ponteiro ou bugs de consistência.

@Merovius O principal problema é quando o tipo é renomeado por um alias.

Eu não considerei isso por completo - apenas de passagem, apenas um pensamento aleatório:

E se você introduzir um alias em seu pacote que o nomeie de volta e o incorpore?

Não sei se isso resolve alguma coisa, mas talvez ganhe algum tempo para quebrar o loop?

@bcmills Não pensei nessa solução alternativa, obrigado. Eu acho que a advertência sobre métodos não divulgados pareceria (para mim) surgir raramente o suficiente na prática para não influenciar minha opinião em geral (a menos que eu não entenda completamente. Sinta-se à vontade para esclarecer, se você acha que isso é útil ) Não acho que acumular mais mudanças seja justificado (ou uma boa ideia).

@Merovius Quanto mais penso nisso, mais gosto da ideia de apelidos de campo.

Encaminhar os métodos explicitamente é tedioso, mesmo se eles forem exportados, e interrompe outros tipos de refatoração (por exemplo, adicionar métodos ao tipo embutido e esperar que o tipo que o embute continue a satisfazer a mesma interface). E renomear campos de estrutura também cai dentro do guarda-chuva geral de permitir o reparo gradual de código.

@Merovius

se p1 deseja renomear um tipo de tipo T1 = T2 e o pacote p2 incorpora p1.T2 em uma estrutura, eles nunca poderão atualizar essa definição para p1.T1, porque um importador p3 pode se referir à estrutura incorporada pelo nome. p2 então não pode mudar para p1.T1 sem quebrar p3; p3 não pode atualizar o nome para p1.T1, sem romper com o p2 atual.

Se entendi seu exemplo, temos:

package p1

type T2 struct {}
type T1 = T2
package p2

import "p1"

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

Acredito que este seja apenas um exemplo específico do caso geral em que desejamos renomear um campo de estrutura; o mesmo problema se aplica se quisermos renomear S.F2 para S.F1.

Neste caso específico, podemos atualizar o pacote p2 para usar a nova API de p1 com um alias de tipo local:

package p2

import "p1"

type T2 = p1.T1

type S struct {
  T2
}

Obviamente, essa não é uma boa solução de longo prazo. Não acho que haja como contornar o fato de que p2 precisará alterar sua API exportada para eliminar o nome T2, no entanto, que procederá da mesma maneira que qualquer renomeação de campo.

Apenas uma nota sobre "mover tipos entre pacotes". Essa formulação não é um pouco problemática?

Pelo que entendi, a proposta permite "referir-se" a uma definição de objeto que está em outro pacote por meio de um novo nome.

Ele não move a definição do objeto, certo? (a menos que alguém escreva o código usando apelidos em primeiro lugar, nesse caso, o usuário é livre para alterar onde o apelido se refere, assim como no pacote de desenho).

@atdiar Referir-se a um tipo em um pacote diferente pode ser usado como uma etapa para mover o tipo. Sim, um alias não move o tipo, mas pode ser usado como uma ferramenta para fazer isso.

@Merovius Fazer isso provavelmente interromperá a reflexão e os plug-ins.

@atdiar , sinto muito, mas não entendo o que você está tentando dizer. Você leu o comentário original deste tópico, o artigo sobre reparos graduais vinculado a ele e a discussão até agora? Se você está tentando adicionar um argumento até agora não considerado à discussão, acredito que precisa ser mais claro.

Finalmente, uma proposta útil e bem escrita. Precisamos de alias de tipo, tenho grandes problemas em criar uma única API sem alias de tipo, até agora, tenho que escrever meu código de uma forma que não gosto muito de fazer isso. Isso deve ser incluído no go v1.8, mas nunca é tarde demais, então vá em frente para o 1.9.

@Merovius
Estou falando explicitamente sobre "mover tipos" entre pacotes. Ele muda a definição do objeto. Por exemplo, no pacote refletido, algumas informações estão vinculadas ao pacote em que um objeto foi definido.
Se você mover a definição, ela pode quebrar.

@kataras não se trata realmente de bons documentos e comentários, é apenas que as definições de tipo não devem ser movidas. Por mais que eu aprecie a proposta do alias, fico desconfiado que as pessoas pensem que podem simplesmente fazer isso.

@atdiar novamente, por favor leia o artigo do comentário original e a discussão até agora. Tipos de movimentação e como lidar com suas preocupações são a principal preocupação deste tópico. Se você acha que o artigo de Russ não aborda adequadamente suas preocupações, seja específico sobre por que a explicação dele não é satisfatória. :)

@kataras Embora eu, pessoalmente, concorde, não acho que seja particularmente útil simplesmente afirmar o quão importante consideramos esse recurso. Deve haver um argumento construtivo a ser feito para abordar as preocupações das pessoas. :)

@Merovius eu li o documento. Não responde à minha pergunta. Acho que fui suficientemente explícito. Está relacionado ao mesmo problema que nos impediu de implementar a proposta de alias anterior.

@atdiar Eu, pelo menos, não entendo. Você está dizendo que mover um tipo quebraria as coisas; a proposta é sobre como evitar essas quebras com reparo gradual, usando um alias, depois atualizar cada dependência reversa até que nenhum código use o tipo antigo, removendo então o tipo antigo. Não vejo como sua afirmação de que "reflexão e plug-ins" estão quebrados se aplica a essas suposições. Se você quiser questionar as suposições, isso já foi discutido.

Também não vejo como qualquer um dos problemas que impedem os aliases de entrar no 1.8 se conecta ao que você disse. As respectivas edições, até onde sei, são # 17746 e # 17784. Se você está se referindo ao problema de incorporação (que pode ser interpretado como relacionado a quebras ou reflexão, embora eu discorde), então isso é abordado na proposta formal (embora, veja acima, eu acredito que a solução proposta merece mais discussão) e você deve ser específico sobre por que não acredita nisso.

Então, sinto muito, mas não, você não foi específico o suficiente. Você tem um número de problema para "o mesmo problema que nos impediu de implementar a proposta de alias anterior" ao qual está se referindo, relacionado ao que você mencionou até agora, para ajudar a entender? Você pode dar um exemplo específico das quebras de que está falando (ver exemplos para este upthread; dar uma sequência de pacotes, definições de tipo e algum código e descrever como ele quebra quando transformado conforme proposto)? Se você deseja que suas preocupações sejam abordadas, você realmente precisa ajudar os outros a compreendê-las primeiro.

@Merovius Portanto, no caso de dependências transitivas em que uma dessas dependências está olhando para reflect.Type.PkgPath (), o que acontece?
Esse é o mesmo problema que ocorre no problema de incorporação.

@atdiar , desculpe, não vejo como isso seja de alguma forma uma preocupação compreensível, à luz da discussão neste tópico até agora e do que trata esta proposta. Vou sair deste subtítulo específico agora e dar a outros, que possam entender melhor sua objeção, a oportunidade de abordá-la.

Deixe-me reformular de forma concisa:

O problema é sobre a igualdade de tipo, dado o fato de que a definição de tipo codifica seu próprio local.
Visto que a igualdade de tipo pode ser e é testada em tempo de execução, não vejo como mover tipos é tão fácil de fazer.

Estou simplesmente alertando que este caso de uso de "tipos móveis" pode estar quebrando muitos pacotes à distância. Preocupação semelhante com plug-ins.

(da mesma forma que alterar o tipo de um ponteiro em um pacote quebraria muitos outros pacotes, se esse paralelo puder tornar as coisas mais claras.)

@atdiar Novamente, este problema é sobre como mover tipos em duas etapas, primeiro descontinuando o local antigo e atualizando as dependências reversas, _então_ mova o tipo. _Claro_ as coisas vão quebrar se você apenas mover os tipos, mas não é disso que se trata. Trata-se de habilitar uma solução gradual e de várias etapas para fazer isso. Se você estiver preocupado com o fato de qualquer uma das soluções propostas aqui não permitir esse processo de várias etapas, seja preciso e descreva uma situação em que nenhuma sequência razoável de reparos graduais possa evitar uma quebra.

@niemeyer

Isso torna um e dois tipos independentes, e seja refletindo ou sob uma interface {}, isso é o que
você vê. Você também pode converter entre um e dois. A proposta do adaptador acima apenas faz isso durar
passo automático para adaptadores. Você pode não gostar da proposta por vários motivos, mas não há nada
contraditório sobre isso.

Você não pode converter entre

 func() one

e

func() two

@Merovius Você não pode pensar em mudar todos os importadores de um pacote com código reparado que existe no mundo. E não estou muito interessado em começar a me aprofundar no controle de versão de pacote aqui.

Para ser claro, não sou contra a proposta de alias, mas a formulação de "mover tipos entre pacotes", que implica em um caso de uso que ainda não é comprovadamente seguro.

@jimmyfrasche sobre a previsibilidade da validade do método no alias:

Já é o caso que func (t T) M() às vezes é válido, às vezes inválido. Não surge muito porque as pessoas não ultrapassam esses limites com muita frequência. Ou seja, funciona bem na prática. https://play.golang.org/p/bci2qnldej. Em qualquer caso, isso está na lista de restrições _possíveis_. Como todas as restrições possíveis, ele adiciona complexidade e queremos ver evidências concretas do mundo real antes de adicionar essa complexidade.

@Merovius ,

Concordo que a situação não é perfeita. No entanto, se eu tiver uma base de código cheia de referências a io.ByteBuffer e quiser movê-la para bytes.Buffer, quero ser capaz de apresentar

package io
type ByteBuffer = bytes.Buffer

_sem_ atualizar qualquer uma das referências existentes para io.ByteBuffer. Se todos os lugares onde io.ByteBuffer estão embutidos mudarem automaticamente o nome do campo para Buffer como resultado da substituição de uma definição de tipo por um alias, então eu quebrei o mundo e não há reparo gradual. Em contraste, se o nome de um io.ByteBuffer incorporado ainda for ByteBuffer, os usos podem ser atualizados um de cada vez em seus próprios reparos graduais (possivelmente tendo que fazer várias etapas; novamente, não é o ideal).

Discutimos isso com alguma profundidade em # 17746. Eu estava originalmente apoiando o nome de um alias io.ByteBuffer incorporado sendo Buffer, mas o argumento acima me convenceu de que eu estava errado. @jimmyfrasche em particular fez alguns bons argumentos sobre o código não mudar dependendo da definição da coisa incorporada. Não acho que seja sustentável proibir completamente os aliases incorporados.

Observe que há uma solução alternativa em p2 em seu exemplo. Se p2 realmente deseja um campo incorporado denominado ByteBuffer sem se referir a io.ByteBuffer, ele pode definir:

type ByteBuffer = bytes.Buffer

e, em seguida, incorpore um ByteBuffer (ou seja, um p2.ByteBuffer) em vez de um io.ByteBuffer. Isso também não é perfeito, mas significa que os reparos podem continuar.

Definitivamente, isso não é perfeito e que as renomeações de campos em geral não são abordadas nesta proposta. Pode ser que a incorporação não seja sensível ao nome subjacente, que deva haver algum tipo de sintaxe para 'incorporar X como nome N'. Também pode ser que devamos adicionar aliases de campo posteriormente. Ambas parecem ideias razoáveis ​​a priori e provavelmente devem ser propostas separadas, avaliadas posteriormente com base em evidências reais de uma necessidade. Se os apelidos de tipo nos ajudarem a chegar ao ponto em que a falta de apelidos de campo é o próximo grande obstáculo para refatorações em grande escala, isso será um progresso!

(/ cc @neild e @bcmills)

@atdiar , sim, é verdade que a reflexão verá através desses tipos de mudanças, e se o código depender dos resultados da reflexão, ele será interrompido. Como a situação com incorporação, não é perfeito. Ao contrário da situação com incorporação, não tenho nenhuma resposta, exceto talvez o código não deva ser escrito usando refletir para ser tão sensível a esses detalhes.

@rsc O que eu tinha em mente era a) proibir a incorporação de um alias e de seu tipo de definição na mesma estrutura (para evitar ambigüidade de b), b) permitir a referência a um campo por qualquer um dos nomes no código-fonte, c) escolher um ou o outro na informação / reflexão do tipo gerado e semelhantes (não importa qual).

Eu diria, acenando com a mão, que isso ajuda a evitar o tipo de rupturas que tentei descrever, ao mesmo tempo que faço uma escolha clara para o caso em que uma escolha é necessária; e, pessoalmente, me importo menos em não quebrar o código que depende de reflexão, do que o código que não depende.

Não tenho certeza agora se entendi seu argumento do ByteBuffer, mas também estou no final de um longo dia de trabalho, então não há necessidade de explicar mais, se não achar convincente, responderei eventualmente :)

@Merovius Acho que faz sentido tentar as regras simples e ver até onde chegamos antes de introduzir as mais complexas. Podemos adicionar (a) e (b) posteriormente, se necessário; (c) é um dado, não importa o quê.

Concordo que talvez (b) seja uma boa ideia em certas circunstâncias, mas talvez não em outras. Se você estiver usando aliases de tipo para o caso de uso "estruturar uma API de um pacote em vários pacotes de implementação" mencionado anteriormente, talvez não queira incorporar o alias para expor o outro nome (que pode estar em um pacote interno e caso contrário, inacessível para a maioria dos usuários). Espero que possamos ganhar mais experiência.

@rsc

Talvez adicionar informações de nível de pacote sobre aliasability aos arquivos de objeto possa ajudar.
(Levando em consideração se os plug-ins go devem continuar funcionando corretamente ou não.)

@Merovius @rsc

a) proibir a incorporação de um alias e de seu tipo de definição na mesma estrutura

Observe que, em muitos casos, isso já é proibido como consequência da maneira como a incorporação interage com os conjuntos de métodos. (Se o tipo incorporado tiver um conjunto de métodos não vazio e um desses métodos for chamado, o programa não conseguirá compilar: https://play.golang.org/p/XkaB2a0_RK.)

Portanto, adicionar uma regra explícita que proíba a dupla incorporação parece que só faria diferença em um pequeno subconjunto de casos; não parece valer a pena a complexidade para mim.

Por que não abordar aliases de tipo como tipos algébricos e oferecer suporte a aliases para um conjunto de tipos, de modo que também tenhamos uma interface vazia equivalente à verificação de tipo em tempo de compilação como um bônus, a la

type Stringeroonie = {string,fmt.Stringer}

@ j7b

Por que não abordar aliases de tipo como tipos algébricos e oferecer suporte a aliases para um conjunto de tipos

Os aliases são semanticamente e estruturalmente equivalentes ao tipo original. Os tipos de dados algébricos não são: no caso geral, eles requerem armazenamento adicional para tags de tipo. (Os tipos de interface Go já carregam essas informações de tipo, mas structs e outros tipos que não são de interface não.)

@bcmills

Isso pode ser um raciocínio incorreto, mas pensei que o problema poderia ser abordado como o alias A do tipo T é equivalente a declarar A como interface {} e permitir que o compilador converta de forma transparente as variáveis ​​do tipo A em T em escopos onde as variáveis ​​do tipo A são declaradas , que eu pensei que seria principalmente um custo de tempo de compilação linear, inequívoco, e criaria uma base para pseudótipos gerenciados pelo compilador, incluindo algébricos usando a sintaxe type T = , e possivelmente também permitir a implementação de tipos como referências imutáveis ​​em tempo de compilação como no que diz respeito ao código do usuário, seria apenas a interface {} s "nos bastidores".

Deficiências nessa linha de pensamento provavelmente seriam produto da ignorância e, como não estou em posição de oferecer uma prova prática de conceito, fico feliz em aceitar que é deficiente e adiar.

@ j7b Mesmo que o ADT seja uma solução para um problema de reparo gradual, eles criam o seu próprio; é impossível adicionar ou remover qualquer membro de um ADT sem quebrar as dependências. Então, em essência, você criaria mais problemas do que resolveria.

Sua ideia de traduzir de forma transparente de e para a interface {} também não funciona para tipos de ordem superior como []interface{} . E, eventualmente, você vai acabar perdendo um dos pontos fortes do go, que é dar aos usuários controle sobre o layout dos dados e, em vez disso, embrulhar tudo em java.

ADT não é a solução aqui.

@Merovius Tenho certeza de que, se uma construção de tipo algébrica inclui renomeação (o que seria consistente com uma definição razoável da mesma), é uma solução, essa interface {} pode servir como um proxy para a forma de projeção e seleção gerenciada pelo compilador descrito, e não tenho certeza de como o layout de dados é relevante nem como você está definindo tipos de "ordem superior", um tipo é apenas um tipo se pode ser declarado e [] interface {} é apenas um tipo.

Deixando tudo isso de lado, tenho certeza de que type T = tem o potencial de ser sobrecarregado de maneiras intuitivas e úteis além da renomeação, tipos algébricos e referências publicamente imutáveis ​​parecem as aplicações mais óbvias, então espero que a especificação acabe declarando essa sintaxe indica um metatipo ou pseudotipo gerenciado por compilador e considera-se todas as maneiras pelas quais um tipo gerenciado por compilador pode ser útil e a sintaxe que melhor expressa esses usos. Visto que uma nova sintaxe não precisa se preocupar com o conjunto de palavras globalmente reservadas quando usada como qualificadores, algo como type A = alias Type seria claro e extensível.

@ j7b

Deixando tudo isso de lado, tenho certeza de que o tipo T = tem o potencial de ser sobrecarregado de maneiras úteis e intuitivas além de renomear,

Certamente espero que não. Go é (na maior parte) bem ortogonal hoje, e manter essa ortogonalidade é uma coisa boa.

A maneira, hoje, que se declara um novo tipo T em Go é type T def , onde def é a definição do novo tipo. Se alguém fosse implementar tipos de dados algébricos (também conhecidos como uniões marcadas), eu esperaria que eles seguissem essa sintaxe em vez da sintaxe para aliases de tipo.

Eu gosto de apresentar um ponto de vista diferente (no suporte) de aliases de tipo, que pode fornecer alguns insights sobre casos de uso alternativos além da refatoração:

Vamos voltar por um momento e assumir que não tínhamos declarações regulares do tipo Go no formato type T <a type> , mas apenas declarações de alias type A = <a type> .

(Para completar a imagem, vamos também supor que os métodos são de alguma forma declarados de forma diferente - não por meio de associação ao tipo nomeado usado como receptor, porque não podemos. Por exemplo, pode-se imaginar a noção de um tipo de classe com os métodos literalmente dentro e, portanto, não precisamos depender de um tipo nomeado para declarar métodos. Dois desses tipos que são estruturalmente idênticos, mas têm métodos diferentes, seriam tipos diferentes. Os detalhes não são importantes aqui para este experimento de pensamento.)

Afirmo que, em tal mundo, poderíamos escrever praticamente o mesmo código que escrevemos agora: usamos os nomes de tipo (alias) para que não tenhamos que nos repetir, e os próprios tipos garantem que usamos dados em um tipo -maneira segura.

Em outras palavras, se Go tivesse sido projetado dessa forma, provavelmente estaríamos bem também, em geral.

Mais ainda, em tal mundo, porque os tipos são idênticos se forem estruturalmente idênticos (não importa o nome), os problemas que temos com a refatoração agora não teriam aparecido em primeiro lugar, e não haveria necessidade de qualquer mudanças no idioma.

Mas não teríamos um mecanismo de segurança que temos no Go atual: não seríamos capazes de introduzir um nome para um tipo e afirmar que agora deve ser um tipo novo e diferente. (Ainda assim, é importante ter em mente que é, em essência, um mecanismo de segurança.)

Em outras linguagens de programação, a noção de fazer um tipo novo e diferente de um tipo existente é chamada de "marca": um tipo obtém é uma marca anexada a ele que o torna diferente de todos os outros tipos. Por exemplo, em Modula-3, havia uma palavra-chave especial BRANDED para fazer isso acontecer (por exemplo, TYPE T = BRANDED REF T0 criaria uma referência nova e diferente para T0). Em Haskell, a palavra new antes de um tipo tem um efeito semelhante.

Voltando ao nosso mundo Go alternativo, podemos nos encontrar em uma posição onde não temos problemas com a refatoração, mas onde queríamos melhorar a segurança do nosso código para que type MyBuffer = []byte e type YourBuffer = []byte denotem tipos diferentes para que não usemos acidentalmente o errado. Podemos propor a introdução de uma forma de branding de tipo exatamente para esse propósito. Por exemplo, podemos querer escrever type MyBuffer = new []byte , ou mesmo type MyBuffer = new YourBuffer com o efeito de que MyBuffer agora é um tipo diferente de YourBuffer.

Em essência, este é o problema duplo do que temos agora. Acontece que em Go, desde o primeiro dia, sempre trabalhamos com tipos "de marca" assim que eles ganhavam um nome. Em outras palavras, type T <a type> é efetivamente type T = new <a type> .

Para resumir: no Go existente, os tipos nomeados são sempre tipos de "marca" e não temos a noção de apenas um nome para um tipo (que agora chamamos de apelidos de tipo). Em várias outras linguagens, apelidos de tipo são a norma, e é necessário usar um mecanismo de "marca" para criar um tipo explicitamente novo e diferente.

A questão é que ambos os mecanismos são inerentemente úteis e, com aliases de tipo, finalmente contornamos para oferecer suporte a ambos.

@griesemer A extensão desse recurso é a proposta de alias inicial que deve limpar a refatoração de maneira ideal. Receio que apenas aliases de tipo criariam casos difíceis de refatoração devido ao seu escopo restrito.

Em ambas as propostas, fico imaginando se a colaboração do vinculador não deve ser necessária porque o nome faz parte da definição de tipo em Go, conforme você explicou.

Não estou familiarizado com o código-objeto, então é apenas uma ideia, mas parece que é possível adicionar seções personalizadas a arquivos-objeto. Se por acaso fosse possível manter uma espécie de lista encadeada desenrolada, preenchida na hora do link dos nomes dos tipos e seus apelidos talvez isso ajudasse. O tempo de execução teria todas as informações de que precisa, sem sacrificar a compilação separada.

A ideia é que o tempo de execução deve ser capaz de retornar dinamicamente os diferentes aliases para um determinado tipo, de modo que as mensagens de erro permaneçam claras (já que o aliasing introduz uma discrepância de nomenclatura entre o código em execução e o código escrito).

Uma alternativa para rastrear o uso de apelidos seria ter uma história de controle de versão concreta em geral, para ser capaz de "mover" definições de objetos entre os pacotes, como foi feito para o pacote de contexto. Mas esse é um problema totalmente diferente.

No final, ainda é uma boa ideia deixar a equivalência estrutural para as interfaces e a equivalência de nomes para os tipos.
Dado o fato de que um tipo pode ser considerado uma interface com mais restrições, parece que a declaração de um alias deve / poderia ser implementada mantendo uma fatia por pacote de strings de nomes de tipos de fatias.

@atdiar Não tenho certeza se você quer dizer o que quero dizer quando diz "compilação separada". Se o pacote P importar io e bytes, todos os três poderão ser compilados como etapas separadas. Entretanto, se io ou bytes mudar, então P deve ser recompilado. _Não_ é o caso que você pode fazer alterações em io ou bytes e então apenas usar uma compilação antiga de P. Mesmo no modo plugin, isso é verdade. Devido a efeitos como cross-package inlining, até mesmo alterações não visíveis pela API na implementação de io ou bytes alteram a ABI efetiva, razão pela qual P deve ser recompilado. Os apelidos de tipo não tornam este problema pior.

@ j7d , em um nível de sistema de tipo, tipos de soma ou qualquer tipo de subtipagem (como sugerido por outros anteriormente na discussão) apenas ajudam com certos tipos de uso. É verdade que podemos pensar em bytes.Buffer como um subtipo de io.Reader ("um Buffer é um Leitor" ou, em seu exemplo, "uma string é um Stringeroonie"). Os problemas acontecem ao construir tipos mais complexos usando esses. O restante deste comentário fala sobre os tipos Go, mas fala sobre seus relacionamentos fundamentais em um nível de subtipagem, não sobre o que Go a linguagem realmente implementa. Go deve implementar regras consistentes com os relacionamentos fundamentais, no entanto.

Um construtor de tipo (uma maneira elegante de dizer "uma maneira de usar um tipo") é covariante se preserva a relação de subtipagem, e contravariante se inverte a relação.

Usar um tipo em um resultado de função é covariante. A func () Buffer "é um" func () Reader, porque retornar um Buffer significa que você retornou um Reader. Usar um tipo em um argumento de função é _não_ covariant. Uma função (Buffer) não é uma função (Leitor), porque a função precisa de um Buffer e alguns Leitores não são Buffers.

Usar um tipo em um argumento de função é contravariante. Uma função (Leitor) é uma função (Buffer), porque a função precisa apenas de um Leitor, e um Buffer é um Leitor. Usar um tipo em um resultado de função _não_ é contravariante. Um func () Reader não é um buffer func (), porque o func retorna um Reader, e alguns leitores não são buffers.

Combinando os dois, um func (Reader) Reader não é um func (Buffer) Buffer, nem vice-versa, porque ou os argumentos não funcionam ou os resultados não funcionam. (A única combinação ao longo dessas linhas que funciona seria que uma função (Leitor) Buffer é uma função (Buffer) Leitor.)

Em geral, se func (X1) X2 é um (subtipo de) func (X3) X4, então deve ser que X3 é um (subtipo de) X1 e da mesma forma X2 é um (subtipo de) X4. No caso do alias usar onde queremos que T1 e T2 sejam intercambiáveis, uma func (T1) T1 é um subtipo de func (T2) T2 somente se T1 for um subtipo de T2 _and_ T2 for um subtipo de T1. Isso basicamente significa que T1 é o mesmo tipo de T2, não um tipo mais geral.

Usei argumentos de função e resultados porque esse é o exemplo canônico (e um bom), mas o mesmo acontece com outras maneiras de construir resultados complexos. Em geral, você obtém covariância para saídas (como func () T, ou <-chan T, ou [...] map T) e contravariância para entradas (como func (T), ou chan <- T, ou map [T ] ...) e igualdade de tipo forçada para entrada + saída (como func (T) T, ou chan T, ou * T, ou [10] T, ou [] T, ou struct {Field T}, ou uma variável do tipo T). Na verdade, o caso mais comum em Go, como você pode ver nos exemplos, é entrada + saída.

Concretamente, um [] Buffer não é um [] Leitor (porque você pode armazenar um Arquivo em um [] Leitor, mas não em um [] Buffer), nem é um [] Leitor um [] Buffer (porque obtendo de um [] O leitor pode retornar um arquivo, enquanto a busca de um [] Buffer deve retornar um Buffer).

Uma conclusão de tudo isso é que, se você deseja resolver o problema geral de reparo do código de forma que o código possa usar T1 ou T2, você não pode fazer isso com nenhum esquema que torne T1 apenas um subtipo de T2 (ou vice-versa). Cada um precisa ser um subtipo do outro - ou seja, eles precisam ser do mesmo tipo - ou alguns desses usos listados serão inválidos.

Ou seja, a subtipagem não é suficiente para resolver o problema de reparo gradual do código. É por isso que apelidos de tipo introduzem um novo nome para o mesmo tipo, de modo que T1 = T2, em vez de tentar subtipagem.

Este comentário também se aplica à sugestão de resposta de

Atualizado o resumo da discussão de nível superior. Alterar:

  • Removido TODO para atualizar o resumo da discussão do adaptador, que parece ter desaparecido.
  • Adicionado resumo da discussão de incorporação e renomeações de campos.
  • Resumo de 'métodos em aliases' movido para sua própria seção fora da lista de perguntas de design, expandido para incluir comentários recentes.
  • Adicionado resumo da discussão do efeito em programas que usam reflexão.
  • Adicionado resumo da discussão da compilação separada.
  • Adicionado resumo da discussão de várias abordagens baseadas em subtipos.

@rsc sobre compilação separada, meu comentário é relativo a se as definições de tipo precisam manter uma lista de seus apelidos (que não é tratável em grande escala, por causa do requisito de compilação separada) ou cada apelido envolve a construção iterativa de uma lista de nomes de apelidos a seguir o gráfico de importação, todos relacionados ao nome de tipo inicial fornecido na definição de tipo. (e como e onde manter essas informações para que o tempo de execução tenha acesso a elas).

@atdiar Não existe essa lista de nomes de alias em nenhum lugar do sistema. O tempo de execução não tem acesso a ele. Aliases não existem em tempo de execução.

@rsc Huh, desculpe. Estou preso com a proposta de alias inicial no head e estava pensando em aliasing para func (enquanto discuto o aliasing para tipos). Nesse caso, haveria uma discrepância entre os nomes no código e os nomes em tempo de execução.
O uso das informações em runtime.Frame para registro precisaria ser repensado nesse caso.
Não se preocupe comigo.

@rsc obrigado por resumir novamente. O nome do campo incorporado ainda me irrita; todas as soluções alternativas propostas contam com erros permanentes para manter os nomes antigos. Embora o ponto mais amplo deste comentário , a saber, que este é um caso especial de renomeação de campos, o que também não é possível, me convence de que isso deve de fato ser visto (e resolvido) como um problema separado. Faria sentido abrir um problema separado para uma solicitação / proposta / discussão para oferecer suporte a renomeações de campo para reparo gradual (possivelmente abordado na mesma versão go)?

@Merovius , concordo que o reparo gradual de código para renomeação de campo parece o próximo problema na sequência. Para iniciar essa discussão, acho que alguém precisaria reunir um conjunto de exemplos do mundo real, tanto para termos alguma evidência de que é um problema generalizado quanto para verificar possíveis soluções. Realisticamente, não vejo isso acontecendo com o mesmo lançamento.

De volta de duas semanas longe. A discussão parece ter convergido. Até mesmo a atualização da discussão há duas semanas foi bastante pequena.

Eu sugiro que nós:

  • aceitar a proposta de alias de tipo como a solução provisória para o problema apresentado acima,
    desde que uma implementação possa estar pronta para as pessoas experimentarem no início do Go 1.9 (1º de fevereiro).
  • crie um branch dev dev.typealias para que os CLs possam ser revisados ​​agora (janeiro) e mesclados no master no início do Go 1.9.
  • tomar uma decisão final sobre manter os aliases de tipo próximo ao início do congelamento do Go 1.9 (como fizemos para aliases generalizados no ciclo do Go 1.8).

+1

Agradeço a história de discussão por trás dessa mudança. Digamos que esteja implementado. Sem dúvida, isso se tornará um detalhe bastante marginal da linguagem, ao invés de uma característica central. Como tal, adiciona complexidade à linguagem e ferramentas desproporcional à sua frequência de uso real. Ele também adiciona mais área de superfície na qual o idioma pode ser inadvertidamente abusado. Por esse motivo, ser excessivamente cauteloso é uma coisa boa, e estou feliz que tenha havido muita discussão até agora.

@Merovius : Desculpe por editar meu post! Achei que ninguém estava lendo. Inicialmente, neste comentário, expressei algum ceticismo de que essa mudança de linguagem seja necessária quando já existem ferramentas como a ferramenta gorename .

@ jcao219 Isso já foi discutido antes, mas, surpreendentemente, não consigo encontrar isso rapidamente aqui. Ele é discutido detalhadamente no tópico original para aliases gerais # 16339 e os tópicos de nozes golang associadas. Resumindo: este tipo de ferramenta aborda apenas como preparar os commits de reparo, não como sequenciar as alterações para evitar quebras. Se as mudanças são feitas por uma ferramenta ou por um humano é irrelevante para o problema, que atualmente não há nenhuma sequência de commits que não quebrará um código ou outro (o comentário original deste problema e o documento associado justificam esta declaração mais em -profundidade).

Para ferramentas mais automatizadas (por exemplo, integrado à ferramenta go ou semelhante), o comentário original aborda isso sob o título "Pode ser uma mudança apenas de ferramenta ou compilador em vez de uma mudança de idioma?".

Em conclusão, digamos que a mudança seja implementada. Sem dúvida, isso se tornará um detalhe bastante marginal da linguagem, ao invés de uma característica central.

Eu gostaria de expressar dúvidas. :) Não considero esta uma conclusão precipitada.

@Merovius

Eu gostaria de expressar dúvidas. :) Não considero esta uma conclusão precipitada.

Acho que quis dizer que as pessoas que usariam esse recurso seriam principalmente os mantenedores de pacotes Go importantes com muitos clientes dependentes. Em outras palavras, ele beneficia aqueles que já são especialistas em Go. Ao mesmo tempo, apresenta uma maneira tentadora de tornar o código menos legível para novos programadores de Go. A exceção é o caso de uso de renomeação de nomes longos, mas os nomes naturais do tipo Go geralmente não são muito longos ou complexos.

Assim como no caso do recurso de importação de ponto, seria aconselhável que os tutoriais e documentos acompanhassem suas menções a esse recurso com uma declaração sobre as diretrizes de uso.

Por exemplo, digamos que eu quisesse usar "github.com/gonum/graph/simple".DirectedGraph e quisesse adicionar digraph para evitar digitar simple.DirectedGraph , seria uma boa caso de uso? Ou esse tipo de renomeação deve ser restrito a nomes excessivamente longos gerados por coisas como protobuf?

@ jcao219 , o resumo da discussão no topo desta página responde às suas perguntas. Em particular, consulte estas seções:

  • Isso pode ser uma mudança de ferramenta ou apenas do compilador em vez de uma mudança de linguagem?
  • Que outros usos os apelidos podem ter?
  • Restrições (as notas gerais que começam nessa seção)

Para seu ponto mais geral, sobre especialistas em Go versus novos programadores em Go, um objetivo explícito do Go é facilitar a programação em grandes bases de código. O fato de você ser um especialista não está relacionado ao tamanho da base de código em que está trabalhando. (Talvez você esteja apenas começando um novo projeto que outra pessoa iniciou. Talvez ainda seja necessário fazer esse tipo de trabalho).

OK, com base na unanimidade / silêncio aqui, irei (como sugeri na semana passada em https://github.com/golang/go/issues/18130#issuecomment-268614964) marcar esta proposta como aprovada e criar um branch dev.typealias .

O excelente resumo tem uma seção "Que outras questões uma proposta de apelidos de tipo precisa abordar?" Quais são os planos para resolver esses problemas depois que a proposta for declarada como aceita?

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

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

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

@ulikunitz re os problemas (todas essas citações do documento de design assumem 'tipo T1 = T2'):

  1. Manipulação em godoc. O documento de design especifica mudanças mínimas no godoc. Assim que entrarmos, podemos ver se é necessário suporte adicional. Talvez sim, mas talvez não.
  2. Os métodos podem ser definidos em tipos nomeados por alias? sim. Documento de design: "Como T1 é apenas outra maneira de escrever T2, ele não tem seu próprio conjunto de declarações de método. Em vez disso, o conjunto de métodos de T1 é igual ao de T2. Pelo menos para o teste inicial, não há restrição contra declarações de métodos usando T1 como um tipo de receptor, desde que usando T2 na mesma declaração seria válido. "
  3. Se apelidos para apelidos são permitidos, como tratamos os ciclos de apelidos? Sem ciclos. Documento de design: "Em uma declaração de alias de tipo, ao contrário de uma declaração de tipo, T2 nunca deve se referir, direta ou indiretamente, a T1."
  4. Os aliases devem ser capazes de exportar identificadores não exportados? sim. Documento de design: "Não há restrições quanto à forma de T2: pode ser de qualquer tipo, incluindo, mas não se limitando a tipos importados de outras embalagens."
  5. O que acontece quando você incorpora um alias (como você acessa o campo incorporado)? O nome é obtido do alias (o nome visível no programa). Documento de design: https://golang.org/design/18130-type-alias#effect -on-embedding.
  6. Os apelidos estão disponíveis como símbolos no programa integrado? Não. Documento de design: "Os aliases de tipo são quase sempre invisíveis em tempo de execução." (A resposta segue a partir disso, mas não é explicitamente mencionada.)
  7. Injeção de string Ldflags: e se nos referirmos a um alias? Não há aliases de var, portanto, isso não ocorre.

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

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

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

@rsc Muito obrigado pelos esclarecimentos.

Vamos assumir:

package a

import "b"

type T1 = b.T2

Tanto quanto eu entendo, T1 é essencialmente idêntico a b.T2 e, portanto, é um tipo não local e nenhum método novo pode ser definido. O identificador T1 é, entretanto, reexportado no pacote a. Esta é uma interpretação correta?

@ulikunitz isso é correto

T1 denota exatamente o mesmo tipo que b.T2. É simplesmente um nome diferente. Se algo é exportado ou não, é baseado apenas em seu nome (não tem nada a ver com o tipo que denota).

Para tornar a resposta de @griesemer explícita: sim, T1 é exportado do pacote a (porque é T1, não t1).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Isso agora está no mestre, antes da abertura do Go 1.9. Sinta-se à vontade para sincronizar no master e experimentar. Obrigado.

Redirecionado de # 18893

package main

import (
        "fmt"
        "q"
)

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

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

O que você esperava ver?

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

O que você viu em vez disso?

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

Discussão

Os aliases não devem ser aplicados a tipos sem nome. Não existe uma história de "reparo de código" na passagem de um tipo sem nome para outro. Permitir apelidos em tipos não nomeados significa que não posso mais ensinar Go como tipos simplesmente nomeados e não nomeados. Em vez disso, tenho que dizer

ah, a menos que seja um alias, nesse caso você deve se lembrar que _pode ser_ um tipo sem nome, mesmo quando você importa de outro pacote.

E pior, permitirá às pessoas promulgar padrões anti legibilidade como

type Any = interface{}

Não permita que tipos não nomeados tenham aliases.

@davecheney

Não há nenhuma história de "reparo de código" na mudança de um tipo sem nome para outro.

Não é verdade. E se você quiser alterar o tipo de um parâmetro de método de um tipo nomeado para um não nomeado ou vice-versa? A etapa 1 é adicionar o alias; a etapa 2 é atualizar os tipos que implementam esse método para usar o novo tipo; a etapa 3 é remover o alias.

(É verdade que você pode fazer isso hoje renomeando o método duas vezes. A renomeação dupla é tediosa na melhor das hipóteses.)

E pior, permitirá às pessoas promulgar padrões anti legibilidade como
type Any = interface{}

As pessoas já podem escrever type Any interface{} hoje. Que dano adicional os aliases apresentam neste caso?

As pessoas já podem escrever qualquer interface {} hoje. Que dano adicional os aliases apresentam neste caso?

Eu o chamei de anti-padrão porque é exatamente isso que ele é. type Any interface{} , porque é a pessoa que _escreve_ o código digita algo um pouco mais curto, isso faz um pouco mais de sentido para ela.

Por outro lado, _todos_ os leitores, que têm experiência na leitura de código Go e reconhecem interface{} tão instintivamente quanto seu rosto no espelho, precisam aprender e reaprender cada variante de Any , Object , T , e mapeie-os para coisas como type Any interface{} , type Any map[interface{}]interface{} , type Any struct{} por pacote.

Certamente você concorda que nomes específicos de pacotes para expressões idiomáticas Go comuns são um resultado negativo para a legibilidade?

Certamente você concorda que nomes específicos de pacotes para expressões idiomáticas Go comuns são um resultado negativo para a legibilidade?

Eu concordo, mas como o exemplo em questão (de longe a ocorrência mais comum desse antipatterrn que encontrei) pode ser feito sem aliases, não entendo como esse exemplo se relaciona à proposta de aliases de tipo.

O fato de que o antipadrão é possível sem apelidos de tipo significa que já devemos educar os programadores Go para evitá-lo, independentemente da existência de apelidos para tipos não nomeados.

E, de fato, aliases de tipo permitem a _ remoção gradual_ daquele antipadrão de bases de código nas quais ele já existe.

Considerar:

package antipattern

type Any interface{}  // not an alias

type Widget interface{
  Frozzle(Any) error
}

func Bozzle(w Widget) error {
  …
}

Hoje, os usuários de antipattern.Bozzle estariam presos usando antipattern.Any em suas Widget implementações, e não há como remover antipattern.Any com reparos graduais. Mas com aliases de tipo, o proprietário do pacote antipattern poderia redefini-lo assim:

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

E agora os chamadores podem migrar de Any para interface{} gradualmente, permitindo que o mantenedor de antipattern eventualmente o remova.

Meu ponto é que não há justificativa para aliasing tipos sem nome, então
desautorizar esta opção continuaria a sublinhar a inadequação de
a prática.

O oposto, permitir o alias de tipos sem nome permite não um, mas dois
formas deste anti padrão.

Na quinta-feira, 2 de fevereiro de 2017, 16:34, Bryan C. Mills [email protected] escreveu:

Certamente você concorda que nomes específicos de pacotes para expressões idiomáticas Go comuns são
um negativo líquido para legibilidade?

Eu concordo, mas desde o exemplo em questão (de longe o mais comum
ocorrência daquele antipatterrn que encontrei) pode ser feito sem
aliases, não entendo como esse exemplo se relaciona com a proposta de
digite aliases.

O fato de o antipadrão ser possível sem apelidos de tipo significa que
já devemos educar os programadores Go para evitá-lo, independentemente de
podem existir apelidos para tipos sem nome.

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/18130#issuecomment-276872714 ou mudo
o segmento
https://github.com/notifications/unsubscribe-auth/AAAcA6BGrFjjTi7eW1BPp7o81XIekbGXks5rYWr-gaJpZM4LBBEL
.

@davecheney Acho que não temos nenhuma evidência de que ser capaz de dar um nome a um literal de tipo arbitrário seja prejudicial. Este também não é um recurso "surpresa" inesperado - ele foi discutido em detalhes no documento de design . Nesse ponto, faz sentido usar isso por algum tempo e ver aonde nos leva.

Como contra-exemplo, existem APIs públicas que usam literais de tipo apenas porque a API não deseja restringir um cliente a um tipo específico (consulte https://golang.org/pkg/go/types/#Info por exemplo ) Ter esse literal de tipo explícito pode ser uma documentação útil. Mas, ao mesmo tempo, pode ser muito chato ter que repetir o mesmo tipo literal em todos os lugares; e de fato ser um impedimento à legibilidade. Ser capaz de falar convenientemente sobre IntSet vez de map[int]struct{} sem estar preso àquela e apenas IntSet definição é um ponto positivo em minha mente. É aí que type IntSet = map[int]struct{} está exatamente certo.

Por fim, gostaria de voltar a https://github.com/golang/go/issues/18130#issuecomment -268411811 caso você tenha perdido. Declarações de tipo irrestrito usando = são realmente a declaração de tipo "elementar", e estou feliz que finalmente as tenhamos em Go.

Talvez type intSet = map[int]struct{} (não exportado) seja a melhor maneira de usar aliases de tipo sem nome, mas isso soa como o domínio de CodeReviewComments e práticas de programação recomendadas, em vez de limitar o recurso.

Dito isso, %T é uma ferramenta útil para ver os tipos ao depurar ou explorar o sistema de tipos. Eu me pergunto se deveria haver um verbo de formato semelhante que inclua o alias? q.B = []int no exemplo de @davecheney .

@nathany Como você implementa esse verbo? As informações do alias não estão presentes no tempo de execução. (No que diz respeito ao pacote reflect , o alias é _o mesmo tipo_ da coisa para a qual é alias.)

@bcmills eu pensei que poderia ser o caso ... 😞

Imagino que ferramentas de análise estática e plug-ins de editor ainda existam para ajudar a trabalhar com aliases, então tudo bem.

Em 2 de fevereiro de 2017, às 17:01, "Nathan Youngman" [email protected] escreveu:

Dito isso,% T é uma ferramenta útil para ver os tipos ao depurar ou explorar o
sistema de tipo. Eu me pergunto se deveria haver um verbo de formato semelhante que
inclui o alias? qB = [] int em @davecheney
https://github.com/davecheney's example.

Acho que a melhor solução é adicionar um modo de consulta ao guru para responder a esta
pergunta:

que são os aliases declarados em todo o GOPATH (ou um determinado pacote) para
este tipo fornecido na linha de comando?

Não estou preocupado com o abuso de aliasing de tipos sem nome, mas potenciais
aliases duplicados para o mesmo tipo sem nome.

@davecheney Eu adicionei sua sugestão à seção "Restrições" do resumo da discussão no topo. Como todas as restrições, nossa posição geral é que as restrições aumentam a complexidade (consulte as notas acima) e provavelmente precisaríamos ver evidências reais de danos generalizados para introduzir uma restrição. Ter que mudar a maneira como você ensina Go não é suficiente: qualquer mudança que fizermos no idioma exigirá que você mude a maneira como você ensina Go.

Conforme observado no documento de design e na lista de discussão, estamos trabalhando em uma terminologia melhor para tornar as explicações mais fáceis.

@minux , como @bcmills apontou, as informações de alias não existem em tempo de execução (completamente fundamental para o design). Não há como implementar um "% T que inclua o alias".

Em 2 de fevereiro de 2017, 20h33, "Russ Cox" [email protected] escreveu:

@minux https://github.com/minux , como @bcmills
https://github.com/bcmills apontou, as informações do alias não existem
em tempo de execução (completamente fundamental para o design). Não há como
implemente um "% T que inclui o alias".

Estou sugerindo um modo de consulta guru Go (https://golang.org/x/tools/cmd/guru)
para mapeamento reverso de alias, que é baseado na análise de código estático. Isto
não importa se as informações do alias estão disponíveis no tempo de execução ou não.

@minux , ah, entendo, você está respondendo por e-mail e o Github faz com que o texto citado pareça um texto que você mesmo escreveu. Eu estava respondendo ao texto que você citou de Nathan Youngman, pensando que era seu. Desculpe pela confusão.

Em relação à terminologia e ao ensino, achei o background dos tipos de marca postado por @griesemer bastante informativo. Obrigado por isso.

Ao explicar os tipos e as conversões de tipo, os esquilos bebês inicialmente pensam que estou falando sobre um apelido de tipo, provavelmente devido à familiaridade com outras linguagens.

Qualquer que seja a terminologia final, eu poderia imaginar a introdução de apelidos de tipo antes dos tipos nomeados (de marca), especialmente porque a declaração de novos tipos nomeados provavelmente virá após a introdução de byte e rune em qualquer livro ou currículo. No entanto, quero estar atento à preocupação de @davecheney em não encorajar

Para type intSet map[int]struct{} , dizemos que map[int]struct{} é o tipo _subjacente_. Como chamamos ambos os lados de type intSet = map[int]struct{} ? Alias ​​e tipo de alias?

Quanto a %T , já preciso explicar que byte e rune resultam em uint8 e int32 , então isso não é diferente.

Se qualquer coisa, eu acho que os apelidos de tipo farão byte e rune mais fáceis de explicar. IMO, o desafio será saber quando usar tipos nomeados versus aliases de tipo e, então, ser capaz de comunicar isso.

@nathany Acho que faz muito sentido introduzir os "tipos de alias" primeiro - embora eu não usasse o termo necessariamente. As declarações de "alias" recém-introduzidas são simplesmente declarações regulares que não fazem nada de especial. O identificador à esquerda e o tipo à direita são um e o mesmo, eles denotam tipos idênticos. Nem tenho certeza se precisamos dos termos alias ou tipo de alias (não chamamos um nome de constante de alias e o valor da constante de constante de alias).

A declaração de tipo tradicional (sem alias) faz mais trabalho: primeiro cria um novo tipo do tipo à direita antes de vincular o identificador à esquerda a ele. Assim, o identificador e o tipo à direita não são os mesmos (eles compartilham apenas o mesmo tipo subjacente). Este é claramente o conceito mais complicado.

Precisamos de um novo termo para esses tipos recém-criados porque qualquer tipo agora pode ter um nome. E precisamos ser capazes de nos referir a eles, uma vez que existem regras de especificação referentes a eles (identidade de tipo, atribuibilidade, tipos de base de receptor).

Aqui está outra maneira de descrever isso, que pode ser útil em um ambiente de ensino: Um tipo pode ser colorido ou descolorido. Todos os tipos pré-declarados e todos os literais de tipo não têm cor. A única maneira de criar um novo tipo colorido é por meio de uma declaração de tipo tradicional (sem alias) que primeiro pinta (uma cópia) o tipo à direita com uma cor nova e nunca antes usada (removendo a cor antiga, se houver, inteiramente no processo) antes de vincular o identificador à esquerda a ele. Novamente, o identificador e o tipo colorido (criado implícita e invisivelmente) são idênticos, mas são diferentes do tipo (colorido ou sem cor diferente) escrito à direita.

Usando essa analogia, podemos reformular várias outras regras existentes também:

  • Um tipo colorido é sempre diferente de qualquer outro tipo (porque cada declaração de tipo usa uma cor nova, nunca antes usada).
  • Os métodos só podem ser associados a tipos de base de receptor que são coloridos.
  • O tipo subjacente de um tipo é aquele tipo despojado de todas as suas cores.
    etc.

não chamamos um nome de constante de alias, e o valor da constante de constante de alias

bom ponto 👍

Não tenho certeza se a analogia colorido vs. incolor é mais fácil de entender, mas demonstra que há mais de uma maneira de explicar os conceitos.

Tipos tradicionais com nome / marca / colorido certamente requerem mais explicações. Especialmente quando um tipo nomeado pode ser declarado usando um tipo nomeado existente. Existem diferenças bastante sutis para se manter em mente.

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

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

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

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

Porém, não é intransponível. Supondo que isso aconteça no Go 1.9, suspeito que veremos a segunda edição de vários livros de Go. 😉

Eu regularmente me refiro à especificação Go para a terminologia aceita, então estou muito curioso para saber quais termos são escolhidos no final.

Precisamos de um novo termo para esses tipos recém-criados porque qualquer tipo agora pode ter um nome.

Algumas ideias:

  • "distinto" ou "distinto" (como em, pode ser distinguido de outros tipos)
  • "único" (como em, é um tipo diferente de todos os outros tipos)
  • "concreto" (como em, é uma entidade que existe no tempo de execução)
  • "identificável" (como em, o tipo tem uma identidade)

@bcmills Temos pensado em tipos distintos, únicos, distintos, com marca, coloridos, definidos, sem apelido, etc. "Concreto" é enganoso porque uma interface também pode ser colorida, e uma interface é a encarnação de um tipo abstrato. "Identificável" também parece enganoso porque uma "struct {int}" é tão identificável quanto qualquer tipo nomeado explicitamente (sem alias).

Eu recomendaria contra:

  • "de cor" (em contextos de não programação, a frase "tipos de cor" carrega fortes conotações de preconceito racial)
  • "sem alias" (é confuso, pois o destino do alias pode ou não ser o que antes era chamado de "tipo nomeado")
  • "definido" (aliases também são definidos, eles são apenas definidos como aliases)

"com marca" poderia funcionar: carrega uma conotação de "tipos como gado", mas isso não me parece intrinsecamente ruim.

Únicas e distintas parecem ser as opções que se destacam até agora.

Eles são simples e compreensíveis sem muito contexto ou conhecimento adicional. Se eu não conhecesse a distinção, acho que pelo menos teria uma noção geral do que elas implicam. Não posso dizer isso sobre as outras opções.

Depois de aprender o termo, isso não importa, mas um nome conotativo evita barreiras desnecessárias para internalizar a distinção.

Esta é a definição de um argumento de bicicleta. Robert tem um CL pendente em https://go-review.googlesource.com/#/c/36213/ que parece perfeitamente normal.

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

Quero trazer à tona a questão de go fix novamente.

Para deixar claro que não estou sugerindo 'remover' o alias. Talvez seja algo útil e adequado para outros empregos, isso é outra história.

É algo muito importante, IMO, que o título se refira a tipos móveis. Não desejo confundir a questão. Nosso objetivo é lidar com uma espécie de mudança de interface em um projeto. Quando chegamos a uma mudança na interface, não é verdade que esperamos que todos os usuários usem essas duas interfaces (antiga e nova) como a mesma, eventualmente , e é por isso que dizemos 'reparo gradual de código'. Esperamos que os usuários removam / alterem o uso do antigo.

Ainda considero a ferramenta o melhor método para reparar o código, algo parecido com a ideia que o @ tux21b sugeriu. Por exemplo:

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

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

A única razão de @rsc dizer não aqui é que as mudanças afetarão outras ferramentas. Mas acho que não é verdade neste fluxo de trabalho : se houver um pacote desatualizado (por exemplo, uma dependência) usa o nome / caminho obsoleto do pacote, por exemplo, x/net/context , podemos corrigir o código primeiro , assim como o documento diz como migrar o código para a nova versão, mas não codificando, por meio de uma tabela configurável em formato de texto. Então você pode usar qualquer ferramenta sempre que quiser, assim como Go da nova versão. Existe um efeito colateral: ele modificará o código.

@LionNatsu , acho que você está certo, mas acho que é um problema separado: devemos adotar convenções para pacotes para explicar a clientes em potencial como atualizar seu código em resposta a alterações de API de forma mecânica? Talvez, mas teríamos que descobrir quais são essas convenções. Você pode abrir uma edição separada para este tópico, apontando para esta conversa? Obrigado.

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

Com esta proposta na ponta, agora posso criar este pacote:

package safe

import "unsafe"

type Pointer = unsafe.Pointer

que permite que os programas criem valores unsafe.Pointer sem importar unsafe diretamente:

package main

import "safe"

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

O documento de design das declarações de alias originais indica isso como explicitamente suportado. Não está explícito nesta proposta de alias de tipo mais recente, mas funciona.

Na questão da declaração de alias, o motivo para isso é: _ "O motivo pelo qual permitimos aliasing para unsafe.Pointer é que já é possível definir um tipo que tem inseguro.Pointer como tipo subjacente." _ Https://github.com/ golang / go / issues / 16339 # issuecomment -232435361

Embora seja verdade, acho que permitir um alias de unsafe.Pointer introduz algo novo: os programas agora podem criar unsafe.Pointer valores sem importar explicitamente não seguro.

Para escrever o programa acima antes desta proposta, eu teria que mover o safe.Pointer lançado em um pacote que importa não seguro. Isso pode tornar um pouco mais difícil auditar programas quanto ao uso de programas não seguros.

@crawshaw , você não poderia ter feito isso antes?

package safe

import (
  "reflect"
  "unsafe"
)

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

Acredito que permitiria exatamente o mesmo programa compilar, com a mesma falta de importação do pacote main .

(Não seria necessariamente um programa válido: a conversão uintptr -to- Pointer inclui uma chamada de função, portanto não atende à restrição de pacote unsafe que " ambas as conversões devem aparecer na mesma expressão, com apenas a aritmética intermediária entre elas ". No entanto, suspeito que seria possível construir um programa válido equivalente sem importar unsafe de main fazendo uso de coisas como reflect.SliceHeader .)

Parece que exportar um tipo não seguro oculto é apenas outra regra a ser adicionada à auditoria.

Sim, eu queria salientar que o aliasing direto inseguro. O ponteiro torna o código mais difícil de auditar, o suficiente para que eu espero que ninguém acabe fazendo isso.

@crawshaw Por meu comentário, isso também era verdade antes de termos o alias de tipo. O seguinte é válido:

package a

import "unsafe"

type P unsafe.Pointer
package main

import "./a"
import "fmt"

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

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

Ou seja, no pacote principal, posso fazer aritmética insegura usando a.P , embora não haja um pacote unsafe e a.P não seja um apelido. Isso sempre foi possível.

Você está se referindo a mais alguma coisa?

Meu erro. Achei que não funcionou. (Tive a impressão de que as regras especiais se aplicavam a inseguros. O ponteiro não se propagaria para novos tipos definidos a partir dele.)

A especificação não é clara sobre isso. Olhando para a implementação de go / types, descobri que minha implementação inicial exigia unsafe.Pointer exatamente, não apenas algum tipo que por acaso tinha um tipo subjacente de unsafe.Pointer . Acabei de encontrar # 6326 que foi quando alterei go / types para ser compatível com gc.

Talvez devêssemos proibir isso para definições de tipo regulares e também proibir apelidos de unsafe.Pointer . Não vejo nenhuma boa razão para permitir isso e isso compromete a explicitação de ter que importar unsafe para código não seguro.

Isso aconteceu. Acho que não sobrou nada aqui.

Esta página foi útil?
0 / 5 - 0 avaliações