Paulo Henrique Rodrigues Pinheiro

Um blog sobre programação para programadores!


Começando com generics em Go

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.