Um pequeno experimento com linguagem Go (Golang), para testar generics

Imagem criada no Bing Image Creator com o prompt “imagem de marmota gorda de óculos programadora com camiseta da linguagem go”

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.