Começando com generics em Go
Um pequeno experimento com linguagem Go (Golang), para testar generics
Motivação
Estava estudando algoritmos de ordenação, e implementando em Go. Achei chato fazer um código que só trabalhasse com inteiros, lembrando que Golang tem generics.A partir disso, muitas leituras, tentativas, até o fracasso. Implementar no algoritmo foi fácil, o problema quase incontornável foi nos testes, já que insisti em seguir o "table driven tests", para variados tipos em uma só função de teste.
Retomando o desafio, pensei em uma forma mais simples de testar a mesma estrutura, para não ficar perdendo tempo com erros e detalhes diferentes do novo objetivo de aprender generics.
Aqui segue o resultado desse experimento.
A tarefa
Nessa subtask, defini que trabalharia em uma função mais simples, que mantivesse o espírito do problema, mas que me permitisse estudar principalmente a forma de testar.
Resolvi implementar o que chamo "método de ordenação if". Dados dois números, a função deve retorná-los ordenados (ifsort.go
):
package ifsort
import "golang.org/x/exp/constraints"
func Sort[T constraints.Ordered](a, b T) (T, T) {
if a > b {
return b, a
}
return a, b
}
Poderíamos ter várias funções, cada qual com um tipo diferente, mas o corpo da função seria o mesmo. Essa é a uma das belezas dos generis: reutilização de código, evitando que depois do "copia e cola", algo fique diferente em alguma dessas funções clonadas.
Poderia, por exemplo, usar as primitivas da linguagem para definir um tipo interface, como em:
type MyGenericType interface {
int | float64 | string
}
Mas já que temos algo pronto :), bora lá, está completo e com mais possibilidades. O Ordered
é definido como:
type Ordered interface {
Integer | Float | ~string
}
Note que Integer e Float expandem para todas suas variantes. E o operador ~
usado no tipo string
, significa que construções como MyStringType
serão contempladas. O operador |
indica a união de todas essas possibilidades.
O mais importante, com esse tipo: garantimos que nossa função só trabalha com tipos que possam ser comparados; nesse caso concreto, precisamos garantir que tenham implementado o operador >
.
O pacote constraints
nos traz vários tipos, que garantem certas propriedades:
https://pkg.go.dev/golang.org/x/exp/constraints#section-documentation
Uma boa introdução sobre generics
pode ser encontrada no próprio site da linguagem:
https://go.dev/blog/intro-generics
Testando
Óbvio que numa situação real, não precisamos testar cada tipo para cobrir o espectro do generics que estivermos usando. Mas, como se tratava de um experimento, prefiro sempre escrever testes do que criar uma função main
e ficar alterando e testando manualmente.
Eis o arquivo de teste (ifsort_test.go
):
package ifsort
import (
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/exp/constraints"
)
func runAssertEqual[T constraints.Ordered](t *testing.T, input []T, output []T) {
a, b := Sort(input[0], input[1])
assert.Equal(t, a, output[0])
assert.Equal(t, b, output[1])
}
type TestIntType struct {
input []int
output []int
}
func TestBubbleInt(t *testing.T) {
for _, test := range []TestIntType{
{[]int{1, 2}, []int{1, 2}},
{[]int{50, -101}, []int{-101, 50}},
} {
runAssertEqual(t, test.input, test.output)
}
}
func TestBubbleFloat64(t *testing.T) {
runAssertEqual(t, []float64{22.2, 11.1}, []float64{11.1, 22.2})
}
func TestBubbleString(t *testing.T) {
runAssertEqual(t, []string{"z", "a"}, []string{"a", "z"})
}
Não é uma prática recomendada, reutilização de código em um teste, e nem eu gosto muito disso, mas para o objetivo aqui, tratava-se de mais uma oportunidade para brincar com generics
:
func runAssertEqual[T constraints.Ordered](t *testing.T, input []T, output []T) {
a, b := Sort(input[0], input[1])
assert.Equal(t, a, output[0])
assert.Equal(t, b, output[1])
}
Mais uma vez, independente do tipo, chamamos a função de ordenação, e então verificamos o resultado. E para facilitar a leitura do código, usa-se o pacote testify
:
https://github.com/stretchr/testify
Usando essa abordagem, facilita-se a execução dos testes específicos. Tarefa para o fim de ano, voltar ao problema original dos algoritmos de ordenação, que requerem uma abordagem levemente diferente, por conta dos slices, que serão usados, no lugar de parâmetros individuais.