Início Tecnologia Quebrando e explicando parâmetros de tipo

Quebrando e explicando parâmetros de tipo

15
0

 

Sinices de pacote de assinaturas de função

O slices.Clone A função é bem simples: faz uma cópia de uma fatia de qualquer tipo.

func Clone[S ~[]E, E any](s S) S {
    return append(s[:0:0], s...)
}

Isso funciona porque anexar a uma fatia com capacidade zero alocará uma nova matriz de apoio. O corpo da função acaba sendo mais curto que a assinatura da função, que é em parte porque o corpo é curto, mas também porque a assinatura é longa. Nesta postagem do blog, explicaremos por que a assinatura está escrita da maneira que é.

Clone simples

Começaremos escrevendo um genérico simples Clone função. Este não é o do slices pacote. Queremos pegar uma fatia de qualquer tipo de elemento e devolver uma nova fatia.

func Clone1[E any](s []E) []E {
    // body omitted
}

A função genérica Clone1 tem um único parâmetro de tipo E. É preciso um único argumento s que é uma fatia do tipo Ee retorna uma fatia do mesmo tipo. Essa assinatura é direta para qualquer pessoa familiarizada com os genéricos em Go.

No entanto, há um problema. Os tipos de fatia nomeados não são comuns no GO, mas as pessoas os usam.

// MySlice is a slice of strings with a special String method.
type MySlice []string

// String returns the printable version of a MySlice value.
func (s MySlice) String() string {
    return strings.Join(s, "+")
}

Digamos que queremos fazer uma cópia de um MySlice E então obtenha a versão imprimível, mas com as cordas em ordem classificada.

func PrintSorted(ms MySlice) string {
    c := Clone1(ms)
    slices.Sort(c)
    return c.String() // FAILS TO COMPILE
}

Infelizmente, isso não funciona. O compilador relata um erro:

c.String undefined (type []string has no field or method String)

Podemos ver o problema se instanciarmos manualmente Clone1 substituindo o parâmetro de tipo pelo argumento do tipo.

func InstantiatedClone1(s []string) []string

O GO Regras de atribuição Permita -nos passar um valor do tipo MySlice para um parâmetro do tipo []stringentão ligando Clone1 está bem. Mas Clone1 retornará um valor do tipo []stringnão é um valor do tipo MySlice. O tipo []string não tem um String Método, portanto, o compilador relata um erro.

Clone flexível

Para corrigir esse problema, temos que escrever uma versão de Clone Isso retorna o mesmo tipo que seu argumento. Se pudermos fazer isso, então quando ligarmos Clone com um valor do tipo MySliceele retornará o resultado do tipo MySlice.

Sabemos que tem que parecer algo assim.

func Clone2[S ?](s S) S // INVALID

Esse Clone2 A função retorna um valor que é o mesmo tipo que seu argumento.

Aqui escrevi a restrição como ?mas isso é apenas um espaço reservado. Para fazer esse trabalho, precisamos escrever uma restrição que nos permitirá escrever o corpo da função. Para Clone1 Poderíamos apenas usar uma restrição de any Para o tipo de elemento. Para Clone2 Isso não vai funcionar: queremos exigir isso s ser um tipo de fatia.

Já que sabemos que queremos uma fatia, a restrição de S tem que ser uma fatia. Não nos importamos com o que é o tipo de elemento da fatia, então vamos chamá -lo Ecomo fizemos com Clone1.

func Clone3[S []E](s S) S // INVALID

Isso ainda é inválido, porque não declaramos E. O argumento do tipo E pode ser qualquer tipo, o que significa que também deve ser um parâmetro de tipo. Como pode ser qualquer tipo, sua restrição é any.

func Clone4[S []E, E any](s S) S

Isso está chegando perto e, pelo menos, será compilado, mas ainda não estamos lá. Se compilarmos esta versão, recebemos um erro quando ligarmos Clone4(ms).

MySlice does not satisfy []string (possibly missing ~ for []string in []string)

O compilador está nos dizendo que não podemos usar o argumento do tipo MySlice Para o parâmetro de tipo Sporque MySlice não satisfaz a restrição []E. Isso é porque []E como uma restrição apenas permite um tipo de fatia literal, como []string. Não permite um tipo nomeado como MySlice.

Restrições de tipo subjacente

Como a mensagem de erro sugere, a resposta é adicionar um ~.

func Clone5[S ~[]E, E any](s S) S

Para repetir, escreva parâmetros e restrições de tipo [S []E, E any] significa que o tipo de argumento para S pode ser qualquer tipo de fatia sem nome, mas não pode ser um tipo nomeado definido como uma fatia literal. Escrita [S ~[]E, E any]com um ~significa que o argumento do tipo S pode ser qualquer tipo cujo tipo subjacente é um tipo de fatia.

Para qualquer tipo nomeado type T1 T2 o tipo subjacente de T1 é o tipo subjacente de T2. O tipo subjacente de um tipo predeclarado como int ou um tipo literal como []string é apenas o tipo em si. Para os detalhes exatos, Veja a especificação do idioma. Em nosso exemplo, o tipo subjacente de MySlice é []string.

