Go: proposta: spec: facilidades de programação genéricas

Criado em 14 abr. 2016  ·  816Comentários  ·  Fonte: golang/go

Esta edição propõe que o Go suporte alguma forma de programação genérica.
Tem o rótulo Go2, já que para Go1.x a linguagem está mais ou menos pronta.

Acompanhando esta edição está uma proposta genérica geral de @ianlancetaylor que inclui quatro propostas falhas específicas de mecanismos genéricos de programação para Go.

A intenção não é adicionar genéricos ao Go neste momento, mas sim mostrar às pessoas como seria uma proposta completa. Esperamos que isso seja útil para qualquer pessoa que proponha mudanças de idioma semelhantes no futuro.

Go2 LanguageChange NeedsInvestigation Proposal generics

Comentários muito úteis

Deixe-me lembrar preventivamente a todos da nossa política https://golang.org/wiki/NoMeToo . A festa emoji está acima.

Todos 816 comentários

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

Deixe-me lembrar preventivamente a todos da nossa política https://golang.org/wiki/NoMeToo . A festa emoji está acima.

Há o Resumo das Discussões Genéricas do Go , que tenta fornecer uma visão geral das discussões de diferentes lugares. Ele também fornece alguns exemplos de como resolver problemas, onde você gostaria de usar genéricos.

Existem dois "requisitos" na proposta vinculada que podem complicar a implementação e reduzir a segurança do tipo:

  • Defina tipos genéricos com base em tipos que não são conhecidos até que sejam instanciados.
  • Não exija um relacionamento explícito entre a definição de um tipo ou função genérica e seu uso. Ou seja, os programas não deveriam ter que dizer explicitamente que o tipo T implementa G genérico.

Esses requisitos parecem excluir, por exemplo, um sistema semelhante ao sistema de traços de Rust, onde tipos genéricos são restringidos por limites de traços. Por que eles são necessários?

Torna-se tentador construir genéricos na biblioteca padrão em um nível muito baixo, como em C++ std::basic_string, std::alocador>. Isso tem seus benefícios - caso contrário ninguém faria isso - mas tem efeitos abrangentes e às vezes surpreendentes, como em mensagens de erro C++ incompreensíveis.

O problema em C++ surge do código gerado pela verificação de tipo. É necessário haver uma verificação de tipo adicional antes da geração do código. A proposta de conceitos de C++ possibilita isso ao permitir que o autor do código genérico especifique os requisitos de um tipo genérico. Dessa forma, a compilação pode falhar na verificação de tipo antes da geração de código e mensagens de erro simples podem ser impressas. O problema com os genéricos de C++ (sem conceitos) é que o código genérico _é_ a especificação do tipo genérico. É isso que cria as mensagens de erro incompreensíveis.

O código genérico não deve ser a especificação de um tipo genérico.

@tamird É uma característica essencial dos tipos de interface do Go que você pode definir um tipo de interface não T e depois definir um tipo de interface I tal que T implemente I. Veja https://golang.org/doc/faq#implements_interface . Seria inconsistente se Go implementasse uma forma de genéricos para os quais um tipo genérico G só pudesse ser usado com um tipo T que dissesse explicitamente "Eu posso ser usado para implementar G".

Não estou familiarizado com Rust, mas não conheço nenhuma linguagem que exija que T declare explicitamente que pode ser usado para implementar G. Os dois requisitos que você mencionou não significam que G não possa impor requisitos a T, apenas como I impõe requisitos em T. Os requisitos apenas significam que G e T podem ser escritos independentemente. Esse é um recurso altamente desejável para genéricos, e não consigo imaginar abandoná-lo.

@ianlancetaylor https://doc.rust-lang.org/book/traits.html explica os traços de Rust. Embora eu ache que eles são um bom modelo em geral, eles seriam um mau ajuste para o Go como existe hoje.

@sbunce Eu também pensei que os conceitos fossem a resposta, e você pode ver a ideia espalhada pelas várias propostas antes da última. Mas é desencorajador que os conceitos tenham sido originalmente planejados para o que se tornou o C++11, e agora estamos em 2016, e ainda são controversos e não estão particularmente próximos de serem incluídos na linguagem C++.

Haveria valor na literatura acadêmica para qualquer orientação sobre a avaliação de abordagens?

O único artigo que li sobre o assunto é Os desenvolvedores se beneficiam de tipos genéricos? (paywall desculpe, você pode pesquisar no Google seu caminho para um download de pdf) que tinha o seguinte a dizer

Consequentemente, uma interpretação conservadora do experimento
é que os tipos genéricos podem ser considerados como uma compensação
entre as características positivas da documentação e a
características de extensibilidade negativa. A parte emocionante de
o estudo é que mostrou uma situação em que o uso de um
sistema de tipo estático (mais forte) teve um impacto negativo na
tempo de desenvolvimento e, ao mesmo tempo, o benefício esperado
fit – a redução do tempo de correção do erro de tipo – não apareceu.
Achamos que tais tarefas podem ajudar em experimentos futuros em
identificar o impacto dos sistemas de tipo.

Eu também vejo https://github.com/golang/go/issues/15295 também faz referência a genéricos orientados a objetos leves e flexíveis .

Se fôssemos nos apoiar na academia para orientar a decisão, acho que seria melhor fazer uma revisão de literatura inicial e provavelmente decidir cedo se pesaríamos os estudos empíricos de maneira diferente daqueles que se baseiam em provas.

Por favor, veja: http://dl.acm.org/citation.cfm?id=2738008 por Barbara Liskov:

O suporte para programação genérica em linguagens de programação orientadas a objetos modernas é estranho e carece de poder expressivo desejável. Introduzimos um mecanismo de generalidade expressivo que adiciona poder expressivo e fortalece a verificação estática, mantendo-se leve e simples em casos de uso comuns. Como classes e conceitos de tipo, o mecanismo permite que tipos existentes modelem restrições de tipo retroativamente. Para poder expressivo, expomos modelos como construções nomeadas que podem ser definidas e selecionadas explicitamente para testemunhar restrições; em usos comuns de genericidade, no entanto, os tipos testemunham implicitamente as restrições sem esforço adicional do programador.

Eu acho que o que eles fizeram lá é muito legal - desculpe se este é o lugar incorreto para parar, mas não consegui encontrar um lugar para comentar em /propostas e não encontrei um problema apropriado aqui.

Pode ser interessante ter um ou mais transpiladores experimentais - um compilador de código-fonte Go genéricos para Go 1.xy.
Quero dizer - muita conversa/argumentos-para-minha-opinião, e ninguém está escrevendo código-fonte que _tenta_ implementar _algum tipo_ de genéricos para Go.

Apenas para obter conhecimento e experiência com Go e genéricos - para ver o que funciona e o que não funciona.
Se todas as soluções genéricas Go não forem realmente boas, então; Não há genéricos para Go.

A proposta também pode incluir as implicações no tamanho do binário e no consumo de memória? Eu esperaria que houvesse duplicação de código para cada tipo de valor concreto para que as otimizações do compilador funcionassem neles. Espero uma garantia de que não haverá duplicação de código para tipos de ponteiro concretos.

Eu ofereço uma matriz de decisão de Pugh. Meus critérios incluem impactos de perspicuidade (complexidade da fonte, tamanho). Eu também forcei os critérios de classificação para determinar os pesos dos critérios. O seu pode variar, é claro. Eu usei "interfaces" como a alternativa padrão e comparei isso com genéricos de "copiar/colar", genéricos baseados em modelo (eu tinha em mente algo parecido com o funcionamento da linguagem D) e algo que chamei de genéricos de estilo de instanciação de tempo de execução. Tenho certeza de que isso é uma grande simplificação. No entanto, pode gerar algumas ideias sobre como avaliar as escolhas... este deve ser um link público para minha Planilha Google, aqui

Fazer um ping em @yizhouzhang e @andrewcmyers para que eles possam expressar suas opiniões sobre gêneros como genéricos em Go. Parece que pode ser uma boa combinação :)

O design genérico que criamos para o Genus tem verificação de tipo estático e modular, não requer pré-declaração de que os tipos implementam alguma interface e vem com desempenho razoável. Eu definitivamente olharia para isso se você estiver pensando em genéricos para Go. Parece um bom ajuste do meu entendimento de Go.

Aqui está um link para o artigo que não requer acesso à Biblioteca Digital ACM:
http://www.cs.cornell.edu/andru/papers/genus/

A página inicial do Genus está aqui: http://www.cs.cornell.edu/projects/genus/

Ainda não lançamos o compilador publicamente, mas planejamos fazê-lo em breve.

Feliz em responder a quaisquer perguntas que as pessoas tenham.

Em termos da matriz de decisão do @mandolyte , Genus pontua 17, empatado em #1. Eu acrescentaria mais alguns critérios para pontuar, no entanto. Por exemplo, a verificação de tipo modular é importante, como outros, como @sbunce , observados acima, mas os esquemas baseados em modelo não têm. O relatório técnico do documento Genus tem uma tabela muito maior na página 34, comparando vários designs genéricos.

Acabei de ler todo o documento Summary of Go Generics , que foi um resumo útil das discussões anteriores. O mecanismo de genéricos em Genus, a meu ver, não sofre dos problemas identificados para C++, Java ou C#. Os genéricos de gênero são reificados, diferentemente do Java, para que você possa descobrir os tipos em tempo de execução. Você também pode instanciar em tipos primitivos, e você não obtém boxing implícito nos lugares que você realmente não quer: arrays de T onde T é um primitivo. O sistema de tipos é o mais próximo de Haskell e Rust -- na verdade um pouco mais poderoso, mas acho que também é intuitivo. A especialização primitiva ala C# não é suportada atualmente no gênero, mas poderia ser. Na maioria dos casos, a especialização pode ser determinada no tempo do link, portanto, a geração de código em tempo de execução verdadeiro não seria necessária.

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

Uma maneira de restringir tipos genéricos que não requer a adição de novos conceitos de linguagem: https://docs.google.com/document/d/1rX4huWffJ0y1ZjqEpPrDy-kk-m9zWfatgCluGRBQveQ/edit?usp=sharing.

Genus parece muito legal e é claramente um avanço importante da arte, mas não vejo como isso se aplicaria ao Go. Alguém tem um esboço de como ele se integraria ao sistema/filosofia do tipo Go?

A questão é que a equipe de go está tentando impedir. O título afirma claramente as intenções da equipe go. E se isso não bastasse para deter todos os compradores, as características exigidas de um domínio tão amplo nas propostas de ian deixam claro que se você quer genéricos, eles não querem você. É estúpido até mesmo tentar dialogar com a equipe de go. Para quem procura genéricos em go, digo fraturar a linguagem. Comece uma nova jornada - muitos se seguirão. Eu já vi ótimos trabalhos feitos em forks. Organizem-se, juntem-se a uma causa

Se alguém quiser tentar desenvolver uma extensão genérica para Go com base no design Genus, ficaremos felizes em ajudar. Não conhecemos o Go o suficiente para produzir um design que se harmonize com a linguagem existente. Acho que o primeiro passo seria uma proposta de design do espantalho com exemplos elaborados.

@andrewcmyers esperando que @ianlancetaylor trabalhe com você nisso. Apenas ter alguns exemplos para olhar ajudaria muito.

Eu li o jornal Genus. Até onde eu entendo, parece bom para Java, mas não parece ser um ajuste natural para Go.

Um aspecto chave do Go é que quando você escreve um programa Go, a maior parte do que você escreve é ​​código. Isso é diferente de C++ e Java, onde muito mais do que você escreve são tipos. Gênero parece ser principalmente sobre tipos: você escreve restrições e modelos, em vez de código. O sistema de tipos de Go é muito simples. O sistema de tipos de Genus é muito mais complexo.

As idéias de modelagem retroativa, embora claramente úteis para Java, não parecem se encaixar em Go. As pessoas já usam tipos de adaptadores para combinar tipos existentes com interfaces; nada mais deve ser necessário ao usar genéricos.

Seria interessante ver essas ideias aplicadas ao Go, mas não estou otimista quanto ao resultado.

Eu não sou um especialista em Go, mas seu sistema de tipos não parece mais simples do que o Java pré-genérico. A sintaxe de tipo é um pouco mais leve de uma maneira agradável, mas a complexidade subjacente parece a mesma.

No Genus, as restrições são tipos, mas os modelos são códigos. Os modelos são adaptadores, mas se adaptam sem adicionar uma camada de embalagem real. Isso é muito útil quando você deseja, digamos, adaptar uma matriz inteira de objetos a uma nova interface. A modelagem retroativa permite tratar a matriz como uma matriz de objetos que satisfaz a interface desejada.

Eu não ficaria surpreso se fosse mais complicado do que Java (pré-genéricos) em um sentido teórico de tipos, embora seja mais simples de usar na prática.

Complexidade relativa à parte, eles são diferentes o suficiente para que Genus não consiga mapear 1:1. Nenhuma subtipagem parece ser grande.

Se você está interessado:

O resumo mais breve das diferenças filosóficas/de design relevantes que mencionei está contido nas seguintes entradas de perguntas frequentes:

Ao contrário da maioria das linguagens, a especificação Go é muito curta e clara sobre as propriedades relevantes do sistema de tipos, começando em https://golang.org/ref/spec#Constants e indo direto até a seção intitulada "Blocks" (todos os quais é inferior a 11 páginas impressas).

Ao contrário dos genéricos Java e C#, o mecanismo Genus genéricos não é baseado em subtipagem. Por outro lado, parece-me que Go tem subtipagem, mas subtipagem estrutural. Essa também é uma boa combinação para a abordagem Genus, que tem um sabor estrutural em vez de depender de relacionamentos pré-declarados.

Não acredito que Go tenha subtipagem estrutural.

Enquanto dois tipos cujo tipo subjacente é idêntico são, portanto, idênticos
podem ser substituídos um pelo outro, https://play.golang.org/p/cT15aQ-PFr

Isso não se estende a dois tipos que compartilham um subconjunto comum de campos,
https://play.golang.org/p/KrC9_BDXuh.

Na quinta-feira, 28 de abril de 2016 às 13h09, Andrew Myers [email protected]
escreveu:

Ao contrário dos genéricos Java e C#, o mecanismo Genus genéricos não é baseado em
subtipagem. Por outro lado, parece-me que Go tem subtipagem,
mas subtipagem estrutural. Essa também é uma boa combinação para a abordagem Genus,
que tem um sabor estrutural em vez de depender de pré-declarados
relacionamentos.


Você está recebendo isso porque está inscrito neste tópico.
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/golang/go/issues/15292#issuecomment -215298127

Obrigado, eu estava interpretando mal parte da linguagem sobre quando os tipos implementam interfaces. Na verdade, parece-me que as interfaces Go, com uma extensão modesta, poderiam ser usadas como restrições de estilo Genus.

É exatamente por isso que eu dei um ping em você, o gênero parece uma abordagem muito melhor do que os genéricos do Java/C #.

Houve algumas ideias com relação à especialização nos tipos de interface; por exemplo, a abordagem _package templates_ "propostas" 1 2 são exemplos disso.

tl;dr; o pacote genérico com especialização de interface ficaria assim:

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

Versão 1. com especialização no escopo do pacote:

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

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

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

Versão 2. a especialização no escopo da declaração:

package main
import set

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

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

Os genéricos no escopo do pacote impedirão que as pessoas abusem significativamente do sistema de genéricos, uma vez que o uso é limitado a algoritmos básicos e estruturas de dados. Basicamente, impede a construção de novas abstrações de linguagem e código funcional.

A especialização com escopo de declaração tem mais possibilidades ao custo, tornando-a mais propensa a abusos e é mais detalhada. Mas, código funcional seria possível, por exemplo:

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

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

A abordagem de especialização de interface tem propriedades interessantes:

  • Pacotes já existentes usando interfaces seriam especializados. por exemplo, eu seria capaz de chamar sort.Sort[[Interface:MyItems]](...) e fazer a classificação funcionar no tipo concreto em vez da interface (com ganhos potenciais de inlining).
  • O teste é simplificado, só tenho que garantir que o código genérico funcione com interfaces.
  • É fácil dizer como funciona. ou seja, imagine que [[E: int]] substitui todas as declarações de E por int .

Mas, há problemas de detalhamento ao trabalhar em pacotes:

type op
import "set"

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

_Claro, a coisa toda é mais simples de declarar do que de implementar. Internamente, provavelmente existem muitos problemas e maneiras de como isso pode funcionar._

_PS, para os resmungos sobre o progresso lento dos genéricos, eu aplaudo o Go Team por gastar mais tempo em questões que têm um benefício maior para a comunidade, por exemplo, bugs do compilador/tempo de execução, SSA, GC, http2._

@egonelbre seu ponto de que os genéricos no nível do pacote impedirão o "abuso" é realmente importante que acho que a maioria das pessoas ignora. Isso, além de sua relativa simplicidade semântica e sintática (somente as construções package e import são afetadas) os tornam muito atraentes para o Go.

@andrewcymyers interessante que você acha que as interfaces Go funcionam como restrições no estilo Genus. Eu teria pensado que eles ainda têm o problema de que você não pode expressar restrições de parâmetros de vários tipos com eles.

Uma coisa que acabei de perceber, no entanto, é que em Go você pode escrever uma interface inline. Portanto, com a sintaxe correta, você pode colocar a interface no escopo de todos os parâmetros e capturar restrições de vários parâmetros:

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

Acho que o maior problema com interfaces como restrições é que os métodos não são tão difundidos em Go quanto em Java. Tipos internos não possuem métodos. Não existe um conjunto de métodos universais como aqueles em java.lang.Object. Os usuários normalmente não definem métodos como Equals ou HashCode em seus tipos, a menos que precisem especificamente, porque esses métodos não qualificam um tipo para uso como chaves de mapa ou em qualquer algoritmo que precise de igualdade.

(Igualdade em Go é uma história interessante. A linguagem fornece seu tipo "==" se atender a determinados requisitos (consulte https://golang.org/ref/spec#Logical_operators, procure por "comparable"). Qualquer tipo com " ==" pode servir como uma chave de mapa. Mas se seu tipo não merecer "==", então não há nada que você possa escrever que faça funcionar como uma chave de mapa.)

Como os métodos não são difundidos e não há uma maneira fácil de expressar as propriedades dos tipos internos (como os operadores com os quais eles trabalham), sugeri usar o próprio código como o mecanismo de restrição genérico. Veja o link no meu comentário de 18 de abril, acima. Esta proposta tem seus problemas, mas um bom recurso é que o código numérico genérico ainda pode usar os operadores usuais, em vez de chamadas de método complicadas.

A outra maneira é adicionar métodos a tipos que não os possuem. Você pode fazer isso na linguagem existente de uma maneira muito mais leve do que em Java:

digite Int int
func (i Int) Less(j Int) bool { return i < j }

O tipo Int "herda" todos os operadores e outras propriedades de int. Embora você tenha que lançar entre os dois para usar Int e int juntos, o que pode ser uma dor.

Modelos de gênero podem ajudar aqui. Mas eles teriam que ser mantidos muito simples. Acho que @ianlancetaylor foi muito estreito em sua caracterização de Go como escrevendo mais código, menos tipos. O princípio geral é que Go abomina a complexidade. Nós olhamos para Java e C++ e estamos determinados a nunca ir lá. (Sem ofensa.)

Portanto, uma ideia rápida para um recurso semelhante a um modelo seria: fazer com que o usuário escreva tipos como Int acima e, em instanciações genéricas, permita "int com Int", o que significa usar o tipo int, mas tratá-lo como Int. Então não há nenhuma construção de linguagem aberta chamada modelo, com sua palavra-chave, semântica de herança e assim por diante. Não entendo os modelos bem o suficiente para saber se isso é viável, mas está mais no espírito do Go.

@jba Certamente concordamos com o princípio de evitar complexidade. "O mais simples possível, mas não mais simples." Eu provavelmente deixaria alguns recursos do Genus fora do Go por esses motivos, pelo menos no começo.

Uma das coisas boas sobre a abordagem Genus é que ela lida com tipos internos sem problemas. Lembre-se de que tipos primitivos em Java não possuem métodos, e Genus herda esse comportamento. Em vez disso, Genus trata tipos primitivos _como se_ eles tivessem um conjunto bastante grande de métodos com o propósito de satisfazer restrições. Uma tabela de hash requer que suas chaves possam ser criptografadas e comparadas, mas todos os tipos primitivos satisfazem essa restrição. Portanto, instanciações de tipo como Map[int, boolean] são perfeitamente legais sem maiores problemas. Não há necessidade de distinguir entre dois tipos de inteiros (int vs Int) para conseguir isso. No entanto, se o int não estivesse equipado com operações suficientes para alguns usos, usaríamos um modelo quase exatamente como o uso do Int acima.

Outra coisa que vale a pena mencionar é a ideia de "modelos naturais" em Genus. Normalmente, você não precisa declarar um modelo para usar um tipo genérico: se o argumento de tipo satisfizer a restrição, um modelo natural será gerado automaticamente. Nossa experiência é que este é o caso usual; declarar modelos nomeados explícitos normalmente não é necessário. Mas se um modelo for necessário - por exemplo, se você quiser hash ints de uma maneira não padrão - a sintaxe será semelhante à que você sugeriu: Map[int with fancyHash, boolean] . Eu diria que Genus é sintaticamente leve em casos de uso normal, mas com energia de reserva quando necessário.

@egonelbre O que você está propondo aqui se parece com tipos virtuais, que são suportados pelo Scala. Há um artigo do ECOOP'97 de Kresten Krab Thorup, "Genericidade em Java com tipos virtuais", que explora essa direção. Também desenvolvemos mecanismos para tipos virtuais e classes virtuais em nosso trabalho ("J&: nested interseção para composição de software escalável", OOPSLA'06).

Como as inicializações literais são difundidas em Go, tive que me perguntar como seria uma função literal. Eu suspeito que o código para lidar com isso existe em grande parte em Go gerar, corrigir e renomear. Talvez inspire alguém :-)

// a definição do tipo de função (genérica)
digite Sum64 func (X, Y) float64 {
return float64(X) + float64(Y)
}

// instancia um, posicionalmente
eu := 42
var j uint = 86
soma := &Soma64{i, j}

// instancia um, por tipos de parâmetros nomeados
soma := &Soma64{ X: int, Y: uint}

// agora use...
resultado := soma(i, j) // resultado é 128

A proposta de Ian exige demais. Não podemos desenvolver todos os recursos de uma só vez, ele existirá em um estado inacabado por muitos meses.

Enquanto isso, o projeto inacabado não pode ser chamado de idioma oficial do Go até que seja concluído, porque isso corre o risco de fragmentar o ecossistema.

Então a questão é como planejar isso.

Também uma grande parte do projeto seria desenvolver o corpus de referência.
desenvolver as coleções genéricas reais, algoritmos e outras coisas de tal forma que todos concordamos que eles são idiomáticos, enquanto usamos os novos recursos do go 2.0

Uma sintaxe possível?

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

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

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

@md2perpe , a sintaxe não é a parte difícil deste problema. Na verdade, é de longe o mais fácil. Por favor, veja a discussão e os documentos vinculados acima.

@md2perpe Discutimos a parametrização de pacotes inteiros ("módulos") como uma maneira de generalidade interna - parece ser uma maneira de reduzir a sobrecarga sintática. Mas tem outros problemas; por exemplo, não está claro como parametrizá-lo com tipos que não são de nível de pacote. Mas a ideia ainda pode valer a pena explorar em detalhes.

Eu gostaria de compartilhar uma perspectiva: em um universo paralelo, todas as assinaturas de função Go sempre foram restritas a mencionar apenas tipos de interface e, em vez de demanda por genéricos hoje, há uma maneira de evitar a indireção associada aos valores de interface. Pense em como você resolveria esse problema (sem alterar o idioma). Eu tenho algumas ideias.

@thwd Assim, o autor da biblioteca continuaria usando interfaces, mas sem a troca de tipo e as asserções de tipo necessárias hoje. E o usuário da biblioteca simplesmente passaria tipos concretos como se a biblioteca fosse usar os tipos como estão... e então o compilador reconciliaria os dois? E se não pudesse dizer por quê? (como o operador módulo foi usado na biblioteca, mas o usuário forneceu uma fatia de algo.

Estou perto? :-)

@mandolyte sim! vamos trocar e-mails para não poluir este tópico. Você pode me alcançar em "me at thwd dot me". Qualquer outra pessoa lendo isso que possa estar interessada; me mande um e-mail e eu te adiciono ao tópico.

É um ótimo recurso para type system e collection library .
Uma sintaxe potencial:

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

Por interface

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

super type ou type implement :

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

O acima também conhecido em java:

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

@leaxoy como dito antes, a sintaxe não é a parte difícil aqui. Veja discussão acima.

Apenas esteja ciente de que o custo da interface é incrivelmente grande.

Por favor, explique por que você acha que o custo da interface é "inacreditavelmente"
ampla.
Não deve ser pior do que as chamadas virtuais não especializadas do C++.

@minux não posso falar sobre os custos de desempenho, mas em relação à qualidade do código. interface{} não pode ser verificado em tempo de compilação, mas os genéricos podem. Na minha opinião, isso é, na maioria dos casos, mais importante do que os problemas de desempenho de usar interface{} .

@xoviat

Não há realmente nenhuma desvantagem nisso porque o processamento necessário para isso não diminui a velocidade do compilador.

Existem (pelo menos) duas desvantagens.

Um é o aumento do trabalho para o vinculador: se as especializações para dois tipos resultarem no mesmo código de máquina subjacente, não queremos compilar e vincular duas cópias desse código.

Outra é que os pacotes parametrizados são menos expressivos que os métodos parametrizados. (Veja as propostas vinculadas no primeiro comentário para detalhes.)

O tipo hiper é uma boa ideia?

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

O tipo hiper é uma boa ideia?

O que você está descrevendo aqui é apenas parametrização de tipo ala C++ (ou seja, templates). Ele não verifica o tipo de maneira modular porque não há como saber se o tipo aType tem uma operação + a partir das informações fornecidas. Parametrização de tipo restrito como em CLU, Haskell, Java, Genus é a solução.

@golang101 Tenho uma proposta detalhada nesse sentido. Enviarei um CL para adicioná-lo à lista, mas dificilmente será adotado.

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

@andrewcmyers

Ele não verifica o tipo de maneira modular porque não há como saber se o tipo aType tem uma operação + a partir das informações fornecidas.

Claro que existe. Essa restrição está implícita na definição da função e as restrições dessa forma podem ser propagadas para todos os chamadores (transitivos) de tempo de compilação de getAddFunc .

A restrição não faz parte de um Go _type_ — ou seja, não pode ser codificada no sistema de tipos da parte de tempo de execução da linguagem — mas isso não significa que não possa ser avaliada de forma modular.

Adicionada minha proposta como 2016-09-compile-time-functions.md .

Não espero que seja adotado, mas pode pelo menos servir como um ponto de referência interessante.

@bcmills Eu sinto que as funções de tempo de compilação são uma ideia poderosa, além de qualquer consideração de genéricos. Por exemplo, eu escrevi um solucionador de sudoku que precisa de um popcount. Para acelerar isso, pré-calculei os popcounts para os vários valores possíveis e armazenei-os como Go source . Isso é algo que se pode fazer com go:generate . Mas se houvesse uma função de tempo de compilação, essa tabela de pesquisa também poderia ser calculada em tempo de compilação, evitando que o código gerado pela máquina tivesse que ser confirmado no repositório. Em geral, qualquer tipo de função matemática memoizável é um bom ajuste para tabelas de pesquisa pré-fabricadas com funções de tempo de compilação.

Mais especulativamente, pode-se também querer, por exemplo, baixar uma definição de protobuf de uma fonte canônica e usá-la para construir tipos em tempo de compilação. Mas talvez isso seja demais para ser permitido em tempo de compilação?

Eu sinto que as funções de tempo de compilação são muito poderosas e muito fracas ao mesmo tempo: elas são muito flexíveis e podem cometer erros de maneiras estranhas / retardar a compilação da maneira como os modelos C++ fazem, mas por outro lado são muito estáticos e difíceis de adaptar a coisas como funções de primeira classe.

Para a segunda parte, não vejo uma maneira de fazer algo como uma "fatia de funções que processam fatias de um tipo específico e retornam um elemento", ou em uma sintaxe ad-hoc []func<T>([]T) T , que é muito fácil de fazer essencialmente em todas as linguagens funcionais estaticamente tipadas. O que é realmente necessário é que os valores sejam capazes de assumir tipos paramétricos, não alguma geração de código em nível de código-fonte.

@bunsim

Para a segunda parte, não vejo uma maneira de fazer algo como uma "fatia de funções que processam fatias de um tipo específico e retornam um elemento",

Se você está falando de um único parâmetro de tipo, na minha proposta isso seria escrito:

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

Se você está falando sobre misturar parâmetros de tipo e parâmetros de valor, não, minha proposta não permite isso: parte do objetivo das funções de tempo de compilação é poder operar em valores unboxed, e o tipo de parametricidade de tempo de execução Eu acho que você está descrevendo praticamente requer boxe de valores.

Sim, mas na minha opinião esse tipo de coisa que requer boxing deve ser permitido mantendo a segurança de tipo, talvez com uma sintaxe especial que indique o "boxedness". Uma grande parte da adição de "genéricos" é realmente evitar a segurança de tipo de interface{} mesmo quando a sobrecarga de interface{} não é evitável. (Talvez permita apenas certas construções de tipo paramétrico com tipos de ponteiro e interface que "já" estão em caixa? Os objetos em caixa Integer etc do Java não são completamente uma má ideia, embora fatias de tipos de valor sejam complicadas)

Eu apenas sinto que as funções de tempo de compilação são muito parecidas com C++, e seria extremamente decepcionante para pessoas como eu esperando que o Go2 tivesse um sistema de tipo paramétrico moderno baseado em uma teoria de tipo de som em vez de um hack baseado na manipulação de pedaços de código-fonte escritos em uma linguagem sem genéricos.

@bcmills
O que você propõe não será modular. Se o módulo A usa o módulo B, que usa o módulo C, que usa o módulo D, uma mudança em como um parâmetro de tipo é usado em D pode precisar se propagar de volta para A, mesmo que o implementador de A não tenha ideia de que D está no sistema. O acoplamento frouxo fornecido pelos sistemas modulares será enfraquecido e o software ficará mais frágil. Este é um dos problemas com modelos C++.

Se, por outro lado, as assinaturas de tipo capturam os requisitos nos parâmetros de tipo, como em linguagens como CLU, ML, Haskell ou Genus, um módulo pode ser compilado sem nenhum acesso aos componentes internos dos módulos dos quais depende.

@bunsim

Uma grande parte da adição de "genéricos" é realmente evitar a insegurança de tipo da interface{} mesmo quando a sobrecarga da interface{} não é evitável.

"não evitável" é relativo. Observe que a sobrecarga do boxe é o ponto 3 no post de Russ de 2009 (https://research.swtch.com/generic).

esperando que o Go2 tenha um sistema de tipo paramétrico moderno baseado em uma teoria de tipo de som em vez de um hack baseado na manipulação de pedaços de código-fonte

Uma boa "teoria do tipo de som" é descritiva, não prescritiva. Minha proposta em particular se baseia no cálculo lambda de segunda ordem (ao longo das linhas do Sistema F), onde gotype representa o tipo type e todo o sistema de tipo de primeira ordem é içado para o segundo -order ("tempo de compilação").

Também está relacionado ao trabalho de teoria do tipo modal de Davies, Pfenning, et al na CMU. Para algum conhecimento, eu começaria com Uma Análise Modal de Computação Estágio e Tipos Modais como Especificações de Preparação para Geração de Código em Tempo de Execução .

É verdade que a teoria de tipos subjacente em minha proposta é menos formalmente especificada do que na literatura acadêmica, mas isso não significa que não esteja lá.

@andrewcmyers

Se o módulo A usa o módulo B, que usa o módulo C, que usa o módulo D, uma mudança na forma como um parâmetro de tipo é usado em D pode precisar se propagar até A, mesmo que o implementador de A não tenha ideia de que D está no sistema.

Isso já é verdade em Go hoje: se você olhar com atenção, notará que os arquivos de objeto gerados pelo compilador para um determinado pacote Go incluem informações sobre as partes das dependências transitivas que afetam a API exportada.

O acoplamento frouxo fornecido pelos sistemas modulares será enfraquecido e o software ficará mais frágil.

Eu ouvi esse mesmo argumento usado para defender a exportação de tipos interface em vez de tipos concretos em APIs Go, e o inverso acaba sendo mais comum: a abstração prematura restringe os tipos e dificulta a extensão das APIs. (Para um exemplo, veja #19584.) Se você quiser confiar nessa linha de argumento, acho que precisa fornecer alguns exemplos concretos.

Este é um dos problemas com modelos C++.

A meu ver, os principais problemas com os modelos C++ são (em nenhuma ordem específica):

  • Excessiva ambiguidade sintática.
    uma. Ambiguidade entre nomes de tipo e nomes de valor.
    b. Suporte excessivamente amplo para sobrecarga do operador, levando a uma capacidade enfraquecida de inferir restrições do uso do operador.
  • Confiança excessiva na resolução de sobrecarga para metaprogramação (ou, de forma equivalente, evolução ad-hoc do suporte à metaprogramação).
    uma. Especialmente regras de recolhimento de referência wrt.
  • Aplicação excessivamente ampla do princípio SFINAE, levando a restrições muito difíceis de propagar e muitas condicionais implícitas nas definições de tipo, levando a relatórios de erros muito difíceis.
  • Uso excessivo de token-colando e inclusão textual (o pré-processador C) em vez de substituição AST e artefatos de compilação de ordem superior (que felizmente parece ser pelo menos parcialmente abordado com Módulos).
  • Falta de boas linguagens de bootstrapping para compiladores C++, levando a relatórios de erros ruins em linhagens de compiladores de longa duração (por exemplo, a cadeia de ferramentas GCC).
  • A duplicação (e às vezes a multiplicação) de nomes resultantes do mapeamento de conjuntos de operadores em "conceitos" com nomes diferentes (em vez de tratar os próprios operadores como as restrições fundamentais).

Eu tenho codificado em C++ de vez em quando há uma década e estou feliz em discutir as deficiências de C++ longamente, mas o fato de que as dependências do programa são transitivas nunca esteve nem remotamente perto do topo da minha lista de reclamações.

Por outro lado, precisando atualizar uma cadeia de dependências O(N) apenas para adicionar um único método a um tipo no módulo A e poder usá-lo no módulo D? Esse é o tipo de problema que me atrasa regularmente. Onde a parametricidade e o acoplamento fraco entram em conflito, eu escolho a parametricidade qualquer dia.

Ainda assim, acredito firmemente que metaprogramação e polimorfismo paramétrico devem ser separados, e a confusão deles em C++ é a causa raiz do motivo pelo qual os modelos C++ são irritantes. Simplificando, C++ tenta implementar uma ideia de teoria de tipos usando essencialmente macros em esteróides, o que é muito problemático, pois os programadores gostam de pensar em modelos como polimorfismo paramétrico real e são atingidos por comportamentos inesperados. As funções de tempo de compilação são uma ótima ideia para metaprogramação e substituição do hack que é go generate , mas não acredito que deva ser a maneira abençoada de fazer programação genérica.

O polimorfismo paramétrico "real" ajuda no acoplamento fraco e não deve entrar em conflito com ele. Também deve ser fortemente integrado com o resto do sistema de tipos; por exemplo, provavelmente deve ser integrado ao sistema de interface atual, para que muitos usos de tipos de interface possam ser reescritos em coisas como:

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

o que deve evitar sobrecarga de interface (como o uso de Rust), embora neste caso não seja muito útil.

Um exemplo melhor pode ser o pacote sort , onde você poderia ter algo como

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

onde Comparable é simplesmente uma boa e velha interface que os tipos podem implementar. Sort pode então ser chamado em uma fatia de tipos de valor que implementam Comparable , sem encaixá-los em tipos de interface.

@bcmills Dependências transitivas não restritas pelo sistema de tipos estão, na minha opinião, no centro de algumas de suas reclamações sobre C++. Dependências transitivas não são um grande problema se você controla os módulos A, B, C e D. Em geral, você está desenvolvendo o módulo A e pode estar apenas fracamente ciente de que o módulo D está lá embaixo e, inversamente, o desenvolvedor de D pode não ter conhecimento de A. Se o módulo D agora, sem fazer nenhuma alteração nas declarações visíveis em D, começa a usar algum novo operador em um parâmetro de tipo - ou simplesmente usa esse parâmetro de tipo como um argumento de tipo para um novo módulo E com seu próprio restrições implícitas — essas restrições serão transmitidas a todos os clientes, que podem não estar usando argumentos de tipo que satisfaçam as restrições. Nada diz ao desenvolvedor D que eles estão estragando tudo. Na verdade, você tem uma espécie de inferência de tipo global, com todas as dificuldades de depuração que isso implica.

Acredito que a abordagem que adotamos no Gênero [ PLDI'15 ] é muito melhor. Os parâmetros de tipo têm restrições explícitas, mas leves (eu entendo seu ponto sobre o suporte a restrições de operação; o CLU mostrou como fazer isso desde 1977). A verificação de tipo de gênero é totalmente modular. O código genérico pode ser compilado apenas uma vez para otimizar o espaço de código ou especializado para argumentos de tipo específicos para um bom desempenho.

@andrewcmyers

Se o módulo D agora, sem fazer nenhuma alteração nas declarações visíveis em D, começa a usar algum novo operador em um parâmetro de tipo […] [clientes] podem não estar usando argumentos de tipo que satisfaçam as restrições. Nada diz ao desenvolvedor D que eles estão estragando tudo.

Claro, mas isso já é verdade para muitas restrições implícitas em Go, independentemente de qualquer mecanismo de programação genérico.

Por exemplo, uma função pode receber um parâmetro do tipo interface e inicialmente chamar seus métodos sequencialmente. Se essa função for alterada posteriormente para chamar esses métodos simultaneamente (gerando goroutines adicionais), a restrição "deve ser segura para uso simultâneo" não será refletida no sistema de tipos.

Da mesma forma, o sistema de tipo Go hoje não especifica restrições nos tempos de vida das variáveis: algumas implementações de io.Writer supõem erroneamente que podem manter uma referência à fatia passada e lê-la mais tarde (por exemplo, fazendo a gravação real de forma assíncrona em uma goroutine em segundo plano), mas isso causará corridas de dados se o chamador de Write tentar reutilizar a mesma fatia de apoio para um Write subsequente.

Ou uma função usando um switch de tipo pode seguir um caminho diferente de um método adicionado a um dos tipos no switch.

Ou uma função que verifica um código de erro específico pode falhar se a função que gera o erro alterar a maneira como relata essa condição. (Por exemplo, consulte https://github.com/golang/go/issues/19647.)

Ou uma função que verifica um tipo de erro específico pode quebrar se os wrappers em torno do erro forem adicionados ou removidos (como aconteceu no pacote net padrão no Go 1.5).

Ou o buffer em um canal exposto em uma API pode mudar, introduzindo deadlocks e/ou corridas.

...e assim por diante.

Go não é incomum nesse sentido: restrições implícitas são onipresentes em programas do mundo real.


Se você tentar capturar todas as restrições relevantes em anotações explícitas, acabará indo em uma das duas direções.

Em uma direção, você constrói um sistema complexo e extremamente abrangente de tipos e anotações dependentes, e as anotações acabam recapitulando uma parte substancial do código que anotam. Como espero que você possa ver claramente, essa direção não está de forma alguma de acordo com o design do restante da linguagem Go: Go favorece a simplicidade de especificação e a concisão do código sobre a tipagem estática abrangente.

Na outra direção, as anotações explícitas cobririam apenas um subconjunto das restrições relevantes para uma determinada API. Agora, as anotações fornecem uma falsa sensação de segurança: o código ainda pode quebrar devido a alterações nas restrições implícitas, mas a presença de restrições explícitas leva o desenvolvedor a pensar que qualquer alteração "type-safe" também mantém a compatibilidade.


Não é óbvio para mim por que esse tipo de estabilidade de API precisa ser obtido por meio de anotação explícita de código-fonte: o tipo de estabilidade de API que você está descrevendo também pode ser alcançado (com menos redundância no código) por meio de análise de código-fonte. Por exemplo, você pode imaginar que a ferramenta api analise o código e produza um conjunto de restrições muito mais rico do que pode ser expresso no sistema de tipos formal da linguagem, e dando à ferramenta guru a capacidade de consultar o conjunto calculado de restrições para qualquer função, método ou parâmetro da API.

@bcmills Você não está fazendo do perfeito o inimigo do bom? Sim, existem restrições implícitas que são difíceis de capturar em um sistema de tipos. (E um bom design modular evita a introdução de tais restrições implícitas quando viável.) Seria ótimo ter uma análise abrangente que pudesse verificar estaticamente todas as propriedades que você deseja verificar -- e fornecer explicações claras e não enganosas aos programadores sobre onde eles devem ser verificados. estão cometendo erros. Mesmo com o progresso recente no diagnóstico e localização automática de erros , não estou prendendo a respiração. Por um lado, as ferramentas de análise só podem analisar o código que você fornece a elas. Os desenvolvedores nem sempre têm acesso a todo o código que pode ser vinculado ao deles.

Então, onde há restrições fáceis de capturar em um sistema de tipos, por que não dar aos programadores a capacidade de escrevê-las? Temos 40 anos de experiência em programação com parâmetros de tipo restritos estaticamente. Esta é uma anotação estática simples e intuitiva que vale a pena.

Uma vez que você começa a construir um software maior que estratifica módulos de software, você começa a querer escrever comentários explicando tais restrições implícitas de qualquer maneira. Supondo que haja uma maneira boa e verificável de expressá-los, por que não deixar o compilador entrar na piada para que ele possa ajudá-lo?

Observo que alguns de seus exemplos de outras restrições implícitas envolvem tratamento de erros. Acho que nossa verificação estática leve de exceções [ PLDI 2016 ] abordaria esses exemplos.

@andrewcmyers

Então, onde há restrições fáceis de capturar em um sistema de tipos, por que não dar aos programadores a capacidade de escrevê-las?
[…]
Uma vez que você começa a construir um software maior que estratifica módulos de software, você começa a querer escrever comentários explicando tais restrições implícitas de qualquer maneira. Supondo que haja uma maneira boa e verificável de expressá-los, por que não deixar o compilador entrar na piada para que ele possa ajudá-lo?

Na verdade, concordo completamente com esse ponto e costumo usar um argumento semelhante em relação ao gerenciamento de memória. (Se você tiver que documentar invariantes em alias e retenção de dados de qualquer maneira, por que não impor essas invariantes em tempo de compilação?)

Mas eu levaria esse argumento um passo adiante: o inverso também vale! Se você _não_ precisa escrever um comentário para uma restrição (porque é óbvio no contexto para os humanos que trabalham com o código), por que você precisa escrever esse comentário para o compilador? Independentemente das minhas preferências pessoais, o uso de coleta de lixo e valores zero por Go indica claramente um viés de "não exigir que os programadores declarem invariantes óbvios". Pode ser que a modelagem estilo Gênero possa expressar muitas das restrições que seriam expressas nos comentários, mas como isso se sai em termos de eliminar as restrições que também seriam eliminadas nos comentários?

Parece-me que os modelos de estilo Genus são mais do que apenas comentários de qualquer maneira: eles realmente mudam a semântica do código em alguns casos, eles não apenas o restringem. Agora teríamos dois mecanismos diferentes — interfaces e modelos de tipo — para parametrizar comportamentos. Isso representaria uma grande mudança na linguagem Go: descobrimos algumas práticas recomendadas para interfaces ao longo do tempo (como "definir interfaces no lado do consumidor") e não é óbvio que essa experiência se traduziria em um sistema tão radicalmente diferente, mesmo negligenciando a compatibilidade com Go 1.

Além disso, uma das excelentes propriedades do Go é que sua especificação pode ser lida (e amplamente compreendida) em uma tarde. Não é óbvio para mim que um sistema de restrições no estilo Genus possa ser adicionado à linguagem Go sem complicar substancialmente - eu estaria curioso para ver uma proposta concreta de mudanças nas especificações.

Aqui está um ponto de dados interessante para "metaprogramação". Seria bom para certos tipos nos pacotes sync e atomic — ou seja, atomic.Value e sync.Map — para suportar métodos CompareAndSwap , mas esses só funcionam para tipos que são comparáveis. O restante das APIs atomic.Value e sync.Map permanecem úteis sem esses métodos, portanto, para esse caso de uso, precisamos de algo como SFINAE (ou outros tipos de APIs definidas condicionalmente) ou temos que cair de volta a uma hierarquia mais complexa de tipos.

Eu quero abandonar essa ideia de sintaxe criativa de usar silábicos aborígenes.

@bcmills Você pode explicar mais sobre esses três pontos?

  1. Ambiguidade entre nomes de tipo e nomes de valor.
  2. Suporte excessivamente amplo para sobrecarga do operador
    3. Confiança excessiva na resolução de sobrecarga para metaprogramação

@mahdix Claro.

  1. Ambiguidade entre nomes de tipo e nomes de valor.

Este artigo dá uma boa introdução. Para analisar um programa C++, você deve saber quais nomes são tipos e quais são valores. Ao analisar um programa C++ com modelo, você não tem essas informações disponíveis para os membros dos parâmetros do modelo.

Um problema semelhante surge em Go para literais compostos, mas a ambiguidade está entre valores e nomes de campo em vez de valores e tipos. Neste código Go:

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

a é um nome de campo literal ou a constante a está sendo usada como uma chave de mapa ou índice de matriz?

  1. Suporte excessivamente amplo para sobrecarga do operador

A pesquisa dependente de argumento é um bom lugar para começar. Sobrecargas de operadores em C++ podem ocorrer como métodos no tipo receptor ou como funções livres em qualquer um dos vários namespaces, e as regras para resolver essas sobrecargas são bastante complexas.

Existem muitas maneiras de evitar essa complexidade, mas a mais simples (como o Go atualmente faz) é não permitir totalmente a sobrecarga do operador.

  1. Confiança excessiva na resolução de sobrecarga para metaprogramação

A biblioteca <type_traits> é um bom lugar para começar. Confira a implementação em sua vizinhança amigável libc++ para ver como a resolução de sobrecarga entra em jogo.

Se Go alguma vez suportar metaprogramação (e mesmo isso é muito duvidoso), eu não esperaria que envolvesse resolução de sobrecarga como a operação fundamental para proteger definições condicionais.

@bcmills
Como eu nunca usei C++, você poderia esclarecer onde a sobrecarga do operador por meio da implementação de 'interfaces' predefinidas está em termos de complexidade. Python e Kotlin são exemplos disso.

Eu acho que o próprio ADL é um grande problema com modelos C++ que não foram mencionados, porque eles forçam o compilador a atrasar a resolução de todos os nomes até o momento da instanciação, e podem resultar em bugs muito sutis, em parte porque o "ideal" e " compiladores preguiçosos" se comportam de maneira diferente aqui e o padrão permite isso. O fato de suportar a sobrecarga do operador não é, de longe, a pior parte.

Esta proposta é baseada em Templates, um sistema de macro expansão não seria suficiente? Não estou falando de go generate ou projetos como o gottemplate. Estou falando mais assim:

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

Macro pode reduzir o clichê e o uso de reflexão.

Eu acho que C++ é um exemplo bom o suficiente para que os genéricos não sejam baseados em modelos ou macros. Especialmente considerando que o Go tem coisas como funções anônimas que realmente não podem ser "instanciadas" em tempo de compilação, exceto como uma otimização.

@samadadi , você pode expressar seu ponto de vista sem dizer "o que há de errado com vocês". Dito isto, o argumento da complexidade já foi levantado várias vezes.

Go não é a primeira linguagem a tentar alcançar a simplicidade omitindo o suporte para polimorfismo paramétrico (genéricos), apesar desse recurso se tornar cada vez mais importante nos últimos 40 anos - na minha experiência, é um grampo dos cursos de programação do segundo semestre.

O problema de não ter o recurso na linguagem é que os programadores acabam recorrendo a soluções ainda piores. Por exemplo, os programadores Go geralmente escrevem modelos de código que são macro-expandidos para produzir o código "real" para vários tipos desejados. Mas a linguagem de programação real é aquela que você digita, não aquela que o compilador vê. Portanto, essa estratégia significa efetivamente que você está usando uma linguagem (não mais padrão) que tem toda a fragilidade e o excesso de código dos modelos C++.

Conforme observado em https://blog.golang.org/toward-go2 , precisamos fornecer "relatórios de experiência", para que as necessidades e as metas de design possam ser determinadas. Você poderia levar alguns minutos e documentar os casos macro que você observou?

Por favor, mantenha este bug no tópico e civil. E novamente, https://golang.org/wiki/NoMeToo. Por favor, comente apenas se você tiver informações únicas e construtivas para adicionar.

@mandolyte É muito fácil encontrar explicações detalhadas na web defendendo a geração de código como um substituto (parcial) para genéricos:
https://appliedgo.net/generics/
https://www.calhoun.io/using-code-generation-to-survive-without-generics-in-go/
http://blog.ralch.com/tutorial/golang-code-generation-and-generics/

Claramente, há um monte de gente lá fora, adotando essa abordagem.

@andrewcmyers , existem algumas limitações, bem como advertências de conveniência ao usar a geração de código, MAS .
Geralmente - se você acredita que essa abordagem é melhor/boa o suficiente, acho que o esforço para permitir uma geração um pouco semelhante de dentro da cadeia de ferramentas go seria uma bênção.

  • A otimização do compilador pode ser um desafio neste caso, mas o tempo de execução será consistente, E a manutenção do código, a experiência do usuário (simplicidade...), as melhores práticas padrão e os padrões de código unificados podem ser mantidos.
    Além disso - toda a cadeia de ferramentas será mantida a mesma, exceto as ferramentas de depuração (profilers , depuradores de etapas, etc.) que verão linhas de código que não foram escritas pelo desenvolvedor, mas é um pouco como entrar no código ASM durante a depuração - apenas é um código legível :) .

Desvantagem - nenhum precedente (que eu saiba) para essa abordagem dentro da cadeia de ferramentas go.

Para resumir - considere a geração de código como parte do processo de compilação, não deve ser muito complicado, bastante seguro, otimizado em tempo de execução, pode manter a simplicidade e mudanças muito pequenas na linguagem.

IMHO: É um compromisso facilmente alcançado, com um preço baixo.

Para ser claro, não considero a geração de código no estilo macro, seja feita com gen, cpp, gofmt -r ou outras ferramentas de macro/modelo, como uma boa solução para o problema dos genéricos, mesmo que padronizada. Ele tem os mesmos problemas que os modelos C++: código bloat, falta de verificação de tipo modular e dificuldade de depuração. Fica pior quando você começa, como é natural, construir código genérico em termos de outro código genérico. Na minha opinião, as vantagens são limitadas: manteria a vida relativamente simples para os escritores do compilador Go e produz código eficiente - a menos que haja pressão de cache de instruções, uma situação frequente em software moderno!

Eu acho que o ponto era que a geração de código é usada para substituir
genéricos, então os genéricos devem procurar resolver a maioria desses casos de uso.

Em qua, 26 de julho de 2017, 22:41 Andrew Myers, [email protected] escreveu:

Para ser claro, não considero a geração de código no estilo macro, seja feita
com gen, cpp, gofmt -r ou outras ferramentas de macro/modelo, para ser uma boa
solução para o problema dos genéricos mesmo que padronizados. Tem o mesmo
problemas como modelos C++: código bloat, falta de verificação de tipo modular e
dificuldade de depuração. Fica pior à medida que você começa, como é natural, a construir
código genérico em termos de outro código genérico. A meu ver, as vantagens são
limitado: manteria a vida relativamente simples para os escritores do compilador Go
e produz código eficiente - a menos que haja cache de instruções
pressão, uma situação frequente em softwares modernos!


Você está recebendo isso porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/15292#issuecomment-318242016 ou silenciar
o segmento
https://github.com/notifications/unsubscribe-auth/AT4HVb2SPMpe5dlEDUQeadIRKPaB74zoks5sR_jSgaJpZM4IG-xv
.

Sem dúvida, a geração de código não é uma solução REAL, mesmo que embrulhada com algum suporte à linguagem para tornar a aparência como uma "parte da linguagem"

Meu ponto foi muito custo-benefício.

Btw, se você olhar para alguns dos substitutos de geração de código, você pode ver facilmente como eles poderiam ter sido muito mais legíveis, mais rápidos e não possuem alguns conceitos errados (por exemplo, iteração sobre matrizes de ponteiros versus valores) se a linguagem lhes fornecesse ferramentas melhores por esta.

E talvez esse seja um caminho melhor para resolver a curto prazo, que não pareceria um patch:
antes de pensar no "melhor suporte genérico que também será idiomático" (acredito que algumas implementações acima levariam anos para realizar a integração completa), implemente alguns conjuntos de funções suportadas "na linguagem" que são necessárias de qualquer maneira (como uma compilação em estruturas cópia profunda) tornaria essa solução de geração de código muito mais utilizável.

Depois de ler as propostas de genéricos de @bcmills e @ianlancetaylor , fiz as seguintes observações:

Funções de tempo de compilação e tipos de primeira classe

Gosto da ideia de avaliação em tempo de compilação, mas não vejo o benefício de limitá-la a funções puras. Esta proposta apresenta o gotype embutido, mas limita seu uso a funções const e quaisquer tipos de dados definidos dentro do escopo da função. Do ponto de vista de um usuário de biblioteca, a instanciação é limitada a funções construtoras como "New" e leva a assinaturas de função como esta:

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

O tipo de retorno aqui não pode ser separado em um tipo de função porque estamos limitados a funções puras. Além disso, a assinatura define dois novos "tipos" na própria assinatura (K e V), o que significa que, para analisar um único parâmetro, devemos analisar toda a lista de parâmetros. Isso é bom para um compilador, mas gostaria de saber se adiciona complexidade à API pública de um pacote.

Parâmetros de tipo em Go

Tipos parametrizados permitem a maioria dos casos de uso de programação genérica, por exemplo, a capacidade de definir estruturas de dados genéricas e operações sobre diferentes tipos de dados. A proposta lista exaustivamente os aprimoramentos do verificador de tipos que seriam necessários para produzir melhores erros de compilação, tempos de compilação mais rápidos e binários menores.

Na seção "Type Checker", a proposta também lista algumas restrições de tipo úteis para acelerar o processo, como "Indexable", "Comparable", "Callable", "Composite", etc... O que não entendo é por que não permitir que o usuário especifique suas próprias restrições de tipo? A proposta afirma que

Não há restrições sobre como os tipos parametrizados podem ser usados ​​em uma função parametrizada.

No entanto, se os identificadores tivessem mais restrições vinculadas a eles, isso não teria o efeito de auxiliar o compilador? Considerar:

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

vs

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

Separar restrições de tipo de parâmetros de tipo e permitir restrições definidas pelo usuário também pode melhorar a legibilidade, tornando os pacotes genéricos mais fáceis de entender. Curiosamente, as falhas listadas no final da proposta em relação à complexidade das regras de dedução de tipo poderiam realmente ser mitigadas se essas regras fossem explicitamente definidas pelo usuário.

@smasher164

Gosto da ideia de avaliação em tempo de compilação, mas não vejo o benefício de limitá-la a funções puras.

A vantagem é que torna possível a compilação separada. Se uma função de tempo de compilação puder modificar o estado global, o compilador deverá ter esse estado disponível ou registrar as edições nele de forma que o vinculador possa sequenciá-las no momento do link. Se uma função de tempo de compilação puder modificar o estado local, precisaríamos de alguma maneira de rastrear qual estado é local versus global. Ambos adicionam complexidade, e não é óbvio que qualquer um forneça benefícios suficientes para compensá-lo.

@smasher164

O que não entendo é por que não permitir que o usuário especifique suas próprias restrições de tipo?

As restrições de tipo nessa proposta correspondem a operações na sintaxe da linguagem. Isso reduz a área de superfície dos novos recursos: não há necessidade de especificar sintaxe adicional para tipos de restrição, porque todas as restrições sintáticas podem ser inferidas do uso.

se os identificadores tivessem mais restrições vinculadas a eles, isso não teria o efeito de auxiliar o compilador?

A linguagem deve ser projetada para seus usuários, não para os compiladores-escritores.

não há necessidade de especificar sintaxe adicional para tipos de restrição porque todas as restrições sintáticas podem ser inferidas a partir do uso.

Esta é a rota que o C++ caiu. Requer uma análise global do programa para identificar os usos relevantes. O código não pode ser raciocinado pelos programadores de forma modular, e as mensagens de erro são detalhadas e incompreensíveis.

Pode ser tão fácil e leve especificar as operações necessárias. Veja CLU (1977) para um exemplo.

@andrewcmyers

Requer uma análise global do programa para identificar os usos relevantes. O código não pode ser raciocinado pelos programadores de forma modular,

Isso está usando uma definição particular de "modular", que eu não acho que seja tão universal quanto você parece supor. Sob a proposta de 2013, cada função ou tipo teria um conjunto inequívoco de restrições inferidas de baixo para cima de pacotes importados, exatamente da mesma forma que o tempo de execução (e as restrições de tempo de execução) de funções não paramétricas são derivadas de baixo para cima. das cadeias de chamadas hoje.

Você presumivelmente poderia consultar as restrições inferidas usando guru ou uma ferramenta semelhante, e poderia responder a essas consultas usando informações locais dos metadados do pacote exportado.

e as mensagens de erro são detalhadas e incompreensíveis.

Temos alguns exemplos (GCC e MSVC) demonstrando que as mensagens de erro geradas ingenuamente são incompreensíveis. Eu acho que é um exagero supor que as mensagens de erro para restrições implícitas são intrinsecamente ruins.

Acho que a maior desvantagem das restrições inferidas é que elas facilitam o uso de um tipo de uma maneira que introduz uma restrição sem entendê-la completamente. Na melhor das hipóteses, isso significa apenas que seus usuários podem se deparar com falhas inesperadas em tempo de compilação, mas na pior das hipóteses, isso significa que você pode quebrar o pacote para os consumidores introduzindo uma nova restrição inadvertidamente. Restrições especificadas explicitamente evitariam isso.

Pessoalmente, também não acho que as restrições explícitas estejam fora de sintonia com a abordagem Go existente, já que as interfaces são restrições explícitas do tipo de tempo de execução, embora tenham expressividade limitada.

Temos alguns exemplos (GCC e MSVC) demonstrando que as mensagens de erro geradas ingenuamente são incompreensíveis. Eu acho que é um exagero supor que as mensagens de erro para restrições implícitas são intrinsecamente ruins.

A lista de compiladores em que a inferência de tipo não local - que é o que você propõe - resulta em mensagens de erro ruins é um pouco mais longa do que isso. Inclui SML, OCaml e GHC, onde muito esforço já foi feito para melhorar suas mensagens de erro e onde há pelo menos alguma estrutura de módulo explícita ajudando. Você pode ser capaz de fazer melhor, e se você criar um algoritmo para boas mensagens de erro com o esquema que você propõe, você terá uma boa publicação. Como ponto de partida para esse algoritmo, você pode achar úteis nossos artigos POPL 2014 e PLDI 2015 sobre localização de erros. Eles são mais ou menos o estado da arte.

porque todas as restrições sintáticas podem ser inferidas a partir do uso.

Isso não limita a amplitude dos programas genéricos com verificação de tipo? Por exemplo, observe que a proposta de parâmetros de tipo não especifica uma restrição "Iterable". Na linguagem atual, isso corresponderia a uma fatia ou canal, mas um tipo composto (digamos, uma lista vinculada) não atenderia necessariamente a esses requisitos. Definindo uma interface como

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

ajuda o caso de lista vinculada, mas agora os tipos de fatia e canal internos devem ser estendidos para satisfazer essa interface.

Uma restrição que diz "Aceito o conjunto de todos os tipos que são Iterables, slices ou canais" parece uma situação ganha-ganha para o usuário, autor do pacote e implementador do compilador. O ponto que estou tentando mostrar é que as restrições são um superconjunto de programas sintaticamente válidos, e alguns podem não fazer sentido do ponto de vista da linguagem, mas apenas do ponto de vista da API.

A linguagem deve ser projetada para seus usuários, não para os compiladores-escritores.

Concordo, mas talvez eu devesse ter formulado de forma diferente. A eficiência aprimorada do compilador pode ser um efeito colateral das restrições definidas pelo usuário. O principal benefício seria a legibilidade, já que o usuário tem uma ideia melhor do comportamento de sua API do que o compilador. A desvantagem aqui é que os programas genéricos teriam que ser um pouco mais explícitos sobre o que eles aceitam.

E se em vez de

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

separamos a ideia de "interfaces" de "restrições". Então podemos ter

type T generic

type Iterable class {
    Next() T
}

onde "class" significa uma classe de tipo no estilo Haskell, não uma classe no estilo Java.

Ter "typeclasses" separados de "interfaces" pode ajudar a esclarecer um pouco da não ortogonalidade das duas ideias. Então Sortable (ignorando sort.Interface) pode ser algo como:

type T generic

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

type Sortable class {
    Next() Comparable
}

Aqui estão alguns comentários para a seção "Classes e conceitos de tipo" em Genus por @andrewcmyers e sua aplicabilidade para Go.

Esta seção aborda as limitações das classes e conceitos de tipos, declarando

primeiro, a satisfação da restrição deve ser testemunhada de forma única

Não sei se entendi essa limitação. Amarrar uma restrição a identificadores separados não impediria que ela fosse exclusiva para um determinado tipo? Parece-me que a cláusula "where" em Genus essencialmente constrói um tipo/restrição de uma determinada restrição, mas isso parece análogo a instanciar uma variável de um determinado tipo. Uma restrição dessa maneira se assemelha a um tipo .

Aqui está uma simplificação dramática das definições de restrição, adaptadas para Go:

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

Assim, uma declaração de mapa apareceria como:

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

onde em Gênero, poderia ser assim:

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

e na proposta de Type-Params existente, ficaria assim:

type Map[K,V] struct {
}

Acho que todos podemos concordar que permitir restrições para alavancar o sistema de tipos existente pode remover a sobreposição entre os recursos da linguagem e facilitar a compreensão de novos.

e segundo, seus modelos definem como adaptar um único tipo, enquanto em uma linguagem com subtipagem, cada tipo adaptado em geral representa todos os seus subtipos.

Essa limitação parece menos pertinente ao Go, pois a linguagem já possui boas regras de conversão entre tipos nomeados/sem nome e interfaces sobrepostas.

Os exemplos apresentados propõem modelos como solução, o que parece ser um recurso útil, mas não necessário para Go. Se uma biblioteca espera que um tipo implemente http.Handler, por exemplo, e o usuário deseja comportamentos diferentes dependendo do contexto, escrever adaptadores é simples:

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

Na verdade, é isso que a biblioteca padrão faz.

@smasher164

primeiro, a satisfação da restrição deve ser testemunhada de forma única
Não sei se entendi essa limitação. Amarrar uma restrição a identificadores separados não impediria que ela fosse exclusiva para um determinado tipo?

A ideia é que em Genus você possa satisfazer a mesma restrição com o mesmo tipo de mais de uma maneira, diferentemente de Haskell. Por exemplo, se você tem um HashSet[T] , você pode escrever HashSet[String] para hash strings da maneira usual, mas HashSet[String with CaseInsens] para hash e comparar strings com CaseInsens model, que presumivelmente trata strings de uma maneira que não diferencia maiúsculas de minúsculas. Gênero realmente distingue esses dois tipos; isso pode ser um exagero para Go. Mesmo que o sistema de tipos não o acompanhe, ainda parece importante poder substituir as operações padrão fornecidas por um tipo.

kind Qualquer interface{} // aceita qualquer tipo que satisfaça a interface{}.
type T Any // Declara um tipo de Qualquer tipo. Também o vincula a um identificador.
kind Eq T == T // aceita qualquer tipo para o qual a igualdade é definida.
type Map[K Eq, V Any] struct { ...
}

O equivalente moral disso em Gênero seria:

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

Em Familia escreveríamos apenas:

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

Edit: retirando isso em favor de uma solução baseada em reflexão, conforme descrito em #4146. Uma solução baseada em genéricos, conforme descrevi abaixo, cresce linearmente no número de composições. Embora uma solução baseada em reflexão sempre tenha uma desvantagem de desempenho, ela pode se otimizar em tempo de execução para que a desvantagem seja constante, independentemente do número de composições.

Esta não é uma proposta, mas um caso de uso potencial a ser considerado ao criar uma proposta.

Duas coisas são comuns no código Go hoje

  • encapsulando um valor de interface para fornecer funcionalidade adicional (encapsulando um http.ResponseWriter para um framework)
  • ter métodos opcionais que às vezes têm valores de interface (como Temporary() bool em net.Error )

Estes são bons e úteis, mas eles não se misturam. Depois de encapsular uma interface, você perde a capacidade de acessar qualquer método não definido no tipo de encapsulamento. Ou seja, dado

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

Se você envolver um erro nessa estrutura, ocultará quaisquer métodos adicionais no erro original.

Se você não encapsular o erro na estrutura, não poderá fornecer o contexto extra.

Digamos que a proposta genérica aceita permite que você defina algo como o seguinte (sintaxe arbitrária que tentei tornar intencionalmente feia para que ninguém se concentre nela)

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

Aproveitando a incorporação, podemos incorporar qualquer tipo concreto que satisfaça a interface de erro e tanto envolvê-la quanto ter acesso a seus outros métodos. Infelizmente, isso só nos leva a parte do caminho.

O que realmente precisamos aqui é pegar um valor arbitrário da interface de erro e incorporar seu tipo dinâmico.

Isso levanta imediatamente duas preocupações

  • o tipo teria que ser criado em tempo de execução (provavelmente necessário para refletir de qualquer maneira)
  • a criação do tipo teria que entrar em pânico se o valor do erro for nil

Se isso não o azedou no pensamento, você também precisa de um mecanismo para "saltar" a interface para seu tipo dinâmico, seja por uma anotação na lista de parâmetros genéricos para dizer "sempre instanciar no tipo dinâmico de valores de interface " ou por alguma função mágica que só pode ser chamada durante a instanciação de tipo para desembalar a interface para que seu tipo e valor possam ser emendados corretamente.

Sem isso, você está apenas instanciando MyError no próprio tipo de erro, não no tipo dinâmico da interface.

Digamos que temos uma função mágica unbox para extrair e (de alguma forma) aplicar as informações:

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

Agora digamos que temos um erro não nulo, err , cujo tipo dinâmico é *net.DNSError . Então isso

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

imprimiria true . Mas se o tipo dinâmico de err fosse *os.PathError , teria sido impresso falso.

Espero que a semântica proposta seja clara, dada a sintaxe obtusa usada na demonstração.

Também espero que haja uma maneira melhor de resolver esse problema com menos mecanismo e cerimônia, mas acho que o acima poderia funcionar.

@jimmyfrasche Se estou entendendo o que você quer, é um mecanismo de adaptação sem wrapper. Você deseja expandir o conjunto de operações que um tipo oferece sem envolvê-lo em outro objeto que oculte o original. Esta é uma funcionalidade que o Genus oferece.

@andrewcmyers não.

Estruturas em Go permitem a incorporação. Se você adicionar um campo sem nome, mas com um tipo a uma estrutura, ele fará duas coisas: criará um campo com o mesmo nome do tipo e permitirá despacho transparente para qualquer método desse tipo. Isso soa muito como herança, mas não é. Se você tivesse um tipo T que tivesse um método Foo(), os seguintes são equivalentes

type S struct {
  T
}

e

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

(quando Foo é chamado seu "this" é sempre do tipo T).

Você também pode incorporar interfaces em estruturas. Isso fornece à estrutura todos os métodos no contrato da interface (embora você precise atribuir algum valor dinâmico ao campo implícito ou causará um pânico com o equivalente a uma exceção de ponteiro nulo)

Go tem interfaces que definem um contrato em termos de métodos de um tipo. Um valor de qualquer tipo que satisfaça o contrato pode ser enquadrado em um valor dessa interface. Um valor de uma interface é um ponteiro para o manifesto de tipo interno (tipo dinâmico) e um ponteiro para um valor desse tipo dinâmico (valor dinâmico). Você pode fazer asserções de tipo em um valor de interface para (a) obter o valor dinâmico se você declarar para seu tipo sem interface ou (b) obter um novo valor de interface se você declarar para uma interface diferente que o valor dinâmico também satisfaça. É comum usar o último para "teste de recursos" de um objeto para ver se ele suporta métodos opcionais. Para reutilizar um exemplo anterior, alguns erros têm um método "Temporary() bool" para que você possa ver se algum erro é temporário com:

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

Também é comum envolver um tipo em outro tipo para fornecer recursos extras. Isso funciona bem com tipos que não são de interface. Quando você encapsula uma interface, você também oculta os métodos que não conhece e não pode recuperá-los com asserções do tipo "teste de recurso": o tipo encapsulado expõe apenas os métodos necessários da interface, mesmo que tenha métodos opcionais . Considerar:

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

type I interface {
  Foo()
}

type B struct {
  I
}

var i I = B{A{}}

Você não pode chamar Bar em i ou mesmo saber que ele existe, a menos que você saiba que o tipo dinâmico de i é um B, então você pode desembrulhar e acessar o campo I para digitar assert sobre isso .

Isso causa problemas reais, especialmente ao lidar com interfaces comuns como erro ou Leitor.

Se houvesse uma maneira de retirar o tipo dinâmico e o valor de uma interface (de alguma maneira segura e controlada), você poderia parametrizar um novo tipo com isso, definir o campo incorporado para o valor e retornar uma nova interface. Então você obtém um valor que satisfaz a interface original, tem qualquer funcionalidade aprimorada que você deseja adicionar, mas o resto dos métodos do tipo dinâmico original ainda estão lá para serem testados.

@jimmyfrasche De fato. O que o Genus permite que você faça é usar um tipo para satisfazer um contrato de "interface" sem encaixotá-lo. O valor ainda tem seu tipo original e suas operações originais. Além disso, o programa pode especificar quais operações o tipo deve usar para satisfazer o contrato - por padrão, são as operações que o tipo fornece, mas o programa pode fornecer novas se o tipo não tiver as operações necessárias. Ele também pode substituir as operações que o tipo usaria.

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

@jimmyfrasche Para mim, parece que o principal problema aqui é obter o tipo/valor dinâmico de uma variável. Deixando de lado a incorporação, um exemplo simplificado seria

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

O valor atribuído a e precisa ter um tipo dinâmico (ou concreto) de algo como *net.DNSError , que implementa error . Aqui estão algumas maneiras possíveis de uma futura mudança de idioma resolver esse problema:

  1. Tenha uma função mágica do tipo unbox que descubra o valor dinâmico de uma variável. Isso se aplica a qualquer tipo que não seja concreto, por exemplo, sindicatos.
  2. Se a alteração de idioma oferecer suporte a variáveis ​​de tipo, forneça um meio de obter o tipo dinâmico da variável. Com informações de tipo, podemos escrever a função unbox nós mesmos. Por exemplo,
func unbox(v T1) T2 {
    t := dynTypeOf(v)
    return v.(t)
}

wrap pode ser escrito da mesma forma que antes, ou como

func wrap(ec extraContext, err error) error {
  if err == nil {
    return nil
  }
  t := dynTypeOf(err)
  return MyError{
    e: v.(t),
    extraContext: ec,
  }
}
  1. Se a mudança de idioma suportar restrições de tipo, aqui está uma ideia alternativa:
type E1 which_is_a_type_satisfying error
type E2 which_is_a_type_satisfying error

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

Neste exemplo, aceitamos um valor de qualquer tipo que implemente erro. Qualquer usuário de wrap que espera um error receberá um. No entanto, o tipo de e dentro MyError é o mesmo do err que é passado, que não se limita a um tipo de interface. Se alguém quisesse o mesmo comportamento que 2,

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

Como ninguém mais parece ter feito isso, gostaria de destacar os "relatos de experiência" muito óbvios para genéricos, conforme solicitado por https://blog.golang.org/toward-go2.

O primeiro é o tipo map embutido:

m := make(map[string]string)

O próximo é o tipo chan embutido:

c := make(chan bool)

Finalmente, a biblioteca padrão está repleta de alternativas interface{} onde os genéricos funcionariam com mais segurança:

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

Pode haver outros que eu estou perdendo. O ponto é que cada um dos itens acima é onde eu esperaria que os genéricos fossem úteis.

(Nota: não estou incluindo sort.Sort aqui porque é um excelente exemplo de como interfaces podem ser usadas em vez de genéricos.)

http://www.yinwang.org/blog-cn/2014/04/18/golang
Eu acho que o genérico é importante. Caso contrário, não pode lidar com tipos semelhantes. Às vezes, a interface não pode resolver o problema.

Sintaxe simples e sistema de tipos são as vantagens importantes do Go. Se você adicionar genéricos, a linguagem se tornará uma bagunça feia como Scala ou Haskell. Além disso, esse recurso atrairá fanboys pseudo-acadêmicos, que eventualmente transformarão os valores da comunidade de "Vamos fazer isso" para "Vamos falar sobre teoria e matemática de CS". Evite genéricos, é um caminho para o abismo.

@bxqgit , por favor, mantenha isso civilizado. Não há necessidade de insultar ninguém.

Quanto ao que o futuro trará, veremos, mas sei que, embora em 98% do meu tempo não precise de genéricos, sempre que precisar, gostaria de poder usá-los. Como eles são usados ​​vs como eles são usados ​​indevidamente é uma discussão diferente. Educar os usuários deve ser parte do processo.

@bxqgit
Existem situações em que os genéricos são necessários, como estruturas de dados genéricas (Árvores, Pilhas, Filas, ...) ou funções genéricas (Map, Filter, Reduce, ...) e essas coisas são inevitáveis, usando interfaces em vez de genéricos em essas situações apenas adicionam uma enorme complexidade tanto para o escritor de código quanto para o leitor de código e também tem um efeito ruim na eficiência do código em tempo de execução, então deve ser muito mais racional adicionar genéricos de linguagem do que tentar usar interfaces e refletir para escrever complexos e código ineficiente.

@bxqgit Adicionar genéricos não necessariamente adiciona complexidade à linguagem, isso também pode ser alcançado com uma sintaxe simples. Com genéricos, você está adicionando uma restrição de tipo de tempo de compilação variável que é muito útil com estruturas de dados, como disse @riwogo .

O sistema de interface atual em go é muito útil, mas é muito ruim quando você precisa, por exemplo, de uma implementação geral de list, que com interfaces precisa de uma restrição de tipo de tempo de execução, no entanto, se você adicionar genéricos, o tipo genérico pode ser substituído em tempo de compilação com o tipo real, tornando a restrição desnecessária.

Além disso, lembre-se, as pessoas por trás vão, desenvolvem a linguagem usando o que você chama de "teoria e matemática CS", e também são as pessoas que "estão fazendo isso".

Além disso, lembre-se, as pessoas por trás vão, desenvolvem a linguagem usando o que você chama de "teoria e matemática CS", e também são as pessoas que "estão fazendo isso".

Pessoalmente, não vejo muita teoria e matemática de CS no design da linguagem Go. É uma linguagem bastante primitiva, o que é bom na minha opinião. Além disso, essas pessoas de quem você está falando decidiram evitar os genéricos e fizeram as coisas. Se funciona bem, por que mudar alguma coisa? Geralmente, acho que evoluir e estender constantemente a sintaxe da linguagem é uma má prática. Ele só adiciona complexidade que leva ao caos de Haskell e Scala.

O template é complicado mas o Generics é simples

Veja as funções SortInts, SortFloats, SortStrings no pacote de classificação. Ou SearchInts, SearchFloats, SearchStrings. Ou os métodos Len, Less e Swap de byName no pacote io/ioutil. Cópia clichê pura.

As funções de copiar e anexar existem porque tornam as fatias muito mais úteis. Genéricos significaria que essas funções são desnecessárias. Os genéricos tornariam possível escrever funções semelhantes para mapas e canais, sem mencionar os tipos de dados criados pelo usuário. É verdade que as fatias são o tipo de dados composto mais importante, e é por isso que essas funções eram necessárias, mas outros tipos de dados ainda são úteis.

Meu voto é não para genéricos de aplicativos generalizados, sim para funções genéricas mais internas como append e copy que funcionam em vários tipos de base. Talvez sort e search possam ser adicionados para os tipos de coleção?

Para meus aplicativos, o único tipo que está faltando é um conjunto não ordenado (https://github.com/golang/go/issues/7088), eu gostaria disso como um tipo interno para obter a digitação genérica como slice e map . Coloque o trabalho no compilador (benchmarking para cada tipo base e um conjunto selecionado de tipos struct e depois ajuste para melhor desempenho) e mantenha anotações adicionais fora do código do aplicativo.

smap embutido em vez de sync.Map também, por favor. Da minha experiência, usar interface{} para segurança de tipo de tempo de execução é uma falha de design. A verificação de tipo em tempo de compilação é um dos principais motivos para usar o Go.

@pciet

Pela minha experiência, usar a interface{} para segurança do tipo de tempo de execução é uma falha de design.

Você pode apenas escrever um wrapper pequeno (tipo seguro)?
https://play.golang.org/p/tG6hd-j5yx

@pierrre Esse invólucro é melhor do que um cheque de reflect.TypeOf(item).AssignableTo(type) . Mas escrever seu próprio tipo com map + sync.Mutex ou sync.RWMutex é a mesma complexidade sem a declaração de tipo que sync.Map requer.

Meu uso de mapa sincronizado tem sido para mapas globais de mutexes com var myMapLock = sync.RWMutex{} ao lado em vez de fazer um tipo. Isso poderia ser mais limpo. Um tipo interno genérico soa bem para mim, mas exige um trabalho que não posso fazer, e prefiro minha abordagem em vez de declaração de tipo.

Suspeito que a reação visceral negativa aos genéricos que muitos programadores Go parecem ter surge porque sua principal exposição aos genéricos foi por meio de modelos C++. Isso é lamentável porque o C++ errou tragicamente os genéricos desde o primeiro dia e vem agravando o erro desde então. Os genéricos para Go podem ser muito mais simples e menos propensos a erros.

Seria decepcionante ver o Go se tornando cada vez mais complexo adicionando tipos parametrizados embutidos. Seria melhor apenas adicionar o suporte à linguagem para programadores escreverem seus próprios tipos parametrizados. Em seguida, os tipos especiais poderiam ser fornecidos apenas como bibliotecas, em vez de sobrecarregar a linguagem principal.

@andrewcmyers "Genéricos para Go poderia ser muito mais simples e menos propenso a erros." --- como genéricos em C#.

É decepcionante ver o Go se tornando cada vez mais complexo ao adicionar tipos parametrizados embutidos.

Apesar da especulação nesta edição, acho extremamente improvável que isso aconteça.

O expoente na medida de complexidade dos tipos parametrizados é a variância.
Os tipos de Go (exceto interfaces) são invariáveis ​​e isso pode e deve ser
manteve a regra.

Uma implementação de genéricos "tipo copiar-colar" mecânica e assistida por compilador
resolveria 99% do problema de uma forma fiel aos princípios básicos de Go
princípios de superficialidade e não surpresa.

Aliás, essa e dezenas de outras ideias viáveis ​​já foram discutidas
antes e alguns até culminaram em abordagens boas e viáveis. Neste
ponto, estou no limite do ódio de papel alumínio sobre como todos eles desapareceram
silenciosamente no vazio.

Em 28 de novembro de 2017 23:54, "Andrew Myers" [email protected] escreveu:

Suspeito que a reação visceral negativa aos genéricos que muitos Go
programadores parecem ter surgido porque sua principal exposição aos genéricos foi
através de modelos C++. Isso é lamentável porque o C++ recebeu os genéricos tragicamente
errado desde o primeiro dia e vem agravando o erro desde então. Genéricos para
Go poderia ser muito mais simples e menos propenso a erros.

É decepcionante ver o Go se tornando cada vez mais complexo ao adicionar
tipos parametrizados embutidos. Seria melhor apenas adicionar o idioma
suporte para programadores escreverem seus próprios tipos parametrizados. Então o
tipos especiais poderiam ser fornecidos apenas como bibliotecas em vez de desordenar
a linguagem central.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/15292#issuecomment-347691444 ou silenciar
o segmento
https://github.com/notifications/unsubscribe-auth/AJZ_jPsQd2qBbn9NI1wZeT-O2JpyraTMks5s7I81gaJpZM4IG-xv
.

Sim, você pode ter genéricos sem modelos. Os modelos são uma forma de polimorfismo paramétrico avançado principalmente para instalações de metaprogramação.

@ianlancetaylor Rust permite que um programa implemente uma característica T em um tipo existente Q , desde que sua caixa defina T ou Q .

Apenas um pensamento: eu me pergunto se Simon Peyton Jones (sim, da fama de Haskell) e/ou os desenvolvedores do Rust podem ajudar. Rust e Haskell têm provavelmente os dois sistemas de tipos mais avançados de qualquer linguagem de produção, e Go deve aprender com eles.

Há também Phillip Wadler , que trabalhou em Generic Java , que eventualmente levou à implementação de genéricos que Java tem hoje.

@tarcieri Não acho que os genéricos do Java sejam muito bons, mas são testados em batalha.

@DemiMarie Tivemos Andrew Myers lançando aqui, felizmente.

Com base na minha experiência pessoal, acho que as pessoas que sabem muito sobre diferentes idiomas e sistemas de tipos diferentes podem ser muito úteis no exame de ideias. Mas para produzir as ideias em primeiro lugar, o que precisamos são pessoas que estejam muito familiarizadas com o Go, como ele funciona hoje e como ele pode funcionar razoavelmente no futuro. Go foi projetado para ser, entre outras coisas, uma linguagem simples. Importar ideias de linguagens como Haskell ou Rust, que são significativamente mais complicadas que Go, provavelmente não será uma boa opção. E, em geral, as ideias de pessoas que ainda não escreveram uma quantidade razoável de código Go provavelmente não serão adequadas; não que as ideias sejam ruins como tal, apenas que elas não se encaixem bem com o resto da linguagem.

Por exemplo, é importante entender que Go já tem suporte parcial para programação genérica usando tipos de interface e já tem suporte (quase) completo usando o pacote reflect. Embora essas duas abordagens à programação genérica sejam insatisfatórias por várias razões, qualquer proposta de genéricos em Go precisa interagir bem com elas e, ao mesmo tempo, abordar suas deficiências.

Na verdade, enquanto estou aqui, há algum tempo pensei em programação genérica com interfaces por um tempo e cheguei a três razões pelas quais ela não é satisfatória.

  1. Interfaces requerem que todas as operações sejam expressas como métodos. Isso torna difícil escrever uma interface para tipos internos, como tipos de canal. Todos os tipos de canal suportam o operador <- para operações de envio e recebimento, e é bastante fácil escrever uma interface com os métodos Send e Receive , mas para atribuir um valor de canal para esse tipo de interface, você deve escrever os métodos clichê Send e Receive . Esses métodos padronizados serão exatamente os mesmos para cada tipo de canal diferente, o que é tedioso.

  2. As interfaces são tipadas dinamicamente e, portanto, os erros que combinam diferentes valores tipados estaticamente são capturados apenas em tempo de execução, não em tempo de compilação. Por exemplo, uma função Merge que mescla dois canais em um único canal usando seus métodos Send e Receive exigirá que os dois canais tenham elementos do mesmo tipo, mas isso verificação só pode ser feita em tempo de execução.

  3. As interfaces são sempre encaixotadas. Por exemplo, não há como usar interfaces para agregar um par de outros tipos sem colocar esses outros tipos em valores de interface, exigindo alocações de memória adicionais e busca de ponteiro.

Estou feliz em kibitz sobre propostas de genéricos para Go. Talvez também seja interessante a crescente quantidade de pesquisas sobre genéricos em Cornell ultimamente, aparentemente relevantes para o que pode ser feito com Go:

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

No benchmarking map vs. slice para um tipo de conjunto não ordenado, escrevi testes de unidade separados para cada um, mas com tipos de interface posso combinar essas duas listas de teste em uma:

type Item interface {
    Equal(Item) bool
}

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

Testando a remoção de um item:

type RemoveCase struct {
    Set
    Item
    Out Set
}

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

Dessa forma, posso juntar meus casos anteriormente separados em uma fatia de casos sem problemas:

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

Para cada tipo concreto tive que definir os métodos de interface. Por exemplo:

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

Esses testes genéricos podem usar uma verificação de tipo em tempo de compilação proposta:

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

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

Pensando mais nisso, não parece que uma verificação de tipo em tempo de compilação seria possível para tal teste, pois você teria que executar o programa para saber se um tipo é passado para o método de interface correspondente.

Então, que tal um tipo "genérico" que é uma interface e tem uma declaração de tipo invisível adicionada pelo compilador quando usada concretamente?

@andrewcmyers O artigo "Familia" foi interessante (e muito acima da minha cabeça). Uma noção chave era herança. Como os conceitos mudariam para uma linguagem como Go, que depende de composição em vez de herança?

Obrigado. A parte de herança não se aplica a Go -- se você estiver interessado apenas em genéricos para Go, você pode parar de ler após a seção 4 do artigo. A principal coisa sobre esse artigo que é relevante para Go é que ele mostra como usar interfaces tanto da maneira como elas são usadas para Go agora quanto como restrições em tipos para abstrações genéricas. O que significa que você obtém o poder das classes do tipo Haskell sem adicionar uma construção totalmente nova à linguagem.

@andrewcmyers Você pode dar um exemplo de como isso ficaria em Go?

A principal coisa sobre esse artigo que é relevante para Go é que ele mostra como usar interfaces tanto da maneira como elas são usadas para Go agora quanto como restrições em tipos para abstrações genéricas.

Meu entendimento é que a interface Go define uma restrição em um tipo (por exemplo, "este tipo pode ser comparado para igualdade usando a 'interface de tipo Comparable' porque satisfaz ter um método Eq"). Não tenho certeza se entendi o que você quer dizer com uma restrição de tipo.

Eu não estou familiarizado com Haskell, mas lendo uma visão geral rápida me fez adivinhar que os tipos que se encaixam em uma interface Go se encaixam nessa classe de tipo. Você pode explicar o que é diferente nas classes do tipo Haskell?

Uma comparação concreta entre Familia e Go seria interessante. Obrigado por compartilhar seu papel.

As interfaces Go podem ser vistas como descrevendo uma restrição nos tipos, por meio de subtipagem estrutural. No entanto, essa restrição de tipo, como está, não é expressiva o suficiente para capturar as restrições desejadas para programação genérica. Por exemplo, você não pode expressar a restrição de tipo chamada Eq no documento do Familia.

Algumas reflexões sobre a motivação para facilidades de programação mais genéricas em Go:

Portanto, há minha lista de teste genérica que realmente não precisa de nada adicionado à linguagem. Na minha opinião, o tipo genérico que propus não satisfaz o objetivo do Go de compreensão direta, não tem muito a ver com o termo de programação geralmente aceito, e fazer a afirmação de tipo não era feio, pois um pânico na falha é multar. Já estou satisfeito com as facilidades de programação genérica do Go para minha necessidade.

Mas sync.Map é um caso de uso diferente. Há uma necessidade na biblioteca padrão de uma implementação de mapa sincronizado genérico maduro além de apenas uma estrutura com um mapa e mutex. Para manipulação de tipo, podemos envolvê-lo com outro tipo que define um tipo{} não de interface e faz uma declaração de tipo, ou podemos adicionar uma verificação de reflexão internamente para que os itens que seguem o primeiro devem corresponder ao mesmo tipo. Ambos têm verificações de tempo de execução, o encapsulamento requer a reescrita de cada método para cada tipo de uso, mas adiciona uma verificação de tipo de tempo de compilação para entrada e oculta a declaração do tipo de saída, e com a verificação interna ainda temos que fazer uma declaração de tipo de saída de qualquer maneira. De qualquer forma, estamos fazendo conversões de interface sem nenhum uso real de interfaces; interface{} é um hack da linguagem e não ficará claro para novos programadores de Go. Embora json.Marshal seja um bom design na minha opinião (incluindo as tags struct feias, mas sensatas).

Acrescentarei que, como o sync.Map está na biblioteca padrão, o ideal é que ele troque a implementação pelos casos de uso medidos em que a estrutura simples tem melhor desempenho. O mapa não sincronizado é uma armadilha comum na programação concorrente Go e uma correção de biblioteca padrão deve funcionar.

O mapa regular tem apenas uma verificação de tipo em tempo de compilação e não requer nenhum desses andaimes. Eu argumento que sync.Map deve ser o mesmo ou não deve estar na biblioteca padrão para Go 2.

Propus adicionar sync.Map à lista de tipos internos e fazer o mesmo para futuras necessidades semelhantes. Mas meu entendimento é dar aos programadores Go uma maneira de fazer isso sem ter que trabalhar no compilador e passar pelo desafio de aceitação de código aberto é a ideia por trás dessa discussão. Na minha opinião, corrigir sync.Map é um caso real que define parcialmente o que essa proposta de genéricos deve ser.

Se você adicionar o sync.Map como um built-in, até onde você vai? você caso especial cada recipiente?
sync.Map não é o único contêiner e alguns são melhores para alguns casos do que outros.

@Azareal : @chowey listou estes em agosto:

Por fim, a biblioteca padrão está repleta de alternativas{} de interface em que os genéricos funcionariam com mais segurança:

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

Pode haver outros que eu estou perdendo. O ponto é que cada um dos itens acima é onde eu esperaria que os genéricos fossem úteis.

E eu gostaria do conjunto não ordenado para tipos que podem ser comparados para igualdade.

Eu gostaria de muito trabalho colocado em uma implementação de variável no tempo de execução para cada tipo com base em benchmarking para que a melhor implementação possível seja geralmente a que é usada.

Gostaria de saber se existem implementações alternativas razoáveis ​​com Go 1 que atinjam o mesmo objetivo para esses tipos de biblioteca padrão sem interface{} e sem genéricos.

interfaces golang e classes do tipo haskell superam duas coisas (que são muito boas!):

1.) (Type Constraint) Agrupam diferentes tipos com uma tag, o nome da interface
2.) (Dispatch) Eles oferecem despacho diferente em cada tipo para um determinado conjunto de funções via implementação de interface

Mas,

1.) Às vezes você quer apenas grupos anônimos como um grupo de int, float64 e string. Como você deve nomear essa interface, NumericandString?

2.) Muitas vezes, você não deseja despachar de forma diferente para cada tipo de interface, mas fornecer apenas um método para todos os tipos de interface listados (Talvez possível com métodos padrão de interfaces )

3.) Muitas vezes, você não deseja enumerar todos os tipos possíveis para um grupo. Em vez disso, você segue o caminho preguiçoso e diz que eu quero todos os tipos T implementando alguma Interface A e o compilador, em seguida, pesquisa todos os tipos em todos os arquivos de origem que você edita e em todas as bibliotecas que você usa para gerar as funções apropriadas em tempo de compilação.

Embora o último ponto seja possível via polimorfismo de interface, ele tem a desvantagem de ser um polimorfismo de tempo de execução envolvendo conversões e como você restringe a entrada de parâmetros de uma função para conter tipos que implementam mais de uma interface ou uma das muitas interfaces. O caminho certo é introduzir novas interfaces estendendo outras interfaces (por aninhamento de interface) para obter algo semelhante, mas não com as melhores práticas.

Por falar nisso.
Admito aos que dizem que go já tem polimorfismo e exatamente por isso go não é mais uma linguagem simples como C. É uma linguagem de programação de sistema de alto nível. Então, por que não expandir o polimorfismo que oferece.

Aqui está uma biblioteca que comecei hoje para tipos de conjuntos não ordenados genéricos: https://github.com/pciet/unordered

Isso fornece exemplos de documentação e teste que tipo wrapper pattern (obrigado @pierrre) para segurança de tipo em tempo de compilação e também tem a verificação de reflexão para segurança de tipo em tempo de execução.

Que necessidades existem para os genéricos? Minha atitude negativa em relação aos tipos genéricos de biblioteca padrão anteriormente se concentrava no uso de interface{}; minha reclamação pode ser resolvida com um tipo específico de pacote para interface{} (como type Item interface{} em pciet/unordered) que documenta as restrições inexprimíveis pretendidas.

Não vejo a necessidade de um recurso de idioma adicional quando apenas a documentação pode nos levar lá agora. Já existem grandes quantidades de código testado em batalha na biblioteca padrão que fornece recursos genéricos (consulte https://github.com/golang/go/issues/23077).

Seu código verifica o tipo em tempo de execução (e dessa perspectiva não é melhor do que apenas interface{} se não for pior). Com os genéricos, você poderia ter os tipos de coleção com verificações de tipo em tempo de compilação.

As verificações em tempo de execução do @zerkms podem ser desativadas definindo asserting = false (isso não iria na biblioteca padrão), há um padrão de uso para verificações em tempo de compilação e, de qualquer maneira, uma verificação de tipo apenas examina a estrutura da interface (usando interface adiciona mais despesas do que a verificação de tipo). Se a interface não estiver funcionando, você terá que escrever seu próprio tipo.

Você está dizendo que o código genérico de desempenho maximizado é uma necessidade fundamental. Não foi para meus casos de uso, mas talvez a biblioteca padrão possa se tornar mais rápida e talvez outros precisem de algo assim.

as verificações em tempo de execução podem ser desativadas definindo asserting = false

então nada garante a exatidão

Você está dizendo que o código genérico de desempenho maximizado é uma necessidade fundamental.

Eu não disse isso. A segurança de tipo seria um grande negócio. Sua solução ainda está infectada com interface{} .

mas talvez a biblioteca padrão possa se tornar mais rápida, e talvez outros precisem de algo assim.

pode ser, se a equipe de desenvolvimento principal estiver feliz em implementar o que eu precisar sob demanda e rapidamente.

@pciet

Não vejo a necessidade de um recurso de idioma adicional quando apenas a documentação pode nos levar lá agora.

Você diz isso, mas não tem problemas em usar os recursos genéricos da linguagem na forma de fatias e a função make.

Não vejo a necessidade de um recurso de idioma adicional quando apenas a documentação pode nos levar lá agora.

Então, por que se preocupar em usar uma linguagem de tipagem estática? Você pode usar uma linguagem tipada dinamicamente como Python e confiar na documentação para garantir que os tipos de dados corretos sejam enviados à sua API.

Acho que uma das vantagens do Go são as facilidades de impor algumas restrições pelo compilador para evitar bugs futuros. Esses recursos podem ser estendidos (com suporte a genéricos) para impor algumas outras restrições para evitar mais alguns bugs no futuro.

Você diz isso, mas não tem problemas em usar os recursos genéricos da linguagem na forma de fatias e a função make.

Estou dizendo que os recursos existentes nos levam a um bom ponto de equilíbrio que tem soluções de programação genéricas e deve haver fortes razões reais para mudar do sistema do tipo Go 1. Não como uma mudança melhoraria a linguagem, mas quais problemas as pessoas estão enfrentando agora, como manter muita alternância de tipo em tempo de execução para interface{} nos pacotes de biblioteca padrão fmt e banco de dados, que seriam corrigidos.

Então, por que se preocupar em usar uma linguagem de tipagem estática? Você pode usar uma linguagem tipada dinamicamente como Python e confiar na documentação para garantir que os tipos de dados corretos sejam enviados à sua API.

Ouvi sugestões para escrever sistemas em Python em vez de linguagens com tipagem estática e as organizações fazem.

A maioria dos programadores Go que usam a biblioteca padrão usa tipos que não podem ser completamente descritos sem documentação ou sem examinar a implementação. Tipos com subtipos paramétricos ou tipos gerais com restrições aplicadas apenas corrigem um subconjunto desses casos programaticamente e gerariam muito trabalho já feito na biblioteca padrão.

Na proposta para os tipos de soma sugeri um recurso de construção para o switch do tipo de interface onde uma interface usada em uma função ou método tem um erro de compilação emitido quando um possível valor atribuído à interface não corresponde a nenhum caso de switch do tipo de interface contido.

Uma função/método que recebe uma interface pode rejeitar alguns tipos na compilação por não ter nenhum caso padrão e nenhum caso para o tipo. Isso parece uma adição de programação genérica razoável se o recurso for viável de implementar.

Se as interfaces Go pudessem capturar o tipo do implementador, poderia haver uma forma de genéricos que fosse completamente compatível com a sintaxe Go atual - uma forma de parâmetro único de genéricos ( demonstração ).

@dc0d para tipos de contêiner genéricos, acredito que esse recurso adiciona verificação de tipo em tempo de compilação sem exigir um tipo de wrapper: https://gist.github.com/pciet/36a9dcbe99f6fb71f5fc2d3c455971e5

@pciet Você está certo. No código fornecido, nº 4, o exemplo indica que o tipo é capturado para fatias e canais (e matrizes). Mas não para mapas, porque existe apenas um parâmetro de tipo: o implementador. E como um mapa precisa de dois parâmetros de tipo, são necessárias interfaces de wrapper.

BTW eu tenho que enfatizar o propósito demonstrativo desse código, como uma linha de pensamento. Eu não sou nenhum designer de linguagem. Esta é apenas uma maneira hipotética de pensar sobre a implementação de genéricos em Go:

  • Compatível com Go atual
  • Simples (parâmetro de tipo genérico único, que _sente_ como _this_ em outro OO, referindo-se ao implementador atual)

A discussão de generalidade e todos os casos de uso possíveis no contexto de um desejo de minimizar impactos enquanto maximiza casos de uso importantes e flexibilidade de expressão é uma análise muito complexa. Não tenho certeza se algum de nós será capaz de destilá-lo em um pequeno conjunto de princípios, também conhecido como essência generativa. Estou tentando. De qualquer forma, aqui estão alguns dos meus pensamentos iniciais da minha leitura _curiosa_ deste tópico…

@adg escreveu:

Acompanhando esta edição está uma proposta genérica geral de @ianlancetaylor que inclui quatro propostas falhas específicas de mecanismos genéricos de programação para Go.

Afaics, a seção vinculada extraída a seguir não indica um caso de falta de generalidade nas interfaces atuais, _“Não há como escrever um método que receba uma interface para o chamador fornecido tipo T, para qualquer T, e retorne um valor de o mesmo tipo T.”_.

Não há como escrever uma interface com um método que receba um argumento do tipo T, para qualquer T, e retorne um valor do mesmo tipo.

Então, de que outra forma o código no tipo de site de chamada pode verificar se ele tem um tipo T como valor de resultado? Por exemplo, a referida interface pode ter um método de fábrica para construir o tipo T. É por isso que precisamos parametrizar as interfaces no tipo T.

Interfaces não são simplesmente tipos; também são valores. Não há como usar tipos de interface sem usar valores de interface e os valores de interface nem sempre são eficientes.

Concordamos que, como as interfaces não podem ser parametrizadas explicitamente no tipo T em que operam, o tipo T não é acessível ao programador.

Então, é isso que os limites de tipo de classe fazem no site de definição de função, tomando como entrada um tipo T e tendo uma cláusula where ou requires declarando a(s) interface(s) necessária(s) para o tipo T. Em muitas circunstâncias esses dicionários de interface podem ser monomorfizados automaticamente em tempo de compilação para que nenhum ponteiro de dicionário (para as interfaces) seja passado para a função em tempo de execução (monomorfização que presumo que o compilador Go se aplica a interfaces atualmente?). Por 'valores' na citação acima, presumo que ele se refira ao tipo de entrada T e não ao dicionário de métodos para o tipo de interface implementado pelo tipo T.

Se permitirmos parâmetros de tipo em tipos de dados (por exemplo struct ), então o tipo T acima pode ser parametrizado para que realmente tenhamos um tipo T<U> . Fábricas para esses tipos que precisam reter o conhecimento de U são chamadas de tipos superiores (HKT) .

Os genéricos permitem recipientes polimórficos de tipo seguro.

Cf também a questão dos contêineres _heterogêneos_ discutida abaixo. Portanto, por polimórfico queremos dizer a generalidade do tipo de valor do contêiner (por exemplo, tipo de elemento da coleção), mas também há a questão de saber se podemos colocar mais de um tipo de valor no contêiner simultaneamente, tornando-os heterogêneos.


@tamir escreveu:

Esses requisitos parecem excluir, por exemplo, um sistema semelhante ao sistema de traços de Rust, onde tipos genéricos são restringidos por limites de traços.

Os limites de traço de Rust são essencialmente limites de classe de tipo.

@alex escreveu:

Traços de ferrugem. Embora eu ache que eles são um bom modelo em geral, eles seriam um mau ajuste para o Go como existe hoje.

Por que você acha que eles se encaixam mal? Talvez você esteja pensando nos objetos trait que empregam o despacho em tempo de execução, portanto, têm menos desempenho do que o monomorfismo? Mas esses podem ser considerados separadamente do princípio de generalidade dos limites da classe de tipos (cf minha discussão sobre contêineres/coleções heterogêneas abaixo). Afaics, as interfaces do Go já são limites do tipo trait e cumprem o objetivo das typeclasses que é vincular os dicionários aos tipos de dados no site da chamada, em vez do anti-padrão de OOP que se vincula antecipadamente (mesmo que ainda em compilação time) dicionários para tipos de dados (na instanciação/construção). As classes de tipo podem (pelo menos uma melhoria parcial dos graus de liberdade) resolver o problema de expressão que a OOP não pode.

@jimmyfrasche escreveu:

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

Eu concordo com o link acima que typeclasses de fato não são subtipagem e não estão expressando nenhum relacionamento de herança. E concordo em não confundir desnecessariamente “genericidade” (como um conceito mais geral de reutilização ou modularidade do que polimorfismo paramétrico) com herança, como faz a subclasse.

No entanto, também quero salientar que as hierarquias de herança (também conhecidas como subtipagem) são inevitáveis 1 na atribuição de (entradas de função) e de (saídas de função) se a linguagem suportar uniões e interseções, porque, por exemplo, um int ν string pode aceitar uma atribuição de um int ou um string mas nenhum deles pode aceitar uma atribuição de um int ν string . Sem as uniões afaik, as únicas maneiras alternativas de fornecer contêineres/coleções heterogêneas estaticamente tipadas são subclasses ou polimorfismo existencialmente limitado (também conhecido como objetos de traço em Rust e quantificação existencial em Haskell). Os links acima contêm discussões sobre as compensações entre existenciais e sindicatos. Afaik, a única maneira de fazer contêineres/coleções heterogêneos em Go agora é subsumir todos os tipos a um interface{} vazio que está jogando fora as informações de digitação e eu presumo exigir casts e inspeção de tipo de tempo de execução, que tipo de 2 derrota o ponto de tipagem estática.

O “anti-padrão” a ser evitado é a subclasse, também conhecida como herança virtual (veja também “EDIT#2” sobre os problemas com subsunção implícita e igualdade, etc).

1 Independentemente de serem combinados estruturalmente ou nominalmente porque a subtipagem é devido ao Princípio de Substituição de Liskov baseado em conjuntos comparativos e na direção de atribuição com entradas de função opostas aos valores de retorno, por exemplo, um parâmetro de tipo de struct ou interface não pode residir nas entradas da função e nos valores de retorno, a menos que seja invariante em vez de co- ou contra-variante.

2 O absolutismo não se aplica porque não podemos verificar o universo do não-determinismo ilimitado. Então, como eu entendo que seja, este tópico é sobre a escolha de um limite ótimo (“ponto ideal”) para o nível de declaração de digitação wrt para os problemas de generalidade.

@andrewcmyers escreveu:

Ao contrário dos genéricos Java e C#, o mecanismo Genus genéricos não é baseado em subtipagem.

É a herança e a subclassificação ( não a subtipagem estrutural ) que é o pior antipadrão que você não deseja copiar de Java, Scala, Ceilão e C++ (não relacionado aos problemas com modelos C++ ).

@thwd escreveu:

O expoente na medida de complexidade dos tipos parametrizados é a variância. Os tipos de Go (exceto interfaces) são invariáveis ​​e isso pode e deve ser mantido como regra.

A subtipagem com imutabilidade evita a complexidade da covariância. A imutabilidade também melhora alguns dos problemas com subclasses (por exemplo Rectangle vs. Square ), mas não outros (por exemplo, subsunção implícita, igualdade, etc).

@bxqgit escreveu:

Sintaxe simples e sistema de tipos são as vantagens importantes do Go. Se você adicionar genéricos, a linguagem se tornará uma bagunça feia como Scala ou Haskell.

Observe que Scala tenta mesclar OOP, subclassing, FP, módulos genéricos, HKT e typeclasses (via implicit ) em um PL. Talvez as classes de tipos sozinhas sejam suficientes.

Haskell não é necessariamente obtuso por causa dos genéricos typeclass, mas mais provavelmente porque está impondo funções puras em todos os lugares e empregando a teoria da categoria monádica para modelar efeitos imperativos controlados.

Assim, acho que não é correto associar a obtusidade e complexidade desses PLs com typeclasses em, por exemplo, Rust. E não vamos culpar as classes de tipos pelas vidas de Rust + abstração de empréstimo de mutabilidade exclusiva.

Afaics, na seção Semantics dos _Type Parameters in Go_, o problema encontrado por @ianlancetaylor é uma questão de conceituação porque ele (afaics) aparentemente reinventando typeclasses involuntariamente:

Podemos juntar SortableSlice e PSortableSlice para ter o melhor dos dois mundos? Não exatamente; não há como escrever uma função parametrizada que suporte um tipo com um método Less ou um tipo interno. O problema é que SortableSlice.Less não pode ser instanciado para um tipo sem um método Less , e não há como instanciar um método apenas para alguns tipos, mas não para outros.

A cláusula requires Less[T] para o limite de typeclass (mesmo se implicitamente inferido pelo compilador) no método Less para []T está em T não []T . A implementação da typeclass Less[T] (que contém um método Less ) para cada T fornecerá uma implementação no corpo da função do método ou atribuirá o < função interna como a implementação. No entanto, acredito que isso requer HKT U[T] se os métodos de Sortable[U] precisarem de um parâmetro de tipo U representando o tipo de implementação, por exemplo, []T . Afair @keean tem outra maneira de estruturar uma classificação empregando uma typeclass separada para o tipo de valor T que não requer um HKT.

Observe que esses métodos para []T podem estar implementando uma typeclass Sortable[U] , onde U é []T .

(Técnico à parte: pode parecer que poderíamos mesclar SortableSlice e PSortableSlice tendo algum mecanismo para instanciar apenas um método para alguns argumentos de tipo, mas não para outros. No entanto, o resultado seria sacrificar a compilação -time segurança de tipo, pois usar o tipo errado levaria a um pânico em tempo de execução. Em Go, já é possível usar tipos e métodos de interface e declarações de tipo para selecionar o comportamento em tempo de execução. Não há necessidade de fornecer outra maneira de fazer isso usando parâmetros de tipo .)

A seleção da typeclass vinculada no site de chamada é resolvida em tempo de compilação para um T estaticamente conhecido. Se for necessário despacho dinâmico heterogêneo, veja as opções que expliquei no meu post anterior.

Espero que @keean encontre tempo para vir aqui e ajudar a explicar typeclasses, pois ele é mais especialista e me ajudou a aprender esses conceitos. Posso ter alguns erros na minha explicação.

Nota PS para aqueles que já leram meu post anterior, note que eu o editei extensivamente cerca de 10 horas depois de postá-lo (depois de um pouco de sono) para tornar os pontos sobre contêineres heterogêneos mais coerentes.


A seção Ciclos parece estar incorreta. A construção em tempo de execução da instância S[T]{e} de um struct não tem nada a ver com a seleção da implementação da função genérica chamada. Ele provavelmente está pensando que o compilador não sabe se está especializando a implementação da função genérica para o tipo dos argumentos, mas todos esses tipos são conhecidos em tempo de compilação.

Talvez a especificação da seção Verificação de tipo possa ser simplificada estudando o conceito de @keean de um grafo conectado de tipos distintos como nós para um algoritmo de unificação. Quaisquer tipos distintos conectados por uma aresta devem ter tipos congruentes, com arestas criadas para quaisquer tipos que se conectam por atribuição ou de outra forma no código-fonte. Se houver união e interseção (do meu post anterior), então a direção de atribuição deve ser levada em consideração (de alguma forma? ). Cada tipo desconhecido distinto começa com um mínimo de limites superiores (LUB) de Top e um maior limite inferior (GLB) de Bottom e, em seguida, as restrições podem alterar esses limites. Os tipos conectados precisam ter limites compatíveis. As restrições devem ser todos limites de tipo de classe.

Em Implementação :

Por exemplo, sempre é possível implementar funções parametrizadas gerando uma nova cópia da função para cada instanciação, onde a nova função é criada substituindo os parâmetros de tipo pelos argumentos de tipo.

Acredito que o termo técnico correto seja monomorfização .

Essa abordagem renderia o tempo de execução mais eficiente ao custo de tempo de compilação extra considerável e tamanho de código maior. É provável que seja uma boa escolha para funções parametrizadas que são pequenas o suficiente para serem incorporadas, mas seria uma troca ruim na maioria dos outros casos.

A criação de perfil informaria ao programador quais funções podem se beneficiar mais da monomorfização. Talvez o otimizador Java Hotspot faça otimização de monomorfização em tempo de execução?

@egonelbre escreveu:

Há o Resumo das Discussões Genéricas do Go , que tenta fornecer uma visão geral das discussões de diferentes lugares.

A seção Visão geral parece implicar que o uso universal de referências boxing em Java para instâncias em um contêiner é o único eixo de design que se opõe diametralmente à monomorfização de templates do C++. Mas os limites de tipo de classe (que também podem ser implementados com modelos C++ , mas sempre monomorfizados) são aplicados a funções e não a parâmetros de tipo de contêiner. Assim afaics a visão geral está faltando o eixo de projeto para typeclasses onde podemos escolher se monomorfizar cada função limitada de typeclass. Com typeclasses nós sempre tornamos os programadores mais rápidos (menos clichê) e podemos obter um equilíbrio mais refinado entre tornar os compiladores/execução mais rápidos/mais lentos e o inchaço do código maior/menor. De acordo com o meu post anterior, talvez o ideal seria se a escolha das funções para monomorfizar fosse orientada pelo perfilador (automaticamente ou mais provavelmente por anotação).

Na seção Problemas: Estruturas de Dados Genéricas :

Contras

  • Estruturas genéricas tendem a acumular recursos de todos os usos, resultando em tempos de compilação maiores ou em excesso de código ou na necessidade de um vinculador mais inteligente.

Para typeclasses, isso não é verdade ou é um problema menor, porque as interfaces só precisam ser implementadas para tipos de dados fornecidos a funções que usam essas interfaces. Typeclasses são sobre a vinculação tardia da implementação à interface, ao contrário da OOP, que vincula todos os tipos de dados aos seus métodos para a implementação class .

Além disso, nem todos os métodos precisam ser colocados em uma única interface. A cláusula requires (mesmo se implicitamente inferida pelo compilador) em uma typeclass vinculada para uma declaração de função pode misturar e combinar interfaces necessárias.

  • As estruturas genéricas e as APIs que operam nelas tendem a ser mais abstratas do que as APIs criadas especificamente, o que pode impor uma carga cognitiva aos chamadores

Um contra-argumento que acho que melhora significativamente essa preocupação é que a carga cognitiva de aprender um número ilimitado de reimplementações de casos especiais dos algoritmos genéricos essencialmente iguais é ilimitada. Considerando que, aprender as APIs genéricas abstratas é limitado.

  • As otimizações detalhadas são muito não genéricas e específicas do contexto, portanto, é mais difícil otimizá-las em um algoritmo genérico.

Este não é um contra válido. A regra 80/20 diz para não adicionar complexidade ilimitada (por exemplo, otimização prematura) para código que, quando perfilado, não a exige. O programador é livre para otimizar em 20% dos casos, enquanto os 80% restantes são tratados pela complexidade limitada e carga cognitiva das APIs genéricas.

O que realmente estamos chegando aqui é a regularidade de uma linguagem e APIs genéricas ajudam, não prejudicam isso. Esses contras realmente não são conceituados corretamente.

Soluções alternativas:

  • usar estruturas mais simples em vez de estruturas complicadas

    • por exemplo, use map[int]struct{} em vez de Set

Rob Pike (e eu também o vi fazer esse ponto no vídeo) parece estar perdendo o ponto de que os contêineres genéricos não são suficientes para criar funções genéricas. Precisamos desse T em map[T] para que possamos passar o tipo de dados genérico em funções para entradas, saídas e para nosso próprio struct . Generics apenas em parâmetros de tipo de contêiner é totalmente insuficiente para expressar APIs genéricas e APIs genéricas são necessárias para complexidade limitada e carga cognitiva e obtenção de regularidade em um ecossistema de linguagem. Também não vi o aumento do nível de refatoração (portanto, a composição reduzida de módulos que não podem ser facilmente refatorados) que o código não genérico requer, que é o problema da expressão que mencionei no meu primeiro post.

Na seção Abordagens genéricas :

Modelos de pacote
Esta é uma abordagem usada por Modula-3, OCaml, SML (os chamados “functors”) e Ada. Em vez de especificar um tipo individual para especialização, todo o pacote é genérico. Você especializa o pacote corrigindo os parâmetros de tipo ao importar.

Posso estar enganado, mas isso não parece muito correto. Os functores ML (não devem ser confundidos com os functores FP) também podem retornar uma saída que permanece parametrizada por tipo. Caso contrário, não haveria como usar os algoritmos em outras funções genéricas, portanto, os módulos genéricos não seriam capazes de reutilizar (importando com tipos concretos para) outros módulos genéricos. Isso parece ser uma tentativa de simplificar demais e, em seguida, perder completamente o ponto de genéricos, reutilização de módulos, etc.

Em vez disso, meu entendimento é que essa parametrização de tipo de pacote (também conhecido como módulo) permite a capacidade de aplicar parâmetros de tipo a um agrupamento de struct , interface e func .

Sistema de tipos mais complicado
Esta é a abordagem que Haskell e Rust adotam.
[…]
Contras:

  • difícil de encaixar em linguagem mais simples (https://groups.google.com/d/msg/golang-nuts/smT_0BhHfBs/MWwGlB-n40kJ)

Citando @ianlancetaylor no documento vinculado:

Se você acredita nisso, então vale a pena ressaltar que o núcleo do
map e código de fatia no tempo de execução Go não é genérico no sentido de
usando polimorfismo de tipo. É genérico no sentido de que olha para
digite informações de reflexão para ver como mover e comparar o tipo
valores. Então temos prova por existência que é aceitável escrever
código "genérico" em Go escrevendo código não polimórfico que usa tipo
informações de reflexão de forma eficiente e, em seguida, envolver esse código em
clichê seguro em tempo de compilação (no caso de mapas e fatias
esta placa de caldeira é, obviamente, fornecida pelo compilador).

E é isso que um compilador transpilando de um superconjunto de Go com genéricos adicionados, produziria como código Go. Mas o embrulho não seria baseado em algum delineamento como pacote, pois faltaria a composabilidade que já mencionei. O ponto é que não há atalho para um bom sistema de tipos genéricos combináveis. Ou fazemos isso corretamente ou não fazemos nada, porque adicionar algum hack não combinável que não seja realmente genérico criará eventualmente uma inércia de retalhos de genericidade meia-boca de retalhos e irregularidade de casos de canto e soluções alternativas tornando o código do ecossistema Go ininteligível.

Também é verdade que a maioria das pessoas que escrevem grandes programas complexos em Go tem
não encontrou uma necessidade significativa de genéricos. Até agora tem sido mais como
uma verruga irritante - a necessidade de escrever três linhas de clichê para
cada tipo a ser classificado - em vez de uma grande barreira para escrever
código.

Sim, esse tem sido um dos pensamentos em minha mente sobre se ir para um sistema de classes de tipos completo é justificável. Se todas as suas bibliotecas são baseadas em torno dele, então, aparentemente, pode ser uma bela harmonia, mas se estamos contemplando a inércia dos hacks Go existentes para generalidade, talvez a sinergia adicional obtida seja baixa para muitos projetos ?

Mas se um transpilador de uma sintaxe typeclass emular a maneira manual existente Go pode modelar genéricos (Edit: que acabei de ler que @andrewcmyers afirma é plausível ), isso pode ser menos oneroso e encontrar sinergias úteis. Por exemplo, percebi que duas classes de tipo de parâmetro podem ser emuladas com interface implementado em um struct que emula uma tupla, ou @jba mencionou uma ideia para empregar interface inline no contexto . Aparentemente struct são estruturalmente em vez de nominalmente digitados a menos que seja dado um nome com type ? Também confirmei que um método de interface pode inserir outro interface para que seja possível transpilar de HKT no seu exemplo de classificação sobre o qual escrevi no meu post anterior aqui. Mas preciso pensar mais nisso quando não estiver com tanto sono.

Acho justo dizer que a maioria da equipe Go não gosta de C++
templates, nos quais uma linguagem Turing completa foi sobreposta
outra linguagem Turing completa tal que as duas linguagens tenham
sintaxes completamente diferentes, e programas em ambas as linguagens são
escrito de maneiras muito diferentes. Os modelos C++ servem como advertência
conto porque a implementação complexa permeou todo o
biblioteca padrão, fazendo com que as mensagens de erro C++ se tornem uma fonte de
maravilha e espanto. Este não é um caminho que Go jamais seguirá.

Duvido que alguém discorde! O benefício da monomorfização é ortogonal às desvantagens de um mecanismo de metaprogramação genérico completo de Turing.

Aliás, o erro de design dos modelos C++ me parece ser a mesma essência generativa da falha dos functores ML generativos (em oposição aos aplicativos). Aplica-se o Princípio da Menor Potência.


@ianlancetaylor escreveu:

É decepcionante ver o Go se tornando cada vez mais complexo ao adicionar tipos parametrizados embutidos.

Apesar da especulação nesta edição, acho extremamente improvável que isso aconteça.

Espero que sim. Acredito firmemente que o Go deveria adicionar um sistema genérico coerente ou simplesmente aceitar que nunca terá genéricos.

Acho que uma bifurcação para um transpilador é mais provável de acontecer, em parte porque tenho financiamento para implementá-lo e estou interessado em fazê-lo. Mas ainda estou analisando a situação.

Isso fraturaria o ecossistema, mas pelo menos o Go pode permanecer puro em seus princípios minimalistas. Assim, para evitar fraturar o ecossistema e permitir algumas outras inovações que eu gostaria, provavelmente não faria um superconjunto e o nomearia Zero .

@pciet escreveu:

Meu voto é não para genéricos de aplicativos generalizados, sim para funções genéricas mais internas como append e copy que funcionam em vários tipos de base. Talvez sort e search possam ser adicionados para os tipos de coleção?

Expandir essa inércia talvez impeça que um recurso genérico abrangente chegue ao Go. Aqueles que queriam genéricos provavelmente partiriam para pastos mais verdes. @andrewcmyers reiterou isso:

Seria decepcionante ver o Go se tornando cada vez mais complexo adicionando tipos parametrizados embutidos. Seria melhor apenas adicionar o suporte à linguagem para programadores escreverem seus próprios tipos parametrizados.

@shelby3

Afaik, a única maneira de fazer contêineres/coleções heterogêneos em Go agora é subsumir todos os tipos a uma interface vazia{} que está jogando fora as informações de digitação e eu presumo exigir casts e inspeção de tipo de tempo de execução, o que meio que anula o ponto de digitação estática.

Consulte o padrão de wrapper nos comentários acima para verificação de tipo estático de coleções{} de interface em Go.

O ponto é que não há atalho para um bom sistema de tipos genéricos combináveis. Ou fazemos corretamente ou não fazemos nada, porque adicionar algum hack não combinável que não é realmente genérico…

Você pode explicar isso mais? Para o caso de tipos de coleção, ter uma interface definindo o comportamento genérico necessário dos itens contidos parece razoável para escrever funções.

@pciet este código está literalmente fazendo exatamente o que @shelby3 estava descrevendo e considerando um antipadrão. Citando você de antes:

Isso fornece exemplos de documentação e teste que tipo wrapper pattern (obrigado @pierrre) para segurança de tipo em tempo de compilação e também tem a verificação de reflexão para segurança de tipo em tempo de execução.

Você está pegando código que não tem informações de tipo e, tipo por tipo, adicionando casts e inspeção de tipo de tempo de execução usando reflect. Isso é exatamente o que @shelby3 estava reclamando. Costumo chamar essa abordagem de "monomorfização manual" e é exatamente o tipo de tarefa tediosa que acho que é melhor para um compilador.

Esta abordagem tem uma série de desvantagens:

  • Requer wrappers tipo por tipo, mantidos manualmente ou com uma ferramenta semelhante a go generate
  • (Se feito à mão em vez de uma ferramenta) oportunidade de cometer erros no clichê que não serão detectados até o tempo de execução
  • Requer despacho dinâmico em vez de despacho estático, que é mais lento e usa mais memória
  • Usa reflexão de tempo de execução em vez de asserções de tipo de tempo de compilação, que também são lentas
  • Não combinável: atua inteiramente em tipos concretos sem oportunidades de usar limites do tipo typeclass (ou mesmo do tipo interface) em tipos, a menos que você passe outra camada de indireção para cada interface não vazia sobre a qual você também deseja abstrair

Você pode explicar isso mais? Para o caso de tipos de coleção, ter uma interface definindo o comportamento genérico necessário dos itens contidos parece razoável para escrever funções.

Agora, em todos os lugares em que você deseja usar um limite em vez de ou além de um tipo concreto, também é necessário escrever o mesmo clichê de verificação de tipos para cada tipo de interface. Ele apenas compõe ainda mais a explosão (talvez combinatória) de wrappers de tipo estático que você precisa escrever.

Existem também ideias que, até onde eu sei, simplesmente não podem ser expressas no sistema de tipos do Go hoje, como um limite em uma combinação de interfaces. Imagine que temos:

type Foo interface {
    ...
}

type Bar interface {
    ...
}

Como expressamos, usando uma verificação de tipo puramente estática, que queremos um tipo que implemente Foo e Bar ? Até onde eu sei, isso não é possível em Go (exceto recorrer a verificações de tempo de execução que podem falhar, evitando a segurança do tipo estático).

Com um sistema genérico baseado em typeclass podemos expressar isso como:

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

@tarcieri

Como expressamos, usando uma verificação de tipo puramente estática, que queremos um tipo que implemente Foo e Bar?

simplesmente assim:

type T interface {
    Foo
    Bar
}

func baz(t T) { ... }

@sbinet puro, TIL

Pessoalmente, considero a reflexão de tempo de execução um recurso errado, mas isso sou só eu... Posso explicar o motivo se alguém estiver interessado.

Acho que qualquer um que implemente genéricos de qualquer tipo deveria ler primeiro os "Elementos de Programação" de Stepanov várias vezes. Isso evitaria muitos problemas de Not Invented Here e reinventaria a roda. Depois de ler isso, deve ficar claro por que "C++ Concepts" e "Haskell Typeclasses" são o caminho certo para fazer genéricos.

Vejo que este problema parece ativo novamente
Aqui está um playground de proposta de espantalho
https://go-li.github.io/test.html
basta colar programas de demonstração daqui
https://github.com/go-li/demo

Muito obrigado por sua avaliação deste único parâmetro
funções genéricas.

Mantemos o gccgo hackeado e
este projeto seria impossível sem você, então nós
queria contribuir de volta.

Também estamos ansiosos para quaisquer genéricos que você adote, continue com o ótimo trabalho!

@anlhor onde estão os detalhes de implementação sobre isso? Onde se pode ler sobre a sintaxe? O que é implementado? O que não é implementado? Quais são as especificações para essas implementações? Quais são os prós e contras disso?

O link do playground contém o pior exemplo possível disso:

package main

import "fmt"

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

Esse código não me diz nada sobre como usá-lo e o que posso testar.

Se você pudesse melhorar essas coisas, ajudaria a entender melhor qual é a sua proposta e como ela se compara às anteriores / ver como os outros pontos levantados aqui se aplicam ou não a ela.

Espero que isso ajude você a entender os problemas com seu comentário.

@joho escreveu:

Haveria valor na literatura acadêmica para qualquer orientação sobre a avaliação de abordagens?

O único artigo que li sobre o assunto é Os desenvolvedores se beneficiam de tipos genéricos ? (paywall desculpe, você pode pesquisar no Google seu caminho para um download de pdf) que tinha o seguinte a dizer

Consequentemente, uma interpretação conservadora do experimento
é que os tipos genéricos podem ser considerados como uma compensação
entre as características positivas da documentação e a
características de extensibilidade negativa.

Presumo que OOP e subclasses (por exemplo, classes em Java e C++) não serão considerados seriamente porque Go já tem um tipo de tipo interface (sem o parâmetro de tipo genérico T explícito), Java é citado como o que não deve ser copiado, e porque muitos argumentaram que eles são um antipadrão. Upthread eu vinculei a alguns desses argumentos. Poderíamos aprofundar essa análise se alguém estiver interessado.

Eu ainda não estudei pesquisas mais recentes, como o sistema Genus mencionado upthread . Estou desconfiado de sistemas de “pia de cozinha” que tentam misturar tantos paradigmas (por exemplo, subclasses, herança múltipla, OOP, linearização de traços, implicit , typeclasses, tipos abstratos, etc), devido às reclamações sobre Scala ter tantos casos de canto na prática, embora talvez isso melhore com o Scala 3 (também conhecido como Dotty e o cálculo DOT). Estou curioso para saber se a tabela de comparação deles está comparando com o Scala 3 experimental ou a versão atual do Scala?

Então, afaics, o que resta são functores ML e classes de tipos Haskell em termos de sistemas de genéricos comprovados, que melhoram significativamente a extensibilidade e a flexibilidade em comparação com OOP + subclasses.

Eu escrevi algumas das discussões privadas que @keean e eu tivemos sobre módulos functor ML versus typeclasses. Os destaques parecem ser:

  • typeclasses _modela uma álgebra_ (mas sem axiomas verificados ) e implementa cada tipo de dados para cada interface apenas de uma maneira. Permitindo assim a seleção implícita das implementações pelo compilador sem anotação no site da chamada.

  • Os functores de aplicativo têm transparência referencial, enquanto os functores generativos criam uma nova instância em cada instanciação, o que significa que eles não são invariantes na ordem de inicialização.

  • Functores de ML são mais poderosos/flexíveis do que typeclasses, mas isso tem o custo de mais anotações e potencialmente mais interações de casos de canto. E de acordo com @keean , eles exigem tipos dependentes (para tipos associados ), que é um sistema de tipos mais complexo. @keean acha que a _expressão de genericidade de Stepanov como uma álgebra_ plus typeclasses é suficientemente poderosa e flexível , de modo que parece ser o ponto ideal para a genericidade de ponta e comprovada (em Haskell e agora em Rust). No entanto, os axiomas não são impostos por typeclasses.

  • Eu sugeri adicionar uniões para contêineres heterogêneos com typeclasses para estender ao longo de outro eixo do Problema de Expressão, embora isso exija imutabilidade ou cópia (apenas para os casos em que a extensibilidade heterogênea é empregada) que é conhecida por ter um O(log n) desaceleração em comparação com a imperatividade mutável irrestrita.

@larsth escreveu:

Pode ser interessante ter um ou mais transpiladores experimentais - um compilador de código-fonte Go genéricos para Go 1.xy.

PS Duvido que o Go adote um sistema de digitação tão sofisticado, mas estou contemplando um transpilador para a sintaxe Go existente, como mencionei no meu post anterior (veja a edição na parte inferior). E eu quero um sistema genérico robusto junto com esses recursos Go muito desejáveis. Genéricos Typeclass em Go parece ser o que eu quero.

@bcmills escreveu sobre sua proposta sobre funções de tempo de compilação para generalidade:

Eu ouvi esse mesmo argumento usado para defender a exportação de tipos interface em vez de tipos concretos em APIs Go, e o inverso acaba sendo mais comum: a abstração prematura restringe os tipos e dificulta a extensão das APIs. (Para um exemplo, veja #19584.) Se você quiser confiar nessa linha de argumento, acho que precisa fornecer alguns exemplos concretos.

Certamente é verdade que as abstrações do sistema de tipos necessariamente abandonam alguns graus de liberdade e, às vezes, escapamos dessas restrições com “inseguro” (ou seja, em violação da abstração verificada estaticamente), mas isso deve ser compensado pelos benefícios de desacoplamento modular com invariantes anotados sucintamente.

Ao projetar um sistema para a generalidade, provavelmente desejamos aumentar a regularidade e a previsibilidade do ecossistema como um dos principais objetivos, especialmente se a filosofia central do Go for levada em consideração (por exemplo, programadores médios são uma prioridade).

Aplica-se o Princípio da Menor Potência. O poder/flexibilidade das funções de tempo de compilação “escondidas em” para generalidade tem que ser ponderado em relação à sua capacidade de prejudicar, por exemplo, a legibilidade do código-fonte no ecossistema (onde o desacoplamento modular é extremamente importante porque o leitor não 'não precisa ler uma quantidade de código potencialmente ilimitada devido a dependências transitivas implícitas, para entender um determinado módulo/pacote!). A resolução implícita de instâncias de implementação de typeclass tem esse problema se sua álgebra não for respeitada .

Claro, mas isso já é verdade para muitas restrições implícitas em Go, independentemente de qualquer mecanismo de programação genérico.

Por exemplo, uma função pode receber um parâmetro do tipo interface e inicialmente chamar seus métodos sequencialmente. Se essa função for alterada posteriormente para chamar esses métodos simultaneamente (gerando goroutines adicionais), a restrição "deve ser segura para uso simultâneo" não será refletida no sistema de tipos.

Mas o afaik Go não tentou projetar uma abstração para modular esses efeitos. Rust tem essa abstração (que, aliás, acho que é um exagero pita/tsuris/limiting para alguns/a maioria dos casos de uso e defendo uma abstração de modelo de thread único mais fácil, mas infelizmente o Go não suporta a restrição de todas as goroutines geradas ao mesmo thread ) . E Haskell requer controle monádico sobre os efeitos devido à imposição de funções puras para transparência referencial .


@alerca escreveu:

Acho que a maior desvantagem das restrições inferidas é que elas facilitam o uso de um tipo de uma maneira que introduz uma restrição sem entendê-la completamente. Na melhor das hipóteses, isso significa apenas que seus usuários podem se deparar com falhas inesperadas em tempo de compilação, mas na pior das hipóteses, isso significa que você pode quebrar o pacote para os consumidores introduzindo uma nova restrição inadvertidamente. Restrições especificadas explicitamente evitariam isso.

Acordado. Ser capaz de quebrar código clandestinamente em outros módulos porque os invariantes dos tipos não são explicitamente anotados é notoriamente insidioso.


@andrewcmyers escreveu:

Para ser claro, não considero a geração de código no estilo macro, seja feita com gen, cpp, gofmt -r ou outras ferramentas de macro/modelo, como uma boa solução para o problema dos genéricos, mesmo que padronizada. Ele tem os mesmos problemas que os modelos C++: código bloat, falta de verificação de tipo modular e dificuldade de depuração. Fica pior quando você começa, como é natural, construir código genérico em termos de outro código genérico. Na minha opinião, as vantagens são limitadas: manteria a vida relativamente simples para os escritores do compilador Go e produz código eficiente - a menos que haja pressão de cache de instruções, uma situação frequente em software moderno!

@keean parece concordar com você.

@shelby3 obrigado pelos comentários. Você pode da próxima vez fazer os comentários/edições diretamente no próprio documento. É mais fácil rastrear onde as coisas precisam ser corrigidas e mais fácil garantir que todas as notas recebam uma resposta adequada.

A seção Visão geral parece implicar que o uso universal de referências de boxing do Java para instâncias ...

Adicionado comentário para deixar claro que não se trata de uma lista abrangente. Está lá principalmente para que as pessoas entendam a essência de diferentes trade-offs. A lista completa de diferentes abordagens está mais abaixo.

Estruturas genéricas tendem a acumular recursos de todos os usos, resultando em tempos de compilação maiores ou em excesso de código ou na necessidade de um vinculador mais inteligente.
Para typeclasses, isso não é verdade ou é um problema menor, porque as interfaces só precisam ser implementadas para tipos de dados fornecidos a funções que usam essas interfaces. Typeclasses são sobre a vinculação tardia da implementação à interface, ao contrário da OOP, que vincula todos os tipos de dados aos seus métodos para a implementação da classe.

Essa afirmação é sobre o que acontece com estruturas de dados genéricas a longo prazo. Em outras palavras, uma estrutura de dados genérica geralmente acaba coletando todos os diferentes usos - em vez de ter várias implementações menores para diferentes propósitos. Apenas como exemplo, veja https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html.

É importante notar que, apenas o "design mecânico" e "quanta flexibilidade" não é suficiente para criar uma boa "solução genérica". Também precisa de boas instruções, como as coisas devem ser usadas e o que evitar, e considere como as pessoas acabam usando.

Estruturas genéricas e as APIs que operam nelas tendem a ser mais abstratas do que as APIs construídas para fins específicos...

Um contra-argumento que acho que melhora significativamente essa preocupação é que a carga cognitiva de aprender um número ilimitado de reimplementações de casos especiais dos algoritmos genéricos essencialmente iguais é ilimitada ...

Adicionada uma nota sobre a carga cognitiva de muitas APIs semelhantes.

As reimplementações de casos especiais não são ilimitadas na prática. Você verá apenas um número fixo de especialização.

Este não é um contra válido.

Você pode discordar de alguns pontos, eu discordo de alguns deles até certo ponto, mas eu entendo o ponto de vista deles e tento entender os problemas que as pessoas enfrentam no dia-a-dia. O objetivo do documento é coletar opiniões diferentes, não julgar "como algo é irritante para alguém".

No entanto, o documento se posiciona sobre "problemas rastreáveis ​​a problemas do mundo real", porque problemas abstratos e facilitados em fóruns tendem a se transformar em conversas sem sentido sem que qualquer entendimento seja construído.

O que realmente estamos chegando aqui é a regularidade de uma linguagem e APIs genéricas ajudam, não prejudicam isso.

Claro que na prática você pode precisar desse estilo de otimização apenas para menos de 1% dos casos.

Soluções alternativas:

As soluções alternativas não se destinam a substituir os genéricos. Mas, sim, uma lista de possíveis soluções para diferentes tipos de problemas.

Modelos de pacote

Posso estar enganado, mas isso não parece muito correto. Os functores ML (não devem ser confundidos com os functores FP) também podem retornar uma saída que permanece parametrizada por tipo.

Você pode fornecer uma redação mais clara e, se necessário, dividir em duas abordagens diferentes?

@egonelbre obrigado também por responder para que eu possa saber em quais pontos preciso esclarecer melhor meus pensamentos.

Você pode da próxima vez fazer os comentários/edições diretamente no próprio documento.

Desculpe, gostaria de poder cumprir, mas nunca usei os recursos de discussão do Google Doc, não tenho tempo para aprender e também prefiro poder vincular minhas discussões no Github para referência futura.

Apenas como exemplo, veja https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html.

O design da biblioteca de coleções do Scala foi criticado por muitas pessoas, incluindo um dos ex-membros da equipe-chave . Um comentário postado na LtU é representativo. Observe que adicionei o seguinte a uma das minhas postagens anteriores neste tópico para resolver isso:

Estou desconfiado de sistemas de “pia de cozinha” que tentam misturar tantos paradigmas (por exemplo, subclasses, herança múltipla, OOP, linearização de traços, implicit , typeclasses, tipos abstratos, etc), devido às reclamações sobre Scala ter tantos casos de canto na prática, embora talvez isso melhore com o Scala 3 (também conhecido como Dotty e o cálculo DOT).

Eu não acho que a biblioteca de coleções do Scala seria representativa de bibliotecas criadas para uma PL com apenas typeclasses para polimorfismo. De fato, as coleções Scala empregam o anti-padrão de herança , o que causou as hierarquias complexas, combinado com ajudantes implicit como CanBuildFrom que explodiram o orçamento da complexidade. E acho que se o ponto de @keean for respeitado sobre os _Elements of Programming_ de Stepanov ser uma álgebra , uma biblioteca de coleções elegante poderia ser criada. Foi a primeira alternativa que vi para uma biblioteca de coleções baseada em functor (FP) (ou seja, não copiando Haskell ) também baseada em matemática. Eu quero ver isso na prática, que é uma das razões pelas quais estou colaborando/discutindo com ele no projeto de um novo PL. E a partir deste momento, estou planejando que essa linguagem inicialmente transpile para Go (embora eu esteja há anos tentando encontrar uma maneira de evitar fazê-lo). Espero que possamos experimentar em breve para ver como funciona.

Minha percepção é que a comunidade/filosofia Go prefere esperar para ver o que funciona na prática e adotá-lo mais tarde, uma vez comprovado, do que se apressar e poluir a linguagem com experimentos fracassados. Porque, como você reiterou, todas essas alegações abstratas não são tão construtivas (exceto talvez para os teóricos do design PL). Também é provavelmente implausível projetar um sistema de genéricos coerente por comitê.

Também precisa de boas instruções, como as coisas devem ser usadas e o que evitar, e considere como as pessoas acabam usando.

E acho que vai ajudar não misturar tantos paradigmas diferentes disponíveis para o programador na mesma linguagem. Aparentemente, não é necessário ( @keean e eu precisamos provar essa afirmação). Acho que nós dois atribuímos a filosofia de que o orçamento de complexidade é finito e é o que você deixa de fora do PL que é tão importante quanto os recursos incluídos.

No entanto, o documento se posiciona sobre "problemas rastreáveis ​​a problemas do mundo real", porque problemas abstratos e facilitados em fóruns tendem a se transformar em conversas sem sentido sem que qualquer entendimento seja construído.

Acordado. E também é difícil para todos seguirem os pontos abstratos. O diabo está nos detalhes e nos resultados reais na natureza.

Claro que na prática você pode precisar desse estilo de otimização apenas para menos de 1% dos casos.

Go já possui interface para genericidade, podendo assim tratar os casos em que não necessite de polimorfismo paramétrico no tipo T para a instância da interface fornecida pelo site de chamada.

Acho que li em algum lugar, talvez tenha sido upthread, o argumento de que na verdade a biblioteca padrão do Go está sofrendo com a inconsistência do uso ideal dos idiomas mais atualizados. Não sei se isso é verdade, pois ainda não tenho experiência com Go. O que quero dizer é que o paradigma de genéricos escolhido infecta todas as bibliotecas. Então sim, a partir de agora você pode afirmar que apenas 1% do código precisaria dele, porque já há inércia em idiomas que evitam a necessidade de genéricos.

Você pode estar correto. Também tenho meu ceticismo sobre o quanto usarei qualquer recurso de linguagem específico. Acho que a experimentação para descobrir é o caminho que vou seguir. O projeto PL é um processo iterativo, então o problema é lutar contra a inércia que se desenvolve dificulta a iteração do processo. Então eu acho que Rob Pike está correto no vídeo onde ele sugere escrever programas que escrevem código para programas (ou seja, vão escrever ferramentas de geração e transpiladores) para experimentar e testar ideias.

Quando pudermos mostrar que um conjunto específico de recursos é superior na prática (e esperamos também popularidade de uso) àqueles atualmente em Go, talvez possamos ver algum consenso sobre adicioná-los ao Go. Eu encorajo outros a também criarem sistemas experimentais que transpilem para Go.

Você pode fornecer uma redação mais clara e, se necessário, dividir em duas abordagens diferentes?

Eu adiciono minha voz àqueles que querem desencorajar a tentativa de colocar algum recurso de modelagem excessivamente simplista em Go e alegar que é genérico. IOW, eu acho que um sistema genérico de funcionamento adequado que não vai acabar sendo uma inércia ruim é fundamentalmente incompatível com o desejo de ter um design excessivamente simplista para genéricos. Afaik, um sistema genérico precisa de um design holístico bem pensado e comprovado. Ecoando o que @larsth escreveu , encorajo aqueles com propostas sérias a primeiro construir um transpilador (ou implementar em um fork do frontend gccgo) e depois experimentar a proposta para que todos possamos entender melhor suas limitações. Fui encorajado a ler upthread que @ianlancetaylor não achava que uma poluição de inércia ruim seria adicionada ao Go. Quanto à minha reclamação específica sobre a proposta de parametrização de nível de pacote, minha sugestão para quem está propondo, por favor, considere se deve fazer um compilador que todos possamos usar para brincar e então todos podemos falar sobre exemplos do que gostamos e não t gosto sobre isso. Caso contrário, estamos nos enganando porque talvez eu nem tenha entendido corretamente a proposta descrita abstratamente. Não devo entender a proposta, pois não entendo como a embalagem parametrizada pode ser reutilizada em outra embalagem também parametrizada. IOW, se um pacote recebe parâmetros, ele também precisa instanciar outros pacotes com parâmetros. Mas parecia que a proposta estava afirmando que a única maneira de instanciar um pacote parametrizado era com um tipo concreto, não com parâmetros de tipo.

Desculpa tão prolixo. Quero ter certeza de que não estou sendo mal interpretado.

@ shelby3 ah, então não entendi a reclamação inicial. Em primeiro lugar, devo deixar claro que as seções em "Abordagens Genéricas" não são propostas concretas. São abordagens ou, em outras palavras, decisões de design maiores que podem ser tomadas em uma abordagem genérica concreta. No entanto, os agrupamentos são fortemente motivados por implementações existentes ou propostas concretas/informais. Além disso, suspeito que ainda faltam pelo menos 5 grandes ideias nessa lista.

Para a abordagem de "modelos de pacote" existem duas variações dela (veja as discussões vinculadas no documento):

  1. pacotes genéricos baseados em "interface",
  2. pacotes explicitamente genéricos.

Para 1. ele não requer que o pacote genérico faça nada de especial -- por exemplo, o atual container/ring se tornaria utilizável para especialização. Imagine "especialização" aqui substituindo todas as instâncias da interface no pacote pelo tipo concreto (e ignorando importações circulares). Quando esse pacote especializa outro pacote, ele pode usar a "interface" como a especialização - segue que então esse uso também será especializado.

Para 2. você pode olhar para eles de duas maneiras. Uma é a especialização concreta recursiva em cada importação - semelhante ao modelo/macroing, em nenhum momento haveria um "pacote parcialmente aplicado". Claro que também pode ser visto do lado funcional, que o pacote genérico é um parcial com parâmetros e então você o especializa.

Então, sim, você pode usar um pacote parametrizado em outro.

Ecoando o que @larsth escreveu, encorajo aqueles com propostas sérias a primeiro construir um transpilador (ou implementar em um fork do frontend gccgo) e depois experimentar a proposta para que todos possamos entender melhor suas limitações.

Eu sei que isso não foi explicitamente direcionado a essa abordagem, mas tem 4 protótipos diferentes para testar a ideia. Claro, eles não são transpiladores completos, mas são suficientes para testar algumas das ideias. ou seja, não tenho certeza se alguém implementou o caso "usando pacote parametrizado de outro".

Pacotes parametrizados soam muito como módulos de ML (e functores de ML são os parâmetros que podem ser outros pacotes). Existem duas maneiras de trabalhar "aplicativo" ou "generativo". Um functors aplicativo é como um valor ou um tipo de alias. Um functor generativo deve ser construído e cada instância é diferente. Outra maneira de pensar sobre isso é que para um pacote ser aplicativo ele deve ser puro (ou seja, não há variáveis ​​mutáveis ​​no nível do pacote). Se houver estado no nível do pacote, ele deve ser generativo, pois esse estado precisa ser inicializado, e importa qual "instância" de um pacote generativo você realmente passa como parâmetro para outros pacotes que, por sua vez, devem ser generativos. Por exemplo, os pacotes Ada são generativos.

O problema com a abordagem de pacotes generativos é que ela cria muitos clichês, onde você está instanciando pacotes com parâmetros. Você pode olhar os genéricos da Ada para ver como é isso.

As classes de tipo evitam esse clichê selecionando implicitamente a classe de tipo com base nos tipos usados ​​apenas na função. Você também pode visualizar as classes de tipo como sobrecarga restrita com despacho múltiplo, onde a resolução de sobrecarga quase sempre ocorre estaticamente em tempo de compilação, com exceções para recursão polimórfica e tipos existenciais (que são essencialmente variantes das quais você não pode converter, você só pode usar as interfaces para as quais a variante confirma).

Um functors aplicativo é como um valor ou um tipo de alias. Um functor generativo deve ser construído e cada instância é diferente. Outra maneira de pensar sobre isso é que para um pacote ser aplicativo ele deve ser puro (ou seja, não há variáveis ​​mutáveis ​​no nível do pacote). Se houver estado no nível do pacote, ele deve ser generativo, pois esse estado precisa ser inicializado, e importa qual "instância" de um pacote generativo você realmente passa como parâmetro para outros pacotes que, por sua vez, devem ser generativos. Por exemplo, os pacotes Ada são generativos.

Obrigado pela terminologia exata, preciso pensar em como integrar essas ideias no documento.

Além disso, não consigo ver uma razão pela qual você não poderia ter um "alias de tipo automático para um pacote gerado" - em certo sentido, algo entre a abordagem "functor aplicativo" e "funtor generativo". Obviamente, quando o pacote contém alguma forma de estado, pode ser complicado depurar e entender.

O problema com a abordagem de pacotes generativos é que ela cria muitos clichês, onde você está instanciando pacotes com parâmetros. Você pode olhar os genéricos da Ada para ver como é isso.

Até onde eu vejo, isso criaria menos clichê do que modelos C++, mas mais do que classes de tipo. Você tem um bom programa do mundo real para Ada que demonstre o problema? _(Por mundo real, quero dizer código que alguém está/estava usando na produção.)_

Claro, dê uma olhada no meu go-board Ada: https://github.com/keean/Go-Board-Ada/blob/master/go.adb

Embora esta seja uma definição bastante vaga de produção, o código é otimizado, funciona tão bem quanto a versão C++ e seu código aberto, e o algoritmo foi refinado ao longo de vários anos. Você também pode ver a versão C++: https://github.com/keean/Go-Board/blob/master/go.cpp

Isso mostra (eu acho) que os genéricos de Ada são uma solução mais limpa que os templates C++ (mas isso não é difícil), por outro lado é difícil fazer o acesso rápido às estruturas de dados em Ada devido às restrições em retornar uma referência .

Se você quiser olhar para um sistema de pacotes genéricos para uma linguagem imperativa, acho que Ada é um dos melhores para olhar. É uma pena que eles decidiram seguir vários paradigmas e adicionar todas as coisas OO à Ada. Ada é um Pascal aprimorado, e Pascal era uma linguagem pequena e elegante. Os genéricos Pascal mais Ada ainda seriam uma linguagem bem pequena, mas teriam sido muito melhores na minha opinião. Porque o foco da Ada mudou para uma abordagem OO, encontrar boa documentação e exemplos de como fazer as mesmas coisas com genéricos parece difícil de encontrar.

Embora eu ache que as classes de tipos têm algumas vantagens, eu poderia viver com os genéricos do estilo Ada, há alguns problemas que me impedem de usar Ada mais amplamente, acho que ele obtém valores/objetos errados (acho que poucas linguagens acertam, 'C' sendo um dos únicos), é difícil trabalhar com ponteiros (variáveis ​​de acesso) e criar abstrações de ponteiro seguro, e não fornece uma maneira de usar pacotes com polimorfismo de tempo de execução (fornece um modelo de objeto para isso, mas adiciona um paradigma totalmente novo em vez de tentar encontrar uma maneira de ter polimorfismo em tempo de execução usando pacotes).

A solução para o polimorfismo em tempo de execução é tornar os pacotes de primeira classe para que instâncias de assinaturas de pacotes possam ser passadas como argumentos de função, isso infelizmente requer tipos dependentes (veja o trabalho feito em Tipos de Objetos Dependentes para Scala para limpar a bagunça que eles fizeram com seu sistema de tipos original).

Então, acho que os genéricos de pacote podem funcionar, mas Ada levou décadas para lidar com todos os casos extremos, então eu examinaria um sistema de genéricos de produção para ver quais refinamentos usam na produção produzida. No entanto, Ada ainda fica aquém porque os pacotes não são de primeira classe e não podem ser usados ​​em polimorfismo de tempo de execução, e isso precisaria ser resolvido.

@keean escreveu :

Pessoalmente, considero a reflexão de tempo de execução um recurso errado, mas isso sou só eu... Posso explicar o motivo se alguém estiver interessado.

O apagamento de tipo permite “Teoremas de graça”, o que tem implicações práticas . A reflexão de tempo de execução gravável (e talvez até legível devido a relações transitivas com código imperativo?) torna impossível garantir transparência referencial em qualquer código e, portanto, certas otimizações de compilador não são possíveis e mônadas seguras de tipo não são possíveis. Percebo que Rust ainda não tem um recurso de imutabilidade. OTOH, a reflexão permite outras otimizações que não seriam possíveis se não pudessem ser digitadas estaticamente.

Eu também havia declarado upthread:

E é isso que um compilador transpilando de um superconjunto de Go com genéricos adicionados, produziria como código Go. Mas o embrulho não seria baseado em algum delineamento como pacote, pois faltaria a composabilidade que já mencionei. O ponto é que não há atalho para um bom sistema de tipos genéricos combináveis. Ou fazemos isso corretamente ou não fazemos nada, porque adicionar algum hack não combinável que não seja realmente genérico criará eventualmente uma inércia de retalhos de genericidade meia-boca de retalhos e irregularidade de casos de canto e soluções alternativas tornando o código do ecossistema Go ininteligível.


@keean escreveu:

[…] para que um pacote seja aplicativo, ele deve ser puro (ou seja, não há variáveis ​​mutáveis ​​no nível do pacote)

E nenhuma função impura pode ser empregada para inicializar variáveis ​​imutáveis.

@egonelbre escreveu:

Então, sim, você pode usar um pacote parametrizado em outro.

O que eu aparentemente tinha em mente eram “pacotes parametrizados de primeira classe” e o polimorfismo de tempo de execução proporcional (também conhecido como dinâmico) que @keean mencionou posteriormente, porque presumi que os pacotes parametrizados foram propostos em vez de typeclasses ou OOP.

EDIT: mas existem dois significados possíveis para módulos de “primeira classe”: módulos como valores de primeira classe, como em Successor ML e MixML, distinguidos de módulos como valores de primeira classe com tipos de primeira classe como em 1ML, e a compensação necessária na recursão do módulo (ou seja, misturando ) entre eles.

@keean escreveu:

A solução para o polimorfismo em tempo de execução é tornar os pacotes de primeira classe para que instâncias de assinaturas de pacotes possam ser passadas como argumentos de função, isso infelizmente requer tipos dependentes (veja o trabalho feito em Tipos de Objetos Dependentes para Scala para limpar a bagunça que eles fizeram com seu sistema de tipos original).

O que você quer dizer com tipos dependentes? (EDIT: presumo que agora ele quis dizer digitação “não dependente de valor”, ou seja, “ funções cujo tipo de resultado depende do argumento [runtime?] ['s type]”) Certamente não depende dos valores de, por exemplo int dados, como em Idris. Acho que você está se referindo à digitação dependente (ou seja, rastreamento) do tipo dos valores que representam instâncias de módulo instanciadas na hierarquia de chamadas para que essas funções polimórficas possam ser monomorfizadas em tempo de compilação? O polimorfismo de tempo de execução entra devido a esses tipos monomorfizados serem o tipo existencial vinculado a tipos dinâmicos? F-ing Modules demonstraram que tipos “dependentes” não são absolutamente necessários para modelar módulos ML no sistema F ω . Simplifiquei demais se presumir que @rossberg reformulou o modelo de tipagem para remover todos os requisitos de monomorfização?

O problema com a abordagem de pacote generativo é que ela cria muitos clichês […]
As classes de tipo evitam esse clichê selecionando implicitamente a classe de tipo com base nos tipos usados ​​apenas na função.

Não existe também um clichê com functores de ML de aplicativo? Não há unificação conhecida de typeclasses e functors ML (módulos) que retém a brevidade sem introduzir restrições que são necessárias para evitar (cf também ) a antimodularidade inerente do critério de exclusividade global de instâncias de implementação de typeclass.

Typeclasses só podem implementar cada tipo de uma maneira e, de outra forma, requerem o clichê do wrapper newtype para superar a limitação. Aqui está outro exemplo de várias maneiras de implementar um algoritmo. Afaics, @keean contornou essa limitação em seu exemplo de classificação typeclass substituindo a seleção implícita por um Relation explicitamente selecionado empregando tipos de quebra data para nomear diferentes relações genericamente no tipo de valor, mas estou duvidando se tais táticas são gerais para todas as variantes de modularidade. No entanto, uma solução mais generalizada (que pode ajudar a melhorar o problema de modularidade da singularidade global , possivelmente combinada com uma restrição órfã como melhoria do versionamento proposto para resolução órfã , empregando um não padrão para implementações que poderiam ser órfãs) pode ser ter um parâmetro de tipo extra implicitamente em todos os typeclass interface , que quando não especificado é padronizado para a correspondência implícita normal, mas quando especificado (ou quando não especificado não corresponde a nenhum outro 2 ) seleciona a implementação que tem o mesmo valor em sua lista delimitada por vírgulas de valores personalizados (portanto, isso é uma correspondência modular mais generalizada do que nomear uma instância implement específica). A lista delimitada por vírgulas é para que uma implementação possa ser diferenciada em mais de um grau de liberdade, como se tivesse duas especializações ortogonais. O especializado não padrão desejado pode ser especificado na declaração da função ou no site de chamada. No local da chamada, por exemplo, f<non-default>(…) .

Então, por que precisaríamos de módulos parametrizados se tivéssemos typeclasses? Afaics apenas para (← link importante para clicar) substituição porque a reutilização de typeclasses para esse propósito não se encaixa bem, por exemplo, queremos que um módulo de pacote seja capaz de abranger vários arquivos e queremos poder abrir implicitamente o conteúdo de o módulo no escopo sem clichê adicional . Então, talvez seguir em frente com uma parametrização de pacote _sintático-somente_ de substituição (não de primeira classe) seja um primeiro passo razoável que possa abordar a generalidade no nível do módulo, permanecendo aberto à compatibilidade e não sobreposição de funcionalidade se adicionar classes de tipo posteriormente para nível de função genericidade. macros são, por exemplo , digitadas ou apenas substituição sintática (também conhecida como “pré-processador”). Se digitado, os módulos duplicam a funcionalidade de typeclasses, o que é indesejável tanto do ponto de vista de minimizar os paradigmas/conceitos sobrepostos do PL quanto possíveis casos de canto devido a interações da sobreposição ( como aquelas ao tentar oferecer functores e typeclasses de ML ). Os módulos tipados são mais modulares porque as modificações em qualquer implementação encapsulada dentro do módulo que não modifica as assinaturas exportadas não podem fazer com que os consumidores do módulo se tornem incompatíveis (além do problema de antimodularidade mencionado acima de instâncias de implementação sobrepostas de typeclass). Estou interessado em ler os pensamentos de @keean sobre isso.

[…] com exceções para recursão polimórfica e tipos existenciais (que são essencialmente variantes das quais você não pode expulsar, você só pode usar as interfaces para as quais a variante confirma).

Para ajudar outros leitores. Por “recursão polimórfica”, acho que se refere a tipos de classificação mais alta, por exemplo, retornos de chamada parametrizados definidos em tempo de execução, onde o compilador não pode monomorfizar o corpo da função de retorno de chamada porque não é conhecido em tempo de compilação. Os tipos existenciais são, como mencionei antes, equivalentes aos objetos trait de Rust, que são uma maneira de obter contêineres heterogêneos com uma ligação posterior no problema de expressão do que class subclasse herança virtual, mas não tão aberta à extensão na expressão Problema como uniões com estruturas de dados imutáveis ​​ou copiando 3 que têm um custo de desempenho O(log n) .

1 O que não requer HKT no exemplo acima, porque SET não está exigindo o tipo elem é um parâmetro de tipo do tipo genérico de set , ou seja, não é set<elem> .

2 No entanto, se existisse mais de uma implementação não padrão e nenhuma implementação padrão, a seleção seria ambígua, de modo que o compilador deveria gerar um erro.

3 Observe que a mutação com estruturas de dados imutáveis ​​não exige necessariamente a cópia de toda a estrutura de dados, se a estrutura de dados for inteligente o suficiente para isolar o histórico, como uma lista vinculada individualmente.

Implementar func pick(a CollectionOfT, count uint) []T seria um bom exemplo de aplicação de genéricos (de https://github.com/golang/go/issues/23717):

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

A abordagem{} da interface aqui é complicada.

Comentei algumas vezes sobre esse problema que um dos principais problemas com a abordagem de modelo C++ é sua dependência da resolução de sobrecarga como mecanismo para metaprogramação em tempo de compilação.

Parece que Herb Sutter chegou à mesma conclusão: agora existe uma proposta interessante para programação em tempo de compilação em C++ .

Ele tem alguns elementos em comum com o pacote Go reflect e minha proposta anterior para funções de tempo de compilação em Go .

Oi.
Escrevi uma proposta de genéricos com restrições para Go. Você pode lê-lo aqui . Talvez possa ser adicionado como um documento de 15292. É principalmente sobre restrições e é lido como uma emenda aos Parâmetros de tipo de Taylor em Go .
Ele pretende ser um exemplo de uma maneira viável (eu acredito) de fazer genéricos 'tipo seguro' em Go, - espero que possa adicionar algo a essa discussão.
Observe que, embora eu tenha lido (a maioria) este tópico muito longo, não segui todos os links nele, portanto, outros podem ter feito sugestões semelhantes. Se for o caso, peço desculpas.

br. Chr.

Sintaxe bikeshedding:

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

poderia ser

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

que se parece mais com Go to me. :-)

Vários elementos aqui.

Uma coisa é substituir : por _ e substituir # por ... .
Suponho que você poderia fazer isso se preferir.

Outra coisa é substituir constraint[T] Array por type[T] Array constraint .
Isso parece indicar que as restrições são tipos, o que não acho correto. Formalmente, uma restrição é um _predicado_ no conjunto de todos os tipos, ou seja. um mapeamento do conjunto de tipos para o conjunto { true , false }.
Ou, se preferir, você pode pensar em uma restrição simplesmente como _um conjunto de_ tipos.
Não é do tipo _a_.

br. Chr.

Por que constraint não é apenas um interface ?

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

Uma interface seria um pouco mais útil como restrição com a seguinte proposta: #23796 que por sua vez também daria algum mérito à própria proposta.

Além disso, se a proposta de tipos de soma for aceita de alguma forma (#19412), eles devem ser usados ​​para restringir o tipo.

Embora eu acredite na palavra-chave constraint, algo parecido deve ser adicionado, para não repetir grandes restrições e evitar erros devido à distração.

Finalmente, para a parte do bikeshedding, acho que as restrições devem ser listadas no final de uma definição, para evitar superlotação (ferrugem parece ter uma boa ideia aqui):

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

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

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


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

@urandom : Acho que é uma grande vantagem ter interfaces implementadas implicitamente em vez de explicitamente. A proposta @surlykke neste comentário eu acho que é muito mais próxima de outra sintaxe Go em espírito.

@surlykke Peço desculpas se a proposta tiver a resposta para alguma delas.

Um uso de genéricos é permitir funções de estilo internas. Como você implementaria o len no nível do aplicativo com isso? O layout da memória é diferente para cada entrada permitida, então como isso é melhor do que uma interface?

A “escolha” descrita anteriormente tem um problema semelhante em que a indexação em um mapa versus a indexação em uma fatia são diferentes. No caso do mapa, se houve uma conversão para slice primeiro, o mesmo código de picking pode ser usado, mas como isso é feito?

Coleções é outro uso:

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

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

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

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

Para o caso do tipo de coleção, não estou convencido de que isso seja uma grande vitória sobre os genéricos do Go 1, pois há compensações de legibilidade.

Eu concordo que os parâmetros de tipo devem ter algum tipo de restrição. Caso contrário, estaremos repetindo os erros dos modelos C++. A questão é: quão expressivas devem ser as restrições?

Em uma extremidade, poderíamos apenas usar interfaces. Mas, como você aponta, muitos padrões úteis não podem ser capturados dessa maneira.

Depois, há sua ideia, e outras semelhantes, que tentam criar um conjunto de restrições úteis e fornecer uma nova sintaxe para expressá-las. Além do problema de adicionar ainda mais sintaxe, não está claro onde parar. Como você aponta, sua proposta captura muitos padrões, mas não todos.

No outro extremo está a ideia que proponho neste documento . Ele usa o próprio código Go como a linguagem de restrição. Você pode capturar praticamente qualquer restrição dessa maneira e não requer uma nova sintaxe.

@jba
É um pouco verboso. Talvez se Go tivesse uma sintaxe lambda seria um pouco mais palatável. Por outro lado, parece que o maior problema que está tentando resolver é verificar se um tipo suporta algum tipo de operador. Poderia ser mais fácil se o Go tivesse interfaces predefinidas para vários operadores:

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

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

ou algo nesse sentido.

Se o problema for com a extensão de builtins, talvez o problema esteja na maneira como a linguagem cria tipos de adaptadores. Por exemplo, o inchaço associado a sort.Interface não é a razão por trás de https://github.com/golang/go/issues/16721 e sort.Slice?
Olhando para https://github.com/golang/go/issues/21670#issuecomment -325739411, a ideia de @Sajmani de ter literais de interface pode ser o ingrediente necessário para que os parâmetros de tipo funcionem facilmente com builtins.
Veja a seguinte definição de Iterador:

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

Se print for uma função que simplesmente itera sobre uma lista e imprime seu conteúdo, o exemplo a seguir usa literais de interface para construir uma interface satisfatória para print .

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

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

Já se pode fazer isso se eles declararem globalmente tipos cuja única responsabilidade é satisfazer uma interface. No entanto, essa conversão de uma função para um método torna as interfaces (e, portanto, as "restrições") mais fáceis de satisfazer. Não poluímos declarações de nível superior com adaptadores simples (como "widgetsByName" na classificação).
Os tipos definidos pelo usuário obviamente também podem aproveitar esse recurso, como visto neste exemplo de LinkedList:

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

@geovanisouza92 : Restrições como as descrevi são mais expressivas do que interfaces (campos, operadores). Eu considerei brevemente estender interfaces em vez de introduzir restrições, mas acho que seria uma mudança muito intrusiva para um elemento existente do Go.

@pciet Não tenho certeza do que você quer dizer com 'nível de aplicativo'. Go tem uma função len embutida que pode ser aplicada a array, ponteiro para array, slice, string e canal, então, na minha proposta, se um parâmetro de tipo é restrito a ter um desses como tipo subjacente , len pode ser aplicado a ele.

@pciet Sobre o seu exemplo com Comparable constraint/interface. Observe que, se você definir (a variante de interface):

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

Então você pode colocar qualquer coisa que implemente Comparable em Set . Compare isso com:

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

onde você só pode colocar valores do tipo Foo em FooSet . Essa é a segurança do tipo mais forte.

@urandom Novamente, não sou fã de:

type MyConstraint constraint {....}

pois não acredito que uma restrição seja um tipo. Além disso, eu definitivamente não permitiria:

var myVar MyConstraint

o que não faz sentido para mim. Outra indicação de que as restrições não são tipos.

@urandom Em bikeshedding: acredito que as restrições devem ser declaradas apenas ao lado dos parâmetros de tipo. Considere uma função comum, definida assim:

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

Você não pode ler isso da esquerda para a direita. Em vez disso, você primeiro leria func MyFunc(i) para determinar que é uma definição de função. Então você teria que pular para o final para descobrir o que é i , e então voltar para o corpo da função. Não é o ideal, OMI. E não vejo como as definições genéricas devam ser diferentes.
Mas, obviamente, essa discussão é ortogonal àquela sobre se Go deve ter restrições ou genéricos.

@surlykke
Eu estou bem com isso não sendo um tipo. O mais importante é que eles tenham um nome para que possam ser referidos por vários tipos.

Para funções, se seguirmos a sintaxe de ferrugem, seria:

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

Portanto, ele não ocultará coisas como o nome da função ou seus parâmetros, e você não precisaria ir até o final do corpo da função para ver quais são as restrições nos tipos genéricos

@surlykke para a posteridade, você poderia localizar onde sua proposta poderia ser adicionada:
https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4

É um ótimo lugar para "compilar" todas as propostas.

Outra questão que coloco a todos vocês é como lidar com a especialização de diferentes instanciações de um tipo genérico. Na proposta de parâmetros de tipo , a maneira de fazer isso é gerar a mesma função de modelo para cada tipo instanciado, substituindo o parâmetro de tipo pelo nome do tipo. Para ter uma funcionalidade separada para diferentes tipos, execute uma troca de tipo no parâmetro de tipo.

É seguro supor que, quando o compilador vê uma chave de tipo em um parâmetro de tipo, é permitido gerar uma implementação separada para cada asserção? Ou isso está muito envolvido em uma otimização, já que parâmetros de tipo aninhados nas estruturas declaradas podem criar um aspecto paramétrico para a geração de código?

Na proposta de funções de tempo de compilação , como sabemos que essas declarações são geradas em tempo de compilação, uma troca de tipo não representa nenhum custo de tempo de execução.

Um cenário prático: Se considerarmos um caso do pacote math/bits , executar uma declaração de tipo para chamar OnesCount para cada uintXX superaria o ponto de ter uma biblioteca de manipulação de bits eficiente. Se, no entanto, as asserções de tipo forem transformadas nas seguintes

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

Uma chamada para

var x uint8 = 255
bits.OnesCount(x)

chamaria a seguinte função gerada (o nome não é importante aqui):

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

@jba Essa é uma proposta interessante, mas para mim destaca principalmente o fato de que a definição da função paramétrica em si geralmente é suficiente para definir suas restrições.

Se você for usar “operadores usados ​​em uma função” como as restrições, então qual a vantagem de escrever uma segunda função contendo um subconjunto dos operadores usados ​​na primeira?

@bcmills Um deles é uma especificação e o outro é a implementação. É a mesma vantagem da digitação estática: você pode detectar erros mais cedo.

Se a implementação for a especificação, à la C++ templates, então qualquer mudança na implementação potencialmente quebra os dependentes. Isso pode não ser descoberto até muito mais tarde, quando os dependentes recompilarem e os descobridores não tiverem contexto para entender a mensagem de erro. Com a especificação no mesmo pacote, você pode detectar a quebra localmente.

@mandolyte Não tenho certeza de onde adicioná-lo - talvez um parágrafo em 'Abordagens genéricas' chamado 'Genéricos com restrições'?
O documento não parece conter muito sobre a restrição de parâmetros de tipo, portanto, se você adicionasse um parágrafo onde minha proposta seria mencionada, outras abordagens para restrições também poderiam ser listadas lá.

@surlykke a abordagem geral do documento é fazer uma mudança no que parece certo e tentarei aceitar, incorporar e organizar isso com o restante do documento. Eu adicionei uma seção aqui . Sinta-se livre para adicionar coisas que eu perdi.

@egonelbre Isso é muito bom. Obrigado!

@jba
Eu gosto da sua proposta, mas acho que é muito pesada para golang. Isso me lembra muitos modelos em c++. O principal problema que eu acho é que você pode escrever um código realmente complexo com ele.
Decidir se duas instâncias de interface genéricas se sobrepõem porque o conjunto restrito de tipos se sobrepõe seria uma tarefa difícil, causando tempos de compilação mais lentos. O mesmo para geração de código.

Eu acho que as restrições propostas são mais leves para ir. Pelo que ouvi é que as restrições aka typeclasses podem ser implementadas ortogonalmente ao sistema de tipos de uma linguagem.

Eu tenho que concordar fortemente que não devemos ir com restrições implícitas do corpo da função. Eles são amplamente considerados uma das falhas mais significativas dos modelos C++:

  • As restrições não são facilmente visíveis. Embora o godoc teoricamente possa enumerar todas as restrições na documentação, elas não são visíveis no código-fonte, exceto implicitamente.
  • Por isso, é possível incluir acidentalmente uma restrição adicional que só fica visível quando você tenta usar a função de uma forma não esperada. Ao exigir a especificação explícita das restrições, o programador deve saber exatamente quais restrições estão introduzindo.
  • Ele toma a decisão sobre quais tipos de restrições são permitidos muito mais ad hoc. Por exemplo, posso definir a seguinte função? Quais são as restrições reais em T, U e V aqui? Se exigirmos que o programador especifique explicitamente as restrições, então seremos conservadores no tipo de restrições que permitimos (nos deixando expandir isso lenta e deliberadamente). Se tentarmos ser conservadores de qualquer maneira, como damos uma mensagem de erro para uma função como essa? "Erro: não é possível atribuir uv() a T porque impõe uma restrição ilegal"?
func[T, U, V] Foo(u U, v V) {
  var t T = u.v(V) + 1;
}
  • Chamar funções genéricas em outras funções genéricas piora as situações acima, pois agora você precisa examinar todas as restrições dos destinatários para entender as restrições da função que está escrevendo ou lendo.
  • A depuração pode ser muito difícil, porque as mensagens de erro não devem fornecer informações suficientes para encontrar a origem da restrição ou devem vazar detalhes internos da função. Por exemplo, se F tiver algum requisito em um tipo T e o autor de F estiver tentando descobrir de onde veio esse requisito, eles gostariam que o compilador alertá-los para exatamente qual instrução dá origem à restrição (especialmente se vier de um callee genérico). Mas um usuário de F não quer essa informação e, de fato, se ela estiver incluída nas mensagens de erro, estaremos vazando detalhes de implementação de F nas mensagens de erro de seus usuários, o que são uma experiência de usuário terrível.

@alercah

Por exemplo, posso definir a seguinte função?

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

Não. u.v(V) é um erro de sintaxe porque V é um tipo e a variável t não é usada.

No entanto, você pode definir esta função, que pode ser a que você pretendia:

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

Quais são as restrições reais em T, U e V aqui?

  • O tipo V é irrestrito.
  • O tipo U deve ter um método v que aceite um único parâmetro ou varargs de algum tipo atribuível de V , porque u.v é invocado com um único argumento do tipo V .

    • U.v poderia ser um campo do tipo função, mas sem dúvida isso deveria implicar um método; veja #23796.

  • O tipo retornado por U.v deve ser numérico, pois a constante 1 é adicionada a ele.
  • O tipo de retorno U.v deve ser atribuído a T , porque u.v(…) + 1 é atribuído a uma variável do tipo T .
  • O tipo T deve ser numérico, porque o tipo de retorno de U.v é numérico e atribuível a T .

(Um aparte: você poderia argumentar que U e V deveriam ter a restrição “copiável” porque os argumentos desses tipos são passados ​​por valor, mas o sistema de tipos não genéricos existente não impõe essa restrição também. Isso é assunto para uma proposta separada.)

Se exigirmos que o programador especifique explicitamente as restrições, então seremos conservadores no tipo de restrições que permitimos (nos deixando expandir isso lenta e deliberadamente).

Sim, isso é verdade: mas omitir uma restrição seria um defeito sério, independentemente de essas restrições serem implícitas ou não. IMO, o papel mais importante das restrições é resolver a ambiguidade. Por exemplo, nas restrições acima, o compilador deve estar preparado para instanciar u.v como um método de argumento único ou variável.

A ambiguidade mais interessante ocorre para literais, onde precisamos desambiguar entre tipos struct e tipos compostos:

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

Se tentarmos ser conservadores de qualquer maneira, como damos uma mensagem de erro para uma função como essa? "Erro: não é possível atribuir uv() a T porque impõe uma restrição ilegal"?

Não tenho certeza do que você está perguntando, pois não vejo restrições conflitantes para este exemplo. O que você quer dizer com “restrição ilegal”?

A depuração pode ser muito difícil, porque as mensagens de erro não devem fornecer informações suficientes para encontrar a origem da restrição ou devem vazar detalhes internos da função.

Nem todas as restrições relevantes podem ser expressas pelo sistema de tipos (consulte também https://github.com/golang/go/issues/22876#issuecomment-347035323). Algumas restrições são impostas por pânicos em tempo de execução; alguns são reforçados pelo detector de corrida; as restrições mais perigosas são meramente documentadas e não são detectadas.

Todos esses “detalhes internos vazam” até certo ponto. (Veja também https://xkcd.com/1172/.)

Por exemplo, se […] o autor de F está tentando descobrir de onde veio esse requisito, eles gostariam que o compilador os alertasse sobre exatamente qual declaração dá origem à restrição (especialmente se vier de um callee genérico). Mas um usuário de F não quer essa informação[.]

Pode ser? É assim que os autores de API usam anotações de tipo em linguagens inferidas por tipo, como Haskell e ML, mas também leva a um buraco de coelho de tipos profundamente paramétricos (“ordem superior”) em geral.

Por exemplo, suponha que você tenha esta função:

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

Como você expressa as restrições explícitas no tipo Arg ? Eles dependem da instanciação específica de F . Esse tipo de dependência parece estar faltando em muitas das propostas recentes de restrições.

Não. uv(V) é um erro de sintaxe porque V é um tipo e a variável t não é usada.

No entanto, você pode definir esta função, que pode ser a que você pretendia:

Sim, essa era a intenção, minhas desculpas.

O tipo T deve ser numérico, porque o tipo de retorno de U.v é numérico e atribuível a T .

Devemos realmente considerar isso uma restrição? É dedutível das outras restrições, mas é mais ou menos útil chamar isso de restrição distinta? As restrições implícitas fazem essa pergunta de uma maneira que as restrições explícitas não fazem.

Sim, isso é verdade: mas omitir uma restrição seria um defeito sério, independentemente de essas restrições serem implícitas ou não. IMO, o papel mais importante das restrições é resolver a ambiguidade. Por exemplo, nas restrições acima, o compilador deve estar preparado para instanciar uv como um método de argumento único ou variável.

Eu quis dizer "restrições que permitimos", como na linguagem. Com restrições explícitas, é muito mais fácil decidir que tipo de restrições estamos dispostos a permitir que os usuários escrevam, em vez de apenas dizer que a restrição é "o que quer que faça as coisas compilarem". Por exemplo, meu exemplo Foo acima envolve um tipo adicional implícito separado de T , U ou V , já que devemos considerar o tipo de retorno de u.v . Este tipo não é explicitamente referido de forma alguma na declaração de f ; as propriedades que ele deve ter são completamente implícitas. Da mesma forma, estamos dispostos a permitir tipos de classificação mais alta ( forall )? Não consigo criar um exemplo de cabeça, mas também não consigo me convencer de que você não pode escrever implicitamente um limite de tipo de classificação mais alta.

Outro exemplo é se devemos permitir que uma função aproveite a sintaxe sobrecarregada. Se uma função implicitamente restrita fizer for i := range t para algum t do tipo genérico T , a sintaxe funcionará se T for qualquer array, slice, channel, ou mapa. Mas a semântica é bem diferente, especialmente se T for um tipo de canal. Por exemplo, se t == nil (o que pode acontecer desde que T seja uma matriz), a iteração não faz nada, pois não há elementos em uma fatia ou mapa nulo, ou bloqueia para sempre já que é isso que os canais nil fazem. Esta é uma grande arma esperando para acontecer. Da mesma forma está fazendo m[i] = ... ; se eu pretendo que m seja um mapa, precisarei me proteger contra ele ser realmente uma fatia, pois o código pode entrar em pânico em uma atribuição fora do alcance, caso contrário.

Na verdade, acho que isso se presta a outro argumento contra restrições implícitas: os autores de API podem escrever instruções artificiais apenas para adicionar restrições. Por exemplo for _, _ := range t { break } previne um canal enquanto ainda permite mapas, slices e arrays; x = append(x) força x a ter o tipo de fatia. var _ = make(T, 0) permite fatias, mapas e canais, mas não matrizes. Haverá um livro de receitas de como adicionar restrições implicitamente para que alguém não possa chamar sua função com um tipo para o qual você não escreveu o código correto. Não consigo nem pensar em uma maneira de escrever código que compile apenas para tipos de mapa, a menos que eu também conheça o tipo de chave. E não acho que isso seja hipotético; mapas e fatias se comportam de maneira bastante diferente para a maioria das aplicações

Não tenho certeza do que você está perguntando, pois não vejo restrições conflitantes para este exemplo. O que você quer dizer com “restrição ilegal”?

Refiro-me a uma restrição que não é permitida pela linguagem, como se a linguagem decidir não permitir restrições de classificação mais alta.

Nem toda restrição relevante pode ser expressa pelo sistema de tipos (veja também #22876 (comentário)). Algumas restrições são impostas por pânicos em tempo de execução; alguns são reforçados pelo detector de corrida; as restrições mais perigosas são meramente documentadas e não são detectadas.

Todos esses “detalhes internos vazam” até certo ponto. (Veja também https://xkcd.com/1172/.)

Eu realmente não vejo como #22876 entra nisso; que está tentando usar o sistema de tipos para expressar um tipo diferente de restrição. Sempre será verdade que não podemos expressar algumas restrições em valores, ou em programas, mesmo com um sistema de tipos de complexidade arbitrária. Mas estamos falando apenas sobre restrições de tipos aqui. O compilador precisa ser capaz de responder à pergunta "Posso instanciar este genérico com o tipo T ?" o que significa que ele deve entender as restrições, sejam elas implícitas ou explícitas. (Observe que algumas linguagens, como C++ e Rust, não podem decidir essa questão em geral porque ela pode depender de computação arbitrária e, portanto, se volta para o Problema da Parada, mas elas ainda expressam as restrições que precisam ser satisfeitas.)

O que quero dizer é mais como "qual mensagem de erro o exemplo a seguir deve fornecer?"

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

Podemos dizer Error: cannot call IndirectlyConstrained with [T = int]; T must have a method with signature func (T t) DoSomething() . Essa mensagem de erro é útil para um usuário de IndirectlyConstrained , porque define claramente a restrição que está faltando. Mas não fornece nenhuma informação para alguém tentando depurar por que IndirectlyConstrained tem essa restrição, o que é um grande problema de usabilidade se for uma função grande. Poderíamos adicionar Note: this constraint exists on T because IndirectlyConstrained calls DirectlyConstrained with [U = T] on line N , mas agora estamos vazando detalhes da implementação de IndirectlyConstrained . Além disso, não explicamos por que IndirectlyConstrained tem a restrição, então adicionamos outro Note: this constraint exists on U because DirectlyConstrained calls t.DoSomething() on line M ? E se a restrição implícita vier de algum chamado quatro níveis abaixo da pilha de chamadas?

Além disso, como formatamos essas mensagens de erro para tipos que não estão explicitamente listados como parâmetros? Por exemplo, se no exemplo acima, IndirectlyConstrained chama DirectlyConstrained(t.U()) . Como nos referimos ao tipo? Neste caso poderíamos dizer the type of t.U() , mas o valor não será necessariamente o resultado de uma única expressão; ele pode ser construído sobre várias instruções. Então precisaríamos sintetizar uma expressão com os tipos corretos para colocar na mensagem de erro, uma que nunca aparece no código, ou precisaríamos encontrar outra maneira de se referir a ela que seria menos clara para o pobre chamador que violou a restrição.

Como você expressa as restrições explícitas no tipo Arg? Eles dependem da instanciação específica de F. Esse tipo de dependência parece estar faltando em muitas das propostas recentes de restrições.

Solte F e faça com que o tipo de f seja func (Arg) Result . Sim, ele ignora funções variádicas, mas o resto do Go também. Uma proposta para fazer varargs funcs atribuíveis a assinaturas compatíveis poderia ser feita separadamente.

Para casos em que realmente exigimos limites de tipo de ordem superior, pode ou não fazer sentido incluí-los em genéricos v1. Restrições explícitas nos forçam a decidir explicitamente se queremos dar suporte a tipos de ordem superior e como. A falta de consideração até agora é um sintoma, eu acho, do fato de que Go atualmente não tem como se referir a propriedades de tipos internos. É uma questão geral aberta de como qualquer sistema genérico permitirá funções genéricas sobre todos os tipos numéricos, ou todos os tipos inteiros, e a maioria das propostas não se concentrou muito nisso.

Por favor, avalie minha implementação de genéricos em seu próximo projeto
http://go-li.github.io/

Podemos dizer Error: cannot call IndirectlyConstrained with [T = int]; T must have a method with signature func (T t) DoSomething() . Esta mensagem de erro […] não fornece nenhuma informação para alguém tentando depurar porque IndirectlyConstrained tem essa restrição, que é um grande problema de usabilidade se for uma função grande.

Eu quero apontar uma grande suposição que você está fazendo aqui: que a mensagem de erro de go build é a ferramenta _only_ que o programador tem disponível para diagnosticar o problema.

Para usar uma analogia: se você encontrar um error em tempo de execução, você tem várias opções para depuração. O erro em si contém apenas uma mensagem simples, que pode ou não ser adequada para descrever o erro. Mas não é a única informação que você tem disponível: por exemplo, você também tem quaisquer declarações de log que o programa emitiu, e se for um bug muito complicado você pode carregá-lo em um depurador interativo.

Ou seja, a depuração em tempo de execução é um processo interativo. Então, por que devemos assumir depuração não interativa para erros de tempo de compilação? Como uma alternativa, poderíamos ensinar a ferramenta guru sobre restrições de tipo. Então, a saída do compilador seria algo como:

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

Isso fornece ao usuário do pacote genérico as informações necessárias para depurar o site de chamada imediata, mas _também_ fornece uma trilha para o mantenedor do pacote (e, mais importante, seu ambiente de edição!) para investigar mais.

Poderíamos adicionar Note: this constraint exists on T because IndirectlyConstrained calls DirectlyConstrained with [U = T] on line N , mas agora estamos vazando detalhes da implementação de IndirectlyConstrained .

Sim, isso é o que quero dizer sobre informações vazando de qualquer maneira. Você já pode usar guru describe para espiar dentro de uma implementação. Você pode espiar dentro de um programa em execução usando um depurador, e não apenas procurar na pilha, mas também descer para funções de baixo nível arbitrariamente.

Concordo absolutamente que devemos ocultar informações provavelmente irrelevantes _por padrão_, mas isso não significa que devemos escondê-las em absoluto.

Se uma função restrita implicitamente faz i := range t para algum t do tipo genérico T , a sintaxe funciona se T for qualquer array, slice, channel , ou mapa. Mas a semântica é bem diferente, especialmente se T for um tipo de canal.

Acho que esse é o argumento mais convincente para restrições de tipo, mas isso não exige que as restrições explícitas sejam tão detalhadas quanto o que algumas pessoas estão propondo. Para desambiguar os sites de chamada, parece suficiente restringir os parâmetros de tipo por algo mais próximo de reflect.Kind . Não precisamos descrever operações que já estão claras no código; em vez disso, só precisamos dizer coisas como “ T é um tipo de fatia”. Isso leva a um conjunto muito mais simples de restrições:

  • um tipo sujeito a operações de índice precisa ser rotulado como linear ou associativo,
  • um tipo sujeito a operações range precisa ser rotulado como nil-empty ou nil-blocking,
  • um tipo com literais precisa ser rotulado como tendo campos ou índices, e
  • (talvez) um tipo com operações numéricas precise ser rotulado como ponto fixo ou flutuante.

Isso leva a uma linguagem de restrição muito mais estreita, talvez algo como:

TypeConstraint = "sliceable" | "map" | "chan" | "struct" | "integer" | "float" | "type"

com exemplos como:

func[T:integer, U, V] Foo(u U, v V) {
    var _ T = u.v(v) + 1;
}
func [S:sliceable, T] append(s S, x ...T) S {
    dst := s
    if cap(s) - len(s) < len(x) {
        dst = make(S, len(s), nextSizeClass(cap(s)))
        copy(dst, s)
    }
    copy(dst[len(s):cap(s)], x)
    return dst[:len(s)+len(x)]
}

Sinto que demos um grande passo em direção ao genérico personalizado ao introduzir o alias de tipo.
O alias de tipo possibilita supertipos (tipo de tipos).
Podemos tratar tipos como valores em using.

Para tornar as explicações mais simples, podemos adicionar um novo elemento de código, genre .
A relação entre gêneros e tipos é como a relação entre tipos e valores.
Em outras palavras, um gênero significa um tipo de tipos.

Cada tipo de tipo, exceto os tipos struct e interface e função, corresponde a um gênero pré-declarado.

  • Bool
  • Corda
  • Int8, Uint8, Int16, Uint16, Int32, Uint32, Int64, Uint64, Int, Uint, Uintptr
  • Float32, Float64
  • Complexo64, Complexo128
  • Matriz, Fatia, Mapa, Canal, Ponteiro, UnsafePointer

Existem alguns outros gêneros pré-declarados, como Comaprable, Numérico, Interger, Float, Complex, Container, etc. Podemos usar Type ou * denota o gênero de todos os tipos.

Os nomes de todos os gêneros integrados começam com uma letra maiúscula.

Cada tipo de estrutura e interface e função corresponde a um gênero.

Também podemos declarar gêneros personalizados:

genre Addable = Numeric | String
genre Orderable = Interger | Float | String
genre Validator = func(int) bool // each parameter and result type must be a specified type.
genre HaveFieldsAndMethods = {
    width  int // we must use a specific type to define the fields.
    height int // we can't use a genre to define the fields.
    Load(v []byte) error // each parameter and result type must be a specified type.
    DoSomthing()
}
genre GenreFromStruct = aStructType // declare a genre from a struct type
genre GenreFromInterface = anInterfaceType // declare a genre from an interface type
genre GenreFromStructInterface = aStructType + anInterfaceType
genre ComparableStruct = HaveFieldsAndMethods & Comprable
genre UncomparableStruct = HaveFieldsAndMethods &^ Comprable

Para tornar a explicação a seguir consistente, é necessário um modificador de gênero.
O modificador de gênero é indicado por Const . Por exemplo:

  • Const Integer é um gênero (diferente de Integer ) e sua instância deve ser um valor constante cujo tipo deve ser um inteiro. No entanto, o valor constante pode ser visto como um tipo especial.
  • Const func(int) bool é um gênero (diferente de func(int) bool ) e sua instância deve ser um valor de função delcared. No entanto, a declaração da função pode ser vista como um tipo especial.

(A solução do modificador é um pouco complicada, talvez existam outras soluções de design melhores.)

Ok, vamos continuar.
Precisamos de outro conceito. Encontrar um bom nome para ele não é fácil,
Vamos chamá-lo crate .
Geralmente, a relação entre engradados e gêneros é como a relação entre funções e tipos.
Uma caixa pode receber tipos como parâmetros e tipos de retorno.

Uma declaração de grade (suponha que o código a seguir seja declarado no pacote lib ):

crate Example [T Float, S {width, height T}, N Const Integer] [*, *, *] {
    type MyArray [N]T

    func Add(a, b T) T {
        return a+b
    }

    type M struct {
        x T
        y S
    }

    func (m *M) Area() T {
        m.DoSomthing()
        return m.y.width * m.y.height
    }

    func (m *M) Perimeter() T {
        return 2 * Add(m.y.width, m.y.height)
    }

    export M, Add, MyArray
}

Usando a caixa acima.

import "lib"

// We can use AddFunc as a normal delcared function.
// Its genre is "Const func (a, b T) T"
type Rect, AddFunc, Array = lib.Example[float32, struct{x, y float32}, 100]

func demo() {
    var r Rect
    a, p = r.Area(), r.Perimeter()
    _ = AddFunc(a, p)
}

Minhas ideias absorvem muitas ideias de outras mostradas acima.
Eles não estão muito maduros agora.
Eu as coloco aqui só porque eu acho que elas são interessantes,
e eu não quero melhorá-lo mais.
Tantas células cerebrais foram mortas ao consertar os buracos nas ideias.
Espero que essas idéias possam trazer algumas inspirações para outros esquilos.

O que você chama de “gênero” é na verdade chamado de “tipo”, e é bem conhecido no
comunidade de programação funcional. O que você chama de engradado é um restrito
tipo de functor ML.

Em quarta-feira, 4 de abril de 2018, 12h41, dotaheor [email protected] escreveu:

Sinto que demos um grande passo em direção ao genérico personalizado ao introduzir
tipo alias.
O alias de tipo possibilita supertipos (tipo de tipos).
Podemos tratar tipos como valores em using.

Para tornar as explicações mais simples, podemos adicionar um novo elemento de código, gênero.
A relação entre gêneros e tipos é como a relação entre tipos
e valores.
Em outras palavras, um gênero significa um tipo de tipos.

Cada tipo de tipo, exceto tipos de estrutura e interface e função,
corresponde a um gênero pré-declarado.

  • Bool
  • Corda
  • Int8, Uint8, Int16, Uint16, Int32, Uint32, Int64, Uint64, Int, Uint,
    Uintptr
    & Float32, Float64
  • Complexo64, Complexo128
  • Matriz, Fatia, Mapa, Canal, Ponteiro, UnsafePointer

Existem alguns outros gêneros pré-declarados, como Comaprable, Numérico,
Interger, Float, Complex, Container, etc. Podemos usar Type ou * denota
o gênero de todos os tipos.

Os nomes de todos os gêneros integrados começam com uma letra maiúscula.

Cada tipo de estrutura e interface e função corresponde a um gênero.

Também podemos declarar gêneros personalizados:

gênero Adicável = Numérico | Corda
gênero Ordenável = Interger | Flutuar | Corda
gene Validator = func(int) bool // cada parâmetro e tipo de resultado deve ser um tipo especificado.
genero HaveFieldsAndMethods = {
largura int // devemos usar um tipo específico para definir os campos.
height int // não podemos usar um gênero para definir os campos.
Load(v []byte) error // cada parâmetro e tipo de resultado deve ser um tipo especificado.
Fazer Algo()
}
gênero GenreFromStruct = aStructType // declara um gênero de um tipo struct
gênero GenreFromInterface = anInterfaceType // declara um gênero de um tipo de interface
gênero GenreFromStructInterface = aStructType | um Tipo de Interface

Para tornar a explicação a seguir consistente, é necessário um modificador de gênero.
O modificador de gênero é denotado por Const. Por exemplo:

  • Const Integer é um gênero e sua instância deve ser um valor constante
    qual tipo deve ser um número inteiro.
    No entanto, o valor constante pode ser visto como um tipo especial.
  • Const func(int) bool é um gênero e sua instância deve ser um delcared
    valor da função.
    No entanto, a declaração da função pode ser vista como um tipo especial.

(A solução do modificador é um pouco complicada, talvez haja outro design melhor
soluções.)

Ok, vamos continuar.
Precisamos de outro conceito. Encontrar um bom nome para ele não é fácil,
Vamos chamá-lo de caixa.
Geralmente, a relação entre caixas e gêneros é como a relação
entre funções e tipos.
Uma caixa pode receber tipos como parâmetros e tipos de retorno.

Uma declaração de grade (suponha que o código a seguir seja declarado em lib
pacote):

Exemplo de caixa [T Flutuante, S {largura, altura T}, N Const Integer] [*, *, *] {
digite MyArray [N]T

func Adicionar(a, b T) T {
retornar a+b
}

// Um ​​gênero de escopo de caixa. Só pode ser usado na caixa.

// M é um tipo de gênero G
tipo M estrutura {
xT
y S
}

func (m *M) Área() T {
m.DoSomthing()
return mywidth * myheight
}

func (m *M) Perímetro() T {
return 2 * Add(mywidth, myheight)
}

exportar M, Adicionar, MeuArray
}

Usando a caixa acima.

importar "lib"

// Podemos usar AddFunc como uma função delcared normal.
digite Rect, AddFunc, Array = lib.Example(float32, struct{x, y float32})

demonstração funcional() {
var r Rect
a, p = r.Área(), r.Perímetro()
_ = AdicionarFunc(a, p)
}

Minhas ideias absorvem muitas ideias de outras mostradas acima.
Eles não estão muito maduros agora.
Eu as coloco aqui só porque eu acho que elas são interessantes,
e eu não quero melhorá-lo mais.
Tantas células cerebrais foram mortas ao consertar os buracos nas ideias.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/15292#issuecomment-378665695 ou silenciar
o segmento
https://github.com/notifications/unsubscribe-auth/AGGWB78BrjN0BxRfroH-jRNy4mCXgSwCks5tlPfMgaJpZM4IG-xv
.

Eu sinto que há algumas diferenças entre Kind e Genre.

A propósito, se um engradado retornar apenas um tipo, podemos usar sua chamada como um tipo diretamente.

package lib

// export a type
crate List [T *] * {
    type List struct {
        ...
    }

    export List
}

use-o:

import "lib"

var l lib.List[int]

Haveria algumas regras de "dedução de gênero", assim como "dedução de tipo" no sistema atual.

@dotaheor , @DemiMarie está correto. Seu conceito de “gênero” soa exatamente como o “tipo” da teoria dos tipos. (Sua proposta exige uma regra de subclassificação, mas isso não é incomum.)

A palavra-chave genre em sua proposta define novos tipos como supertipos de tipos existentes. A palavra-chave crate define objetos com “assinaturas de engradados”, que são um tipo que não é um subtipo de Type .

Como um sistema formal, sua proposta parece ser algo como:

Caixa ::= χ | ⋯
Digite ::= τ | χ | int | bool | ⋯ | func(τ) | func(τ) τ | []τ | χ[τ₁, …]

CrateSig ::= [κ₁, …] ⇒ [κₙ, …]
Tipo ::= κ | exactly τ | kindOf κ | Map | Chan | ⋯ | Const κ | Type | CrateSig

Para abusar de alguma notação da teoria dos tipos:

  • Leia “⊢” como “implica”.
  • Leia “ k1k2 ” como “ k1 é um subtipo de k2 ”.
  • Leia “:” como “é do tipo”.

Então as regras são algo como:

τ : exactly τ
exactly τkindOf exactly τ
kindOf exactly τType

τ : κ₁κ₁κ₂τ : κ₂

τ₁ : Typeτ₂ : TypekindOf exactly map[τ₁]τ₂Map
MapType

κ₁κ₂Const κ₁Const κ₂

[…]
(E assim por diante, para todos os tipos integrados)


As definições de tipo conferem tipos, e os tipos subjacentes são reduzidos aos tipos de tipos internos:

type τ₁ τ₂τ₂ : κτ₁ : kindOf κ

kindOf kindOf κkindOf κ
kindOf MapMap
[…]


genre define novos relacionamentos de subtipo:
genre κ = κ₁ | κ₂κ₁κ
genre κ = κ₁ | κ₂κ₂κ

(Você pode definir Numeric e similares em termos de | .)

genre κ = κ₁ & κ₂ ∧ ( κ₃κ₁ ) ∧ ( κ₃κ₂ ) ⊢ κ₃κ


A regra de expansão da caixa é semelhante:
type τₙ, … = χ[τ₁, …] ∧ ( χ : [κ₁, …] ⇒ [κₙ, …] ) ∧ ( τ₁ : κ₁ ) ∧ ⋯ ⊢ τₙ : κₙ

Isso tudo é apenas falando sobre os tipos, é claro. Se você quiser transformá-lo em um sistema de tipos, também precisará de regras de tipo. 🙂


Então, o que você está descrevendo é uma forma muito bem compreendida de parametricidade. Isso é bom, pois é bem compreendido, mas decepcionante, pois não ajuda a resolver os problemas únicos que o Go apresenta.

Os problemas realmente interessantes e complicados que o Go apresenta são principalmente em torno da inspeção de tipo dinâmico. Como os parâmetros de tipo devem interagir com as declarações de tipo e reflexão?

(Por exemplo, deveria ser possível definir interfaces com métodos de tipos paramétricos? Em caso afirmativo, o que acontece se você digitar um valor dessa interface com um novo parâmetro em tempo de execução?)

Em uma nota relacionada, houve uma discussão sobre como tornar o código genérico sobre tipos internos e definidos pelo usuário? Como fazer código que pode lidar com bigints e inteiros primitivos?

Em uma nota relacionada, houve uma discussão sobre como tornar o código genérico sobre tipos internos e definidos pelo usuário? Como fazer código que pode lidar com bigints e inteiros primitivos?

Mecanismos baseados em classe de tipo, como em Genus e Familia, podem fazer isso com eficiência. Consulte nosso artigo PLDI 2015 para obter detalhes.

@DemiMarie
Eu acho que "gênero" == "conjunto de traços".

[editar]
Talvez traits seja uma palavra-chave melhor.
Podemos ver que cada tipo também é um conjunto de características.

A maioria das características são definidas para um único tipo apenas.
Mas um traço mais complexo pode definir uma relação entre dois tipos.

[editar 2]
supondo que existam dois conjuntos de características A e B, podemos fazer as seguintes operações:

A + B: union set
A - B: difference set
A & B: intersection set

O conjunto de características de um tipo de argumento deve ser um superconjunto do gênero de parâmetro correspondente (um conjunto de características).
O conjunto de características de um tipo de resultado deve ser um subconjunto do gênero de resultado correspondente (um conjunto de características).

(NA MINHA HUMILDE OPINIÃO)

Ainda acho que religar Type Aliases é o caminho a seguir, para adicionar genéricos ao Go. Não precisa de uma grande mudança na linguagem. Pacotes que são generalizados desta forma, ainda podem ser usados ​​no Go 1.x. E não há necessidade de adicionar restrições porque é possível fazer isso definindo o tipo padrão para o alias de tipo, para algo que já atende a essas restrições. E o aspecto mais importante da religação de aliases de tipo é que os tipos compostos internos (fatias, mapas e canais) não precisam ser alterados e generalizados.

@dc0d

Como os aliases de tipo devem substituir os genéricos?

@sighoya Rebinding Type Aliases pode substituir genéricos (não apenas aliases de tipo). Vamos supor que um pacote introduz alguns aliases de tipo de nível de pacote como:

package likedlist

type T = interface{}

type LinkedList struct {
    // ...
}

Se Type Alias ​​Rebinding (e recursos do compilador) for fornecido, é possível usar este pacote, para criar listas vinculadas para diferentes tipos concretos, em vez de interface vazia:

package main

import (
    "likedlist"
)

type intLL = likedlist.LinkedList(likedlist.T = int)
type stringLL = likedlist.LinkedList(likedlist.T = string)

func main() {}

Se usarmos alias como tal, a maneira a seguir é mais limpa.

// pkg.go
package pkg

type ListNode struct {
    prev, next *ListNode
    element    ?Element
}

func Add(x, y ?T) ?T {
    return x+y
}



// main.go
package main

import "pkg"

type intList = pkg.ListNode[Element=int]
func stringAdd = pkg.Add[T=string]

func main() {
}

@dc0d e como exatamente isso seria implementado? O código é bom, mas não diz nada sobre como ele realmente funciona por dentro. E, olhando para a história das propostas de genéricos, para Go é muito importante, não apenas como parece e se sente.

@dotaheor Isso é incompatível com o Go 1.x.

@creker Implementei uma ferramenta (chamada goreuse ) que usa essa técnica para gerar código e nasceu como um conceito para Type Alias ​​Rebinding.

Ele pode ser encontrado aqui . Há um vídeo de 15 minutos que explica a ferramenta.

@dc0d para que funcione como modelos C++ gerando implementações especializadas. Eu não acho que seria aceito como equipe Go (e, francamente, eu e muitas outras pessoas aqui) parece ser contra qualquer coisa semelhante aos modelos C++. Aumenta os binários, retarda a compilação, possivelmente não seria capaz de produzir erros significativos. E, além disso, não é compatível com pacotes somente binários que o Go suporta. É por isso que o C++ optou por escrever modelos em arquivos de cabeçalho.

@creker

por isso funciona como modelos C++ gerando implementações especializadas para cada tipo usado.

Eu não sei (Faz cerca de 16 anos desde que escrevi qualquer C++). Mas pela sua explicação parece que sim. No entanto, não tenho certeza se ou como eles são os mesmos.

Eu não acho que seria aceito como equipe Go (e, francamente, eu e muitas outras pessoas aqui) parece ser contra qualquer coisa semelhante aos modelos C++.

Claro que todos aqui têm boas razões para suas preferências com base em suas prioridades. O primeiro da minha lista é a compatibilidade com o Go 1.x.

Aumenta binários,

Pode.

retarda a compilação,

Eu duvido muito disso (como pode ser experimentado com goreuse ).

E, além disso, não é compatível com pacotes somente binários que o Go suporta.

Não tenho certeza. Outras formas de implementação de genéricos suportam isso?

possivelmente não seria capaz de produzir erros significativos.

Isso pode ser um pouco problemático. Ainda assim, isso acontece em tempo de compilação e pode ser compensado, empregando algumas ferramentas, em grande parte. Além disso, se o alias de tipo que atua como parâmetro de tipo para o pacote for uma interface, pode simplesmente ser verificado se é atribuível a partir do tipo fornecido concreto. Embora o problema para tipos primitivos como int e string e structs permaneça.

@dc0d

Eu penso um pouco sobre isso.
Além de ser estabelecido internamente nas interfaces, o 'T' no seu exemplo

type T=interface{}

é tratado como uma variável de tipo mutável, mas deve ser um alias para um tipo específico, ou seja, const referência a um tipo.
O que você quer é o tipo T, mas isso implicaria na introdução de genéricos.

@sighoya Não tenho certeza se entendi o que você disse.

É estabelecido internamente em interfaces

Não é verdade. Conforme descrito no meu comentário original, é possível usar tipos específicos que atendem a uma restrição. Por exemplo, o alias de tipo de parâmetro de tipo pode ser declarado como:

type T = int

E apenas os tipos que possuem o operador + (ou - ou * ; depende se esse operador é usado no corpo do pacote) podem ser usados ​​como valor de tipo que fica nesse parâmetro de tipo.

Portanto, não são apenas as interfaces que podem ser usadas como espaços reservados para parâmetros de tipo.

mas isso implicaria a introdução de genéricos.

Esta _é_ uma forma de introduzir/implementar genéricos na própria linguagem Go.

@dc0d

Para fornecer polimorfismo, você usará a interface{}, pois isso permite definir T para qualquer tipo posteriormente.

Definir 'tipo T=Int' não ganharia muito.

Se você disser que 'tipo T' é não declarado/indefinido primeiro, o que pode ser definido mais tarde, então você tem algo como genéricos.

O problema com isso é que 'T' mantém o módulo/pacote amplo e não é local para nenhuma função ou estrutura (tudo bem, talvez uma declaração de tipo aninhado em uma estrutura que pode ser acessada de fora).

Por que não escrever em vez disso?:

fun<type T>(t T)

ou

fun[type T](t T)

Além disso, precisamos de algum mecanismo de inferência de tipos para deduzir os tipos corretos ao chamar uma função genérica ou struct sem especialização de parâmetro de tipo em primeiro lugar.

@dc0d escreveu

E apenas os tipos que têm o operador + (ou - ou *; depende se esse operador é usado no corpo do pacote) podem ser usados ​​como um valor de tipo que fica nesse parâmetro de tipo.

Você pode elaborar mais sobre isso?

@sighoya

Para fornecer polimorfismo, você usará a interface{}, pois isso permite definir T para qualquer tipo posteriormente.

O polimorfismo não é alcançado por ter tipos compatíveis, ao religar aliases de tipo. A única restrição real é o corpo do pacote genérico. Devem ser compatíveis mecanicamente.

Você pode elaborar mais sobre isso?

Por exemplo, se um alias de tipo de parâmetro de tipo de nível de pacote for definido como:

package genericadd

type T = int

func Add(a, b T) T { return a + b }

Então praticamente todos os tipos numéricos podem ser atribuídos a T , como:

package main

import (
    "genericadd"
)

var add = genericadd.Add(
    T = float64
)

func main() {
    var (
        a, b float64
    )

    println(add(a, b))
}

@dc0d

No entanto, não tenho certeza se ou como eles são os mesmos.

Eles são os mesmos no sentido de que funcionam de maneira praticamente idêntica pelo que vejo. Para cada compilador de instanciação de modelo de classe geraria uma implementação exclusiva se for a primeira vez que ele vê o uso da combinação específica de modelo de classe e sua lista de parâmetros. Isso aumenta o tamanho do binário, pois agora você tem várias implementações do mesmo modelo de classe. Retarda a compilação, pois o compilador agora precisaria gerar essas implementações e fazer todos os tipos de verificações. No caso de C++, o aumento no tempo de compilação pode ser enorme. Seus exemplos de brinquedos são rápidos, mas os de C++ também.

Não tenho certeza. Outras formas de implementação de genéricos suportam isso?

Outras linguagens não têm problema com isso. Em particular, C# é o mais familiar para mim. Mas ele usa geração de código em tempo de execução que a equipe Go descarta completamente. Java também funciona, mas sua implementação não é a melhor, para dizer o mínimo. Algumas das propostas do ianlancetaylor podem lidar com pacotes somente binários pelo que eu entendo.

A única coisa que não entendo é se os pacotes somente binários devem ser suportados. Não os vejo mencionados explicitamente nas propostas. Eu realmente não me importo com eles, mas ainda assim, é um recurso de linguagem.

Apenas para testar meu entendimento... considere este repositório de algoritmos de copiar/colar [ aqui ]. A menos que você queira usar "int", o código não pode ser usado diretamente. Deve ser copiado, colado e modificado para funcionar. E por modificações, quero dizer que cada instância de "int" deve ser alterada para qualquer tipo que você realmente precise.

A abordagem de alias de tipo faria as modificações uma vez para, digamos, T, e inseriria uma linha "tipo T int". Então o compilador precisaria religar T a outra coisa, digamos float64.

Portanto:
a) Eu diria que não haveria lentidão do compilador a menos que você realmente usasse essa técnica. Então é sua escolha.
b) Dado o novo material vgo, onde várias versões do mesmo código podem ser usadas... o que significa que deve haver algum método de esconder as fontes usadas, então certamente o compilador pode acompanhar se dois usos da mesma reencadernação são usados ​​e evitam a duplicação. Então eu acho que o excesso de código seria o mesmo que as técnicas atuais de copiar/colar.

Parece-me que entre os aliases de tipo e o vgo vindouro, as bases para essa abordagem aos genéricos estão quase completas ...

Existem alguns "desconhecidos" listados na proposta [ aqui ]. Então seria bom aprofundar um pouco mais.

@mandolyte você pode adicionar outro nível de indireção envolvendo tipos especializados em algum contêiner geral. Dessa forma, sua implementação pode permanecer a mesma. O compilador fará então toda a mágica. Acho que a proposta de parâmetros de tipo de Ian funciona dessa maneira.

Acho que o usuário precisa de uma escolha entre o apagamento de tipo e a monomorfização.
O último é o motivo pelo qual o Rust fornece abstrações de custo zero. Vá também.

Em segunda-feira, 9 de abril de 2018, 8h32 Antonenko Artem [email protected]
escreveu:

@mandolyte https://github.com/mandolyte você pode adicionar outro nível de
indireção envolvendo tipos especializados em algum contêiner geral. que
como sua implementação pode permanecer a mesma. O compilador fará então tudo
a mágica. Acho que a proposta de parâmetros de tipo de Ian funciona dessa maneira.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/15292#issuecomment-379735199 ou silenciar
o segmento
https://github.com/notifications/unsubscribe-auth/AGGWB1v9h5kWmuHCBuoewTTSX751OHgrks5tm1TsgaJpZM4IG-xv
.

Parece-me que há uma confusão compreensível nesta discussão sobre a troca entre modularidade e desempenho. A técnica C++ de redigitalização e instanciação de código genérico em cada tipo para o qual é usado é ruim para modularidade, ruim para distribuições binárias e, devido ao excesso de código, ruim para desempenho. A parte boa dessa abordagem é que ela especializa automaticamente o código gerado para os tipos que estão sendo usados, o que é particularmente útil quando os tipos usados ​​são tipos primitivos como int . Java traduz homogeneamente o código genérico, mas paga um preço pelo desempenho, principalmente quando o código usa o tipo T[] .

Felizmente, existem algumas maneiras de resolver isso sem a não modularidade do C++ e sem a geração de código em tempo de execução completo:

  1. Gere instanciações especializadas para tipos primitivos. Isso pode ser feito automaticamente ou por diretiva do programador. Algum despacho é necessário para acessar a instanciação correta, mas pode ser dobrado no despacho já necessário por uma tradução homogênea. Isso funcionaria de maneira semelhante ao C#, mas não requer geração de código em tempo de execução completo; um pouco de suporte extra pode ser desejável no tempo de execução para configurar tabelas de despacho como carregamentos de código.
  2. Use uma única implementação genérica na qual um array de T é realmente representado como um array de um tipo primitivo quando T é instanciado como um tipo primitivo. Essa abordagem, que usamos no PolyJ, Genus e Familia, melhora muito o desempenho em relação à abordagem Java, embora não seja tão rápida quanto uma implementação totalmente especializada.

@dc0d

O polimorfismo não é alcançado por ter tipos compatíveis, ao religar aliases de tipo. A única restrição real é o corpo do pacote genérico. Devem ser compatíveis mecanicamente.

Tipo aliases é o caminho errado, porque deve ser uma referência constante.
É melhor escrever 'T Type' diretamente e então você verá que usa de fato genéricos.

Por que você deseja usar uma variável de tipo global 'T' para todo o pacote/módulo, o tipo local vars em <> ou [] é mais modular.

@creker

Em particular, C# é o mais familiar para mim. Mas ele usa geração de código em tempo de execução que a equipe Go descarta completamente.

Para tipos de referência, mas não para tipos de valor.

@DemiMarie

Acho que o usuário precisa de uma escolha entre o apagamento de tipo e a monomorfização.
O último é o motivo pelo qual o Rust fornece abstrações de custo zero. Vá também.

"Type Erasure" é ambíguo, vou assumir que você quer dizer Type Parameter Erasure, o que o Java fornece que também não é bem verdade.
Java tem monomorfização, mas monomorfiza (semi) constantemente para o limite superior na restrição genérica que é principalmente Object.
Para fornecer métodos e campos de outros tipos, o limite superior é convertido internamente para o seu tipo apropriado, o que é bastante feio.
Se o projeto Valhalla for aceito, as coisas mudarão para tipos de valor, mas infelizmente não para tipos de referência.

Go não precisa seguir o Java Way porque:

"A compatibilidade binária para pacotes compilados não é garantida entre versões"

enquanto isso não é possível em Java.

Parece-me que há uma confusão compreensível nesta discussão sobre a troca entre modularidade e desempenho. A técnica C++ de redigitalização e instanciação de código genérico em cada tipo para o qual é usado é ruim para modularidade, ruim para distribuições binárias e, devido ao excesso de código, ruim para desempenho.

De que tipo de performance você está falando aqui?

Se por “ingestão de código” e “desempenho” você quer dizer “tamanho binário” e “pressão de cache de instrução”, então o problema é bastante simples de resolver: contanto que você não retenha informações de depuração para cada especialização, você pode recolher funções com os mesmos corpos na mesma função no momento do link (o chamado “modelo Borland” ). Isso trata trivialmente as especializações para tipos primitivos e tipos sem chamadas para métodos não triviais.

Se por “ingestão de código” e “desempenho” você quer dizer “tamanho de entrada do vinculador” e “tempo de vinculação”, o problema também é bastante direto, se você puder fazer certas suposições (razoáveis) sobre seu sistema de compilação. Em vez de emitir cada especialização em cada unidade de compilação, você pode emitir uma lista de especializações necessárias e fazer com que o sistema de compilação instancie cada especialização exclusiva exatamente uma vez antes da vinculação (o “modelo Cfront”). IIRC, este é um dos problemas que os módulos C++ tentam resolver.

Portanto, a menos que você queira dizer um terceiro tipo de “inchaço de código” e “desempenho” que eu perdi, parece que você está falando sobre um problema com a implementação, não com a especificação: _contanto que a implementação não retenha depuração em excesso informações,_ os problemas de desempenho são bastante simples de resolver.


O maior problema para Go é que, se não formos cuidadosos, torna-se possível usar asserções de tipo ou reflexão para produzir uma nova instância de um tipo parametrizado em tempo de execução, o que nenhuma quantidade de inteligência de implementação – a não ser um todo caro. – análise de programa – pode corrigir.

Isso é realmente uma falha de modularidade, mas não tem nada a ver com o excesso de código: em vez disso, vem do fato de que os tipos de funções (e métodos) Go não capturam um conjunto completo de restrições em seus argumentos.

@sighoya

Para tipos de referência, mas não para tipos de valor.

Pelo que li, C# JIT faz especialização em tempo de execução para cada tipo de valor e uma vez para todos os tipos de referência. Não há especialização em tempo de compilação (tempo IL). É por isso que a abordagem C# é completamente ignorada - a equipe Go não quer depender da geração de código em tempo de execução, pois limita as plataformas nas quais Go pode ser executado. Em particular, no iOS, você não tem permissão para gerar código em tempo de execução. Funciona e eu realmente fiz um pouco disso, mas a Apple não permite isso na AppStore.

Como você fez isso?

Em segunda-feira, 9 de abril de 2018, 15h41 Antonenko Artem [email protected]
escreveu:

@sighoya https://github.com/sighoya

Para tipos de referência, mas não para tipos de valor.

Pelo que li, C# JIT faz especialização em tempo de execução para cada valor
type e uma vez para todos os tipos de referência. Não há tempo de compilação
especialização. É por isso que a abordagem C# é completamente ignorada - Equipe Go
não quer depender da geração de código em tempo de execução, pois limita as plataformas Go
pode correr. Em particular, no iOS, você não tem permissão para gerar código
em tempo de execução. Funciona e eu fiz um pouco disso, mas a Apple não permite
na AppStore.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/15292#issuecomment-379870005 ou silenciar
o segmento
https://github.com/notifications/unsubscribe-auth/AGGWB-tslGeUSGXl2ZlEDLf0dCATUaYvks5tm7lvgaJpZM4IG-xv
.

@DemiMarie lançou meu antigo código de pesquisa apenas para ter certeza (essa pesquisa foi descartada por outros motivos). Mais uma vez, o depurador me enganou. Eu aloco uma página, escrevo algumas instruções para ela, protejo-a com PROT_EXEC e pulo para ela. No depurador funciona. Sem o aplicativo depurador é SIGKILLed com mensagem CODESIGN no log de falhas, conforme o esperado. Então, não funciona mesmo sem AppStore. Argumento ainda mais forte contra a geração de código em tempo de execução se o iOS for importante para o Go.

Primeiro, seria útil refletir mais uma vez sobre as 5 Regras de Programação de Rob Pike .

Segundo (IMHO):

Sobre compilação lenta e tamanho binário, quantos tipos genéricos são usados ​​em tipos comuns de aplicativos que estão sendo desenvolvidos usando Go (_n geralmente é pequeno_ da Regra 3)? A menos que o problema precise de um alto nível de cardinalidade em conceitos concretos (grande número de tipos), essa sobrecarga pode ser ignorada. Mesmo assim, eu argumentaria que algo está errado com essa abordagem. Ao implementar um sistema de e-commerce, ninguém define um tipo separado para cada tipo de produto e suas variações e talvez as possíveis customizações.

Verbosidade é uma boa forma de simplicidade e familiaridade (por exemplo, na sintaxe) que torna as coisas mais óbvias e limpas. Embora eu duvide que o excesso de código seja maior usando o Type Alias ​​Rebinding, gosto da sintaxe familiar do Go-ish e da verbosidade óbvia que a acompanha. Um dos objetivos do Go é ser fácil de ler (embora eu pessoalmente ache relativamente fácil e agradável escrever também).

Eu não entendo como isso pode prejudicar o desempenho porque em tempo de execução, apenas tipos limitados concretos estão sendo usados, os quais foram gerados em tempo de compilação. Não há sobrecarga de tempo de execução.

A única preocupação com o Type Alias ​​Rebinding que vejo pode ser a distribuição binária.

@dc0d prejudicar o desempenho geralmente significa preencher o cache de instruções devido a diferentes implementações de modelos de classe. Como exatamente isso se relaciona com o desempenho real é uma questão em aberto, não conheço nenhum benchmark, mas teoricamente é um problema.

Quanto ao tamanho binário. É outra questão teórica que as pessoas geralmente trazem à tona (como fiz anteriormente), mas como o código real sofrerá com isso é, novamente, uma questão em aberto. Por exemplo, a especialização para todos os tipos de ponteiro e interface pode ser a mesma, eu acho. Mas a especialização para todos os tipos de valor seria única. E isso também inclui estruturas. O uso de contêineres genéricos para armazená-los é comum e causaria um volume significativo de código, pois as implementações de contêineres genéricos não são pequenas.

A única preocupação com o Type Alias ​​Rebinding que vejo pode ser a distribuição binária.

Aqui ainda não tenho certeza. A proposta de genéricos tem que suportar pacotes somente binários ou podemos apenas mencionar que pacotes somente binários não suportam genéricos. Seria muito mais fácil, com certeza.

Como foi mencionado anteriormente, se não for necessário dar suporte à depuração, um
pode combinar instanciações de template idênticas.

Em terça-feira, 10 de abril de 2018, 5h46 Kaveh Shahbazian [email protected]
escreveu:

Primeiro, seria útil refletir sobre as 5 regras de programação de Rob Pike
https://users.ece.utexas.edu/%7Eadnan/pike.html mais uma vez.

Segundo (IMHO):

Sobre compilação lenta e tamanho binário, quantos tipos genéricos são usados ​​em
tipos comuns de aplicativos que estão sendo desenvolvidos usando Go ( n égeralmente pequeno da Regra 3)? A menos que o problema precise de um alto nível de
cardinalidade em conceitos concretos (grande número de tipos) que a sobrecarga pode
ser esquecido. Mesmo assim, eu argumentaria que algo está errado com isso
aproximação. Ao implementar um sistema de e-commerce, ninguém define um
tipo para cada tipo de produto e suas variações e talvez as possíveis
personalizações.

Verbosidade é uma boa forma de simplicidade e familiaridade (por exemplo, em
sintaxe) o que torna as coisas mais óbvias e limpas. Enquanto eu duvido disso
código bloat seria maior usando Type Alias ​​Rebinding, eu gosto do
sintaxe Go-ish familiar e a verbosidade óbvia que a acompanha. Um de
os objetivos do Go é ser fácil de ler (enquanto eu pessoalmente acho
relativamente fácil e agradável de escrever também).

Eu não entendo como isso pode prejudicar o desempenho porque em tempo de execução, apenas
estão sendo usados ​​tipos delimitados de concreto que foram gerados em
tempo de compilação. Não há sobrecarga de tempo de execução.

A única preocupação com o Type Alias ​​Rebinding que vejo, pode ser o binário
distribuição.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/15292#issuecomment-380040032 ou silenciar
o segmento
https://github.com/notifications/unsubscribe-auth/AGGWB6aDfoHz2wbsmu8mCGEt652G_VE9ks5tnH9xgaJpZM4IG-xv
.

As instanciações nem precisam ser “idênticas” no sentido de “usar os mesmos argumentos”, ou mesmo “usar argumentos com o mesmo tipo subjacente”. Eles só precisam estar próximos o suficiente para resultar no mesmo código gerado. (Para Go, isso também implica “as mesmas máscaras de ponteiro”.)

@creker

Pelo que li, C# JIT faz especialização em tempo de execução para cada tipo de valor e uma vez para todos os tipos de referência. Não há especialização em tempo de compilação (tempo IL).

Bem, isso às vezes é um pouco complicado porque seu código de byte é interpretado apenas a tempo antes do código ser executado, então a geração de código é feita antes de executar o programa, mas após a compilação, então você está certo no sentido da vm que está sendo executada enquanto o código é gerado.

Eu acho que o sistema genérico de c# seria bom se, em vez disso, gerarmos código em tempo de compilação.
A geração de código de tempo de execução no sentido de c# não é possível com go, porque go não é uma vm.

@dc0d

A única preocupação com o Type Alias ​​Rebinding que vejo pode ser a distribuição binária.

Você pode elaborar um pouco.

@sighoya Meu erro; Eu quis dizer não distribuição binária, mas pacotes binários - que pessoalmente não tenho ideia de quão importante é.

@creker Belo resumo! (MO) A menos que uma razão forte seja encontrada, qualquer forma de sobrecarregar as construções da linguagem Go deve ser evitada. Uma razão para usar o Type Alias ​​Rebinding é evitar a sobrecarga de tipos compostos internos, como fatias ou mapas.

Verbosidade é uma boa forma de simplicidade e familiaridade (por exemplo, na sintaxe) que torna as coisas mais óbvias e limpas. Embora eu duvide que o excesso de código seja maior usando o Type Alias ​​Rebinding, gosto da sintaxe familiar do Go-ish e da verbosidade óbvia que a acompanha. Um dos objetivos do Go é ser fácil de ler (embora eu pessoalmente ache relativamente fácil e agradável escrever também).

Discordo dessa noção. Sua proposta forçará os usuários a fazer a coisa mais difícil conhecida por qualquer programador - nomear as coisas. Então, vamos acabar com um código cheio de notação húngara, que não só parece ruim, é desnecessariamente verboso e causa gagueira. Além disso, outras propostas também trazem uma sintaxe go-ish e, ao mesmo tempo, não apresentam esses problemas.

Existem três categorias de nomes que temos que inventar diariamente:

  • Para Entidades/Lógicas de Domínio
  • Tipos de dados/lógica do fluxo de trabalho do programa
  • Serviços/Tipos de Dados de Interface/Lógica

Quantas vezes um programador conseguiu evitar nomear qualquer coisa em seu código, nunca?

Difícil ou não, precisa ser feito diariamente. E a maioria de seus obstáculos vem da incompetência na estruturação de uma base de código - não das dificuldades do próprio processo de nomenclatura. Essa citação - pelo menos em sua forma atual - fez um grande desserviço ao mundo da programação até agora. Ele simplesmente tenta enfatizar a importância da nomeação. Porque nos comunicamos por meio de nomes em nosso código.

E os nomes se tornam muito mais poderosos quando acompanham uma prática de estruturação de código; tanto em termos de layout de código (um arquivo, estrutura de diretórios, pacotes/módulos) quanto práticas (padrões de design, abstrações de serviços - como REST, gerenciamento de recursos - programação concorrente, acesso ao disco rígido, taxa de transferência/latência).

Quanto à sintaxe e à verbosidade, prefiro a verbosidade à concisão inteligente (pelo menos no contexto de Go) - novamente, Go deve ser fácil de ler, não necessariamente fácil de escrever (o que estranhamente também acho bom nisso) .

Li muitos relatos de experiência e propostas sobre o porquê e como implementar genéricos em Go.

Você se importa se eu tentar realmente implementá-los no meu interpretador Go gomacro ?

Tenho alguma experiência no assunto, tendo adicionado genéricos a dois idiomas no passado

  1. uma linguagem agora abandonada que criei quando era ingênuo :) Transpilou para o código-fonte C
  2. Common Lisp com minha biblioteca cl-parametric-types - também suporta especializações parciais e completas de tipos e funções genéricos

@cosmos72 daria um bom relato de experiência ver um protótipo de uma técnica que preservasse a segurança do tipo.

Acabei de começar a trabalhar nisso. Você pode acompanhar o progresso em https://github.com/cosmos72/gomacro/tree/generics-v1

No momento estou começando com uma mistura (ligeiramente modificada) da terceira e quarta proposta de Ian listada em https://github.com/golang/proposal/blob/master/design/15292-generics.md#Proposal

@cosmos72 Há um resumo das propostas no link abaixo. Sua mistura é uma delas?
https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4

Eu li esse documento, ele resume muitas abordagens diferentes para genéricos por várias linguagens de programação.

No momento estou indo para a técnica de "especialização de tipo" usada por C++, Rust e outros, possivelmente com um pouco de "escopos de modelo parametrizado" porque a sintaxe mais geral do Go para novos tipos é type ( Foo ...; Bar ...) e estou estendendo para template[T1,T2...] type ( Foo ...; Bar ...) .
Além disso, estou mantendo a porta aberta para "Especialização restrita".

Eu gostaria também de implementar a "especialização de função polimórfica", ou seja, fazer com que a especialização seja inferida automaticamente pela linguagem no local da chamada se não for especificada pelo programador, mas acho que pode ser um pouco complexo de implementar. Vamos ver.

A mistura a que me referi é entre https://github.com/golang/proposal/blob/master/design/15292/2013-10-gen.md e https://github.com/golang/proposal/blob/ master/design/15292/2013-12-type-params.md

Atualização: para evitar enviar spam para esta edição oficial do Go além do anúncio inicial, provavelmente é melhor continuar a discussão específica do gomacro na edição gomacro nº 24: adicionar genéricos

Atualização 2: primeiras funções de modelo compiladas e executadas com sucesso. Veja https://github.com/cosmos72/gomacro/tree/generics-v1

Só para constar, é possível reformular minha opinião (sobre genéricos e Type Alias ​​Rebinding):

Os genéricos devem ser adicionados como um recurso do compilador (geração de código, modelos, etc), não um recurso de linguagem (interferindo no sistema de tipos do Go em todos os níveis).

@dc0d
Mas os modelos C++ não são um compilador e um recurso de linguagem?

@sighoya A última vez que escrevi C++ profissionalmente foi por volta de 2001. Então, posso estar errado. Mas assumindo que as implicações da nomenclatura são precisas - a parte do "modelo" - sim (ou melhor, não); pode ser um recurso do compilador (e não um recurso de linguagem), acompanhado por algumas construções de linguagem, que provavelmente não estão sobrecarregando nenhuma construção de linguagem que esteja envolvida no sistema de tipos.

Eu apoio @dc0d. Se você considerar, esse recurso nada mais seria do que um gerador de código integrado.

Sim: o tamanho do binário pode e VAI aumentar, mas agora usamos geradores de código, que são praticamente os mesmos, mas como um recurso externo. Se eu tiver que criar meu modelo como:

type BinaryTreeOfStrings struct {
    left, right *BinaryTreeOfStrings;
    string content;
}

// Its methods here

type BinaryTreeOfBigInts struct {
    left, right *BinaryTreeOfBigInts;
    uint64 content;
}

// AGAIN the same methods but different type

... Eu realmente gostaria que, em vez de copiar ou usar uma ferramenta externa, esse recurso se tornasse parte do próprio compilador.

Observe:

  • Sim, o código final seria duplicado. Certo como se usássemos um gerador. E o binário seria maior.
  • Sim, a ideia não é original, mas emprestada do C++.
  • Sim, funções de MyTypenão envolvendo nada com o tipo T (direta ou indiretamente) também seria repetido. Isso poderia ser otimizado (por exemplo, métodos que se referem a algo do tipo T - que não o ponteiro para o objeto de recebimento da mensagem - serão gerados para cada T ; métodos que contêm invocações para métodos que ser gerado para cada T , também será gerado para cada T , recursivamente - enquanto métodos onde sua única referência a T é *T no receptor, e outros métodos que chamem apenas aqueles métodos seguros e satisfaçam os mesmos critérios, podem ser feitos apenas uma vez). De qualquer forma, IMO este ponto é grande e menos direto: eu ficaria muito feliz mesmo que essa otimização não exista.
  • Os argumentos de tipo devem ser explícitos na minha opinião. Especialmente quando um objeto satisfaz interfaces potencialmente infinitas. Novamente: um gerador de código.

Até agora em meu comentário, minha proposta é implementá-lo como está: como um gerador de código suportado por compilador , em vez de uma ferramenta externa.

Seria lamentável para Go seguir a rota C++. Muitas pessoas veem a abordagem C++ como uma bagunça que colocou os programadores contra toda a ideia de genéricos: dificuldade de depuração, falta de modularidade, excesso de código. Todas as soluções de "gerador de código" são realmente apenas substituição de macro - se é assim que você deseja escrever código, por que precisamos de suporte ao compilador?

@andrewcmyers Eu tive essa proposta Type Alias ​​Rebinding na qual escrevemos apenas pacotes normais e em vez de usar interface{} explicitamente, apenas o usamos como type T = interface{} como um parâmetro genérico em nível de pacote. E isso é tudo.

  • Nós o depuramos como um pacote normal - é um código real, não uma criatura de meia-vida intermediária.
  • Não há necessidade de se intrometer no sistema do tipo Go em todos os níveis - pense apenas na atribuição.
  • É explícito. Nenhum mojo escondido. Claro que pode-se achar que não é possível encadear chamadas genéricas sem problemas, uma desvantagem. Eu vejo isso como um avanço! Alterando o tipo em duas chamadas consecutivas, em uma declaração não é Goish (IMO).
  • E o melhor de tudo, é compatível com a série Go 1.x (x >= 8).

Embora a ideia não seja nova, a forma como Go permite implementá-la é pragmática e clara.

Bônus adicional: não há sobrecarga de operadores em Go. Mas definindo o valor padrão do alias de tipo como (por exemplo) type T = int , não os únicos tipos válidos que podem ser usados ​​para personalizar este pacote genérico, são tipos numéricos que possuem uma implementação interna para + operador.

Além disso, o parâmetro de tipo de alias pode ser forçado a preencher mais de uma interface apenas adicionando alguns tipos e instruções de validador.

Agora, isso seria muito feio usando qualquer notação explícita para um tipo genérico que tem um parâmetro que implementa as interfaces Error e Stringer e também é um tipo numérico que suporta o operador + !

agora usamos geradores de código, que são praticamente os mesmos, mas como um recurso externo.

A diferença é que a maneira amplamente aceita de gerar código (via go generate ) acontece em tempo de confirmação/desenvolvimento, não em tempo de compilação. Fazer isso em tempo de compilação implica que você precisa permitir a execução de código arbitrário no compilador, as bibliotecas podem aumentar os tempos de compilação em ordens de magnitude e/ou você terá dependências de compilação separadas (ou seja, o código não pode mais ser construído apenas com o Go ferramenta). Eu gosto de Go por empurrar a invocação de meta-programação para o desenvolvedor upstream.

Ou seja, como todas as abordagens para resolver esses problemas, essa abordagem também possui desvantagens e envolve trade-offs. Pessoalmente, eu diria que os genéricos reais com suporte no sistema de tipos não são apenas melhores (ou seja, têm um conjunto de recursos mais poderoso), mas também podem manter a vantagem de uma compilação previsível e segura.

Vou ler todas as coisas acima, eu prometo, e ainda adicionarei um pouco também - GoLang SDK para Apache Beam parece um exemplo / vitrine bastante brilhante de problemas que o designer de bibliotecas tem que suportar para fazer qualquer coisa _corretamente_ de alto nível.

Há pelo menos duas implementações experimentais para genéricos Go. No início desta semana eu passei algum tempo com (1). Fiquei satisfeito ao descobrir que o impacto na legibilidade do código era mínimo. E descobri que o uso de funções anônimas para fornecer testes de igualdade funcionou bem; então estou convencido de que a sobrecarga do operador não é necessária. O único problema que encontrei foi no tratamento de erros. O idioma comum de “return nil,err” não funcionará se o tipo for, digamos, um inteiro ou uma string. Existem várias maneiras de contornar isso, todas com um custo de complexidade. Posso ser um pouco estranho, mas gosto do tratamento de erros do Go. Portanto, isso me leva a observar que uma solução genérica Go deve ter uma palavra-chave universal para o valor zero de um tipo. O compilador simplesmente o substituiria por zero para tipos numéricos, uma string vazia para tipos de string e nil para structs.

Embora essa implementação não imponha uma abordagem em nível de pacote, certamente seria natural fazê-lo. E, é claro, essa implementação não abordou todos os detalhes técnicos sobre onde o código instanciado pelo compilador deve ir (se houver), como os depuradores de código funcionariam etc.

Foi muito bom usar o mesmo código de algoritmo para inteiros e algo como um Point:

type Point struct {
    x,y int
}

Veja (2) para meus testes e observações.

(1) https://github.com/albrow/fo; o outro é o mencionado https://github.com/cosmos72/gomacro#generics
(2) https://github.com/mandolyte/fo-experiments

@mandolyte Você pode usar *new(T) para obter o valor zero de qualquer tipo.

Uma construção de linguagem como default(T) ou zero(T) (o primeiro é aquele
em C# IIRC) seria claro, mas OTOH maior que *new(T) (embora mais
performer).

2018-07-06 9:15 GMT-05:00 Tom Thorogood [email protected] :

@mandolyte https://github.com/mandolyte Você pode usar *new(T) para obter o
valor zero de qualquer tipo.


Você está recebendo isso porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/15292#issuecomment-403046735 ou silenciar
o segmento
https://github.com/notifications/unsubscribe-auth/AlhWhQ5cQwnc3x_XUldyJXCHYzmr6aN3ks5uD3ETgaJpZM4IG-xv
.

--
Este é um teste para assinaturas de correio a serem usadas no TripleMint

19642 é para discutir um valor zero genérico

@tmthrgd De alguma forma, perdi esse pequeno detalhe. Obrigado!

prelúdio

Os genéricos têm tudo a ver com construções personalizáveis ​​especializadas. Três categorias de especialização são:

  • Tipos especializados, Type<T> - um _array_;
  • Computações especializadas, F<T>(T) ou F<T>(Type<T>) - um _array classificável_;
  • Notação especializada, _LINQ_ por exemplo - instruções select ou for em Go;

Claro que existem linguagens de programação que apresentam construções ainda mais genéricas. Mas linguagens de programação convencionais como _C++_, _C#_ ou _Java_ fornecem mais ou menos construções de linguagem limitadas a esta lista.

pensamentos

A primeira categoria de tipos/construtos genéricos deve ser independente de tipo.

A segunda categoria de tipos/construtos genéricos precisa _agir_ sobre uma _propriedade_ do parâmetro de tipo. Por exemplo, um _array classificável_ deve ser capaz de _comparar_ a _propriedade comparável_ de seus itens. Assumindo que T.(P) é uma propriedade de T e A(T.(P)) é um cálculo/ação que atua sobre essa propriedade, o (A, .(P)) pode ser aplicado a cada item individual ou ser declarado como uma computação especializada, passada para a computação personalizável original. Um exemplo do último caso em Go é a interface sort.Interface que também tem a função de contrapartida separada sort.Reverse .

A terceira categoria de tipos/construtos genéricos são notações de linguagem _tipo-especializadas_ - parece não ser uma coisa Go _em geral_.

perguntas

continua ...

Qualquer feedback mais descritivo do que um emoji é muito bem-vindo!

@dc0d Eu recomendaria estudar os "Elementos de programação" de Sepanov antes de tentar definir os genéricos. O TL;DR é escrever um código concreto para começar, digamos um algoritmo que ordena um array. Mais tarde, adicionamos outros tipos de coleção como um Btree, etc. Notamos que estamos escrevendo muitas cópias do algoritmo de classificação que são essencialmente as mesmas, então definimos algum conceito, digamos 'classificável'. Agora queremos categorizar os algoritmos de ordenação, talvez pelo padrão de acesso que eles requerem, digamos, apenas encaminhamento, passagem única (um fluxo), encaminhamento apenas passagem múltipla (uma lista vinculada simples), bidirecional (uma lista duplamente vinculada), acesso aleatório (uma variedade). Quando adicionamos um novo tipo de coleção, precisamos apenas indicar em qual categoria de "coordenada" ele se enquadra para obter acesso a todos os algoritmos de classificação relevantes. Essas categorias de algoritmos são muito parecidas com interfaces 'Go'. Eu estaria procurando estender interfaces em Go para suportar vários parâmetros de tipo e tipos abstratos/associados. Eu não acho que as funções precisam de parametrização de tipo ad-hoc.

@dc0d Como uma tentativa de dividir os genéricos em partes componentes, eu não havia considerado 3, "notação especializada", como sua própria parte separada antes. Talvez possa ser caracterizado como definindo DSLs utilizando restrições de tipo.

Eu poderia argumentar que seu 1 e 2 são "estruturas de dados" e "algoritmos", respectivamente. Com essa terminologia, fica um pouco mais claro por que pode ser difícil separá-los de forma limpa, pois geralmente são altamente dependentes um do outro. Mas sort.Interface é um bom exemplo de onde você pode traçar uma linha entre armazenamento e comportamento (com um pouco de açúcar recente para torná-lo mais agradável), uma vez que codifica os requisitos Indexable e Comparable no comportamento mínimo necessário para implementar o algoritmo de classificação com "swap" e "less" (e len). Mas isso parece quebrar em estruturas de dados mais complicadas, como árvores ou heaps, que atualmente exigem algumas contorções para mapear o comportamento puro como interfaces Go.

Eu poderia imaginar uma adição genérica relativamente pequena às interfaces (ou não) que poderia permitir que a maioria das estruturas de dados e algoritmos de livros didáticos fossem implementados de forma relativamente limpa, sem contorções (como sort.Interface é hoje), mas não é poderoso o suficiente para projetar DSLs. Se queremos nos limitar a uma implementação de genéricos tão restrita quando estamos tendo todo o trabalho de adicionar genéricos é uma questão diferente.

Estruturas de coordenadas @infogulch para árvores binárias são "coordenadas bifurcadas" e existem equivalentes para outras árvores. No entanto, você também pode projetar a ordenação de uma árvore por meio de uma das três ordens, pré-encomenda, em ordem e pós-encomenda. Tendo decidido por um deles, a árvore pode ser endereçada como uma coordenada bidirecional, e a família de algoritmos de classificação definida em coordenadas bidirecionais seria otimamente eficiente.

O ponto é que você categoriza os algoritmos de classificação por seus padrões de acesso. Há apenas um número finito de algoritmos de ordenação ótimos para cada padrão de acesso. Você não se importa com as estruturas de dados neste momento. Falar sobre estruturas mais complexas perde o foco, queremos categorizar a família de algoritmos de classificação e não as estruturas de dados. Quaisquer dados que você tenha, você terá que usar um dos algoritmos que existem para classificá-los, então a questão é qual das categorizações de padrões de acesso a dados disponíveis de algoritmos de classificação é ideal para as estruturas de dados que você possui.

(NA MINHA HUMILDE OPINIÃO)

@infogulch

Talvez possa ser caracterizado como definindo DSLs utilizando restrições de tipo

Você está certo. Mas como eles fazem parte do conjunto de construções de linguagem, a IMO chamá-los de DSLs seria um pouco impreciso.

1 e 2 ... muitas vezes são altamente dependentes

Novamente verdade. Mas há muitos casos em que há necessidade de um tipo de contêiner ser passado, enquanto o uso real ainda não está decidido - nesse ponto em um programa. É por isso que 1 é necessário para ser estudado por conta própria.

sort.Interface é um bom exemplo de onde você pode traçar uma linha entre _storage_ e _behavior_

Bem dito;

isso parece quebrar em estruturas de dados mais complicadas

Essa é uma das minhas perguntas: generalizar o parâmetro type e descrevê-lo em termos de restrições (como List<T> where T:new, IDisposable ) ou fornecer um _protocol_ generalizado aplicável a todos os itens (de um conjunto; de um determinado tipo)?

@keean

a questão torna-se qual das categorizações de padrões de acesso a dados disponíveis de algoritmos de classificação é ideal para as estruturas de dados que você possui

Verdadeiro. O acesso por índice é uma _propriedade_ de uma fatia (ou array). Portanto, o primeiro requisito para um contêiner classificável (ou contêiner _tree_ -able qualquer que seja o algoritmo _tree_) é fornecer um utilitário _access & mutate (swap)_. O segundo requisito é que os itens sejam comparáveis. Essa é a parte confusa (para mim) sobre o que você chama de algoritmos: os requisitos devem ser atendidos em ambos os lados (no contêiner e no parâmetro de tipo). Esse é o ponto que não consigo imaginar uma implementação pragmática de genéricos em Go. Cada lado do problema pode ser descrito perfeitamente em termos de interfaces. Mas como combinar esses dois em uma notação eficaz?

Os algoritmos @dc0d requerem interfaces, as estruturas de dados as fornecem. Isso é suficiente para a generalidade total, desde que as interfaces sejam suficientemente poderosas. As interfaces são parametrizadas por tipos, mas você precisa de variáveis ​​de tipo.

Tomando o exemplo 'sort', 'Ord' é uma propriedade do tipo armazenado no container, não o próprio container. O padrão de acesso é uma propriedade do contêiner. Padrões de acesso simples são 'iteradores', mas esse nome vem de C++, Stepanov prefere 'coordenadas', pois pode ser aplicado a contêineres multidimensionais mais complexos.

Tentando definir sort, queremos algo assim:

bubble_sort : forall T U I => T U -> T U requires
   ForwardIterator<T>, Readable<T>, Writable<T>,
   Ord<U>,  ValueType(T) == U, Distance type(T) == I

Nota: não estou sugerindo esta notação, apenas tentando puxar algum outro trabalho relacionado, a cláusula require está na sintaxe preferida por Stepanov, o tipo de função é de Haskell, cujas classes de tipos provavelmente representam uma boa implementação desses conceitos.

@keean
Talvez eu esteja entendendo mal você, mas não acho que você possa simplesmente restringir algoritmos apenas a interfaces, pelo menos da maneira como as interfaces são definidas agora.
Considere sort.Slice, por exemplo, estamos interessados ​​em classificar fatias, e não vejo como alguém construiria uma interface que representasse todas as fatias.

@urandom você abstrai os algoritmos e não as coleções. Então você pergunta quais padrões de acesso a dados existem em algoritmos de "classificação" e então os classifica. Portanto, não importa se o contêiner é uma "fatia", não estamos tentando definir todas as operações que você deseja realizar em uma fatia, estamos tentando determinar os requisitos de um algoritmo e usá-lo para definir uma interface. Uma fatia não é especial, é apenas um tipo T sobre o qual podemos definir um conjunto de operações.

Portanto, as interfaces estão relacionadas a bibliotecas de algoritmos e você pode definir suas próprias interfaces para suas próprias estruturas de dados para poder usar esses algoritmos. As bibliotecas podem vir com interfaces pré-definidas para os tipos internos.

@keean
Achei que era isso que você queria dizer. Mas no contexto do Go, isso provavelmente significaria que precisaria haver uma revisão significativa do que as interfaces podem definir. Eu imagino que várias operações internas, como iterações ou operadores, precisariam ser expostas por meio de métodos para que coisas como sort.Slice ou math.Max fossem genéricas nas interfaces.

Então você teria que ter suporte para a seguinte interface (pseudo-código):

type [T] OrderedIterator interface {
   Len() int
   ValueAt(i int) *T
}

...
package sort

func [T] Slice(s [T]OrderedIterator, func(i, j int) bool) {
   ...
}

e todas as fatias teriam então esses métodos?

@urandom Um iterador não é uma abstração de uma coleção, mas uma abstração da referência/ponteiro em uma coleção. Por exemplo, o iterador de encaminhamento pode ter um único método 'sucessor' (às vezes 'próximo'). Ser capaz de acessar dados no local de um iterador não é uma propriedade do iterador (caso contrário, você acabará com sabores de leitura/gravação/mutáveis ​​de iterador). É melhor definir "referências" separadamente como interfaces legíveis, graváveis ​​e mutáveis:

type T ForwardIterator interface {
   type DistanceType D
   successor(x T) T
}

type T Readable interface {
   type ValueType U 
   source(x T) U
}

Nota: O tipo 'T' não é a fatia, mas o tipo do iterador na fatia. Isso poderia ser apenas um ponteiro simples, se adotarmos o estilo C++ de passar um iterador inicial e final para funções como sort.

Para um iterador de acesso aleatório, teríamos algo como:

type T RandomIterator interface {
   type DistanceType D
   setPosition(x DistanceType)
}

Portanto, um iterador/coordenada é uma abstração da referência a uma coleção, não a coleção em si. O nome 'coordenada' expressa isso muito bem, se você pensar no iterador como a coordenada e na coleção como o mapa.

Não estamos vendendo o Go short por não alavancar fechamentos de função e funções anônimas? Ter funções/métodos como um tipo de primeira classe em Go pode ajudar. Por exemplo, usando a sintaxe de albrow/fo , um tipo de bolha pode ter esta aparência:

type SortableContainer[C,T] struct {
    Less func(C,T,T) bool
    Swap func(C,int,int)
    Next func(C) (T,bool)
}

func (bs *SortableContainer[C,T]) BubbleSort(container C, e1,e2 T) {    
    swapCount := 1
    var item1, item2 T
    item1, ok1 = bs.Next()
    if !ok1 {return}
    item2, ok2 = bs.Next()
    if !ok2 {return}
    for swapCount > 0 {
        swapCount = 0
        for {
            if Less(item2, item1) { 
                bs.Swap(C,item2,item1)
                swapCount += 1
            }
        }
    }
}

Por favor, ignore quaisquer erros... completamente não testado!

@mandolyte Não tenho certeza se isso foi endereçado a mim? Eu realmente não vejo nenhuma diferença entre o que eu estava sugerindo e seu exemplo, exceto que você está usando interfaces multiparâmetros, e eu estava dando exemplos usando tipos abstratos/associados. Para ser claro, acho que você precisa de interfaces multiparâmetros e tipos abstratos/associados para total generalidade, nenhum dos quais é atualmente suportado pelo Go.

Sugiro que suas interfaces sejam menos gerais do que as que propus, porque vinculam a ordem de classificação, o padrão de acesso e a acessibilidade na mesma interface, o que obviamente resultará na proliferação de interfaces, por exemplo, duas ordens (menos , maior), três tipos de acesso (somente leitura, somente gravação, mutável) e cinco padrões de acesso (passagem única direta, passagem múltipla direta, bidirecional, indexado, aleatório) levariam a 36 interfaces em comparação com apenas 11 se as preocupações forem mantidas separadas.

Você pode definir as interfaces que proponho com interfaces multiparâmetros em vez de tipos abstratos como este:

type I ForwardIterator interface {
   successor(x I) I
}
type R V Readable interface {
   source(x R) V
}
type V Ord interface {
   less(x V, y V) : bool
}

Observe que o único que precisa de dois parâmetros de tipo é a interface Readable. No entanto, perdemos a capacidade de um objeto iterador 'conter' o tipo dos objetos iterados, o que é um grande problema, pois agora temos que mover o tipo 'valor' no sistema de tipos e temos que corrigi-lo . Isso leva a uma proliferação de parâmetros de tipo que não é bom e aumenta a possibilidade de erros de codificação. Também perdemos a capacidade de definir o 'DistanceType' no iterador, que é o menor tipo de número necessário para contar os elementos na coleção, o que é útil para mapear para int8, int16, int32 etc, para fornecer o tipo que você precisa contar elementos sem estouro.

Isso está intimamente ligado ao conceito de 'dependência funcional'. Se um tipo for funcionalmente dependente de outro tipo, ele deve ser um tipo abstrato/associado. Somente se os dois tipos forem independentes, eles devem ser parâmetros de tipo separados.

Alguns problemas:

  1. Não é possível usar a sintaxe f(x I) atual para interfaces multiparâmetros. Eu não gosto que essa sintaxe confunda interfaces (que são restrições de tipos) com tipos de qualquer maneira.
  2. Seria necessário uma maneira de declarar tipos parametrizados.
  3. Seria necessário haver uma maneira de declarar tipos associados para interfaces com um determinado conjunto de parâmetros de tipo.

@keean Não tenho certeza se entendi como ou por que a contagem de interfaces fica tão alta. Aqui está um exemplo completo de trabalho: https://play.folang.org/p/BZa6BdsfBgZ (baseado em fatia, não um contêiner geral, portanto, nenhum método Next() necessário).

Ele usa apenas uma estrutura de tipo, sem nenhuma interface. Eu tenho que fornecer todas as funções e encerramentos anônimos (provavelmente é aí que está a troca?). O exemplo usa o mesmo algoritmo de classificação de bolhas para classificar uma fatia de inteiros e uma fatia de pontos "(x,y)", em que a distância da origem é a base da função Less().

De qualquer forma, eu esperava mostrar como ter funções no sistema de tipos pode ajudar.

@mandolyte Acho que não entendi o que você estava propondo. Vejo que você está falando de "folang", que já possui alguns recursos de programação funcionais interessantes adicionados ao Go. O que você implementou é basicamente canalizar manualmente uma classe de tipos multiparâmetros. Você está passando o que é conhecido como dicionário de funções para a função de classificação. Isso está fazendo explicitamente o que uma interface faria implicitamente. Esses tipos de recursos provavelmente são necessários antes de interfaces multiparâmetros e tipos associados, mas você eventualmente terá problemas ao passar todos esses dicionários. Eu acho que as interfaces fornecem um código mais limpo e legível.

Classificar uma fatia é um problema resolvido. Aqui está o código para uma fatia quicksort.go implementada usando a linguagem go-li (golang melhorada) .

func main(){
    var data = []int{5,3,1,8,9}

    Sort(data, func(a *int, b *int) int {
        return *a - *b
    })

    fmt.Println(data)
}

Você pode experimentar isso no playground

Exemplo completo que você pode colar no playground , pois importar o pacote quicksort não funciona no playground.

@go-li Tenho certeza de que você pode classificar uma fatia, seria um pouco ruim se não pudesse. O ponto é que, genericamente, você gostaria de poder classificar qualquer contêiner linear com o mesmo código, de modo que você só precise escrever um algoritmo de classificação uma vez, não importa qual contêiner (estrutura de dados) você esteja classificando e não importa qual o conteúdo é.

Quando você pode fazer isso, a biblioteca padrão pode fornecer funções de classificação universais e ninguém precisa escrever uma novamente. Há dois benefícios para isso, menos erros, pois é mais difícil do que você pensa escrever um algoritmo de classificação correto, Stepanov usa o exemplo de que a maioria dos programadores não pode definir corretamente o par 'min' e 'max', então que esperança temos que ser correto para algoritmos mais complexos. O outro benefício é que quando há apenas uma definição de cada algoritmo de ordenação, quaisquer melhorias na clareza ou desempenho que possam ser feitas beneficiam todos os programas que o utilizam. As pessoas podem gastar seu tempo tentando melhorar o algoritmo comum em vez de ter que escrever seu próprio para cada tipo de dado diferente.

@keean
Outra questão relacionada à nossa discussão anterior. Não consigo descobrir como alguém seria capaz de definir uma função de mapeamento que altera itens de um iterável, retornando um novo tipo iterável concreto cujos itens podem ser de um tipo diferente do original.

E imagino que um usuário de tal função gostaria de um tipo concreto retornado, não outra interface.

@urandom Supondo que não queremos fazer isso 'no local', o que seria inseguro, o que você deseja é uma função de mapa que tenha um 'iterador de leitura' de um tipo e um 'iterador de gravação' de outro tipo, que pode ser definido algo como:

map<I, O, U>(first I, last I, out O, fn U) requires
   ForwardIterator<I>, Readable<I>,
   ForwardIterator<O>, Writable<O>,
   UnaryFunction<U>, Domain(U) == ValueType(I), Codomain(U) == ValueType(O)

Para maior clareza, "ValueType" é um tipo associado das interfaces "Readable" e "Writable", "Domain" e "Codomain" são tipos associados da interface "UnaryFunction". Obviamente, ajuda muito se o compilador puder derivar automaticamente as interfaces para tipos de dados como "UnaryFunction". Embora isso pareça reflexão, não é, e tudo acontece em tempo de compilação usando tipos estáticos.

@keean Como modelar essas restrições de leitura e gravação no contexto das interfaces atuais do Go?

Quero dizer, quando temos um tipo A e queremos converter para o tipo B , a assinatura dessa UnaryFunction seria func (input A) B (certo?), mas como isso pode ser modelado usando apenas interfaces e como esse map genérico (ou filter , reduce , etc.) seria modelado para manter o pipeline de tipos?

@geovanisouza92 Acho que "Type Families" funcionaria bem, pois podem ser implementados como um mecanismo ortogonal no sistema de tipos e, em seguida, integrados à sintaxe das interfaces, como é feito em Haskell.

Uma família de tipos é como uma função restrita em tipos (um mapeamento). Como as implementações de interface são selecionadas por tipo, podemos fornecer um mapeamento de tipo para cada implementação.

Então, se definirmos:

ValueType MyIntArrayIterator -> Int

As funções são um pouco mais complicadas, mas uma função tem um tipo, por exemplo:

fn(x : Int) Float

Nós escreveríamos este tipo:

Int -> Float

É importante perceber que -> é apenas um construtor de tipo infixo, como '[]' para um Array é um construtor de tipo, poderíamos facilmente escrever isso;

Fn Int Float
Or
Fn<Int, Float>

Dependendo de nossa preferência por sintaxe de tipo. Agora podemos ver claramente como podemos definir:

Domain  Fn<Int, Float> -> Int
Codomain Fn<Int, Float> -> Float

Agora, embora possamos fornecer todas essas definições manualmente, elas podem ser facilmente derivadas pelo compilador.

Dadas essas famílias de tipos, podemos ver que a definição de mapa que dei acima requer apenas os tipos IO e U para instanciar o genérico, pois todos os outros tipos são funcionalmente dependentes deles. Podemos ver que esses tipos são fornecidos diretamente pelos argumentos.

Obrigado, @keean.

Isso funcionaria bem para funções internas/predefinidas. Você está dizendo que o mesmo conceito seria aplicado para funções definidas pelo usuário ou libs do usuário?

Essas "Famílias de Tipos" serão transportadas em tempo de execução, no caso de algum contexto de erro?

Que tal interfaces vazias, interruptores de tipo e reflexão?


EDIT: Estou apenas curioso, não reclamando.

@giovanisouza92 bem, ninguém se comprometeu a ter genéricos, então espero ceticismo. Minha abordagem é que, se você for fazer genéricos, deve fazê-los corretamente.

No meu exemplo, 'map' é definido pelo usuário. Não há nada de especial nisso, e dentro da função você simplesmente usa os métodos das interfaces que você exigiu nesses tipos exatamente como você faz no Go agora. A única diferença é que podemos exigir um tipo para satisfazer várias interfaces, as interfaces podem ter vários parâmetros de tipo (embora o exemplo do mapa não use isso) e também existem tipos associados (e restrições em tipos como a igualdade de tipo '==' mas isso é como uma igualdade Prolog e unifica os tipos). É por isso que existe a sintaxe diferente para especificar as interfaces exigidas por uma função. Observe que há outra diferença importante:

f(x I, y I) requires ForwardIterator<I>

Vs

f(x ForwardIterator, y ForwardIterator)

Observe que há uma diferença no último 'x' e 'y' podem ser tipos diferentes que satisfaçam a interface ForwardIterator, enquanto na sintaxe anterior 'x' e 'y' devem ser do mesmo tipo (que satisfaça o iterador de encaminhamento). Isso é importante para que as funções não sejam restritas e permita que tipos concretos sejam propagados muito mais durante a compilação.

Acho que não muda nada em relação a trocas de tipo e reflexão, porque estamos apenas estendendo o conceito de interfaces. Como go tem informações de tipo de tempo de execução, você não entra no mesmo problema que Haskell e exige tipos existenciais.

Pensando em Go, polimorfismo de tempo de execução e famílias de tipos, provavelmente gostaríamos de restringir a própria família de tipos a uma interface para evitar ter que tratar todos os tipos associados como uma interface vazia em tempo de execução, o que seria lento.

Então, à luz desses pensamentos, eu modificaria minha proposta acima para que, ao declarar uma interface, você declarasse uma interface/tipo para cada tipo associado que todas as implementações dessa interface teriam que fornecer um tipo associado que satisfizesse essa interface. Dessa forma, podemos saber que é seguro chamar qualquer método dessa interface nos tipos associados em tempo de execução sem ter que mudar de tipo de uma interface vazia.

@keean
Para avançar no debate, deixe-me esclarecer o equívoco que sinto é semelhante à síndrome não inventada aqui.

O iterador bidirecional (na sintaxe T func (*T) *[2]*T ) tem o tipo func (*) *[2]* na sintaxe go-li. Em palavras, ele pega um ponteiro para algum tipo e retorna um ponteiro para dois ponteiros para o elemento seguinte e anterior do mesmo tipo. É o tipo fundamental concreto fundamental usado por uma lista duplamente vinculada .

Agora você pode escrever o que chama de map, o que chamo de função genérica foreach. Não se engane, isso funciona não apenas na lista vinculada, mas em qualquer coisa que exponha um iterador bidirecional!

func Foreach(link func(*) *[2]*, list **, direction byte, f func(*)) {

    if nil == *list {
        return
    }

    var end *
    end = *list

    var e *
    e = (*link(*list))[direction]
    f(end)

    for (e != end) && ((*link(e))[direction] != nil) {
        var newe = (*link(e))[direction]
        f(e)
        e = newe
    }
    return
}

O Foreach pode ser usado de duas maneiras, você o usa com um lambda em uma iteração do tipo loop for sobre elementos de lista ou coleção.

const forward = 1
const backwards = 0
Foreach(iterator, collection, forward, func(element *element_type){
    // do something with every element
})

Ou você pode usá-lo para mapear funcionalmente uma função para cada elemento da coleção.

Foreach(iterator, collection, backwards, function_to_be_mapped_on_elements)

É claro que o iterador bidirecional também pode ser modelado usando interfaces em go 1.
interface Iterator { Iter() [2]Iterator } Você precisa modelá-lo usando interfaces para envolver ("caixa") o tipo subjacente. O usuário do iterador, em seguida, digita afirma o tipo conhecido assim que localiza e deseja visitar um elemento de coleção específico. Isso é potencialmente inseguro em tempo de compilação.

O que você está descrevendo a seguir são as diferenças entre a abordagem herdada e a abordagem baseada em genéricos.

func modern(x func  (*) *[2]*, y func  (*) *[2]*){}

essa abordagem de tipo de tempo de compilação verifica se as duas coleções têm o mesmo tipo subjacente, em outras palavras, se os iteradores realmente retornam os mesmos tipos concretos

func modern_T_syntax<T>(x func  (*T) *[2]*T, y func  (*T) *[2]*T){}

O mesmo que acima, mas usando o familiar T significa sintaxe de espaço reservado para o tipo

func legacy(x Iterator, y Iterator){}

Neste caso, o usuário pode passar, por exemplo, lista encadeada inteira como x e lista encadeada flutuante como y. Isso pode levar a possíveis erros de tempo de execução, pânico ou outras decoerências internas, mas tudo depende do que o legado faria com os dois iteradores.

Agora o equívoco. Você afirma que fazer iteradores e fazer classificações genéricas para classificar esses iteradores seria o caminho a seguir. Isso seria uma coisa realmente ruim de se fazer, eis por que

O iterador e a lista vinculada são dois lados da mesma moeda. Prova: qualquer coleção que exponha o iterador simplesmente se anuncia como uma lista vinculada. Digamos que você precise classificar isso. Fazer o que?

Obviamente, você exclui a lista vinculada de sua base de código e a substitui por uma árvore binária. Ou se você quer ser chique use uma árvore de busca balanceada como avl, red-black, como proposto não sei quantos anos atrás por Ian et all. Ainda isso não foi feito genericamente em Golang. Agora esse seria o caminho a seguir.

Outra solução é rapidamente em loop de tempo O(N) sobre o iterador, coletar os ponteiros para elementos em uma fatia de ponteiros genéricos, denotados []*T e classificar esses ponteiros genéricos usando a classificação de fatias ruim

Por favor, dê uma chance às ideias de outras pessoas

@go-li Se quisermos evitar a síndrome do não-inventado-aqui, devemos procurar uma definição de Alex Stepanov, pois ele praticamente inventou a programação genérica. Aqui está como eu o definiria, tirado da página 111 de "Elementos de programação" de Stepanov:

Bidirectional iterator<T> =
    ForwardIterator<T>
/\ predecessor : T -> T
/\ predecessor takes constant time
/\ (forall i in T) successor(i) is defined =>
        predecessor(successor(i)) is defined and equals i
/\ (forall i in T) predecessor(i) is defined =>
        successor(predecessor(i)) is defined and equals i

Isso depende da definição de ForwardIterator:

ForwardIterator<T> =
    Iterator<T>
/\ regular_unary_function(successor)

Então, essencialmente, temos uma interface que declara uma função successor e uma função predecessor , juntamente com alguns axiomas que eles devem cumprir para serem válidos.

Com relação legacy não é que o legado vá dar errado, obviamente não dá errado em Go atualmente, mas o compilador está perdendo oportunidades de otimização, e o sistema de tipos está perdendo a oportunidade de propagar tipos concretos ainda mais. Também está limitando os programadores a especificar sua intenção com precisão. Um exemplo seria uma função de identidade, que pretendo retornar exatamente o tipo que é passado:

id(x T) T

Talvez também valha a pena mencionar a diferença entre um tipo paramétrico e um tipo quantificado universalmente. Um tipo paramétrico seria id<T>(x T) T enquanto o quantificado universalmente é id(x T) T (normalmente omitimos o quantificador universal mais externo neste caso forall T ). Com tipos paramétricos, o sistema de tipos deve ter um tipo para T fornecido no site de chamada para id , com quantificação universal que não é necessária desde que T seja unificado com um tipo concreto antes da compilação terminar. Outra maneira de entender isso é que a função paramétrica não é um tipo, mas um modelo para um tipo, e só é um tipo válido depois que T for substituído por um tipo concreto. Com a função quantificada universalmente id na verdade tem um tipo forall T . T -> T que pode ser passado pelo compilador assim como Int .

@go-li

Obviamente, você exclui a lista vinculada de sua base de código e a substitui por uma árvore binária. Ou se você quer ser chique use uma árvore de busca balanceada como avl, red-black, como proposto não sei quantos anos atrás por Ian et all. Ainda isso não foi feito genericamente em Golang. Agora esse seria o caminho a seguir.

Ter estruturas de dados ordenadas não significa que você nunca precise classificar dados.

Se quisermos evitar a síndrome do não-inventado-aqui, devemos procurar uma definição de Alex Stepanov, já que ele praticamente inventou a programação genérica.

Eu contestaria qualquer afirmação de que a programação genérica foi inventada por C++. Leia o Liskov et al. Artigo CACM de 1977 se você quiser ver um modelo inicial de programação genérica que realmente funciona (tipo seguro, modular, sem excesso de código): https://dl.acm.org/citation.cfm?id=359789 (consulte a Seção 4 )

Acho que devemos parar com essa discussão e esperar que a equipe golang (russ) venha com algumas postagens no blog e depois implemente uma solução 👍 (veja vgo) Eles vão fazer isso 🎉

https://peter.bourgon.org/blog/2018/07/27/a-response-about-dep-and-vgo.html

Espero que esta história sirva como um aviso para os outros: se você estiver interessado em fazer contribuições substanciais para o projeto Go, nenhuma due diligence independente pode compensar um design que não se origina da equipe principal.

Este tópico mostra como a equipe principal não está interessada em participar ativamente na busca de uma solução com a comunidade.

Mas no final, se eles puderem fazer uma solução sozinhos novamente, tudo bem para mim, apenas faça 👍

@andrewcmyers Bem, talvez "inventado" tenha sido um pouco exagerado, provavelmente é mais como David Musser em 1971, que mais tarde trabalhou com Stepanov em algumas bibliotecas genéricas para Ada.

Elements of Programming não é um livro sobre C++, os exemplos podem estar em C++, mas isso é uma coisa bem diferente. Eu acho que este livro é leitura essencial para quem quer implementar genéricos em qualquer idioma. Antes de dispensar Stepanov, você deve realmente ler o livro para ver do que se trata.

Esse problema já está sobrecarregando os limites da escalabilidade do GitHub. Por favor, mantenha a discussão aqui focada em questões concretas para as propostas do Go.

Seria lamentável para Go seguir a rota C++.

@andrewcmyers Sim, concordo plenamente, não use C++ para sugestões de sintaxe ou como referência para fazer as coisas corretamente. Em vez disso, dê uma olhada em D para se inspirar .

@nomad-software

Eu gosto muito de D, mas o go precisa dos poderosos recursos de metaprogramação de tempo de compilação que D oferece?

Eu não gosto da sintaxe do modelo, também em C++, decorrente da idade da pedra.

Mas e o ParametricType normalpadrão encontrado em Java ou C #, se necessário, também pode sobrecarregar isso com ParametricType

E mais, eu não gosto da sintaxe de chamada de template em D com seu símbolo bang, o símbolo bang é bastante usado hoje em dia para denotar acesso mutável ou imutável para os parâmetros de uma função.

@nomad-software Eu não estava sugerindo que a sintaxe C++ ou o mecanismo de modelo é o caminho certo para fazer genéricos. Mais do que "conceitos" como definidos por Stepanov tratam os tipos como uma álgebra, que é a maneira correta de fazer genéricos. Veja as classes de tipo Haskell para ver como isso pode ficar. As classes do tipo Haskell são muito próximas aos modelos e conceitos de C++ semanticamente, se você entender o que está acontecendo.

Portanto, +1 por não seguir a sintaxe de c++ e +1 por não implementar um sistema de modelo inseguro de tipo :-)

@keean A razão para a sintaxe D é evitar completamente <,> e aderir à gramática livre de contexto. Isso faz parte do meu ponto de usar D como inspiração. <,> é uma escolha muito ruim para a sintaxe de parâmetros genéricos.

@nomad-software Como apontei acima (em um comentário agora oculto), você precisa especificar os parâmetros de tipo para tipos paramétricos, mas não para tipos quantificados universalmente (daí a diferença entre Rust e Haskell, a maneira como os tipos são tratados é realmente diferente no sistema de tipos). Também conceitos C++ == classes de tipo Haskell == interfaces Go, pelo menos em um nível conceitual.

A sintaxe D é realmente preferível:

auto add(T)(T lhs, T rhs) {
    return lhs + rhs;

Por que isso é melhor que o estilo C++/Java/Rust:

T add<T>(T lhs, T rhs) {
    return lhs + rhs;
}

Ou estilo Scala:

T add[T](T lhs, T rhs) {
    return lhs + rhs;
}

Eu fiz algumas reflexões sobre a sintaxe para parâmetros de tipo. Eu nunca fui fã de "colchetes angulares" em C++ e Java porque eles tornam a análise bastante complicada e, portanto, impedem o desenvolvimento de ferramentas. Os colchetes são, na verdade, uma escolha clássica (de CLU, System F e outras linguagens antigas com polimorfismo paramétrico).

No entanto, a sintaxe do Go é bastante delicada, talvez porque já seja tão concisa. Sintaxes possíveis baseadas em colchetes ou parênteses criam ambiguidades gramaticais ainda piores do que aquelas introduzidas por colchetes angulares. Então, apesar das minhas predisposições, os colchetes parecem ser a melhor escolha para o Go. (Claro, também existem colchetes angulares reais que não criariam nenhuma ambiguidade — ⟨⟩ — mas exigiriam o uso de caracteres Unicode).

Claro, a sintaxe precisa usada para parâmetros de tipo é menos importante do que acertar a semântica . Nesse ponto, a linguagem C++ é um modelo ruim. O trabalho do meu grupo de pesquisa sobre genéricos em Gênero (PLDI 2015) e Família (OOPSLA 2017) oferece outra abordagem que estende classes de tipos e as unifica com interfaces.

@andrewcmyers Eu acho que ambos os artigos são interessantes, mas eu diria que não é uma boa direção para Go, já que Genus é orientado a objetos e Go não é, e Familia unifica subtipagem e polimorfismo paramétrico, e Go não tem nenhum. Acho que Go deveria simplesmente adotar polimorfismo paramétrico ou quantificação universal, não precisa de subtipagem, e na minha opinião é uma linguagem melhor por não tê-la.

Acho que Go deveria estar procurando por genéricos que não exijam orientação a objetos e não exijam subtipagem. Go já tem interfaces, que eu acho que são um bom mecanismo para genéricos. Se você puder ver que interfaces Go == conceitos de c++ == classes de tipo Haskell, parece-me que a maneira de adicionar genéricos mantendo o sabor de 'Go' seria estender interfaces para receber vários parâmetros de tipo (eu como tipos associados em interfaces também, mas isso pode ser uma extensão separada, ajuda a aceitar vários parâmetros de tipo). Essa seria a mudança de chave, mas para habilitar isso, seria necessário haver uma sintaxe 'alternativa' para interfaces em assinaturas de função, para que você possa obter os vários parâmetros de tipo para as interfaces, que é onde entra toda a sintaxe de colchetes angulares .

As interfaces Go não são classes de tipo — são apenas tipos — mas unificar interfaces com classes de tipo é o que o Familia mostra uma maneira de fazer. Os mecanismos de Genus e Familia não estão vinculados ao fato de as linguagens serem totalmente orientadas a objetos. As interfaces Go já o tornam "orientado a objetos" da maneira que importa, então acho que as ideias podem ser adaptadas de forma ligeiramente simplificada.

@andrewcmyers

As interfaces Go não são classes de tipos — são apenas tipos

Eles não se comportam como tipos para mim, pois permitem polimorfismo. O objeto em uma matriz polimórfica como Addable[] ainda tem seu tipo real (visível por reflexão de tempo de execução), então eles se comportam exatamente como classes de tipo de parâmetro único. O fato de serem colocados no lugar de um tipo em assinaturas de tipo é simplesmente uma notação abreviada omitindo a variável de tipo. Não confunda a notação com a semântica.

f(x : Addable) == f<T>(x : T) requires Addable<T>

Essa identidade é, obviamente, válida apenas para interfaces de parâmetro único.

A única diferença significativa entre interfaces e classes de tipo de parâmetro único é que as interfaces são definidas localmente, mas isso é útil porque evita o problema de coerência global que Haskell tem com suas classes de tipo. Eu acho que este é um ponto interessante no espaço de design. As interfaces multiparâmetros dariam a você todo o poder das classes de tipos multiparâmetros com o benefício de serem locais. Não há necessidade de adicionar herança ou subtipagem à linguagem Go (que são os dois principais recursos que definem OO, eu acho).

NA MINHA HUMILDE OPINIÃO:

Ainda ter um tipo padrão seria preferível a uma DSL dedicada a expressar restrições de tipo. Como ter uma função f(s T fmt.Stringer) que é uma função genérica que aceita qualquer tipo que também seja/satisfaça a interface fmt.Stringer .

Desta forma é possível ter uma função genérica como:

func add(a, b T int) T int {
    return a + b
}

Agora a função add() funciona com qualquer tipo T que como int s suporta o operador + .

@ dc0d Concordo que parece atraente olhando para a sintaxe atual do Go. No entanto, não é 'completo', pois não pode representar todas as restrições necessárias para genéricos, e ainda haverá um esforço para estender isso ainda mais. Isso resultará em uma proliferação de diferentes sintaxes que eu vejo como em conflito com o objetivo da simplicidade. Minha opinião é que a simplicidade não é simples, tem que ser o mais simples, mas ainda oferecer o poder expressivo necessário. Atualmente, vejo que a principal limitação do Go no poder expressivo genérico é a falta de interfaces multiparâmetros. Por exemplo, uma interface de coleção pode ser definida como:

type T U Collection interface {
   member(c T, v U) Bool
   insert(c T, v U) T
}

Então isso faz sentido certo? Gostaríamos de escrever interfaces sobre coisas como coleções. Então a questão é como você usa essa interface em uma função. Minha sugestão seria algo como:

func[T, U] f(c T, e U) (Bool, T) requires Collection[T, U] {
   a := member(c, e)
   d := insert(c, e)
   return a, d
}

A sintaxe é apenas uma sugestão, no entanto, eu realmente não me importo com a sintaxe, desde que você possa expressar esses conceitos na linguagem.

@keean Não seria preciso se eu dissesse que não me importo com a sintaxe. Mas o ponto era a ênfase em ter um tipo padrão para cada parâmetro genérico. Nesse sentido, o exemplo fornecido para interface se tornará:

type Collection interface (T interface{}, U interface{}) {
   member(c T, v U) Bool
   insert(c T, v U) T
}

Agora a parte (T interface{}, U interface{}) ajuda na definição de restrições. Por exemplo, se os membros devem satisfazer fmt.Stringer , a definição seria:

type Collection interface (T fmt.Stringer, U fmt.Stringer) {
   member(c T, v U) Bool
   insert(c T, v U) T
}

@dc0d Isso seria novamente restritivo no sentido de que você deseja restringir por mais de um parâmetro de tipo, considere:

type OrderedCollection[T, U] interface
   requires Collection[T, U], Ord[U] {...}

Acho que vejo de onde você está vindo com o posicionamento do parâmetro, você poderia ter:

type OrderedCollection interface(T, U)
   requires Collection(T, U), Ord(U) {...}

Como eu disse, não sou muito preocupado com a sintaxe, pois posso me acostumar com a maioria das sintaxes. Pelo exposto, presumo que você prefira os parênteses '()' para interfaces multiparâmetros.

@keean Vamos considerar a interface heap.Interface . A definição atual na biblioteca padrão é:

type Interface interface {
    sort.Interface
    Push(x interface{}) // add x as element Len()
    Pop() interface{}   // remove and return element Len() - 1.
}

Agora vamos reescrevê-lo como uma interface genérica, empregando o tipo padrão:

type Interface interface (T interface{}) {
    sort.Interface
    Push(x T) // add x as element Len()
    Pop() T   // remove and return element Len() - 1.
}

Isso não quebra nenhuma série de código Go 1.x por aí. Uma implementação seria minha proposta para Type Alias ​​Rebinding. Mas tenho certeza de que pode haver implementações melhores.

Ter tipos padrão nos permite escrever código genérico que pode ser usado com código de estilo Go 1.x. E a biblioteca padrão pode se tornar genérica, sem quebrar nada. Isso é grande vitória IMO.

@dc0d então você está sugerindo uma melhoria incremental? O que você está sugerindo parece bom para mim como uma melhoria incremental, mas ainda tem um poder expressivo genérico limitado. Como você implementaria as interfaces "Collection" e "OrderedCollection"?

Considere que várias extensões parciais de linguagem podem levar a um produto final mais complexo (com várias sintaxes alternativas) do que implementar a solução completa da maneira mais simples possível.

@keean Eu não entendo a parte requires Collection[T, U], Ord[U] . Como eles estão restringindo os parâmetros de tipo T e U ?

@dc0d Eles funcionam da mesma maneira que em uma função, mas se aplicam a tudo. Portanto, para qualquer par de tipos TU que seja uma OrderedCollection, exigimos que TU também seja uma instância de Collection e que U seja Ord. Portanto, em qualquer lugar que usamos OrderedCollection, podemos usar métodos de Collection e Ord conforme apropriado.

Se formos minimalistas, isso não é obrigatório, pois podemos incluir as interfaces extras nos tipos de função onde precisamos, por exemplo:

type OrderedCollection interface(T, U)
{
   first(c T) U
}

func[T] first(c T[]) T requires Collection(T[], T), Ord T
{...}

func[T] f(c T[]) requires OrderedCollection(T[], T), Collection(T[], T), Ord(T)
{...}

Mas isso pode ser mais legível:

type OrderedCollection interface(T, U) 
   requires Collection(T, U), Ord(U)
{
   first(c T) U
}

func[T] first(c T[]) T
{...}

func[T] f(c T[]) requires OrderedCollection(T[], T)
{...}

@keean (IMO) Enquanto houver um valor padrão obrigatório para os parâmetros de tipo, me sinto feliz. Dessa forma, é possível manter a compatibilidade retroativa com a série de códigos Go 1.x. Esse é o ponto principal que tentei fazer.

@keean

As interfaces Go não são classes de tipos — são apenas tipos

Eles não se comportam como tipos para mim, pois permitem polimorfismo.

Sim, eles permitem polimorfismo de subtipo . Go tem subtipagem por meio de tipos de interface. Ele não possui hierarquias de subtipo explicitamente declaradas, mas isso é amplamente ortogonal. O que faz com que Go não seja totalmente orientado a objetos é a falta de herança.

Alternativamente, você pode ver as interfaces como aplicações existencialmente quantificadas de classes de tipo. Acredito que seja isso que você tem em mente. Foi o que fizemos em Genus e Familia.

@andrewcmyers

Sim, eles permitem polimorfismo de subtipo.

Vá até onde eu sei que é invariável, não há covariância ou contravariância, isso fala fortemente que isso não é subtipagem. Sistemas de tipo polimórfico são invariáveis, então para mim parece que Go está mais próximo desse modelo, e tratar interfaces como classes de tipo de parâmetro único parece mais de acordo com a simplicidade de Go. A falta de covariância e contravariância é um grande benefício para os genéricos, basta olhar para a confusão que essas coisas criam em linguagens como C#:

https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance

Acho que Go deve evitar totalmente esse tipo de complexidade. Para mim, isso significa que não queremos genéricos e subtipagem no mesmo sistema de tipos.

Alternativamente, você pode ver as interfaces como aplicações existencialmente quantificadas de classes de tipo. Acredito que seja isso que você tem em mente. Foi o que fizemos em Genus e Familia.

Como Go possui informações de tipo em tempo de execução, não há necessidade de quantificação existencial. Em Haskell, os tipos são unboxed (como os tipos 'C' nativos) e isso significa que uma vez que colocamos algo em uma coleção existencial, não podemos (facilmente) recuperar o tipo do conteúdo, tudo o que podemos fazer é usar as interfaces fornecidas (classes de tipo ). Isso é implementado armazenando um ponteiro para as interfaces junto com os dados brutos. Em Go, o tipo de dados é armazenado, os dados são 'Boxed' (como em C# boxed e unboxed data). Como tal Go não se limita apenas às interfaces armazenadas com os dados, pois é possível (pelo uso de um type-case) recuperar o tipo dos dados na coleção, o que só é possível em Haskell implementando um 'Reflection' typeclass (embora seja difícil obter os dados, é possível serializar o tipo e os dados, para dizer strings, e depois desserializar fora da caixa existencial). Portanto, a conclusão que tenho é que as interfaces Go se comportam exatamente como as classes de tipo, se Haskell fornecesse a classe de tipo 'Reflection' como incorporada. Como tal, não há caixa existencial, e ainda podemos digitar o conteúdo das coleções, mas as interfaces se comportam exatamente como as classes de tipos. A diferença entre Haskell e Go está na semântica de dados encaixotados versus não encaixotados, e as interfaces são classes de tipo de parâmetro único. Com efeito, quando 'Go' trata uma interface como um tipo, o que está realmente fazendo é:

Addable[] == exists T . T[] requires Addable[T], Reflection[T]

Provavelmente vale a pena notar que esta é a mesma maneira que "Trait Objects" funcionam em Rust.

Go pode evitar totalmente existenciais (sendo visível para o programador), covariância e contravariância, o que é uma coisa boa, e isso tornará os genéricos muito mais simples e poderosos na minha opinião.

Vá até onde eu sei que é invariável, não há covariância ou contravariância, isso fala fortemente que isso não é subtipagem.

Sistemas de tipo polimórfico são invariáveis, então para mim parece mais próximo a este modelo, e tratar interfaces como classes de tipo de parâmetro único parece mais de acordo com a simplicidade do Go.

Posso sugerir que ambos estão corretos? Em que as interfaces são equivalentes às classes de tipo, mas as classes de tipo são uma forma de subtipagem. As definições de subtipagem que encontrei até agora são bastante vagas e imprecisas e se resumem a "A é um subtipo de B, se um pode ser substituído pelo outro". Que, IMO, pode ser facilmente argumentado para ser satisfeito por classes de tipo .

Observe que o argumento de variação em si não está realmente funcionando IMO. A variação é uma propriedade de construtores de tipos, não uma linguagem. E é bastante normal que nem todos os construtores de tipo em uma linguagem sejam variantes (por exemplo, muitas linguagens com subtipagem têm matrizes mutáveis, que precisam ser invariantes para serem seguras para o tipo). Portanto, não vejo por que você não poderia ter subtipagem sem construtores de tipo variante.

Além disso, acredito que essa discussão é um pouco ampla demais para um problema no repositório Go. Não se trata de discutir os meandros das teorias de tipos, mas sobre se e como adicionar genéricos ao Go.

@Merovius Variance é uma propriedade associada à subtipagem. Em idiomas sem subtipagem, não há variação. Para que haja variação em primeiro lugar, você precisa ter subtipagem, que introduz o problema de covariância/contravariância aos construtores de tipo. Você está certo, porém, que em uma linguagem com subtipagem, é possível ter todos os construtores de tipo invariáveis.

Classes de tipo definitivamente não são subtipagem, porque uma classe de tipo não é um tipo. No entanto, podemos ver 'tipos de interface' em Go como o que Rust chama de 'objeto de traço' efetivamente um tipo derivado da classe de tipos.

A semântica de Go parece se encaixar em qualquer modelo no momento, porque não tem variação e tem 'objetos de traço' implícitos. Então, talvez Go esteja em um ponto de inflexão, genéricos e o sistema de tipos possam ser desenvolvidos ao longo das linhas de subtipagem, introduzindo variação e terminando com algo como genéricos em C#. Alternativamente, o Go poderia introduzir interfaces multiparâmetros, permitindo interfaces para coleções, e isso quebraria o vínculo imediato entre interfaces e 'tipos de interface'. Por exemplo, se você tiver:

type (T, U) Collection interface {
    member : (c T, e U) Bool
    insert: (c T, e U) T
}

member(c int32[], e int32) Bool {...}
insert(c int32[], e int32) int32[] {...}

member(c float32[], e float32) Bool {...}
insert(c float32[], e float32) float32[] {...}

Não há mais uma relação de subtipo óbvia entre os tipos T, U e a interface Collection. Portanto, você só pode visualizar a relação entre o tipo de instância e os tipos de interface como subtipagem para o caso especial de interfaces de parâmetro único, e não podemos expressar abstrações de coisas como coleções com interfaces de parâmetro único.

Eu acho que para genéricos você claramente precisa ser capaz de modelar coisas como coleções, então interfaces multiparâmetros são obrigatórias para mim. No entanto, acho que a interação entre covariância e contravariância em genéricos cria um sistema de tipos excessivamente complexo, portanto, gostaria de evitar a subtipagem.

@keean Como as interfaces podem ser usadas como tipos e as classes de tipos não são tipos, a explicação mais natural da semântica Go é que as interfaces não são classes de tipos. Eu entendo que você está argumentando para generalizar interfaces como classes de tipo; Acho que é uma direção razoável para seguir a linguagem e, de fato, já exploramos essa abordagem extensivamente em nosso trabalho publicado.

Para saber se Go tem subtipagem, considere o seguinte código:

package main

type Cloneable interface {
    Clone() Cloneable
}

type CloneableZ interface {
    Clone() Cloneable
    zero() int
}

type S struct {}

func (t S) Clone() Cloneable {
    c := t
    return c
}

func (t S) zero() int {
    return 0
}

var x CloneableZ = S{}
var y Cloneable = x

func main() {
    print("ok\n")
}

A atribuição de x a y demonstra que o tipo de y pode ser usado onde o tipo de x é esperado. Esta é uma relação de subtipagem, a saber: CloneableZ <: Cloneable , e também S <: CloneableZ . Mesmo se você explicasse as interfaces em termos de classes de tipo, ainda haveria um relacionamento de subtipagem em jogo aqui, algo como S <: ∃T.CloneableZ[T] <: ∃T.Cloneable[T] .

Observe que seria perfeitamente seguro para Go permitir que a função Clone retornasse um S , mas Go impõe regras desnecessariamente restritivas para conformidade com interfaces: na verdade, as mesmas regras que Java originalmente imposta. A subtipagem não requer construtores de tipo não invariáveis, como observou o @Merovius .

@andrewcmyers O que acontece com interfaces multiparâmetros, como aquelas necessárias para abstrair coleções?

Além disso, a atribuição de x para y pode ser vista como uma demonstração de herança de interface sem subtipagem. Em Haskell (que claramente não tem subtipagem) você escreveria:

class Cloneable t => CloneableZ t where...

Onde temos x é um tipo que implementa CloneableZ que por definição também implementa Cloneable , então obviamente pode ser atribuído a y .

Para tentar resumir, você pode visualizar uma interface como um tipo e Go para ter subtipagem limitada sem construtores de tipo covariante ou contravariante, ou você pode vê-la como um "objeto de traço", ou talvez em Go nós o chamaríamos de " objeto de interface", que é efetivamente um contêiner polimórfico restrito por uma interface "typeclass". No modelo typeclass não há subtipagem e, portanto, não há razão para pensar em covariância e contravariância.

Se ficarmos com o modelo de subtipagem, não podemos ter tipos de coleção, é por isso que C++ teve que introduzir templates, porque a subtipagem orientada a objetos não é suficiente para definir genericamente conceitos como contêineres. Acabamos com dois mecanismos para abstração, objetos e subtipagem, e templates/traits e genéricos, e as interações entre os dois ficam complexas, veja C++, C# e Scala para exemplos. Haverá chamadas contínuas para introduzir construtores covariantes e contravariantes para aumentar o poder dos genéricos, de acordo com essas outras linguagens.

Se queremos coleções genéricas sem introduzir um sistema genérico separado, devemos pensar em interfaces como classes de tipos. Interfaces multiparâmetros significariam não pensar mais em subtipagem e, em vez disso, pensar em herança de interface. Se queremos melhorar os genéricos em Go e permitir abstrações de coisas como coleções, e não queremos a complexidade dos sistemas de tipos de linguagens como C++, C#, Scala etc, então interfaces multiparâmetros e herança de interface são o caminho. ir.

@keean

O que acontece com interfaces multiparâmetros, como aquelas necessárias para abstrair coleções?

Por favor, veja nossos artigos sobre Gênero e Família, que suportam restrições de tipo multiparâmetro. O Familia unifica essas restrições com interfaces e permite que interfaces restrinjam vários tipos.

Se ficarmos com o modelo de subtipagem, não podemos ter tipos de coleção

Não tenho certeza do que você quer dizer com "o modelo de subtipagem", mas está bem claro que Java e C# têm tipos de coleção, então essa afirmação não faz muito sentido para mim.

Onde temos x é um tipo que implementa CloneableZ que por definição também implementa Cloneable, então obviamente pode ser atribuído a y.

Não, no meu exemplo, x é uma variável e y é outra variável. Se eu sei que y é algum tipo CloneableZ e x é algum tipo Cloneable , isso não significa que eu possa atribuir de y a x. É isso que meu exemplo está fazendo.

Para esclarecer que a subtipagem é necessária para modelar Go, abaixo está uma versão aprimorada do exemplo cujo equivalente moral não verifica o tipo em Haskell. O exemplo mostra que a subtipagem permite a criação de coleções heterogêneas nas quais diferentes elementos possuem diferentes implementações. Além disso, o conjunto de implementações possíveis é aberto.

type Cloneable interface {
    Clone() Cloneable
}

type CloneableZ interface {
    Clone() Cloneable
    zero() int
}

type S struct {}

func (t S) Clone() Cloneable {
    c := t
    return c
}

type T struct { x int }

func (t T) Clone() Cloneable {
    c := t
    return c
}

func (t S) zero() int {
    return 0
}

var x CloneableZ = S{}
var y Cloneable = T{}
var a [2]Cloneable = [2]Cloneable{x, y}

@andrewcmyers

Não tenho certeza do que você quer dizer com "o modelo de subtipagem", mas está bem claro que Java e C# têm tipos de coleção, então essa afirmação não faz muito sentido para mim.

Veja por que o C++ desenvolveu templates, o modelo de subtipagem OO não era capaz de expressar os conceitos genéricos necessários para generalizar coisas como coleções. C# e Java também tiveram que introduzir um sistema genérico completo separado de objetos, subtipagem e herança, e então tiveram que limpar a bagunça das interações complexas dos dois sistemas com coisas como construtores de tipo covariante e contravariante. Com o benefício da retrospectiva, podemos evitar a subtipagem OO e, em vez disso, observar o que acontece se adicionarmos interfaces (classes de tipo) a uma linguagem simplesmente tipada. Isso é o que Rust fez, então vale a pena dar uma olhada, mas é claro que é complicado por toda a vida. Go tem GC então não teria essa complexidade. Minha sugestão é que Go possa ser estendido para permitir interfaces multiparâmetros e evitar essa complexidade.

Em relação à sua alegação de que você não pode fazer este exemplo em Haskell, aqui está o código:

{-# LANGUAGE ExistentialQuantification #-}

class ICloneable t where
    clone :: t -> t

class ICloneable t => ICloneableZ t where
    zero :: t

data S = S deriving Show

instance ICloneable S where
    clone x = x

data T = T Int deriving Show

instance ICloneable T where
    clone x = x

instance ICloneableZ T where
    zero = T 0

data Cloneable = forall a . (ICloneable a, Show a) => ToCloneable a

instance Show Cloneable where
    show (ToCloneable x) = show x

main = do
    x <- return S
    y <- return (T 27)
    a <- return [ToCloneable x, ToCloneable y]
    putStrLn (show a)

Algumas diferenças interessantes, Go deriva automaticamente este tipo data Cloneable = forall a . (ICloneable a, Show a) => ToCloneable a pois é assim que você transforma uma interface (que não tem armazenamento) em um tipo (que tem armazenamento), Rust também deriva esses tipos e os chama de "objetos de traço" . Em outras linguagens como Java, C# e Scala, descobrimos que você não pode instanciar interfaces, o que na verdade é "correto", interfaces não são tipos, elas não têm armazenamento, Go está derivando o tipo de um contêiner existencial automaticamente para você, para que você possa tratar a interface como um tipo, e Go esconde isso de você dando ao contêiner existencial o mesmo nome da interface da qual é derivado. A outra coisa a notar é que este [2]Cloneable{x, y} coage todos os membros a Cloneable , enquanto Haskell não tem tais coerções implícitas, e temos que coagir explicitamente os membros com ToCloneable .

Também me foi dito que não devemos considerar os subtipos S e T de Cloneable porque S e T não são estruturalmente compatível. Podemos literalmente declarar qualquer tipo uma instância de Cloneable (apenas declarando a definição relevante da função clone em Go) e esses tipos não precisam ter nenhuma relação entre si.

A maioria das propostas para Genéricos parecem incluir tokens adicionais que acho que prejudicam a legibilidade e a sensação simples de Go. Eu gostaria de propor uma sintaxe diferente que eu acho que poderia funcionar bem com a gramática existente do Go (até acontece de destacar a sintaxe muito bem no Github Markdown).

Os principais pontos da proposta:

  • A gramática de Go parece sempre ter uma maneira fácil de determinar quando uma declaração de tipo terminou porque há algum token ou palavra-chave específica que estamos procurando. Se isso for verdade em todos os casos, argumentos de tipo podem simplesmente ser adicionados seguindo os próprios nomes de tipo.
  • Como a maioria das propostas, o mesmo identificador significa o mesmo tipo em qualquer declaração de função. Esses identificadores nunca escapam da declaração.
  • Na maioria das propostas, você precisa declarar argumentos de tipo genérico, mas nesta proposta está implícito. Algumas pessoas alegam que isso prejudica a legibilidade ou a clareza (a implícita é ruim) ou restringe a capacidade de nomear um tipo, as refutações seguem:

    • Quando se trata de prejudicar a legibilidade, acho que você pode argumentar de qualquer maneira, o extraou [T] prejudica a legibilidade ao fazer muito ruído sintático.

    • A implícita, quando usada corretamente, pode ajudar uma linguagem a ser menos detalhada. Eliminamos declarações de tipo com := o tempo todo porque a informação oculta por isso simplesmente não é importante o suficiente para ser explicada a cada vez.

    • Nomear um tipo concreto (não genérico) a ou t provavelmente é uma prática ruim, portanto, esta proposta pressupõe que é seguro reservar esses identificadores para atuar como argumentos de tipo genérico. Embora isso exigiria uma migração de correção, talvez?

package main

import "fmt"

type LinkedList a struct {
  Head *Node a
  Tail *Node a
}

type Node a {
  Next *Node a
  Prev *Node a

  Value a
}

func main() {
  // Not sure about how recursive we could get with the inference
  ll := LinkedList string {
    // The string bit could be inferred
    Head: Node string { Value: "hello world" },
  }
}

func (l *LinkedList a) Append(value a) {
  newNode := &Node{Value: value}

  if l.Tail == nil {
    l.Head = newNode
    l.Tail = l.Head
    return
  }

  l.Tail.Next = newNode
  l.Tail = l.Tail.Next
}

Isso é retirado de um Gist que tem um pouco mais de detalhes, bem como tipos de soma propostos aqui: https://gist.github.com/aarondl/9b950373642fcf5072942cf0fca2c3a2

Esta não é uma proposta de genéricos totalmente liberada e não é para ser, há muitos problemas a serem resolvidos para poder adicionar genéricos ao Go. Este aborda apenas a sintaxe, e espero que possamos conversar sobre se o que é proposto é viável / desejável ou não.

@aarondl
Parece bom para mim, usando esta sintaxe, teríamos:

type Collection a b interface {
   member(c a, e b) Bool
   insert(c a, e b) a
}

func insert(c *LinkedList a, e a) *LinkedList a {
   c.Append(e)
   return c
}

@keean Você poderia explicar um pouco o tipo Collection . não consigo entender:

type Collection a b interface {
   member(c a, e b) Bool
   insert(c a, e b) a
}

@dc0d Collection é uma interface que abstrai _all_ coleções, ou seja, árvores, listas, slices etc, para que possamos ter operações genéricas como member e insert que funcionarão em qualquer coleção contendo qualquer tipo de dado. No exemplo acima dei o exemplo de definição de 'inserir' para o tipo LinkedList no exemplo anterior:

func insert(c *LinkedList a, e a) *LinkedList a {
   c.Append(e)
   return c
}

Também poderíamos defini-lo para uma fatia

func insert(c []a, e a) []a {
   return append(c, e)
}

No entanto, nem precisamos do tipo de funções paramétricas com variáveis ​​de tipo como ilustrado por @aarondl com tipo polimórfico a para que isso funcione, pois você pode apenas definir para tipos concretos:

func insert(c *LinkedList int, e int) *LinkedList int {
   c.Append(e)
   return c
}

func insert(c *LinkedList float, e float) *LinkedList float {
   c.Append(e)
   return c
}

func insert(c int[], e int) int[] {
   return append(c, e)
}

func insert(c float[], e float) float[] {
   return append(c, e)
}

Portanto Collection é uma interface para generalizar tanto o tipo de um contêiner quanto o tipo de seu conteúdo, permitindo escrever funções genéricas que operam em todas as combinações de contêiner e conteúdo.

Não há razão para que você também não possa ter uma fatia de coleções []Collection onde o conteúdo seria todo de diferentes tipos de coleção com diferentes tipos de valores, desde que member e insert fossem definidos para cada combinação .

@aarondl Dado que type LinkedList a já é uma declaração de tipo válida, só posso ver duas maneiras de torná-la analisável de forma inequívoca: Tornando a gramática sensível ao contexto (entrando nos problemas de analisar C, ugh) ou usando lookahead ilimitado ( que a gramática go tende a evitar, por causa de mensagens de erro ruins no caso de falha). Eu posso estar entendendo mal alguma coisa, mas IMO que fala contra uma abordagem sem token.

@keean Interfaces em Go usam métodos, não funções. Na sintaxe específica que você sugeriu, não há nada que anexe insert a *LinkedList para o compilador (em Haskell isso é feito por meio de declarações instance ). Também é normal que os métodos alterem o valor em que estão operando. Nada disso é um Show-Stopper, apenas apontando que a sintaxe que você está sugerindo não funciona bem com Go. Provavelmente mais algo como

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

O que também demonstra mais algumas perguntas sobre como os parâmetros de tipo são definidos e como isso deve ser analisado.

@aarondl também tenho mais perguntas sobre sua proposta. Por exemplo, ele não permite restrições, então você só obtém polimorfismo irrestrito. O que, em geral, não é tão útil, pois você não tem permissão para fazer nada com os valores que está recebendo (por exemplo, você não pode implementar Collection com um mapa, pois nem todos os tipos são chaves de mapa válidas). O que deve acontecer quando alguém tenta fazer algo assim? Se for um erro em tempo de compilação, ele reclama da instanciação (mensagens de erro C++ adiante) ou da definição (não dá pra fazer basicamente nada, porque não há nada que funcione com todos os tipos)?

@keean Ainda não consigo entender como a está restrito a ser uma lista (ou fatia ou qualquer outra coleção). Esta é uma gramática especial dependente do contexto para coleções? Se sim qual o valor dela? Não é possível declarar tipos definidos pelo usuário dessa maneira.

@Merovius Isso significa que o Go não pode fazer despacho múltiplo e torna o primeiro argumento de uma 'função' especial? Isso sugere que os tipos associados seriam mais adequados do que as interfaces de vários parâmetros. Algo assim:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt(c Collection, e Collection.Element) {...}

No entanto, isso ainda tem problemas porque não há nada que restrinja as duas coleções a serem do mesmo tipo... Você acabaria precisando de algo como:

func[A] useIt(c A, e A.Element) requires A:Collection

Para tentar explicar a diferença, as interfaces multiparâmetros têm tipos _input_ extras que participam da seleção de instâncias (daí a conexão com despacho múltiplo), enquanto os tipos associados são tipos _output_, apenas o tipo receptor participa da seleção de instâncias e, em seguida, os tipos associados dependem do tipo do receptor.

@dc0d a e b são parâmetros de tipo da interface, assim como em uma classe de tipo Haskell. Para algo ser considerado um Collection tem que definir os métodos que correspondem aos tipos na interface onde a e b podem ser de qualquer tipo. No entanto, como o @Merovius apontou, as interfaces Go são baseadas em métodos e não suportam despacho múltiplo, portanto, interfaces com vários parâmetros podem não ser uma boa opção. Com o modelo de método de despacho único do Go, ter tipos associados em interfaces, em vez de parâmetros múltiplos, parece ser um ajuste melhor. No entanto, a falta de despacho múltiplo torna a implementação de funções como unify(x, y) difícil, e você tem que usar o padrão de despacho duplo que não é muito bom.

Para explicar um pouco mais a questão dos multiparâmetros:

type Cloneable[A] interface {
   clone(x A) A
}

Aqui a significa qualquer tipo, não importa o que seja, desde que as funções corretas sejam definidas, consideramos Cloneable . Consideraríamos as interfaces como restrições de tipos em vez de tipos em si.

func clone(x int) int {...}

então no caso de 'clone' nós substituímos a por int na definição da interface, e podemos chamar clone se a substituição for bem sucedida. Isso se encaixa muito bem com esta notação:

func[A] test(x A) A requires Cloneable[A] {...}

Isso é equivalente a:

type Cloneable interface {
   clone() Cloneable
}

mas declara uma função, não um método, e pode ser estendido com vários parâmetros. Se você tem uma linguagem com despacho múltiplo, não há nada de especial sobre o primeiro argumento de uma função/método, então por que escrevê-lo em um lugar diferente.

Como o Go não tem despacho múltiplo, tudo isso começa a parecer muito para mudar de uma só vez. Parece que os tipos associados seriam mais adequados, embora mais limitados. Isso permitiria coleções abstratas, mas não soluções elegantes para coisas como unificação.

@Merovius Obrigado por dar uma olhada na proposta. Deixe-me tentar resolver suas preocupações. Estou triste que você tenha rejeitado a proposta antes de discutirmos mais, espero poder mudar sua mente - ou talvez você possa mudar a minha :)

Antecipação ilimitada:
Então, como mencionei na proposta, atualmente parece que a gramática Go tem uma boa maneira de detectar o "fim" de praticamente tudo sintaticamente. E ainda o faríamos por causa dos argumentos genéricos implícitos. Letras minúsculas sendo a construção sintática que cria esse argumento genérico - ou o que quer que decidamos fazer esse token embutido, talvez até voltemos para uma coisa tokenizada como @a na proposta se gostarmos da sintaxe o suficiente, mas não é possível dada a dificuldade do compilador sem tokens, embora a proposta perca muito charme assim que você fizer isso.

Independentemente do problema com type LinkedList a nesta proposta não é tão difícil porque sabemos que a é um argumento de tipo genérico e, portanto, isso falharia com um erro de compilador igual a type LinkedList falha hoje com: prog.go:3:16: expected type, found newline (and 1 more errors) . O post original realmente não saiu e disse isso, mas você não tem mais permissão para nomear um tipo concreto [a-z]{1} que eu acho que resolve esse problema e é um sacrifício com o qual todos ficaríamos bem fazendo (só posso ver prejuízos na criação de tipos reais com nomes de letra única no código Go hoje).

É apenas polimorfismo irrestrito
A razão pela qual eu omiti qualquer tipo de traços ou restrições de argumentos genéricos é porque sinto que esse é o papel das interfaces em Go, se você quiser fazer algo com um valor, esse valor deve ser um tipo de interface e não um tipo totalmente genérico. Acho que essa proposta também funciona bem com interfaces.

Sob esta proposta, ainda teríamos o mesmo problema que temos agora com operadores como + então você não poderia fazer uma função add genérica para todos os tipos numéricos, mas você poderia aceitar uma função add genérica como um argumento. Considere o seguinte:

func Sort(slice []a, compare func (a, a) bool) { ... }

Dúvidas sobre escopo

Você deu um exemplo aqui:

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

O escopo desses identificadores como regra está vinculado à declaração/definição específica em que eles estão. Eles não são compartilhados em nenhum lugar e não estou vendo uma razão para eles serem.

@keean Isso é muito interessante, embora, como outros apontaram, você teria que alterar o que mostrou lá para realmente poder implementar as interfaces (atualmente, no seu exemplo, não há métodos com receptores, apenas funções). Tentando pensar mais sobre como isso afeta minha proposta original.

Letras minúsculas sendo a construção sintática que cria esse argumento genérico

Não me sinto bem com isso; requer ter produções separadas para o que é um identificador dependendo do contexto e também significa proibir arbitrariamente certos identificadores para tipos. Mas não é realmente o momento de falar sobre esses detalhes.

Sob esta proposta, ainda teríamos o mesmo problema que temos agora com operadores como +

Eu não entendo essa frase. Atualmente, o operador + não tem nenhum desses problemas, pois os tipos de seus operandos são conhecidos localmente e a mensagem de erro é clara e inequívoca e aponta para a origem do problema. Estou correto em supor que você está dizendo que deseja proibir qualquer uso de valores genéricos que não seja permitido para todos os tipos possíveis (não consigo pensar em muitas dessas operações)? E criar um erro de compilador para a expressão incorreta na função genérica? IMO que limitaria muito o valor dos genéricos.

se você quiser fazer algo com um valor, esse valor deve ser um tipo de interface e não um tipo totalmente genérico.

As duas principais razões pelas quais as pessoas querem genéricos são desempenho (evitar encapsulamento de interfaces) e segurança de tipo (certificando-se de que o mesmo tipo seja usado em lugares diferentes, sem se importar com qual é). Isso parece ignorar essas razões.

você pode aceitar uma função add genérica como um argumento.

Verdadeiro. Mas bem pouco ergonômico. Considere quantas reclamações existem sobre a API sort . Para muitos contêineres genéricos, a quantidade de funções que o chamador teria que implementar e passar parece ser proibitiva. Considere, como seria uma implementação container/heap sob esta proposta e como seria melhor do que a implementação atual, em termos de ergonomia? Parece que as vitórias são insignificantes aqui, na melhor das hipóteses. Você teria que implementar funções mais triviais (e duplicar para/referência em cada site de uso), não menos.

@Merovius

pensando nesse ponto do @aarondl

você pode aceitar uma função add genérica como um argumento.

Seria melhor ter uma interface Addable para permitir a sobrecarga de adição, dada alguma sintaxe para definir operadores infixos:

type Addable interface {
   + (x Addable, y Addable) Addable
}

Infelizmente, isso não funciona, porque não expressa que esperamos que todos os tipos sejam iguais. Para definir addable, precisaríamos de algo como as interfaces multiparâmetros:

type Addable[A] interface {
   + (x A, y A) A
}

Então você também precisaria de Go para fazer despacho múltiplo, o que significaria que todos os argumentos em uma função são tratados como um receptor para correspondência de interface. Portanto, no exemplo acima, qualquer tipo é Addable se houver uma função + definida nele que satisfaça as definições de função na definição da interface.

Mas, dadas essas alterações, agora você pode escrever:

type S struct {
   value: int
}

func (+) (x S, y S) S {
   return S {
      value: x.value + y.value
   }
}

func main() {
    println(S {value: 27} + S {value: 5})
}

É claro que a sobrecarga de funções e o despacho múltiplo podem ser algo que as pessoas nunca querem em Go, mas coisas como definir aritmética básica em tipos definidos pelo usuário, como vetores, matrizes, números complexos etc., sempre serão impossíveis. Como eu disse acima, 'tipos associados' em interfaces permitiriam algum aumento na capacidade de programação genérica, mas não generalidade total. O despacho múltiplo (e presumivelmente sobrecarga de funções) é algo que poderia acontecer em Go?

coisas como definir aritmética básica em tipos definidos pelo usuário como vetores, matrizes, números complexos etc, sempre serão impossíveis.

Alguns podem considerar isso um recurso :) AFAIR há alguma proposta ou tópico circulando em algum lugar discutindo se deveria. FWIW, eu acho que isso é - novamente - vagando fora do tópico. Sobrecarga de operador (ou ideias gerais de "como tornar o Go mais Haskell") não é realmente o ponto deste problema :)

O despacho múltiplo (e presumivelmente sobrecarga de funções) é algo que poderia acontecer em Go?

Nunca diga nunca. Eu não esperaria isso, porém, pessoalmente.

@Merovius

Alguns podem considerar isso um recurso :)

Claro, e se Go não fizer isso, existem outras linguagens que o farão :-) Go não precisa ser tudo para todos. Eu estava apenas tentando estabelecer algum escopo para genéricos em Go. Meu foco é criar linguagens totalmente genéricas, pois tenho aversão a me repetir e clichê (e não gosto de macros). Se eu ganhasse um centavo para cada vez que tivesse que escrever uma lista encadeada ou uma árvore em 'C' para algum tipo de dado específico. Na verdade, torna alguns projetos impossíveis para uma equipe pequena por causa do volume de código que precisa ser mantido em sua cabeça para entendê-lo e, em seguida, mantido por meio de alterações. Às vezes acho que as pessoas que não entendem a necessidade de genéricos ainda não escreveram um programa grande o suficiente. É claro que você pode ter uma grande equipe de desenvolvedores trabalhando em algo e apenas ter cada desenvolvedor responsável por uma pequena parte do código total, mas estou interessado em tornar um único desenvolvedor (ou equipe pequena) o mais eficaz possível.

Dado que a sobrecarga de função e o despacho múltiplo estão fora do escopo, e também dados os problemas de análise com a sugestão de @aarondl , parece que adicionar tipos associados a interfaces e parâmetros de tipo a funções seria o máximo que você gostaria ir com genéricos em Go.

Algo assim parece ser o tipo certo de coisa:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt<T>(c T, e T.Element) requires T:Collection {...}

Em seguida, haveria uma decisão na implementação de usar tipos paramétricos ou tipos quantificados universalmente. Com tipos paramétricos (como Java), uma função 'genérica' não é realmente uma função, mas algum tipo de modelo de função de tipo seguro e, como tal, não pode ser passado como um argumento, a menos que tenha seu parâmetro de tipo fornecido:

f(useIt) // not okay with parametric types
f(useIt<List>) // okay with parametric types

Com tipos quantificados universalmente, você pode passar useIt como um argumento e, em seguida, pode ser fornecido com um parâmetro de tipo dentro f . A razão para favorecer os tipos paramétricos é porque você pode monomorfizar o polimorfismo em tempo de compilação, o que significa que não há elaboração de funções polimórficas em tempo de execução. Não tenho certeza se isso é uma preocupação com Go, porque Go já está fazendo despacho em tempo de execução em interfaces, portanto, desde que o parâmetro de tipo para useIt implemente Coleção, você pode despachar para o receptor correto em tempo de execução, tão universal a quantificação é provavelmente o caminho certo para o Go.

Eu me pergunto, SFINAE mencionado apenas por @bcmills. Nem sequer mencionado na proposta (embora Sort esteja lá como exemplo).
Como seria o Sort para slice e linkedlist?

@keean
Não consigo descobrir como alguém definiria uma coleção genérica 'Slice' com sua sugestão. Você parece estar definindo um 'IntSlice' que pode estar implementando 'Collection' (embora Insert retorne um tipo diferente do desejado pela interface), mas isso não é um 'slice' genérico, pois parece ser apenas para ints , e as implementações de método são apenas para ints. Precisamos definir implementação específica por tipo?

Às vezes acho que as pessoas que não entendem a necessidade de genéricos ainda não escreveram um programa grande o suficiente.

Posso assegurar-lhe que a impressão é falsa. E FWIW, ISTM que "o outro lado" está colocando "não vendo a necessidade" no mesmo balde que "não vendo o uso". Eu vejo o uso e não refuto. Eu realmente não vejo a necessidade , no entanto. Estou indo bem sem, mesmo em grandes bases de código.

E não confunda "querer que sejam bem feitas e apontar onde as propostas existentes não estão" com "opor-se fundamentalmente à própria ideia".

também devido aos problemas de análise com a sugestão de @aarondl .

Como eu disse, não acho que falar sobre o problema de análise seja realmente produtivo agora. Problemas de análise podem ser resolvidos. A falha do polimorfismo restrito é muito mais séria, semanticamente. IMO, adicionar genéricos sem isso simplesmente não vale o esforço.

@urandom

Não consigo descobrir como alguém definiria uma coleção genérica 'Slice' com sua sugestão.

Como dado acima, você ainda precisaria definir uma implementação separada para cada tipo de fatia, mas ainda ganharia com a capacidade de escrever algoritmos em termos de interface genérica. Se você quiser permitir uma implementação genérica para todas as fatias, precisará permitir tipos e métodos associados paramétricos. Observe que movi o parâmetro type para depois da palavra-chave para que ocorra antes do tipo do receptor.

type<T> []T.Element = Int

func<T> ([]T) Member(e T) Bool {...}
func<T> ([]T) Insert(e T) Collection {...}

No entanto, agora você também precisa lidar com a especialização, porque alguém poderia definir o tipo e os métodos associados para o []int mais especializado e você teria que lidar com qual deles usar. Normalmente você usaria a instância mais específica, mas adiciona outra camada de complexidade.

Não tenho certeza de quanto isso realmente ganha você. Com meu exemplo original acima, você pode escrever algoritmos genéricos para atuar em coleções gerais usando a interface, e você só precisa fornecer os métodos e tipos associados para os tipos que você realmente usa. A grande vitória para mim é poder definir algoritmos como classificar em coleções arbitrárias e colocar esses algoritmos em uma biblioteca. Se eu tiver uma lista de "formas", só preciso definir os métodos de interface de coleção para minha lista de formas e posso usar qualquer algoritmo na biblioteca sobre elas. Ser capaz de definir os métodos de interface para todos os tipos de fatias é de menos interesse para mim e pode ser muita complexidade para o Go?

@Merovius

Eu realmente não vejo a necessidade, no entanto. Estou indo bem sem, mesmo em grandes bases de código.

Se você puder lidar com um programa de 100.000 linhas, poderá fazer mais com 100.000 linhas genéricas do que com 100.000 linhas não genéricas (devido à repetição). Portanto, você pode ser um desenvolvedor super-estrela capaz de lidar com bases de código muito grandes, mas ainda conseguiria mais com uma base de código genérica muito grande, pois estaria eliminando a redundância. Esse programa genérico se expandiria para um programa não genérico ainda maior. Parece-me que você ainda não atingiu seu limite de complexidade.

No entanto, acho que você está certo 'necessidade' é muito forte, estou feliz em escrever código go, com apenas uma frustração ocasional sobre a falta de genéricos, e posso contornar isso simplesmente escrevendo mais código, e em Go esse código é completamente direto e literais.

A falta de polimorfismo restrito é muito mais séria, semanticamente. IMO, adicionar genéricos sem isso simplesmente não vale o esforço.

Eu concordo com isto.

você poderá fazer mais com 100.000 linhas genéricas do que com 100.000 linhas não genéricas (devido à repetição)

Estou curioso, do seu exemplo hipotético, qual % dessas linhas seria uma função genérica?
Na minha experiência, isso é menos de 2% (de uma base de código com 115k LOC), então não acho que seja um bom argumento, a menos que você escreva uma biblioteca para "coleções"

Eu gostaria que eventualmente tivéssemos genéricos

@keean

Em relação à sua alegação de que você não pode fazer este exemplo em Haskell, aqui está o código:

Este código não é moralmente equivalente ao código que escrevi. Ele apresenta um novo tipo de wrapper Cloneable além da interface ICloneable. O código Go não precisava de um wrapper; nem outros idiomas que suportam subtipagem.

@andrewcmyers

Este código não é moralmente equivalente ao código que escrevi. Ele apresenta um novo tipo de wrapper Cloneable além da interface ICloneable.

Não é isso que este código faz:

type Cloneable interface {...}

Ele induz um tipo de dados 'Cloneable' derivado da interface. Você não vê o 'ICloneable' porque não tem declarações de instância para interfaces, apenas declara os métodos.

Você pode considerar a subtipagem quando os tipos que implementam uma interface não precisam ser estruturalmente compatíveis?

@keean Eu consideraria Cloneable apenas um tipo, não realmente um "tipo de dados". Em uma linguagem como Java, essencialmente não haveria custo adicional para a abstração Cloneable , porque não haveria wrapper, ao contrário do seu código.

Parece-me limitante e indesejável exigir similaridade estrutural entre os tipos que implementam uma interface, então estou confuso sobre o que você está pensando aqui.

@andrewcmyers
Estou usando tipo e tipo de dados de forma intercambiável. Qualquer tipo que possa conter dados é um tipo de dados.

porque não haveria wrapper, ao contrário do seu código.

Sempre há um wrapper porque os tipos Go são sempre encaixotados, então o wrapper existe em torno de tudo. Haskell precisa que o wrapper seja explícito porque possui tipos sem caixa.

similaridade estrutural entre tipos implementando uma interface, então estou confuso sobre o que você está pensando aqui.

A subtipagem estrutural exige que os tipos sejam 'estruturalmente compatíveis'. Como não há hierarquia de tipos explícita como em uma linguagem OO com herança, a subtipagem não pode ser nominal, portanto deve ser estrutural, se existir.

Eu entendo o que você quer dizer, no entanto, que eu descreveria como considerando uma interface como uma classe base abstrata, não uma interface, com algum tipo de relacionamento de subtipo nominal implícito com qualquer tipo que implemente os métodos necessários.

Na verdade, acho que Go se encaixa nos dois modelos agora, e poderia ir de qualquer maneira a partir daqui, mas sugiro que chamá-lo de interface e não de classe sugere uma maneira de pensar sem subtipagem.

@keean Não entendi seu comentário. Primeiro você me diz que discorda e que eu "ainda não atingi meu limite de complexidade" e então você me diz que concorda (naquele "necessidade" é uma palavra muito forte). Eu também acho que seu argumento é falacioso (você assume que LOC é a principal medida de complexidade e que cada linha de código é igual). Mas acima de tudo, não acho que "quem está escrevendo programas mais complicados" seja realmente uma linha de discussão produtiva. Eu estava apenas tentando esclarecer que o argumento "se você discorda de mim, isso deve significar que você não está trabalhando em problemas tão difíceis ou interessantes" não é convincente e não parece de boa fé. Espero que você possa confiar que as pessoas podem discordar de você sobre a importância desse recurso enquanto são igualmente competentes e fazem coisas igualmente interessantes.

@merovius
Eu estava dizendo que você provavelmente é um programador mais capaz do que eu e, portanto, capaz de trabalhar com mais complexidade. Certamente não acho que você esteja trabalhando em problemas menos interessantes ou menos complexos, e lamento que tenha sido assim. Passei ontem tentando fazer um scanner funcionar, o que foi um problema muito desinteressante.

Posso pensar que os genéricos me ajudam a escrever programas mais complexos com minha capacidade cerebral limitada e também admitir que não "preciso" de genéricos. É uma questão de grau. Ainda posso programar sem genéricos, mas não posso necessariamente escrever software da mesma complexidade.

Espero que isso lhe assegure que estou agindo de boa fé, não tenho nenhuma agenda oculta aqui e, se Go não adotar genéricos, ainda o usarei. Eu tenho uma opinião sobre a melhor forma de fazer genéricos, mas não é a única opinião, só posso falar por experiência própria. Se eu não estiver ajudando, há muitas outras coisas em que posso gastar meu tempo, então apenas diga a palavra, e eu me concentrarei em outro lugar.

@Merovius Obrigado pelo diálogo contínuo.

| As duas principais razões pelas quais as pessoas querem genéricos são desempenho (evitar encapsulamento de interfaces) e segurança de tipo (certificando-se de que o mesmo tipo seja usado em lugares diferentes, sem se importar com qual é). Isso parece ignorar essas razões.

Talvez estejamos olhando para o que propus de maneira muito diferente, já que, da minha perspectiva, faz as duas coisas até onde posso dizer? No exemplo da lista vinculada, não há envolvimento com interfaces e, portanto, deve ter o desempenho como se fosse escrito à mão para um determinado tipo. No lado de segurança de tipo é o mesmo. Existe um contra-exemplo que você pode dar aqui para me ajudar a entender de onde você está vindo?

| Verdadeiro. Mas bem pouco ergonômico. Considere quantas reclamações existem sobre a API de classificação. Para muitos contêineres genéricos, a quantidade de funções que o chamador teria que implementar e passar parece ser proibitiva. Considere, como seria uma implementação de contêiner/heap sob esta proposta e como seria melhor do que a implementação atual, em termos de ergonomia? Parece que as vitórias são insignificantes aqui, na melhor das hipóteses. Você teria que implementar funções mais triviais (e duplicar para/referência em cada site de uso), não menos.

Na verdade, não estou nem um pouco preocupado com isso. Não acredito que a quantidade de funções seja proibitiva, mas definitivamente estou aberto a ver alguns contra-exemplos. Lembre-se de que a API da qual as pessoas reclamaram não era aquela para a qual você precisava fornecer uma função, mas a original aqui: https://golang.org/pkg/sort/#Interface onde você precisava criar um novo tipo que era simplesmente sua fatia + tipo e, em seguida, implemente 3 métodos nele. À luz das reclamações e da dor associada a essa interface, foi criado o seguinte: https://golang.org/pkg/sort/#Slice , eu não tenho nenhum problema com esta API e recuperaríamos as penalidades de desempenho desta sob a proposta que estamos discutindo simplesmente alterando a definição para func Slice(slice []a, less func(a, a) bool) .

Em termos da estrutura de dados container/heap , não importa qual proposta genérica você aceite, que precisa de uma reescrita completa. container/heap assim como o pacote sort está apenas fornecendo algoritmos em cima de sua própria estrutura de dados, mas nenhum pacote possui a estrutura de dados porque, caso contrário, teríamos []interface{} e o custos associados a isso. Presumivelmente, nós os mudaríamos, pois você poderia ter um Heap que possui uma fatia com um tipo concreto graças aos genéricos, e isso é verdade em qualquer uma das propostas que vi aqui (incluindo a minha) .

Estou tentando separar as diferenças em nossas perspectivas sobre o que eu propus. E acho que a raiz do desacordo (além de qualquer preferência pessoal sintaticamente) é que não há restrições sobre os tipos genéricos. Mas ainda estou tentando descobrir o que isso nos traz. Se a resposta é que nada no que diz respeito ao desempenho tem permissão para usar uma interface, então não há muito o que dizer aqui.

Considere a seguinte definição de tabela de hash:

// Hasher turns a key into a hash
type Hasher interface {
  func Hash() []byte
}

type HashTable v struct {
   Keys   []Hasher
   Values []v
}

// Note that the generic arguments must be repeated here and immediately
// understood without reading another line of code, which to me
// is a readability win over the sudden appearance of the K and V which are
// defined elsewhere in the code in the example below. This is of course because
// the tokenized type declarations with constraints are fairly painful in general
// and repeating them everywhere is simply too much.
func (h (*HashTable v)) Insert(key Hasher, value v) { ... }

Estamos dizendo que o []Hasher não é inicial devido a problemas de desempenho/armazenamento e que para ter uma implementação de Genéricos bem-sucedida em Go, devemos absolutamente ter algo como o seguinte?

// Without selecting another proposal I have no idea how the constraint might be defined or implemented so let's just pretend
type [K: Hasher, V] HashTable a struct {
   Keys   []K
   Values []V
}

func (h *HashTable) Insert(key K, value V) { ... }

Espero que você veja de onde estou vindo. Mas é definitivamente possível que eu não entenda as restrições que você deseja impor a determinado código. Talvez haja casos de uso que eu não considerei, independentemente, espero chegar a uma compreensão mais completa de quais são os requisitos e como a proposta está falhando neles.

Talvez estejamos olhando para o que propus de maneira muito diferente, já que, da minha perspectiva, faz as duas coisas até onde posso dizer?

O "isto" na seção que você está citando está se referindo ao uso de interfaces. A questão não é que sua proposta também não, é que sua proposta não permite polimorfismo restrito, o que exclui a maioria dos usos para eles. E a alternativa que você sugeriu para onde interfaces, que também não abordam o caso de uso principal para genéricos (por causa das duas coisas que mencionei).

Por exemplo, sua proposta (como escrita originalmente) não permitia escrever um mapa genérico de qualquer tipo, pois isso exigiria poder pelo menos comparar chaves usando == (que é uma restrição, portanto, implementar um map requer polimorfismo restrito).

À luz das queixas e dores associadas a esta interface foi criado o seguinte: https://golang.org/pkg/sort/#Slice

Observe que essa interface ainda não é possível em sua proposta de genéricos, pois ela depende de reflexão para comprimento e troca (portanto, novamente, você tem uma restrição nas operações de fatia). Mesmo se aceitarmos essa API como o limite inferior do que os genéricos devem ser capazes de realizar (muitas pessoas não aceitariam. Ainda há muitas reclamações sobre a falta de segurança de tipo nessa API), sua proposta não passaria aquela barra.

Mas também, novamente, você está citando uma resposta para um ponto específico que você fez, ou seja, que você pode obter polimorfismo restrito passando literais de função na API. E essa maneira específica que você sugeriu para contornar a falta de polimorfismo restrito exigiria a implementação mais ou menos da API antiga. ou seja, você está citando minha resposta a este argumento, que você está apenas repetindo:

recuperaríamos as penalidades de desempenho disso sob a proposta que estamos discutindo simplesmente alterando a definição para func Slice(slice []a, less func(a, a) bool).

Essa é a API antiga. Você está dizendo "minha proposta não permite polimorfismo restrito, mas isso não é problema, porque simplesmente não podemos usar genéricos e, em vez disso, usar as soluções existentes (reflexão/interfaces)". Bem, responder a "sua proposta não permite os casos de uso mais básicos para os quais as pessoas querem genéricos" com "podemos apenas fazer as coisas que as pessoas já estão fazendo sem genéricos para os casos de uso mais básicos" não parece nos entender em qualquer lugar, tbm. Uma proposta genérica que não te ajuda a escrever nem mesmo tipos básicos de container, sort, max… simplesmente não parece valer a pena.

isso é verdade em qualquer uma das propostas que vi aqui (incluindo a minha).

A maioria das propostas genéricas inclui alguma maneira de restringir os parâmetros de tipo. ou seja, para expressar "o parâmetro de tipo deve ter um método Less", ou "o parâmetro de tipo deve ser comparável". O seu - AFAICT - não.

Considere a seguinte definição de tabela de hash:

Sua definição está incompleta. a) O tipo de chave também precisa de igualdade e b) você não está impedindo o uso de diferentes tipos de chave. ou seja, isso seria legal:

type hasherA uint64

func (a hasherA) Hash() []byte {
    b := make([]byte, 8)
    binary.BigEndian.PutUint64(b, uint64(a))
    return b
}

type hasherB string

func (b hasherB) Hash() []byte {
    return []byte(b)
}

h := new(HashTable int)
h.Insert(hasherA(42), 1)
h.Insert(hasherB("Hello world"), 2)

No entanto, não deve ser legal, pois você está usando diferentes tipos de chaves. ou seja, o contêiner não é verificado de acordo com o grau que as pessoas desejam. Você precisa parametrizar a tabela de hash sobre o tipo de chave e valor

type HashTable k v struct {
    Keys []k
    Values []v
}

func (h *(HashTable k v)) Insert(key k, value v) {
    // You can't actually do anything with k, as it's unconstrained. i.e. you can't hash it, compare it…
    // Implementing this is impossible in your proposal.
}

// If it weren't impossible, you'd get this:
h := new(HashTable hasherA int)
h[hasherA(42)] = 1
h[hasherB("Hello world")] = 2 // compile error - can't use hasherB as hasherA

Ou, se ajudar, imagine que você está tentando implementar um conjunto de hash. Você teria o mesmo problema, mas agora o contêiner resultante não tem nenhuma verificação de tipo adicional sobre interface{} .

É por isso que sua proposta não aborda os casos de uso mais básicos: ela depende de interfaces para restringir o polimorfismo, mas na verdade não fornece nenhuma maneira de verificar a consistência dessas interfaces. Você pode ter uma verificação de tipo consistente ou ter polimorfismo restrito, mas não ambos. Mas você precisa de ambos.

que para ter uma implementação de Genéricos bem-sucedida em Go, devemos ter algo como o seguinte?

É pelo menos como me sinto sobre isso, sim, praticamente. Se uma proposta não permite escrever contêineres de tipo seguro ou classificar ou… ela realmente não adiciona nada à linguagem existente que seja significativo o suficiente para justificar o custo.

@Merovius Ok. Acho que entendi o que você quer. Tenha em mente que seus casos de uso estão muito longe do que eu quero. Eu não estou realmente ansioso por recipientes seguros, embora eu suspeite - como você afirmou - que pode ser uma opinião minoritária. Algumas das maiores coisas que eu gostaria de ver são tipos de resultados em vez de erros e manipulação fácil de fatias sem duplicação ou reflexão em todos os lugares que minha proposta faz um trabalho razoável de endereçamento. No entanto, posso ver como, da sua perspectiva, "não aborda os casos de uso mais básicos" se seu caso de uso básico for escrever contêineres genéricos sem o uso de interfaces,

Observe que essa interface ainda não é possível em sua proposta de genéricos, pois ela depende de reflexão para comprimento e troca (portanto, novamente, você tem uma restrição nas operações de fatia). Mesmo se aceitarmos essa API como o limite inferior do que os genéricos devem ser capazes de realizar (muitas pessoas não aceitariam. Ainda há muitas reclamações sobre a falta de segurança de tipo nessa API), sua proposta não passaria aquela barra.

Lendo isso, fica claro que você entendeu mal a maneira como as fatias genéricas funcionariam/deveriam funcionar sob essa proposta. É por meio desse mal-entendido que você chegou à falsa conclusão de que "essa interface ainda não é possível na sua proposta". Sob qualquer proposta, uma fatia genérica deve ser possível, é o que eu acho. E len() no mundo, como eu vi, seria definido como: func len(slice []a) , que é um argumento de fatia genérico, o que significa que ele pode contar o comprimento de maneira não-reflexiva para qualquer fatia. Este é muito o ponto desta proposta como eu disse acima (fácil manipulação de fatias) e sinto muito por não ter conseguido transmitir isso bem através dos exemplos que dei e da essência que fiz. Uma fatia genérica deve poder ser usada tão facilmente quanto um []int é hoje, direi novamente que qualquer proposta que não aborde isso (trocas de fatia/array, atribuição, len, cap, etc. ) está aquém na minha opinião.

Tudo isso dito, agora estamos realmente claros sobre quais são os objetivos um do outro. Quando propus o que fiz, disse muito que era simplesmente uma proposta sintática e que os detalhes eram super confusos. Mas nós meio que entramos nos detalhes de qualquer maneira e um desses detalhes acabou sendo a falta de restrições, quando eu escrevi eu simplesmente não os tinha em mente porque eles não são importantes para o que eu gostaria de fazer , não quer dizer que não podemos adicioná-los ou que eles não são desejáveis. O principal problema em continuar com a sintaxe proposta e tentar inserir restrições seria que a definição de um argumento genérico atualmente se repete (intencionalmente), então não há referência a código em outro lugar para determinar restrições etc. não vejo como poderíamos manter isso.

O melhor contra-exemplo é aquela função de classificação que discutimos anteriormente.

type Sort(slice []a:Lesser, less func(a:Lesser, a:Lesser)) { ... }

Como você pode ver, não há uma boa maneira de fazer isso acontecer, e as abordagens de token-spam para Genéricos começam a soar melhor novamente. Para definir restrições sobre isso, precisamos alterar duas coisas da proposta original:

  • É preciso haver uma maneira de apontar para um argumento de tipo e dar-lhe restrições.
  • As restrições precisam durar mais do que uma única definição, talvez esse escopo seja um tipo, talvez esse escopo seja um arquivo (arquivo na verdade parece bastante razoável).

Isenção de responsabilidade: O seguinte não é uma alteração real da proposta porque estou apenas lançando símbolos aleatórios, estou apenas usando essas sintaxes como exemplos para ilustrar o que poderíamos fazer para alterar a proposta como está originalmente

// Decorator style, follows the definition of the type thorugh all
// of it's methods.
<strong i="14">@a</strong>: Lesser, Hasher, Equaler
func Sort(slice []a) { ... }
<strong i="15">@k</strong>: Equaler, Hasher
type HashTable k v struct

// Inline, follows the definition of the type through
// all of it's methods.
func [a: Hasher, Equaler] Sort(slice []a) { ... }
type [k: Hasher, Equaler] HashTable k v struct

// File-scope global style, if k appears as a generic argument
// it's constrained by this that appears at the top of the file underneath
// the imports but before any other code.
<strong i="16">@k</strong>: Equaler, Hasher

Mais uma vez, observe que nenhuma das opções acima eu realmente quero adicionar à proposta. Estou apenas mostrando que tipo de construções poderíamos usar para resolver o problema, e como elas se parecem é um tanto irrelevante no momento.

A pergunta que precisamos responder é: ainda ganhamos valor com os argumentos genéricos implícitos? O ponto principal da proposta era manter a sensação limpa do estilo Go da linguagem, manter as coisas simples, manter as coisas com ruído suficientemente baixo, eliminando tokens excessivos. Nos muitos casos em que não há restrições necessárias, por exemplo, uma função de mapa ou a definição de um tipo de resultado, parece bom, parece Go, é útil? Assumindo que as restrições também estão disponíveis de uma forma ou de outra.

func map(slice []a, mapper func(a) b) {
  for i := range slice {
    slice[i] = mapper(slice[i])
  }
}

type Result a b struct {
  Ok  a
  Err b
}

@aarondl vou tentar explicar. A razão pela qual você precisa de restrições de tipo é porque essa é a única maneira de chamar funções ou métodos em um tipo. considere o tipo irrestrito a que tipo pode ser, bem, pode ser uma string ou um Int ou qualquer coisa. Portanto, não podemos chamar nenhuma função ou método porque não conhecemos o tipo. Poderíamos usar uma mudança de tipo e reflexão de tempo de execução para obter o tipo e, em seguida, chamar algumas funções ou métodos nele, mas isso é algo que queremos evitar com genéricos. Quando você restringe um tipo, por exemplo a é um Animal, podemos chamar qualquer método definido para um animal em a .

No seu exemplo, sim, você pode passar uma função mapeadora, mas isso resultará em funções que recebem muitos argumentos e é basicamente como uma linguagem sem interfaces, apenas funções de primeira classe. Para passar todas as funções que você usará no tipo a obterá uma lista muito longa de funções em qualquer programa real, especialmente se você estiver escrevendo principalmente código genérico para injeção de dependência, o que você deseja fazer para minimizar o acoplamento.

Por exemplo, e se a função que chama map também for genérica? E se a função que chama isso for genérica etc. Como definimos o mapeador se ainda não sabemos o tipo de a ?

func m(slice []a) []b {
   mapper := func(x a) b {...}
   return map(slice, mapper)
}

Quais funções podemos chamar em x ao tentar definir mapper ?

@keean Eu entendo o propósito e a função das restrições. Eu simplesmente não os valorizo ​​tanto quanto coisas simples como estruturas de contêineres genéricas (não contêineres genéricos, por assim dizer) e fatias genéricas e, portanto, nem as incluí na proposta original.

Eu ainda acredito principalmente que as interfaces são a resposta certa para problemas como o que você está falando onde você está fazendo injeção de dependência, que simplesmente não parece ser o lugar certo para genéricos, mas quem sou eu para dizer. A sobreposição entre suas responsabilidades é bastante grande aos meus olhos, por isso @Merovius e eu tivemos que discutir se poderíamos ou não viver sem eles, e ele me convenceu de que seriam úteis em alguns casos de uso, portanto, exploramos um pouco do que poderíamos fazer para adicionar o recurso à proposta que fiz originalmente.

Quanto ao seu exemplo, você não pode chamar nenhuma função em x. Mas você ainda pode operar na fatia como qualquer outra fatia, o que é tremendamente útil por conta própria. Também não tenho certeza qual é o func dentro do func ... talvez você quis dizer atribuir a um var?

@aarondl
Obrigado, consertei a sintaxe, mas acho que o significado ainda ficou claro.

Os exemplos que dei acima usaram polimorfismo paramétrico e interfaces para atingir algum nível de programação genérica, no entanto, a falta de despacho múltiplo sempre colocará um teto no nível de generalidade alcançável. Como tal, parece que o Go não vai fornecer os recursos que procuro em uma linguagem, isso não significa que eu não possa usar o Go para algumas tarefas, e de fato eu já estou e funciona bem, mesmo que eu tenha para recortar e colar código que realmente só precisa de uma definição. Só espero que no futuro, se esse código precisar ser alterado, o desenvolvedor possa encontrar todas as instâncias coladas dele.

Estou então em dúvida se a generalidade limitada possível sem grandes mudanças na linguagem é uma boa ideia, considerando a complexidade que isso adicionará. Talvez Go seja melhor permanecer simples, e as pessoas possam adicionar macro como pré-processamento ou outras linguagens que compilam para Go, para fornecer esses recursos? Por outro lado, adicionar polimorfismo paramétrico seria um bom primeiro passo. Permitir que esses parâmetros de tipo sejam restritos seria um bom próximo passo. Então você poderia adicionar parâmetros de tipo associados a interfaces, e você teria algo razoavelmente genérico, mas isso é provavelmente o máximo que você pode obter sem despacho múltiplo. Ao dividir em recursos menores separados, acho que você aumentaria a chance de serem aceitos?

@keean
O despacho múltiplo é tudo o que é necessário? Muito poucos idiomas o suportam nativamente. Mesmo C++ não suporta isso. C# meio que suporta via dynamic mas eu nunca usei na prática e a palavra-chave em geral é muito rara no código real. Os exemplos que lembro lidam com algo como análise JSON, não escrevendo genéricos.

O despacho múltiplo é tudo o que é necessário?

IMHO, acho que @keean fala sobre despacho múltiplo estático fornecido por typeclasses/interfaces.
Isso é fornecido em C++ por sobrecarga de método (não sei para C#)

O que você quer dizer é despacho múltiplo dinâmico, que é bastante complicado em linguagens estáticas sem tipos de união. As linguagens dinâmicas contornam esse problema omitindo a verificação de tipo estático (inferência de tipo parcial para linguagens dinâmicas, o mesmo para o tipo "Dinâmico" do C#).

Um tipo pode ser fornecido como "apenas" um parâmetro?

func Append(t, t2 type, arr []t, value t2) []t {
    v := t(value) // conversion
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

@Inuart escreveu:

Um tipo pode ser fornecido como "apenas" um parâmetro?

Questionável até que ponto isso seria possível ou desejado em

O que você deseja pode ser alcançado se as restrições genéricas forem suportadas:

func Append(arr []t, value s) []t  requires Convertible<s,t>{
    v := t(value) // conversion
    return append(arr, v)
}

var arr []int64
v := 0.5

arr = Append(arr, v)

Isso também deve ser possível com restrições:

func convert(value s) t requires Convertible<s,t>{
    return t(value);
}

f:float64:=2.0

i:int64=convert(f)

Pelo que vale, nossa linguagem Genus suporta despacho múltiplo. Os modelos para uma restrição podem fornecer várias implementações para as quais são despachadas.

Eu entendo que a notação Convertible<s,t> é necessária para a segurança do tempo de compilação, mas talvez possa ser degradada para uma verificação de tempo de execução

func Append(t, t2 type, arr []t, value t2) []t {
    v, ok := t(value) // conversion
    if !ok {
        panic(...) // or return an err
    }
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

Mas isso se parece mais com açúcar de sintaxe para reflect .

@Inuart o ponto é que o compilador pode verificar se o tipo implementa a typeclass em tempo de compilação, portanto, a verificação em tempo de execução é desnecessária. O benefício é um melhor desempenho (chamado abstração de custo zero). Se for uma verificação de tempo de execução, você também pode usar reflect .

@creker

O despacho múltiplo é tudo o que é necessário?

Eu estou em mente muito sobre isso. Por um lado, o despacho múltiplo (com classes do tipo multiparâmetros) não funciona bem com existenciais, o que 'Go' chama de 'valores de interface'.

type Equals<T> interface {eq(right T) bool}
(left I) eq(right I) bool {return left == right}
(left I) eq(right F) bool {return false}
(left F) eq(right I) bool {return false}
(left F) eq(right F) bool {return left == right}

func main() {
    x := []Equals<?>{I{2}, F{4.0}, I{2}, F{4.0}}
}

Não podemos definir a fatia de Equals porque não temos como indicar que o parâmetro do lado direito é da mesma coleção. Não podemos nem fazer isso em Haskell:

data Equals = forall a . IEquals a a => Equals a

Isso não é bom porque só permite que um tipo seja comparado consigo mesmo

data Equals = forall a b . IEquals a b => Equals a

Isso não é bom porque não temos como restringir b a ser outro existencial na mesma coleção que a (se a estiver em uma coleção).

No entanto, torna muito fácil estender com um novo tipo:

(left K) eq(right I) bool {return false}
(left K) eq(right F) bool {return false}
(left I) eq(right K) bool {return false}
(left F) eq(right K) bool {return false}
(left K) eq(right K) bool {return left == right}

E isso seria ainda mais conciso com instâncias padrão ou especialização.

Por outro lado, podemos reescrever isso em 'Go' que funciona agora:

package main

type I struct {v int}
type F struct {v float32}

type EqualsInt interface {eqInt(left I) bool}
func (right I) eqInt (left I) bool {return left == right}
func (right F) eqInt (left I) bool {return false}

type EqualsFloat interface {eqFloat(left F) bool}
func (right I) eqFloat (left F) bool {return false}
func (right F) eqFloat (left F) bool {return left == right}

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

type EqualsLeft interface {eq(right EqualsRight) bool}
func (left I) eq (right EqualsRight) bool {return right.eqInt(left)}
func (left F) eq (right EqualsRight) bool {return right.eqFloat(left)}

type Equals interface {
    EqualsLeft
    EqualsRight
}

func main() {
    x := []Equals{I{2}, F{4.0}, I{2}, F{4.0}}
    println(x[0].eq(x[1]))
    println(x[1].eq(x[0]))
    println(x[0].eq(x[2]))
    println(x[1].eq(x[3]))
}

Isso funciona bem com o existencial (valor da interface), porém é muito mais complexo, mais difícil de ver o que está acontecendo e como funciona, e tem a grande restrição de que precisamos de uma interface por tipo e precisamos codificar o aceitável tipos do lado direito como este:

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

O que significa que teríamos que modificar a fonte da biblioteca para adicionar um novo tipo porque a interface EqualsRight não é extensível.

Portanto, sem interfaces multiparâmetros, não podemos definir operadores genéricos extensíveis como igualdade. Com interfaces multiparâmetros existenciais (valores de interface) tornam-se problemáticos.

Meu principal problema com muitas das sintaxes propostas (sintaces?) Blah[E] é que o tipo subjacente não mostra nenhuma informação sobre conter genéricos.

Por exemplo:

type Comparer[C] interface {
    Compare(other C) bool
}
// or
type Comparer c interface {
    Compare(other c) bool
}
...

Isso significa que estamos declarando um novo tipo que adiciona mais informações ao tipo subjacente. O objetivo da declaração type não é definir um nome baseado em outro tipo?

Eu proporia uma sintaxe mais ao longo da linha de

type Comparer interface[C] {
    Compare(other C) bool
}

Isso significa que realmente Comparer é apenas um tipo baseado em interface[C] { ... } , e interface[C] { ... } é, obviamente, seu próprio tipo separado de interface { ... } . Isso permite que você use uma interface genérica sem nomeá-la, se desejar (o que é permitido com interfaces normais). Eu acho que esta solução é um pouco mais intuitiva e funciona bem com o sistema de tipos do Go, embora por favor me corrija se eu estiver errado.

Nota: Declarar um tipo genérico só seria permitido em interfaces, structs e funcs com as seguintes sintaxes:
interface[G] { ... }
struct[G] { ... }
func[G] (vars...) { ... }

Então "implementando" os genéricos teria as seguintes sintaxes:
interface[G] { ... }[string]
struct[G] { ... }[string]
func[G] (vars...) { ... }[int](args...)

E com alguns exemplos para deixar mais claro:

Interfaces

package add

type Adder interface[E] {
    // Adds the element and returns the size
    Add(elem E) int
}

// Adds the integer 5 to any implementation of Adder[int].
func AddFiveTo(a Adder[int]) int {
    return a.Add(5)
}

Estruturas

package heap

type List struct[T] {
    slice []T
}

func (l *List) Add(elem T) { // T is a type defined by the receiver
    l.slice = append(l.slice, elem)
}

Funções

func[A] AddManyTo(a Adder[A], many ...A) {
    for _, each := range a {
        a.Add(each)
    }
}

Isso é uma resposta ao rascunho de contratos do Go2 e usarei sua sintaxe, mas estou postando aqui, pois se aplica a qualquer proposta de polimorfismo paramétrico.

A incorporação de parâmetros de tipo não deve ser permitida.

Considerar

type X(type T C) struct {
  R // A regular type with method Foo()
  T // Some type parameter
}
// X defines some methods other than Foo(),
// some of which invoke Foo.

para algum tipo arbitrário R e algum contrato arbitrário C que não contém Foo() .

T terá todos os seletores exigidos por C mas uma instanciação específica de T também pode ter outros seletores arbitrários, incluindo Foo .

Digamos que Bar seja uma struct, admissível em C , que tenha um campo chamado Foo .

X(Bar) pode ser uma instanciação ilegal. Sem uma forma de especificar no contrato que um tipo não possui um seletor, isso teria que ser uma propriedade inferida.

Métodos de X(Bar) podem continuar a resolver referências a Foo como X(Bar).R.Foo . Isso torna possível escrever o tipo genérico, mas pode ser confuso para um leitor não familiarizado com os detalhes das regras de resolução. Fora dos métodos de X , o seletor permaneceria ambíguo então, enquanto interface { Foo() } não depende dos parâmetros de X , algumas instanciações de X não satisfazê-lo.

Não permitir a incorporação de um parâmetro de tipo é mais simples.

(Se isso for permitido, no entanto, o nome do campo será T pela mesma razão que o nome do campo de um S incorporado definido como type S = io.Reader é S e não Reader mas também porque o tipo que instancia T não precisa necessariamente ter um nome.)

@jimmyfrasche Eu acho que os campos incorporados com tipos genéricos são úteis o suficiente para que seja bom permiti-los, mesmo que haja um pouco de constrangimento em alguns lugares. Minha sugestão seria assumir em todo código genérico que o tipo embutido definiu todos os campos e métodos possíveis em todos os níveis possíveis, de modo que dentro do código genérico todos os métodos embutidos e campos de tipos não genéricos sejam apagados.

Assim dado:

type R struct(type T) {
    io.Reader
    T
}

métodos em R não seriam capazes de invocar Read em R sem indiretamente através de Reader. Por exemplo:

func (r R) Do() {
     r.Read(buf)     // Illegal
     r.Reader.Read(buf)  // ok
}

A única desvantagem que posso ver disso é que o tipo dinâmico pode conter mais membros do que o tipo estático. Por exemplo:

func (r R) Do() {
    var x interface{} = r
    x.(io.Reader)    // Succeeds
}

@rogpeppe

A única desvantagem que posso ver disso é que o tipo dinâmico pode conter mais membros do que o tipo estático.

Este é o caso com parâmetros de tipo diretamente, então acho que também deve funcionar com tipos paramétricos. Acho que a solução para o problema que o @jimmyfrasche apresentou pode ser colocar o conjunto de métodos desejado do tipo parametrizado no contrato.

contract C(t T) {
  interface { Foo() } (X(T){})
  // ...
}

type X(type T C) struct {
  R // A regular type with method Foo()
  T // Some type parameter
}
// X defines some methods other than Foo(),
// some of which invoke Foo.

Isso permitiria que Foo fosse chamado em X diretamente. Claro, isso entraria em conflito com a regra "sem nomes locais nos contratos" ...

@stevenblenkinsop Hmm, é possível, se estranho, fazer isso sem se referir a X

contract C(t T) {
  struct{ R; T }{}.Foo
}

C ainda está vinculado à implementação de X embora um pouco mais vagamente.

Se você não fizer isso, e você escrever

func (x X(T)) Fooer() interface { Foo() } {
  return x
}

ele compila? Não estaria sob a regra do @rogpeppe que parece que precisaria ser adotado também para quando você não fizer a garantia no contrato. Mas isso se aplica apenas quando você incorpora um argumento de tipo sem um contrato suficiente ou para todas as incorporações?

Seria mais fácil simplesmente desautorizá-lo.

Comecei a trabalhar nessa proposta antes que o rascunho do Go2 fosse anunciado.

Eu estava pronto para descartar o meu feliz quando vi o anúncio, mas ainda estou inseguro com a complexidade do rascunho, então terminei o meu. É menos poderoso, mas mais simples. Se nada mais, pode ter alguns bits que valem a pena roubar.

Ele expande a sintaxe das propostas anteriores de @ianlancetaylor , pois era isso que estava disponível quando comecei. Isso não é fundamental. Pode ser substituído por uma sintaxe (type T etc. ou algo equivalente. Eu só precisava de alguma sintaxe como notação para a semântica.

Ele está localizado aqui: https://gist.github.com/jimmyfrasche/656f3f47f2496e6b49e041cd8ac716e4

A regra teria que ser que qualquer método promovido a partir de uma profundidade maior do que a de um parâmetro de tipo incorporado não pode ser chamado a menos que (1) a identidade do argumento de tipo seja conhecida ou (2) o método seja declarado como chamável na type pelo contrato que restringe o parâmetro type. O compilador também pode determinar os limites superior e inferior na profundidade que um método promovido deve ter dentro do tipo externo O e usá-los para determinar se o método pode ser chamado em um tipo que incorpora O , ou seja, se há potencial de conflito com outros métodos promovidos ou não. Algo semelhante também se aplicaria a qualquer parâmetro de tipo declarado como tendo métodos que podem ser chamados, onde os intervalos de profundidade dos métodos dentro do parâmetro de tipo seriam [0, inf).

A incorporação de parâmetros de tipo parece útil demais para proibi-lo completamente. Por um lado, permite composição transparente, que o padrão de interfaces embutidas não permite.

Também encontrei um uso potencial na definição de contratos. Se você quiser aceitar um valor do tipo T (que pode ser um tipo de ponteiro) que possa ter métodos definidos em *T , e você quiser colocar esse valor em uma interface, você não pode necessariamente colocar T na interface, já que os métodos podem estar em *T , e você não pode necessariamente colocar *T na interface porque T pode ser um tipo de ponteiro (e, portanto, *T pode ter um método vazio definido). No entanto, se você tivesse um wrapper como

type Wrapper(type T) { T }

você pode colocar *Wrapper(T) na interface em todos os casos se o seu contrato disser que satisfaz a interface.

Você não pode simplesmente fazer

type Interface interface {
  SomeMethod(int) error
}

contract MightBeAPointer(t T) {
  Interface(t)
}

func Example(type T MightBeAPointer)(v T) {
  var i Interface = v
  // ...
}

Estou tentando lidar com o caso em que alguém liga

type S struct{}
func (s *S) SomeMethod(int) error { ... }
...
var s S
Example(S)(s)

Isso não funcionará porque S não pode ser convertido em Interface , apenas *S pode.

Obviamente, a resposta pode ser "não faça isso". No entanto, a proposta de contratos descreve contratos como:

contract Contract(t T) {
    var _ error = t.SomeMethod(int(0))
}

S satisfaria este contrato devido ao endereçamento automático, assim como *S . O que estou tentando resolver é a lacuna de capacidade entre chamadas de método e conversões de interface em contratos.

De qualquer forma, isso é um pouco tangente, mostrando um uso potencial para incorporar parâmetros de tipo.

Re embedding, acho que “pode embutir em um struct” é outra restrição que os contratos teriam que capturar se permitidos.

Considerar:

contract Embeddable(type X, Y) {
    type S struct {
        X
        Y
    }
}

type Embedded(type First, Second Embeddable) struct {
        First
        Second
}

// Error: First and Second both provide method Read.
// That must be diagnosed to the Embeddable contract, not the definition of Embedded itself.
type Boom = Embedded(*bytes.Buffer, *strings.Reader)

A incorporação de tipos de @bcmills com seletores ambíguos é permitida, então não tenho certeza de como esse contrato deve ser interpretado.

De qualquer forma, se você estiver apenas incorporando tipos conhecidos, tudo bem. Se você está apenas incorporando parâmetros de tipo, tudo bem. O único caso que fica estranho é quando você incorpora um ou mais tipos conhecidos E um ou mais parâmetros de tipo e somente quando os seletores dos tipos conhecidos e os argumentos de tipo não são disjuntos

A incorporação de tipos de @bcmills com seletores ambíguos é permitida, então não tenho certeza de como esse contrato deve ser interpretado.

Hum, bom ponto. Falta mais uma restrição para acionar o erro.¹

contract Embeddable(type X, Y) {
    type S struct {
        X
        Y
    }
    var _ io.Reader = S{}
}

¹ https://play.golang.org/p/3wSg5aRjcQc

Isso requer um X ou Y mas não ambos para ser um io.Reader . É interessante que o sistema de contratos seja expressivo o suficiente para permitir isso. Estou feliz por não ter que descobrir as regras de inferência de tipo para tal fera.

Mas esse não é realmente o problema.

É quando você faz

type S (type T C) struct {
  io.Reader
  T
}
func (s *S(T)) X() io.Reader {
  return s
}

Isso deve falhar ao compilar porque T pode ter um seletor Read a menos que C tenha

struct{ io.Reader; T }.Read

Mas então quais são as regras quando C não garante que os conjuntos de seletores sejam disjuntos e S não faça referência aos seletores? É possível que cada instanciação S satisfaça uma interface, exceto para tipos que criam um seletor ambíguo?

É possível que cada instanciação S satisfaça uma interface, exceto para tipos que criam um seletor ambíguo?

Sim, parece ser esse o caso. Será que isso implica em algo mais profundo... 🤔

Não consegui construir nada irremediavelmente desagradável, mas a assimetria é bastante desagradável e me deixa desconfortável:

type I interface { /* ... */ }
a := G(A) // ok, A satisfies contract
var _ I = a // ok, no selector overlap
b := G(B) // ok, B satisfies contract
var _ = b // error, selector overlap

Estou preocupado com as mensagens de erro quando G0(B) usa um G1(B) usa um arquivo . . . usa um Gn(B) e Gn é o que causa o erro. . . .

FTR, você não precisa passar pelo problema de seletores ambíguos para acionar erros de tipo com incorporação.

// Error: Duplicate field name Reader
type Boom = Embedded(*bytes.Reader, *strings.Reader)

Você está assumindo que o nome do campo incorporado é baseado no tipo de argumento, enquanto é mais provável que seja o nome do parâmetro de tipo incorporado. Isso é como quando você incorpora um alias de tipo e o nome do campo é o alias em vez do nome do tipo que ele alias.

Na verdade, isso é especificado no projeto de projeto na seção sobre tipos parametrizados :

Quando um tipo parametrizado é uma estrutura e o parâmetro de tipo é incorporado como um campo na estrutura, o nome do campo é o nome do parâmetro de tipo, não o nome do argumento de tipo.

type Lockable(type T) struct {
    T
    mu sync.Mutex
}

func (l *Lockable(T)) Get() T {
    l.mu.Lock()
    defer l.mu.Unlock()
    return l.T
}

(Nota: isso funciona mal se você escrever Lockable(X) na declaração do método: o método deve retornar lT ou lX? Talvez devêssemos simplesmente banir a incorporação de um parâmetro de tipo em uma estrutura.)

Estou apenas sentado aqui à margem e observando. Mas também ficando um pouco preocupado.

Uma coisa que não tenho vergonha de dizer é que 90% dessa discussão está na minha cabeça.

Parece que 20 anos ganhando a vida escrevendo software sem saber o que é genérico, ou polimorfismo paramétrico, não me impediu de fazer o trabalho.

Infelizmente, só dediquei um ano atrás para aprender Go. Eu fiz a falsa suposição de que era uma curva de aprendizado íngreme e levaria muito tempo para se tornar produtivo.

Eu não poderia estar mais errado.

Consegui aprender Go o suficiente para construir um microsserviço que destruiu completamente o serviço node.js com o qual eu estava tendo problemas de desempenho em menos de um fim de semana.

Ironicamente, eu estava apenas brincando. Eu não estava particularmente falando sério sobre conquistar o mundo com Go.

E, no entanto, dentro de algumas horas, eu me encontrei sentando da minha postura desleixada e derrotada, como se estivesse na beirada do meu assento assistindo a um thriller de ação. A API que eu estava construindo surgiu tão rapidamente. Percebi que essa era realmente uma linguagem na qual valia a pena investir meu precioso tempo, porque era obviamente muito pragmática em seu design.

E é isso que eu amo em Go. É muito rápido..... Para aprender. Todos nós aqui sabemos de suas capacidades de desempenho. Mas a velocidade com que pode ser aprendida é incomparável com as outras 8 línguas que aprendi ao longo dos anos.

Desde então venho cantando louvores ao Go, e consegui que mais 4 Devs se apaixonassem por ele. Eu apenas sento com eles por algumas horas e construo algo. Os resultados falam por si.

Simplicidade e rapidez para aprender. Esses são os verdadeiros recursos matadores da linguagem.

Linguagens de programação que exigem meses de aprendizado árduo muitas vezes não retêm os próprios desenvolvedores que procuram atrair. Temos trabalho a fazer e empregadores que querem ver o progresso diário (obrigado ágil, agradeço)

Portanto, há duas coisas que espero que a equipe Go possa levar em consideração:

1) Que problema do dia a dia estamos procurando resolver?

Não consigo encontrar um exemplo do mundo real, com uma rolha de show que seria resolvida por genéricos, ou seja lá como eles vão ser chamados.

Exemplos de estilo de livro de receitas de tarefas do dia a dia que são problemáticas, com um exemplo de como elas podem ser melhoradas com essas propostas de mudança de linguagem.

2) Mantenha-o simples, como todos os outros ótimos recursos do Go

Há alguns comentários incrivelmente inteligentes aqui. Mas estou certo de que a maioria dos desenvolvedores que usam Go no dia a dia para programação geral, como eu, estão perfeitamente felizes e produtivos com as coisas do jeito que estão.

Talvez um argumento do compilador para habilitar esses recursos avançados? '--hardcore'

Eu ficaria muito triste se impactássemos negativamente o desempenho do compilador. Apenas diga n

E é isso que eu amo em Go. É muito rápido..... Para aprender. Todos nós aqui sabemos de suas capacidades de desempenho. Mas a velocidade com que pode ser aprendida é incomparável com as outras 8 línguas que aprendi ao longo dos anos.

Eu concordo completamente. A combinação de poder com simplicidade em uma linguagem totalmente compilada é algo completamente único. Eu definitivamente não quero que Go perca isso, e por mais que eu queira genéricos, não acho que valha a pena por esse preço. Eu não acho que é necessário perder isso, no entanto.

Não consigo encontrar um exemplo do mundo real, com uma rolha de show que seria resolvida por genéricos, ou seja lá como eles vão ser chamados.

Eu tenho dois principais casos de uso principais para genéricos: Eliminação padrão de segurança de tipo de estruturas de dados complexas, como árvores binárias, conjuntos e sync.Map , e a capacidade de escrever funções de segurança de tipo _compile-time_ que operam com base puramente na funcionalidade de seus argumentos, em vez de seu layout na memória. Existem algumas coisas mais extravagantes que eu não me importaria de poder fazer, mas eu não me importaria de _não_ poder fazê-las se for impossível adicionar suporte para elas sem quebrar completamente a simplicidade da linguagem.

Para ser honesto, já existem recursos no idioma que são bastante abusivos. A principal razão pela qual eles _não_ são abusados ​​com tanta frequência, eu acho, é a cultura Go de escrever código 'idiomático', combinada com a biblioteca padrão que fornece exemplos limpos e fáceis de encontrar de tal código, na maioria das vezes. Obter um bom uso de genéricos na biblioteca padrão definitivamente deve ser uma prioridade quando eles forem implementados.

@camstuart

Não consigo encontrar um exemplo do mundo real, com uma rolha de show que seria resolvida por genéricos, ou seja lá como eles vão ser chamados.

Os genéricos são para que você não precise escrever o código sozinho. Portanto, você nunca precisará implementar outra lista vinculada, árvore binária, deque ou fila de prioridade novamente. Você nunca precisará implementar um algoritmo de classificação, um algoritmo de particionamento ou um algoritmo de rotação etc. e girar). Se você puder reutilizar esses componentes, a taxa de erro diminuirá, porque toda vez que você reimplementar uma fila de prioridade ou um algoritmo de particionamento, há uma chance de você errar e introduzir um bug.

Genéricos significam que você escreve menos código e reutiliza mais. Eles significam que funções de biblioteca padrão e bem mantidas e tipos de dados abstratos podem ser usados ​​em mais situações, para que você não precise escrever seus próprios.

Melhor ainda, tudo isso pode ser feito tecnicamente em Go agora, mas apenas com uma perda quase completa de segurança de tipo em tempo de compilação _e_ com alguma sobrecarga de tempo de execução potencialmente grande. Os genéricos permitem que você faça isso sem nenhuma dessas desvantagens.

Implementação de função genérica:

/*

* "generic" is a KIND of types, just like "struct", "map", "interface", etc...
* "T" is a generic type (a type of kind generic).
* var t = T{int} is a value of type T, values of generic types looks like a "normal" type

*/

type T generic {
    int
    float64
    string
}

func Sum(a, b T{}) T{} {
    return a + b
}

Chamador de função:

Sum(1, 1) // 2
// same as:
Sum(T{int}(1), T{int}(1)) // 2

Implementação de estrutura genérica:

type ItemT generic {
    interface{}
}

type List struct {
    l []ItemT{}
}

func NewList(t ItemT) *List {
    l := make([]t)
    return &List{l}
}

func (p *List) Push(item ItemT{}) {
    p.l = append(p.l, item)
}

Chamador:

list := NewList(ItemT{int})
list.Push(42)

Como alguém apenas aprendendo Swift e não gostando, mas com bastante experiência em outras linguagens como Go, C, Java, etc; Eu realmente acredito que genéricos (ou templates, ou o que você quiser chamar) não é uma coisa boa para adicionar à linguagem Go.

Talvez eu seja apenas mais experiente com a versão atual do Go, mas para mim isso parece uma regressão ao C++, pois é mais difícil entender o código que outras pessoas escreveram. O espaço reservado clássico T para tipos torna muito difícil entender o que uma função está tentando fazer.

Eu sei que este é um pedido de recurso popular para que eu possa lidar com ele se ele chegar, mas eu queria adicionar meus 2 centavos (opinião).

@jlubawy
Você conhece outra maneira de nunca precisar implementar uma lista vinculada ou algoritmo de classificação rápida? Como Alexander Stepanov aponta, a maioria dos programadores não pode definir corretamente as funções "min" e "max", então que esperança temos de implementar corretamente algoritmos mais complexos sem muito tempo de depuração. Prefiro extrair versões padrão desses algoritmos de uma biblioteca e aplicar apenas aos tipos que tenho. Que alternativa existe?

@jlubawy

ou modelo, ou como você quiser chamá-lo

Tudo depende da implementação. se estamos falando de modelos C++, sim, eles são difíceis de entender em geral. Até mesmo escrevê-los é difícil. Por outro lado, se pegarmos os genéricos do C#, isso é outra coisa completamente diferente. O conceito em si não é um problema aqui.

Se você não sabia, a Go Team anunciou um rascunho do Go 2.0:
https://golang.org/s/go2designs

Há um rascunho para o design Genéricos no Go 2.0 (contrato). Você pode querer dar uma olhada e dar feedback sobre o Wiki deles.

Esta é a seção relevante:

Genéricos

Depois de ler o rascunho, pergunto:

Por que

T:Adicionável

significa "um tipo T implementando o contrato Addable"? Por que adicionar um novo
conceito quando já temos INTERFACES para isso? A atribuição de interfaces é
verificado em tempo de compilação, então já temos os meios para não precisar de nenhum
conceito adicional aqui. Podemos usar este termo para dizer algo como: Qualquer
tipo T implementando a interface Addable. Além disso, T:_ ou T:Qualquer
(sendo Any uma palavra-chave especial ou um alias integrado de interface{}) faria
o truque.

Só não sei por que reimplementar a maioria das coisas assim. Não faz
sentido e será redundante (assim como redundante é o novo tratamento de erros wrt
o tratamento de pânicos).

14/09/2018 6:15 GMT-05:00 Koala Yeung [email protected] :

Se você não sabia, a Go Team anunciou um rascunho do Go 2.0:
https://golang.org/s/go2designs

Há um rascunho para o design Genéricos no Go 2.0 (contrato). Você pode querer
para dar uma olhada e dar feedback
https://github.com/golang/go/wiki/Go2GenericsFeedback em seu Wiki
https://github.com/golang/go/wiki/Go2GenericsFeedback .

Esta é a seção relevante:

Genéricos


Você está recebendo isso porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/15292#issuecomment-421326634 ou silenciar
o segmento
https://github.com/notifications/unsubscribe-auth/AlhWhS8xmN5Y85_aUKT5VnutoOKUAaLLks5ua4_agaJpZM4IG-xv
.

--
Este é um teste para assinaturas de correio a serem usadas no TripleMint

Edit: "[...] faria o truque SE VOCÊ NÃO PRECISA DE NENHUM REQUISITO ESPECÍFICO
O ARGUMENTO TIPO".

17/09/2018 11:10 GMT-05:00 Luis Masuelli [email protected] :

Depois de ler o rascunho, pergunto:

Por que

T:Adicionável

significa "um tipo T implementando o contrato Addable"? Por que adicionar um novo
conceito quando já temos INTERFACES para isso? A atribuição de interfaces é
verificado em tempo de compilação, então já temos os meios para não precisar de nenhum
conceito adicional aqui. Podemos usar este termo para dizer algo como: Qualquer
tipo T implementando a interface Addable. Além disso, T:_ ou T:Qualquer
(sendo Any uma palavra-chave especial ou um alias integrado de interface{}) faria
o truque.

Só não sei por que reimplementar a maioria das coisas assim. Não faz
sentido e será redundante (assim como redundante é o novo tratamento de erros wrt
o tratamento de pânicos).

14/09/2018 6:15 GMT-05:00 Koala Yeung [email protected] :

Se você não sabia, a Go Team anunciou um rascunho do Go 2.0:
https://golang.org/s/go2designs

Há um rascunho para o design Genéricos no Go 2.0 (contrato). Você pode
quero dar uma olhada e dar feedback
https://github.com/golang/go/wiki/Go2GenericsFeedback em seu Wiki
https://github.com/golang/go/wiki/Go2GenericsFeedback .

Esta é a seção relevante:

Genéricos


Você está recebendo isso porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/15292#issuecomment-421326634 ou silenciar
o segmento
https://github.com/notifications/unsubscribe-auth/AlhWhS8xmN5Y85_aUKT5VnutoOKUAaLLks5ua4_agaJpZM4IG-xv
.

--
Este é um teste para assinaturas de correio a serem usadas no TripleMint

--
Este é um teste para assinaturas de correio a serem usadas no TripleMint

@luismasuelli-jobsity Se eu li o histórico de implementações genéricas em Go corretamente, parece que o motivo de introduzir Contratos é porque eles não queriam sobrecarga de operadores em Interfaces.

Uma proposta anterior que acabou sendo rejeitada usava interfaces para restringir o polimorfismo paramétrico, mas parece ter sido rejeitada porque você não poderia usar operadores comuns como '+' em tais funções porque não é definível em uma interface. Os contratos permitem que você escreva t == t ou t + t para que você possa indicar que o tipo deve suportar igualdade ou adição etc.

Edit: O Go também não suporta várias interfaces de parâmetro de tipo, então, de certa forma, o Go separou typeclass em duas coisas separadas, contratos que relacionam os parâmetros de tipo de funções entre si e interfaces que fornecem métodos. O que perde é a capacidade de selecionar uma implementação typeclass com base em vários tipos. É sem dúvida mais simples se você precisar usar apenas interfaces ou contratos, mas mais complexo se precisar usar os dois juntos.

Por que T:Addable significa "um tipo T implementando o contrato Addable"?

Na verdade, não é isso que significa; apenas parece assim para um argumento de tipo. Em outra parte do rascunho, ele faz o comentário de que você só pode ter um contrato por função, e é aí que entra a principal diferença. Os contratos são, na verdade, declarações sobre os tipos da função, não apenas os tipos independentemente. Por exemplo, se você tiver

func Example(type K, V someContract)(k K, v V) V

você pode fazer algo como

contract someContract(k K, v V) {
  k.someMethod(v)
}

Isso simplifica muito a coordenação de vários tipos sem precisar especificar os tipos de forma redundante na assinatura da função. Lembre-se, eles estão tentando evitar o 'padrão genérico de repetição curiosa'. Por exemplo, a mesma função com interfaces parametrizadas usadas para restringir os tipos seria algo como

type someMethoder(V) interface {
  someMethod(V)
}

func Example(type K: someMethoder(V), V)(k K, v V) V

Isso é meio estranho. A sintaxe do contrato ainda permite que você faça isso se precisar, porque os 'argumentos' do contrato são preenchidos automaticamente pelo compilador se o contrato tiver o mesmo número deles que a função faz parâmetros de tipo. Você pode especificá-los manualmente se quiser, o que significa que você _poderia_ fazer func Example(type K, V someContract(K, V))(k K, v V) V se realmente quisesse, embora não seja particularmente útil nesta situação.

Uma maneira de deixar mais claro que os contratos são sobre funções inteiras, não argumentos individuais, seria simplesmente associá-los com base no nome. Por exemplo,

contract Example(k K, v V) {
  k.someMethod(v)
}

func Example(type K, V)(k K, v V) V

seria o mesmo que o anterior. A desvantagem, no entanto, é que os contratos não seriam reutilizáveis ​​e você perde a capacidade de especificar os argumentos do contrato manualmente.

Edit: Para mostrar ainda mais por que eles querem resolver o padrão curiosamente repetido, considere o problema do caminho mais curto ao qual eles se referiam. Com interfaces parametrizadas, a definição acaba parecendo

type E(Node) interface {
  Nodes() []Node
}

type N(Edge) interface {
  Edges() (from, to Edge)
}

type Graph(type Node: N(Edge), Edge: E(Node)) struct { ... }
func New(type Node: N(Edge), Edge: E(Node))(nodes []Node) *Graph(Node, Edge) { ... }
func (*Graph(Node, Edge)) ShortestPath(from, to Node) []Edge { ... }

Pessoalmente, gosto bastante da maneira como os contratos são especificados para funções. Não estou muito interessado em ter apenas corpos de função 'normais' como a especificação real do contrato, mas acho que muitos dos problemas potenciais podem ser resolvidos com a introdução de algum tipo de simplificador do tipo gofmt que simplifica automaticamente os contratos para você, removendo partes estranhas. Então você _poderia_ apenas copiar um corpo de função para ele, simplificá-lo e modificá-lo a partir daí. Eu não tenho certeza de como isso será possível para implementar, no entanto, infelizmente.

Algumas coisas ainda serão um pouco difíceis de especificar, e a aparente sobreposição entre contratos e interfaces ainda parece um pouco estranha.

Acho a versão "CRTP" muito mais clara, mais explícita e mais fácil de trabalhar (não há necessidade de criar contratos que só existem para definir a relação entre contratos pré-existentes sobre um conjunto de variáveis). É certo que isso pode ser apenas os muitos anos de familiaridade com a ideia.

Esclarecimentos. Pela minuta do projeto , o contrato pode ser aplicado tanto a funções quanto a tipos .

"""
É sem dúvida mais simples se você precisar usar apenas interfaces ou contratos, mas mais complexo se precisar usar os dois juntos.
"""

Enquanto eles permitirem que você, dentro de um contrato, faça referência a uma ou mais interfaces (em vez de apenas operadores e funções, permitindo DRY), esse problema (e minha reclamação) será resolvido. Há uma chance de eu ter lido errado ou não ter lido completamente as coisas dos contratos, e também uma chance de que o referido recurso seja suportado e eu não tenha notado. Se não for, deveria ser.

Você não pode fazer o seguinte?

contract Example(t T, v V) {
  t.(interface{
    SomeMethod() V
  })
}

Você não pode usar uma interface declarada em outro lugar devido à restrição de que você não pode fazer referência a identificadores do mesmo pacote em que o contrato é declarado, mas você pode fazer isso. Ou eles poderiam simplesmente remover essa restrição; parece um pouco arbitrário.

@DeedleFake Não, porque qualquer tipo de interface pode ser declarado por tipo (e, em seguida, entrar em pânico em tempo de execução, mas os contratos não são executados). Mas você pode usar uma atribuição em vez disso.

t.(someInterface) também significa que deve ser uma interface

Bom ponto. Ops.

Quanto mais exemplos disso eu vejo, mais propenso a erros 'descobrir a partir de um corpo de função' parece ser.

Há muitos casos em que é confuso para uma pessoa, mesma sintaxe para diferentes operações, nuances de implicações de diferentes construções, etc., mas uma ferramenta seria capaz de pegar isso e reduzi-lo a uma forma normal. Mas então a saída de tal ferramenta torna-se uma sublinguagem de fato para expressar restrições de tipo que temos que aprender de cor, tornando ainda mais surpreendente quando alguém se desvia e escreve um contrato à mão.

Eu também vou notar que

contract I(t T) {
  var i interface { Foo() }
  i = t
  t.(interface{})
}

expressa que T deve ser uma interface com pelo menos Foo() mas também pode ter qualquer outro número de métodos adicionais.

T deve ser uma interface com pelo menos Foo() , mas também pode ter qualquer outro número de métodos adicionais

Mas isso é um problema? Você geralmente não quer restringir as coisas para que elas permitam funcionalidades específicas, mas você não se importa com outras funcionalidades? Caso contrário, um contrato como

contract Example(t T) {
  t + t
}

não permitiria a subtração, por exemplo. Mas do ponto de vista do que estou implementando, não me importo se um tipo permite subtração ou não. Se eu o restringisse de ser capaz de realizar subtração, então as pessoas arbitrariamente não seriam capazes de, por exemplo, passar qualquer coisa que faça para uma função Sum() ou algo assim. Isso parece arbitrariamente restritivo.

Não, não é problema algum. Era apenas uma propriedade pouco intuitiva (para mim), mas talvez isso se devesse ao café insuficiente.

É justo dizer que a declaração de contrato atual precisa ter melhores mensagens do compilador para trabalhar. E as regras para um contrato válido devem ser rígidas.

Oi
Fiz uma proposta de restrições para genéricos que postei neste tópico cerca de ½ ano atrás.
Agora eu fiz uma versão 2 . As principais mudanças são:

  • A sintaxe foi adaptada à proposta pela go-team.
  • A restrição por campos foi omitida, o que permite algumas simplificações.
  • Os parágrafos considerados não estritamente necessários foram retirados.

Eu pensei em uma pergunta interessante (mas talvez mais detalhada do que apropriada neste estágio do design?) sobre identidade de tipo recentemente:

func Foo() interface{} {
    type S struct {}
    return S{}
}

func Bar(type T)() interface{} {
    type S struct {}
    return S{}
}

func Baz(type T)() interface{} {
    type S struct{t T}
    return S{}
}

func main() {
    fmt.Println(Foo() == Foo()) // 1
    fmt.Println(Bar(int)() == Bar(string)()) // 2
    fmt.Println(Baz(int)() == Baz(string)()) // 3
}
  1. Imprime true , porque os tipos dos valores retornados são originários da mesma declaração de tipo.
  2. Impressões…?
  3. Imprime false , presumo.

ou seja, a questão é, quando dois tipos declarados em uma função genérica são idênticos e quando não são. Eu não acho que isso esteja descrito no design ~spec~? Pelo menos não consigo encontrar agora :)

@merovius , suponho que o caso do meio deveria ser:

fmt.Println(Bar(int)() == Bar(int)()) // 2

Este é um caso interessante, e depende se os tipos são "generativos" ou "aplicativos". Na verdade, existem variantes de ML que adotam abordagens diferentes. Tipos de aplicativo visualizam o genérico como uma função de tipo e, portanto, f(int) == f(int). Os tipos generativos visualizam o genérico como um modelo de tipo que cria um novo tipo de 'instância' exclusivo toda vez que é usado para t<int> != t<int>. Isso deve ser abordado em todo o nível do sistema de tipos, pois tem implicações sutis para unificação, inferência e solidez. Para mais detalhes e exemplos de problemas, recomendo a leitura do artigo "F-ing modules" de Andreas Rossberg: https://people.mpi-sws.org/~rossberg/f-ing/ embora o artigo esteja falando sobre ML " functors" isso ocorre porque o ML separa seu sistema de tipos em dois níveis, e os functors são MLs equivalentes a um genérico e estão disponíveis apenas no nível do módulo.

@keean Você supõe errado.

@merovius Sim, meu erro, vejo que a pergunta é porque o parâmetro de tipo não é usado (um tipo fantasma).

Com tipos generativos, cada instanciação resultaria em um tipo exclusivo diferente para 'S', portanto, mesmo que o parâmetro não seja usado, eles não seriam iguais.

Com tipos de aplicativos, o 'S' de cada instanciação seria do mesmo tipo e, portanto, seriam iguais.

Seria estranho se o resultado no caso 2 mudasse com base nas otimizações do compilador. Parece U.B.

É 2018 pessoas, não acredito que realmente tenho que digitar isso como em 1982:

func min(x, y int) int {
se x < y {
retornar x
}
retornar y
}

func max(x, y int) int {
se x > y {
retornar x
}
retornar y
}

Quero dizer, sério, caras MIN(INT,INT) INT, como isso NÃO está no idioma?
Estou com fome.

@dataf3l Se você quiser que eles funcionem conforme o esperado com pré-encomendas, então:

func min(x, y int) int {
   if x <= y {
      return x
   }
   return y
}

É assim que o par (min(x, y), max(x, y)) é sempre distinto e é (x, y) ou (y, x), e que é, portanto, um tipo estável de dois elementos.

Então, outra razão pela qual eles devem estar no idioma ou em uma biblioteca é que as pessoas geralmente os erram :-)

Eu pensei sobre o < vs <=, para números inteiros, não tenho certeza se vejo a diferença.
Talvez eu seja apenas burra...

Não tenho certeza se vejo a diferença.

Não há nenhum neste caso.

@cznic true neste caso, pois são inteiros, no entanto, como o tópico era sobre genéricos, presumi que o comentário da biblioteca era sobre ter definições genéricas de min e max para que os usuários não precisassem declará-los. Relendo o OP, posso ver que eles querem apenas min e max simples para números inteiros, então foi mal, mas eles estavam fora do tópico pedindo funções de integração simples em um tópico sobre genéricos :-)

Os genéricos são uma adição crucial a essa linguagem, especialmente devido à falta de estruturas de dados incorporadas. Até agora, minha experiência com Go é que é uma linguagem ótima e fácil de aprender. Porém, tem uma grande desvantagem, que é que você precisa codificar as mesmas coisas repetidamente.

Talvez eu esteja perdendo alguma coisa, mas isso parece uma falha bastante grande na linguagem. Resumindo, existem poucas estruturas de dados embutidas e toda vez que criamos uma estrutura de dados, temos que copiar e colar o código para suportar cada T .

Não tenho certeza de como contribuir além de postar minha observação aqui como 'usuário'. Eu não sou um programador experiente o suficiente para contribuir com o design ou implementação, então só posso dizer que os genéricos aumentariam muito a produtividade na linguagem (desde que o tempo de construção e as ferramentas permaneçam incríveis como são agora).

@webern Obrigado. Consulte https://go.googlesource.com/proposal/+/master/design/go2draft.md .

@ianlancetaylor , depois de postar, uma ideia bastante radical/única surgiu na minha cabeça que eu acho que seria 'leve' no que diz respeito à linguagem e ferramentas. Eu não li seu link completamente ainda, eu vou. Mas se eu quisesse enviar uma ideia/proposta de programação genérica em formato MD, como eu faria isso?

Obrigado.

@webern Escreva (a maioria das pessoas tem usado gists para o formato markdown) e atualize o wiki aqui https://github.com/golang/go/wiki/Go2GenericsFeedback

Muitos outros já o fizeram.

Mesclei (contra a última dica) e carreguei o CL de nossa implementação de protótipo pré-Gophercon de um analisador (e impressora) implementando o projeto de rascunho de contratos. Se você estiver interessado em experimentar a sintaxe, dê uma olhada: https://golang.org/cl/149638 .

Para brincar com ele:

1) Escolha o CL em um repositório recente:
git fetch https://go.googlesource.com/go refs/changes/38/149638/2 && git cherry-pick FETCH_HEAD

2) Reconstrua e instale o compilador:
vá instalar cmd/compilar

3) Use o compilador:
go ferramenta compilar foo.go

Consulte a descrição do CL para obter detalhes. Aproveitar!

contract Addable(t T) {
    t + t
}

func Sum(type T Addable)(x []T) T {
    var total T
    for _, v := range x {
        total += v
    }
    return total
}

Este design genérico, func Sum(type T Addable)(x []T) T , é MUITO, MUITO, MUITO FEIO!!!

Para ser comparado com func Sum(type T Addable)(x []T) T , acho que func Sum<T: Addable> (x []T) T é mais claro, e não tem nenhum ônus para o programador vindo de outras linguagens de programação.

Você quer dizer que a sintaxe é mais detalhada?
Deve haver alguma razão pela qual não é func Sum(T Addable)(x []T) T .

sem a palavra-chave type não haverá como diferenciar entre uma função genérica e uma que retorna outra função, que está sendo chamada.

@urandom Isso é apenas um problema no momento da instanciação e não exigimos a palavra-chave type , mas apenas convivemos com a ambiguidade AIUI.

O problema é que, sem a palavra-chave type , func Foo(x T) (y T) poderia ser analisado como declarando uma função genérica recebendo um T e retornando nada ou uma função não genérica recebendo um T e devolvendo um T .

soma funcional(x [] T) T

Concordo, prefiro algo nesse sentido. Dada a expansão do escopo linguístico representado pelos genéricos, acho que seria razoável introduzir essa sintaxe para "chamar a atenção" para uma função genérica.

Eu também acho que isso tornaria o código um pouco mais fácil (leia-se: menos Lisp-y) de analisar para leitores humanos, bem como reduzir as chances de encontrar alguma ambiguidade de análise obscura mais adiante (veja "Most Vexing Parse" do C++, para ajudar a motivar uma abundância de cautela).

É 2018 pessoas, não acredito que realmente tenho que digitar isso como em 1982:

func min(x, y int) int {
se x < y {
retornar x
}
retornar y
}

func max(x, y int) int {
se x > y {
retornar x
}
retornar y
}

Quero dizer, sério, caras MIN(INT,INT) INT, como isso NÃO está no idioma?
Estou com fome.

Há uma razão para isso.
Se você não entender, você pode aprender ou ir embora.
Sua escolha.

Espero sinceramente que melhorem.
Mas sua atitude de "você pode aprender ou ir embora" não está fornecendo um bom exemplo para os outros seguirem. lê desnecessariamente abrasivo. Eu não acho que essa comunidade seja sobre @petar-dambovaliev. no entanto, não cabe a mim dizer o que fazer, ou como se comportar online, esse não é o meu lugar.

Eu sei que há muitos sentimentos fortes sobre os genéricos, mas lembre-se de nossos valores Gopher . Por favor, mantenha a conversa respeitosa e acolhedora de todos os lados.

@bcmills obrigado, você faz da comunidade um lugar melhor.

@katzdm concordou, a linguagem já tem tantos parênteses, esse novo material parece realmente ambíguo para mim

Definir generics parece inevitável introduzindo coisas como type's type , o que torna Go bastante complicado.

Espero que isso não seja muito off-topic, mas um recurso de function overload parece suficiente para mim.

BTW, eu sei que houve alguma discussão sobre sobrecarga .

@xgfone Concordo, que a linguagem já tem tantos parênteses, tornando o código pouco claro.
func Sum<T: Addable> (x []T) T ou func Sum<type T Addable> (x []T) T é melhor e mais claro.

Para consistência (com genéricos integrados), func Sum[T: Addable] (x []T) T é melhor que func Sum<T: Addable> (x []T) T .

Posso ser influenciado por trabalhos anteriores em outros idiomas, mas Sum<T: Addable> (x []T) T parece mais distinto e legível à primeira vista.

Também concordo com @katzdm que é melhor chamar a atenção para algo novo no idioma. Também é bastante familiar para desenvolvedores não-Go entrando em Go.

FWIW, há aproximadamente 0% de chance de Go usar colchetes angulares para genéricos. A gramática de C++ não pode ser analisada porque você não pode diferenciar um < b > c (uma série de comparações legal, mas sem sentido) de uma invocação genérica sem entender os tipos de a, b e c. Outras linguagens evitam usar colchetes angulares para genéricos por esse motivo.

func a < b Addable> (...
Eu acho que você pode se você perceber que depois func você só pode ter o nome da função, um ( ou um < .

@carlmjohnson espero que esteja certo

f := sum<int>(10)

Mas aqui você sabe que sum é um contrato..

A gramática de C++ não pode ser analisada porque você não pode diferenciar um < b > c (uma série de comparações legal, mas sem sentido) de uma invocação genérica sem entender os tipos de a, b e c.

Acho que vale ressaltar que enquanto Go, diferentemente de C++, não permite isso no sistema de tipos, já que os operadores < e > retornam bool s em Go e < e > não podem ser usados ​​com bool s, _é_ sintaticamente legal, então isso ainda é um problema.

Outro problema com colchetes angulares é List<List<int>> , em que >> é tokenizado como um operador de deslocamento à direita.

Quais foram os problemas com o uso de [] ? Parece-me que a maioria dos itens acima são resolvidos usando-os:

  • Sintaticamente, f := sum[int](10) , para usar o exemplo acima, não é ambíguo porque tem a mesma sintaxe que um acesso de matriz ou mapa, e então o sistema de tipos pode descobrir isso mais tarde, o mesmo que já tem que fazer para a diferença entre acessos array e map, por exemplo. Isso é diferente do caso de <> porque um único < é legal, levando a ambiguidade, mas um único [ não é.
  • func Example[T](v T) T também é inequívoco.
  • ]] não é seu próprio token, então esse problema também é evitado.

O rascunho do design menciona uma ambiguidade nas declarações de tipo , como em type A [T] int , mas acho que isso pode ser resolvido com relativa facilidade de duas maneiras diferentes. Por exemplo, a definição genérica pode ser movida para a própria palavra-chave, em vez do nome do tipo, ou seja:

  • func[T] Example(v T) T
  • type[T] A int

A complicação aqui pode vir do uso de blocos de declaração de tipo, como

type (
  A int
)

mas acho que isso é raro o suficiente para dizer basicamente que, se você precisar de genéricos, não poderá usar um desses blocos.

Acho que seria muito lamentável escrever

type[T] A []T
var s A[int]

porque os colchetes se movem de um lado de A para o outro. Claro que poderia ser feito, mas devemos buscar melhor.

Dito isso, o uso da palavra-chave type na sintaxe atual significa que podemos substituir parênteses por colchetes.

Isso não parece tão diferente do tipo de array vs. sintaxe de expressão ser [N]T vs. arr[i] , em termos de como algo é declarado não correspondendo ao modo como é usado. Sim, em var arr [N]T , os colchetes terminam no mesmo lado de arr como ao usar arr , mas normalmente pensamos na sintaxe em termos de sintaxe de tipo vs expressão sendo oposto.

Eu estendi e melhorei algumas das minhas velhas ideias imaturas para tentar unificar genéricos personalizados e embutidos.

Não tenho certeza se discutir ( vs < vs [ e o uso de type é perda de bicicleta ou realmente há um problema com a sintaxe

@ianlancetaylor ... se perguntou se o feedback justificava algum ajuste no design proposto? Minha própria percepção do feedback foi que muitos sentiram que interfaces e contratos poderiam ser combinados, pelo menos inicialmente. Parecia haver uma mudança depois de um tempo que os dois conceitos deveriam ser mantidos separados. Mas eu poderia estar lendo as tendências erradas. Adoraria ver uma opção experimental em um lançamento este ano!

Sim, estamos considerando mudanças no projeto de rascunho, inclusive analisando as muitas contrapropostas que as pessoas fizeram. Nada está finalizado.

Apenas para adicionar algum relato de experiência prática:
Eu implementei genéricos como uma extensão de linguagem no meu interpretador Go https://github.com/cosmos72/gomacro. Curiosamente, ambas as sintaxes

type[T] Pair struct { First T; Second T }
type Pair[T] struct { First T; Second T }

acabou por introduzir muitas ambiguidades no analisador: o segundo poderia ser analisado como uma declaração de que Pair é um array de T structs, onde T é algum inteiro constante. Quando Pair é usado, também há ambiguidades: Pair[int] também pode ser analisado como uma expressão em vez de um tipo: pode estar indexando um array/fatia/mapa chamado Pair com a expressão de índice int (nota: int e outros tipos básicos NÃO são palavras-chave reservadas em Go), então tive que recorrer a uma nova sintaxe - reconhecidamente feia, mas faz o trabalho:

template[T] type Pair struct { First T; Second T }
type pairOfInt = Pair#[int]
var p Pair#[int]

e da mesma forma para funções:

template[T] func Sum(args ...T) T { /*...*/ }
Sum#[int] (1,2,3)

Então, embora em teoria eu concorde que a sintaxe é uma questão superficial, devo salientar que:
1) de um lado, a sintaxe é o que os programadores Go serão expostos - por isso deve ser expressivo, simples e possivelmente palatável
2) por outro lado, uma má escolha de sintaxe complicará o analisador, o verificador de tipos e o compilador para resolver as ambiguidades introduzidas

Pair[int] também pode ser analisado como uma expressão em vez de um tipo: pode estar indexando um array/fatia/mapa chamado Pair com a expressão de índice int

Esta não é uma ambiguidade de análise, apenas uma semântica (até após a resolução do nome); a estrutura sintática é a mesma de qualquer maneira. Observe que Sum#[int] também pode ser um tipo ou uma expressão dependendo do que Sum é. O mesmo vale para (*T) no código existente. Contanto que a resolução de nomes não afete a estrutura do que está sendo analisado, tudo bem.

Compare isso com os problemas com <> :

f ( a < b , c < d >> (e) )

Você não pode nem tokenizar isso, já que >> pode ser um ou dois tokens. Então, você não pode dizer se há um ou dois argumentos para f ... a estrutura da expressão muda significativamente dependendo do que é denotado por a .

De qualquer forma, estou interessado em ver qual é o pensamento atual da equipe sobre genéricos, em particular, se "restrições são apenas código" foi iterado ou abandonado. Eu posso entender querer evitar a definição de uma linguagem de restrição distinta, mas acontece que escrever código que restringe suficientemente os tipos envolvidos força um estilo não natural, e você também tem que colocar limites no que o compilador pode realmente inferir sobre os tipos com base no código porque, caso contrário, essas inferências podem se tornar arbitrariamente complexas ou podem se basear em fatos sobre a linguagem que podem mudar no futuro.

@cosmos72

Talvez eu esteja errado, mas além do que foi dito por @stevenblenkinsop , é possível que um termo:

a b

pode também implicar que b não é um tipo se b for um alfanumérico (sem operador/sem separador) com [identifier] opcional anexado e a não for uma palavra-chave especial/alfanumérico especial (por exemplo, sem importação/ pacote/tipo/função)?.

Não sei muito a gramática de ir.

De alguma forma, tipos como int e Sum[int] seriam tratados de qualquer maneira como expressões:

type (
    nodeList = []*Node  // nodeList and []*Node are identical types
    Polar    = polar    // Polar and polar denote identical types
)

Se go permitisse funções infixas, então a type tag seria ambíguo, pois type poderia ser uma função infixa ou um tipo.

Percebi hoje que a visão geral do problema desta proposta reivindica o Swift:

Declarar que T satisfaz o protocolo Equatable torna válido o uso de == no corpo da função. Equatable parece ser um built-in no Swift, não é possível definir de outra forma.

Isso parece ser mais um aparte do que algo que está afetando profundamente as decisões tomadas sobre este tópico, mas na chance de dar às pessoas muito mais espertas do que eu alguma inspiração, eu queria salientar que não há realmente nada de especial sobre Equatable além de ser pré-definido na linguagem (principalmente para que muitos outros tipos internos possam "se conformar" a isso). É perfeitamente possível criar protocolos semelhantes:

protocol Equatable2 {
    static func == (lhs: Self, rhs: Self) -> Bool
}

class uniq: Equatable2 {
    static func == (lhs: uniq, rhs: uniq) -> Bool {
        return false
    }
}

let narf = uniq(), poit = uniq()

func !=<T: Equatable2> (lhs: T, rhs: T) -> Bool {
    return !(lhs == rhs)
}

print(narf != poit)

@sighoya
Eu estava falando sobre ambiguidades da sintaxe a[b] proposta para genéricos, já que ela já é usada para indexar fatias e mapas - não sobre a b .

Nesse meio tempo, tenho estudado Haskell e, embora soubesse de antemão que utilizava extensivamente a inferência de tipos, a expressividade e sofisticação de seus genéricos me surpreenderam.

Infelizmente, ele tem um esquema de nomenclatura bastante peculiar, por isso nem sempre é fácil de entender à primeira vista. Por exemplo, um class é na verdade uma restrição para tipos (genéricos ou não). A classe Eq é a restrição para tipos cujos valores podem ser comparados com '==' e '/=':

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool

significa que um tipo a satisfaz a restrição Eq se existir uma "especialização" (na verdade uma "instância" no jargão Haskell) das funções infixas == e /= que aceita dois argumentos, cada um com o tipo a e retorna um resultado Bool .

Atualmente, estou tentando adaptar algumas das ideias encontradas nos genéricos de Haskell para uma proposta de genéricos de Go e ver como elas se encaixam. Estou muito feliz em ver que a investigação está acontecendo com outras linguagens além de C++ e Java:

o exemplo Swift acima, e meu exemplo Haskell, mostram que restrições em tipos genéricos já são usadas na prática por várias linguagens de programação, e que uma quantidade não trivial de experiência em várias abordagens de genéricos e restrições existe e está disponível entre os programadores dessas (e outras) linguagens.

Na minha opinião, certamente vale a pena estudar essa experiência antes de finalizar uma proposta de genéricos Go.

Stray pensou: se a forma de restrição que você deseja que o tipo genérico satisfaça for mais ou menos congruente com uma definição de interface, você pode usar a sintaxe de afirmação de tipo existente à qual já estamos acostumados:

type Comparer interface {
  Compare(v interface{}) (*int, error)
}
type PriorityQueue<T.(Comparer)> struct {
  things []T
}

Desculpas se isso já foi discutido exaustivamente em outro lugar; Eu não vi, mas ainda estou me prendendo na literatura. Eu tenho ignorado isso por um tempo porque, bem, eu não quero genéricos em nenhuma versão do Go. Mas a ideia parece estar ganhando força e uma sensação de inevitabilidade na comunidade em geral.

@jesse-amano É interessante que você não queira genéricos em nenhuma versão do Go. Acho isso difícil de entender porque, como programador, realmente não gosto de me repetir. Sempre que programo em 'C' me vejo tendo que implementar as mesmas coisas básicas como uma lista ou uma árvore em algum novo tipo de dados, e inevitavelmente minhas implementações estão cheias de bugs. Com os genéricos, podemos ter apenas uma versão de qualquer algoritmo, e toda a comunidade pode contribuir para tornar essa versão a melhor. Qual é a sua solução para não se repetir?

Em relação ao outro ponto, Go parece estar introduzindo uma nova sintaxe para restrições genéricas porque as interfaces não permitem sobrecarregar operadores (como '==' e '+'). Há duas maneiras de avançar a partir disso, definir um novo mecanismo para restrições genéricas, que é a maneira que Go parece estar indo, ou permitir que interfaces sobrecarreguem operadores, que é a maneira que eu prefiro.

Eu prefiro a segunda opção porque mantém a sintaxe da linguagem menor e mais simples, e permite que novos tipos numéricos sejam declarados que podem usar os operadores usuais, por exemplo, números complexos que você pode adicionar com '+'. O argumento contra isso parece ser que as pessoas podem abusar da sobrecarga de operadores para fazer '+' fazer coisas estranhas, mas isso não parece um argumento para mim porque eu já posso abusar de qualquer nome de função, por exemplo, posso escrever uma função chamada 'print ' que apaga todos os dados do meu disco rígido e encerra o programa. Eu gostaria da capacidade de restringir sobrecargas de operadores e funções para estar em conformidade com certas propriedades axiomáticas, como comutatividade ou associatividade, mas se isso não se aplicar a operadores e funções, não vejo muito sentido. Um operador é apenas uma função infixa, e uma função é apenas um operador prefixo, afinal.

Outro ponto a ser mencionado é que restrições genéricas que fazem referência a vários parâmetros de tipo são muito úteis, se restrições genéricas de parâmetro único são predicados em tipos, restrições multiparâmetros são relações em tipos. As interfaces Go não podem ter mais de um parâmetro de tipo, então, novamente, uma nova sintaxe precisa ser introduzida ou as interfaces precisam ser redesenhadas.

Então, de certa forma, concordo com você, Go não foi projetado como uma linguagem genérica, e qualquer tentativa de incluir genéricos será sub-ótima. Talvez seja melhor manter Go sem genéricos e projetar uma nova linguagem em torno de genéricos desde o início para manter a linguagem pequena com uma sintaxe simples.

@keean Eu não tenho uma aversão tão forte a me repetir algumas vezes quando preciso, e a abordagem de Go para tratamento de erros, receptores de métodos etc. geralmente parece fazer um bom trabalho em manter a maioria dos bugs à distância.

Em um punhado de casos nos últimos quatro anos, me encontrei em situações em que um algoritmo complexo, mas generalizável, precisava ser aplicado a mais de duas estruturas de dados complexas, mas autoconsistentes, e em todos os casos - e digo isso com toda a seriedade - achei a geração de código via go:generate mais do que suficiente.

Ao ler os relatos de experiência, em muitos casos acho que go:generate ou uma ferramenta semelhante poderia ter resolvido o problema e, em alguns outros casos, sinto que talvez Go1 não fosse a linguagem certa, e outra coisa poderia ter sido usado em vez disso (talvez com um wrapper de plug-in se algum código Go precisar usá-lo). Mas estou ciente de que é bastante fácil para mim especular o que eu poderia ter feito, o que poderia ter funcionado; Até agora, não tive nenhuma experiência prática que me fez desejar que o Go1 tivesse mais maneiras de expressar tipos genéricos, mas pode ser que eu tenha uma maneira estranha de pensar sobre as coisas, ou pode ser que eu tenha sido extremamente sortudo por apenas funcionar em projetos que realmente não precisavam de genéricos.

Espero que, se o Go2 acabar suportando uma sintaxe genérica, ele tenha um mapeamento bastante direto para a lógica que será gerada, sem casos de borda estranhos possivelmente decorrentes de boxing/unboxing, "reificação", cadeias de herança etc. que outras línguas têm que se preocupar.

@jesse-amano Na minha experiência, porém, não são apenas algumas vezes, todo programa é uma composição de algoritmos bem conhecidos. Não me lembro da última vez que escrevi um algoritmo original, talvez um problema de otimização complexo que precisasse de conhecimento de domínio.

Ao escrever um programa, a primeira coisa que faço é tentar dividir o problema em pedaços bem conhecidos que posso compor, um analisador de argumentos, algum fluxo de arquivos, layout de interface do usuário baseado em restrições. Não são apenas algoritmos complexos que as pessoas cometem erros, dificilmente alguém pode escrever uma implementação correta de "min" e "max" na primeira vez (Veja: http://componentsprogramming.com/writing-min-function-part5/ ).

O problema com go:generate é que basicamente é apenas um processador de macro, não tem segurança de tipo, você de alguma forma tem que digitar check e error check no código gerado, o que você não pode fazer até que tenha executado a geração. Este tipo de meta-programação é muito difícil de depurar. Eu não quero escrever um programa para escrever o programa, eu só quero escrever o programa :-)

Portanto, a diferença com os genéricos é que eu posso escrever um programa _direto_ simples que pode ser verificado de erros e verificado pelo meu entendimento do significado, sem ter que gerar o código, e depurá-lo e trabalhar os bugs de volta ao gerador.

Um exemplo bem simples é "swap", quero apenas trocar dois valores, não importa o que sejam:

swap<A>(x: *A, y: *A) {
   let tmp = *x
   *x = *y
   *y = tmp
}

Agora eu acho que é trivial ver se esta função está correta, e é trivial ver que é genérico e pode ser aplicado a qualquer tipo. Por que eu iria querer digitar essa função de novo e de novo para cada tipo de ponteiro para um valor que eu possa querer usar swap. É claro que posso construir algoritmos genéricos maiores a partir disso, como uma classificação no local. Eu não acho que o código go:generate mesmo para um algoritmo simples seria fácil de ver se está correto.

Eu poderia facilmente cometer um erro como:

let tmp = *x
*y = *x
*x = tmp

digitando isso à mão toda vez que eu queria trocar o conteúdo de dois ponteiros.

Eu entendo que a maneira idiomática de fazer esse tipo de coisa em Go é usar uma interface vazia, mas isso não é seguro para o tipo e é lento. No entanto, parece-me que o Go não tem os recursos certos para suportar elegantemente esse tipo de programação genérica, e as interfaces vazias fornecem uma escotilha de escape para contornar os problemas. Em vez de mudar completamente o estilo do go, parece melhor desenvolver uma linguagem adequada para esse tipo de genérico do zero. Curiosamente, 'Rust' acerta muitas coisas genéricas, mas porque usa gerenciamento de memória estática em vez de coleta de lixo, adiciona muita complexidade que não é realmente necessária para a maioria das programações. Eu acho que entre Haskell, Go e Rust provavelmente há todos os bits necessários para fazer uma linguagem genérica mainstream decente, apenas tudo misturado.

Para informações: atualmente estou escrevendo uma lista de desejos em genéricos Go,

com a intenção de realmente implementá-lo no meu interpretador Go gomacro , que já possui uma implementação diferente de genéricos Go (modelados após modelos C++).

Ainda não está completo, comentários são bem-vindos :)

@keean

Eu li a postagem do blog que você vinculou sobre a função min e as quatro postagens que levaram a ela. Não observei sequer uma tentativa de argumentar que "dificilmente alguém pode escrever uma implementação correta de 'min'...". O escritor realmente parece reconhecer que sua primeira implementação _está_ correta... desde que o domínio seja restrito a números. É a introdução de objetos e classes, e a exigência de que eles sejam comparados ao longo de apenas uma dimensão, a menos que os valores nessa dimensão sejam os mesmos, exceto quando — e assim por diante, que cria a complexidade adicional. Os sutis requisitos ocultos envolvidos na necessidade de definir cuidadosamente as funções de comparação e classificação em um objeto complexo são exatamente o motivo pelo qual eu _não_ gosto de genéricos como um conceito (pelo menos em Go; Java com Spring parece que já é um ambiente bom o suficiente para compor juntar um monte de bibliotecas maduras em um aplicativo).

Pessoalmente, não vejo necessidade de segurança de tipo em geradores de macro; se eles estiverem gerando código legível ( gofmt ajuda a definir a barra para isso bastante baixo), então a verificação de erros em tempo de compilação deve ser suficiente. De qualquer forma, não deve importar para o usuário do gerador (ou código que o invoca) para produção; no conjunto reconhecidamente pequeno de vezes que fui chamado para escrever um algoritmo genérico como uma macro, um punhado de testes de unidade (geralmente float, string e pointer-to-struct - se houver algum tipo codificado que deveria for codificado, um desses três será incompatível com ele; se algum desses três não puder ser usado no algoritmo genérico, então não é um algoritmo genérico) foi suficiente para garantir que a macro funcionasse adequadamente.

swap é um mau exemplo. Desculpe, mas é. Já é um one-liner em Go, não há necessidade de uma função genérica para envolvê-lo e não há espaço para um programador cometer um erro não óbvio.

*y, *x = *x, *y

Também já existe um sort na biblioteca padrão . Ele usa interfaces. Para tornar uma versão específica para seu tipo, defina:

type myslice []mytype
func (s myslice) Len() int { return len(s) }
func (s myslice) Less(i, j int) bool { return s[i].whatWouldAlsoBeNeededInAGenericImpl(s[j]) }
func (s myslice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

É certo que há vários bytes a mais para digitar do que SortableList<mytype>(myThings).Sort() , mas é _lot_ menos denso para ler, não é tão provável que "gagueje" no resto de um aplicativo e, se surgirem bugs, é improvável precisar de algo tão pesado quanto um rastreamento de pilha para encontrar a causa. A abordagem atual tem várias vantagens, e eu me preocupo que as perderemos se nos basearmos demais nos genéricos.

@jesse-amano
Os problemas com 'min/max' se aplicam mesmo se você não entender a necessidade de uma classificação estável. Por exemplo, um desenvolvedor implementa min/max para algum tipo de dados em um módulo e, em seguida, é usado em uma classificação ou algum outro algoritmo por outro membro da equipe sem verificação adequada de suposições e leva a erros estranhos porque não é estável.

Eu acho que programação é principalmente compor algoritmos padrão, muito raramente os programadores criam novos algoritmos inovadores, então min/max e sort são apenas exemplos. Escolher falhas nos exemplos específicos que escolhi apenas mostra que não escolhi exemplos muito bons, não aborda o ponto real. Eu escolhi "swap" porque é muito simples e rápido para eu digitar. Eu poderia ter escolhido muitos outros, classificar, girar, particionar, que são algoritmos muito gerais. Não demora muito quando você está escrevendo um programa que usa uma coleção como uma árvore vermelha/preta para se cansar de ter que refazer a árvore para cada tipo de dados diferente dos quais você deseja uma coleção, porque você deseja segurança de tipo e uma interface vazia é um pouco melhor que "void*" em 'C'. Então você teria que fazer o mesmo novamente para cada algoritmo que usa cada uma dessas árvores, como iteração de pré-ordem, em ordem, pós-ordem, pesquisa, e isso antes de entrarmos em qualquer coisa sofisticada como os algoritmos de rede de Tarjan (disjuntos conjuntos, heaps, árvores geradoras mínimas, caminhos mais curtos, fluxos etc.)

Acho que os geradores de código têm seu lugar, por exemplo, gerando um validador de um esquema json ou um analisador de uma definição gramatical, mas não acho que eles sejam um substituto adequado para genéricos. Para programação genérica, quero ser capaz de escrever qualquer algoritmo uma vez e tê-lo claro, simples e direto.

De qualquer forma, concordo com você sobre 'Go', não acho que um 'Go' foi projetado desde o início para ser uma boa linguagem genérica, e adicionar genéricos agora provavelmente não resultará em uma boa linguagem genérica, e vai perder um pouco da franqueza e simplicidade que já tem. Pessoalmente, se você está tendo que buscar um gerador de código (além de coisas como gerar validadores de json-schema ou analisadores de um arquivo de gramática), provavelmente está usando o idioma errado de qualquer maneira.

Edit: Em relação ao teste de genéricos com "float" "string" "pointer-to-struct", não acho que existam muitos algoritmos genéricos que funcionem em um conjunto tão diversificado de tipos, exceto talvez 'swap'. As verdadeiras funções 'genéricas' são realmente limitadas a shuffles e não ocorrem com muita frequência. Os genéricos restritos são muito mais interessantes, onde os tipos genéricos são restritos por alguma interface. Como você pode ver, com o exemplo de classificação no local da biblioteca padrão, você pode fazer alguns genéricos restritos funcionarem em 'Go' em casos limitados. Gosto da maneira como as interfaces Go funcionam e você pode fazer muito com elas. Eu gosto ainda mais dos verdadeiros genéricos restritos. Eu realmente não gosto de adicionar um segundo mecanismo de restrição como a atual proposta de genéricos faz. Uma linguagem em que as interfaces restringem diretamente os tipos seria muito mais elegante.

É interessante que, até onde eu saiba, a única razão pela qual as novas restrições foram introduzidas é porque Go não permite que operadores sejam definidos em interfaces. As propostas genéricas anteriores permitiam que os tipos fossem limitados por interfaces, mas foram abandonadas porque não lidavam com operadores como '+'.

@keean
Talvez haja um lugar melhor para uma discussão prolongada. (Talvez não; eu dei uma olhada e este parece ser o _o_ lugar para discutir genéricos em Go2.)

Eu certamente entendo a necessidade de um tipo estável! Eu suspeito que os autores da biblioteca padrão Go1 original também entenderam, já que sort.Stable está lá desde o lançamento público.

Eu acho que a grande coisa sobre o pacote sort da biblioteca padrão é que ele _não_ funciona apenas em fatias. Certamente é mais simples quando o receptor é uma fatia, mas tudo o que você realmente precisa é saber quantos valores estão no contêiner (o método Len() int ), como compará-los (o Less(int, int) bool ) e como trocá-los (o método Swap(int, int) , é claro). Você pode implementar sort.Interface usando canais! É lento, é claro, porque os canais não são projetados para indexação eficiente, mas pode ser comprovado com um orçamento de tempo de execução generoso.

Não quero criticar, mas o problema com um exemplo ruim é que... é ruim. Coisas como sort e min são apenas _não_ pontos a favor de um recurso de linguagem de alto impacto como os genéricos. Eu sinto muito fortemente que fazer furos nesses exemplos _faz_ abordar o ponto real; _meu_ ponto é que não há necessidade de genéricos quando já existe uma solução melhor na linguagem.

@jesse-amano

melhor solução já existe na linguagem

Qual deles? Não vejo nada melhor do que genéricos com restrição de tipo. Os geradores não são Go, puro e simples. Interfaces e reflexão produzem código inseguro, lento e propenso a pânico. Essas soluções são boas o suficiente porque não há mais nada. Os genéricos resolveriam o problema com clichês, construções de interface vazias inseguras e, o pior de tudo, eliminaria muitos usos de reflexão que são ainda mais propensos a pânicos de tempo de execução. Mesmo a nova proposta do pacote de erros sofre com a falta de genéricos e sua API se beneficiaria muito com eles. Você pode ver As como um exemplo - não idiomático, propenso a pânico, difícil de usar, requer vet check para usar corretamente. Tudo porque Go não tem nenhum tipo de genérico.

sort , min e outros algoritmos genéricos são ótimos exemplos porque mostram o principal benefício dos genéricos - a composição. Eles permitem construir uma extensa biblioteca de rotinas de transformação genéricas que podem ser encadeadas. E o mais importante, seria fácil de usar, seguro, rápido (pelo menos é possível com genéricos), sem necessidade de clichê, geradores, interface{}, reflexão e outros recursos de linguagem obscuros usados ​​apenas porque não há outra maneira.

@creker

Qual deles?

Para classificar coisas, o pacote sort . Qualquer coisa que implemente sort.Interface pode ser classificada (com um algoritmo estável ou instável de sua escolha; algumas versões in-loco são fornecidas pelo pacote sort , mas você pode escrever o seu próprio com um API semelhante ou diferente). Como a biblioteca padrão sort.Sort e sort.Stable operam no valor passado pela lista de argumentos, o valor que você recebe de volta é o mesmo que o valor inicial — e, portanto, necessariamente, o tipo você recebe de volta é o mesmo tipo com o qual você começou. É perfeitamente seguro para tipos, e o compilador faz todo o trabalho de inferir se o seu tipo implementa a interface necessária e é capaz de _pelo menos_ tantas otimizações de tempo de compilação quanto seria possível com uma função sort<T> de estilo genérico .

Para trocar coisas, o one-liner x, y = y, x . Novamente, nenhuma declaração de tipo, conversão de interface ou reflexão é necessária. É apenas trocar dois valores. O compilador pode facilmente garantir que suas operações sejam de tipo seguro.

Não há uma única ferramenta específica que eu considere ser uma solução melhor do que os genéricos em todos os casos, mas para qualquer problema que os genéricos devem resolver, acredito que há uma solução melhor. Eu posso estar errado aqui; Ainda estou aberto a ver um exemplo de algo que os genéricos podem fazer onde todas as soluções existentes seriam terríveis. Mas se eu posso fazer buracos nele, então não é um desses exemplos.

Também não gosto muito do pacote xerrors , mas xerrors.As não me parece não-idiomático; é uma API muito semelhante a json.Unmarshal , afinal. Pode precisar de melhor documentação e/ou código de exemplo, mas de outra forma está tudo bem.

Mas não, sort e min são, por si só, exemplos terríveis. O primeiro já existe em Go e é perfeitamente componível, tudo sem necessidade de genéricos. Este último é, em seu sentido mais amplo, uma das saídas de sort (que já resolvemos), e nos casos em que uma solução mais especializada ou otimizada pode ser necessária, você escreveria a solução especializada de qualquer maneira, em vez de se apoiar genéricos. Novamente, não há geradores, interface{}, reflexão ou recursos de linguagem "obscuros" usados ​​no pacote sort da biblioteca padrão. Existem interfaces não vazias (que são bem definidas na API para que você receba erros em tempo de compilação se usá-las incorretamente, inferidas para não precisar de conversões e verificadas em tempo de compilação para não precisar afirmações). Pode haver algum clichê _se_ a coleção que você está classificando for uma fatia, mas se for uma estrutura (como uma que representa o nó raiz de uma árvore de pesquisa binária?), você pode fazer isso satisfazer o sort.Interface também, então é _mais_ flexível do que uma coleção genérica.

@jesse-amano

meu ponto é que não há necessidade de genéricos quando já existe uma solução melhor na linguagem

Eu acho que a melhor solução é realmente relativamente baseada em como você a vê. Se tivermos uma linguagem melhor, poderíamos ter uma solução melhor, é por isso que queremos tornar essa linguagem melhor. Por exemplo, se existir um genérico melhor, poderíamos ter sort melhor em nosso stdlib, pelo menos a maneira atual de implementar a interface de classificação não é uma boa experiência de usuário para mim, ainda tenho que digitar muito código semelhante que eu sinto fortemente que poderíamos abstrair.

@jesse-amano

Acho que o melhor do pacote de classificação da biblioteca padrão é que ele não funciona apenas em fatias.

Eu concordo, eu gosto do tipo padrão.

O primeiro já existe em Go e é perfeitamente componível, tudo sem necessidade de genéricos.

Esta é uma falsa dicotomia. Interfaces em Go já são uma forma de genéricos. O mecanismo não é a coisa em si. Olhe além da sintaxe e veja o objetivo, que é a capacidade de expressar qualquer algoritmo de forma genérica sem limitações. A abstração de interface de 'sort' é genérica, ela permite que qualquer tipo de dados que possa implementar os métodos necessários seja classificado. A notação é simplesmente diferente. Poderíamos escrever:

f<T>(x: T) requires Sortable(T)

O que significaria que o tipo 'T' deve implementar a interface 'Sortable'. Em 'Go' pode ser escrito func f(x Sortable) . Portanto, pelo menos a aplicação de funções em Go pode ser tratada genericamente, mas há operações que não podem gostar de aritmética ou de desreferenciação. Go se sai muito bem, pois interfaces podem ser consideradas predicados de tipo, mas Go não tem resposta para relações em tipos.

É fácil ver as limitações com o Go, considere:

func merge(x, y Sortable)

onde vamos mesclar duas coisas classificáveis, no entanto, Go não nos permite impor que essas duas coisas sejam iguais. Compare isso com:

merge<T>(x: T, y: T) requires Sortable(T)

Aqui fica claro que estamos mesclando dois tipos classificáveis ​​que são iguais. 'Go' joga fora as informações de tipo subjacentes e apenas trata qualquer coisa "classificável" da mesma forma.

Vamos tentar um exemplo melhor: digamos que eu queira escrever uma árvore vermelha/preta que possa conter qualquer tipo de dados, como uma biblioteca, para que outras pessoas possam usá-la.

Interfaces em Go já são uma forma de genéricos.

Em caso afirmativo, esse problema pode ser encerrado como já resolvido, porque a declaração original era:

Esta edição propõe que o Go suporte alguma forma de programação genérica.

O equívoco faz um desserviço a todas as partes. Interfaces são de fato _uma_ forma de programação genérica, e elas de fato _não_ necessariamente, por conta própria, resolvem todos os problemas que outras formas de programação genérica podem resolver. Então vamos, por simplicidade, permitir que qualquer problema que possa ser resolvido com ferramentas fora do escopo desta proposta/questão seja considerado "resolvido sem genéricos". (Acredito que a esmagadora maioria dos problemas solucionáveis ​​encontrados no mundo real, se não todos, estão nesse conjunto, mas isso é apenas para garantir que todos falemos a mesma língua.)

Considere: func merge(x, y Sortable)

Não está claro para mim por que mesclar duas coisas classificáveis ​​(ou coisas que implementam sort.Interface ) seria diferente de mesclar duas coleções _em geral_. Para fatias, isso é append ; para mapas, isso é for k, v := range m { n[k] = v } ; e para estruturas de dados mais complexas, há necessariamente estratégias de mesclagem mais complexas dependendo da estrutura (cujo conteúdo pode ser necessário para implementar alguns métodos que a estrutura necessita). Supondo que você esteja falando de um algoritmo de classificação mais complicado que particiona e escolhe sub-algoritmos para as partições antes de mesclá-las novamente, o que você precisa não é que as partições sejam "classificáveis", mas sim algum tipo de garantia de que suas partições são já _classificado_ antes de mesclar. Esse é um tipo de problema muito diferente, e não um que a sintaxe de modelo ajude a resolver de maneira óbvia; naturalmente, você gostaria de alguns testes de unidade bastante rigorosos para garantir a confiabilidade de seu(s) algoritmo(s) de ordenação por mesclagem, mas certamente não gostaria de expor uma API _exportada_ que sobrecarrega o desenvolvedor com esse tipo de coisa.

Você levanta um ponto interessante sobre Go não ter uma boa maneira de verificar se dois valores são do mesmo tipo sem reflexão, trocas de tipo, etc. Eu sinto que usar interface{} é uma solução perfeitamente aceitável no caso de contêineres de uso geral (por exemplo, uma lista vinculada circular), pois o clichê envolvido no encapsulamento da API para segurança de tipo é absolutamente trivial:

type MyStack struct { stack Stack }
func (s *MyStack) Push(v MyType) error { return s.stack.Push(v) }
func (s *MyStack) Pop() (MyType, error) {
  v, err := s.stack.Pop()
  var m MyType
  if v != nil {
    if m, ok := v.(MyType); ok { return m, err; }
    panic("this code should be unreachable from the exported API")
  }
  return nil, err
}

Eu me esforço para imaginar por que esse clichê seria um problema, mas se for, uma alternativa razoável pode ser um modelo (texto/). Você pode anotar os tipos para os quais deseja definir pilhas com um comentário //go:generate stackify MyType github.com/me/myproject/mytype e deixar go generate produzir o clichê para você. Contanto que cmd/stackify/stackify_test.go experimente com pelo menos uma struct e pelo menos um tipo interno, e compile e passe, não vejo por que isso seria um problema - e provavelmente está bem próximo para o que qualquer compilador acabaria fazendo "sob o capô" se você tivesse definido um modelo. A única diferença é que os erros são mais úteis porque são menos densos.

(Também pode haver casos em que queremos um _algo_ genérico que se preocupa mais com o fato de duas coisas serem do mesmo tipo do que com o comportamento delas, que não se enquadram na categoria "contêineres de coisas". Isso seria muito interessante, mas adicionar uma sintaxe de construção de modelo genérico à linguagem ainda pode não ser a única solução possível disponível.)

Supondo que o clichê _não seja_ um problema, estou interessado em resolver o problema de criar uma árvore vermelha/preta que seja tão fácil para os chamadores usarem quanto pacotes como sort ou encoding/json . Eu certamente vou falhar porque... bem, eu não sou tão inteligente. Mas estou animado para descobrir o quão perto eu posso chegar.

Edit: O início de um exemplo pode ser visto aqui , embora esteja longe de ser completo (o melhor que consegui juntar em algumas horas). Claro, também existem outras tentativas de estruturas de dados semelhantes.

@jesse-amano

Nesse caso, esse problema pode ser encerrado como já > resolvido, porque a declaração original era:

Não é apenas que as interfaces _são_ uma forma de genéricos, mas que melhorar a abordagem de interfaces pode nos levar até os genéricos. Por exemplo, interfaces multiparâmetros (onde você pode ter mais de um 'receptor') permitiriam relações em tipos. Permitir que interfaces substituam operadores como adição e desreferenciação removeria a necessidade de qualquer outra forma de restrição de tipos. As interfaces _podem_ ser todas as restrições de tipo que você precisa, se forem projetadas com uma compreensão do ponto de extremidade de genéricos totalmente gerais.

As interfaces são semanticamente semelhantes às classes de tipos de Haskell e às características de Rust que _do_ resolvem esses problemas genéricos. Classes de tipo e características resolvem todos os mesmos problemas genéricos que os modelos C++ resolvem, mas de uma maneira segura para o tipo (mas talvez nem todos os usos da meta-programação, o que eu acho que é uma coisa boa).

Eu me esforço para imaginar por que esse clichê seria um problema, mas se for, uma alternativa razoável pode ser um modelo (texto/).

Eu pessoalmente não tenho problemas com tanto clichê, mas entendo o desejo de não ter nenhum clichê, como programador é chato e repetitivo, e é exatamente o tipo de tarefa que escrevemos programas para evitar. Então, novamente, pessoalmente, acho que escrever uma implementação para uma interface/classe de tipo 'stack' é exatamente a maneira _certa_ de tornar seu tipo de dados 'empilhavel'.

Existem duas limitações com o Go que frustram a programação mais genérica. O problema de equivalência 'tipo', por exemplo, definindo funções matemáticas para que o resultado e todos os argumentos sejam os mesmos. Poderíamos imaginar:

mul<T>(x, y T) T requires Addable(T) {
    r := 0
    for i := 0; i < y; ++i  {
        r = r + x
    }
    return r
}

Para satisfazer as restrições em '+', precisamos garantir que x e y sejam numéricos, mas também do mesmo tipo subjacente.

A outra é a limitação das interfaces a apenas um único tipo de 'receptor'. Essa limitação significa que você não precisa digitar o clichê acima apenas uma vez (o que acho razoável), mas para cada tipo diferente que você deseja colocar no MyStack. O que queremos é declarar o tipo contido como parte da interface:

type Stack<T> interface {...}

Isso permitiria, entre outras coisas, declarar uma implementação paramétrica em T para que possamos colocar qualquer T em MyStack usando a interface Stack, desde que todos os usos de Push e Pop na mesma instância do MyStack opera no mesmo tipo de 'valor'.

Com essas duas mudanças, devemos ser capazes de criar uma árvore genérica vermelha/preta. Deveria ser possível sem eles, mas como o Stack, você terá que declarar uma nova instância da interface para cada tipo que deseja colocar na árvore vermelha/preta.

Do meu ponto de vista, as duas extensões acima para interfaces são tudo o que é necessário para que Go suporte totalmente 'genéricos'.

@jesse-amano
Olhando para o exemplo da árvore vermelha/preta, o que realmente queremos genericamente é a definição de um 'Mapa', a árvore vermelha/preta é apenas uma implementação possível. Como tal, podemos esperar uma interface como esta:

type Map<Key, Value> interface {
   put(x Key, y Value) 
   get(x Key) Value
}

Então a árvore vermelha/preta pode ser fornecida como uma implementação. Idealmente, queremos escrever código que não dependa da implementação, para que você possa fornecer uma tabela de hash, ou uma árvore vermelho-preta, ou um BTree. Nós então escreveríamos nosso código:

f<K, V, T>(index T) T requires Map<K, V> {
   ...
}

Agora, seja o que for f , ele pode funcionar independentemente da implementação do Map, f pode ser uma função de biblioteca escrita por outra pessoa, que não precisa saber se minha aplicação usa um red/ árvore preta ou um mapa de hash.

Em go como está agora, precisaríamos definir um mapa específico como este:

type MapIntString interface {
   put(x Int, y String)
   get(x Int) String
}

O que não é tão ruim, mas significa que a função 'biblioteca' f deve ser escrita para todas as combinações possíveis de tipos de chave e valor se quisermos usá-la em um aplicativo onde não Não conhecemos os tipos de chaves e valores quando escrevemos a biblioteca.

Embora eu concorde com o último comentário do @keean , a dificuldade é escrever uma árvore vermelha/preta em Go que implemente uma interface conhecida, como por exemplo a que acabamos de sugerir.

Sem genéricos, é bem conhecido que para implementar containers agnósticos de tipo é preciso usar interface{} e/ou reflexão - infelizmente ambas as abordagens são lentas e propensas a erros.

@keean

Não é apenas que as interfaces são uma forma de genéricos, mas que melhorar a abordagem de interfaces pode nos levar até os genéricos.

Não vejo nenhuma das propostas vinculadas a esta questão, até o momento, como uma melhoria. Parece bastante incontroverso dizer que todos eles são falhos de alguma forma. Acredito que essas falhas superam severamente qualquer benefício, e muitos dos benefícios _reivindicados_ na verdade já são suportados por recursos existentes. Minha crença é baseada na experiência prática, não na especulação, mas ainda é anedótica.

Eu pessoalmente não tenho problemas com tanto clichê, mas entendo o desejo de não ter nenhum clichê, como programador é chato e repetitivo, e é exatamente o tipo de tarefa que escrevemos programas para evitar.

Eu também não concordo com isso. Como profissional remunerado, meu objetivo é reduzir os custos de tempo/esforço _para mim e para os outros_, aumentando os ganhos do meu empregador, independentemente de como sejam medidos. Uma tarefa "chata" só é ruim se também for demorada; não pode ser difícil, ou não seria chato. Se for apenas um pouco demorado antecipadamente, mas eliminar futuras atividades demoradas e/ou lançar o produto mais cedo, então ainda vale a pena.

Então a árvore vermelha/preta pode ser fornecida como uma implementação.

Acho que fiz um progresso decente nos últimos dias em uma implementação de uma árvore vermelha/preta (está inacabada; falta até mesmo um readme), mas estou preocupado por já ter falhado em ilustrar meu ponto se não for abundante claro que meu objetivo não é trabalhar em direção a uma interface, mas sim trabalhar em direção a uma implementação. Estou escrevendo uma árvore vermelha/preta e, claro, quero que ela seja _útil_, mas não me importo com quais coisas _específicas_ outros desenvolvedores podem querer usá-la.

Eu sei que a interface mínima exigida por uma biblioteca de árvore vermelha/preta é aquela em que existe uma ordenação "fraca" em seus elementos, então eu preciso de algo _como_ uma função chamada Less(v interface{}) bool , mas se o chamador tiver um método que faz algo semelhante, mas não é nomeado Less(v interface{}) bool , cabe a eles escrever os wrappers/shims clichê para fazê-lo funcionar.

Quando você acessa os elementos contidos na árvore vermelha/preta, você obtém interface{} , mas se você estiver disposto a confiar na minha garantia de que a biblioteca forneceu _é_ uma árvore vermelha/preta, não entendo por que você não faria isso t confie que os tipos de elementos que você coloca serão exatamente os tipos de elementos que você retirará. Se você _confiar em ambas as garantias, então a biblioteca não é propensa a erros. Simplesmente escreva (ou cole) uma dúzia de linhas de código para cobrir as asserções de tipo.

Agora você tem uma biblioteca perfeitamente segura (novamente, assumindo não mais do que o nível de confiança que você teria que estar disposto a dar para fazer o download da biblioteca em primeiro lugar) que tem até os nomes exatos das funções que você deseja. Isso é importante. Em um ecossistema no estilo Java, onde os autores de bibliotecas estão se voltando para o código contra uma definição de interface _exata_ (eles quase precisam, porque a linguagem impõe isso por meio da sintaxe class MyClassImpl extends AbstractMyClass implements IMyClass ) e há um monte de burocracia extra, você tem que se esforçar para fazer uma fachada para a biblioteca de terceiros se encaixar nos padrões de codificação da sua organização (que é a mesma quantidade de clichê, se não mais), ou então permitir que isso seja uma "exceção" para padrões de codificação de sua organização (e eventualmente sua organização tem tantas exceções em seus padrões quanto em suas bases de código), ou então desista de usar uma biblioteca perfeitamente boa (assumindo, por uma questão de argumento, que a biblioteca é realmente boa).

Idealmente, queremos escrever código que não dependa da implementação, para que você possa fornecer uma tabela de hash, ou uma árvore vermelho-preta, ou um BTree.

Concordo com esse ideal, mas acho que o Go já o satisfaz. Com uma interface como:

type MyStorage interface {
  Get(KeyType) (ValueType, error)
  Put(KeyType, ValueType) error
}

a única coisa que está faltando é a capacidade de parametrizar o que KeyType e ValueType são, e não estou convencido de que isso seja especialmente importante.

Como um mantenedor (hipotético) de uma biblioteca de árvore vermelha/preta, não me importo com quais são seus tipos. Vou usar interface{} para todas as minhas funções principais que lidam com "alguns dados", e _talvez_ fornecer alguns exemplos de funções exportadas que permitem que você as use mais facilmente com tipos comuns como string e int . Mas cabe ao chamador fornecer a camada extremamente fina em torno dessa API para torná-la segura para quaisquer tipos personalizados que possam acabar definindo. Mas a única coisa importante sobre a API que estou fornecendo é que ela permite que o chamador faça todas as coisas que espera que uma árvore vermelha/preta seja capaz de fazer.

Como um chamador (hipotético) de uma biblioteca de árvore vermelha/preta, provavelmente só a quero para armazenamento rápido e tempo de pesquisa. Eu não me importo que seja uma árvore vermelha/preta. Eu me importo que eu possa Get coisas dele e Put coisas nele, e – importante – eu me importo com o que essas coisas são. Se a biblioteca não oferece funções chamadas Get e Put , ou não pode interagir perfeitamente com os tipos que eu defini, isso não importa para mim, desde que seja fácil para mim para escrever os métodos Get e Put , e fazer meu próprio tipo satisfazer a interface que a biblioteca precisa enquanto estou nisso. Se não for fácil, geralmente acho que a culpa é do autor da biblioteca, não da linguagem, mas mais uma vez é possível que haja contra-exemplos dos quais não estou ciente.

A propósito, o código poderia ficar muito mais emaranhado se não fosse assim. Como você disse, existem muitas implementações possíveis de um armazenamento de chave/valor. Passar um "conceito" abstrato de armazenamento de chave/valor oculta a complexidade de como o armazenamento de chave/valor é realizado, e um desenvolvedor da minha equipe pode escolher o errado para sua tarefa (incluindo uma versão futura de mim mesmo cujo conhecimento da chave /valor implementação de armazenamento paginado sem memória!). O aplicativo ou seus testes de unidade podem, apesar de nossos melhores esforços na revisão de código, conter um código sutil dependente de implementação que para de funcionar de forma confiável quando alguns armazenamentos de chave/valor dependem de uma conexão com um banco de dados e outros não. É uma dor quando o relatório de erro vem com um grande rastreamento de pilha, e a única linha no rastreamento de pilha que faz referência a algo na base de código _real_ aponta para uma linha que usa um valor de interface, tudo porque a implementação dessa interface é código gerado (que você só pode ver no tempo de execução) em vez de uma estrutura comum, com métodos retornando valores de erro legíveis.

@jesse-amano
Concordo com você e gosto da maneira 'Go' de fazer as coisas onde o código do "usuário" declara uma interface que abstrai a maneira como ela funciona e, em seguida, você escreve a implementação dessa interface para a biblioteca/dependência. Isso está ao contrário da maneira como a maioria das outras linguagens pensa sobre interfaces. mas uma vez que você consegue, é muito poderoso.

Eu ainda gostaria de ver as seguintes coisas em uma linguagem genérica:

  • tipos paramétricos, como: RBTree<Int, String> , pois isso reforçaria a segurança de tipo de coleções de usuários.
  • variáveis ​​de tipo, como: f<T>(x, y T) T , porque isso é necessário para definir famílias de funções relacionadas como adição, subtração etc, onde a função é polimórfica, mas exigimos que todos os argumentos sejam do mesmo tipo subjacente.
  • restrições de tipo, como: f<T: Addable>(x, y T) T , que está aplicando interfaces para variáveis ​​de tipo, porque uma vez que introduzimos variáveis ​​de tipo, precisamos de uma maneira de restringir essas variáveis ​​de tipo em vez de tratar Addable como um tipo. Se considerarmos Addable como um tipo e escrevermos f(x, y Addable) Addable , não teremos como saber se os tipos subjacentes originais de x e y são os mesmos que entre si ou o tipo retornado.
  • interfaces multiparâmetros, como: type<K, V> Map<K, V> interface {...} , que podem ser usadas como merge<K, V, T: Map<K, V>>(x, y T) T que nos permite declarar interfaces que são parametrizadas não apenas pelo tipo de container, mas neste caso também pela chave e valor tipos do mapa.

Acho que cada um deles aumentaria o poder abstrativo da linguagem.

Qualquer progresso ou cronograma sobre isso?

@leaxoy Tem palestra agendada sobre "Genéricos em Go" de @ianlancetaylor na GopherCon . Eu esperaria ouvir mais sobre o estado atual das coisas nessa palestra.

@griesemer Obrigado por esse link.

@keean Eu adoraria ver também a cláusula Where do Rust aqui, o que pode ser uma melhoria na sua proposta type constraints . Ele permite usar o sistema de tipos para restringir comportamentos como "iniciar uma transação antes da consulta" a serem verificados sem reflexão de tempo de execução. Confira este vídeo sobre ele: https://www.youtube.com/watch?v=jSpio0x7024

@jadbox desculpe se minha explicação não foi clara, mas a cláusula 'onde' é quase exatamente o que eu estava propondo. As coisas depois de 'onde' em ferrugem são restrições de tipo, mas acho que usei a palavra-chave 'requer' em um post anterior. Essas coisas foram feitas em Haskell pelo menos uma década atrás, exceto que Haskell usa o operador '=>' em assinaturas de tipo para indicar restrições de tipo, mas é o mesmo mecanismo subjacente.

Deixei isso de fora do meu post de resumo acima porque queria manter as coisas simples, mas gostaria de algo assim:

merge<K, V, T>(x, y T) T requires T: Map<K, V>

Mas isso realmente não adiciona nada ao que você pode fazer além de uma sintaxe que pode ser mais legível para conjuntos de restrições longos. Você pode representar qualquer coisa que puder com a cláusula 'where' colocando a restrição depois que eles digitarem a variável na declaração inicial assim:

merge<K, V, T: Map<K, V>>(x, y T) T

Desde que você possa fazer referência às variáveis ​​de tipo antes de serem declaradas, você pode colocar quaisquer restrições nelas e usar uma lista separada por vírgulas para aplicar várias restrições à mesma variável de tipo.

Tanto quanto sei, a única vantagem de uma cláusula 'where'/'requires' é que todas as variáveis ​​de tipo já são declaradas antecipadamente, o que pode facilitar o analisador e a inferência de tipo.

Este ainda é o tópico certo para feedback/discussão sobre a proposta Go 2 Generics atual/mais recente que foi anunciada recentemente?

Em suma, gosto muito do rumo que a proposta está tomando em geral e do mecanismo de contratos em particular. Mas estou preocupado com o que parece ser uma suposição obstinada de que os parâmetros genéricos de tempo de compilação devem (sempre) ser parâmetros de tipo. Eu escrevi alguns comentários sobre esse problema aqui:

Apenas os parâmetros de tipo são genéricos o suficiente para genéricos Go 2?

Certamente os comentários aqui são bons, mas em geral não acho que os problemas do GitHub sejam um bom formato para discussão, pois eles não fornecem nenhum tipo de encadeamento. Eu acho que as listas de discussão são melhores.

Eu não acho que esteja claro ainda com que frequência as pessoas vão querer funções parametrizadas em valores constantes. O caso mais óbvio seria para dimensões de array - mas você já pode fazer isso passando o tipo de array desejado como um argumento de tipo. Fora esse caso, o que realmente ganhamos passando um const como um argumento de tempo de compilação em vez de um argumento de tempo de execução?

Go já oferece muitas maneiras diferentes e ótimas de resolver problemas e nunca devemos adicionar nada de novo, a menos que esteja corrigindo um grande problema e deficiência, o que claramente não está fazendo, e mesmo nessas circunstâncias a complexidade adicional que se segue é muito alto preço a pagar.

Go é único exatamente por causa do jeito que é. Se não estiver quebrado, por favor , não tente consertá-lo!

As pessoas que estão descontentes com a maneira como o Go foi projetado devem usar uma das muitas outras linguagens que já possuem essa complexidade adicional e irritante.

Go é único exatamente por causa do jeito que é. Se não estiver quebrado, por favor, não tente consertá-lo!

Está quebrado, portanto, deve ser consertado.

Está quebrado, portanto, deve ser consertado.

Pode não funcionar da maneira que você acha que deveria, mas uma linguagem nunca pode. Certamente não está quebrado. Considerando as informações disponíveis e o debate, reservar um tempo para tomar uma decisão informada e sensata é sempre a melhor opção. Muitos outros idiomas sofreram, na minha opinião, devido à adição de mais e mais recursos para resolver cada vez mais problemas em potencial. Lembre-se que "não" é temporário, "sim" é para sempre.

Tendo participado de mega-questões anteriores, sugiro que seja aberto um canal no Gopher Slack para quem quiser discutir isso, o assunto é bloqueado temporariamente e, em seguida, postado os horários em que o assunto será descongelado para quem quiser consolidar o discussão do Slack? Os problemas do Github não funcionam mais como um fórum quando o temido link "478 itens ocultos Carregar mais ..." entra.

posso sugerir que seja aberto um canal no Gopher Slack para quem quiser discutir isso
As listas de discussão são melhores porque fornecem um arquivo pesquisável. Um resumo ainda pode ser publicado sobre esta questão.

Tendo participado de mega-questões anteriores, sugiro que seja aberto um canal no Gopher Slack para quem quiser discutir isso

Por favor, não mova a discussão inteiramente para plataformas fechadas. Se em algum lugar, golang-nuts está disponível para todos (ish? Eu também não sei se isso funciona sem uma conta do Google, mas pelo menos é um método padrão de comunicação que todos têm ou podem obter) e deve ser movido para lá . O GitHub é ruim o suficiente, mas eu aceito de má vontade que estamos presos a ele para comunicação, nem todos podem obter uma conta Slack ou podem usar seus clientes terríveis.

nem todos podem obter uma conta Slack ou podem usar seus clientes terríveis

O que "pode" significa aqui? Existem restrições reais no Slack que eu não conheço ou as pessoas simplesmente não gostam de usá-lo? O último é bom, eu acho, mas algumas pessoas também boicotam o Github porque não gostam da Microsoft, então você perde algumas pessoas, mas ganha outras.

nem todos podem obter uma conta Slack ou podem usar seus clientes terríveis

O que "pode" significa aqui? Existem restrições reais no Slack que eu não conheço ou as pessoas simplesmente não gostam de usá-lo? O último é bom, eu acho, mas algumas pessoas também boicotam o Github porque não gostam da Microsoft, então você perde algumas pessoas, mas ganha outras.

O Slack é uma empresa dos EUA e, como tal, seguirá todas as políticas externas impostas pelos EUA.

O Github tem o mesmo problema e foi notícia por expulsar iranianos sem aviso prévio. É lamentável, mas a menos que usemos Tor ou IPFS ou algo assim, teremos que respeitar as leis americanas/europeias para qualquer fórum de discussão prático.

O Github tem o mesmo problema e foi notícia por expulsar iranianos sem aviso prévio. É lamentável, mas a menos que usemos Tor ou IPFS ou algo assim, teremos que respeitar as leis americanas/europeias para qualquer fórum de discussão prático.

Sim, estamos presos ao GitHub e aos Grupos do Google. Não vamos adicionar serviços mais problemáticos à lista. Também o chat não é um bom arquivo; é difícil o suficiente cavar essas discussões quando elas estão bem encadeadas e em golang-nuts (onde elas vêm direto para sua caixa de entrada). Slack significa que se você não está no mesmo fuso horário que todo mundo, você tem que percorrer massas de arquivos de bate-papo, um fora de sequências, etc. listas de discussão significam que você tem pelo menos um pouco organizado em tópicos, e as pessoas tendem a tomar mais tempo em suas respostas para que você não receba toneladas de comentários aleatórios 1-off deixados casualmente. Além disso, eu simplesmente não tenho uma conta no Slack e seus clientes estúpidos não funcionam em nenhuma das máquinas que eu uso. Mutt, por outro lado (ou seu cliente de e-mail de escolha, sim, padrões) funciona em todos os lugares.

Por favor, mantenha esta edição sobre genéricos. Vale a pena discutir o fato de o rastreador de problemas do GitHub não ser ideal para discussões em larga escala, como genéricos, mas não sobre esse assunto. Marquei vários comentários acima como "fora do tópico".

Em relação à singularidade do Go: Go tem alguns recursos interessantes, mas não é tão único quanto alguns parecem pensar. Como dois exemplos, CLU e Modula-3 têm objetivos semelhantes e retornos semelhantes, e ambos suportam genéricos de alguma forma (desde ~1975 no caso de CLU!). Eles não têm suporte industrial no momento, mas FWIW, é possível obter um compilador trabalhando para ambos.

algumas perguntas sobre sintaxe, a palavra-chave type nos parâmetros de tipo é necessária? e faria mais sentido adotar <> para os parâmetros de tipo como outras linguagens? Isso pode tornar as coisas mais legíveis e familiares ...

Embora eu não seja contra a forma como está na proposta, apenas colocando isso para consideração

em vez de:

type Vector(type Element) []Element
var v Vector(int)
func (v *Vector(Element)) Push(x Element) { *v = append(*v, x) }
type VectorInt = Vector(int)

nós poderíamos ter

type Vector<Element> []Element
var v Vector<int>
func (v *Vector<Element>) Push(x Element) { *v = append(*v, x) }
type VectorInt = Vector<int>

A sintaxe <> é mencionada no rascunho, @jnericks (Seu nome de usuário é perfeito para esta discussão...). O principal argumento contra isso é que aumenta massivamente a complexidade do analisador. De maneira mais geral, torna o Go uma linguagem significativamente mais difícil de analisar para pouco benefício. A maioria das pessoas concorda que isso melhora a legibilidade, mas há discordância sobre se vale a pena ou não a troca. Pessoalmente, acho que não.

O uso da palavra-chave type é necessário para desambiguar. Caso contrário, é difícil dizer a diferença entre func Example(T)(arg int) {} e func Example(arg int) (int) {} .

Eu li a última proposta sobre os genéricos. todos correspondem ao meu gosto, exceto a gramática da declaração do contrato.

como sabemos, em go sempre declaramos struct ou interface assim:

type MyStruct struct {
        a int
        s string
}

type MyInterface inteface {
    Method1() err
    Method2() string
}

mas a declaração do contrato na última proposta é assim:

contract Ordered(T) {
    T int, int8
}

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

Na minha opinião, a gramática do contrato é inconsistente na forma com a abordagem tradicional. como sobre a gramática como abaixo:

type Ordered(T) contract {
    T int, int8
}

if there is only one type parameter, the declaration above can be also wrote like this:

type Ordered contract {
    int , int8
}


if there are more than one type parameter, we have to use named parameter:

type G(Node, Edge) contract {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

agora a forma de contrato é consistente com o tradicional. podemos declarar um contrato em um bloco de tipo com struct, interface:

type (
        Sequence contract {
                string, []byte
        }

    Stringer(T) contract {
        T String() string
    }

    Stringer contract { // equivalent with the above Stringer(T), single type parameter could be omitted
        String() string
    }

        MyStruct struct {
                a int
                b string
        }

    G(Node, Edge) contract {
        Node Edges() []Edge
        Edge Nodes() (from Node, to Node)
    }
)

Assim, o "contrato" se torna a palavra-chave do mesmo nível que struct, interface. A diferença é que o contrato é usado para declarar o metatipo para o tipo.

@bigwhite Ainda estamos discutindo essa notação. O argumento a favor da notação sugerida na minuta do projeto é que um contrato não é um tipo (por exemplo, não se pode declarar uma variável de um tipo de contrato) e, portanto, um contrato é um novo tipo de entidade da mesma forma que uma constante , função, variável ou tipo. O argumento a favor de sua sugestão é que um contrato é simplesmente um "tipo de tipo" (ou um tipo de meta) e, portanto, deve seguir uma notação consistente. Outro argumento a favor de sua sugestão é que ela permitiria o uso de literais de contrato "anônimos" sem a necessidade de declará-los explicitamente. Em suma, IMHO isso ainda não está resolvido. Mas também é fácil mudar no caminho.

FWIW, CL 187317 suporta ambas as notações no momento (embora o parâmetro do contrato deva ser escrito com o contrato), por exemplo:

type C contract(X) { ... }

e

contract C (X) { ... }

são aceitos e representados da mesma forma internamente. A abordagem mais consistente seria:

type C(type X) contract { ... }

Um contrato não é um tipo. Não é nem mesmo um meta-tipo, já que os únicos tipos que
se preocupa são seus parâmetros. Não há tipo de receptor separado
do qual o contrato pode ser considerado o metatipo.

Go também tem declarações de função:

func Name(args) { body }

que a sintaxe do contrato proposta espelha mais diretamente.

De qualquer forma, esses tipos de discussões de sintaxe parecem estar em baixa na lista de prioridades em
este ponto. É mais importante olhar para a semântica do rascunho e
como eles afetam o código, que tipo de código pode ser escrito com base nesses
semântica, e que código não pode.

Edit: Em relação aos contratos in-line, Go tem literais de função. Não vejo nenhum motivo para não haver literais de contrato. Haveria apenas um número mais limitado de lugares em que eles poderiam aparecer, já que não são tipos ou valores.

@stevenblenkinsop Eu não iria tão longe a ponto de afirmar com naturalidade que um contrato não é um tipo (ou meta-tipo). Acho que há argumentos muito razoáveis ​​para ambos os pontos de vista. Por exemplo, um contrato de parâmetro único que especifica apenas métodos serve essencialmente como um "limite superior" para um parâmetro de tipo: Qualquer argumento de tipo válido deve implementar esses métodos. É para isso que costumamos usar interfaces. Pode fazer muito sentido permitir interfaces nesses casos em vez de um contrato, a) porque esses casos podem ser comuns; eb) porque satisfazer um contrato neste caso significa simplesmente satisfazer a interface enunciada como um contrato. Ou seja, tal contrato age muito como um tipo contra o qual outro tipo é "comparado".

@griesemer considerar contratos como tipos pode levar a problemas com o paradoxo de Russel (como no tipo de todos os tipos que não são 'membros' de si mesmos). Eu acho que eles são mais bem considerados 'restrições de tipos'. Se considerarmos um sistema de tipos uma forma de 'lógica', podemos prototipar isso em Prolog. Variáveis ​​de tipo tornam-se variáveis ​​lógicas, tipos tornam-se átomos e contratos/restrições podem ser resolvidos por Programação Lógica de Restrições. É tudo muito limpo e não paradoxal. Em termos de sintaxe, poderíamos considerar um contrato uma função em tipos que retorna um booleano.

@keean Qualquer interface já serve como uma "restrição de tipos", mas são tipos. As pessoas da teoria dos tipos veem as restrições dos tipos como tipos, de uma maneira muito formal. Como mencionei acima , há argumentos razoáveis ​​que podem ser feitos para ambos os pontos de vista. Não há "paradoxos lógicos" aqui - na verdade, o protótipo de trabalho em andamento atual modela um contrato como um tipo internamente, pois simplifica as coisas no momento.

As interfaces @griesemer em Go são 'subtipos' e não restrições de tipos. No entanto, acho que a necessidade de contratos e interfaces é uma desvantagem para o design do Go, no entanto, pode ser tarde demais para alterar as interfaces em restrições de tipo em vez de subtipos. Argumentei acima que as interfaces Go não precisam necessariamente ser subtipos, mas não vejo muito suporte para essa ideia. Isso permitiria que interfaces e contratos fossem a mesma coisa - se as interfaces também pudessem ser declaradas para os operadores.

Há paradoxos aqui, então vá com cuidado, o Paradoxo de Girard é a 'codificação' mais comum do Paradoxo de Russel na teoria dos tipos. A teoria dos tipos introduz o conceito de universos para evitar esses paradoxos, e você só pode referenciar tipos no universo 'U' do universo 'U+1'. Internamente, essas teorias de tipo são implementadas como lógicas de ordem superior (por exemplo, Elf usa lambda-prolog). Isso, por sua vez, reduz a resolução de restrições para o subconjunto decidível da lógica de ordem superior.

Portanto, embora você possa pensar neles como tipos, você precisa adicionar um conjunto de restrições de uso (sintáticas ou não) que efetivamente o levem de volta às restrições de tipos. Pessoalmente, acho mais fácil trabalhar diretamente com as restrições e evitar as duas camadas adicionais de abstração, lógica de ordem superior e tipos dependentes. Essas abstrações não acrescentam nada ao poder expressivo do sistema de tipos e exigem mais regras ou restrições para evitar paradoxos.

Em relação ao protótipo atual que trata as restrições como tipos, o perigo vem se você puder usar esse "tipo de restrição" como um tipo normal e, em seguida, construir outro 'tipo de restrição' nesse tipo. Você precisará de verificações para evitar auto-referência (isso normalmente é trivial) e loops de referência mútua. Esse tipo de protótipo deve realmente ser escrito em Prolog, pois permite focar nas regras de implementação. Eu acredito que os desenvolvedores de Rust finalmente perceberam isso há algum tempo (veja Chalk).

@griesemer Interessante, re-modelagem de contratos como tipos. Do meu próprio modelo mental, eu pensaria em restrições como metatipos e contratos como uma espécie de estrutura de nível de tipo.

type A int
func (a A) Foo() int {
    return int(a)
}

type C contract(T, U) {
    T int
    U int, uint
    U Foo() int
}

var B (int, uint; Foo() int).type = A
var C1 C = C(A, B)

Isso me sugere que a sintaxe de estilo de declaração de tipo atual para contratos é a mais correta das duas. Eu acho que a sintaxe definida no rascunho ainda é melhor, pois não requer abordar a questão "se é um tipo, como são seus valores".

@stevenblenkinsop você me perdeu, por que você passa T para C contract quando não é usado, e o que as linhas var estão tentando fazer?

@griesemer obrigado pela sua resposta. Um dos princípios de design do Go é "fornecer apenas uma maneira de fazer algo". É melhor manter apenas um formulário de declaração de contrato. tipo C(tipo X) contrato { ... } é melhor.

@Goodwine Renomeei os tipos para distingui-los dos parâmetros do contrato. Talvez isso ajude? (int, uint; Foo() int).type pretende ser o metatipo de qualquer tipo que tenha um tipo subjacente de int ou uint e que implemente Foo() int . var B se destina a mostrar usando um tipo como valor e atribuindo-o a uma variável cujo tipo é um metatipo (já que um metatipo é como um tipo cujos valores são tipos). var C1 destina-se a mostrar uma variável cujo tipo é um contrato e mostrar um exemplo de algo que pode ser atribuído a tal variável. Basicamente, tentando responder à pergunta "se um contrato é um tipo, como são seus valores?". A questão é mostrar que esse valor não parece ser um tipo.

Eu tenho um problema com contratos com vários tipos.

Você pode adicionar ou deixar para o tipo de contrato de parâmetro, tanto
type Graph (type Node, Edge) struct { ... }
e
type Graph (type Node, Edge G) struct { ... } estão OK.

Mas e se eu quiser apenas adicionar um contrato em um dos dois parâmetros de tipo?

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

VS

contract G(Edge) {
    Edge Nodes() (from Node, to Node)
}

@themez Isso está no rascunho. Você pode usar a sintaxe (type T, U comparable(T)) para restringir apenas um parâmetro de tipo, por exemplo.

@stevenblenkinsop entendi, obrigado.

@themez Isso surgiu algumas vezes agora. Eu acho que há alguma confusão pelo fato de que o uso se parece com um tipo para uma definição de variável. Mas realmente não é; um contrato é mais um detalhe de toda a função do que uma definição de argumento. Acho que a suposição é que você escreveria essencialmente um novo contrato, potencialmente composto de outros contratos para ajudar na repetição, basicamente para cada função/tipo genérico que você criar. Coisas como o que @stevenblenkinsop mencionou estão realmente lá para pegar os casos extremos em que essa suposição não faz sentido.

Pelo menos é essa a impressão que tenho, principalmente pelo fato de serem chamados de 'contratos'.

@keean Acho que estamos interpretando a palavra "restrição" de maneira diferente; Estou usando-o bastante informalmente. Por definição de interfaces, dada uma interface I e uma variável x do tipo I , somente valores com tipos que implementam I podem ser atribuídos a x . Assim I pode ser visto como uma "restrição" nesses tipos (é claro que ainda existem infinitos tipos que satisfazem essa "restrição"). Da mesma forma, pode-se usar I como uma restrição para um parâmetro de tipo P de uma função genérica; somente argumentos de tipo real com conjuntos de métodos que implementam I seriam permitidos. Assim I também limita o conjunto de possíveis tipos de argumentos reais.

Em ambos os casos, a razão para isso é descrever as operações (métodos) disponíveis dentro da função. Se o I for usado como o tipo de um parâmetro (valor), sabemos que esse parâmetro fornece esses métodos. Se o I us usado como uma "restrição" (no lugar de um contrato), sabemos que todos os valores do parâmetro de tipo tão restrito fornecem esses métodos. É obviamente bastante direto.

Eu gostaria de um exemplo concreto de por que essa ideia específica de usar interfaces para contratos de parâmetro único que apenas declaram métodos "quebram" sem algumas restrições, como você mencionou em seu comentário .

Como será apresentada a proposta de contratos? Usando o parâmetro go1.14 dos módulos go? Uma variável de ambiente GO114CONTRACTS ? Ambos? Algo mais..?

Desculpe se isso já foi abordado antes, sinta-se à vontade para me redirecionar para lá.

Uma coisa que eu particularmente gosto no projeto de rascunho de genéricos atual é que ele coloca água limpa entre contracts e interfaces . Eu sinto que isso é importante porque os dois conceitos são facilmente confundidos, embora existam três diferenças básicas entre eles:

  1. Contracts descreve os requisitos de um _conjunto_ de tipos, enquanto interfaces descreve os métodos que um _único_ tipo deve ter para satisfazê-lo.

  2. Contracts pode lidar com operações internas, conversões etc. listando os tipos que os suportam; interfaces só pode lidar com métodos que os próprios tipos internos não possuem.

  3. Quaisquer que sejam em termos teóricos de tipos, contracts não são tipos no sentido que normalmente pensamos neles em Go, ou seja, você não pode declarar variáveis ​​de tipos contract e dar-lhes algum valor. Por outro lado interfaces são tipos, você pode declarar variáveis ​​desses tipos e atribuir valores apropriados a elas.

Embora eu possa ver o sentido de um contract , que requer um único parâmetro de tipo para ter certos métodos, a ser representado por um interface (é algo que eu até defendi no meu próprio passado propostas), sinto agora que seria um movimento infeliz porque novamente turvaria as águas entre contracts e interfaces .

Não havia realmente me ocorrido antes que contracts pudesse ser declarado plausivelmente da maneira que @bigwhite sugeriu usar o padrão 'tipo' existente. No entanto, novamente, não estou interessado na ideia porque sinto que comprometeria (3) acima. Além disso, se for necessário (por motivos de análise) repetir a palavra-chave type ao declarar uma estrutura genérica assim:

type List(type Element) struct {
    next *List(Element)
    val  Element
}

presumivelmente, também seria necessário repeti-lo se contracts fossem declarados de maneira semelhante, o que é um pouco 'gagueira' em comparação com a abordagem de projeto de rascunho.

Outra idéia que não me interessa é 'literais de contrato' que permitiriam que contracts fosse escrito 'no lugar' ao invés de construções separadas. Isso tornaria as definições genéricas de funções e tipos mais difíceis de ler e, como algumas pessoas pensam que já são, não vai ajudar a persuadir essas pessoas de que os genéricos são uma coisa boa.

Desculpe parecer tão resistente às mudanças propostas para o rascunho de genéricos (que reconhecidamente tem alguns problemas), mas, como um defensor entusiasmado de genéricos simples para Go, acho que vale a pena fazer esses pontos.

Eu gostaria de sugerir não chamar predicados sobre tipos de "contratos". Existem duas razões:

  • O termo "contratos" já é usado na ciência da computação de uma forma diferente. Por exemplo, consulte: (https://scholar.google.com/scholar?hl=en&as_sdt=0%2C33&q=contracts+languages&btnG=)
  • Já existem vários nomes para essa ideia na literatura da ciência da computação. Conheço pelo menos ~três~ quatro: "typesets", "type classes", "concepts" e "constraints". Adicionar outro só vai confundir ainda mais as coisas.

@griesemer "restrições nos tipos" são uma coisa puramente de tempo de compilação, porque os tipos são apagados antes do tempo de execução. As restrições fazem com que o código genérico seja elaborado em código não genérico que pode ser executado. Os subtipos existem em tempo de execução e não são restrições no sentido de que uma restrição em tipos seria no mínimo de igualdade ou desigualdade de tipo, com restrições como 'é um subtipo de' opcionalmente disponíveis dependendo do sistema de tipos.

Para mim, a natureza de tempo de execução dos subtipos é a diferença crítica, se X <: Y, podemos passar X onde Y é esperado, mas só conhecemos o tipo como Y sem operações de tempo de execução inseguras. Nesse sentido, não restringe o tipo Y, Y é sempre Y. A subtipagem também é 'direcional', portanto, pode ser covariante ou contravariante, dependendo de ser aplicada a um argumento de entrada ou saída.

Com uma restrição de tipo 'pred(X)', começamos com um X totalmente polimórfico e, em seguida, restringimos os valores permitidos. Então diga apenas X que implementa 'print'. Isso é não direcional e, portanto, não tem covariância ou contravariância. É de fato invariante, pois conhecemos o tipo de base de X em tempo de compilação.

Portanto, acho perigoso pensar em interfaces como restrições de tipos, pois ignora diferenças importantes como covariância e contravariância.

Isso responde sua pergunta ou eu perdi o ponto?

Edit: Devo salientar que estou me referindo às interfaces 'Go' especificamente acima. Os pontos sobre a subtipagem se aplicam a todas as linguagens que possuem subtipos, mas Go é incomum ao tornar as interfaces um tipo e, portanto, ter um relacionamento de subtipagem. Em outras linguagens como Java, uma interface não é explicitamente um tipo (uma classe é um tipo) então interfaces _são_ uma restrição de tipos. Portanto, embora seja correto em geral considerar as interfaces como restrições aos tipos, é errado especificamente para 'Go'.

@Inuart É muito cedo para dizer como isso seria adicionado à implementação. Ainda não há proposta, apenas um rascunho de design. Certamente não será em 1.14.

@andrewcmyers Gosto da palavra "contrato" porque descreve uma relação entre o escritor da função genérica e seu chamador.

Palavras como "typesets" e "type classes" sugerem que estamos falando de um meta-tipo, o que é claro que somos, mas os contratos também descrevem uma relação entre vários tipos. Eu sei que as classes de tipo em, por exemplo, Haskell, podem ter vários parâmetros de tipo, mas parece-me que o nome não se ajusta muito bem à ideia que está sendo descrita.

Eu nunca entendi por que C++ chama isso de "conceito". Afinal, o que isso quer dizer?

"Restrição" ou "restrições" estariam bem para mim. No momento, penso em um contrato como contendo várias restrições. Mas poderíamos mudar esse pensamento.

Não estou muito preocupado com o fato de que existe uma construção de linguagem de programação existente chamada "contrato". Considero essa ideia relativamente semelhante à ideia que queremos expressar, pois é uma relação entre uma função e seus chamadores. Entendo que a forma como essa relação é expressa é bem diferente, mas sinto que há uma semelhança subjacente.

Eu nunca entendi por que C++ chama isso de "conceito". Afinal, o que isso quer dizer?

Um conceito é uma abstração de instanciações que compartilham alguma semelhança, por exemplo, assinaturas.

O termo conceito é de longe um ajuste melhor para interfaces, pois este último também é usado para denotar um limite compartilhado entre dois componentes.

@sighoya Eu também ia mencionar que 'conceitos' são conceituais porque incluem 'axiomas' que são vitais para evitar abusos de operadores. Por exemplo, a adição '+' deve ser associativa e comutativa. Esses axiomas não podem ser representados em C++, portanto, eles existem como idéias abstratas, portanto, 'conceitos'. Assim, um conceito é o 'contrato' sintático mais os axiomas semânticos.

@ianlancetaylor "Restrição" é como chamamos em Gênero (http://www.cs.cornell.edu/~yizhou/papers/genus-pldi2015.pdf), então sou parcial a essa terminologia. O termo "contrato" seria uma escolha completamente razoável, exceto que está em uso muito ativo na comunidade PL para se referir à relação entre interfaces e implementações, que também tem um sabor contratual.

@keean Sem ser um especialista, não acho que a dicotomia que você está pintando esteja refletindo muito bem a realidade. Por exemplo, se o compilador gera versões instanciadas de funções genéricas é inteiramente uma questão de implementação, então é perfeitamente razoável ter uma representação de restrições em tempo de execução, digamos na forma de uma tabela de ponteiros de função para cada operação necessária. Exatamente como as tabelas de métodos de interface, na verdade. Da mesma forma, as interfaces em Go não se ajustam à sua definição de subtipo, porque você pode projetá-las de volta com segurança (via asserções de tipo) e porque você não tem co nem contravariância para nenhum construtor de tipo em Go.

Por último: Seja ou não a dicotomia que você está pintando é realista e precisa, não muda que uma interface seja, no final das contas, apenas uma lista de métodos - e mesmo em sua dicotomia, não há razão para que essa lista possa não pode ser reutilizado como uma tabela representada em tempo de execução ou uma restrição somente em tempo de compilação, dependendo do contexto em que é usado.

Que tal algo como:

tipoRestrição C(T) {
}

ou

tipoContrato C(T) {
}

É diferente de outras declarações de tipo enfatizar que isso não é uma construção de tempo de execução.

Sobre o novo desenho do contrato, tenho algumas dúvidas.

1.

Quando um tipo genérico A incorpora outro tipo genérico B,
ou uma função genérica A chama outra função genérica B,
precisamos também especificar os contratos de B em A?

Se a resposta for verdadeira, se um tipo genérico incorporar muitos outros tipos genéricos,
ou uma função genérica chama muitas outras funções genéricas,
então precisamos combinar muitos contratos em um como o contrato do tipo de incorporação ou função do chamador.
Isso pode causar o problema de envenenamento constante.

  1. Além das restrições atuais do tipo e do conjunto de métodos, precisamos de outras restrições?
    Como conversível de um tipo para outro, atribuível de um tipo para outro,
    comparável entre dois tipos, é um canal de envio, é um canal de recepção,
    tem um conjunto de campos especificado, ...

3.

Se uma função genérica usa uma linha como a seguinte

v.Foo()

Como podemos escrever um contrato que permite que Foo seja um método ou um campo de um tipo de função?

As restrições de tipo @merovius devem ser resolvidas em tempo de compilação, ou o sistema de tipos pode não ser sólido. Isso ocorre porque você pode ter um tipo que depende de outro que não é conhecido até o tempo de execução. Você então tem duas opções: implementar um sistema de tipos totalmente dependente (que permite que a verificação de tipos ocorra em tempo de execução à medida que os tipos se tornam conhecidos) ou adicionar tipos existenciais ao sistema de tipos. Existenciais codificam a diferença de fase de tipos conhecidos estaticamente e tipos que são conhecidos apenas em tempo de execução (tipos que dependem da leitura de IO, por exemplo).

Os subtipos, conforme mencionado acima, normalmente não são conhecidos até o tempo de execução, embora muitas linguagens tenham otimizações no caso de o tipo ser conhecido estaticamente.

Se assumirmos que uma das alterações acima foi introduzida na linguagem (tipos dependentes ou tipos existenciais), ainda precisamos separar os conceitos de subtipagem e restrições de tipo. Para Go especificamente os constritores de tipo são invariáveis, podemos ignorar essas diferenças e podemos considerar que as interfaces Go _são_ restrições nos tipos (estaticamente).

Podemos, portanto, considerar uma interface Go como um contrato de parâmetro único em que o parâmetro é o receptor de todas as funções/métodos. Então, por que go tem interfaces e contratos? Parece-me ser porque Go não quer permitir interfaces para operadores (como '+'), e porque Go não tem tipos dependentes nem tipos existenciais.

Portanto, há dois fatores que criam uma diferença real entre restrições de tipo e subtipagem. Uma é a co/contra-variância, que podemos ignorar em Go devido à invariância do construtor de tipo, e a outra é a necessidade de tipos dependentes ou tipos existenciais para fazer um sistema de tipos que tenha restrições de tipo som se há polimorfismo de tempo de execução dos parâmetros de tipo para as restrições de tipo.

@keean Legal, então AIUI estamos pelo menos de acordo que as interfaces em Go podem ser consideradas restrições :)

Em relação ao resto: Acima você afirmou:

"restrições em tipos" são uma coisa puramente de tempo de compilação, porque os tipos são apagados antes do tempo de execução. As restrições fazem com que o código genérico seja elaborado em código não genérico que pode ser executado.

Essa afirmação é mais específica do que a mais recente, que as restrições precisam ser resolvidas em tempo de compilação. Tudo o que eu estava tentando dizer é que o compilador pode fazer essa resolução (e todas as mesmas verificações de tipo), mas ainda gerar código genérico. Ainda seria bom, porque a semântica do sistema de tipos é a mesma. Mas as restrições ainda teriam uma representação em tempo de execução. Isso é meio minucioso - mas é por isso que sinto que defini-los com base em tempo de execução versus tempo de compilação não é a melhor maneira de fazer isso. Está misturando preocupações de implementação em uma discussão sobre a semântica abstrata de um sistema de tipos.

FWIW, eu argumentei antes que eu preferiria usar interfaces para expressar restrições - e também cheguei à conclusão de que permitir o uso de operadores em código genérico é o principal obstáculo para fazer isso e, portanto, a principal razão para introduzir um conceito na forma de contratos.

@keean Obrigado, mas não, sua resposta não respondeu à minha pergunta. Observe que no meu comentário descrevi um exemplo muito simples de usar uma interface no lugar de um contrato/"restrição" correspondente. Eu pedi um exemplo _simples_ _concrete_ porque esse cenário não funcionaria "sem algumas restrições" como você mencionou em seu comentário anterior. Você não forneceu tal exemplo.

Observe que eu não mencionei subtipos, co- ou contra-variância (que não permitimos em Go de qualquer maneira, as assinaturas devem sempre corresponder), etc. parâmetro de tipo, etc.) para explicar o que quero dizer com "restrição", porque essa é a linguagem comum que todos aqui entendem e, portanto, todos podem acompanhar. (Além disso, ao contrário da sua afirmação aqui , em Java, uma interface parece um tipo para mim de acordo com a especificação Java : "Uma declaração de interface especifica um novo tipo de referência nomeado". Se isso não diz que uma interface é um tipo, então o pessoal do Java Spec tem algum trabalho a fazer.)

Mas parece que você respondeu minha pergunta indiretamente com seu último comentário , como @Merovius já observou, quando você diz: "Podemos, portanto, considerar uma interface Go como um contrato de parâmetro único, onde o parâmetro é o receptor de todas as funções/métodos .". Este é exatamente o ponto que eu estava fazendo no começo, então obrigado por confirmar o que eu disse o tempo todo.

@dotaheor

Quando um tipo genérico A incorpora outro tipo genérico B, ou uma função genérica A chama outra função genérica B, precisamos também especificar os contratos de B em A?

Se um tipo genérico A incorpora outro tipo genérico B, então os parâmetros de tipo passados ​​para B devem satisfazer qualquer contrato usado por B. Para isso, o contrato usado por A deve implicar o contrato usado por B. Ou seja, todas as restrições nos parâmetros de tipo passados ​​para B devem ser expressos no contrato usado por A. Isso também se aplica quando uma função genérica chama outra função genérica.

Se a resposta for verdadeira, então se um tipo genérico incorpora muitos outros tipos genéricos, ou uma função genérica chama muitas outras funções genéricas, então precisamos combinar muitos contratos em um como o contrato do tipo de incorporação ou função de chamador. Isso pode causar o problema de envenenamento constante.

Acho que o que você diz é verdade, mas não é o problema de envenenamento por const. O problema do const-envenenamento é que você tem que espalhar const em todos os lugares em que um argumento é passado, e então se você descobrir algum lugar onde o argumento tem que ser alterado você tem que remover const em todos os lugares. O caso dos genéricos é mais parecido com "se você chamar várias funções, terá que passar valores do tipo correto para cada uma dessas funções".

De qualquer forma, parece-me extremamente improvável que as pessoas escrevam funções genéricas que chamem muitas outras funções genéricas que usam contratos diferentes. Como isso aconteceria naturalmente?

Além das restrições atuais do tipo e do conjunto de métodos, precisamos de outras restrições? Como conversível de um tipo para outro, atribuível de um tipo para outro, comparável entre dois tipos, é um canal de envio, é um canal de recepção, tem um conjunto de campos especificado, ...

Restrições como conversibilidade e designabilidade e comparabilidade são expressas na forma de tipos, como explica o rascunho do projeto. Restrições como canal de envio ou recebimento só podem ser expressas na forma de chan T onde T é algum parâmetro de tipo, como explica o rascunho do projeto. Não há como expressar a restrição de que um tipo tenha um conjunto de campos especificado, mas duvido que isso ocorra com muita frequência. Teremos que ver como isso funciona escrevendo código real para ver o que acontece.

Se uma função genérica usa uma linha como a seguinte

v.Foo()
Como podemos escrever um contrato que permite que Foo seja um método ou um campo de um tipo de função?

No rascunho de design atual, você não pode. Isso parece um caso de uso importante? (Eu sei que o rascunho de design anterior apoiou isso.)

@griesemer você perdeu o ponto em que eu disse que isso só era válido se você introduzisse tipos dependentes ou tipos existenciais no sistema de tipos.

Caso contrário, se você usar um contrato como uma interface, poderá falhar em tempo de execução, pois precisará adiar a verificação de tipo até conhecer os tipos, e a verificação de tipo poderá falhar, o que, portanto, não é seguro para tipos.

Eu também vi interfaces explicadas como subtipos, então você deve ter cuidado para que alguém não tente introduzir co/contra-variância em construtores de tipo no futuro. Melhor não ter interfaces como tipos, então não há possibilidade disso, e as intenções dos designers, de que não são subtipos, são claras.

Para mim, seria um design melhor mesclar interfaces e contratos e torná-los explicitamente restrições de tipo (predicados em tipos).

@ianlancetaylor

De qualquer forma, parece-me extremamente improvável que as pessoas escrevam funções genéricas que chamem muitas outras funções genéricas que usam contratos diferentes. Como isso aconteceria naturalmente?

Por que isso seria incomum? Se eu definir uma função no tipo 'T', vou querer chamar funções em 'T'. Por exemplo, se eu definir uma função de 'soma' sobre 'tipos adicionáveis' por contrato. Agora eu quero construir uma função de multiplicação genérica que chama sum? Muitas coisas na programação têm uma estrutura de soma/produto (qualquer coisa que seja um 'grupo').

Eu não entendo qual será o propósito da interface depois que os contratos estiverem na linguagem, parece que os contratos servirão para o mesmo propósito, para garantir que um tipo tenha um conjunto de métodos definidos nele.

@keean O caso incomum são funções que chamam muitas outras funções genéricas que usam contratos diferentes . Seu contra-exemplo está chamando apenas uma função. Lembre-se de que estou argumentando contra a semelhança com o envenenamento por const.

@mrkaspa A maneira mais simples de pensar é que os contratos são como funções de modelo C++ e as interfaces são como métodos virtuais C++. Há um uso e propósito para ambos.

@ianlancetaylor , por experiência, existem dois problemas que ocorrem semelhantes ao envenenamento const. Ambos ocorrem devido à natureza de árvore das chamadas de função aninhadas. A primeira é quando você deseja adicionar depuração a uma função profundamente aninhada, é necessário adicionar imprimível da folha até a raiz, o que pode envolver tocar em várias bibliotecas de terceiros. A segunda é que você pode acumular um grande número de contratos na raiz, dificultando a leitura das assinaturas de funções. Geralmente é melhor fazer com que o compilador infira as restrições como Haskell faz com classes de tipo para evitar esses dois problemas.

@ianlancetaylor Não sei muito sobre c++, quais serão os casos de uso para interfaces e contratos em golang? quando devo usar interface ou contrato?

@keean Este subthread é sobre um rascunho de design específico para a linguagem Go. Em Go todos os valores são imprimíveis. Não é algo que precisa ser expresso em um contrato. E embora eu esteja disposto a ver evidências de que muitos contratos podem se acumular para uma única função ou tipo genérico, não estou disposto a aceitar uma afirmação de que isso acontecerá. O objetivo do rascunho do design é tentar escrever um código real que o use.

O rascunho do design explica da maneira mais clara possível por que acho que inferir as restrições é uma escolha ruim para uma linguagem como Go, projetada para programação em escala.

@mrkaspa Por exemplo, se você tem um []io.Reader , então você quer um valor de interface, não um contrato. Um contrato exigiria que todos os elementos na fatia fossem do mesmo tipo. Uma interface permitirá que sejam de tipos diferentes, desde que todos os tipos implementem io.Reader .

@ianlancetaylor , na medida em que a interface, cria um novo tipo, enquanto os contratos restringem um tipo, mas não cria um novo, estou certo?

@ianlancetaylor :

Você não poderia fazer algo como o seguinte?

contract Reader(T) {
  T Read([]byte) (int, error)
}

func ReadAll(type T Reader)(readers []T) ([]byte, error) {
  // Use the readers...
}

Agora ReadAll() deveria aceitar um []io.Reader tão bem quanto aceitaria um []*os.File , não é? io.Reader parece satisfazer o contrato, e não me lembro de nada no rascunho sobre valores de interface que não podem ser usados ​​como argumentos de tipo.

Editado: Não importa. Eu entendi errado. Este ainda é um lugar onde você usaria uma interface, então é uma resposta à pergunta de @mrkaspa . Você simplesmente não está usando a interface na assinatura da função; você só está usando onde ele é chamado.

@mrkaspa Sim, isso é verdade.

@ianlancetaylor se eu tivesse uma lista de []io.Reader e este contrato:

contract Reader(T) {
  T Read([]byte) (int, error)
}

func ReadAll(type T Reader)(readers []T) ([]byte, error) {
  // Use the readers...
}

Eu poderia chamar ReadAll em cada interface porque eles satisfazem o contrato?

@ianlancetaylor com certeza as coisas são imprimíveis, mas é fácil encontrar outros exemplos, por exemplo, log em arquivo ou rede, queremos que o log seja genérico para que possamos alterar o destino do log entre nulo, arquivo local, serviço de rede etc. Adicionando o registro em uma função folha requer a adição de restrições até o total da raiz, incluindo a modificação de bibliotecas de terceiros usadas.

O código não é estático, você também deve permitir a manutenção. Na verdade, o código está em 'manutenção' por muito mais tempo do que leva para escrever inicialmente, então há um bom argumento de que devemos projetar linguagens para facilitar a manutenção, refatoração, adição de recursos etc.

Realmente esses problemas só se manifestarão em uma grande base de código, que é mantida ao longo do tempo. Não é algo que você possa escrever um pequeno exemplo rápido para demonstrar.

Esses problemas também existem em outras linguagens genéricas, por exemplo, Ada. Você poderia portar algum aplicativo Ada grande que Go que faz uso extensivo de genéricos, mas se o problema existe em Ada, não vejo nada em Go que mitigaria esse problema.

@mrkaspa Sim.

Neste ponto, sugiro que este tópico de conversa mude para golang-nuts. O rastreador de problemas do GitHub é um lugar ruim para esse tipo de discussão.

@keean Talvez você esteja certo. O tempo vai dizer. Estamos pedindo explicitamente às pessoas que tentem escrever código no rascunho do design. Há pouco valor em discussões puramente hipotéticas.

@keean Não entendo seu exemplo de registro. O problema que você descreve é ​​algo que você pode resolver com interfaces em tempo de execução, não com genéricos em tempo de compilação.

As interfaces @bserdar têm apenas um parâmetro de tipo, portanto, você não pode fazer algo em que um parâmetro é a coisa a ser registrada e um segundo parâmetro de tipo é o tipo do log.

@keean IMO nesse exemplo, você faria a mesma coisa que está fazendo hoje, sem nenhum parâmetro de tipo: Use reflexão para inspecionar a coisa a ser registrada e use context.Context para passar o valor do log. Eu sei que essas ideias são repulsivas para os entusiastas da digitação, mas acabam sendo bastante práticas. É claro que há valor em parâmetros de tipo restritos, e é por isso que estamos tendo essa conversa - mas eu diria que a razão pela qual os casos que vêm à sua mente são os casos que já funcionam muito bem nas bases de código Go atuais em escala , são que esses não são os casos que realmente se beneficiam da verificação de tipo estrita adicional. O que volta ao ponto de Ians - resta saber se esse é um problema que se manifesta na prática.

@merovius Se dependesse de mim, toda a reflexão em tempo de execução seria banida, pois não quero que o software enviado gere erros de digitação em tempo de execução que possam afetar o usuário. Isso permite otimizações de compilador mais agressivas porque você não precisa se preocupar com o alinhamento do modelo de tempo de execução com o modelo estático.

Tendo lidado com a migração de grandes projetos em escala de JavaScript para TypeScript, na minha experiência, a tipagem estrita se torna mais importante quanto maior o projeto e quanto maior a equipe trabalhando nele. Isso ocorre porque você precisa confiar na interface/contrato de um bloco de código sem ter que olhar para a implementação para manter a eficiência ao trabalhar com uma equipe grande.

A parte: claro que depende de como você alcança a escala, agora eu prefiro uma abordagem API-First, começando com um arquivo JSON OpenAPI/Swagger e depois usando geração de código para construir os stubs do servidor e o SDK do cliente. Como tal, o OpenAPI está realmente agindo como seu sistema de tipos para microsserviços.

@ianlancetaylor

Restrições como conversibilidade e designabilidade e comparabilidade são expressas na forma de tipos

Considerando que há tantos detalhes nas regras de conversão do tipo Go, é muito difícil escrever um contrato personalizado C para satisfazer a seguinte função geral de conversão de fatias:

func ConvertSlice(type In, Out C(In, Out)) (x []In) []Out {
    o := make([]Out, len(x))
    for i := range x {
        o[i] = Out(x[i])
    }
    return o
}

Um C perfeito deve permitir conversões:

  • entre quaisquer tipos numéricos inteiros e de ponto flutuante
  • entre quaisquer tipos numéricos complexos
  • entre dois tipos cujos tipos subjacentes são idênticos
  • de um tipo Out que implementa In
  • de um tipo de canal para um tipo de canal bidirecional e os dois tipos de canal têm um tipo de elemento idêntico
  • struct tag relacionado, ...
  • ...

Pelo meu entendimento, não posso escrever tal contrato. Então, precisamos de um contrato convertible embutido?

Não há como expressar a restrição de que um tipo tenha um conjunto de campos especificado, mas duvido que isso apareça com muita frequência

Considerando que a incorporação de tipos é usada frequentemente na programação Go, acho que as necessidades não seriam raras.

@keean Essa é uma opinião válida, mas obviamente não é a que orienta o design e o desenvolvimento do Go. Para participar de forma construtiva, por favor, aceite isso e comece a trabalhar de onde estamos e sob a suposição de que qualquer desenvolvimento da linguagem deve ser uma mudança gradual do status quo. Se você não puder, então existem idiomas que estão mais alinhados com suas preferências e eu sinto que todos - você em particular - ficariam mais felizes se você contribuísse com sua energia para lá.

@merovius Estou preparado para aceitar que as mudanças no Go devem ser graduais e aceitar o status quo.

Eu estava apenas respondendo ao seu comentário como parte de uma conversa, concordando que sou um entusiasta da digitação. Afirmei uma opinião sobre reflexão de tempo de execução, não sugeri que Go abandonasse a reflexão de tempo de execução. Eu trabalho em outros idiomas, uso muitos idiomas no meu trabalho. Estou desenvolvendo (lentamente) minha própria linguagem, mas estou sempre esperançoso que desenvolvimentos para outras linguagens tornarão isso desnecessário.

@dotaheor Concordo que não podemos escrever um contrato geral de conversibilidade hoje. Teremos que ver se isso parece ser um problema na prática.

Respondendo a @ianlancetaylor

Eu não acho que esteja claro ainda com que frequência as pessoas vão querer funções parametrizadas em valores constantes. O caso mais óbvio seria para dimensões de array - mas você já pode fazer isso passando o tipo de array desejado como um argumento de tipo. Fora esse caso, o que realmente ganhamos passando um const como um argumento de tempo de compilação em vez de um argumento de tempo de execução?

No caso de arrays, apenas passar o tipo de array (inteiro) como um argumento de tipo parece ser extremamente limitante, porque o contrato não seria capaz de decompor a dimensão do array ou o tipo de elemento e impor restrições neles. Por exemplo, um contrato com um "tipo de array inteiro" poderia exigir que o tipo de elemento do tipo de array implementasse determinados métodos?

Mas seu pedido de exemplos mais específicos de como parâmetros genéricos não-tipo seriam úteis foi bem aceito, então expandi a postagem do blog para incluir uma seção cobrindo algumas classes significativas de exemplos de casos de uso e alguns exemplos específicos de cada um. Já que se passaram alguns dias, novamente o post do blog está aqui:

Apenas os parâmetros de tipo são genéricos o suficiente para genéricos Go 2?

A nova seção é intitulada "Exemplos de como os genéricos sobre os não-tipos são úteis".

Como um resumo rápido, os contratos para operações de matrizes e vetoriais podem impor restrições apropriadas tanto na dimensionalidade quanto nos tipos de elementos de matrizes. Por exemplo, a multiplicação de matrizes de uma matriz nxm com uma matriz mxp, cada uma representada como uma matriz bidimensional, pode restringir corretamente o número de linhas da primeira matriz para igualar o número de colunas da segunda matriz, etc.

De maneira mais geral, os genéricos podem usar parâmetros que não sejam de tipo para habilitar a configuração em tempo de compilação e a especialização de código e algoritmos de várias maneiras. Por exemplo, uma variante genérica de math/big.Int pode ser configurável em tempo de compilação para um bit específico com e/ou assinatura, satisfazendo demandas de inteiros de 128 bits e outros inteiros de largura fixa não nativos com eficiência razoável provavelmente muito melhor do que o big.Int existente onde tudo é dinâmico. Uma variante genérica de big.Float também pode ser especializada em tempo de compilação para uma precisão específica e/ou outros parâmetros de tempo de compilação, por exemplo, para fornecer implementações genéricas razoavelmente eficientes dos formatos binary16, binary128 e binary256 do IEEE 754-2008 que o Go não suporta nativamente. Muitos algoritmos de biblioteca que podem otimizar sua operação com base no conhecimento das necessidades do usuário ou aspectos particulares dos dados que estão sendo processados ​​- por exemplo, otimizações de algoritmo de gráfico que funcionam apenas em pesos de borda não negativos ou apenas em DAGs ou árvores, ou otimizações de processamento de matriz que contar com matrizes sendo triangulares superior ou inferior, ou aritmética de inteiro grande para criptografia, às vezes precisando ser implementada em tempo constante e às vezes não - poderia usar genéricos para se tornar configurável em tempo de compilação para depender de informações declarativas opcionais, como isso, garantindo que todos os testes dessas opções de tempo de compilação na implementação normalmente sejam compilados por meio de propagação constante.

@bford escreveu:

ou seja, que os parâmetros para genéricos são vinculados a constantes em tempo de compilação.

Este é o ponto que eu não entendo. Por que você precisa dessa condição.
Teoricamente, pode-se redefinir variáveis/parâmetros no corpo. Não importa.
Intuitivamente, suponho que você queira declarar que a primeira aplicação de função deve ocorrer em tempo de compilação.

Mas para esse requisito, uma palavra-chave como comp ou comptime seria mais adequada.
Além disso, se a gramática de golang permitir apenas duas tuplas de parâmetro no máximo para uma função, essa anotação de palavra-chave pode ser deixada de fora porque a primeira tupla de parâmetro de um tipo e de uma função (no caso de duas tuplas de parâmetro) sempre será avaliada em tempo de compilação.

Outro ponto: E se const for estendido para permitir expressões de tempo de execução (true single sign on)?

Em métodos ponteiro vs valor :

Se um método estiver listado em um contrato com um T simples em vez de *T , então pode ser um método de ponteiro ou um método de valor de T . Para evitar se preocupar com essa distinção, em um corpo de função genérico, todas as chamadas de método serão chamadas de método de ponteiro. ...

Como isso se encaixa com a implementação da interface? Se um T tiver algum método de ponteiro (como o MyInt no exemplo), T pode ser atribuído à interface com esse método ( Stringer no exemplo)?

Permitir significa ter outra operação de endereço oculto & , não permitir significa que contratos e interfaces só podem interagir via switch de tipo explícito. Nenhuma das soluções me parece boa.

(Observação: devemos rever essa decisão se ela levar a confusão ou código incorreto.)

Vejo que a equipe já tem algumas ressalvas sobre essa ambiguidade na sintaxe do método do ponteiro. Estou apenas acrescentando que a ambiguidade também afeta a implementação da interface (e também adicionando implicitamente minhas reservas sobre isso).

@fJavierZunzunegui Você está certo, o texto atual implica que, ao atribuir um valor de um parâmetro de tipo a um tipo de interface, uma operação de endereço implícita pode ser necessária. Esse pode ser outro motivo para não usar endereços implícitos ao invocar métodos. Teremos que ver.

Em tipos parametrizados , particularmente em relação aos parâmetros de tipo incorporados como um campo em uma estrutura:

Considerar

type Lockable(type T) struct {
    T
    sync.Locker
}

E se T tivesse um método chamado Lock ou Unlock ? A estrutura não compilaria. Isso não ter uma condição do método X não é suportado por contratos, portanto, temos um código inválido que não quebra o contrato (derrotando todo o propósito dos contratos).

Fica ainda mais complicado se você tiver vários parâmetros incorporados (digamos T1 e T2 ), pois eles não devem ter métodos comuns (novamente, não impostos por contratos). Além disso, o suporte a métodos arbitrários, dependendo dos tipos incorporados, contribui para restrições de tempo de compilação muito limitadas nas opções de tipo para essas estruturas (de maneira muito semelhante às declarações e opções de tipo ).

A meu ver, existem 2 boas alternativas:

  • proibindo a incorporação de parâmetros de tipo completamente: simples, mas com um custo pequeno (se o método for necessário, deve-se escrevê-lo explicitamente na estrutura com o campo).
  • restrinja os métodos que podem ser chamados aos do contrato: semelhante à incorporação de uma interface. Isso se desvia do go normal (um não-objetivo), mas sem custo (os métodos não precisam ser escritos explicitamente na estrutura com o campo).

A estrutura não compilaria.

Ele iria compilar. Tente. O que falha ao compilar é uma chamada para o método ambíguo. Seu ponto ainda é válido, no entanto.

Sua segunda solução, restringindo métodos que podem ser chamados aos mencionados no contrato, não funcionará: mesmo se o contrato em T especificar Lock e Unlock , você ainda não poderia t chamá-los em um Lockable .

@jba obrigado pelos insights sobre a compilação.

Com a segunda solução, quero dizer tratar os parâmetros de tipo incorporados como fazemos com as interfaces agora, de modo que, se o método não estiver no contrato, ele não estará imediatamente acessível após a incorporação. Nesse cenário, como T não tem contrato, ele é efetivamente tratado como interface{} , portanto, não entraria em conflito com o sync.Locker mesmo se T fosse instanciado com um tipo com esses métodos. Isso pode ajudar a explicar meu ponto .

De qualquer forma, eu prefiro a primeira solução (proibindo completamente a incorporação), então se essa for sua preferência, não há motivo para discutir a segunda! :risonho:

O exemplo fornecido por @JavierZunzunegui também abrange outro caso. E se T for uma estrutura que tenha um campo noCopy noCopy ? O compilador deve ser capaz de lidar com esse caso também.

Não tenho certeza se este é exatamente o lugar certo para isso, mas eu queria comentar com um caso de uso concreto do mundo real para tipos genéricos que permitem "parametrização em valores que não são de tipo, como constantes", e especificamente para o caso de matrizes . Espero que isto seja útil.

No meu mundo sem genéricos, escrevo muito código que se parece com isso:

import "math/bits"

// SigEl is the element type used in variable length bit vectors, 
// can be any unsigned integer type
type SigEl = uint

// SigElBits is the number of bits storable in each SigEl
const SigElBits = 8 << uint((^SigEl(0)>>32&1)+(^SigEl(0)>>16&1)+(^SigEl(0)>>8&1))

// HammingDist counts the number bitwise differences between two
// bit vectors b1 and b2. I want this to be generic
// Function will panic at runtime if b1 and b2 aren't of equal length.
func HammingDist(b1, b2 []SigEl) (sum int) {
    // Give the compiler a hint so it won't need to bounds check the slices in loops
    _ = b1[len(b2)-1]  
        // This switch is optimized away because SigElBits is const
    switch SigElBits {   // Yay no golang generics!
    case 64:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount64(uint64(b1[x] ^ b2[x]))
        }
    case 32:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount32(uint32(b1[x] ^ b2[x]))
        }
    case 16:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount16(uint16(b1[x] ^ b2[x]))
        }
    case 8:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount8(uint8(b1[x] ^ b2[x]))
        }
    }
    return sum
}

Isso funciona bem o suficiente, com uma ruga. Muitas vezes preciso de centenas de milhões de []SigEl s, e seu comprimento é geralmente de 128-384 bits no total. Como as fatias impõem uma sobrecarga fixa de 192 bits sobre o tamanho da matriz subjacente, quando a matriz tem 384 bits ou menos, isso impõe uma sobrecarga de memória desnecessária de 50 a 150%, o que é obviamente terrível.

Minha solução é alocar uma fatia de Sig _arrays_ e, em seguida, cortá-los em tempo real como os parâmetros para HammingDist acima:

const SigBits = 256  // Any multiple of SigElBits is valid

// Sig is the bit vector array type
type Sig [SigBits/SigElBits]SigEl

bitVects := make([]Sig, 100000000)
// stuff happens ... 

// Note slicing below, just to make the arrays "generic" for the call 
dist := HammingDist(bitVects[x][:], bitVects[y][:])

O que eu gostaria de poder fazer em vez de tudo isso é definir um tipo de assinatura genérico e reescrever todos os itens acima como (algo como):

contract UnsignedInteger(T) {
    T uint, uint8, uint16, uint32, uint64
}

type Signature (type Element UnsignedInteger, n int) [n]Element

// HammingDist counts the number bitwise differences between two bit vectors
func HammingDist(b1, b2 *Signature) (sum int) {
    for x := range *b1 {
        // Assuming the std lib bits.OnesCount becomes generic over 
        // all UnsignedInteger types
        sum += bits.OnesCount(*b1[x] ^ *b2[x])
    }
    return sum
}

Então, para usar esta biblioteca:

type sigEl = uint   // Any unsigned int type
const sigElBits = 8 << uint((^SigEl(0)>>32&1)+(^SigEl(0)>>16&1)+(^SigEl(0)>>8&1))
const sigBits = 256  // Any multiple of SigElBits is valid
type sig Signature(sigEl, sigBits/sigElBits)

bitVects := make([]sig, 100000000)
// stuff happens ... 

dist := HammingDist(&bitVects[x], &bitVects[y])

Um engenheiro pode sonhar... 🤖

Se você sabe quão grande pode ser o comprimento máximo de bits, você pode usar algo assim:

contract uintArrayOfFixedLength(ElemType,ArrayType)
{
    ArrayType [1]ElemType,[2]ElemType,...,[maxBit]ElemType
    ElemType uint8,uint16,uint32,uint64
}

func HammingDist(type ElemType,ArrayType uintArrayOfFixedLength)(t1,t2 ArrayType) (sum int)
{

}

@vsivsi Não tenho certeza se entendo como você acha que isso melhorará as coisas - você está assumindo que o compilador geraria uma versão instanciada dessa função para cada tamanho de matriz possível? Porque ISTM que a) isso não é muito provável, então b) você acabaria com exatamente as mesmas características de desempenho que você tem agora. A implementação mais provável, IMO, ainda seria que o compilador passasse o comprimento e um ponteiro para o primeiro elemento, então você efetivamente ainda passaria uma fatia, no código gerado (quer dizer, você não passaria a capacidade, mas não acho que uma palavra adicional na pilha realmente importe).

Honestamente, IMO o que você está dizendo é um bom exemplo de uso excessivo de genéricos, onde eles não são necessários - "uma matriz de comprimento indeterminado" é exatamente para que servem as fatias.

@Merovius Obrigado, acho que seu comentário revela alguns pontos de discussão interessantes.

"uma matriz de comprimento indeterminado" é exatamente para que servem as fatias.

Certo, mas no meu exemplo não há arrays de comprimento indeterminado. O comprimento da matriz é uma constante conhecida em _tempo de compilação_. É exatamente para isso que servem os arrays, mas eles são subutilizados no golang IMO porque são muito inflexíveis.

Para ser claro, não estou sugerindo

type Signature (type Element UnsignedInteger, n int) [n]Element

significa que n é uma variável de tempo de execução. Ainda deve ser uma constante no mesmo sentido de hoje:

const n = 10
type nArray [n]uint               // works
type nSigInt Signature(uint, n)   // works 

var m = int(n)
type mArray [m]uint               // error
type mSigInt Signature(uint, m)   // error 

Então vamos ver o "custo" da função HammingDist baseada em fatias. Concordo que a diferença entre passar uma matriz como bitVects[x][:] vs &bitVects[x] é pequena (-ish, um fator de 3 no máximo). A diferença real está no código e na verificação de tempo de execução que precisa acontecer dentro dessa função.

Na versão baseada em fatias, o código de tempo de execução precisa verificar os limites dos acessos de fatias para garantir a segurança da memória. Isso significa que esta versão do código pode entrar em pânico (ou um mecanismo explícito de verificação e retorno de erros é necessário para evitar isso). As atribuições NOP ( _ = b1[len(b2)-1] ) fazem uma diferença significativa no desempenho, dando ao otimizador do compilador uma dica de que ele não precisa verificar todos os acessos de slice no loop. Mas essas verificações de limites mínimos ainda são necessárias, mesmo que os arrays subjacentes passados ​​tenham sempre o mesmo tamanho. Além disso, o compilador pode ter dificuldade em otimizar lucrativamente o loop for/range (digamos, via unrolling ).

Em contraste, a versão genérica baseada em array da função não pode entrar em pânico em tempo de execução (não requerendo tratamento de erros) e ignora a necessidade de qualquer lógica de verificação de limites condicionais. Duvido muito que uma versão genérica compilada da função precise "passar" o comprimento da matriz como você sugere porque é literalmente um valor constante que faz parte do tipo instanciado em tempo de compilação.

Além disso, para pequenas dimensões de array (importante no meu caso), seria fácil para o compilador desenrolar lucrativamente ou até mesmo otimizar totalmente o loop for/range para um ganho de desempenho decente, pois ele saberá no momento da compilação quais são essas dimensões .

O outro grande benefício da versão genérica do código é que ela permite que o usuário do módulo HammingDist determine o tipo unsigned int em seu próprio código. A versão não genérica requer que o próprio módulo seja modificado para alterar o tipo definido SigEl , pois não há como "passar" um tipo para um módulo. Uma consequência dessa diferença é que a implementação da função distance se torna mais simples quando não há necessidade de escrever código separado para cada um dos casos de uint de {8,16,32,64} bits.

Os custos da versão baseada em fatias da função e a necessidade de modificar o código da biblioteca para definir o tipo de elemento são concessões altamente sub-ótimas necessárias para evitar a implementação e manutenção de versões "NxM" dessa função. O suporte genérico para tipos de array parametrizados (constantes) resolveria este problema:

// With generics + parameterized constant array lengths:
type Signature (type Element UnsignedInteger, n int) [n]Element
func HammingDist(b1, b2 *Signature) (sum int) { ... }

// Without generics
func HammingDistL1Uint(b1, b2 [1]uint) (sum int) { ... }
func HammingDistL1Uint8(b1, b2 [1]uint8) (sum int) { ... }
func HammingDistL1Uint16(b1, b2 [1]uint16) (sum int) { ... }
func HammingDistL1Uint32(b1, b2 [1]uint32) (sum int) { ... }
func HammingDistL1Uint64(b1, b2 [1]uint64) (sum int) { ... }

func HammingDistL2Uint(b1, b2 [2]uint) (sum int) { ... }
func HammingDistL2Uint8(b1, b2 [2]uint8) (sum int) { ... }
func HammingDistL2Uint16(b1, b2 [2]uint16) (sum int) { ... }
func HammingDistL2Uint32(b1, b2 [2]uint32) (sum int) { ... }
func HammingDistL2Uint64(b1, b2 [2]uint64) (sum int) { ... }

func HammingDistL3Uint(b1, b2 [3]uint) (sum int) { ... }
func HammingDistL3Uint8(b1, b2 [3]uint8) (sum int) { ... }
func HammingDistL3Uint16(b1, b2 [3]uint16) (sum int) { ... }
func HammingDistL3Uint32(b1, b2 [3]uint32) (sum int) { ... }
func HammingDistL3Uint64(b1, b2 [3]uint64) (sum int) { ... }

// and L4, L5, L6 ... ad nauseum

Evitar o pesadelo acima, ou os custos muito reais das alternativas atuais, parece o oposto do "uso excessivo de genéricos" para mim. Concordo com @sighoya que enumerar todos os comprimentos de array permitidos no contrato pode funcionar para um conjunto muito limitado de casos, mas acredito que seja muito limitado mesmo para o meu caso, pois mesmo se eu colocar o limite superior de suporte em um baixo total de 384 bits, o que exigiria quase 50 termos na cláusula ArrayType [1]ElemType,[2]ElemType,...,[maxBit]ElemType do contrato para cobrir o caso uint8 .

Certo, mas no meu exemplo não há arrays de comprimento indeterminado. O comprimento da matriz é uma constante conhecida em tempo de compilação.

Eu entendo isso, mas observe que eu também não disse "em tempo de execução". Você deseja escrever um código que não saiba o comprimento da matriz. Fatias já podem fazer isso.

Duvido muito que uma versão genérica compilada da função precise "passar" o comprimento da matriz como você sugere porque é literalmente um valor constante que faz parte do tipo instanciado em tempo de compilação.

Uma versão genérica da função seria - porque cada instanciação desse tipo usa uma constante diferente. É por isso que tenho a impressão de que você está assumindo que o código gerado não será genérico, mas expandido para todos os tipos. ou seja, você parece supor que haverá várias instanciações dessa função geradas, para [1]Element , [2]Element , etc. Estou dizendo que isso parece improvável para mim, que parece mais provável que haverá uma versão gerada, que é essencialmente equivalente à versão slice.

Claro que não precisa ser assim. Então, sim, você está certo em que não precisa passar o comprimento do array. Estou apenas prevendo fortemente que isso seria implementado dessa maneira e parece uma suposição questionável de que não será. (FWIW, eu também argumentaria que, se você deseja que o compilador gere corpos de função especializados para comprimentos separados, ele também poderia fazer isso de forma transparente para fatias, mas essa é uma discussão diferente).

O outro grande benefício da versão genérica do código

Para esclarecer: Por "versão genérica", você está se referindo à ideia geral de genéricos, conforme implementado, por exemplo, na minuta de projeto de contrato atual, ou você está se referindo mais especificamente a genéricos com parâmetros não-tipo? Porque as vantagens que você nomeou neste parágrafo também se aplicam ao projeto de projeto de contrato atual.

Não estou tentando argumentar contra os genéricos em geral aqui. Estou apenas explicando por que não acho que seu exemplo sirva para mostrar que precisamos de outros tipos de parâmetros além dos tipos.

// With generics + parameterized constant array lengths:
// Without generics

Esta é uma falsa dicotomia (e tão óbvia que estou um pouco frustrado com você). Há também "com parâmetros de tipo, mas sem parâmetros inteiros":

contract Unsigned(T) {
    T uint, uint8, uint16, uint32, uint64
}
func HammingDist(type T Unsigned) (b1, b2 []T) (sum int) {
    if len(b1) != len(b2) {
        panic("slices of different lengths passed to HammingDist")
    }
    for i := range b1 {
        sum += bits.OnesCount(b1[i]^b2[i]) // Same assumption about OnesCount being generic you made above
    }
    return sum
}

O que me parece bom. É um pouco menos seguro para tipos, exigindo um pânico em tempo de execução se os tipos não corresponderem. Mas, e esse é o meu ponto, essa é a única vantagem de adicionar parâmetros genéricos não-tipo em seu exemplo (e é uma vantagem que já estava clara, IMO). Os ganhos de desempenho que você está prevendo dependem de suposições bastante fortes sobre como os genéricos em geral e os genéricos sobre parâmetros não-tipo especificamente são implementados. Isso eu, pessoalmente, não considero muito provável com base no que ouvi da equipe Go até agora.

Duvido muito que uma versão genérica compilada da função precise "passar" o comprimento da matriz como você sugere porque é literalmente um valor constante que faz parte do tipo instanciado em tempo de compilação.

Você está apenas assumindo que os genéricos funcionariam como modelos C++ e implementações de funções duplicadas, mas isso não está certo. A proposta permite explicitamente implementações únicas com parâmetros ocultos.

Acho que se você realmente precisa de código modelado para um pequeno número de tipos numéricos, não é um fardo tão grande usar um gerador de código. Os genéricos só valem a complexidade do código para coisas como tipos de contêiner, onde há um benefício mensurável de desempenho ao usar tipos primitivos, mas você não pode esperar gerar apenas um pequeno número de modelos de código antecipadamente.

Obviamente, não tenho ideia de como os mantenedores do golang irão implementar alguma coisa, então vou me abster de especular mais e adiar alegremente aqueles com mais conhecimento interno.

O que eu sei é que, para o exemplo de problema do mundo real que compartilhei acima, a diferença de desempenho potencial entre a implementação atual baseada em fatias e uma implementação baseada em matriz genérica bem otimizada é substancial.

BenchmarkHD/256-bit_unrolled_array_HD-20            2000000000           1.05 ns/op        0 B/op          0 allocs/op
BenchmarkHD/256-bit_slice_HD-20                     300000000            5.10 ns/op        0 B/op          0 allocs/op

Código em: https://github.com/vsivsi/hdtest

Essa é uma diferença de desempenho potencial de 5 vezes para o caso de 4x64 bits (um ponto ideal no meu trabalho) com apenas um pequeno desenrolar de loop (e essencialmente nenhum código extra emitido) no caso de matriz. Esses cálculos estão nos loops internos dos meus algoritmos, feitos literalmente muitos trilhões de vezes, então uma diferença de desempenho de 5x é muito grande. Mas para obter esses ganhos de eficiência hoje, preciso escrever cada versão da função, para cada tipo de elemento necessário e comprimento de array.

Mas sim, se otimizações como essas nunca forem implementadas pelos mantenedores, todo o exercício de adicionar comprimentos de array parametrizados a genéricos seria inútil, pelo menos porque poderia beneficiar este caso de exemplo.

De qualquer forma, discussão interessante. Eu sei que essas são questões controversas, então obrigado por mantê-lo civilizado!

@vsivsi FWIW, as vitórias que você está observando desaparecem se você não estiver desenrolando manualmente seus loops (ou se também estiver desenrolando o loop sobre uma fatia) - então isso ainda não suporta seu argumento de que os parâmetros inteiros ajudam porque permitem o compilador para fazer o desenrolamento para você. Parece má ciência para mim, discutir X sobre Y, com base no compilador se tornando arbitrariamente inteligente para X e permanecendo arbitrariamente burro para Y. Não está claro para mim, por que uma heurística de desenrolamento diferente seria acionada no caso de loop em uma matriz , mas não acionar no caso de loop sobre uma fatia com comprimento conhecido em tempo de compilação. Você não está mostrando os benefícios de um certo tipo de genérico sobre outro, você está mostrando os benefícios dessa heurística de desenrolamento diferente.

Mas em qualquer caso, ninguém realmente argumentou que gerar código especializado para cada instanciação de uma função genérica não seria potencialmente mais rápido - apenas que existem outras compensações a serem consideradas ao decidir se você deseja fazer isso.

@Merovius Acho que o caso mais forte para genéricos nesse tipo de exemplo é com elaboração de tempo de compilação (emitindo uma função exclusiva para cada inteiro de nível de tipo) onde o código a ser especializado está em uma biblioteca. Se o usuário da biblioteca for usar um número limitado de instanciações da função, ele terá a vantagem de uma versão otimizada. Portanto, se meu código usa apenas arrays de comprimento 64, posso usar elaborações otimizadas das funções da biblioteca para comprimento 64.

Nesse caso específico, depende da distribuição de frequência dos comprimentos dos arrays, porque talvez não queiramos elaborar todas as funções possíveis se houver milhares delas devido a restrições de memória e lixo de cache de página, o que pode tornar as coisas mais lentas. Se, por exemplo, tamanhos pequenos são comuns, mas maiores são possíveis (uma distribuição de cauda longa em tamanho), podemos elaborar funções especializadas para os inteiros pequenos com loops desenrolados (digamos, 1 a 64) e fornecer uma única versão generalizada com um valor oculto -parâmetro para o resto.

Não gosto da ideia do "compilador arbitrariamente inteligente" e acho que esse é um argumento ruim. Quanto tempo terei que esperar por esse compilador arbitrariamente inteligente? Eu particularmente não gosto da ideia do compilador alterar os tipos, por exemplo otimizar um slice para um Array fazendo especializações ocultas em uma linguagem com reflexão, pois quando você reflete sobre aquele slice algo inesperado pode acontecer.

Em relação ao "dilema genérico", pessoalmente eu iria com "tornar o compilador mais lento/fazer mais trabalho", mas tente torná-lo o mais rápido possível usando uma boa implementação e uma compilação separada. Rust parece se sair muito bem, e após o recente anúncio da Intel parece que poderia substituir 'C' como a principal linguagem de programação de sistemas. O tempo de compilação não parecia ser um fator na decisão da Intel, já que a memória de tempo de execução e a segurança de simultaneidade com a velocidade 'C' pareciam ser os fatores-chave. Os "traços" de Rust são uma implementação razoável de classes de tipos genéricas, eles têm alguns casos de canto irritantes que eu acho que vêm de seu design de sistema de tipos.

Voltando à nossa discussão anterior, tenho que ter o cuidado de separar a discussão sobre genéricos em geral e como eles podem se aplicar especificamente ao Go. Como tal, não tenho certeza se Go deve ter genéricos, pois complica o que é uma linguagem simples e elegante, da mesma forma que 'C' não possui genéricos. Ainda acho que há uma lacuna no mercado para uma linguagem que tenha implementações genéricas como recurso principal, mas que permaneça simples e elegante.

Eu estou querendo saber se houve algum progresso sobre isso.

Quanto tempo eu posso tentar genéricos. Eu estive esperando por muito tempo

@Nsgj Você pode conferir este CL: https://go-review.googlesource.com/c/go/+/187317/

Na especificação atual, isso é possível?

contract Point(T) {
  T struct { X, Y float64 }
}

Em outras palavras, o tipo deve ser uma estrutura com dois campos, X e Y, do tipo float64.

edit: com uso de exemplo

func generate(type T Point)() T {
  return T{X: randomFloat64(), Y: randomFloat64()}
}

@abuchanan-nr Sim, o projeto de design atual permitiria isso, embora seja difícil ver como isso seria útil.

Também não tenho certeza se é útil, mas não vi um exemplo claro de uso de um tipo de estrutura personalizado em uma lista de tipos de um contrato. A maioria dos exemplos usa tipos internos.

FWIW, eu estava imaginando uma biblioteca de gráficos 2D. Você pode querer que cada vértice tenha vários campos específicos do aplicativo, como cor, força, etc. Mas você também pode querer uma biblioteca genérica de métodos e algoritmos apenas para a parte geométrica, que realmente depende apenas das coordenadas X,Y. Pode ser bom passar seu tipo de vértice personalizado para esta biblioteca, por exemplo

type MyVertex struct {
  X, Y float64
  Color color.Color
  OtherAttr int
}
p := geo.RandomPolygon(MyVertex)()

for _, vert := range p.Vertices() {
  p.Color = randColor()
}

Mais uma vez, não tenho certeza de que acaba sendo um bom design na prática, mas é onde minha imaginação estava na época :)

Consulte https://godoc.org/image#Image para saber como isso é feito no Go padrão hoje.

No que diz respeito aos Operadores/Tipos de contratos :

Isso resulta em uma duplicação de muitos métodos genéricos, pois precisaríamos deles em formato de operador ( + , == , < , ...) e formato de método ( Plus(T) T , Equal(T) bool , LessThan(T) bool , ...).

Proponho que unifiquemos essas duas abordagens em uma, o formato do método. Para conseguir isso, os tipos pré-declarados ( int , int64 , string , ...) precisariam ser convertidos em tipos com métodos arbitrários. Para o caso simples (trivial) isso já é possível ( type MyInt int; func (i MyInt) LessThan(o MyInt) bool {return int(i) < int(o)} ), mas o valor real está nos tipos compostos ( []int -> []MyInt , map[int]struct{} -> map[MyInt]struct{} , e assim por diante para canal, ponteiro, ...), o que não é permitido (veja FAQ ). Permitir essas conversões é uma mudança significativa por si só, então expandi os detalhes técnicos na Proposta de conversão de tipo relaxada . Isso permitiria que funções genéricas não lidassem com operadores e ainda suportassem todos os tipos, incluindo os pré-declarados.

Observe que essa alteração também beneficia os tipos não pré-declarados. Sob a proposta atual, dado type X struct{S string} (que vem de uma biblioteca externa, então você não pode adicionar métodos a ela), digamos que você tenha um []X e queira passá-lo para uma função genérica esperando []T , por T satisfazendo o contrato Stringer . Isso exigiria um type X2 X; func(x X2) String() string {return x.S} e uma cópia profunda de []X em []X2 . De acordo com as alterações propostas para esta proposta, você salva a cópia completa inteiramente.

NOTA: a proposta de conversão de tipo relaxado mencionada requer desafio.

@JavierZunzunegui Fornecer um "formato de método" (ou formato de operador) para operadores unários/binários básicos não é o problema. É bastante simples introduzir métodos como +(x int) int simplesmente permitindo símbolos de operador como nomes de métodos e estendendo isso para tipos internos (embora mesmo isso seja dividido em turnos, pois o operador do lado direito pode ser um tipo inteiro arbitrário - não temos como expressar isso no momento). O problema é que isso não é suficiente. Uma das coisas que um contrato precisa expressar é se um valor x do tipo X pode ser convertido no tipo de um parâmetro de tipo T como em T(x) (e vice-versa). Ou seja, é preciso inventar um "formato de método" para conversões permitidas. Além disso, precisa haver uma maneira de expressar que uma constante não tipada c pode ser atribuída a (ou convertida em) uma variável do tipo parâmetro de tipo T : é legal atribuir, digamos, 256 a t do tipo T ? E se T for byte ? Tem mais algumas coisas assim. Pode-se inventar a notação de "formato de método" para essas coisas, mas fica complicado rapidamente e não está claro se é mais compreensível ou legível.

Não estou dizendo que não pode ser feito, mas não encontramos uma abordagem satisfatória e clara. O rascunho de design atual, que simplesmente enumera os tipos, por outro lado, é bastante simples de entender.

@griesemer Isso pode ser difícil em Go devido a outras prioridades, mas é um problema bem resolvido em geral. É uma das razões pelas quais vejo as conversões implícitas como ruins. Existem outras razões como mágica acontecendo que não é visível para alguém que está lendo o código.

Se não houver conversões implícitas no sistema de tipos, posso usar a sobrecarga para controlar com precisão o intervalo de tipos aceitos e as interfaces controlar a sobrecarga.

Eu tenderia a expressar similaridade entre tipos usando interfaces, portanto, operações como '+' seriam expressas genericamente como operações em uma interface numérica em vez de um tipo. Você precisa ter variáveis ​​de tipo, bem como interfaces para expressar a restrição de que ambos os argumentos e o resultado da adição devem ser do mesmo tipo.

Então aqui o operador de adição é declarado para operar sobre tipos com uma interface Numeric. Isso se encaixa muito bem com a matemática, onde 'inteiros' e 'adição' formam um "grupo", por exemplo.

Você terminaria com algo como:

+(T Addable)(x T, y T) T

Se você permitir a seleção de interface implícita, o operador '+' pode ser apenas um método da interface Numeric, mas acho que isso causaria problemas com a seleção de método em Go?

@griesemer sobre seu ponto de vista sobre conversões:

Uma das coisas que um contrato precisa expressar é se um valor x do tipo X pode ser convertido no tipo de um parâmetro de tipo T como em T(x) (e vice-versa). Ou seja, é preciso inventar um "formato de método" para conversões permitidas

Eu posso ver como isso seria uma complicação, mas eu não acho que seja necessário. Do jeito que eu vejo, essas conversões aconteceriam fora do código genérico, pelo chamador. Um exemplo (usando Stringify conforme o projeto de rascunho):

Stringify(int)([]int{1,2}) // does not compile
type MyInt int
func (i MyInt) String() string {...}
Stringify(MyInt)([]MyInt([]int{1,2})) // OK. Generic type MyInt could be inferred

Acima, no que diz respeito Stringify o argumento é do tipo []MyInt e atende ao contrato. O código genérico não pode converter tipos genéricos em qualquer outra coisa (além das interfaces que eles implementam, conforme o contrato), precisamente porque o contrato não declara nada sobre isso.

@JavierZunzunegui Não vejo como o chamador pode fazer essas conversões sem expô-las na interface/contrato. Por exemplo, talvez eu queira implementar um algoritmo numérico genérico (uma função parametrizada) operando em vários tipos inteiros ou de ponto flutuante. Como parte desse algoritmo, o código da função precisa atribuir valores constantes c1 , c2 , etc. a valores do tipo de parâmetro T . Não vejo como o código pode fazer isso sem saber que não há problema em atribuir essas constantes a uma variável do tipo T . (Certamente não gostaria de ter que passar essas constantes para a função.)

func NumericAlgorithm(type T SomeContract)(vector []T) T {
   ...
   vector[i] = 3.1415  // <<< how do we know this is valid without the contract telling us?
   ...
}

precisa atribuir valores constantes c1 , c2 , etc. a valores do tipo de parâmetro T

@griesemer Eu (na minha opinião de como os genéricos são/devem ser) diria que o acima é a declaração de problema errada. Você está exigindo que T seja definido como float32 , mas um contrato apenas declara quais métodos estão disponíveis para T , não como ele é definido. Se você precisar disso, você pode manter vector como []T e exigir um argumento func(float32) T ( vector[i] = f(c1) ), ou muito melhor manter vector como []float32 e requer T por contrato para ter um método DoSomething(float32) ou DoSomething([]float32) , já que estou assumindo o T e o flutuadores devem interagir em algum ponto. Isso significa que T pode ou não ser definido como type T float32 , tudo o que podemos dizer é que tem os métodos exigidos pelo contrato.

@JavierZunzunegui Não estou dizendo que T seja definido como float32 - pode ser um float32 , um float64 , ou mesmo um dos tipos complexos. De maneira mais geral, se a constante fosse um inteiro, poderia haver uma variedade de tipos inteiros que seriam válidos para passar para essa função e alguns que não são. Certamente não é uma "declaração de problema errado". O problema é real - certamente não foi planejado querer escrever tais funções - e o problema não desaparece declarando-o "errado".

@griesemer Entendo, pensei que você estivesse preocupado apenas com a conversão, não registrei o elemento-chave que está lidando com constantes não tipadas.

Você pode fazer conforme minha resposta acima, com T tendo um método DoSomething(X) , e a função recebendo um argumento adicional func(float64) X , então a forma genérica é definida por dois tipos ( T,X ). A maneira como você descreve o problema X é normalmente float32 ou float64 e o argumento da função é func(f float64) float32 {return float32(f)} ou func(f float64) float64 {return f} .

Mais significativamente, como você destaca, para o caso inteiro, há o problema de que formatos inteiros menos precisos podem não ser suficientes para uma determinada constante. A abordagem mais segura é manter a função genérica de dois tipos ( T,X ) privada e expor publicamente apenas MyFunc32 / MyFunc64 /etc.

Eu admito que MyFunc32(int32) / MyFunc64(int64) /etc. é menos prático que um único MyFunc(type T Numeric) (o oposto é indefensável!). Mas isso é apenas para implementações genéricas que dependem de uma constante e principalmente uma constante inteira - quantos deles existem? Quanto ao resto, você obtém a liberdade adicional de não ficar restrito a alguns tipos internos por T .

E, claro, se a função não for cara, você pode estar perfeitamente bem fazendo o cálculo como int64 / float64 e expondo apenas isso, mantendo-o simples e irrestrito em T .

Nós realmente não podemos dizer às pessoas "você pode escrever funções genéricas em qualquer tipo T, mas essas funções genéricas não podem usar constantes não tipadas". Go é acima de tudo uma linguagem simples. Idiomas com restrições bizarras como essa não são simples.

Sempre que uma abordagem proposta aos genéricos se torna difícil de explicar de maneira simples, devemos descartar essa abordagem. É mais importante manter a linguagem simples do que adicionar genéricos à linguagem.

@JavierZunzunegui Uma das propriedades interessantes do código parametrizado (genérico) é que o compilador pode personalizá-lo com base nos tipos com os quais o código é instanciado. Por exemplo, pode-se querer usar um tipo byte em vez de int porque isso leva a uma economia significativa de espaço (imagine uma função que aloca grandes fatias do tipo genérico). Portanto, simplesmente restringir o código a um tipo "grande o suficiente" é uma resposta insatisfatória, mesmo para uma linguagem "opinativa" como Go.

Além disso, não se trata apenas de algoritmos que usam constantes não tipadas "grandes" que podem não ser tão comuns: descartar esses algoritmos com uma pergunta "quantos deles existem de qualquer maneira" é simplesmente acenar com a mão para desviar um problema que existe. Apenas para sua consideração: Parece razoável que um grande número de algoritmos use constantes inteiras como -1, 0, 1. Observe que não se pode usar -1 em conjunto com inteiros não tipados, apenas para dar um exemplo simples. Claramente, não podemos simplesmente ignorar isso. Precisamos ser capazes de especificar isso em um contrato.

@ianlancetaylor @griesemer obrigado pelo feedback - posso ver que há um conflito significativo na minha mudança proposta com constantes não tipadas e números inteiros negativos, vou deixar isso para trás.

Posso chamar sua atenção para o segundo ponto em https://github.com/golang/go/issues/15292#issuecomment -546313279:

Observe que essa alteração também beneficia os tipos não pré-declarados. Na proposta atual, dado o tipo X struct{S string} (que vem de uma biblioteca externa, então você não pode adicionar métodos a ela), digamos que você tenha um []X e queira passá-lo para uma função genérica esperando [ ]T, para T satisfazendo o contrato Stringer. Isso exigiria um tipo X2 X; func(x X2) String() string {return xS} e uma cópia profunda de []X em []X2. De acordo com as alterações propostas para esta proposta, você salva a cópia completa inteiramente.

O relaxamento das regras de conversão (se tecnicamente viável) ainda seria útil.

@JavierZunzunegui Discutir conversões do tipo []B([]A) se B(a) (com a do tipo A ) for permitido parece ser principalmente ortogonal aos recursos genéricos. Acho que não precisamos trazer isso aqui.

@ianlancetaylor Não tenho certeza de quão relevante isso é para Go, mas não acho que as constantes sejam realmente sem tipo, elas devem ter um tipo, pois o compilador deve escolher uma representação de máquina. Eu acho que um termo melhor é constante de tipo indeterminado, pois a constante pode ser representável por vários tipos diferentes. Uma solução é usar um tipo de união para que uma constante como 27 tenha um tipo como int16|int32|float16|float32 uma união de todos os tipos possíveis. Então T em um tipo genérico pode ser esse tipo de união. O único requisito é que em algum momento devemos resolver a união para um único tipo. O caso mais problemático seria algo como print(27) porque nunca há um único tipo para resolver, nesses casos qualquer tipo na união serviria, e poderíamos escolher com base em um parâmetro de otimização como espaço/velocidade etc. .

@keean O nome exato e o tratamento do que a especificação chama de "constantes não tipadas" está fora do tópico sobre esse problema. Vamos, por favor, levar essa discussão para outro lugar. Obrigado.

@ianlancetaylor Estou feliz em, no entanto, essa é uma das razões pelas quais acho que Go não pode ter uma implementação genérica limpa/simples, todos esses problemas estão interconectados e as escolhas originais feitas para Go não foram tomadas com programação genérica em mente. Eu acho que outra linguagem, projetada para tornar os genéricos simples por design, é necessária, para Go, os genéricos sempre serão algo adicionado à linguagem posteriormente, e a melhor opção para manter a linguagem limpa e simples pode ser não tê-los.

Se hoje eu projetasse uma linguagem simples com tempos de compilação rápidos e flexibilidade comparável, escolheria sobrecarga de métodos e polimorfismo estrutural (subtipagem) via interfaces golang e sem genéricos. Na verdade, isso permitiria a sobrecarga em diferentes interfaces anônimas com campos diferentes.

A escolha de genéricos tem a vantagem de reutilização de código limpo, mas introduz mais ruído, o que fica complicado se as restrições forem adicionadas, levando às vezes a um código dificilmente compreensível.
Então, se temos genéricos, por que não usar um sistema de restrição avançado como uma cláusula where, tipos de tipo superior ou talvez tipos de classificação mais alta e também tipagem dependente?
Todas essas questões eventualmente surgirão se adotarmos os genéricos, mais cedo ou mais tarde.

Afirmando claramente, não sou contra os genéricos, mas estou pensando se é o caminho a seguir para conservar a simplicidade do go.

Se a introdução de genéricos em go for inevitável, seria razoável refletir sobre o impacto nos tempos de compilação ao monomorfizar funções genéricas.
Não seria um bom padrão box generics, ou seja, gerar uma cópia para todos os tipos de entrada juntos, e só especializar se explicitamente solicitado pelo usuário com alguma anotação na definição -ou chamar site?

Em relação ao impacto no desempenho do tempo de execução, isso reduziria o desempenho devido ao problema de boxing/unboxing, caso contrário, existem engenheiros c++ de nível especialista recomendando genéricos de boxing como o java para mitigar falhas de cache.

@ianlancetaylor @griesemer Reconsiderei a questão de constantes não tipadas e genéricos 'não-operador' (https://github.com/golang/go/issues/15292#issuecomment-547166519) e descobri uma maneira melhor de lidar com isso.

Dê os tipos numéticos ( type MyInt32 int32 , type MyInt64 int64 , ...), estes têm muitos métodos que satisfazem o mesmo contrato ( Add(T) T , ...), mas criticamente não outros que arriscaria estourar func(MyInt64) FromI64(int64) MyInt64 mas não ~ func(MyInt32) FromI64(int64) MyInt32 ~. Isso permite o uso de constantes numéricas (atribuídas explicitamente ao valor de precisão mais baixo necessário) com segurança (1) , pois os tipos numéricos de baixa precisão não atenderão ao contrato necessário, mas todos os mais altos atenderão. Veja playground , usando interfaces no lugar de genéricos.

Uma vantagem de relaxar os genéricos numéricos além dos tipos internos (não específicos para esta última revisão, então eu deveria ter compartilhado na semana passada) é que permite instanciar métodos genéricos com tipos de verificação de estouro - veja playground . A verificação de estouro é uma solicitação/proposta muito popular (https://github.com/golang/go/issues/31500 e problemas relacionados).


(1) : A garantia de tempo de compilação sem estouro para constantes não tipadas é forte dentro do mesmo 'branch' ( int[8/16/32/64] e uint[8/16/32/64] ). Cruzando ramificações, uma constante uint[X] só é instanciada com segurança para int[2X+] e uma constante int[X] não pode ser instanciada com segurança por nenhum uint[X] . Mesmo relaxando estes (permitindo int[X]<->uint[X] ) seria simples e seguro seguindo alguns padrões mínimos, e criticamente qualquer complexidade recai sobre o escritor do código genérico, não sobre o usuário do genérico (que está preocupado apenas com o contrato , e pode esperar que qualquer tipo numérico que o atenda seja válido).

Métodos genéricos - foi a queda do Java!

@ianlancetaylor Estou feliz em, no entanto, essa é uma das razões pelas quais acho que Go não pode ter uma implementação genérica limpa/simples, todos esses problemas estão interconectados e as escolhas originais feitas para Go não foram tomadas com programação genérica em mente. Eu acho que outra linguagem, projetada para tornar os genéricos simples por design, é necessária, para Go, os genéricos sempre serão algo adicionado à linguagem posteriormente, e a melhor opção para manter a linguagem limpa e simples pode ser não tê-los.

Concordo 100%. Por mais que eu adoraria ver algum tipo de genérico implementado, acho que o que vocês estão preparando atualmente destruirá a simplicidade da linguagem Go.

A ideia atual de estender interfaces é assim:

type I1(type P1) interface {
        m1(x P1)
}

type I2(type P1, P2) interface {
        m2(x P1) P2
        type int, float64
}

func f(type P1 I1(P1), P2 I2(P1, P2)) (x P1, y P2) P2

Desculpem todos, mas por favor, não façam isso! Ele enfeita a beleza do Go big time.

Tendo escrito quase 100 mil linhas de código Go agora, estou bem em não ter genéricos.

No entanto, pequenas coisas como apoiar

// Allow mulitple types in Slices and Maps declarations
func Reverse(s []<int,string>) {
    first := 0
    last := len(s) - 1
    for first < last {
        s[first], s[last] = s[last], s[first]
        first++
        last--
    }
}

//  Allow multiple types in variable declarations
func Index (s <string, []byte>, b byte) int {
    for i := 0; i < len(s); i++ {
        if s[i] == b {
            return i
        }
    }
    return -1
}

// Allow slices and maps declarations with interface values
func ToStrings (s []Stringer) []string {
    r := make([]string, len(s))
    for i, v := range s {
        r[i] = v.String()
    }
    return r
}

ajudaria.

Proposta de sintaxe para poder separar completamente os genéricos do código Go regular

package graph

// Example how you would define generics completely separat from Go 1 code
contract (Node, Edge)G {
    Node Edges() []Edge
    Edge Nodes() (from, to Node)
}

type (type Node, Edge G) ( Graph )
func (type Node, Edge G) ( New )
const _ = (Node, Edge) Graph

// Unmodified Go 1 code
type Graph struct { ... }
func New(nodes []Node) *Graph { ... }
func (g *Graph) ShortestPath(from, to Node) []Edge { ... }

@martinrode No entanto, pequenas coisas como apoiar
... permite vários tipos em declarações de Slices e Maps

Isso não responde às necessidades de algumas funções de fatia genéricas funcionais, por exemplo head() , tail() , map(slice, func) , filter(slice, func)

Você pode escrever isso sozinho para cada projeto em que precisar, mas nesse ponto é um risco de ficar obsoleto devido à repetição de copiar e colar e incentiva a complexidade do código Go para economizar a simplicidade da linguagem.

(Em um nível pessoal, também é meio cansativo saber que tenho um conjunto de recursos que quero implementar e não ter uma maneira limpa de expressá-los sem também responder às restrições de idioma)

Considere o seguinte em go atual, não genérico:

Tenho uma variável x do tipo externallib.Foo , obtida de uma biblioteca externallib que não controlo.
Eu quero passá-lo para uma função SomeFunc(fmt.Stringer) , mas externallib.Foo não tem um método String() string . Eu posso simplesmente fazer:

type MyFoo externallib.Foo
func (mf MyFoo) String() string {...}
// ...
SomeFunc(MyFoo(x))

Considere o mesmo com os genéricos.

Eu tenho uma variável x do tipo []externallib.Foo . Eu quero passar para AnotherFunc(type T Stringer)(s []T) . Isso não pode ser feito sem uma cópia profunda e cara da fatia em um novo []MyFoo . Se em vez de uma fatia fosse um tipo mais complexo (digamos, um chan ou mapa), ou o método modificasse o receptor, torna-se ainda mais ineficiente e tedioso, se possível.

Isso pode não ser um problema dentro da biblioteca padrão, mas isso ocorre apenas porque ela não possui dependências externas. Isso é um luxo que praticamente nenhum outro projeto terá.

Minha sugestão é relaxar a conversão para permitir []Foo([]Bar{}) para qualquer Foo definido como type Foo Bar , ou vice-versa, e igualmente para mapas, matrizes, canais e ponteiros, recursivamente. Observe que estas são todas cópias rasas baratas. Mais detalhes técnicos em Proposta de conversão de tipo relaxado .


Isso foi apresentado pela primeira vez como um recurso secundário em https://github.com/golang/go/issues/15292#issuecomment -546313279.

@JavierZunzunegui Eu não acho que isso esteja realmente relacionado aos genéricos. Sim, você pode fornecer um exemplo usando genéricos, mas pode fornecer um exemplo semelhante sem usar genéricos. Acho que essa questão deveria ser discutida separadamente, não aqui. Consulte também https://golang.org/doc/faq#convert_slice_with_same_underlying_type. Obrigado.

Sem genéricos, tal conversão não tem quase nenhum valor, pois em geral []Foo não atenderá nenhuma interface, ou pelo menos nenhuma interface que faça uso de ser uma fatia. A exceção são as interfaces que possuem um padrão muito específico para usá-lo, como sort.Interface , para as quais você não precisa converter a fatia de qualquer maneira.

A versão não genérica do acima ( func AnotherFunc(type T Stringer)(s []T) ) é

type SliceOfStringers interface {
  Len() int
  Get(int) fmt.Stringer
}
func AnotherFunc(s SliceOfStringers) {...}

Pode ser menos prático do que a abordagem genérica, mas pode ser feito para lidar com qualquer slice e fazê-lo sem copiá-lo, independentemente do tipo subjacente ser um fmt.Stringer . Do jeito que está, os genéricos não podem, apesar de, em princípio, serem uma ferramenta muito mais adequada para o trabalho. E com certeza, se adicionarmos genéricos, é justamente para tornar slices, maps etc mais comuns em APIs, e manipulá-los com menos clichê. No entanto, eles introduzem um novo problema, sem equivalência em um mundo apenas de interface, que pode até não ser inevitável, mas imposto artificialmente pela linguagem.

A conversão de tipo que você menciona aparece com frequência suficiente em código não genérico que é um FAQ. Vamos, por favor, mover esta discussão para outro lugar. Obrigado.

Qual é o estado disso? Algum rascunho ATUALIZADO? Estou esperando por genéricos desde
quase 2 anos atrás. Quando teremos genéricos?

El mar., 4 de fev. de 2020 a la(s) 13:28, Ian Lance Taylor (
[email protected]) escreveu:

A conversão de tipo que você menciona aparece com bastante frequência em código não genérico
que é um FAQ. Vamos, por favor, mover esta discussão para outro lugar. Obrigado.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/15292?email_source=notifications&email_token=AJMFNBN3MFHDMENAFXIKBLDRBGXUTA5CNFSM4CA35RX2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEKYV5RI#issuecomment-58204
ou cancelar
https://github.com/notifications/unsubscribe-auth/AJMFNBO5UTKNPL3MSA3NESLRBGXUTANCNFSM4CA35RXQ
.

--
Este é um teste para assinaturas de correio a serem usadas no TripleMint

Estamos trabalhando nisso. Algumas coisas levam tempo.

O trabalho é feito offline? Eu adoraria vê-lo evoluindo ao longo do tempo, de uma forma que o "público em geral" como eu não possa comentar para evitar ruídos.

Embora tenha sido fechado desde então para manter a discussão sobre genéricos em um só lugar, confira #36177, onde @Griesemer se vincula a um protótipo em que está trabalhando e faz alguns comentários interessantes sobre seus pensamentos sobre o assunto até agora.

Acho que estou certo em dizer que o protótipo está apenas lidando com os aspectos de verificação de tipo da proposta de 'contratos' do rascunho, mas o trabalho certamente parece promissor para mim.

@ianlancetaylor Sempre que uma abordagem proposta aos genéricos se torna difícil de explicar de maneira simples, devemos descartar essa abordagem. É mais importante manter a linguagem simples do que adicionar genéricos à linguagem.

Esse é um grande ideal pelo qual lutar, mas, na realidade, o desenvolvimento de software às vezes não é _simples de explicar_.

Quando a linguagem é limitada para expressar tais ideias _não simples de expressar_, os engenheiros de software acabam reinventando essas facilidades repetidamente, porque essas ideias malditas _difíceis de expressar_ são algumas vezes essenciais para a lógica dos programas.

Veja Istio, Kubernetes, operator-sdk e, até certo ponto, Terraform e até mesmo biblioteca protobuf. Todos eles escapam do sistema de tipo Go usando reflexão, implementando um novo sistema de tipo em cima de Go usando interfaces e geração de código, ou uma combinação destes.

@omeid

Veja Istio, Kubernetes

Já lhe ocorreu que a razão pela qual eles estão fazendo essas coisas absurdas é porque o design principal deles não faz nenhum sentido e, como resultado, eles tiveram que resultar nos jogos reflect para cumpri-lo ?

Eu mantenho que designs melhores para programas golang (tanto na fase de design quanto na API) não _requer_ genéricos.

Por favor, não os adicione ao golang.

Programar é difícil. Kubelet é um lugar escuro. Os genéricos dividem mais as pessoas do que a política americana. Eu quero acreditar.

Quando a linguagem é limitada de expressar idéias tão difíceis de expressar, os engenheiros de software acabam reinventando essas facilidades repetidas vezes, porque essas idéias difíceis de expressar às vezes são essenciais para a lógica dos programas.

Veja Istio, Kubernetes, operator-sdk e, até certo ponto, Terraform e até mesmo biblioteca protobuf. Todos eles escapam do sistema de tipo Go usando reflexão, implementando um novo sistema de tipo em cima de Go usando interfaces e geração de código, ou uma combinação destes.

Não acho que seja um argumento persuasivo. Idealmente, a linguagem Go deve ser fácil de ler, escrever e entender, ao mesmo tempo que possibilita a execução de operações arbitrariamente complexas. Isso é consistente com o que você está dizendo: as ferramentas que você mencionou precisam fazer algo complexo, e o Go lhes dá uma maneira de fazer isso.

Idealmente, a linguagem Go deve ser fácil de ler, escrever e entender, ao mesmo tempo que possibilita a execução de operações arbitrariamente complexas.

Concordo com isso, mas como esses são objetivos múltiplos, às vezes eles estarão em tensão uns com os outros. Código que naturalmente "quer" ser escrito em um estilo genérico muitas vezes se torna menos fácil de ler do que poderia ser quando tem que recorrer a técnicas como reflexão.

Código que naturalmente "quer" ser escrito em um estilo genérico muitas vezes se torna menos fácil de ler do que poderia ser quando tem que recorrer a técnicas como reflexão.

É por isso que esta proposta permanece aberta e por que temos um rascunho de design para uma possível implementação de genéricos (https://blog.golang.org/why-generics).

Olhe para ... mesmo biblioteca protobuf. Todos eles escapam do sistema de tipo Go usando reflexão, implementando um novo sistema de tipo em cima de Go usando interfaces e geração de código, ou uma combinação destes.

Falando da experiência com protobufs, existem alguns casos em que os genéricos podem melhorar a usabilidade e/ou implementação da API, mas a grande maioria da lógica não se beneficiará dos genéricos. Generics presume que as informações de tipo concreto são conhecidas em tempo de compilação . Para protobufs, a maior parte da situação envolve casos em que as informações de tipo são conhecidas apenas em tempo de execução .

Em geral, noto que as pessoas muitas vezes apontam para qualquer uso da reflexão e afirmam isso como evidência da necessidade de genéricos. Não é tão simples. Uma distinção crucial é se as informações de tipo são conhecidas em tempo de compilação ou não. Em vários casos, fundamentalmente não é.

@dsnet Obrigado interessante, nunca pensei em protobuf não ser compatível com genéricos. Sempre assumi que toda ferramenta que está gerando código clichê go como por exemplo protoc, com base em um esquema predefinido, seria capaz de gerar código genérico sem reflexão usando a proposta genérica atual. Você se importaria de atualizar isso na especificação com um exemplo ou em uma nova postagem do blog go onde você descreve esse problema com mais detalhes?

as ferramentas que você mencionou precisam fazer algo complexo, e o Go oferece uma maneira de fazer isso.

Usar modelos de texto para gerar código Go dificilmente é uma facilidade por design, eu diria que é um band-aid ad-hoc, idealmente, pelo menos os pacotes padrão ast e parser devem permitir a geração de código Go arbitrário.

A única coisa que você pode argumentar que o Go oferece para lidar com lógica complexa é talvez o Reflection, mas isso mostra rapidamente suas limitações, sem falar em código crítico de desempenho, mesmo quando usado na biblioteca padrão, por exemplo, o tratamento JSON do Go é primitivo no melhor.

É difícil argumentar que usar modelos de texto ou reflexão para fazer _algo já complexo_ se encaixa no ideal de:

Sempre que uma abordagem proposta para algo ~genérico~ complexo se torna difícil de explicar de maneira simples, devemos descartar essa abordagem.

Eu acho que a solução que os projetos mencionados vieram para resolver seu problema é muito complexa e não é fácil de entender. Portanto, o Go não possui as facilidades que permitem aos usuários expressar problemas complexos em termos tão simples e diretos quanto possível.

Em geral, noto que as pessoas muitas vezes apontam para qualquer uso da reflexão e afirmam isso como evidência da necessidade de genéricos.

Talvez haja um equívoco geral, mas a biblioteca protobuf, especialmente a nova API, poderia ser muito mais simples com _generics_, ou algum tipo de _sum type_.

Um dos autores dessa nova API protobuf acabou de dizer que "a grande maioria da lógica não se beneficiará dos genéricos", então não tenho certeza de onde você está chegando que "especialmente a nova API pode ser muito mais rápida simples com genéricos". Em que se baseia isso? Você pode fornecer alguma evidência de que seria muito mais simples?

Falando como alguém que usou as APIs protobuf em algumas linguagens que incluem genéricos (Java, C++), não posso dizer que notei diferenças significativas de usabilidade com a API Go e suas APIs. Se sua afirmação fosse verdadeira, eu esperaria que houvesse alguma diferença.

@dsnet Também disse que "há alguns casos em que os genéricos podem melhorar a usabilidade e/ou implementação da API".

Mas se você quiser um exemplo de como as coisas podem ser mais simples, comece descartando o tipo Value , pois é em grande parte um tipo de soma ad-hoc.

@omeid Este problema é sobre genéricos, não tipos de soma. Portanto, não tenho certeza de como esse exemplo é relevante.

Especificamente, minha pergunta é: como ter genéricos resultaria em uma implementação de protobuf ou API que é "saltos e limites muito mais simples" do que a nova (ou antiga, aliás) API?

Isso parece não estar de acordo com minha leitura do que @dsnet disse acima, nem com minha experiência com as APIs protobuf Java e C++.

Além disso, seu comentário sobre o tratamento primitivo de JSON em Go também me parece igualmente estranho. Você pode explicar como você acha que a API de encoding/json seria melhorada por genéricos?

AFAIK, implementações de análise JSON em Java usam reflexão (não genéricos). É verdade que a API de nível superior na maioria das bibliotecas JSON provavelmente usará um método genérico (por exemplo, Gson ), mas um método que recebe um parâmetro genérico irrestrito T e retorna um valor do tipo T fornece muito pouca verificação de tipo adicional quando comparado ao json.Unmarshal . Na verdade, acho que o único erro que o único cenário de erro adicional não capturado por json.Unmarshal em tempo de compilação é se você passar um valor sem ponteiro. (Além disso, observe as advertências na documentação da API do Gson para usar uma função diferente para tipos genéricos e não genéricos. Novamente, isso argumenta que os genéricos complicaram sua API, em vez de simplificá-la; neste caso, é para dar suporte à serialização/desserialização de genéricos tipos).

(O suporte a JSON em C++ é pior para AFAICT; as várias abordagens que conheço usam quantidades significativas de macros ou envolvem escrever manualmente funções de análise/serialização. Novamente, isso não acontece)

Se você espera que os genéricos acrescentem muito ao suporte do Go para JSON, temo que fique desapontado.


@gertcuykens Toda implementação de protobuf em todas as linguagens que conheço usa geração de código, independentemente de terem genéricos ou não. Isso inclui Java, C++, Swift, Rust, JS (e TS). Eu não acho que ter genéricos remove automaticamente todos os usos de geração de código (como prova de existência, eu escrevi geradores de código que geram código Java e código C++); parece ilógico esperar que qualquer solução para genéricos satisfaça essa barreira.


Só para ficar bem claro: eu apoio a adição de genéricos ao Go. Mas acho que devemos ter uma visão clara sobre o que vamos conseguir com isso. Não acredito que obteremos melhorias significativas nas APIs protobuf ou JSON.

Eu não acho que o protobuf seja um caso particularmente bom para genéricos. Você não precisa de genéricos no idioma de destino, pois pode simplesmente gerar código especializado diretamente. Isso também se aplicaria a outros sistemas semelhantes, como Swagger/OpenAPI.

Onde os genéricos parecem ser úteis para mim, e podem oferecer simplificação e segurança de tipo, seria escrever o próprio compilador protobuf.

O que você precisa é de uma linguagem que seja capaz de uma representação segura de tipo de sua própria árvore de sintaxe abstrata. Pela minha própria experiência, isso requer pelo menos genéricos e tipos de dados abstratos generalizados. Você poderia então escrever um compilador protobuf de tipo seguro para uma linguagem na própria linguagem.

Onde os genéricos parecem ser úteis para mim, e podem oferecer simplificação e segurança de tipo, seria escrever o próprio compilador protobuf.

Eu realmente não vejo como. O pacote go/ast já fornece uma representação do AST do Go. O compilador Go protobuf não o usa porque trabalhar com um AST é muito mais complicado do que apenas emitir strings, mesmo que seja mais seguro para o tipo.

Talvez você tenha um exemplo do compilador protobuf para alguma outra linguagem?

@neild Eu comecei dizendo que não achava que o protobuf era um exemplo muito bom. Há ganhos a serem obtidos com o uso de genéricos, mas eles dependem muito de quão importante é a segurança de tipo para você, e isso seria contrabalançado pelo quão intrusiva é a implementação de genéricos. Uma implementação ideal sairia do seu caminho, a menos que você cometa um erro e, nesse caso, as vantagens superariam o custo de mais casos de uso.

Olhando para o pacote go/ast, ele não tem uma representação digitada do AST porque isso requer genéricos e GADTs. Por exemplo, um nó 'adicionar' precisaria ser genérico no tipo dos termos que estão sendo adicionados. Com um AST não seguro de tipo, toda a lógica de verificação de tipo deve ser codificada à mão, o que tornaria complicado.

Com uma boa sintaxe de modelo e expressões seguras de tipo, você pode tornar isso tão fácil quanto emitir strings, mas também com segurança de tipo. Por exemplo, veja (isto é mais sobre o lado da análise): https://stackoverflow.com/questions/11104536/how-to-parse-strings-to-syntax-tree-using-gadts

Por exemplo, considere JSX como uma sintaxe literal para o HTML Dom em JavaScript Vs TSX como uma sintaxe literal para o Dom em TypeScript.

Podemos escrever expressões genéricas tipadas que se especializam no código final. Tão fácil de escrever quanto strings, mas tipo verificado (em sua forma genérica).

Um dos principais problemas com geradores de código é que a verificação de tipo só acontece no código emitido, o que dificulta a escrita de modelos corretos. Com os genéricos, você pode escrever os modelos como expressões reais com verificação de tipo, para que a verificação seja feita diretamente no modelo, não no código emitido, o que torna muito mais fácil acertar e manter.

Os parâmetros de tipo variadic estão faltando no design atual, o que parece uma grande falta da funcionalidade dos genéricos. Um design complementar (talvez) segue o design atual do contrato:

contract Comparables(Ts...) {
    if  len(Ts) > 0 {
        Comparables(Ts[1:]...)
    } else {
        Comparable(Ts[0])
    }
}

contract Comparable(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

type Keys(type Ts ...Comparables) struct {
    fs ...Ts
}

type Metric(type Ts ...Comparables) struct {
    mu sync.Mutex
    m  map[Keys(Ts...)]int
}

func (m *Metric(Ts...)) Add(vs ...Ts) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[Keys(Ts...))]int)
    }
    m[Keys(Ts...){vs...}]++
}


// To use the metric

m := Metric(int, float64, string){m: make(map[Keys(int, float64, string)]int}
m.Add(1, 2.0, "variadic")

Exemplo inspirado daqui .

Não está claro para mim como isso adiciona alguma segurança acima apenas usando interface{} . Existe um problema real com as pessoas passando não comparáveis ​​em uma métrica?

Não está claro para mim como isso adiciona alguma segurança acima apenas usando interface{} . Existe um problema real com as pessoas passando não comparáveis ​​em uma métrica?

Comparables neste exemplo requer que Keys seja composto por uma série de tipos comparáveis. A ideia-chave é mostrar o design dos parâmetros de tipo variável, não o significado do próprio tipo.

Não quero ficar muito preso ao exemplo, mas estou escolhendo porque acho que muitos exemplos de "extensão de tipo" acabam empurrando a contabilidade sem adicionar nenhuma segurança prática. Nesse caso, se você vir um tipo ruim em tempo de execução ou potencialmente com go vet, poderá reclamar.

Além disso, estou um pouco preocupado que permitir tipos de tipos abertos como esse levaria ao problema de referências paradoxais, como ocorre na lógica de segunda ordem. Você poderia definir C como o contrato de todos os tipos que não estão em C?

Além disso, estou um pouco preocupado que permitir tipos de tipos abertos como esse levaria ao problema de referências paradoxais, como ocorre na lógica de segunda ordem. Você poderia definir C como o contrato de todos os tipos que não estão em C?

Desculpe, mas não entendo como este exemplo permite tipos abertos e se relaciona com o paradoxo de Russell, Comparables é definido por uma lista de Comparable .

Não gosto da ideia de escrever código Go dentro de um contrato. Se eu posso escrever uma declaração if , posso escrever uma declaração for ? Posso chamar uma função? Posso declarar variáveis? Por que não?

Também parece desnecessário. func F(a ...int) significa que a é []int . Por analogia, func F(type Ts ...comparable) significaria que cada tipo na lista é comparable .

Nestas linhas

type Keys(type Ts ...Comparables) struct {
    fs ...Ts
}

você parece estar definindo uma estrutura com vários campos, todos chamados fs . Não tenho certeza de como isso deve funcionar. Existe alguma maneira de usar referir-se a campos nesta estrutura além de usar reflexão?

Então a pergunta é: o que se pode fazer com parâmetros de tipo variadic? O que alguém quer fazer?

Aqui eu acho que você está usando parâmetros de tipo variável para definir um tipo de tupla com um número arbitrário de campos.

O que mais alguém pode querer fazer?

Não gosto da ideia de escrever código Go dentro de um contrato. Se eu posso escrever uma declaração if , posso escrever uma declaração for ? Posso chamar uma função? Posso declarar variáveis? Por que não?

Também parece desnecessário. func F(a ...int) significa que a é []int . Por analogia, func F(type Ts ...comparable) significaria que cada tipo na lista é comparable .

Depois de revisar o exemplo um dia depois, acho que você está absolutamente certo. O Comparables é uma ideia idiota. O exemplo só quer transmitir a mensagem de usar len(args) para determinar o número de parâmetros. Acontece que para funções, func F(type Ts ...Comparable) é bom o suficiente.

O exemplo recortado:

contract Comparable(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

type Keys(type Ts ...Comparable) struct {
    fs ...Ts
}

type Metric(type Ts ...Comparable) struct {
    mu sync.Mutex
    m  map[Keys(Ts...)]int
}

func (m *Metric(Ts...)) Add(vs ...Ts) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[Keys(Ts...))]int)
    }
    m[Keys(Ts...){vs...}]++
}


// To use the metric

m := Metric(int, float64, string){m: make(map[Keys(int, float64, string)]int}
m.Add(1, 2.0, "variadic")

você parece estar definindo uma estrutura com vários campos, todos chamados fs . Não tenho certeza de como isso deve funcionar. Existe alguma maneira de usar referir-se a campos nesta estrutura além de usar reflexão?

Então a pergunta é: o que se pode fazer com parâmetros de tipo variadic? O que alguém quer fazer?

Aqui eu acho que você está usando parâmetros de tipo variável para definir um tipo de tupla com um número arbitrário de campos.

O que mais alguém pode querer fazer?

Parâmetros de tipo variadic são direcionados para tuplas por sua definição se usarmos ... para isso, não significando que tuplas são o único caso de uso, mas pode-se usá-lo em qualquer struct e qualquer função.

Como existem apenas dois lugares que aparecem com parâmetros de tipo variadic: struct ou function, temos facilmente o que está claro antes para funções:

func F(type Ts ...Comparable) (args ...Ts) {
    if len(args) > 1 {
        F(args[1:])
        return
    }
    // ... do stuff with args[0]
}

Por exemplo, a função variadic Min não é possível no projeto atual, mas é possível com parâmetros de tipo variadic:

func Min(type T ...Comparable)(p1 T, pn ...T) T {
    switch l := len(pn); {
    case l > 1:
        return Min(pn[0], pn[1:]...)
    case l == 1:
        if p1 >= pn[0] { return pn[0] }
        return p1
    case l < 1:
        return p1
    }
}

Para definir um Tuple com parâmetros de tipo variadic:

type Tuple(type Ts ...Comparable) struct {
    fs ...Ts
}

Quando três parâmetros de tipo são instanciados por 'Ts', ele pode ser traduzido para

type Tuple(type T1, T2, T3 Comparable) struct {
    fs_1 T1
    fs_2 T2
    fs_3 T3
}

como uma representação intermediária. Para usar o fs , existem várias maneiras:

  1. parâmetros descompactar
k := Tuple(int, float64, string){1, 2.0, "variadic"}
fs1, fs2, fs3 := k.fs // translated to fs1, fs2, fs3 := k.fs_1, k.fs_2, k.fs_3
println(fs1) // 1
println(fs2) // 2.0
println(fs3) // variadic
  1. use for loop
for idx, f := range k.fs {
    println(idx, ": ", f)
}
// Output:
// 0: 1
// 1: 2.0
// 2: variadic
  1. use index (não tenho certeza se as pessoas veem que isso é uma ambiguidade para array/slice ou map)
k.fs[0] = ... // translated to k.fs_1 = ...
f2 := k.fs[1] // translated to f2 := k.fs_2
  1. use o pacote reflect , basicamente funciona como um array
t := Tuple(int, float64, string){1, 2.0, "variadic"}

fs := reflect.ValueOf(t).Elem().FieldByName("fs")
val := reflect.ValueOf(fs)
if val.Kind() == reflect.VariadicTypes {
    for i := 0; i < val.Len(); i++ {
        e := val.Index(i)
        switch e.Kind() {
        case reflect.Int:
            fmt.Printf("%v, ", e.Int())
        case reflect.Float64:
            fmt.Printf("%v, ", e.Float())
        case reflect.String:
            fmt.Printf("%v, ", e.String())
        }
    }
}

Nada realmente novo em comparação com o uso de uma matriz.

Por exemplo, a função variadic Min não é possível no projeto atual, mas é possível com parâmetros de tipo variadic:

func Min(type T ...Comparable)(p1 T, pn ...T) T {
    switch l := len(pn); {
    case l > 1:
        return Min(pn[0], pn[1:]...)
    case l == 1:
        if p1 >= pn[0] { return pn[0] }
        return p1
    case l < 1:
        return p1
    }
}

Isso não faz sentido para mim. Parâmetros de tipo variável só fazem sentido se os tipos puderem ser de tipos diferentes. Mas chamar Min em uma lista de tipos diferentes não faz sentido. Go não suporta o uso >= em valores de tipos diferentes. Mesmo que de alguma forma permitíssemos isso, poderíamos ser solicitados a Min(int, string)(1, "a") . Isso não tem nenhum tipo de resposta.

Embora seja verdade que o design atual não permite Min de um número variável de tipos diferentes, ele suporta chamar Min em um número variável de valores do mesmo tipo. Que eu acho que é a única maneira razoável de usar Min qualquer maneira.

func Min(type T comparable)(s ...T) T {
    if len(s) == 0 {
        panic("Min of no elements")
    }
    r := s[0]
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

Para alguns dos outros exemplos em https://github.com/golang/go/issues/15292#issuecomment -599040081, é importante observar que em Go slices e arrays têm elementos que são todos do mesmo tipo. Ao usar parâmetros de tipos variáveis, os elementos são de tipos diferentes. Portanto, não é realmente o mesmo que uma fatia ou matriz.

Embora seja verdade que o design atual não permite Min de um número variável de tipos diferentes, ele suporta chamar Min em um número variável de valores do mesmo tipo. Que eu acho que é a única maneira razoável de usar Min qualquer maneira.

func Min(type T comparable)(s ...T) T {
    if len(s) == 0 {
        panic("Min of no elements")
    }
    r := s[0]
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

Verdadeiro. Min foi um mau exemplo. Foi adicionado tarde e não tinha um pensamento claro, como você pode ver no histórico de edição de comentários. Um exemplo real é o Metric que você ignorou.

é importante notar que em Go slices e arrays possuem elementos que são todos do mesmo tipo. Ao usar parâmetros de tipos variáveis, os elementos são de tipos diferentes. Portanto, não é realmente o mesmo que uma fatia ou matriz.

Ver? Vocês são aquelas pessoas que veem isso como uma ambiguidade para array/slice ou map. Como eu disse em https://github.com/golang/go/issues/15292#issuecomment -599040081, a sintaxe é bem parecida com array/slice e map, mas está acessando elementos com tipos diferentes. Isso realmente importa? Ou pode-se provar que isso é ambiguidade? O que é possível no Go 1 é:

m := map[interface{}]int{1: 2, "2": 3, 3.0: 4}
for i, e := range m {
    println(i, e)
}

i é considerado o mesmo tipo? Aparentemente, dizemos que i é interface{} , mesmo tipo. Mas uma interface realmente expressa o tipo? Os programadores precisam verificar manualmente quais são os tipos possíveis. Ao usar for , [] e descompactar, eles realmente importam para o usuário que eles não estão acessando o mesmo tipo? Quais são os argumentos contra isso? O mesmo para fs :

for idx, f := range k.fs {
    switch f.(type) { // compare to interface{}, here is zero overhead.
    case int:
        // ...
    case float64:
        // ...
    case string:
        // ...
    }
}

Se você tiver que usar um switch de tipo para acessar um elemento de um tipo genérico variad, não vejo a vantagem. Eu posso ver como com algumas opções de técnica de compilação pode ser um pouco mais eficiente em tempo de execução do que usar interface{} . Mas acho que a diferença seria bastante pequena e não vejo por que seria mais seguro para o tipo. Não é imediatamente óbvio que vale a pena tornar a linguagem mais complexa.

Eu não pretendia ignorar o exemplo Metric , só não vejo ainda como usar tipos genéricos variadic para simplificar a escrita. Se eu precisar usar um switch de tipo no corpo de Metric , acho que prefiro escrever Metric2 e Metric3 .

Qual é a definição de "tornar a linguagem mais complexa"? Todos concordamos que os genéricos são uma coisa complexa e nunca tornarão a linguagem mais simples do que o Go 1. Você já fez grandes esforços para projetá-lo e implementá-lo, mas não está muito claro para os usuários do Go: qual é a definição de "parece escrevendo... Vá"? Existe uma métrica quantificada para medi-la? Como uma proposta de linguagem poderia argumentar que não está tornando a linguagem mais complexa? No modelo de proposta de linguagem Go 2, os objetivos são bastante diretos em sua primeira impressão:

  1. abordar um assunto importante para muitas pessoas,
  2. ter um impacto mínimo sobre todos os outros, e
  3. vêm com uma solução clara e bem compreendida.

Mas, as perguntas podem ser: Quantos são "muitos"? O que significa "importante"? Como medir o impacto em uma população desconhecida? Quando um problema é bem compreendido? Go está dominando a nuvem, mas dominar outras áreas como computação numérica científica (por exemplo, aprendizado de máquina), renderização gráfica (por exemplo, enorme mercado 3D) se tornará um dos alvos do Go? O problema se encaixa mais em "Prefiro fazer A do que B em Go & Não há caso de uso porque podemos fazer de outra maneira" ou "B não é oferecido, portanto não usamos Go & O caso de uso ainda não existe porque a linguagem não pode expressá-lo facilmente"? ... Achei que essas perguntas são dolorosas e intermináveis, e às vezes nem vale a pena respondê-las.

Voltando ao exemplo Metric , não há necessidade de acesso a indivíduos. Descompactar o conjunto de parâmetros parece não ser uma necessidade real aqui, embora as soluções que "coincidem" com a linguagem existente estejam usando [ ] indexação e dedução de tipo possam resolver o problema de type-safe:

f2 := k.fs[1] // f2 is a float64

@changkun Se houvesse métricas claras e objetivas para decidir quais recursos de linguagem são bons e ruins, não precisaríamos de designers de linguagem - poderíamos apenas escrever um programa para projetar uma linguagem ideal para nós. Mas não há - sempre se resume às preferências pessoais de algum conjunto de pessoas. Que é também, BTW, por que não faz sentido discutir se uma linguagem é "boa" ou não - a única questão é se você, pessoalmente, gosta dela. No caso do Go, as pessoas cujas preferências decidem são as pessoas da equipe Go e as coisas que você cita não são métricas, são perguntas orientadoras para ajudá-lo a convencê-los.

Pessoalmente, FWIW, sinto que os parâmetros de tipo variável falham em dois desses três. Eu não acho que eles abordam uma questão importante para muitas pessoas - o exemplo de métricas pode se beneficiar deles, mas o IMO apenas um pouco e é um caso de uso muito especializado. E eu não acho que eles vêm com uma solução clara e bem compreendida. Desconheço qualquer linguagem que suporte algo assim. Mas posso estar errado. Definitivamente seria útil, se alguém tiver exemplos de outras linguagens que suportem isso - poderia fornecer informações sobre como geralmente é implementado e, mais importante, como é usado. Talvez seja usado de forma mais ampla do que posso imaginar.

@Merovius Haskell tem funções polivariádicas como demonstramos no artigo HList: http://okmij.org/ftp/Haskell/polyvariadic.html#polyvar -fn
É claramente complexo fazer isso em Haskell, mas não impossível.

O exemplo motivador é o acesso a banco de dados seguro de tipo, onde coisas como junções e projeções seguras de tipo podem ser feitas, e o esquema de banco de dados declarado na linguagem.

Por exemplo, uma tabela de banco de dados se parece muito com um registro, onde há nomes e tipos de colunas. A operação de junção relacional usa dois registros arbitrários e produz um registro com os tipos de ambos. É claro que você pode fazer isso manualmente, mas é propenso a erros, é muito tedioso, ofusca o significado do código com todos os tipos de registro declarados manualmente e, claro, o grande recurso de um banco de dados SQL é que ele suporta ad-hoc consultas, portanto, você não pode pré-compilar todos os tipos de registro possíveis, pois não sabe necessariamente quais consultas deseja até fazê-las.

Portanto, um operador de junção relacional de tipo seguro em registros e tuplas seria um bom caso de uso. Estamos apenas pensando no tipo da função aqui - cabe ao programador o que a função realmente faz, seja uma junção na memória de duas matrizes de tuplas ou se ela gera SQL para executar em um banco de dados externo e empacotar os resultados de volta de uma forma segura para o tipo.

Esse tipo de coisa fica muito mais organizado em C# com LINQ. A maioria das pessoas parece pensar no LINQ como adicionar funções lambda e mônadas ao C#, mas não funcionaria para seu caso de uso principal sem polivariáveis, pois você simplesmente não pode definir um operador de junção de tipo seguro sem funcionalidade semelhante.

Eu acho que os operadores relacionais são importantes. Depois dos operadores básicos dos tipos Booleano, binário, int, float e string, os conjuntos provavelmente vêm em seguida, e depois as relações.

BTW, C++ também oferece, embora não queiramos argumentar que queremos esse recurso no Go porque o XXX o possui :)

Acho que seria muito estranho se k.fs[0] e k.fs[1] tivessem tipos diferentes. Não é assim que outros valores indexáveis ​​funcionam em Go.

O exemplo de métricas é baseado em https://medium.com/@sameer_74231/go -experience-report-for-generics-google-metrics-api-b019d597aaa4. Eu acho que o código requer reflexão para recuperar os valores. Eu acho que se vamos adicionar genéricos variádicos ao Go, devemos obter algo melhor do que reflexão para recuperar os valores. Caso contrário, simplesmente não parece ajudar muito.

Acho que seria muito estranho se k.fs[0] e k.fs[1] tivessem tipos diferentes. Não é assim que outros valores indexáveis ​​funcionam em Go.

O exemplo de métricas é baseado em https://medium.com/@sameer_74231/go -experience-report-for-generics-google-metrics-api-b019d597aaa4. Eu acho que o código requer reflexão para recuperar os valores. Eu acho que se vamos adicionar genéricos variádicos ao Go, devemos obter algo melhor do que reflexão para recuperar os valores. Caso contrário, simplesmente não parece ajudar muito.

Nós vamos. Você está solicitando algo que não existe. Se você não gosta [``] , há duas opções restantes: ( ) ou {``} , e vejo que você pode argumentar que parênteses se parece com uma chamada de função e as chaves se parecem com inicialização de variável. Ninguém gosta args.0 args.1 já que isso não parece Go. A sintaxe é trivial.

Na verdade, passo algum tempo de fim de semana lendo o livro "o design e a evolução do C++", há muitos insights interessantes sobre decisões e lições, embora tenha sido escrito em 1994:

_"[...] Em retrospecto, subestimei a importância das restrições na legibilidade e detecção precoce de erros."_ ==> Ótimo projeto de contrato

"_a sintaxe da função à primeira vista também parece melhor sem palavra-chave extra:_

T& index<class T>(vector<T>& v, int i) { /*...*/ }
int i = index(v1, 10);

_Parece haver problemas irritantes com essa sintaxe mais simples. É muito inteligente. É relativamente difícil identificar uma declaração de modelo em um programa porque [...] Os colchetes <...> foram escolhidos de preferência aos parênteses porque os usuários os acharam mais fáceis de ler. [...] Por acaso, Tom Pennello provou que os parênteses teriam sido mais fáceis de analisar, mas isso não muda a observação principal de que os leitores (humanos) preferem <...> _
" ==> não é semelhante a func F(type T C)(v T) T ?

_"Eu, no entanto, acho que fui muito cauteloso e conservador quando se trata de especificar recursos de modelo. Eu poderia ter incluído recursos como [...]. Esses recursos não teriam aumentado muito o fardo dos implementadores, e os usuários teriam sido ajudados."_

Por que parece tão familiar?

Os parâmetros de tipo variável de indexação (ou tupla) precisam ser separados para indexação em tempo de execução e indexação em tempo de compilação. Acho que você pode argumentar que a falta de suporte para indexação em tempo de execução pode confundir os usuários porque não é consistente com a indexação em tempo de compilação. Mesmo para indexação em tempo de compilação, um parâmetro "modelo" não tipo também está ausente no design atual.

Com todas as evidências, a proposta (exceto relato de experiência) tenta evitar discutir esse recurso, e começo a acreditar que não se trata de adicionar genéricos variados ao Go, mas apenas removê-lo por design.

Concordo que Design and Evolution of C++ é um bom livro, mas C++ e Go têm objetivos diferentes. A citação final é boa; Stroustrup nem menciona o custo da complexidade da linguagem para os usuários da linguagem. Em Go sempre tentamos considerar esse custo. Go pretende ser uma linguagem simples. Se adicionássemos todos os recursos que ajudariam os usuários, não seria simples. Como C++ não é simples.

Com todas as evidências, a proposta (exceto relato de experiência) tenta evitar discutir esse recurso, e começo a acreditar que não se trata de adicionar genéricos variados ao Go, mas apenas removê-lo por design.

Desculpe, não sei o que você quer dizer aqui.

Pessoalmente, sempre considerei a possibilidade de tipos genéricos variádicos, mas nunca tive tempo para descobrir como isso funcionaria. A maneira como funciona em C++ é muito sutil. Eu gostaria de ver se podemos primeiro fazer com que os genéricos não variádicos funcionem. Certamente há tempo para adicionar genéricos variádicos, se possível, mais tarde.

Quando critico os pensamentos anteriores, não estou dizendo que os tipos variádicos não podem ser feitos. Estou apontando problemas que acho que precisam ser resolvidos. Se eles não puderem ser resolvidos, então não estou convencido de que os tipos variádicos valham a pena.

Stroustrup nem menciona o custo da complexidade da linguagem para os usuários da linguagem. Em Go sempre tentamos considerar esse custo. Go pretende ser uma linguagem simples. Se adicionássemos todos os recursos que ajudariam os usuários, não seria simples. Como C++ não é simples.

Não é verdade OMI. Deve-se notar que C++ é o primeiro praticante que carrega genéricos (bem, ML é a primeira linguagem). Pelo que li no livro, recebo a mensagem de que C++ foi planejado para ser uma linguagem simples (não oferecer genéricos no início, loop Experiment-Simplify-Ship para design de linguagem, mesma história). O C++ também teve a fase de congelamento de recursos por vários anos, que é o que temos em Go "The Compatability Promise". Mas fica um pouco fora de controle com o tempo por causa de muitas razões razoáveis, o que não fica claro para o Go se ele pegar o antigo caminho do C++ após o lançamento dos genéricos.

Certamente há tempo para adicionar genéricos variádicos, se possível, mais tarde.

O mesmo sentimento para mim. Os genéricos variádicos também estão ausentes na primeira versão padronizada dos modelos.

Estou apontando problemas que acho que precisam ser resolvidos. Se eles não puderem ser resolvidos, então não estou convencido de que os tipos variádicos valham a pena.

Eu entendo suas preocupações. Mas o problema está basicamente resolvido, mas só precisa ser traduzido corretamente para Go (e acho que ninguém gosta da palavra "traduzir"). O que eu li da sua proposta histórica de genéricos, eles basicamente seguem o que falhou na proposta inicial do C++ e comprometeu o que Stroustrup lamentou. Estou interessado em seus contra-argumentos sobre isso.

Teremos que discordar sobre os objetivos do C++. Talvez os objetivos originais fossem mais parecidos, mas olhando para o C++ hoje, acho que está claro que seus objetivos são muito diferentes dos objetivos do Go, e acho que esse tem sido o caso há pelo menos 25 anos.

Ao escrever várias propostas para adicionar genéricos ao Go, é claro que observei como os modelos C++ funcionam, bem como muitas outras linguagens (afinal, o C++ não inventou os genéricos). Eu não olhei para o que Stroustrup lamentou, então se chegamos ao mesmo lugar, então, ótimo. Meu pensamento é que os genéricos em Go são mais como genéricos em Ada ou D do que em C++. Ainda hoje, C++ não possui contratos, que eles chamam de conceitos, mas ainda não foram adicionados à linguagem. Além disso, C++ intencionalmente permite programação complexa em tempo de compilação e, de fato, os modelos C++ são eles próprios uma linguagem Turing completa (embora eu não saiba se isso foi intencional). Sempre considerei isso algo a ser evitado para Go, pois a complexidade é extrema (embora seja mais complexa em C++ do que seria em Go devido à sobrecarga e resolução de métodos, que Go não possui).

Depois de tentar a implementação do contrato atual por cerca de um mês, estou um pouco imaginando qual será o destino das funções internas existentes. Todos eles podem ser implementados de forma genérica:

func Append(type T)(slice []T, elems ...T) []T {...}
func Copy(type T)(dst, src []T) int {...}
func Delete(type K, V)(m map[K]V, k K) {...}
func Make(type T, I Integer(I))(siz ...I) T {...}
func New(type T)() *T {...}
func Close(type T)(c chan<- T) {...}
func Panic(type T)(v T) {...}
func Recover(type T)() T {...}
func Print(type ...T)(args ...T) {...}
func Println(type ...T)(args ...T) {...}

Será que eles vão embora no Go2? Como o Go 2 poderia lidar com um impacto tão grande na base de código do Go 1 existente? Estas parecem ser questões em aberto.

Além disso, esses dois são um pouco especiais:

func Len(type T C)(t T) int {...}
func Cap(type T C)(t T) int {...}

Como implementar tal contrato C com o design atual, de modo que um parâmetro de tipo só possa ser fatia genérica []Ts , map map[Tk]Tv e canal chan Tc onde T Ts Tk Tv Tc são diferentes?

@changkun Eu não acho que "eles podem ser implementados com genéricos" seja um motivo convincente para removê-los. E você menciona uma razão bastante clara e forte pela qual eles não devem ser removidos. Então eu não acho que eles serão. Acho que isso torna o resto das perguntas obsoletas.

@changkun Eu não acho que "eles podem ser implementados com genéricos" seja um motivo convincente para removê-los. E você menciona uma razão bastante clara e forte pela qual eles não devem ser removidos.

Sim, concordo que não é o convincente para removê-los, por isso disse explicitamente. No entanto, mantê-los junto com os genéricos “viola” a filosofia existente do Go, cujas características da linguagem são ortogonais. A compatibilidade é a principal preocupação, mas a adição de contratos provavelmente matará o enorme código atual "desatualizado".

Então eu não acho que eles serão. Acho que isso torna o resto das perguntas obsoletas.

Vamos tentar não ignorar a questão e considerá-la como um caso de uso real de contratos. Se surgirem requisitos semelhantes, como poderíamos implementá-lo com o design atual?

Claramente não vamos nos livrar das funções pré-declaradas existentes.

Embora seja possível escrever uma assinatura de função parametrizada para delete , close , panic , recover , print e println , não acho que seja possível implementá-los sem depender de funções mágicas internas.

Existem versões parciais de Append e Copy em https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-contracts.md#append. Não está completo, porque append e copy têm casos especiais para um segundo argumento do tipo string , que não é suportado pelo rascunho de design atual.

Observe que a assinatura de Make , acima, não é válida de acordo com o projeto atual. New não é exatamente o mesmo que new , mas perto o suficiente.

Com o rascunho de design atual, Len e Cap teriam que receber um argumento do tipo interface{} e, como tal, não seria seguro em tempo de compilação.

https://go-review.googlesource.com/c/go/+/187317

Por favor, não use extensões de arquivo .go2 , temos módulos para fazer esse tipo de coisa de versão? Eu entendo se você está fazendo isso como uma solução temporária para facilitar a vida ao experimentar contratos, mas certifique-se de que no final o arquivo go.mod vai cuidar da mistura de pacotes go sem o necessidade de extensões de arquivo .go2 . Seria um golpe contra os desenvolvedores de módulos que se esforçam para garantir que os módulos funcionem da melhor maneira possível. Usar extensões de arquivo .go2 é como dizer, não, eu não me importo com suas coisas de módulo que vão fazer do meu jeito de qualquer maneira, porque eu não quero que meu compilador de pré-módulo go de 10 anos quebre .

Os arquivos .go2 @gertcuykens são apenas para o experimento; eles não serão usados ​​quando os genéricos chegarem ao compilador.

(Vou ocultar nossos comentários, já que eles realmente não acrescentam à discussão e são longos o suficiente.)

Recentemente eu explorei uma nova sintaxe genérica na linguagem K que eu projetei, porque K emprestou muita gramática de Go, então esta gramática Genérica também pode ter algum valor de referência para Go.

O problema identifier<T> é que ele entra em conflito com operadores de comparação e também com operadores de bits, então não concordo com esse design.

O identifier[T] do Scala tem uma aparência melhor do que o design anterior, mas depois de resolver o conflito acima, ele tem um novo conflito com o design do índice identifier[index] .
Por esse motivo, o design do índice do Scala foi alterado para identifier(index) . Isso não funciona bem para idiomas que já usam [] como índice.

No rascunho do Go, foi declarado que os genéricos usam (type T) , o que não causará conflitos, pois type é uma palavra-chave, mas o compilador ainda precisa de mais julgamento quando é chamado para resolver o identifier(type)(params) . Embora seja melhor do que as soluções acima, ainda não me satisfaz.

Por acaso, lembrei-me do desenho especial de invocação de método em OC, que me deu inspiração para um novo desenho.

E se colocarmos o identificador e o genérico como um todo e colocá-los em [] juntos?
Podemos obter os [identifier T] . Esse desenho não entra em conflito com o índice, pois deve ter pelo menos dois elementos, separados por espaços.
Quando houver vários genéricos, podemos escrever [identifier T V] assim, e isso não entrará em conflito com o design existente.

Substituindo este design em Go, podemos obter o seguinte exemplo.
Por exemplo

type [Item T] struct {
    Value T
}

func (it [Item T]) Print() {
    println(it.Value)
}

func [TestGenerics T V]() {
    var a = [Item T]{}
    a.Print()
    var b = [Item V]{}
    b.Print()
}

func main() {
    [TestGenerics int string]()
}

Isso parece muito claro.

Outro benefício de usar [] é que ele tem alguma herança do projeto Slice and Map original do Go e não causará uma sensação de fragmentação.

[]int  ->  [slice int]

map[string]int  ->  [map string int]

Podemos fazer um exemplo mais complicado

var a map[int][]map[string]map[string][]string

var b [map int [slice [map string [map string [slice string]]]]]

Este exemplo ainda mantém um efeito relativamente claro e, ao mesmo tempo, tem um pequeno impacto na compilação.

Eu implementei e testei esse design em K e funciona bem.

Acho que esse design tem um certo valor de referência e pode ser digno de discussão.

Recentemente eu explorei uma nova sintaxe genérica na linguagem K que eu projetei, porque K emprestou muita gramática de Go, então esta gramática Genérica também pode ter algum valor de referência para Go.

O problema identifier<T> é que ele entra em conflito com operadores de comparação e também com operadores de bits, então não concordo com esse design.

O identifier[T] do Scala tem uma aparência melhor do que o design anterior, mas depois de resolver o conflito acima, ele tem um novo conflito com o design do índice identifier[index] .
Por esse motivo, o design do índice do Scala foi alterado para identifier(index) . Isso não funciona bem para idiomas que já usam [] como índice.

No rascunho do Go, foi declarado que os genéricos usam (type T) , o que não causará conflitos, pois type é uma palavra-chave, mas o compilador ainda precisa de mais julgamento quando é chamado para resolver o identifier(type)(params) . Embora seja melhor do que as soluções acima, ainda não me satisfaz.

Por acaso, lembrei-me do desenho especial de invocação de método em OC, que me deu inspiração para um novo desenho.

E se colocarmos o identificador e o genérico como um todo e colocá-los em [] juntos?
Podemos obter os [identifier T] . Esse desenho não entra em conflito com o índice, pois deve ter pelo menos dois elementos, separados por espaços.
Quando houver vários genéricos, podemos escrever [identifier T V] assim, e isso não entrará em conflito com o design existente.

Substituindo este design em Go, podemos obter o seguinte exemplo.
Por exemplo

type [Item T] struct {
    Value T
}

func (it [Item T]) Print() {
    println(it.Value)
}

func [TestGenerics T V]() {
    var a = [Item T]{}
    a.Print()
    var b = [Item V]{}
    b.Print()
}

func main() {
    [TestGenerics int string]()
}

Isso parece muito claro.

Outro benefício de usar [] é que ele tem alguma herança do projeto Slice and Map original do Go e não causará uma sensação de fragmentação.

[]int  ->  [slice int]

map[string]int  ->  [map string int]

Podemos fazer um exemplo mais complicado

var a map[int][]map[string]map[string][]string

var b [map int [slice [map string [map string [slice string]]]]]

Este exemplo ainda mantém um efeito relativamente claro e, ao mesmo tempo, tem um pequeno impacto na compilação.

Eu implementei e testei esse design em K e funciona bem.

Acho que esse design tem um certo valor de referência e pode ser digno de discussão.

excelente

Depois de algumas idas e vindas e várias releituras, em geral apoio o rascunho do projeto atual para Contratos em Go. Eu aprecio a quantidade de tempo e esforço que foi dedicado a isso. Embora o escopo, os conceitos, a implementação e a maioria das compensações pareçam sólidos, minha preocupação é que a sintaxe precise ser revisada para melhorar a legibilidade.

Eu escrevi uma série de mudanças propostas para resolver isso:

Os pontos-chave são:

  • Sintaxe de chamada de método/asserção de tipo para declaração de contrato
  • O "Contrato Vazio"
  • Delimitadores não parênteses

Correndo o risco de antecipar o ensaio, darei alguns trechos de sintaxe sem explicação, convertidos de amostras no rascunho de design de Contratos atual. Observe que a forma F«T» dos delimitadores é ilustrativa, não prescritiva; veja a redação para detalhes.

type List«type Element contract{}» struct {
    next *List«Element»
    val  Element
}

e

contract viaStrings«To, From» {
    To.Set(string)
    From.String() string
}

func SetViaStrings«type To, From viaStrings»(s []From) []To {
    r := make([]To, len(s))
    for i, v := range s {
        r[i].Set(v.String())
    }
    return r
}

e

func Keys«type K comparable, V contract{}»(m map[K]V) []K {
    r := make([]K, 0, len(m))
    for k := range m {
        r = append(r, k)
    }
    return r
}

k := maps.Keys(map[int]int{1:2, 2:4})

e

contract Numeric«T» {
    T.(int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        complex64, complex128)
}

func DotProduct«type T Numeric»(s1, s2 []T) T {
    if len(s1) != len(s2) {
        panic("DotProduct: slices of unequal length")
    }
    var r T
    for i := range s1 {
        r += s1[i] * s2[i]
    }
    return r
}

Sem realmente alterar os contratos sob o capô, isso é muito mais legível para mim como desenvolvedor Go. Também me sinto muito mais confiante ao ensinar este formulário para alguém que está aprendendo Go (embora no final do currículo).

@ianlancetaylor Com base no seu comentário em https://github.com/golang/go/issues/36533#issuecomment -579484523 Estou postando neste tópico em vez de iniciar um novo problema. Ele também está listado na página de comentários genéricos . Não tenho certeza se preciso fazer mais alguma coisa para que isso seja "considerado oficialmente" (ou seja , grupo de revisão da proposta do Go 2 ?) ou se o feedback ainda está sendo coletado ativamente.

Da minuta do projeto de contratos:

Por que não usar a sintaxe F<T> como C++ e Java?
Ao analisar o código dentro de uma função, como v := F<T> , no ponto de ver o < é ambíguo se estamos vendo uma instanciação de tipo ou uma expressão usando o operador < . Resolver isso requer uma antecipação efetivamente ilimitada. Em geral, nos esforçamos para manter o analisador Go simples.

Não particularmente em conflito com meu último post: Delimitadores de chave angular para contratos Go

Apenas algumas idéias sobre como contornar esse ponto do analisador ficar confuso. Amostras de casal:

// Lifted from the design draft
func New<type K, V>(compare func(K, K) int) *Map<K, V> {
    return &Map{<K, V> compare: compare}
}

// ...

func (m *Map<K, V>) InOrder() *Iterator<K, V> {
    sender, receiver := chans.Ranger(<keyValue<K, V>>)
    var f func(*node<K, V>) bool
    f = func(n *node<K, V>) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue{<K, V> n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Essencialmente, apenas uma posição diferente para os parâmetros de tipo em cenários em que < pode ser ambíguo.

@toolbox Em relação ao seu comentário de colchete angular. Obrigado, mas para mim, pessoalmente, essa sintaxe é como primeiro tomar uma decisão de que devemos usar colchetes angulares para parâmetros de tipo e argumentos de tipo e, em seguida, descobrir uma maneira de martelá-los. Acho que, se adicionarmos genéricos ao Go, precisamos apontar para algo que se encaixe de forma clara e fácil na linguagem existente. Eu não acho que mover colchetes angulares dentro de colchetes atinge esse objetivo.

Sim, este é um detalhe menor, mas acho que quando se trata de sintaxe, detalhes menores são muito importantes. Acho que se vamos adicionar argumentos de tipo e parâmetros, eles precisam funcionar de maneira simples e intuitiva.

Eu certamente não afirmo que a sintaxe no rascunho de design atual seja perfeita, mas afirmo que ela se encaixa facilmente na linguagem existente. O que precisamos fazer agora é escrever mais código de exemplo para ver como funciona na prática. Um ponto-chave é: com que frequência as pessoas realmente precisam escrever argumentos de tipo fora das declarações de função e quão confusos são esses casos? Acho que não sabemos.

É uma boa ideia usar [] para tipos genéricos e usar () para funções genéricas? Isso seria mais consistente com os genéricos básicos atuais.

A comunidade poderia votar nele? Pessoalmente, prefiro _qualquer coisa_ a adicionar mais parênteses, já é difícil ler algumas definições de função para fechamentos etc., isso adiciona mais confusão

Não acho que um voto seja uma boa maneira de projetar uma linguagem. Especialmente com um conjunto muito difícil (provavelmente impossível) de determinar e incrivelmente grande de eleitores elegíveis.

Confio nos designers e na comunidade Go para convergir para a melhor solução e
então não senti a necessidade de pesar em nada nesta conversa.
No entanto, eu só tinha que dizer como fiquei inesperadamente encantado com o
sugestão da sintaxe F«T».

(Outros colchetes Unicode:
https://unicode-search.net/unicode-namesearch.pl?term=BRACKET.)

Saúde,

  • Prumo

Em sex, 1 de maio de 2020 às 19:43 Matt Mc [email protected] escreveu:

Após algumas idas e vindas e várias releituras, em geral apoio a
rascunho de design atual para Contratos em Go. Eu aprecio a quantidade de tempo
e esforço que tem ido para ele. Embora o escopo, os conceitos,
implementação, e a maioria das compensações parece sólida, minha preocupação é que o
sintaxe precisa ser revisada para melhorar a legibilidade.

Eu escrevi uma série de mudanças propostas para resolver isso:

Os pontos-chave são:

  • Sintaxe de chamada de método/asserção de tipo para declaração de contrato
  • O "Contrato Vazio"
  • Delimitadores não parênteses

Correndo o risco de antecipar o ensaio, darei alguns trechos de
sintaxe, convertida de amostras no rascunho de design de Contratos atual. Observação
que a forma F«T» de delimitadores é ilustrativa, não prescritiva; Vejo
a redação para obter detalhes.

type List«type Element contract{}» struct {
próximo *Lista«Elemento»
Elemento val
}

e

contrato viaStrings«Para, De» {
Para.Set(string)
From.String() string
}
func SetViaStrings«tipo Para, De viaStrings»(s []De) []Para {
r := make([]Para, len(s))
para i, v := intervalo s {
r[i].Set(v.String())
}
retornar r
}

e

Teclas func«tipo K comparável, contrato V{}»(m map[K]V) []K {
r := make([]K, 0, len(m))
para k := intervalo m {
r = anexar(r, k)
}
retornar r
}
k := maps.Keys(map[int]int{1:2, 2:4})

e

contrato Numérico«T» {
T.(int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
complexo64, complexo128)
}
func PontoProduto«tipo T Numérico»(s1, s2 []T) T {
if len(s1) != len(s2) {
panic("DotProduct: fatias de comprimento desigual")
}
var r T
para i := intervalo s1 {
r += s1[i] * s2[i]
}
retornar r
}

Sem realmente mudar os contratos sob o capô, isso é muito mais
legível para mim como desenvolvedor Go. Eu também me sinto muito mais confiante
ensinar este formulário para alguém que está aprendendo Go (embora no final do
currículo).

@ianlancetaylor https://github.com/ianlancetaylor Com base no seu comentário
em #36533 (comentário)
https://github.com/golang/go/issues/36533#issuecomment-579484523 Eu sou
postando neste tópico em vez de iniciar um novo problema. Também está listado
na página de comentários genéricos
https://github.com/golang/go/wiki/Go2GenericsFeedback . Não tenho certeza se eu
precisa fazer qualquer outra coisa para obtê-lo "oficialmente considerado" (ou seja, Go 2
grupo de revisão de propostas https://github.com/golang/go/issues/33892 ?) ou se
feedback ainda está sendo coletado ativamente.


Você está recebendo isso porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/15292#issuecomment-622657596 ou
Cancelar subscrição
https://github.com/notifications/unsubscribe-auth/AACQ2NJRBNLLDGY2XGCCQCLRPOCEHANCNFSM4CA35RXQ
.

Todos nós queremos a melhor sintaxe possível para Go. O rascunho de design usa parênteses porque funcionou com o restante do Go sem causar ambiguidades de análise significativas. Ficamos com eles porque eram a melhor solução em nossas mentes naquele momento e porque havia peixes maiores para fritar. Até agora eles (parênteses) têm se mantido razoavelmente bem.

No final do dia, se uma notação muito melhor for encontrada, isso é muito fácil de alterar, desde que não tenhamos uma garantia de compatibilidade para aderir (o analisador é ajustado trivialmente e qualquer corpo de código pode ser convertido facilmente com gofmt).

@ianlancetaylor Obrigado pela resposta, é apreciado.

Você está certo; essa sintaxe era "não use parênteses para argumentos de tipo" e escolhendo o que eu achava ser o melhor candidato, fazendo alterações para tentar aliviar os problemas de implementação com o analisador.

Se a sintaxe for difícil de ler (difícil saber o que está acontecendo de relance), ela realmente se encaixa facilmente na linguagem existente? É aí que eu acho que a postura fica aquém.

É verdade, à medida que você aborda, essa inferência de tipo pode reduzir bastante a quantidade de argumentos de tipo que precisam ser passados ​​no código do cliente. Eu pessoalmente acredito que um autor de biblioteca deve se esforçar para exigir que argumentos de tipo zero sejam passados ​​ao usar seu código, e ainda assim isso ocorrerá na prática.

Ontem à noite, por acaso, encontrei a sintaxe do modelo para D , que é surpreendentemente semelhante em alguns aspectos:

template Square(T) {
    T Square(T t) {
        return t * t;
    }
}

writefln("The square of %s is %s", 3, Square!(int)(3));

template TCopy(T) {
    void copy(out T to, T from) {
        to = from;
    }
}

int i;
TCopy!(int).copy(i, 3);

Há duas diferenças principais que vejo:

  1. Eles têm ! como o operador de instanciação para empregar os modelos.
  2. Seu estilo de declaração (sem vários valores de retorno, métodos aninhados em classes) significa que há nativamente menos parênteses no código comum, portanto, usar parênteses para parâmetros de tipo não cria a mesma ambiguidade visual.

Operador de instanciação

Ao usar Contratos, a ambiguidade visual primária é entre uma instanciação e uma chamada de função (ou uma conversão de tipo, ou...?). Parte do motivo pelo qual isso é problemático é que as instanciações são em tempo de compilação e as chamadas de função são em tempo de execução. Go tem muitas pistas visuais que informam ao leitor a que campo cada cláusula pertence, mas a nova sintaxe confunde isso, então não é óbvio se você estiver olhando para tipos ou fluxo de programa.

Um exemplo inventado:

// Instantiation with unexported types and then function call,
// or chained method call?
a := draw(square, ellipse)(canvas, color)

Proposta: use um operador de instanciação para especificar parâmetros de tipo. O ! que D usa parece perfeitamente aceitável. Alguns exemplos de sintaxe:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map!(K, V) {
    return &Map!(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator!(K, V) {
    sender, receiver := chans.Ranger!(keyValue!(K, V))()
    var f func(*node!(K, V)) bool
    f = func(n *node!(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue!(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Do meu ponto de vista pessoal, o código acima é uma ordem de magnitude mais fácil de ler. Acho que isso esclarece todas as ambiguidades, tanto visualmente quanto para o analisador. Além disso, me pergunto se essa pode ser a mudança mais importante que poderia ser feita nos Contratos.

Estilo de declaração

Ao declarar tipos e funções e métodos, há menos "tempo de execução ou tempo de compilação?" problema. Um Gopher vê uma linha começando com type ou func e sabe que está olhando para uma declaração, não para o comportamento do programa.

No entanto, algumas ambiguidades visuais ainda existem:

// Type-parameterized function,
// or function with multiple return values?
func Draw(cvs canvas, t tool)(canvas, tool) {
    // ...
}
func Draw(type canvas, tool)(cvs canvas, t tool) {
    // ...
}

// Type-parameterized struct, or function call?
func Set(elem constructible) rect {
    // ...
}
type Set(type Elem comparable) struct{
    // ...
}

// Method call, or type-parameterized function?
func Map(type Element)(s []Element, f func(Element) Element) (results []Element) {
    // ...
}
func (t Element) Map(s []Element, f func(Element) Element) (results []Element) {
    // ...
}

Pensamentos:

  • Eu acho que essas questões menos importantes do que o problema de instanciação.
  • A solução mais óbvia seria alterar os delimitadores usados ​​para argumentos de tipo.
  • Possivelmente colocar algum outro tipo de operador ou personagem lá ( ! pode se perder, e # ?) poderia desambiguar as coisas.

EDIT: @griesemer obrigado pelo esclarecimento adicional!

Obrigado. Apenas para colocar a questão natural: por que é importante saber se uma chamada específica é avaliada em tempo de execução ou em tempo de compilação? Por que essa é a pergunta chave?

@toolbox

// Instantiation with unexported types and then function call,
// or chained method call?
a := draw(square, ellipse)(canvas, color)

Por que isso importaria de qualquer maneira? Para um leitor casual, não importa se este é um pedaço de código que foi executado durante o tempo de compilação ou tempo de execução. Para todos os outros, eles podem apenas dar uma olhada na definição da função para saber o que está acontecendo. Seus exemplos posteriores não parecem ser ambíguos.

Na verdade, usar () para parâmetros de tipo faz algum sentido, pois parece que você está chamando uma função que retorna uma função - e isso é mais ou menos correto. A diferença é que a primeira função é aceitar tipos, que geralmente são maiúsculos, ou muito conhecidos.

Nesta fase, é muito mais importante descobrir as dimensões do galpão, não sua cor.

Eu não acho que o @toolbox está falando é realmente uma diferença entre tempo de compilação e tempo de execução. Sim, essa é uma diferença, mas não é a mais importante. O importante é: isso é uma chamada de função ou uma declaração de tipo? Você quer saber porque eles se comportam de maneira diferente e você não quer ter que deduzir se alguma expressão está fazendo duas chamadas de função ou uma, porque isso é uma grande diferença. Ou seja, uma expressão como a := draw(square, ellipse)(canvas, color) é ambígua sem fazer o trabalho de examinar o ambiente ao redor.

Ser capaz de analisar visualmente o fluxo de controle do programa é importante. Acho que Go foi um grande exemplo disso.

Obrigado. Apenas para colocar a questão natural: por que é importante saber se uma chamada específica é avaliada em tempo de execução ou em tempo de compilação? Por que essa é a pergunta chave?

Desculpe, parece que estraguei minha comunicação. Este é o ponto-chave que eu estava tentando passar:

não é óbvio se você estiver olhando para tipos ou fluxo de programa

(No momento, um é resolvido durante a compilação e o outro ocorre em tempo de execução, mas essas são... características, não o ponto-chave, que @infogulch pegou corretamente - obrigado!)


Eu vi a opinião em alguns lugares que os genéricos no rascunho podem ser comparados a chamadas de função: é uma espécie de função de tempo de compilação que retorna a função ou tipo real . Embora isso seja útil como um modelo mental do que está ocorrendo durante a compilação, não se traduz sintaticamente. Sintaticamente, eles devem ser nomeados como funções. Aqui está um exemplo:

// Example from the Contracts draft
Print(int)([]int{1, 2, 3})

// New naming that communicates behavior and intent
MakePrintFunc(int)([]int{1, 2, 3}) // Chained function call, great!

Lá, isso realmente se parece com uma função que retorna uma função; Eu acho que é bastante legível.

Outra maneira de fazer isso seria sufixar tudo com Type , então fica claro pelo nome que quando você "chama" a função você está recebendo um tipo. Caso contrário, não é óbvio que (por exemplo) Pair(...) produza um tipo de estrutura em vez de uma estrutura. Mas se essa convenção estiver em vigor, este código fica claro: a := drawType(square, ellipse)(canvas, color)

(Percebo que um precedente é a convenção "-er" para interfaces.)

Observe que eu particularmente não apoio o acima como uma solução, estou apenas ilustrando como acho que "genéricos como funções" não são expressos de forma completa e inequívoca pela sintaxe atual.


Novamente, @infogulch resumiu muito bem meu ponto. Sou a favor da diferenciação visual de argumentos de tipo para que fique claro que eles fazem parte do tipo .

Talvez a parte visual seja aprimorada pelo realce de sintaxe do editor.

Eu não sei muito sobre analisadores e como você não pode fazer muita antecipação.

Do ponto de vista do usuário, não quero ver mais um caractere no meu código, então «» não receberia meu suporte (não os encontrei no meu teclado!).

No entanto, ver colchetes seguidos por colchetes também não é muito agradável aos olhos.

Que tal usar colchetes simples?

a := draw{square, ellipse}(canvas, color)

Em Print(int)([]int{1,2,3}) a única diferença comportamental é "tempo de compilação vs. tempo de execução", no entanto. Sim, MakePrintFunc em vez de Print enfatizaria mais essa semelhança, mas… não é um argumento para não usar MakePrintFunc ? Porque na verdade esconde a real diferença comportamental.

FWIW, se alguma coisa você parece estar argumentando para usar separadores diferentes para funções paramétricas e tipos paramétricos. Porque Print(int) pode ser considerado equivalente a uma função que retorna uma função (avaliada em tempo de compilação), enquanto Pair(int, string) não pode - é uma função que retorna um tipo . Print(int) na verdade é uma expressão válida que resulta em um func , enquanto Pair(int, string) não é uma expressão válida, é uma especificação de tipo. Portanto, a diferença real no uso não é "funções genéricas versus não genéricas", é "funções genéricas versus tipos genéricos". E a partir desse POV, acho que há um forte argumento a ser feito para usar () pelo menos para funções paramétricas, porque enfatiza a natureza das funções paramétricas para realmente representar valores - e talvez devêssemos usar <> para tipos paramétricos.

Eu acho que o argumento para () para tipos paramétricos vem da programação funcional, onde esses tipos de retorno de funções são um conceito real chamado construtores de tipo e podem realmente ser usados ​​e referenciados como funções. E FWIW, é também por isso que eu não argumentaria para não usar () para tipos paramétricos. Pessoalmente, estou muito confortável com esse conceito e preferiria a vantagem de menos separadores diferentes, à vantagem de desambiguar funções paramétricas de tipos paramétricos - afinal, não temos problemas com identificadores puros referentes a tipos ou valores também .

Eu não acho que o @toolbox está falando é realmente uma diferença entre tempo de compilação e tempo de execução. Sim, essa é uma diferença, mas não é a mais importante. O importante é: isso é uma chamada de função ou uma declaração de tipo? Você _quer_ saber porque eles se comportam de maneira diferente e você não quer ter que deduzir se alguma expressão está fazendo duas chamadas de função ou uma, porque isso é uma grande diferença. Ou seja, uma expressão como a := draw(square, ellipse)(canvas, color) é ambígua sem fazer o trabalho de examinar o ambiente ao redor.

Ser capaz de analisar visualmente o fluxo de controle do programa é importante. Acho que Go foi um grande exemplo disso.

As declarações de tipo seriam muito fáceis de ver, pois todas começam com a palavra-chave type . Seu exemplo obviamente não é um deles.

Talvez a parte visual seja aprimorada pelo realce de sintaxe do editor.

Eu acho que, idealmente, a sintaxe deve ser clara, não importa a cor. Esse foi o caso do Go, e não acho que seria bom cair desse padrão.

Que tal usar colchetes simples?

Eu acredito que isso infelizmente entra em conflito com um literal de estrutura.

Em Print(int)([]int{1,2,3}) a única diferença comportamental é "tempo de compilação vs. tempo de execução", no entanto. Sim, MakePrintFunc em vez de Print enfatizaria mais essa semelhança, mas… não é um argumento para não usar MakePrintFunc ? Porque na verdade esconde a real diferença comportamental.

Bem, por um lado, é por isso que eu apoiaria Print!(int)([]int{1,2,3}) acima MakePrintFunc(int)([]int{1,2,3}) . É claro que algo único está acontecendo.

Mas, novamente, a pergunta que @ianlancetaylor fez anteriormente: por que importa se o tipo de instanciação/função de retorno de função é tempo de compilação versus tempo de execução?

Pensando nisso, se você escrevesse algumas chamadas de função e o compilador conseguisse otimizá-las e calcular seu resultado em tempo de compilação, você ficaria feliz pelo ganho de desempenho! Em vez disso, o aspecto importante é o que o código está fazendo, qual é o comportamento? Isso deve ser óbvio à primeira vista.

Quando vejo Print(...) meu primeiro instinto é "essa é uma chamada de função que grava em algum lugar". Ele não comunica "isso retornará uma função". Na minha opinião, qualquer um deles é melhor porque pode comunicar o comportamento e a intenção:

  • MakePrintFunc(...)
  • Print!(...)
  • Print<...>

Em outras palavras, esse pedaço de código "refere" ou de alguma forma "me dá" uma função que agora pode ser chamada no código a seguir.

FWIW, se alguma coisa você parece estar argumentando para usar separadores diferentes para funções paramétricas e tipos paramétricos. ...

Não, eu sei que os últimos exemplos foram sobre funções, mas eu defendo uma sintaxe consistente para funções paramétricas e tipos paramétricos. Eu não acredito que a equipe Go adicionaria Genéricos em Go a menos que eles sejam um conceito unificado com uma sintaxe unificada.

Quando vejo Print(...) meu primeiro instinto é "essa é uma chamada de função que grava em algum lugar". Ele não comunica "isso retornará uma função".

Nem func Print(…) func(…) , quando chamado como Print(…) . No entanto, estamos coletivamente bem com isso. Sem uma sintaxe de chamada especial, se uma função retornar um func .
A sintaxe Print(…) diz exatamente o que ela faz hoje: Que Print é uma função que retorna algum valor, que é o que Print(…) avalia. Se você estiver interessado no tipo que a função retorna, veja sua definição.
Ou, muito mais provavelmente, use o fato de que na verdade é Print(…)(…) como um indicador de que ele retorna uma função.

Pensando nisso, se você escrevesse algumas chamadas de função e o compilador conseguisse otimizá-las e calcular seu resultado em tempo de compilação, você ficaria feliz pelo ganho de desempenho!

Certo. Nós já temos isso. E estou muito feliz por não precisar de anotações sintáticas específicas para torná-las especiais, mas posso apenas confiar que o compilador fornecerá heurísticas continuamente aprimoradas sobre quais funções são essas.

Na minha opinião, qualquer um deles é melhor porque pode comunicar o comportamento e a intenção:

Observe que o primeiro pelo menos é 100% compatível com o design. Ele não prescreve nenhum formulário para os identificadores usados ​​e espero que você não sugira prescrever isso (e se você fizer isso, eu estaria interessado em saber por que as mesmas regras não se aplicam apenas ao retorno de func ).

Não, eu sei que os últimos exemplos foram sobre funções, mas eu defendo uma sintaxe consistente para funções paramétricas e tipos paramétricos.

Bem, concordo, como disse :) Só estou dizendo que não entendo como os argumentos que você está fazendo podem ser aplicados ao longo do eixo "genérico vs. não genérico", pois não há mudanças comportamentais importantes entre os dois. Eles fariam sentido ao longo do eixo "tipo vs. função", porque se algo é uma especificação de tipo ou uma expressão é muito importante para o contexto em que pode ser usado. Eu ainda não concordaria, mas pelo menos entenderia eles :)

@Merovius obrigado pelo seu comentário.

Nem func Print(…) func(…) , quando chamado como Print(…) . No entanto, estamos coletivamente bem com isso. Sem uma sintaxe de chamada especial, se uma função retornar um func.
A sintaxe Print(…) diz exatamente o que ela faz hoje: Que Print é uma função que retorna algum valor, que é o que Print(…) avalia. Se você estiver interessado no tipo que a função retorna, veja sua definição.

Acredito que o nome de uma função deve estar relacionado ao que ela faz. Portanto, espero que Print(...) imprima algo, independentemente do que retornar. Acredito que essa seja uma expectativa razoável e que possa ser encontrada na maioria dos códigos Go existentes.

Se eu vir Print(...)(...) , ele comunica que o primeiro () imprimiu algo e que a função retornou algum tipo de função, e o segundo () está executando esse comportamento adicional .

(Eu ficaria surpreso se essa fosse uma opinião incomum ou rara, mas não discutiria com alguns resultados da pesquisa.)

Observe que o primeiro pelo menos é 100% compatível com o design. Ele não prescreve nenhum formulário para os identificadores usados ​​e espero que você não sugira prescrever isso (e, se o fizer, estou interessado em saber por que as mesmas regras não se aplicam apenas ao retorno de um func).

Você está certo, eu sugeri isso :)

Olha, eu listei as 3 maneiras que eu poderia pensar para corrigir a ambiguidade visual introduzida pelos parâmetros de tipo em funções e tipos. Se você não vir nenhuma ambiguidade, então você não vai gostar de nenhuma das sugestões!

Só estou dizendo que não entendo como os argumentos que você está fazendo podem ser aplicados ao longo do eixo "genérico versus não genérico", já que não há mudanças comportamentais importantes entre os dois. Eles fariam sentido ao longo do eixo "tipo vs. função", porque se algo é uma especificação de tipo ou uma expressão é muito importante para o contexto em que pode ser usado.

Veja os pontos acima sobre ambiguidade e 3 soluções propostas.

Parâmetros de tipo são uma coisa nova.

  • Se quisermos raciocinar sobre eles como uma coisa nova , proponho alterar delimitadores ou adicionar um operador de instanciação para diferenciá-los totalmente do código regular: chamadas de função, conversões de tipo etc.
  • Se quisermos raciocinar sobre elas como apenas mais uma função , proponho nomear essas funções claramente, de modo que identifier em identifier(...) comunique o comportamento e o valor de retorno.

Eu prefiro o primeiro. Em ambos os casos, as alterações seriam globais na sintaxe do parâmetro de tipo, conforme discutido.

Há algumas outras maneiras de esclarecer isso:

  1. Pesquisa
  2. Tutorial

1. Pesquisa

Prefácio: Isto não é uma democracia. Eu acho que as decisões são baseadas em dados, e tanto a lógica articulada quanto os dados amplos da pesquisa podem ajudar no processo de decisão.

Eu não tenho os meios para fazer isso, mas eu estaria interessado em saber o que aconteceria se você pesquisasse alguns milhares de Esquilos em "classificar estes por clareza".

Linha de base:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map(K, V) {
    return &Map(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator(K, V) {
    sender, receiver := chans.Ranger(keyValue(K, V))()
    var f func(*node(K, V)) bool
    f = func(n *node(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Operador de instanciação:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map!(K, V) {
    return &Map!(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator!(K, V) {
    sender, receiver := chans.Ranger!(keyValue!(K, V))()
    var f func(*node!(K, V)) bool
    f = func(n *node!(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue!(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Chaves de ângulo: (ou chaves de ângulo duplo, de qualquer maneira)

// Lifted from the design draft
func New<type K, V>(compare func(K, K) int) *Map<K, V> {
    return &Map<K, V>{compare: compare}
}

// ...

func (m *Map<K, V>) InOrder() *Iterator<K, V> {
    sender, receiver := chans.Ranger<keyValue<K, V>>()
    var f func(*node<K, V>) bool
    f = func(n *node<K, V>) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue<K, V>{n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Funções apropriadamente nomeadas:

// Lifted from the design draft
func NewConstructor(type K, V)(compare func(K, K) int) *MapType(K, V) {
    return &MapType(K, V){compare: compare}
}

// ...

func (m *MapType(K, V)) InOrder() *IteratorType(K, V) {
    sender, receiver := chans.RangerType(keyValueType(K, V))()
    var f func(*nodeType(K, V)) bool
    f = func(n *nodeType(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValueType(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

...Engraçado, eu realmente gosto bastante do último.

(Como você acha que isso se sairia no amplo mundo de Gophers @Merovius ?)

2. Tutorial

Acho que esse seria um exercício muito útil: escreva um tutorial amigável para iniciantes para sua sintaxe favorita e peça para algumas pessoas lerem e aplicarem. Com que facilidade os conceitos são comunicados? Quais são as perguntas frequentes e como você as responde?

O rascunho do design destina-se a comunicar o conceito a Gophers experientes. Ele segue a cadeia da lógica, mergulhando você lentamente. Qual é a versão concisa? Como você explica as Regras de Ouro dos Contratos em um post de blog de fácil assimilação?

Isso pode apresentar um tipo de ângulo ou fatia de dados diferente dos relatórios de feedback típicos.

@toolbox Acho que o que você ainda não respondeu é: Por que isso é um problema para funções paramétricas, mas não para funções não paramétricas que retornam func ? Posso, hoje, escrever

func Print(a string) func(string) {
    return func(b string) {
        fmt.Println(a+b)
    }
}

func main() {
    Print("foo")("bar")
}

Por que isso é bom e não leva você a ficar super confuso com a ambiguidade, mas assim que Print pega um parâmetro de tipo em vez de um parâmetro de valor, isso fica insuportável? E você (deixando de lado as questões óbvias de compatibilidade) também sugeriria que adicionássemos uma restrição para funcionar corretamente, que isso não deveria ser possível, a menos que Print fosse renomeado para MakeXFunc por alguns X ? Se não, por que não?

@toolbox isso realmente seria um problema quando a suposição é que a inferência de tipos pode muito bem remover a necessidade de especificar os tipos paramétricos para funções, deixando apenas uma chamada de função de aparência simples?

@Merovius Eu não acho que o problema seja com a sintaxe Print("foo")("bar") si, porque já é possível em Go 1, justamente porque tem uma única interpretação possível . O problema é que com a proposta não modificada a expressão Foo(X)(Y) agora é ambígua e pode significar que você está fazendo duas chamadas de função (como em Go 1), ou pode significar que você está fazendo uma chamada de função com argumentos de tipo . O problema é conseguir deduzir localmente o que o programa faz, e essas duas interpretações semânticas possíveis são muito diferentes .

@urandom Concordo que a inferência de tipos pode eliminar a maior parte dos parâmetros de tipo fornecidos explicitamente, mas não acho que empurrar toda a complexidade cognitiva para os cantos escuros da linguagem apenas porque raramente são usados ​​é uma boa ideia qualquer. Mesmo que seja raro o suficiente que a maioria das pessoas normalmente não os encontre, eles ainda o encontrarão às vezes, e permitir que algum código tenha um fluxo de controle confuso, desde que não seja "a maioria" do código, deixa um gosto ruim na boca. Especialmente porque o Go é atualmente tão acessível ao ler o código "encanamento", incluindo stdlib. Talvez a inferência de tipos seja tão boa que "raro" se torne "nunca", e os programadores Go permaneçam altamente disciplinados e nunca projetem um sistema onde os parâmetros de tipo sejam necessários; então toda essa questão é basicamente discutível. Mas eu não apostaria nisso.

Acho que o principal objetivo do argumento do @toolbox é que não devemos sobrecarregar alegremente a sintaxe existente com semântica sensível ao contexto e, em vez disso, devemos encontrar alguma outra sintaxe que não seja ambígua (mesmo que esteja apenas fazendo uma pequena adição, como Foo(X)!(Y) .) Acho que essa é uma medida importante ao considerar as opções de sintaxe.

Eu usei e li um pouco de código D , nos dias (~2008-2009), e devo dizer que o ! estava sempre me enganando.

deixe-me pintar este galpão com # , $ ou @ , em vez disso (já que eles não têm nenhum significado em Go ou C).
isso poderia abrir a possibilidade de usar chaves sem qualquer confusão com mapas, fatias ou estruturas.

  • Foo@{X}(Y)
  • Foo${X}(Y)
  • Foo#{X}(Y)
    ou colchetes.

Em discussões como essa, é essencial olhar para o código real.

Por exemplo, considere que poucas pessoas escrevem Foo(X)(Y) . Em Go, nomes de tipos e nomes de variáveis ​​e nomes de funções parecem exatamente os mesmos, mas as pessoas raramente ficam confusas sobre o que estão vendo. As pessoas entendem que int64(v) é uma conversão de tipo e F(v) é uma chamada de função, mesmo que pareçam exatamente iguais.

Precisamos olhar para o código real para ver se os argumentos de tipo são realmente confusos na prática. Se forem, devemos ajustar a sintaxe. Na ausência de código real, simplesmente não sabemos.

Em quarta-feira, 6 de maio de 2020, às 13h, Ian Lance Taylor escreveu:

As pessoas entendem que int64(v) é uma conversão de tipo e F(v) é uma
chamada de função, mesmo que pareçam exatamente iguais.

Eu não tenho uma opinião de uma forma ou de outra agora sobre a proposta
sintaxe, mas não acho que este exemplo em particular seja muito bom. Pode
ser verdade para tipos embutidos, mas na verdade fiquei confuso com isso
problema exato várias vezes (eu estava procurando por uma função
definição e estar muito confuso sobre como o código estava funcionando antes
Percebi que provavelmente era um tipo e não consegui encontrar a função porque
não era uma chamada de função). Não é o fim do mundo, e
provavelmente não é um problema para pessoas que gostam de IDEs sofisticados, mas eu
desperdiçou 5 minutos ou mais grepping ao redor para isso várias vezes.

—Sam

--
Sam Whited

@ianlancetaylor uma coisa que notei pelo seu exemplo é que você pode escrever uma função que recebe um tipo e retorna outro tipo com o mesmo significado, então chamar um tipo como uma conversão de tipo básico como int64(v) faz sentido no da mesma forma que strconv.Atoi(v) faz sentido.

Mas enquanto você pode fazer UseConverter(strconv.Atoi) , UseConverter(int64) não é possível no Go 1. Ter o parêntese para o parâmetro de tipo pode abrir algumas possibilidades se o genérico puder ser usado para conversão como:

func StrToNumber(type K)(s string) K {
  asInt := strconb.Atoi(s)
  return K(asInt)
}

Por que isso é bom e não leva você a ficar super confuso com a ambiguidade

Seu exemplo não está certo. Eu não me importo se a primeira chamada leva argumentos ou parâmetros de tipo. Você tem uma função Print que não imprime nada. Você pode imaginar ler/revisar esse código? Print("foo") com o segundo conjunto de parênteses omitido parece bom, mas é secretamente um não-op.

Se você enviasse esse código para mim em um PR, eu diria para você mudar o nome para PrintFunc ou MakePrintFunc ou PrintPlusFunc ou algo que comunicasse seu comportamento.

Eu usei e li um pouco de código D, antigamente (~2008-2009), e devo dizer que o ! estava sempre me enlouquecendo.

Ha, interessante. Não tenho nenhuma preferência particular por um operador de instanciação; essas parecem opções decentes.

Em Go, nomes de tipos e nomes de variáveis ​​e nomes de funções parecem exatamente os mesmos, mas as pessoas raramente ficam confusas sobre o que estão vendo. As pessoas entendem que int64(v) é uma conversão de tipo e F(v) é uma chamada de função, mesmo que pareçam exatamente iguais.

Concordo, as pessoas geralmente podem diferenciar rapidamente entre conversões de tipo e chamadas de função. Por que você acha que é isso?

Minha teoria pessoal é que os tipos geralmente são substantivos e as funções geralmente são verbos. Então quando você vê Noun(...) é bem claro que é uma conversão de tipo, e quando você vê Verb(...) é uma chamada de função.

Precisamos olhar para o código real para ver se os argumentos de tipo são realmente confusos na prática. Se forem, devemos ajustar a sintaxe. Na ausência de código real, simplesmente não sabemos.

Isso faz sentido.

Pessoalmente, cheguei a este tópico porque li o rascunho de Contratos (provavelmente 5 vezes, cada vez pulando e indo mais longe quando voltei mais tarde) e achei a sintaxe confusa e desconhecida. Gostei dos conceitos quando finalmente os groquei, mas havia uma barreira enorme por causa da sintaxe ambígua.

Há muito "código real" na parte inferior do rascunho de Contratos, lidando com todos esses casos de uso comuns, o que é ótimo! No entanto, acho complicado analisar visualmente; Estou mais lento lendo e entendendo o código. Parece-me que tenho que olhar para os argumentos das coisas e o contexto mais amplo para saber o que são as coisas e qual é o fluxo de controle, e parece que isso está um passo abaixo do código normal.

Vamos pegar este código real:

import "container/orderedmap"

var m = orderedmap.New(string, string)(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

Quando leio orderedmap.New( , espero que o que se segue sejam os argumentos para a função New , aquelas informações-chave que o mapa ordenado precisa para funcionar. Mas esses estão na verdade no segundo conjunto de parênteses. Eu sou jogado por isso. Isso torna o código mais difícil de grocar.

(Este é apenas um exemplo, não é tudo o que vejo que é ambíguo, mas é difícil ter uma discussão detalhada sobre um amplo conjunto de pontos.)

Aqui está o que eu sugeriria:

// Instantiation operator
var m = orderedmap.New!(string, string)(strings.Compare)
// Alternate delimiters -- notice I don't insist on any particular kind
var m = orderedmap.New<|string, string|>(strings.Compare)
// Appropriately named function
var m = orderedmap.MakeConstructor(string, string)(strings.Compare)

Nos dois primeiros exemplos, uma sintaxe diferente serve para quebrar minha suposição de que o primeiro conjunto de parênteses contém os argumentos para New() , portanto, o código é menos surpreendente e o fluxo é mais observável em um nível alto.

A terceira opção usa nomenclatura para tornar o fluxo não surpreendente. Agora espero que o primeiro conjunto de parênteses contenha os argumentos necessários para criar uma função construtora e espero que o valor de retorno seja uma função construtora que, por sua vez, possa ser chamada para produzir um mapa ordenado.


Eu posso com certeza ler o código no estilo atual. Consegui ler todo o código no rascunho de Contratos. É apenas mais lento porque leva mais tempo para processá-lo. Eu tentei o meu melhor para analisar por que isso acontece e denunciá-lo: além do exemplo orderedmap.New , https://github.com/golang/go/issues/15292#issuecomment -623649521 tem um bom resumo , embora eu provavelmente poderia vir com mais. O grau de ambiguidade varia entre os diferentes exemplos.

Reconheço que não obterei o acordo de todos, porque a legibilidade e a clareza são um tanto subjetivas e talvez influenciadas pelo histórico e pelos idiomas favoritos da pessoa. Eu acho que 4 tipos de ambiguidades de análise são um bom indicador de que temos um problema.

import "container/orderedmap"

var m = orderedmap.NewOf(string, string)(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

Acho que NewOf lê melhor que New porque New geralmente retorna uma instância, não um genérico que cria uma instância.


Você tem uma função Print que não imprime nada.

Para ser claro, uma vez que há alguma inferência de tipo automática, Print(foo) genérico seria uma chamada de impressão real via inferência ou um erro. Em Go hoje, identificadores simples não são permitidos :

package main

import (
    "fmt"
)

func main() {
    fmt.Println
}

./prog.go:8:5: fmt.Println evaluated but not used

Eu me pergunto se existe alguma maneira de tornar a inferência genérica menos confusa.

@toolbox

Seu exemplo não está certo. Eu não me importo se a primeira chamada leva argumentos ou parâmetros de tipo. Você tem uma função de impressão que não imprime nada. Você pode imaginar ler/revisar esse código?

Você omitiu as perguntas de acompanhamento relevantes aqui. Concordo com você que não é realmente legível. Mas você está defendendo uma aplicação dessa restrição em nível de linguagem. Eu não estava dizendo "você está bem com isso" significando "você está bem com este código", mas significando "você está bem com a linguagem que permite esse código".

Qual foi a minha pergunta de acompanhamento. Você acha que Go é uma linguagem pior, porque não colocou uma restrição de nome para funções que retornam func ? Se não, por que seria uma linguagem pior se não colocássemos essa restrição em tais funções, quando elas tomam um argumento de tipo em vez de um argumento de valor?

@Merovius

Mas você está defendendo uma aplicação dessa restrição em nível de linguagem.

Não, ele está argumentando que confiar em padrões de nomenclatura é uma solução válida em potencial para o problema. Uma regra informal como "os autores de tipos são encorajados a nomear seus tipos genéricos de uma maneira que seja menos facilmente confundida com o nome de uma função" é uma solução válida para o problema de ambiguidade, pois literalmente resolveria o problema em casos individuais.

Ele não sugere em nenhum lugar que esta solução deve ser imposta pela linguagem, ele está dizendo que se os mantenedores decidirem manter a proposta atual como está, mesmo assim , existem soluções práticas potenciais para o problema da ambiguidade. E ele está afirmando que o problema da ambiguidade é real e importante a ser considerado.

Edit: Acho que estamos nos desviando um pouco do curso. Acho que um código de exemplo mais "real" seria muito benéfico para a conversa neste momento.

Não, ele está argumentando que confiar em padrões de nomenclatura é uma solução válida em potencial para o problema.

São eles? Tentei perguntar especificamente:

Observe que o primeiro pelo menos é 100% compatível com o design. Ele não prescreve nenhum formulário para os identificadores usados ​​e espero que você não sugira prescrever isso (e, se o fizer, estou interessado em saber por que as mesmas regras não se aplicam apenas ao retorno de um func).

Você está certo, eu sugeri isso :)

Concordo que "prescrever" não é extremamente específico aqui, mas essa é pelo menos a pergunta que eu pretendia. Se eles de fato não estão argumentando a favor de um requisito de nível de linguagem embutido no design, peço desculpas pelo mal-entendido, é claro. Mas me sinto justificado em assumir que "prescrever" é pelo menos mais forte do que "uma regra informal", pelo menos. Especialmente se colocado no contexto das outras duas sugestões que eles apresentam (no mesmo pé) que são construções de nível de linguagem, pois nem usam identificadores válidos atualmente.

Haverá um plano do tipo vgo para permitir que a comunidade experimente a mais recente proposta genérica?

Depois de brincar um pouco com o playground habilitado por contrato, eu realmente não vejo o motivo de todo o alarido sobre a necessidade de diferenciar entre os argumentos de tipo e os regulares.

Considere este exemplo . Deixei os inicializadores de tipo em todas as funções, embora pudesse omitir todos eles e ainda compilaria bem. Isso parece indicar que a grande maioria desse código em potencial nem mesmo os incluiria, o que, por sua vez, não causaria confusão.

No entanto, caso esses parâmetros de tipo sejam incluídos, algumas observações podem ser feitas:
a) os tipos são os embutidos, que todos conhecem e podem identificar imediatamente
b) os tipos são de terceiros e, nesse caso, serão TitleCased, o que os destacaria bastante. Sim, seria possível, embora improvável, que fosse uma função que retornasse outra função, e a primeira chamada consumisse variáveis ​​exportadas de terceiros, mas acho que isso é extremamente raro.
c) os tipos são alguns tipos privados. Nesse caso, eles se pareceriam mais com identificadores de variáveis ​​regulares. No entanto, como eles não são exportados, isso significaria que o código que o leitor está visualizando não faz parte de alguma documentação que eles estão tentando decifrar e, mais importante, eles já estão lendo o código. Portanto, eles podem fazer a etapa extra e simplesmente pular para a definição da função para remover qualquer ambiguidade.

O barulho é sobre como fica sem genéricos https://play.golang.org/p/7BRdM2S5dwQ e para alguém que é novo em programar um Stack separado para cada tipo como StackString, StackInt, ... é muito mais fácil de programar então um Stack(T) na proposta de sintaxe genérica atual. Não tenho dúvidas de que a proposta atual é bem pensada, conforme mostrado pelo seu exemplo, mas o valor da simplicidade e clareza diminui muito. Entendo que a primeira prioridade é descobrir se funciona por meio de testes, mas uma vez que concordamos que a proposta atual cobre a maioria dos casos e não há dificuldades técnicas do compilador, uma prioridade ainda maior é torná-la compreensível para todos, o que sempre foi o motivo número um da Vá com sucesso desde o início.

@Merovius Não, é como @infogulch disse, eu quis dizer criar uma convenção a la -er nas interfaces. Eu mencionei isso acima, desculpe a confusão. (Eu sou um "ele" btw.)

Considere este exemplo. Deixei os inicializadores de tipo em todas as funções, embora pudesse omitir todos eles e ainda compilaria bem. Isso parece indicar que a grande maioria desse código em potencial nem mesmo os incluiria, o que, por sua vez, não causaria confusão.

Que tal o mesmo exemplo em uma versão bifurcada do playground genérico?

Eu usei ::<> para a cláusula de parâmetro de tipo e, se houver um único tipo, você pode omitir o <> . Não deve haver nenhuma ambiguidade no analisador nas chaves angulares, e isso facilita a leitura do código - tanto os genéricos quanto o código usando os genéricos. (E se os parâmetros de tipo forem inferidos, tanto melhor.)

Como eu disse anteriormente, eu não estava preso em ! para instanciação de tipo (e acho que :: fica melhor após a revisão). E isso só ajuda onde os genéricos são usados, não tanto nas declarações. Portanto, isso combina um pouco os dois, omitindo o <> quando desnecessário, um pouco como omitir () para parâmetros de retorno de função, se houver apenas um.

Excerto de amostra:

type Stack::<type E> []E

func (s Stack::E) Peek() E {
    return s[len(s)-1]
}

func (s *Stack::E) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack::E) Push(value E) {
    *s = append(*s, value)
}

type StackIterator::<type E> struct{
    stack Stack::E
    current int
}

func (s *Stack::E) Iter() Iterator::E {
    it := StackIterator::E{stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator::E) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator::E) Value() E { 
    if i.current < 0 {
        var zero E
        return zero
    }

    return i.stack[i.current]
}

// ...

var it Iterator::string = stack.Iter()

it = Filter::string(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::<string, string>(it, func(s string) string {
    return s + ":1"
})

it = Distinct::string(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

Para este exemplo, também ajustei os nomes das variáveis, acho que E para "Element" é mais legível do que T para "Type".

Como eu disse, ao fazer os genéricos parecerem diferentes, o código Go subjacente se torna visível. Você sabe o que está vendo, o fluxo de controle é óbvio, não há ambiguidade, etc.

Também é bom com mais inferência de tipo:

var it Iterator::string = stack.Iter()

it = Filter(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::<string, string>(it, func(s string) string {
    return s + ":1"
})

it = Distinct(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

@toolbox Desculpas, então, estávamos conversando um com o outro :)

alguém que é novo em programar um Stack separado para cada tipo, como StackString, StackInt, ... é muito mais fácil de programar do que um Stack (T)

Eu realmente ficaria surpreso se fosse assim. Ninguém é infalível, e o primeiro bug que se infiltrar mesmo em um simples pedaço de código irá martelar o quão errada essa declaração está a longo prazo.

O objetivo do meu exemplo era ilustrar o uso de funções paramétricas e sua instanciação com tipos concretos, que é o cerne desta discussão, não se a implementação de amostra Stack fosse boa ou não.

O objetivo do meu exemplo era ilustrar o uso de funções paramétricas e sua instanciação com tipos concretos, que é o cerne desta discussão, não se a implementação do Stack de amostra era boa ou não.

Eu não acho que @gertcuykens pretendia derrubar sua implementação do Stack, parece que ele sentiu que a sintaxe genérica é desconhecida e difícil de entender.

No entanto, caso esses parâmetros de tipo sejam incluídos, algumas observações podem ser feitas:
(a)...(b)...(c)...(d)...

Eu vejo todos os seus pontos, aprecio sua análise, e eles não estão errados. Você está certo de que, na maioria dos casos, examinando o código de perto, você pode determinar o que ele está fazendo. Eu não acho que isso refuta os relatórios de desenvolvedores de Go que dizem que a sintaxe é confusa, ambígua ou leva mais tempo para ler, mesmo que eles possam ler.

Em uma base geral, a sintaxe está em um vale estranho. O código está fazendo algo diferente, mas parece semelhante o suficiente às construções existentes para que suas expectativas sejam lançadas e a visibilidade caia. Você também não pode estabelecer novas expectativas porque (apropriadamente) esses elementos são opcionais, tanto como um todo quanto em partes.

Para aqueles casos patológicos mais específicos, @infogulch afirmou bem:

Também não acho que empurrar toda a complexidade cognitiva para os cantos escuros da linguagem só porque raramente são usados ​​seja uma boa ideia. Mesmo que seja raro o suficiente que a maioria das pessoas normalmente não os encontre, eles ainda o encontrarão às vezes, e permitir que algum código tenha um fluxo de controle confuso, desde que não seja "a maioria" do código, deixa um gosto ruim na boca.

Acho que, neste ponto, estamos atingindo a saturação da articulação nesta fatia específica do tópico. Não importa o quanto falemos sobre isso, o teste decisivo será quão rápido e quão bem os desenvolvedores de Go podem aprendê-lo, lê-lo e escrevê-lo.

(E sim, antes que seja apontado, o ônus deve ser do autor da biblioteca, não do desenvolvedor do cliente, mas não acho que queremos o Efeito Boost onde as bibliotecas genéricas são ininteligíveis para o homem na rua. Eu também não quero Não quero que Go se transforme em um Jamboree Genérico, mas em parte confio que as omissões do design limitarão a difusão .)

Temos um playground e podemos fazer bifurcações para outras sintaxes , o que é fantástico. Talvez precisemos de ainda mais ferramentas!

As pessoas deram feedback . Tenho certeza de que mais feedback é necessário e talvez precisemos de sistemas de feedback melhores ou mais simplificados.

@toolbox Você acha que é possível analisar o código quando você sempre omite <> e type assim? Talvez exija uma proposta mais rígida no que pode ser feito, mas talvez valha a pena a troca?

type Stack::E []E

func (s Stack::E) Peek() E {
    return s[len(s)-1]
}

func (s *Stack::E) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack::E) Push(value E) {
    *s = append(*s, value)
}

type StackIterator::E struct{
    stack Stack::E
    current int
}

func (s *Stack::E) Iter() Iterator::E {
    it := StackIterator::E{stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator::E) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator::E) Value() E { 
    if i.current < 0 {
        var zero E
        return zero
    }

    return i.stack[i.current]
}

// ...

var it Iterator::string = stack.Iter()

it = Filter::string(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::string, string (it, func(s string) string {
    return s + ":1"
})

it = Distinct::string(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

Não sei por que, mas esse Map::string, string (... parece estranho. Parece que isso cria 2 tokens, um Map::string e uma chamada de função string .

Além disso, mesmo que isso não seja usado em Go, usar "Identifier :: Identifier" pode dar a impressão errada para usuários iniciantes, pensando que há uma classe/namespace Filter com um string função

Você acha que é possível analisar o código quando você sempre omite <> e digita assim? Talvez exija uma proposta mais rígida no que pode ser feito, mas talvez valha a pena a troca?

Não, eu não penso assim. Concordo com @urandom que o caractere de espaço, sem nada delimitando, faz parecer dois tokens. Pessoalmente, também gosto do escopo dos Contratos e não estou interessado em alterar suas capacidades.

Além disso, mesmo que isso não seja usado em Go, usar "Identifier::Identifier" pode dar a impressão errada para usuários iniciantes, pensando que há uma classe/namespace Filter com uma função de string nele. Reutilizar tokens de outras linguagens amplamente adotadas para algo completamente diferente causará muita confusão.

Eu realmente não usei uma linguagem com :: mas eu já vi isso por aí. Talvez ! seja melhor porque combinaria com D, embora eu ache que :: parece melhor visualmente.

Se seguirmos esse caminho, pode haver muita discussão sobre especificamente quais personagens usar. Aqui está uma tentativa de restringir o que estamos procurando:

  • Algo diferente de identifier() para que não pareça uma chamada de função.
  • Algo que pode incluir vários parâmetros de tipo, para uni-los visualmente da maneira que os parênteses podem.
  • Algo que parece conectado ao identificador, então parece uma unidade.
  • Algo que não seja ambíguo para o analisador.
  • Algo que não entre em conflito com um conceito diferente que tenha uma forte participação do desenvolvedor.
  • Se possível, algo que afete as definições, bem como o uso de genéricos, para que também se tornem mais fáceis de ler.

Há um monte de coisas que poderiam caber.

  • identifier!(a, b) ( recreio )
  • identifier@(a, b)
  • identifier#(a, b)
  • identifier$(a, b)
  • identifier<:a, b:>
  • identifier.<a, b> é como uma afirmação de tipo!
  • identifier:<a, b>
  • etc.

Alguém tem alguma idéia sobre como restringir ainda mais o conjunto de potenciais?

Apenas uma observação rápida de que consideramos todas essas ideias e também consideramos ideias como

func F(T : a, b T) { }
func G() { F(int : 1, 2) }

Mas, novamente, a prova do pudim está em comê-lo. Discussões abstratas na ausência de código valem a pena, mas não levam a conclusões definitivas.

(Não tenho certeza se isso já foi falado antes) Estou vendo que nos casos em que recebemos um struct não poderemos "estender" uma API existente para lidar com tipos genéricos sem quebrar o código de chamada existente.

Por exemplo, dada esta função não genérica

func Repeat(v, n int) []int {
    var r []int
    for i := n; i > 0; i-- {
        r = append(r, v)
    }
    return r
}

Repeat(4, 4)

Podemos torná-lo genérico sem quebrar a compatibilidade com versões anteriores

func Repeat(type T)(v T, n int) []T {
    var r []T
    for i := n; i > 0; i-- {
        r = append(r, v)
    }
    return r
}

Repeat("a", 5)

Mas se quisermos fazer o mesmo com uma função que recebe um genérico struct

type XY struct {
    X, Y int
}

func RangeRepeat(arr []XY) []int {
    var r []int
    for _, n := range arr {
        for i := n.Y; i > 0; i-- {
            r = append(r, n.X)
        }
    }
    return r
}

RangeRepeat([]XY{{1, 1}, {2, 2}, {3, 3}})

parece que o código de chamada precisa ser atualizado

type XY(type T) struct {
    X T
    Y int
}

func RangeRepeat(type T)(arr []XY(T)) []T {
    var r []T
    for _, n := range arr {
        for i := n.Y; i > 0; i-- {
            r = append(r, n.X)
        }
    }
    return r
}

// error: cannot use generic type XY(type T any) without instantiation
// RangeRepeat([]XY{{1, 1}, {2, 2}, {3, 3}}) // error in old code
RangeRepeat([](XY(int)){{1, 1}, {2, 2}, {3, 3}}) // API changed
// RangeRepeat([]XY{{"1", 1}, {"2", 2}, {"3", 3}}) // error
RangeRepeat([](XY(string)){{"1", 1}, {"2", 2}, {"3", 3}}) // ok

Seria incrível poder derivar tipos de estruturas também.

@ianlancetaylor

A minuta do contrato menciona que methods may not take additional type arguments . No entanto, não há menção à substituição do contrato por métodos específicos. Esse recurso seria muito útil para implementar interfaces dependendo de qual contrato um tipo paramétrico está vinculado.

Você já discutiu essa possibilidade?

Outra pergunta para a minuta do contrato. As disjunções de tipo serão restritas a tipos internos? Se não, seria possível utilizar tipos parametrizados, principalmente interfaces na lista de disjunção?

Algo como

type Getter(T) interface {
    Get() T
}

contract(G, T) {
    G Getter(T)
}

seria bastante útil, não apenas para evitar duplicar o método definido da interface para o contrato, mas também para instanciar um tipo parametrizado quando a inferência de tipo falhar e você não tiver acesso ao tipo concreto (por exemplo, não é exportado)

@ianlancetaylor Não tenho certeza se isso já foi discutido antes, mas em relação à sintaxe dos argumentos de tipo para uma função, é possível concatenar a lista de argumentos à lista de argumentos de tipo? Então, para o exemplo do gráfico, em vez de

var g = graph.New(*Vertex, *FromTo)([]*Vertex{ ... })

você usaria

var g = graph.New(*Vertex, *FromTo, []*Vertex{ ... })

Essencialmente, os primeiros K argumentos da lista de argumentos correspondem a uma lista de argumentos de tipo de comprimento K. O restante da lista de argumentos corresponde aos argumentos regulares da função. Isso tem a vantagem de espelhar a sintaxe de

make(Type, size)

que recebe um Type como o primeiro argumento.

Isso simplificaria a gramática, mas precisa de informações de tipo para saber onde os argumentos de tipo terminam e os argumentos regulares começam.

@smasher164 Ele disse alguns comentários de volta que eles consideraram (o que implica que eles o descartaram, embora eu esteja curioso por quê).

func F(T : a, b T) { }
func G() { F(int : 1, 2) }

Isso é o que você está sugerindo, mas com dois pontos para separar os dois tipos de argumentos. Pessoalmente, gosto moderadamente, embora seja uma imagem incompleta; e quanto a declaração de tipo, métodos, instanciação, etc.

Eu quero voltar a algo que o @Inuart disse:

Podemos torná-lo genérico sem quebrar a compatibilidade com versões anteriores

A equipe do Go consideraria alterar a biblioteca padrão dessa maneira para ser consistente com a garantia de compatibilidade do Go 1? Por exemplo, e se strings.Repeat(s string, count int) string fosse substituído por Repeat(type S stringlike)(s S, count int) S ? Você também pode adicionar um comentário //Deprecated a bytes.Repeat mas deixá-lo lá para uso do código legado. Isso é algo que a equipe Go consideraria?

Edit: para ser claro, quero dizer, isso seria considerado no Go1Compat em geral? Ignore o exemplo específico se você não gostar dele.

@carlmjohnson Não. Este código quebraria: f := strings.Repeat , pois funções polimórficas não podem ser referenciadas sem instanciar primeiro.

E a partir daí, acho que a concatenação de argumentos de tipo e argumentos de valor seria um erro, pois impede uma sintaxe natural para se referir a uma versão instanciada de uma função. Seria mais natural se já tivesse curry, mas não tem. Parece estranho ter foo(int, 42) e foo(int) sendo expressões e ambos tendo tipos muito diferentes.

@urandom Sim, discutimos a possibilidade de adicionar restrições adicionais aos parâmetros de tipo de um método individual. Isso faria com que o conjunto de métodos do tipo parametrizado variasse com base nos argumentos de tipo. Isso pode ser útil, ou pode ser confuso, mas uma coisa parece certa: podemos adicioná-lo mais tarde sem quebrar nada. Por isso, adiamos a ideia para mais tarde. Obrigado por trazê-lo à tona.

Exatamente o que pode ser listado na lista de tipos permitidos não é tão claro quanto poderia ser. Acho que temos mais trabalho a fazer lá. Observe que pelo menos no rascunho de design atual listar um tipo de interface na lista de tipos atualmente significa que o argumento de tipo pode ser esse tipo de interface. Isso não significa que o argumento de tipo pode ser um tipo que implementa esse tipo de interface. Acho que atualmente não está claro se pode ser uma instância instanciada de um tipo parametrizado. É uma boa pergunta, no entanto.

@smasher164 @toolbox Os casos a serem considerados ao analisar a combinação de parâmetros de tipo e parâmetros regulares em uma única lista são como separá-los (se estiverem separados) e como lidar com o caso em que não há parâmetros regulares (presumivelmente podemos excluir no caso de nenhum parâmetro de tipo). Por exemplo, se não houver parâmetros regulares, como você distingue entre instanciar a função, mas não chamá-la, e instanciar a função e chamá-la? Embora claramente o último seja o caso mais comum, é razoável que as pessoas queiram escrever o primeiro caso.

Se os parâmetros de tipo fossem colocados dentro dos mesmos parênteses que os parâmetros regulares, então @griesemer disse em # 36177 (seu segundo post) que ele gostou bastante do uso de um ponto e vírgula em vez de dois pontos como separador porque (como resultado de inserção automática de ponto e vírgula) permitia espalhar os parâmetros por várias linhas de uma maneira agradável.

Pessoalmente, também gosto do uso de barras verticais ( |..| ) para incluir os parâmetros de tipo, como às vezes você vê esses usados ​​em outras linguagens (Ruby, Crystal etc.) Então teríamos coisas como:

func F(|T| a, b T) { }
func G() { F(|int| 1, 2) }

As vantagens incluem:

  • Eles fornecem uma boa distinção visual (pelo menos aos meus olhos) entre tipo e parâmetros regulares.
  • Você não precisaria usar a palavra-chave type .
  • Não ter parâmetros regulares não é um problema.
  • O caractere de barra vertical está, obviamente, no conjunto ASCII e, portanto, deve estar disponível na maioria dos teclados.

Você pode até ser capaz de usá-lo fora dos parênteses, mas presumivelmente você teria as mesmas dificuldades de análise que com <...> ou [...] , pois poderia ser confundido com o operador bit a bit 'or', embora possivelmente as dificuldades seriam menos agudas.

Não entendo como as barras verticais ajudam no caso de não haver parâmetros regulares. Eu não entendo como você pode distinguir uma instanciação de função de uma chamada de função.

Uma maneira de distinguir entre esses dois casos seria exigir a palavra-chave type se você estivesse instanciando a função, mas não se a estivesse chamando, o que, como você disse anteriormente, é o caso mais comum.

Concordo que isso poderia funcionar, mas parece muito sutil. Eu não acho que será óbvio para o leitor o que está acontecendo.

Acho que em Go precisamos mirar mais alto do que simplesmente ter uma maneira de fazer algo. Precisamos buscar abordagens que sejam diretas, intuitivas e que se encaixem bem com o restante da linguagem. A pessoa que lê o código deve ser capaz de entender facilmente o que está acontecendo. Claro que nem sempre podemos atingir esses objetivos, mas devemos fazer o melhor que pudermos.

@ianlancetaylor, além de debater sobre sintaxe, que é interessante por si só, gostaria de saber se há algo que nós, como comunidade, possamos fazer para ajudar você e a equipe nesse assunto.

Por exemplo, tenho a ideia de que você gostaria de mais código escrito no estilo da proposta, para melhor avaliar a proposta, tanto sintaticamente quanto de outra forma? E/ou outras coisas?

@toolbox Sim. Estamos trabalhando em uma ferramenta para facilitar isso, mas ainda não está pronta. Muito em breve agora.

Você pode falar mais sobre a ferramenta? Permitiria executar o código?

Este problema é o local preferido para feedback de genéricos? Parece mais ativo que o wiki. Uma observação é que há muitos aspectos na proposta, mas a questão do GitHub reduz a discussão em um formato linear.

A sintaxe F(T:) / G() { F(T:)} parece boa para mim. Não acho que a instanciação que pareça uma chamada de função seja intuitiva para leitores inexperientes.

Eu não entendo exatamente quais são as preocupações em torno da compatibilidade com versões anteriores. Acho que há uma limitação no draft contra a declaração de um contrato, exceto no nível superior. Pode valer a pena pesar (e medir) quanto código realmente quebraria se isso fosse permitido. Meu entendimento é apenas código que usa a palavra-chave contract , que não parece muito código (que poderia ser suportado de qualquer forma, especificando go1 no topo de arquivos antigos). Pese isso contra décadas de mais poder para os programadores. Em geral, parece muito simples proteger o código antigo com esses mecanismos, especialmente com o uso generalizado das famosas ferramentas do go.

Além disso, em relação a essa restrição, suspeito que a proibição de declarar métodos dentro de corpos de função é uma razão pela qual as interfaces não são mais usadas - elas são muito mais complicadas do que passar funções únicas. É difícil dizer se a restrição de nível superior dos contratos seria tão irritante quanto a restrição de métodos - provavelmente não seria - mas, por favor, não use a restrição de métodos como precedente. Para mim isso é uma falha de linguagem.

Eu também gostaria de ver exemplos de como os contratos podem ajudar a reduzir a verbosidade de if err != nil e, mais importante, onde eles seriam insuficientes. Algo como F() (X, error) {return IfError(foo(), func(i, j int) X { return X(i*j}), Identity )} é possível?

Também estou querendo saber se a equipe go antecipa que as assinaturas de função implícitas parecerão um recurso ausente quando Mapa, Filtro e amigos estiverem disponíveis. Isso é algo que precisa ser considerado enquanto novos recursos de digitação implícita são adicionados à linguagem para contratos? Ou pode ser adicionado depois? Ou nunca fará parte da linguagem?

Ansioso para testar a proposta. Desculpe por tantos tópicos.

Pessoalmente, sou bastante cético que muitas pessoas gostariam de escrever métodos dentro de corpos de função. É muito raro definir tipos dentro de corpos de função hoje; declarar métodos seria ainda mais raro. Dito isso, veja #25860 (não relacionado a genéricos).

Não vejo como os genéricos ajudam no tratamento de erros (já um tópico muito detalhado em si). Não entendi seu exemplo, desculpe.

Uma sintaxe literal de função mais curta, também não conectada a genéricos, é #21498.

Quando postei ontem à noite não percebi que é possível jogar com o rascunho
implementação (!!). Uau, é ótimo finalmente poder escrever um código mais abstrato. Não tenho problemas com a sintaxe do rascunho.

Continuando a discussão acima...


Parte da razão pela qual as pessoas não escrevem tipos em corpos de função é porque eles
não pode escrever métodos para eles. Essa restrição pode prender o tipo dentro do
bloco onde foi definido, pois não pode ser transformado de forma concisa em um
interface para uso em outros lugares. Java permite que classes anônimas satisfaçam sua versão
de interfaces, e eles são usados ​​em quantidade razoável.

Podemos ter a discussão da interface em #25860. Eu diria apenas que na época
dos contratos, os métodos se tornarão mais importantes, então sugiro errar no
lado de capacitar tipos e pessoas locais que gostam de escrever encerramentos, não
enfraquecendo-os.

(E para reiterar, por favor, não use compatibilidade estrita go1 [vs virtualmente
99,999% de compatibilidade, como eu a entendo] como um fator na decisão sobre isso
característica.)


Em relação ao tratamento de erros, suspeitei que os genéricos pudessem permitir abstrair
padrões comuns para lidar com tuplas de retorno (T1, T2, ..., error) . Eu não
tenha algo detalhado em mente. Algo como type ErrPair(type T) struct{T T; Err Error} pode ser útil para encadear ações, como Promise em
Java/TypeScript. Talvez alguém tenha pensado mais sobre isso. Uma tentativa de
escrever uma biblioteca auxiliar e um código que use a biblioteca pode valer a pena procurar
em se você estiver procurando por uso real.

Com alguma experimentação acabei com o seguinte. Eu gostaria de tentar isso
técnica em um exemplo maior para ver se usar ErrPair(T) realmente ajuda.

type result struct {min, max point}

// with a generic ErrPair type and generic function errMap2 (like Java's Optional#map() function).
func minMax2(msg *inputTimeSeries) (result, error) {
    return errMap2(
        MakeErrPair(time.Parse(layout, msg.start)).withMessage("bad start"),
        MakeErrPair(time.Parse(layout, msg.end)).withMessage("bad end"),
        func(start, end time.Time) (result, error) {
            min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
                return float64(p.value)
            })
            mkPoint := func(ip inputPoint) point {
                return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
            }
            return result{mkPoint(*min), mkPoint(*max)}, nil
        }).tuple()
}

// without generics, lots of if err != nil 
func minMax(msg *inputTimeSeries) (result, error) { 
    start, err := time.Parse(layout, msg.start)
    if err != nil {
        return result{}, fmt.Errorf("bad start: %w", err)
    }
    end, err := time.Parse(layout, msg.end)
    if err != nil {
        return result{}, fmt.Errorf("bad end: %w", err)
    }
    min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
        return float64(p.value)
    })
    mkPoint := func(ip inputPoint) point {
        return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
    }
    return result{mkPoint(*min), mkPoint(*max)}, nil
}

// Most languages look more like this.
func minMaxWithThrowing(msg *inputTimeSeries) result {
    start := time.Parse(layout, msg.start)) // might throw
    end := time.Parse(layout, msg.end)) // might throw
    min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
        return float64(p.value)
    })
    mkPoint := func(ip inputPoint) point {
        return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
    }
    return result{mkPoint(*min), mkPoint(*max)}
}

(código de exemplo completo disponível aqui )


Para experimentação geral, tentei escrever um pacote S-Expression
aqui .
Eu experimentei alguns pânicos na implementação experimental enquanto tentava
trabalhe com tipos compostos como Form([]*Form(T)) . Posso fornecer mais feedback
depois de trabalhar em torno disso, se for útil.

Eu também não tinha certeza de como escrever um tipo primitivo -> função string:

contract PrimitiveType(T) {
    T bool, int, int8, int16, int32, int64, string, uint, uint8, uint16, uint32, uint64, float32, float64, complex64, complex128
    // string(T) is not a contract
}

func primitiveString(type T PrimitiveType(T))(t T) string  {
    // I'm not sure if this is an artifact of the experimental implementation or not.
    return string(t) // error: `cannot convert t (variable of type T) to string`
}

A função real que eu estava tentando escrever era esta:

// basicFormAdapter implements FormAdapter() for the primitive types.
type basicFormAdapter(type T PrimitiveType) struct{}


func (a *basicFormAdapter(T)) Format(e T, fc *FormatContext) error {
    //This doesn't work: fc.Print(string(e)) -- cannot convert e (variable of type T) to string
    // This also doesn't work: cannot type switch on non-interface value e (type int)
    // switch ee := e.(type) {
    // case int: fc.Print(string(ee))
    // default: fc.Print(fmt.Sprintf("!!! unsupported type %v", e))
    // }
    // IMO, the proposal to allow switching on T is most natural:
    // switch T.(type) {
    //  case int: fc.Print(string(e))
    //  default: fc.Print(fmt.Sprintf("!!! unsupported type %v", e))
    // }

    // This can't be the only way, right?
    rv := reflect.ValueOf(e)
    switch rv.Kind() {
    case reflect.Bool: fc.Print(fmt.Sprintf("%v", e))
    case reflect.Int:fc.Print(fmt.Sprintf("%v", e))
    case reflect.Int8: fc.Print(fmt.Sprintf("int8:%v", e))
    case reflect.Int16: fc.Print(fmt.Sprintf("int16:%v", e))
    case reflect.Int32: fc.Print(fmt.Sprintf("int32:%v", e))
    case reflect.Int64: fc.Print(fmt.Sprintf("int64:%v", e))
    case reflect.Uint: fc.Print(fmt.Sprintf("uint:%v", e))
    case reflect.Uint8: fc.Print(fmt.Sprintf("uint8:%v", e))
    case reflect.Uint16: fc.Print(fmt.Sprintf("uint16:%v", e))
    case reflect.Uint32: fc.Print(fmt.Sprintf("uint32:%v", e))
    case reflect.Uint64: fc.Print(fmt.Sprintf("uint64:%v", e))
    case reflect.Uintptr: fc.Print(fmt.Sprintf("uintptr:%v", e))
    case reflect.Float32: fc.Print(fmt.Sprintf("float32:%v", e))
    case reflect.Float64: fc.Print(fmt.Sprintf("float64:%v", e))
    case reflect.Complex64: fc.Print(fmt.Sprintf("(complex64 %f %f)", real(rv.Complex()), imag(rv.Complex())))
    case reflect.Complex128:
         fc.Print(fmt.Sprintf("(complex128 %f %f)", real(rv.Complex()), imag(rv.Complex())))
    case reflect.String:
        fc.Print(fmt.Sprintf("%q", rv.String()))
    }
    return nil
}

Eu também tentei criar um tipo de tipo 'Resultado'

type Result(type T) struct {
    Value T
    Err error
}

func NewResult(type T)(value T, err error) Result(T) {
    return Result(T){
        Value: value,
        Err: err,
    }
}

func then(type T, R)(r Result(T), f func(T) R) Result(R) {
    if r.Err != nil {
        return Result(R){Err: r.Err}
    }

    v := f(r.Value)
    return  Result(R){
        Value: v,
        Err: nil,
    }
}

func thenTry(type T, R)(r Result(T), f func(T)(R, error)) Result(R) {
    if r.Err != nil {
        return Result(R){Err: r.Err}
    }

    v, err := f(r.Value)
    return  Result(R){
        Value: v,
        Err: err,
    }
}

por exemplo

    r := NewResult(GetInput())
    r2 := thenTry(r, UppercaseAndErr)
    r3 := thenTry(r2, strconv.Atoi)
    r4 := then(r3, Add5)
    if r4.Err != nil {
        // handle err
    }
    return r4.Value, nil

Idealmente, você teria as funções then como métodos no tipo Result.

Além disso, o exemplo de diferença absoluta no rascunho não parece compilar.
Eu acho o seguinte:

func (a ComplexAbs(T)) Abs() T {
    r := float64(real(a))
    i := float64(imag(a))
    d := math.Sqrt(r * r + i * i)
    return T(complex(d, 0))
}

deveria estar:

func (a ComplexAbs(T)) Abs() ComplexAbs(T) {
    r := float64(real(a))
    i := float64(imag(a))
    d := math.Sqrt(r * r + i * i)
    return ComplexAbs(T)(complex(d, 0))
}

Eu tenho um pouco de preocupação com a capacidade de usar vários contract para vincular um parâmetro de tipo.

Em Scala, é comum definir uma função como:

def compute[A: PointLike: HasTime: IsWGS](points: Vector[A]): Map[Int, A] = ???

PointLike , HasTime e IsWGS são alguns pequenos contract (Scala os chama type class ).

Rust também tem um mecanismo semelhante:

fn f<F: A + B>(a F) {}

E podemos usar uma interface anônima ao definir uma função.

type I1 interface {
    A()
}
type I2 interface {
    B()
}
func f(a interface{
    I1
    I2
})

IMO, a interface anônima é uma má prática, pois um interface é um tipo real , o chamador desta função pode ter que declarar uma variável com este tipo. Mas contract apenas uma restrição no parâmetro de tipo, o chamador sempre joga com algum tipo real ou apenas outro parâmetro de tipo, acho que é seguro permitir contrato anonimamente em uma definição de função.

Para desenvolvedores de bibliotecas, é inconveniente definir um novo contract se a combinação de alguns contratos for usada apenas em alguns lugares, isso atrapalhará a base de código. Para o usuário de bibliotecas, ele precisa se aprofundar nas definições para conhecer os reais requisitos da mesma. Se o usuário definir muitas funções para chamar a função na biblioteca, eles podem definir um contrato nomeado para facilitar o uso e podem até adicionar mais contratos a esse novo contrato, se necessário, porque isso é válido

contract C1(T) {
    T A()
}
contract C2(T) {
    T B()
}
contract C3(T) {
    T C()
}

contract PART(T) {
    C1(T)
    C2(T)
}

contract ALL(T) {
    C1(T)
    C2(T)
    C3(T)
}

func f1(type A PART) (a A) {}

func f2(type A ALL) (a A) {
    f1(a)
}

Eu tentei isso no compilador de rascunho, todos eles não podem ser verificados por tipo.

func f(type A C1, C2)(x A)

func f1(type A contract C(A1) {
    C1(A)
    C2(A)
}) (x A)

func f2(type A ((type A1) interface {
    I1(A1)
    I2(A1)
})(A)) (x A)

De acordo com as notas em CL

Um parâmetro de tipo que é restringido por vários contratos não obterá o limite de tipo correto.

Acho que esse trecho estranho é válido depois que esse problema foi resolvido

func f1(type A C1, _ C2(A)) (x A)

Aqui estão alguns dos meus pensamentos:

  • Se tratarmos contract como o tipo de um parâmetro de tipo, type a A <=> var a A , podemos adicionar um açúcar de sintaxe como type a { A1(a); A2(a) } para definir um contrato rapidamente.
  • Caso contrário, podemos tratar a última parte da lista de tipos como uma lista de requisitos, type a, b, A1(a), A2(a), A3(a, b) , este estilo apenas como usar interface para restringir os parâmetros do tipo.

@bobotu É comum em Go compor funcionalidades usando incorporação. Parece natural compor contratos da mesma maneira que você faria com estruturas ou interfaces.

@azunymous Pessoalmente, não sei como me sinto com toda a comunidade Go mudando de vários retornos para Result , embora pareça que a proposta de Contratos permitiria isso até certo ponto. A Go Team parece evitar mudanças de linguagem que comprometam a "sensação" da linguagem, com a qual concordo, mas essa parece ser uma dessas mudanças.

Apenas um pensamento; Eu me pergunto se há alguma tomada sobre este ponto.

@toolbox Eu não acho que seja realmente possível usar algo como um único tipo Result extensivamente fora do caso em que você está apenas passando valores, a menos que você tenha uma massa de Result genérico s e funções de cada combinação de contagens de parâmetros e tipos de retorno. Com muitas funções numeradas ou usando closures, você perderia a legibilidade.

Eu acho que seria mais provável que você visse algo equivalente a errWriter onde você usaria algo assim ocasionalmente quando se encaixasse, nomeado para o caso de uso.

Pessoalmente, não sei como me sinto com toda a comunidade Go mudando de vários retornos para Resultado

Eu não acho que isso aconteceria. Como @azunymous disse, muitas funções têm vários tipos de retorno e um erro, mas um resultado não pode conter todos esses outros valores retornados ao mesmo tempo. O polimorfismo paramétrico não é o único recurso necessário para fazer algo assim; você também precisa de tuplas e desestruturação.

Obrigado! Como eu disse, não é algo que eu tenha pensado profundamente, mas é bom saber que minha preocupação estava fora de lugar.

@toolbox Não pretendo introduzir uma nova sintaxe, o principal problema aqui é a falta de capacidade de usar contrato anônimo como interface anônima.

No compilador de rascunho, parece impossível escrever algo assim. Podemos usar uma interface anônima na definição da função, mas não podemos fazer a mesma coisa para o contrato, mesmo no estilo detalhado.

func f1(type A, B, C, D contract {
    C1(A)
    C2(A, B)
    C3(A, C)
}) (a A, b B, c C, d D)

// Or a more verbose style

func f2(type A, B, C, D (contract (_A, _B, _C) {
    C1(_A)
    C2(_A, _B)
    C3(_A, _C)
})(A, B, C)) (a A, b B, c C, d D)

IMO, esta é uma extensão natural da sintaxe existente. Este ainda é um contrato no final da lista de parâmetros de tipo e ainda usamos a incorporação para compor a funcionalidade. Se Go puder fornecer algum açúcar para gerar parâmetros de tipo de contrato automaticamente como o primeiro snippet, o código será mais fácil de ler e escrever.

func fff(type A C1(A), B C2(B, A), C C3(B, C, A)) (a A, b B, c C)

// is more verbose than

func fff(type A, B, C contract {
    C1(A)
    C2(B, A)
    C3(B, C, A)
}) (a A, b B, c C)

Encontro alguns problemas quando tento implementar um iterador preguiçoso sem a invocação de método dinâmico, assim como o Iterator de Rust.

Eu quero definir um contrato Iterator simples

contract Iterator(T, E) {
    T Next() (E, bool)
}

Como Go não tem o conceito de type member , preciso declarar E como o parâmetro de tipo de entrada.

Uma função para coletar os resultados

func Collect(type I, E Iterator) (input I) []E {
    var results []E
    for {
        e, ok := input.Next()
        if !ok {
            return results
        }
        results = append(results, e)
    }
}

Uma função para mapear elementos

contract MapIO(I, E, O, R) {
    Iterator(I, E)
    Iterator(O, R)
}

func Map(type I, E, O, R MapIO) (input I, f func (e E) R) O {
    return &lazyIterator(I, E, R){
        parent: input,
        f:      f,
    }
}

Tenho dois problemas aqui:

  1. Não consigo retornar um lazyIterator aqui, o compilador diz cannot convert &(lazyIterator(I, E, R) literal) (value of type *lazyIterator(I, E, R)) to O .
  2. Eu preciso declarar um novo contrato chamado MapIO que precisa de 4 linhas, enquanto o Map precisa apenas de 6 linhas. É difícil para os usuários lerem o código.

Suponha que Map possa ser verificado por tipo, espero poder escrever algo como

type staticIterator(type E) struct {
    elem []E
}

func (it *(staticIterator(E))) Next() (E, bool) { panic("todo") }

func main() {
    inpuit := &staticIterator{
        elem: []int{1, 2, 3, 4},
    }
    mapped := Map(input, func (i int) float32 { return float32(i + 1) })
    fmt.Printf("%v\n", Collect(mapped))
}

Infelizmente, o compilador reclama que não pode inferir tipos. Ele para de reclamar isso depois que eu mudo o código para

func main() {
    input := &staticIterator(int){
        elem: []int{1, 2, 3, 4},
    }
    mapped := Map(*staticIterator(int), int, *lazyIterator(*staticIterator(int), int, float32), float32)(input, func (i int) float32 { return float32(i + 1) })
    result := Collect(*lazyIterator(*staticIterator(int), int, float32), float32)(mapped)
    fmt.Printf("%v\n", result)
}

O código é muito difícil de ler e escrever, e há muitas dicas de tipo duplicado.

BTW, o compilador entrará em pânico com:

panic: interface conversion: ast.Expr is *ast.ParenExpr, not *ast.CallExpr

goroutine 1 [running]:
go/go2go.(*translator).instantiateTypeDecl(0xc000251950, 0x0, 0xc0001af860, 0xc0001a5dd0, 0xc00018ac90, 0x1, 0x1, 0xc00018bca0, 0x1, 0x1, ...)
        /home/tuzi/go-tip/src/go/go2go/instantiate.go:191 +0xd49
go/go2go.(*translator).translateTypeInstantiation(0xc000251950, 0xc000189380)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:671 +0x3f3
go/go2go.(*translator).translateExpr(0xc000251950, 0xc000189380)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:518 +0x501
go/go2go.(*translator).translateExpr(0xc000251950, 0xc0001af990)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:496 +0xe3
go/go2go.(*translator).translateExpr(0xc000251950, 0xc00018ace0)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:524 +0x1c3
go/go2go.(*translator).translateExprList(0xc000251950, 0xc00018ace0, 0x1, 0x1)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:593 +0x45
go/go2go.(*translator).translateStmt(0xc000251950, 0xc000189840)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:419 +0x26a
go/go2go.(*translator).translateBlockStmt(0xc000251950, 0xc00018d830)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:380 +0x52
go/go2go.(*translator).translateFuncDecl(0xc000251950, 0xc0001c0390)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:373 +0xbc
go/go2go.(*translator).translate(0xc000251950, 0xc0001b0400)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:301 +0x35c
go/go2go.rewriteAST(0xc000188280, 0xc000188240, 0x0, 0x0, 0xc0001f6280, 0xc0001b0400, 0x1, 0xc000195360, 0xc0001f6280)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:122 +0x101
go/go2go.RewriteBuffer(0xc000188240, 0x7ffe07d6c027, 0xa, 0xc0001ec000, 0x4fe, 0x6fe, 0x0, 0xc00011ed58, 0x40d288, 0x30, ...)
        /home/tuzi/go-tip/src/go/go2go/go2go.go:132 +0x2c6
main.translateFile(0xc000188240, 0x7ffe07d6c027, 0xa)
        /home/tuzi/go-tip/src/cmd/go2go/translate.go:26 +0xa9
main.main()
        /home/tuzi/go-tip/src/cmd/go2go/main.go:64 +0x434

Também acho impossível definir uma função que funcione com um retorno Iterator de um tipo específico.

type User struct {}

func UpdateUsers(type A Iterator(A, User)) (it A) bool { 
    // Access `User`'s field.
}

// And I found this may be possible

contract checkInts(A, B) {
    Iterator(A, B)
    B int
}

func CheckInts(type A, B checkInts) (it A) bool { panic("todo") }

O segundo snippet pode funcionar em alguns cenários, mas é difícil de entender e o tipo B não utilizado parece estranho.

De fato, podemos usar uma interface para concluir essa tarefa.

type Iterator(type E) interface {
    Next() (E, bool)
}

Estou apenas tentando explorar o quão expressivo é o design do Go.

BTW, o código Rust a que me refiro é

fn main() {
    let input = vec![1, 2, 3, 4];
    let mapped = input.iter().map(|x| x * 3);
    let result = f(mapped);
    println!("{:?}", result.collect::<Vec<_>>());
}

fn f<I: Iterator<Item = i32>>(it: I) -> impl Iterator<Item = f32> {
    it.map(|i| i as f32 * 2.0)
}

// The definition of `map` in stdlib is
pub struct Map<I, F> {
    iter: I,
    f: F,
}

fn map<B, F: FnMut(Self::Item) -> B>(self, f: F) -> Map<Self, F>

Aqui está um resumo para https://github.com/golang/go/issues/15292#issuecomment -633233479

  1. Podemos precisar de algo para expressar existential type por func Collect(type I, E Iterator) (input I) []E

    • O tipo real do parâmetro quantificado universal E não pode ser inferido, porque ele apareceu apenas na lista de retorno. Devido à falta de type member para tornar E existencial por padrão, acho que podemos encontrar esse problema em muitos lugares.

    • Talvez possamos usar o existential type mais simples como o curinga do Java ? para resolver a inferência de tipo de func Consume(type I, E Iterator) (input I) . Podemos usar _ para substituir E , func Consume(type I Iterator(I, _)) (input I) .

    • Mas ainda não pode ajudar o problema de inferência de tipo para Collect , não sei se é difícil inferir E , mas Rust parece ser capaz de fazer isso.

    • Ou podemos usar _ como um espaço reservado para tipos que o compilador pode inferir e preencher os tipos ausentes manualmente, como Collect(_, float32) (...) para coletar em um iterador de float32.

  1. Devido à falta de capacidade de devolver um existential type , também temos problemas para coisas como func Map(type I, E, O, R MapIO) (input I, f func (e E) R) O

    • Rust suporta isso usando impl Iterator<E> . Se Go puder fornecer algo assim, podemos retornar um novo iterador sem boxing, pode ser útil para algum código crítico de desempenho.

    • Ou podemos simplesmente retornar um objeto em caixa, é assim que o Rust resolve esse problema antes de suportar existential type na posição de retorno. Mas a questão é a relação entre contract e interface , talvez precisemos definir algumas regras de conversão e deixar o compilador convertê-las automaticamente. Caso contrário, podemos precisar definir um contract e um interface com métodos idênticos para este caso.

    • Caso contrário, só podemos usar o CPS para mover o parâmetro de tipo da posição de retorno para a lista de entrada. por exemplo, func Map(type I, E, O, R MapIO) (input I, f func (e E) R, f1 func (outout O)) . Mas isso é inútil na prática, simplesmente porque devemos escrever o tipo real de O quando passamos uma função para Map .

Acabei de acompanhar um pouco essa discussão, e parece bastante claro que as dificuldades sintáticas com os parâmetros de tipo continuam sendo uma grande dificuldade com o rascunho da proposta. Existe uma maneira de evitar completamente os parâmetros de tipo e obter a maioria das funcionalidades genéricas: #32863 -- talvez seja um bom momento para considerar essa alternativa à luz de algumas dessas discussões adicionais? Se houvesse alguma chance de algo como esse design ser adotado, eu ficaria feliz em tentar modificar o playground de montagem da web para permitir o teste dele.

Minha sensação é que o foco atual está em acertar a semântica da proposta atual, independentemente da sintaxe, porque a semântica é muito difícil de mudar.

Acabei de ver que um artigo sobre Featherweight Go foi publicado no Arxiv e é uma colaboração entre a equipe Go e vários especialistas em teoria dos tipos. Parece que há mais papéis planejados nesse sentido.

Para acompanhar meu comentário anterior, Phil Wadler, famoso por Haskell e um dos autores do artigo, tem uma palestra agendada no "Featherweight Go" na segunda-feira, 8 de junho, às 7h PDT / 10h EDT: http://chalmersfp.org/ . link do youtube

@rcoreilly Acho que só saberemos se as "dificuldades sintáticas" são um grande problema quando as pessoas tiverem mais experiência em escrever e, mais importante, ler código escrito de acordo com o rascunho do design. Estamos trabalhando em maneiras para as pessoas tentarem isso.

Na ausência disso, acho que a sintaxe é simplesmente o que as pessoas veem primeiro e comentam primeiro. Pode ser um grande problema, pode não ser. Nós não sabemos ainda.

Para acompanhar meu comentário anterior, Phil Wadler, famoso por Haskell e um dos autores do jornal, tem uma palestra marcada no "Featherweight Go" na segunda-feira

A palestra de Phil Wadler foi muito acessível e interessante. Fiquei irritado com o limite de tempo aparentemente inútil de uma hora que o impediu de entrar na monomorfização.

Notável que Wadler foi convidado por Pike para aparecer; aparentemente eles se conhecem do Bell Labs. Para mim, Haskell tem um conjunto muito diferente de valores e paradigmas, e é interessante ver como seu (criador? designer principal?) pensa sobre Go e genéricos em Go.

A proposta em si tem uma sintaxe muito próxima de Contracts, mas omite os próprios Contracts, apenas utilizando parâmetros de tipo e interfaces. Uma diferença chave que é destacada é a capacidade de pegar um tipo genérico e definir métodos nele que tenham restrições mais específicas do que o próprio tipo.

Aparentemente a Go Team está trabalhando ou tem um protótipo disso! Isso será interessante. Enquanto isso, como ficaria isso?

package graph

type Node(type e) interface{
    Edges() []e
}

type Edge(type n) interface{
    Nodes() (from n, to n)
}

type Graph(type n Node(e), e Edge(n)) struct { ... }
func New(type n Node(e), e Edge(n))(nodes []n) *Graph(n, e) { ... }
func (g *Graph(type n Node(e), e Edge(n))) ShortestPath(from, to n) []e { ... }

Eu tenho esse direito? Eu penso que sim. Se eu fizer... nada mal, na verdade. Não resolve o problema dos parênteses gaguejantes, mas parece melhorado de alguma forma. Algum tumulto sem nome dentro de mim se acalmou.

E o exemplo de pilha de @urandom ? (Aliasing interface{} para Any e usando uma certa quantidade de inferência de tipo.)

package main

type Any interface{}

type Stack(type t Any) []t

func (s Stack(type t Any)) Peek() t {
    return s[len(s)-1]
}

func (s *Stack(type t Any)) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack(type t Any)) Push(value t) {
    *s = append(*s, value)
}

type StackIterator(type t Any) struct{
    stack Stack(t)
    current int
}

func (s *Stack(type t Any)) Iter() *StackIterator(t) {
    it := StackIterator(t){stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator(type t Any)) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator(type t Any)) Value() t {
    if i.current < 0 {
        var zero t
        return zero
    }

    return i.stack[i.current]
}

type Iterator(type t Any) interface {
    Next() bool
    Value() t
}

func Map(type t Any, u Any)(it Iterator(t), mapF func(t) u) Iterator(u) {
    return mapIt(t, u){it, mapF}
}

type mapIt(type t Any, u Any) struct {
    parent Iterator(t)
    mapF func(t) u
}

func (i mapIt(type t Any, u Any)) Next() bool {
    return i.parent.Next()
}

func (i mapIt(type t Any, u Any)) Value() u {
    return i.mapF(i.parent.Value())
}

func Filter(type t Any)(it Iterator(t), predicate func(t) bool) Iterator(t) {
    return filter(t){it, predicate}
}

type filter(type t Any) struct {
    parent Iterator(t)
    predicateF func(t) bool
}

func (i filter(type t Any)) Next() bool {
    if !i.parent.Next() {
        return false
    }

    n := true
    for n && !i.predicateF(i.parent.Value()) {
        n = i.parent.Next()
    }

    return n
}

func (i filter(type t Any)) Value() t {
    return i.parent.Value()
}

func Distinct(type t comparable)(it Iterator(t)) Iterator(t) {
    return distinct(t){it, map[t]struct{}{}}
}

type distinct(type t comparable) struct {
    parent Iterator(t)
    set map[t]struct{}
}

func (i distinct(type t Any)) Next() bool {
    if !i.parent.Next() {
        return false
    }

    n := true
    for n {
        _, ok := i.set[i.parent.Value()]
        if !ok {
            i.set[i.parent.Value()] = struct{}{}
            break
        }
        n = i.parent.Next()
    }


    return n
}

func (i distinct(type t Any)) Value() t {
    return i.parent.Value()
}

func ToSlice(type t Any)(it Iterator(t)) []t {
    var res []t

    for it.Next() {
        res = append(res, it.Value())
    }

    return res
}

func ToSet(type t comparable)(it Iterator(t)) map[t]struct{} {
    var res map[t]struct{}

    for it.Next() {
        res[it.Value()] = struct{}{}
    }

    return res
}

func Reduce(type t Any)(it Iterator(t), id t, acc func(a, b t) t) t {
    for it.Next() {
        id = acc(id, it.Value())
    }

    return id
}

func main() {
    var stack Stack(string)
    stack.Push("foo")
    stack.Push("bar")
    stack.Pop()
    stack.Push("alpha")
    stack.Push("beta")
    stack.Push("foo")
    stack.Push("gamma")
    stack.Push("beta")
    stack.Push("delta")


    var it Iterator(string) = stack.Iter()

    it = Filter(string)(it, func(s string) bool {
        return s == "foo" || s == "beta" || s == "delta"
    })

    it = Map(string, string)(it, func(s string) string {
        return s + ":1"
    })

    it = Distinct(string)(it)

    println(Reduce(it, "", func(a, b string) string {
        if a == "" {
            return b
        }
        return a + ":" + b
    }))


}

Algo assim, suponho. Eu percebo que na verdade não há contratos nesse código, então não é uma boa representação de como isso é tratado no estilo FGG, mas posso lidar com isso em um momento.

Impressões:

  • Eu gosto de ter o estilo dos parâmetros de tipo nos métodos que correspondem ao das declarações de tipo. Ou seja, dizendo "tipo" e declarando explicitamente os tipos, ("type" param paramType, param paramType...) em vez de (param, param) . Isso o torna visualmente consistente, para que o código seja mais visualizável.
  • Eu gosto de ter os parâmetros de tipo em minúsculas. Variáveis ​​de uma única letra em Go indicam uso extremamente local, mas capitalização significa que é exportado, e elas parecem contrárias quando colocadas juntas. As letras minúsculas são melhores, pois os parâmetros de tipo têm o escopo da função/tipo.

Certo, e os contratos?

Bem, uma coisa que eu gosto é que Stringer está intocado; você não terá uma interface Stringer e um contrato Stringer .

type Stringer interface {
    String() string
}

func Stringify(type t Stringer)(s []t) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}

Também temos o exemplo viaStrings :

type ToString interface {
    Set(string)
}

type FromString interface {
    String() string
}

func SetViaStrings(type to ToString, from FromString)(s []from) []to {
    r := make([]to, len(s))
    for i, v := range s {
        r[i].Set(v.String())
    }
    return r
}

Interessante. Na verdade, não tenho 100% de certeza do que o contrato nos deu nesse caso. Talvez parte disso fosse a regra de que uma função poderia ter vários parâmetros de tipo, mas apenas um contrato.

Equal é abordado no artigo/conversa:

contract equal(T) {
    T Equal(T) bool
}

// becomes

type equal(type t equal(t)) interface{
    Equal(t) bool
}

E assim por diante. Eu sou bastante levado com a semântica. Os parâmetros de tipo são interfaces, portanto, as mesmas regras sobre a implementação de uma interface são aplicadas ao que pode ser usado como parâmetro de tipo. Não é apenas "encaixotado" em tempo de execução - a menos que você passe explicitamente uma interface, suponho, para a qual você esteja livre.

A maior coisa que notei como não coberta é um substituto para a capacidade de Contracts de especificar um intervalo de tipos primitivos. Bem, tenho certeza que uma estratégia para isso, e muitas outras coisas, virá :

8 - CONCLUSÃO

Este é o começo da história, não o fim. Em trabalhos futuros, planejamos examinar outros métodos de implementação além da monomorfização e, em particular, considerar uma implementação baseada na passagem de representações de tipos em tempo de execução, semelhante àquela usada para genéricos .NET. Uma abordagem mista que usa a monomorfização às vezes e a passagem de representações de tempo de execução às vezes pode ser melhor, novamente semelhante à usada para genéricos .NET.

O Featherweight Go é restrito a um pequeno subconjunto do Go. Planejamos um modelo de outros recursos importantes, como atribuições, matrizes, fatias e pacotes, que chamaremos de Bantamweight Go; e um modelo do mecanismo de simultaneidade inovador do Go baseado em “goroutines” e passagem de mensagens, que chamaremos de Cruiserweight Go.

Featherweight Go parece ótimo para mim. Excelente ideia para envolver alguns especialistas em teoria dos tipos. Isso se parece muito mais com o tipo de coisa que eu estava defendendo mais adiante neste tópico.

É bom saber que os especialistas em teoria dos tipos estão trabalhando ativamente nisso!

Até parece semelhante (exceto pela sintaxe ligeiramente diferente) à minha antiga proposta "contratos são interfaces" https://github.com/cosmos72/gomacro/blob/master/doc/generics-cti.md

@toolbox
Ao permitir métodos com restrições diferentes do tipo real (assim como tipos diferentes), o FGG abre algumas possibilidades que não eram viáveis ​​com o rascunho dos contratos atuais. Como exemplo, com o FGG, deve-se ser capaz de definir um Iterator e um ReversibleIterator, e ter os iteradores intermediários e finais (map, filter reduce) suportando ambos (por exemplo, com Next() e NextFromBack() para reversíveis) , dependendo de qual é o iterador pai.

Eu acho que é importante ter em mente que o FGG não é definitivamente onde os genéricos em Go vão acabar. É uma visão deles, de fora. E ignora explicitamente um monte de coisas que acabam complicando o produto final. Além disso, eu não li o jornal, apenas assisti a palestra. Com isso em mente: Até onde eu sei, existem duas maneiras significativas pelas quais o FGG adiciona poder expressivo sobre a minuta dos contratos:

  1. Ele permite adicionar novos parâmetros de tipo aos métodos (como mostrado no exemplo "List and Maps" na palestra). AFAICT isso permitiria implementar Functor (na verdade, esse é o exemplo da lista dele, se não me engano), Monad e seus amigos. Eu não acho que esses tipos específicos sejam interessantes para Gophers, mas existem casos de uso interessantes para isso (por exemplo, uma porta Go do Flume ou conceitos semelhantes provavelmente se beneficiariam). Pessoalmente, sinto que é uma mudança positiva, embora ainda não veja quais são as implicações para a reflexão e afins. Eu sinto que as declarações de método usando isso estão começando a ficar difíceis de ler - especialmente se os parâmetros de tipo de um tipo genérico também devem ser listados no receptor.
  2. Ele permite que os parâmetros de tipo tenham limites mais rígidos em métodos de tipos genéricos do que no próprio tipo. Como mencionado por outros, isso permite que você tenha o mesmo tipo genérico implementando métodos diferentes, dependendo de quais tipos foram instanciados. Não tenho certeza se esta é uma boa mudança, pessoalmente. Parece uma receita para confusão, ter Map(int, T) acabar com métodos que Map(string, T) não tem. No mínimo, o compilador precisa fornecer excelentes mensagens de erro, se algo assim acontecer. Enquanto isso, o benefício parece comparativamente pequeno - especialmente considerando que o fator motivador da conversa (compilação separada) não é super relevante para Go: Como os métodos precisam ser declarados no mesmo pacote que seu tipo de receptor e dado que os pacotes são a unidade de compilação, você não pode realmente estender o tipo separadamente. Eu sei que falar sobre compilação é uma maneira concreta de falar sobre um benefício mais abstrato, mas ainda assim, não sinto que o benefício ajude muito o Go.

Estou ansioso para os próximos passos, em qualquer caso :)

Eu acho que é importante ter em mente que o FGG não é definitivamente onde os genéricos em Go vão acabar.

@Merovius por que você diz isso?

@arl
FG é mais um trabalho de pesquisa sobre o que _poderia_ ser feito. Ninguém disse explicitamente que é assim que o polimorfismo funcionará em Go no futuro. Embora 2 desenvolvedores principais do Go sejam listados como autores no artigo, isso não significa que isso será implementado no Go.

Eu acho que é importante ter em mente que o FGG não é definitivamente onde os genéricos em Go vão acabar. É uma visão deles, de fora. E ignora explicitamente um monte de coisas que acabam complicando o produto final.

Sim, muito bom ponto.

Além disso, vou observar que Wadler está trabalhando como parte de uma equipe, e o produto resultante se baseia e está muito próximo da proposta de Contratos, que é o resultado de anos de trabalho dos desenvolvedores principais.

Ao permitir métodos com restrições diferentes do tipo real (assim como tipos diferentes), o FGG abre algumas possibilidades que não eram viáveis ​​com o rascunho dos contratos atuais. ...

@urandom Estou curioso para saber como é esse exemplo do Iterator; você se importaria de jogar algo juntos?

Separadamente, estou interessado no que os genéricos podem fazer além de mapas, filtros e coisas funcionais, e mais curioso como eles podem beneficiar um projeto como o k8s. (Não que eles iriam refatorar neste ponto, mas ouvi anedoticamente que a falta de genéricos exigiu algum trabalho de pés sofisticado, acho que com Recursos Personalizados? Alguém mais familiarizado com o projeto pode me corrigir.)

Eu sinto que as declarações de método usando isso estão começando a ficar difíceis de ler - especialmente se os parâmetros de tipo de um tipo genérico também devem ser listados no receptor.

Talvez gofmt possa ajudar de alguma forma? Talvez precisemos ir multi-linha. Vale a pena brincar, talvez.

Como mencionado por outros, isso permite que você tenha o mesmo tipo genérico implementando métodos diferentes, dependendo de quais tipos foram instanciados.

Eu vejo o que você está dizendo @Merovius

Foi chamado por Wadler como uma diferença, e permite que ele resolva seu problema de expressão, mas você faz um bom ponto de que os pacotes herméticos do Go parecem limitar o que você pode/deve fazer com isso. Você consegue pensar em algum caso real em que você gostaria de fazer isso?

Como mencionado por outros, isso permite que você tenha o mesmo tipo genérico implementando métodos diferentes, dependendo de quais tipos foram instanciados.

Eu vejo o que você está dizendo @Merovius

Foi chamado por Wadler como uma diferença, e permite que ele resolva seu problema de expressão, mas você faz um bom ponto de que os pacotes herméticos do Go parecem limitar o que você pode/deve fazer com isso. Você consegue pensar em algum caso real em que você gostaria de fazer isso?

Ironicamente, meu primeiro pensamento foi que ele poderia ser usado para resolver alguns dos desafios descritos neste artigo: https://blog.merovius.de/2017/07/30/the-trouble-with-optional-interfaces.html

@Caixa de ferramentas

Separadamente, estou interessado no que os genéricos podem fazer além de mapas e filtros e coisas funcionais,

FWIW, deve-se esclarecer que isso é meio que vender "mapas e filtros e coisas funcionais" a descoberto. Eu pessoalmente não quero map e filter sobre estruturas de dados embutidas no meu código, por exemplo (eu prefiro for-loops). Mas também pode significar

  1. Fornecendo acesso generalizado a qualquer estrutura de dados de terceiros. ou seja map e filter podem ser feitos para trabalhar sobre árvores genéricas, ou mapas ordenados, ou… também. Assim, você pode trocar o que está mapeado por mais poder. E mais importante
  2. Você pode trocar como ele é mapeado. Por exemplo, você pode criar uma versão de Compose que possa gerar várias goroutines para cada função e executá-las simultaneamente, usando canais. Isso facilitaria a execução de pipelines de processamento de dados simultâneos e a ampliação do gargalo automaticamente, precisando apenas escrever func(A) B s. Ou você pode colocar as mesmas funções em uma estrutura que executa milhares de cópias do programa em um cluster, agendando lotes de dados entre eles (é isso que eu mencionei quando vinculei ao Flume acima).

Portanto, embora poder escrever Map e Filter e Reduce possa parecer chato na superfície, as mesmas técnicas abrem algumas possibilidades realmente interessantes para facilitar a computação escalável.

@ChrisHines

Ironicamente, meu primeiro pensamento foi que ele poderia ser usado para resolver alguns dos desafios descritos neste artigo: https://blog.merovius.de/2017/07/30/the-trouble-with-optional-interfaces.html

É um pensamento interessante e certamente parece que deveria. Mas não vejo como, ainda. Se você pegar o exemplo ResponseWriter , parece que isso pode permitir que você escreva wrappers genéricos e de tipo seguro, com métodos diferentes, dependendo do que o ResponseWriter suporta. Mas, mesmo que você possa usar limites diferentes em métodos diferentes, ainda precisa anotá-los. Portanto, embora possa tornar a situação segura para o tipo no sentido de que você não adiciona métodos que não suporta, ainda precisa enumerar todos os métodos aos quais pode oferecer suporte, portanto, o middleware ainda pode mascarar algumas interfaces opcionais apenas por não saber sobre eles. Enquanto isso, você também pode (mesmo sem esse recurso) fazer

type Middleware (type RW http.ResponseWriter) struct {
    RW
}

e sobrescreva métodos seletivos que você gosta - e tenha todos os outros métodos de RW promovidos. Portanto, você nem precisa escrever wrappers e, de forma transparente, obter os métodos que você não conhece.

Portanto, supondo que obtenhamos métodos promovidos para parâmetros de tipo incorporados em estruturas genéricas (e espero que sim), os problemas parecem ser melhor resolvidos por esse método.

Eu acho que a solução específica para http.ResponseWriter é algo como errors.Is/As . Não precisa haver uma mudança de idioma, apenas uma adição de biblioteca para criar um método padrão de encapsulamento de ResponseWriter e uma maneira de consultar se algum dos ResponseWriters em uma cadeia pode manipular, por exemplo, wPush. Estou cético de que os genéricos seriam uma boa opção para algo assim, porque o ponto principal é ter escolha de tempo de execução entre interfaces opcionais, por exemplo, o Push só está disponível em http2 e não se eu estiver criando um servidor de desenvolvimento local http1.

Olhando através do Github, acho que nunca criei um problema para essa ideia, então talvez eu faça isso agora.

Editado: #39558.

@toolbox
Meu palpite é que seria algo assim, junto com seu código interno de monomorfização:

package iter

type Any interface{}

type Iterator(type T Any) interface {
    Next() bool
    Value() T
}

type ReversibleIterator(type T Any) interface {
    Iterator(T)
    NextBack() bool
}

type mapIt(type I Iterator(T), T Any, U Any) struct {
    parent I
    mapF func(T) U
}

func (i mapIt(type I Iterator(T))) Next() bool {
    return i.parent.Next()
}

func (i mapIt(type I Iterator(T), T Any, U Any)) Value() U { 
    return i.mapF(i.parent.Value())
}

func (i mapIt(type I ReversibleIterator(T))) NextBack() bool { 
    return i.parent.NextBack()
}

// Monomorphisation
type mapIt<OnlyForward, int, float64> struct {
    parent OnlyForward,
    mapF func(int) float64
}

func (i mapIt<OnlyForward, int, float64>) Next() bool {
    return i.parent.Next()
}

func (i mapIt<OnlyForward, int, float64>) Value() float64 {
    return i.mapF(i.parent.Value())
}

type mapIt<Slice, int, string> struct {
    parent Slice,
    mapF func(int) string
}

func (i mapIt<Slice, int, string>) Next() bool {
    return i.parent.Next()
}

func (i mapIt<Slice, int, string>) Value() string {
    return i.mapF(i.parent.Value())
}

func (i mapIt<Slice, int, string>) NextBack() bool {
    return i.parent.NextBack()
}



Meu palpite é que seria algo assim, junto com seu código interno de monomorfização:

FWIW aqui está um tweet meu de alguns anos atrás explorando como os iteradores podem funcionar em Go com genéricos. Se você fizer uma substituição global para substituir <T> por (type T) , terá algo não muito longe da proposta atual: https://twitter.com/rogpeppe/status/425035488425037824

FWIW, deve-se esclarecer que isso é meio que vender "mapas e filtros e coisas funcionais" a descoberto. Pessoalmente, não quero mapear e filtrar estruturas de dados internas no meu código, por exemplo (prefiro for-loops). Mas também pode significar...

Entendo seu ponto e não discordo, e sim, nos beneficiaremos das coisas que seus exemplos cobrem.
Mas ainda me pergunto como algo como k8s seria afetado, ou outra base de código com tipos de dados "genéricos" onde os tipos de ações executadas não são mapas ou filtros, ou pelo menos vão além disso. Eu me pergunto o quão eficazes são os Contratos ou FGG em aumentar a segurança de tipo e o desempenho nesses tipos de contextos.

Quer saber se alguém pode apontar para uma base de código, esperançosamente mais simples que o k8s, que se encaixe nesse tipo de categoria?

@urandom uau. Então, se você instanciar um mapIt com um parent que implementa ReversibleIterator então mapIt tem um método NextBack() e se não, não tem t. Será que estou lendo certo?

Pensando nisso, parece que é útil do ponto de vista da biblioteca. Você tem alguns tipos de struct genéricos que são bastante abertos (params de tipo Any ) e eles têm muitos métodos, restritos por várias interfaces. Então, quando você usa a biblioteca em seu próprio código, o tipo que você incorpora no struct lhe dá a capacidade de chamar um determinado conjunto de métodos, para que você obtenha um determinado conjunto de funcionalidades da biblioteca. O que é esse conjunto de funcionalidades é descoberto em tempo de compilação com base nos métodos que seu tipo possui.

... Parece um pouco com o que @ChrisHines trouxe em que você poderia escrever código que tem mais ou menos funcionalidade com base no que seu tipo implementa, mas, novamente, é realmente uma questão do conjunto de métodos disponível aumentar ou diminuir, não o comportamento de um único método, então sim, eu não vejo como a coisa do sequestrador http2 é ajudada com isso.

De qualquer forma, muito interessante.

Não que eu faria isso, mas suponho que isso seria possível:

type OverrideX interface {
    GetX() int
}

type OverrideY interface {
    GetY() int
}

type Inheritor(type child Any) struct {
    Parent
    c child
}

func (i Inheritor(type child OverrideX)) GetX() int {
    return i.c.GetX()
}

func (i Inheritor(type child OverrideY)) GetY() int {
    return i.c.GetY()
}

type Parent struct {
    x, y int
}

func (p Parent) GetX() int {
    return p.x
}

func (p Parent) GetY() int {
    return p.y
}

type Child struct {
    x int
}

func (c Child) GetX() int {
    return c.x
}

func main() {
    i := Inheritor(Child){Parent{5, 6}, Child{3}}
    x, y := i.GetX(), i.GetY() // 3, 6
}

Novamente, principalmente uma piada, mas acho que é bom explorar os limites do que é possível.

Edit: Hm, mostra como você pode ter diferentes conjuntos de métodos dependendo do parâmetro de tipo, mas produz exatamente o mesmo efeito que apenas incorporar Parent em Child . Mais uma vez, exemplo bobo ;)

Eu não sou um grande fã de ter métodos que só podem ser chamados para um determinado tipo. Dado o exemplo do @toolbox , provavelmente seria difícil testar devido ao fato de que alguns métodos só podem ser chamados para algum filho específico - o testador provavelmente perderá algum caso. Também não está claro quais métodos estão disponíveis e exigir que um IDE forneça sugestões não é o que o Go deve exigir. No entanto, você pode implementar isso usando apenas o tipo fornecido pela struct fazendo uma declaração de tipo no método.

func (i Inheritor(type child Any)) GetX() int {
    if c, ok := i.c.(OverrideX); ok {
        return c.GetX()
    }
    return i.Parent.GetX()
}

func (i Inheritor(type child Any)) GetY() int {
    if c, ok := i.c.(OverrideY); ok {
        return c.GetY()
    }
    return i.Parent.GetY()
} 

Esse código também é seguro, claro, fácil de testar e provavelmente é executado de forma idêntica ao original sem confusão.

@TotallyGamerJet
Esse exemplo em particular é seguro de tipo, mas outros não são, e exigirá pânicos de tempo de execução com tipos incompatíveis.

Além disso, não tenho certeza de como o testador poderia perder algum caso, já que eles são provavelmente os que escreveram o código genérico em primeiro lugar. Além disso, se é ou não claro é um pouco subjetivo, embora definitivamente não exija um IDE para deduzir. Tenha em mente que isso não é sobrecarga de função, o método pode ser chamado ou não, então não é como se algum caso pudesse ser ignorado por acidente. Qualquer um pode ver que esse método existe para um determinado tipo e pode precisar lê-lo novamente para entender qual tipo é necessário, mas é isso.

@urandom eu não quis dizer necessariamente com esse exemplo específico que alguém perderia um caso - é muito curto. Eu quis dizer que quando você tem toneladas de métodos que podem ser chamados apenas para determinados tipos. Por isso, mantenho a não utilização de subtipagem (como gosto de chamar). É até possível resolver o "Problema de Expressão" sem usar asserções de tipo ou subtipagem. Veja como:

type Any interface {}

type Evaler(type t Any) interface {
    Eval() t
}

type Num struct {
    value int
}

func (n Num) Eval() int {
    return n.value
}

type Plus(type a Evaler(type t Any)) struct {
    left a
    right a
}

func (p Plus(type a Evaler(type t Any)) Eval() t {
    return p.left.Eval() + p.right.Eval()
}

func (p Plus(type a Evaler(type t Any)) String() string {
    return fmt.Sprintf("(%s+%s)", p.left, p.right)
}

type Expr interface {
    Evaler
    fmt.Stringer
}

func main() {
    var e Expr = Plus(Num){Num{1}, Num{2}}
    var v int = e.Eval() // 3
    var s string = e.String() // "(1+2)"
}

Qualquer uso indevido do método Eval deve ser detectado em tempo de compilação devido ao fato de que não é permitido chamar Eval no Plus com um tipo que não implementa adição. Embora seja possível usar incorretamente o String() (possivelmente adicionando structs), bons testes devem detectar esses casos. E Go geralmente abraça a simplicidade sobre a "correção". A única coisa que se ganha com a subdigitação é mais confusão nos documentos e no uso. Se você puder fornecer um exemplo que exija subdigitação, posso estar mais inclinado a pensar que é uma boa ideia, mas atualmente não estou convencido.
EDIT: Corrigido o erro e melhorado

@TotallyGamerJet no seu exemplo, o método String deve chamar String recursivamente, não Eval

@TotallyGamerJet no seu exemplo, o método String deve chamar String recursivamente, não Eval

@mágico
Eu não estou certo do que você quer dizer. O tipo da estrutura Plus é um Evaler que não garante que fmt.Stringer seja satisfeito. Chamar o String() em ambos os avaliadores exigiria uma declaração de tipo e, portanto, não seria typesafe.

@TotallyGamerJet
Infelizmente, essa é a ideia do método String. Ele deve chamar recursivamente qualquer método String em seus membros, caso contrário, não faz sentido. Mas você já vê que isso exigiria uma declaração de tipo e um panic se você não puder garantir que o método no tipo Plug exija um tipo a que tenha um método String

@urandom
Você está certo! Surpreendentemente, o Sprintf fará essa afirmação de tipo para você. Assim, você pode simplesmente enviar os campos esquerdo e direito. Embora ainda possa entrar em pânico se os tipos no Plus não implementarem Stringer, estou bem com isso porque é possível evitar pânico usando o verbo %v para imprimir a estrutura (ele chamará String( ) se disponível). Acho que esta solução é clara e quaisquer outras incertezas devem ser documentadas no código. Portanto, ainda não estou convencido de por que a subdigitação é necessária.

@TotallyGamerJet
Eu pessoalmente ainda não consigo ver quais problemas podem surgir se for permitido ter métodos com restrições diferentes. O método ainda está lá, e o código descreve claramente quais argumentos (e receptor, no caso especial) são necessários.
Assim como ter um método, aceitar um argumento string , ou um receptor MyType , é claramente legível e não ambíguo, a seguinte definição também seria:

func (rec MyType(type T SomeInterface(T)) Foo() T

Os requisitos estão claramente marcados na própria assinatura. IE é de MyType(type T SomeInterface(T)) e nada mais.

Alterar https://golang.org/cl/238003 menciona este problema: design: add go2draft-type-parameters.md

Alterar https://golang.org/cl/238241 menciona este problema: content: add generics-next-step article

O Natal está adiantado!

  • Eu posso ver que muito esforço foi feito para tornar o documento de design acessível, ele mostra e é ótimo e muito apreciado.
  • Esta iteração é uma grande melhoria aos meus olhos e eu pude ver isso sendo implementado como está.
  • Concordo com praticamente todo o raciocínio e lógica.
  • Assim, se você especificar uma restrição para um único parâmetro de tipo, deverá fazê-lo para todos.
  • Comparável soa bem.
  • As listas de tipos nas interfaces não são ruins; concordo que é melhor do que os métodos do operador, mas na minha opinião é provavelmente a maior área para discussão adicional.
  • A inferência de tipos é (ainda) ótima.
  • A inferência para restrições parametrizadas por tipo de argumento único parece mais inteligência do que clareza.
  • Eu gosto de "Não estamos afirmando que isso é simples" no exemplo do gráfico. Isso é bom.
  • (type *T constraint) parece uma boa solução para o problema do ponteiro.
  • Totalmente de acordo com a mudança func(x(T)) .
  • Acho que queremos inferência de tipo para literais compostos logo de cara? 😄

Obrigado a equipe Go! 🎉

https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#comparable -types-in-constraints

Eu acredito que comparável é mais como um tipo de construção do que uma interface. Acredito que seja um pequeno bug no rascunho da proposta.

type ComparableHasher interface {
    comparable
    Hash() uintptr
}

precisa ser

type ComparableHasher interface {
    type comparable
    Hash() uintptr
}

O playground também parece indicar que precisa ser type comparable
https://go2goplay.golang.org/p/mhrl0xYsMyj

EDIT: Ian Lance Taylor e Robert Griesemer estão corrigindo a ferramenta go2go (era um pequeno bug no tradutor go2go, não no rascunho. O rascunho do design estava correto)

Já houve pensamentos sobre permitir que as pessoas escrevam suas próprias tabelas de hash genéricas e similares? ISTM que atualmente é muito limitado (especialmente em comparação com o mapa embutido). Basicamente, o mapa interno tem comparable como uma restrição de chave, mas é claro que == e != não são suficientes para implementar uma tabela de hash. Uma interface como ComparableHasher apenas passa a responsabilidade de escrever uma função hash para o chamador, não responde à questão de como ela realmente ficaria (além disso, o chamador provavelmente não deveria ser responsável por isso; escrever boas funções de hash é difícil). Por fim, usar ponteiros como chaves pode ser fundamentalmente impossível - converter um ponteiro em um uintptr para usar como um índice arriscaria o GC mover o ponteiro e, portanto, o balde mudar (exceto esse problema, expondo um func hash(type T comparable)(v T) uintptr pré-declarado

Posso aceitar "não é realmente viável" como resposta, só estou curioso para saber se você pensou nisso :)

@gertcuykens Confirmei uma correção na ferramenta go2go para lidar com comparable conforme pretendido.

@Merovius Esperamos que as pessoas que escrevem uma tabela de hash genérica forneçam sua própria função de hash e possivelmente sua própria função de comparação. Ao escrever sua própria função de hash, o pacote https://golang.org/pkg/hash/maphash/ pode ser útil. Você está correto que o hash de um valor de ponteiro deve depender do valor para o qual esse ponteiro aponta; não pode depender do valor do ponteiro convertido para uintptr .

Não tenho certeza se isso é uma limitação da implementação atual da ferramenta, mas uma tentativa de retornar um tipo genérico restrito por uma interface retorna um erro:
https://go2goplay.golang.org/p/KYRFL-vrcUF

Eu implementei um caso de uso do mundo real que eu tinha para genéricos ontem . É uma abstração de pipeline genérica que permite dimensionar estágios do pipeline de forma independente e suporta cancelamento e tratamento de erros (não é executado no playground, porque depende de errgroup , mas executá-lo usando a ferramenta go2go parece trabalhos). Algumas observações:

  • Foi muito divertido. Ter um verificador de tipo funcional realmente ajudou muito na iteração do design, traduzindo falhas de design em erros de tipo. O resultado final é ~100 LOC incluindo comentários. Portanto, no geral, a experiência de escrever código genérico é agradável, IMO.
  • Este caso de uso, pelo menos, funciona sem problemas com inferência de tipo, sem necessidade de instanciações explícitas. Eu acho que isso é um bom presságio para o design de inferência.
  • Acho que este exemplo se beneficiaria da capacidade de ter métodos com parâmetros de tipo extras. A necessidade de uma função de nível superior para Compose significa que a construção do pipeline acontece ao contrário - os últimos estágios do pipeline precisam ser construídos para passá-lo para as funções que estão construindo os estágios anteriores. Se os métodos pudessem ter parâmetros de tipo, você poderia ter Stage ser um tipo concreto e fazer func (s *Stage(A, B)) Compose(type C)(n int, f func(B) C) *Stage(A, C) . E a construção do pipeline seria na mesma ordem em que é canalizada (veja o comentário no playground). É claro que também pode haver uma API mais elegante no rascunho existente que não vejo - é difícil provar uma negativa. Eu estaria interessado em ver um exemplo de trabalho disso.

No geral, eu gosto do novo rascunho, FWIW :) A eliminação de contratos da IMO é uma melhoria, assim como a nova maneira de especificar os operadores necessários por meio de listas de tipos.

[editar: Corrigido um bug no meu código em que um deadlock poderia ocorrer se um estágio de pipeline falhasse. A simultaneidade é difícil]

Uma pergunta para a ramificação da ferramenta: ela acompanhará a última versão go (então v1.15, v1.15.1, ...)?

@urandom : Observe que o valor que você está retornando em seu código é do tipo Foo(T). Cada
tal instanciação de tipo produz um novo tipo definido, neste caso Foo(T).
(Claro, se você tiver vários Foo(T) no código, eles são todos iguais
tipo definido).

Mas o tipo de resultado de sua função é V, que é um parâmetro de tipo. Observação
que o parâmetro de tipo é restringido pela interface do Valuer, mas é
_não_ uma interface (ou mesmo essa interface). V é um parâmetro de tipo que é
um novo tipo de tipo sobre o qual conhecemos coisas descritas por sua restrição.
Com relação à designabilidade, ele age como um tipo definido chamado V.

Então você está tentando atribuir um valor do tipo Foo(T) a uma variável do tipo V
(que não é Foo(T) nem Valuer(T), possui apenas propriedades descritas por
Avaliador(T)). Assim, a atribuição falha.

(Como um aparte, ainda estamos refinando nossa compreensão dos parâmetros de tipo
e eventualmente precisar soletrar com precisão suficiente para que possamos escrever um
especificação Mas tenha em mente que cada parâmetro de tipo é efetivamente um novo
tipo definido sobre nós sabemos apenas o quanto sua restrição de tipo especifica.)

Talvez você quisesse escrever isso: https://go2goplay.golang.org/p/8Hz6eWSn8Ek?

@Inuart Se por ferramenta de ramificação você quer dizer a ramificação dev.go2go: Este é um protótipo, foi construído com conveniência em mente e para fins de experimentação. Queremos que as pessoas brinquem com ele e tentem escrever código, mas não é uma boa ideia _confiar_ no tradutor para software de produção. Muitas coisas podem mudar (até a sintaxe, se necessário). Vamos corrigir bugs e ajustar o design à medida que aprendemos com o feedback. Manter-se atualizado com os últimos lançamentos do Go parece menos importante.

Eu implementei um caso de uso do mundo real que eu tinha para genéricos ontem. É uma abstração de pipeline genérica que permite escalar estágios do pipeline de forma independente e suporta cancelamento e tratamento de erros (não roda no playground, porque depende do errgroup, mas executá-lo usando a ferramenta go2go parece funcionar).

Eu gosto do exemplo. Acabei de ler na íntegra e o que mais me fez tropeçar (nem vale a pena explicar) não tinha nada a ver com os genéricos envolvidos. Acho que a mesma construção sem genéricos não seria muito mais fácil de entender. Também é definitivamente uma daquelas coisas que você quer escrever uma vez, com testes, e não ter que brincar de novo depois.

Uma coisa que pode ajudar na legibilidade e revisão é se a ferramenta Go tivesse uma maneira de exibir a versão monomorfizada do código genérico, para que você possa ver como as coisas ficam. Pode ser inviável, em parte porque as funções podem nem ser monomorfizadas na implementação final do compilador, mas acho que seria valioso se fosse atingível.

Acho que este exemplo se beneficiaria da capacidade de ter métodos com parâmetros de tipo extras.

Eu vi esse comentário em seu playground também; definitivamente a sintaxe de chamada alternativa parece mais legível e direta. Você poderia explicar isso com mais detalhes? Tendo mal entendido seu código de exemplo, estou tendo problemas para fazer o salto :)

Então você está tentando atribuir um valor do tipo Foo(T) a uma variável do tipo V
(que não é Foo(T) nem Valuer(T), possui apenas propriedades descritas por
Avaliador(T)). Assim, a atribuição falha.

Ótima explicação.

...Caso contrário, é triste ver que o post HN foi sequestrado pela multidão Rust. Teria sido bom obter mais feedback dos Gophers sobre a proposta.

Duas perguntas para a equipe Go:

Existe uma diferença entre esses dois, ou é um bug no playground go2? O primeiro compila, o segundo dá um erro

type Addable interface {
    type int, float64
}

func Add(type T Addable)(a, b T) T {
  return a + b
}
type Addable interface {
    type int, float64, string
}

func Add(type T Addable)(a, b T) T {
  return a + b
}

Falha com: invalid operation: operator + not defined for a (variable of type T)

Bem, esta foi uma surpresa muito inesperada e agradável. Eu estava esperando por uma maneira de realmente experimentar isso em algum momento, mas eu não esperava isso tão cedo.

Primeiro de tudo, encontrei um bug: https://go2goplay.golang.org/p/1r0NQnJE-NZ

Em segundo lugar, construí um exemplo de iterador e fiquei um pouco surpreso ao descobrir que essa inferência de tipo não funciona. Eu posso simplesmente fazer com que ele retorne um tipo de interface diretamente, mas não achei que ele não seria capaz de inferir esse, já que todas as informações de tipo necessárias estão chegando pelo argumento.

Edit: Além disso, como várias pessoas disseram, acho que permitir que novos tipos sejam adicionados durante as declarações de métodos seria bastante útil. No que diz respeito à implementação da interface, você pode simplesmente não permitir a implementação da interface, apenas permitir a implementação se a interface também chamar genéricos lá ( type Example interface { Method(type T someConstraint)(v T) bool } ), ou, possivelmente, você poderia implementar a interface se _qualquer_ possível variante dele implementa a interface e, em seguida, faz com que a chamada seja restrita ao que a interface deseja se for chamada por meio da interface. Por exemplo,

```vai
tipo interface de interface {
Get(string) string
}

tipo Exemplo (tipo T) struct {
vT
}

// Isso só funcionará porque Interface.Get é mais específico que Example.Get.
func (e Exemplo(T)) Get(tipo R)(v R) T {
return fmt.Sprintf("%v: %v", v, ev)
}

func FazerAlgo(interinterface) {
// Subjacente é Example(string) e Example(string).Get(string) é assumido porque é obrigatório.
fmt.Println(inter.Get("exemplo"))
}

func main(){
// Permitido porque Example(string).Get(string) é possível.
DoSomething(Example(string){v: "Um exemplo."})
}

@DeedleFake A primeira coisa que você está relatando não é um bug. Você precisará escrever https://go2goplay.golang.org/p/qo3hnviiN4k no momento. Isso está documentado no rascunho do projeto. Em uma lista de parâmetros, escrever a(b) é interpretado como a (b) ( a do tipo entre parênteses b ) para compatibilidade com versões anteriores. Podemos mudar isso daqui para frente.

O exemplo do Iterator é interessante - parece um bug à primeira vista. Por favor, registre um bug (instruções na postagem do blog) e atribua-o a mim. Obrigado.

@Kashomon A postagem do blog (https://blog.golang.org/generics-next-step) sugere a lista de discussão para discussão e apresentação de problemas separados para bugs. Obrigado.

Acho que o problema com + já foi corrigido.

@toolbox

Uma coisa que pode ajudar na legibilidade e revisão é se a ferramenta Go tivesse uma maneira de exibir a versão monomorfizada do código genérico, para que você possa ver como as coisas ficam. Pode ser inviável, em parte porque as funções podem nem ser monomorfizadas na implementação final do compilador, mas acho que seria valioso se fosse atingível.

A ferramenta go2go pode fazer isso. Em vez de usar go tool go2go run x.go2 , escreva go tool go2go translate x.go2 . Isso produzirá um arquivo x.go com o código traduzido.

Dito isto, devo dizer que é bastante desafiador de ler. Não é impossível, mas não é fácil.

@griesemer

Eu entendo que o argumento de retorno pode ser uma interface, mas realmente não entendo por que não pode ser o próprio tipo genérico.

Você pode, por exemplo, usar esse mesmo tipo genérico como parâmetro de entrada, e isso funciona bem:
https://go2goplay.golang.org/p/LuDrlT3zLRb
Isso funciona porque o tipo já foi instanciado?

@urandom escreveu:

Eu entendo que o argumento de retorno pode ser uma interface, mas realmente não entendo por que não pode ser o próprio tipo genérico.

Teoricamente, poderia, mas não faz sentido tornar um tipo de retorno genérico quando o tipo de retorno não é genérico porque é determinado pelo bloco de função, ou seja, pelo valor de retorno.

Normalmente, os parâmetros genéricos são totalmente determinados pela tupla do valor do parâmetro ou pelo tipo do aplicativo de função no site de chamada (determina a instanciação do tipo de retorno genérico).

Teoricamente, você também pode permitir parâmetros de tipo genérico que não são determinados pela tupla do valor do parâmetro e devem ser fornecidos explicitamente, por exemplo:

func f(type S)(i int) int
{
    s S =...
    return 2
}

não sei quanto sentido isso faz.

@urandom eu não quis dizer necessariamente com esse exemplo específico que alguém perderia um caso - é muito curto. Eu quis dizer que quando você tem toneladas de métodos que podem ser chamados apenas para determinados tipos. Por isso, mantenho a não utilização de subtipagem (como gosto de chamar). É até possível resolver o "Problema de Expressão" sem usar asserções de tipo ou subtipagem. Veja como:

type Any interface {}

type Evaler(type t Any) interface {
  Eval() t
}

type Num struct {
  value int
}

func (n Num) Eval() int {
  return n.value
}

type Plus(type a Evaler(type t Any)) struct {
  left a
  right a
}

func (p Plus(type a Evaler(type t Any)) Eval() t {
  return p.left.Eval() + p.right.Eval()
}

func (p Plus(type a Evaler(type t Any)) String() string {
  return fmt.Sprintf("(%s+%s)", p.left, p.right)
}

type Expr interface {
  Evaler
  fmt.Stringer
}

func main() {
  var e Expr = Plus(Num){Num{1}, Num{2}}
  var v int = e.Eval() // 3
  var s string = e.String() // "(1+2)"
}

Qualquer uso indevido do método Eval deve ser detectado em tempo de compilação devido ao fato de que não é permitido chamar Eval no Plus com um tipo que não implementa adição. Embora seja possível usar incorretamente o String() (possivelmente adicionando structs), bons testes devem detectar esses casos. E Go geralmente abraça a simplicidade sobre a "correção". A única coisa que se ganha com a subdigitação é mais confusão nos documentos e no uso. Se você puder fornecer um exemplo que exija subdigitação, posso estar mais inclinado a pensar que é uma boa ideia, mas atualmente não estou convencido.
EDIT: Corrigido o erro e melhorado

Eu não sei, por que não usar '<>'?

@99yun
Consulte as perguntas frequentes incluídas no rascunho atualizado

Por que não usar a sintaxe F\como C++ e Java?
Ao analisar o código dentro de uma função, como v := F\, no ponto de ver o < é ambíguo se estamos vendo uma instanciação de tipo ou uma expressão usando o operador <. Resolver isso requer uma antecipação efetivamente ilimitada. Em geral, nos esforçamos para manter o analisador Go eficiente.

@urandom Um corpo de função genérico é sempre verificado sem instanciação (*); em geral (se for exportado, por exemplo) não podemos saber como será instanciado. Ao ser verificado tipo, ele só pode confiar nas informações disponíveis. Se o tipo de resultado for um parâmetro de tipo e a expressão de retorno for de um tipo diferente que não seja compatível com atribuição, o retorno não funcionará. Ou, em outras palavras, se uma função genérica for invocada com argumentos de tipo (possivelmente inferidos), o corpo da função não será verificado novamente com esses argumentos de tipo. Ele apenas verifica se os argumentos de tipo satisfazem as restrições da função genérica (após instanciar a assinatura da função com esses argumentos de tipo). Espero que ajude.

(*) Mais precisamente, a função genérica é verificada como se fosse instanciada com seus próprios parâmetros de tipo; os parâmetros de tipo são tipos reais; nós apenas sabemos sobre eles tanto quanto suas restrições nos dizem.

Por favor, vamos continuar esta discussão em outro lugar. Se você tiver mais perguntas com um pedaço de código que você acha que deveria estar funcionando, registre um problema para que possamos discuti-lo lá. Obrigado.

Não parece haver uma maneira de usar uma função para criar um valor zero de uma estrutura genérica. Tomemos por exemplo esta função:

func zero(type T)() T {
    var zero T
    return zero
}

Parece funcionar para os tipos básicos (int, float32 etc.). No entanto, quando você tem um struct que possui um campo genérico, as coisas ficam estranhas. Considere por exemplo:

type Opt(type T) struct {
    val T
}

func (o Opt(T)) Do() { /*stuff*/ }

Tudo parece bom. No entanto, ao fazer:

opt := zero(Opt(int))
opt.Do() 

ele não compila dando o erro: opt.Do undefined (type func() Opt(int) has no field or method Do) Eu posso entender se não é possível fazer isso, mas é estranho pensar que é uma função quando int deveria fazer parte do tipo Opt. Mas o mais estranho é que é possível fazer isso:

opt := zero(Opt)      //  But somehow this line compiles
opt(int).Do()         // This will panic

Não tenho certeza de qual parte é um bug e qual parte é pretendida.
Código: https://go2goplay.golang.org/p/M0VvyEYwbQU

@TotallyGamerJet

Sua função zero() não tem argumentos, então não há inferência de tipo acontecendo. Você tem que instanciar a função zero e então chamá-la.

opt := zero(Opt(int))()
opt.Do()

https://go2goplay.golang.org/p/N6ip-nm1BP-

@toolbox
Ah sim. Eu pensei que estava fornecendo o tipo, mas esqueci o segundo conjunto de parênteses para realmente chamar a função. Ainda estou me acostumando com esses genéricos.

Eu sempre entendi que não ter genéricos em Go foi uma decisão de design e não um descuido. Isso tornou o Go muito mais simples e não consigo entender a paranóia exagerada contra uma simples duplicação de cópias. Em nossa empresa, criamos toneladas de código Go e nunca encontramos uma única instância em que preferiríamos os genéricos.

Para nós, isso definitivamente fará com que Go se sinta menos Go e parece que a multidão do hype finalmente conseguiu afetar o desenvolvimento de Go na direção errada. Eles não podiam simplesmente deixar Go em sua beleza simplista, não, eles tinham que ficar reclamando e reclamando até que finalmente conseguissem o que queriam.

Desculpe, não é para degradar ninguém, mas é assim que começa a destruição de uma linguagem lindamente projetada. Qual é o próximo? Se continuarmos mudando as coisas, como muitas pessoas gostariam, acabamos com "C++" ou "JavaScript".

Apenas deixe Vá do jeito que deveria ser!

@iio7 Eu sou o QI mais baixo de todos aqui, meu futuro depende de ter certeza de que posso ler o código de outras pessoas. O hype começou não apenas por causa de genéricos, mas porque o novo design não requer uma mudança de linguagem na proposta atual, então estamos todos empolgados que há uma janela para manter as coisas simples e ainda ter algumas guloseimas genéricas e funcionais. Não me entenda mal, eu sei que sempre haverá uma pessoa na equipe que escreve código como um cientista de foguetes e eu, o macaco, vamos entender assim? Então, os exemplos que você vê agora são os do cientista de foguetes e, para ser honesto, sim, demoro algum tempo para lê-lo, mas no final, com algumas tentativas e erros, eu sei o que eles estão tentando programar. Tudo o que estou dizendo é confiar em Ian e Robert e os outros, eles ainda não terminaram o design. Não ficaria surpreso em um ano ou mais, existem ferramentas que ajudam o compilador a falar uma linguagem de macaco simples e perfeita, não importa o quão difícil seja o código genérico de foguete que você jogue nele. O melhor feedback que você pode dar é reescrever alguns exemplos e apontar se algo é muito projetado para que eles possam garantir que o compilador reclame sobre isso ou seja reescrito por algo como a ferramenta vet automaticamente.

Eu li o FAQ sobre <> mas para uma pessoa estúpida como eu, como é mais difícil para o analisador determinar se é uma chamada genérica se parece com isso v := F<T> em vez de v := F(T) ? Não é mais difícil com os parênteses, pois não saberá se é uma chamada de função com T como argumento regular?

Além disso, acho que o analisador deve ser mantido rápido, mas não vamos esquecer também o que é mais fácil para o programador ler, o que é IMO igualmente importante. É mais fácil entender o que v := F(T) faz imediatamente? Ou v := F<T> é mais fácil? Também importante levar em consideração :)

Não argumentando a favor nem contra v := F<T> , apenas levantando alguns pensamentos que podem valer a pena considerar.

Isso é legal Vá hoje :

    f, c, d, e := 1, 2, 3, 4
    a, b := f < c, d > (e)
    fmt.Println(a, b) // true false

Não faz sentido discutir colchetes angulares a menos que você forneça uma proposta sobre o que fazer a respeito (quebrar a compatibilidade?). É para todos os efeitos uma questão morta. Há efetivamente zero chance de chaves angulares serem adotadas pela equipe Go. Por favor, discuta qualquer outra coisa.

Editar para adicionar: Desculpe se este comentário foi excessivamente curto. Há muita discussão sobre colchetes angulares no Reddit e HN, o que é muito frustrante para mim porque o problema de compatibilidade com as versões anteriores é bem conhecido há muito tempo por pessoas que se preocupam com genéricos. Eu entendo por que as pessoas preferem colchetes angulares, mas não é possível sem uma mudança radical.

Obrigado pelo seu comentário @iio7. Há sempre um risco diferente de zero de que as coisas saiam do controle. É por isso que estamos usando o máximo de cautela ao longo do caminho. Acredito que o que temos agora é um projeto muito mais limpo e ortogonal do que o que tínhamos no ano passado; e, pessoalmente, espero que possamos torná-lo ainda mais simples, especialmente quando se trata de listas de tipos - mas descobriremos à medida que aprendermos mais. (Um tanto ironicamente, quanto mais ortogonal e limpo o design se tornar, mais poderoso ele será e mais complexo será o código que se pode escrever.) As palavras finais ainda não foram ditas. No ano passado, quando tivemos o primeiro projeto potencialmente viável, a reação de muita gente foi parecida com a sua: "Nós realmente queremos isso?" Esta é uma excelente pergunta e devemos tentar respondê-la da melhor forma possível.

A observação do @gertcuykens também está correta - naturalmente as pessoas que brincam com o protótipo go2go estão explorando seus limites o máximo possível (que é o que queremos), mas no processo também produzem código que provavelmente não passaria em uma produção adequada contexto. Até agora eu vi muitos códigos genéricos que são realmente difíceis de decifrar.

Há situações em que o código genérico seria claramente uma vitória; Estou pensando em algoritmos simultâneos genéricos que nos permitiriam colocar um código sutil em uma biblioteca. É claro que existem várias estruturas de dados de contêiner e coisas como classificação, etc. Provavelmente, a grande maioria do código não precisa de genéricos. Em contraste com outras linguagens, onde os recursos genéricos são fundamentais para muito que se faz na linguagem, em Go, os recursos genéricos são apenas mais uma ferramenta no conjunto de ferramentas Go; não o bloco de construção fundamental sobre o qual todo o resto é construído em cima.

Para comparação: nos primeiros dias do Go, todos tendíamos a usar goroutines e canais em excesso. Demorou um pouco para aprender quando eles eram apropriados e quando não. Agora temos algumas diretrizes mais ou menos estabelecidas e as usamos apenas quando realmente apropriado. Espero que o mesmo aconteceria se tivéssemos genéricos.

Obrigado.

Da seção do projeto de rascunho sobre sintaxes baseadas em [T] :

A linguagem geralmente permite uma vírgula à direita em uma lista separada por vírgulas, portanto, A[T,] deve ser permitido se A for um tipo genérico, mas normalmente não seria permitido para uma expressão de índice. No entanto, o analisador não pode saber se A é um tipo genérico ou um valor de fatia, matriz ou tipo de mapa, portanto, esse erro de análise não pode ser relatado até que a verificação de tipo seja concluída. Novamente, solucionável, mas complicado.

Isso não poderia ser facilmente resolvido apenas tornando a vírgula à direita completamente legal em expressões de índice e, em seguida, apenas com gofmt removê-la?

@DeedleFake Possivelmente. Essa seria certamente uma saída fácil; mas também parece um pouco feio, sintaticamente. Não me lembro de todos os detalhes, mas uma versão anterior tinha suporte para parâmetros de tipo de estilo [tipo T]. Veja a ramificação dev.go2go, confirme 3d4810b5ba onde o suporte foi removido. Alguém poderia desenterrar isso de novo e investigar.

O comprimento dos argumentos genéricos em cada lista [] pode ser limitado à maioria para evitar esse problema, assim como os tipos genéricos internos:

  • [N]T
  • []T
  • mapa[K]T
  • chan T

Observe que, os últimos argumentos em tipos genéricos internos não são todos incluídos em [] .
A sintaxe de declaração genérica é como: https://github.com/dotaheor/unify-Go-builtin-and-custom-generics#the -generic-declaration-syntax

@dotaheor Não sei exatamente o que você está perguntando, mas é claramente necessário oferecer suporte a vários argumentos de tipo para um tipo genérico. Por exemplo, https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#containers .

@ianlancetaylor
O que quero dizer é que cada parâmetro de tipo é delimitado por um [] , então o tipo em seu link pode ser declarado como:

type Map[type K][type V] struct

Quando usado, fica assim:

var m Map[string]int

Um argumento de tipo não delimitado por [] indica o fim do uso de um tipo genérico.

Enquanto pensava em ordenar arrays #39355 em conjunto com genéricos, descobri que "comparável" é tratado de forma especial no rascunho de genéricos atual (presumivelmente devido a não ser capaz de listar todos os tipos comparáveis ​​em uma lista de tipos facilmente) como uma restrição de tipo pré-declarada .

Seria bom se o rascunho de genéricos fosse alterado para também definir "pedido"/"pedível" semelhante a como "comparável" é predefinido. É uma relação comumente usada em valores do mesmo tipo e isso permitiria futuras extensões da linguagem go para definir a ordenação em mais tipos (arrays, structs, slices, sum types, check enums, ...) sem se deparar com a complicação que nem todos os tipos ordenados seriam listáveis ​​em uma lista de tipos como "comparável".

Eu não estou sugerindo que para ser decidido deve ser ordenado para mais tipos na especificação da linguagem mas esta mudança para os genéricos deixa-o mais compatível com tal mudança (uma restrição. seria preterido se estiver usando uma lista de tipos). A classificação de pacotes pode começar com a restrição de tipo pré-declarada "ordenada" e depois pode "apenas" trabalhar com matrizes, por exemplo, se alguma vez for alterada e nenhuma correção para a restrição usada.

@martisch Acho que isso só precisaria acontecer quando os tipos ordenados fossem estendidos. Atualmente, constraints.Ordered pode listar todos os tipos (isso não funciona para comparable , por causa de ponteiros, structs, arrays,… serem comparáveis, então isso tem que ser mágico. Mas ordered está atualmente limitado a um conjunto finito de tipos subjacentes internos) e os usuários podem confiar nisso. Se estendermos os pedidos para arrays (por exemplo), ainda podemos adicionar uma nova restrição mágica ordered e incorporá-la em constraints.Ordered . Isso significa que todos os usuários de constraints.Ordered se beneficiariam automaticamente da nova restrição. É claro que os usuários que escrevem sua própria lista de tipos explícita não se beneficiariam - mas é o mesmo se adicionarmos ordered agora, para usuários que não incorporam isso .

Então, IMO, não há nada perdido em adiar isso até que seja realmente significativo. Não devemos adicionar nenhum conjunto de restrições possível como um identificador pré-declarado - muito menos qualquer conjunto de restrições futuro em potencial :)

Se estendermos os pedidos para arrays (por exemplo), ainda podemos adicionar uma nova restrição mágica ordered e incorporá-la em constraints.Ordered .

@Merovius Esse é um bom ponto que eu não tinha pensado. Isso permite estender constraints.Ordered no futuro de maneira consistente. Se também houver um constraints.Comparable , então ele se encaixa perfeitamente na estrutura geral.

@martisch , observe que ordered — ao contrário comparable — não é coerente como um tipo de interface, a menos que também definamos uma ordem total (global) entre tipos concretos ou proíbamos o código não genérico de usar < em variáveis ​​do tipo ordered , ou proibir o uso de comparable como um tipo geral de interface de tempo de execução.

Caso contrário, a transitividade dos “implementos” se rompe. Considere este fragmento de programa:

    var x constraints.Ordered = int(0)
    var y constraints.Ordered = string("0")
    fmt.Println(x < y)

O que deve produzir? (A resposta é intuitiva ou arbitrária?)

@bcmills
Que tal fun (<)(type T Ordered)(t1 T,t2 T) Bool?

Para comparar tipos aritméticos de tipos diferentes:

Se qualquer aritmética S implementar apenas Ordered(T) para S<:T , então:

//Isn't possible I think
interface SorT(S,T)
{ 
type S,T
}

fun (<)(type R SorT(S,T), S Ordered(R), T Ordered(R))(s S, t T) Bool

deve ser único.

Para polimorfismo de tempo de execução, você exigiria que Ordered fosse parametrizável.
Ou:
Você particiona Ordered em tipos de tupla e depois reescreve (<) para ser:

//but isn't supported that either
fun(<)(type R Ordered)(s R.0,t R.1)

Oi!
Eu tenho uma pergunta.

Existe uma maneira de fazer restrição de tipo que passa apenas tipos genéricos com um parâmetro de tipo?
Algo que passe apenas Result(T) / Option(T) /etc, mas não apenas T .
eu tentei

type Box(type T) interface {
    Val() (T, bool)
}

mas requer o método Val()

type Box(type T) interface{}

é semelhante a interface{} , ou seja, Any

também tentei https://go2goplay.golang.org/p/lkbTI7yppmh -> compilação falha

type Box(type T) interface {
       type Box(T)
}

https://go2goplay.golang.org/p/5NsKWNa3E1k -> compilação falha

type Box(type T) interface{}

type Generic(type T) interface {
    type Box(T)
}

https://go2goplay.golang.org/p/CKzE2J-YOpD -> não funciona

type Box(type T) interface{}

type Generic(type T Box(T)) interface {}

Esse comportamento é esperado ou é apenas um bug de verificação de tipo?

As restrições @tdakkota se aplicam a argumentos de tipo e se aplicam à forma totalmente instanciada de argumentos de tipo. Não há como escrever uma restrição de tipo que coloque quaisquer requisitos na forma não instanciada de um argumento de tipo.

Consulte as perguntas frequentes incluídas no rascunho atualizado

Por que não usar a sintaxe Fcomo C++ e Java?
Ao analisar o código dentro de uma função, como v := F, no ponto de ver o < é ambíguo se estamos vendo uma instanciação de tipo ou uma expressão usando o operador <. Resolver isso requer uma antecipação efetivamente ilimitada. Em geral, nos esforçamos para manter o analisador Go eficiente.

@TotallyGamerJet Tanto faz!

Como lidar com valor zero do tipo genérico? Sem enum, como podemos lidar com valor opcional.
Por exemplo: a versão genérica de vector e um func chamado First retornam o primeiro elemento se for comprimento > 0 senão valor zero do tipo genérico.
Como escrevemos esse código? Como não sabemos qual tipo de vetor, se chan/slice/map , podemos return (nil, false) , se struct ou primitive type como string , int , bool , como lidar?

@leaxoy

var zero T deve ser suficiente

@leaxoy

var zero T deve ser suficiente

Uma variável mágica global como nil ?

@leaxoy
var zero T deve ser suficiente

Uma variável mágica global como nil ?

Há uma proposta em discussão para este tópico - veja proposta: Go 2: valor zero universal com inferência de tipo #35966 .

Ele examina várias novas sintaxes alternativas para uma expressão (não uma instrução como var zero T ) que sempre retornará o valor zero de um tipo.

O valor zero parece viável atualmente, mas pode ocupar espaço na pilha ou no heap? Devemos considerar usar enum Option para concluir isso em uma etapa.
Caso contrário, se o valor zero não ocupa espaço, seria melhor e não seria necessário adicionar enum.

O valor zero parece viável atualmente, mas pode ocupar espaço na pilha ou no heap?

Historicamente, acredito, o compilador Go otimizou esses tipos de casos. Eu não estou muito preocupado.

Um valor de tipo padrão pode ser especificado em modelos C++. Uma construção semelhante foi considerada para parâmetros de tipo genérico go? Potencialmente, isso tornaria possível adaptar tipos existentes sem quebrar o código existente.

Por exemplo, considere o tipo asn1.ObjectIdentifier existente que é um []int . Um problema com esse tipo é que ele não é compatível com a especificação ASN.1, que afirma que cada sub-oid pode ser um INTEIRO de comprimento arbitrário (por exemplo *big.Int ). Potencialmente ObjectIdentifier poderia ser modificado para aceitar um parâmetro genérico, mas isso quebraria muito código existente. Se houvesse uma maneira de especificar int o valor padrão do parâmetro, talvez isso possibilitasse a atualização do código existente.

type SignedInteger interface {
    type int, int32, int64, *big.Int
}
type ObjectIdentifier(type T SignedInteger) []T
// type ObjectIdentifier(type T SignedInteger=int) []T  // `int` would be the default instantiation type.

// New code with generic awareness would compile in go2.
var oid1 ObjectIdentifier(int) = ObjectIdentifier(int){1, 2, 3}

// But existing code would fail to compile:
var oid1 ObjectIdentifier = ObjectIdentifier{1, 2, 3}

Só para ficar claro, o asn1.ObjectIdentifier acima é apenas um exemplo. Não estou dizendo que usar genéricos é a única maneira ou a melhor maneira de resolver o problema de conformidade com ASN.1.

Além disso, existem planos para permitir limites de interface finitos parametrizáveis?:

type Ordable(type T, S) interface {
    type S, type T
}

Como dar suporte a where condição no parâmetro de tipo.
Podemos escrever esse código:

type Vector(type T) struct {
    vec []T
}

func (v Vector(T)) Sum() T where T: Summable {
      //
}

func (v Vector(T)) First()  (T, bool) {
     //
}

O método Sum só funciona quando os parâmetros de tipo T são Summable , caso contrário não podemos chamar Sum em Vector.

Olá @leaxoy

Você pode simplesmente escrever algo como https://go2goplay.golang.org/p/pRznN30Qu8V

type Addable interface {
    type int, uint
}

type SummableVector(type T Addable) Vector(T)

func (v SummableVector(T)) Sum() T {
    var r T
    for _, i := range v.vec {
        r = r + i
    }
    return r
}

Eu acho que a cláusula where não parece Go-like e seria difícil analisá-la, deveria ser algo como

type Vector(type T) struct {
    vec []T
}

func (v Vector(T Summable)) Sum() T {
      //
}

func (v Vector(T)) First()  (T, bool) {
     //
}

mas parece especialização de método.

@sebastien-rosset Não consideramos tipos padrão para parâmetros de tipo genérico. A linguagem não tem valores padrão para argumentos de função e não é óbvio por que os genéricos seriam diferentes. Na minha opinião, a capacidade de tornar o código existente compatível com um pacote que adiciona genéricos não é uma prioridade. Se um pacote for reescrito para usar genéricos, não há problema em exigir que o código existente seja alterado ou simplesmente introduzir o código genérico usando novos nomes.

@sighoya

Além disso, existem planos para permitir limites de interface finitos parametrizáveis?

Desculpe, não entendi a pergunta.

Gostaria de lembrar às pessoas que a postagem do blog (https://blog.golang.org/generics-next-step) sugere que a discussão sobre genéricos ocorra na lista de discussão golang-nuts, não no rastreador de problemas. Continuarei lendo esta edição, mas ela tem quase 800 comentários e é completamente desajeitada, além das outras dificuldades do rastreador de problemas, como não ter threading de comentários. Obrigado.

Feedback: Eu ouvi o podcast Go Time mais recente, e devo dizer que a explicação de @griesemer sobre o problema com colchetes angulares foi a primeira vez que eu realmente entendi , ou seja, o que realmente significa "olhar à frente sem limites no analisador" para ir? Muito obrigado pelo detalhe adicional lá.

Além disso, sou a favor dos colchetes. 😄

@ianlancetaylor

a postagem do blog sugere que a discussão sobre genéricos ocorra na lista de discussão golang-nuts, não no rastreador de problemas

Em um post recente no blog [1], @ddevault aponta que o Grupo do Google (onde está essa lista de discussão) requer uma conta do Google. Você precisa de um para postar e, aparentemente, alguns grupos até exigem uma conta para ler. Eu tenho uma conta do Google, então isso não é um problema para mim (e também não estou dizendo que concordo com tudo nessa postagem do blog), mas concordo que, se quisermos ter uma comunidade golang mais justa e se quisermos evitar uma câmara de eco, talvez seja melhor não ter esse tipo de exigência.

Eu não sabia disso sobre os grupos do Google e, se houver alguma exceção para golang-nuts, aceite minhas desculpas e desconsidere isso. Por que vale a pena, aprendi muito lendo este tópico e também fiquei bastante convencido (depois de usar golang por mais de seis anos) de que os genéricos são a abordagem errada para a linguagem. Apenas minha opinião pessoal, e obrigado por nos trazer a linguagem que eu gosto bastante!

Felicidades!

[1] https://drewdevault.com/2020/08/01/pkg-go-dev-sucks.html

@purpleidea Qualquer Grupo do Google pode ser usado como uma lista de e-mails. Você pode participar e participar sem ter uma conta do Google.

@ianlancetaylor

Qualquer Grupo do Google pode ser usado como uma lista de e-mails. Você pode participar e participar sem ter uma conta do Google.

Quando eu for para:

https://groups.google.com/forum/#!forum/golang -nuts

em uma janela privada do navegador (para ocultar minha conta do google na qual estou logado) e clique em "novo tópico" ele me redireciona para uma página de login do google. Como faço para usá-lo sem uma conta do Google?

@purpleidea Ao escrever um e-mail para [email protected] . É uma lista de discussão. Apenas a interface da web precisa de uma conta do Google. O que parece justo - dado que é uma lista de discussão, você precisa de um endereço de e-mail e os grupos obviamente só podem enviar e-mails de uma conta do Gmail.

Acho que a maioria das pessoas não entende o que é uma lista de discussão.

De qualquer forma, você também pode usar qualquer espelho de lista de discussão pública, por exemplo https://www.mail-archive.com/[email protected]/

Tudo isso é ótimo, mas não facilita quando as pessoas criam links para
tópicos nos Grupos do Google (o que acontece com frequência). É incrivelmente
irritante tentar encontrar uma mensagem do ID em um URL.

—Sam

No domingo, 2 de agosto de 2020, às 19:24, Ahmed W. escreveu:
>
>

Acho que a maioria das pessoas não entende o que é uma lista de discussão.

De qualquer forma, você também pode usar qualquer espelho de lista de discussão pública, por exemplo
https://www.mail-archive.com/[email protected]/

— Você está recebendo isso porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/15292#issuecomment-667738419 ou
Cancelar subscrição
https://github.com/notifications/unsubscribe-auth/AAD5EPNQTEUF5SPT6GMM4JLR6XYUBANCNFSM4CA35RXQ
.

--
Sam Whited

Este não é realmente o lugar para ter essa discussão.

Alguma atualização sobre isso? 🤔

@Imperatorn houve, eles simplesmente não foram discutidos aqui. Foi decidido que colchetes [ ] seriam a sintaxe escolhida e a palavra "tipo" não seria necessária ao escrever tipos/funções genéricas. Há também um novo alias "qualquer" para a interface vazia.

O mais recente projeto de rascunho de genéricos está aqui .
Veja também este comentário re: discussões sobre este tópico. Obrigado.

Gostaria de lembrar às pessoas que a postagem do blog (https://blog.golang.org/generics-next-step) sugere que a discussão sobre genéricos ocorra na lista de discussão golang-nuts, não no rastreador de problemas. Continuarei lendo esta edição, mas ela tem quase 800 comentários e é completamente desajeitada, além das outras dificuldades do rastreador de problemas, como não ter threading de comentários. Obrigado.

Sobre isso, embora eu respeite que a Go Team gostaria de tirar essas discussões de um problema por razões práticas, parece que há muitos membros da comunidade no GitHub que não são loucos. Gostaria de saber se o novo recurso Discussões do GitHub seria uma boa opção? 🤔 Tem rosqueamento, aparentemente.

@toolbox O argumento também pode ser feito na outra direção - há pessoas que não têm uma conta no github (e se recusam a obter uma). Você também não precisa estar inscrito no golang-nuts para poder postar e participar lá.

@Merovius Um dos recursos que realmente gosto nos problemas do GitHub é que posso assinar notificações apenas para os problemas em que estou interessado. Não tenho certeza de como fazer isso com os Grupos do Google?

Tenho certeza de que há boas razões para preferir um ou outro. Certamente pode haver uma discussão sobre qual deve ser o fórum preferido. No entanto, novamente, eu não acho que essa discussão deveria estar aqui. Este problema é barulhento o suficiente como é.

@toolbox O argumento também pode ser feito na outra direção - há pessoas que não têm uma conta no github (e se recusam a obter uma). Você também não precisa estar inscrito no golang-nuts para poder postar e participar lá.

Eu entendo o que você está dizendo, e é verdade, mas você está perdendo o alvo. Não estou dizendo que os usuários golang-nuts devem ser instruídos a ir para o GitHub, (como está acontecendo agora ao contrário), estou dizendo que seria bom para os usuários do GitHub ter um fórum de discussão.

Tenho certeza de que há boas razões para preferir um ou outro. Certamente pode haver uma discussão sobre qual deve ser o fórum preferido. No entanto, novamente, eu não acho que essa discussão deveria estar aqui. Este problema é barulhento o suficiente como é.

Concordo que isso é totalmente fora do tópico para este problema, e peço desculpas por tê-lo trazido à tona, mas espero que você veja a ironia.

@keean @Merovius @toolbox e pessoal no futuro.

FYI: Há um problema em aberto para esse tipo de discussão, veja #37469.

Olá,

Em primeiro lugar, obrigado por Go. A linguagem é absolutamente brilhante. Uma das coisas mais incríveis sobre Go, para mim, foi a legibilidade. Eu sou novo no idioma, então ainda estou nos estágios iniciais de descoberta, mas até agora, parece incrivelmente claro, nítido e direto ao ponto.

O único feedback que gostaria de apresentar é que, a partir da minha análise inicial da proposta de genéricos, [T Constraint] não é fácil para mim analisar rapidamente, pelo menos não tão fácil quanto um conjunto de caracteres designado para genéricos . Eu entendo que o estilo C++ F<T Constraint> não é viável devido à natureza do paradigma multi-retorno do go. Quaisquer caracteres não-ascii seriam uma tarefa árdua, então estou muito agradecido por você ter rejeitado essa ideia.

Por favor, considere usar uma combinação de caracteres. Não tenho certeza se as operações bit a bit podem ser mal interpretadas ou atrapalhar as águas da análise, mas F<<T Constraint>> seria bom, na minha opinião. Qualquer combinação de símbolos seria suficiente. Embora possa adicionar algum imposto inicial de varredura ocular, acho que isso pode ser facilmente remediado com ligaduras de fonte como FireCoda e Iosevka . Não há muito que possa ser feito para distinguir clara e facilmente a diferença entre Map[T Constraint] e map[string]T .

Não tenho dúvidas de que as pessoas treinarão suas mentes para distinguir entre as duas aplicações de [] com base no contexto. Eu só suspeito que isso vai aumentar a curva de aprendizado.

Obrigado pela nota. Para não perder o óbvio, mas map[T1]T2 e Map[T1 Constraint] podem ser distinguidos porque o primeiro não tem restrição e o segundo tem uma restrição obrigatória.

A sintaxe foi amplamente discutida em golang-nuts e acho que está resolvida. Estamos felizes em ouvir comentários baseados em dados reais, como ambiguidades de análise. Para comentários baseados em sentimentos e preferências, acho que é hora de discordar e se comprometer.

Obrigado novamente.

@ianlancetaylor Justo o suficiente. Tenho certeza que você está cansado de ouvir detalhes sobre isso :) Para que vale a pena, eu quis dizer diferenciar facilmente a digitalização.

Independentemente disso, estou ansioso para usá-lo. Obrigada.

Uma alternativa genérica para reflect.MakeFunc seria uma grande vitória de desempenho para a instrumentação Go. Mas não vejo como decompor um tipo de função com a proposta atual.

@Julio-Guerra Não tenho certeza do que você quer dizer com "decompor um tipo de função". Você pode, até certo ponto, parametrizar sobre argumentos e tipos de retorno: https://go2goplay.golang.org/p/RwU11S4gC59

package main

import (
    "fmt"
)

func Call[In, Out any](f func(In) Out, v In) Out {
    return f(v)
}

func main() {
    triple := func(i int) int {
        return 3 * i
    }
    fmt.Println(Call(triple, 23))
}

Isso só funciona se o número de ambos for constante.

@Julio-Guerra Não tenho certeza do que você quer dizer com "decompor um tipo de função". Você pode, até certo ponto, parametrizar sobre argumentos e tipos de retorno: https://go2goplay.golang.org/p/RwU11S4gC59

Na verdade, estou me referindo ao que você fez, mas generalizado para qualquer parâmetro de função e lista de tipos de retorno (de forma semelhante à matriz de parâmetros e tipos de retorno de reflect.MakeFunc). Isso permitiria ter wrappers de função generalizados (em vez de usar a geração de código com ferramentas).

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

Questões relacionadas

jayhuang75 picture jayhuang75  ·  3Comentários

myitcv picture myitcv  ·  3Comentários

ashb picture ashb  ·  3Comentários

natefinch picture natefinch  ·  3Comentários

gopherbot picture gopherbot  ·  3Comentários