Desde o tipo subjacente de MySlice é uma fatia, podemos passar um argumento do tipo MySlice para Clone5. Como você deve ter notado, a assinatura de Clone5 é o mesmo que a assinatura de slices.Clone. Finalmente chegamos onde queremos estar.

Antes de seguirmos em frente, vamos discutir por que a sintaxe Go requer um ~. Pode parecer que sempre gostaríamos de permitir a passagem MySliceentão por que não fazer disso o padrão? Ou, se precisarmos apoiar a correspondência exata, por que não virar as coisas, para que uma restrição de []E permite um tipo nomeado enquanto uma restrição de, digamos, =[]Eapenas permite literais do tipo fatia?

Para explicar isso, vamos primeiro observar que uma lista de parâmetros de tipo como [T ~MySlice] não faz sentido. Isso é porque MySlice não é o tipo subjacente de nenhum outro tipo. Por exemplo, se tivermos uma definição como type MySlice2 MySliceo tipo subjacente de MySlice2 é []stringnão MySlice.

Então também [T ~MySlice] não permitiria nenhum tipo, ou seria o mesmo que [T MySlice] e apenas combine MySlice. De qualquer jeito, [T ~MySlice] não é útil. Para evitar essa confusão, o idioma proíbe [T ~MySlice]e o compilador produz um erro como

invalid use of ~ (underlying type of MySlice is []string)

Se GO não exigisse o tilde, de modo que [S []E] corresponderia a qualquer tipo cujo tipo subjacente é []Eentão teríamos que definir o significado de [S MySlice].

Poderíamos proibir [S MySlice]ou poderíamos dizer isso [S MySlice] apenas corresponde MySlicemas qualquer abordagem tem problemas com os tipos pré -declarados. Um tipo predeclarado, como inté seu próprio tipo subjacente. Queremos permitir que as pessoas possam escrever restrições que aceitam qualquer argumento de tipo cujo tipo subjacente seja int. No idioma hoje, eles podem fazer isso escrevendo [T ~int]. Se não exigirmos o tilde, ainda precisaríamos de uma maneira de dizer “qualquer tipo cujo tipo subjacente é int”. A maneira natural de dizer que seria [T int]. Isso significaria que [T MySlice] e [T int] se comportaria de maneira diferente, embora eles pareçam muito parecidos.

Talvez possamos dizer isso [S MySlice] corresponde a qualquer tipo cujo tipo subjacente é o tipo subjacente de MySlicemas isso faz [S MySlice] desnecessário e confuso.

Achamos que é melhor exigir o ~ E seja muito claro sobre quando estamos correspondendo ao tipo subjacente e não ao tipo em si.

Digite inferência

Agora que explicamos a assinatura de slices.Clonevamos ver como realmente usando slices.Clone é simplificado por inferência de tipo. Lembre -se, a assinatura de Clone é

func Clone[S ~[]E, E any](s S) S

Uma chamada de slices.Clone passará uma fatia para o parâmetro s. A inferência de tipo simples permitirá que o compilador inferir que o argumento do tipo para o parâmetro de tipo S é o tipo de fatia que Clone. A inferência de tipo é então poderosa o suficiente para ver que o tipo de tipo E é o tipo de elemento do argumento de tipo passado para S.

Isso significa que podemos escrever

    c := Clone(ms)

sem ter que escrever

    c := Clone[MySlice, string](ms)

Se nos referirmos a Clone Sem chamá -lo, temos que especificar um argumento de tipo Scomo o compilador não tem nada que possa usar para inferir. Felizmente, nesse caso, a inferência do tipo é capaz de inferir o argumento do tipo E do argumento para Se não precisamos especificá -lo separadamente.

Isto é, podemos escrever

    myClone := Clone[MySlice]

sem ter que escrever

    myClone := Clone[MySlice, string]

Desconstruindo parâmetros de tipo

A técnica geral que usamos aqui, na qual definimos um parâmetro de tipo S Usando outro parâmetro de tipo Eé uma maneira de desconstruir os tipos de assinaturas de funções genéricas. Ao desconstruir um tipo, podemos nomear e restringir todos os aspectos do tipo.

Por exemplo, aqui está a assinatura para maps.Clone.

func Clone[M ~map[K]V, K comparable, V any](m M) M

Assim como em slices.Cloneusamos um parâmetro de tipo para o tipo de parâmetro me depois desconstruir o tipo usando dois outros parâmetros de tipo K e V.

Em maps.Clone Nós restringimos K Para ser comparável, conforme necessário para um tipo de chave do mapa. Podemos restringir os tipos de componentes da maneira que gostarmos.

func WithStrings[S ~[]E, E interface { String() string }](s S) (S, []string)

Isso diz que o argumento de WithStrings deve ser um tipo de fatia para o qual o tipo de elemento tem um String método.

Como todos os tipos GO podem ser construídos a partir de tipos de componentes, sempre podemos usar parâmetros de tipo para desconstruir esses tipos e restringi -los como gostarmos.


Ian Lance Taylor

Foto de Robin Jonathan Deutsch no Unsplash

Este artigo está disponível em O blog Go sob uma licença de ação CC por 4,0.

fonte