Começando com generics em Go

paulohrpinheiro

Paulo H. R. Pinheiro

Posted on December 27, 2023

Começando com generics em 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
}

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Mas já que temos algo pronto :), bora lá, está completo e com mais possibilidades. O Ordered é definido como:

type Ordered interface {
    Integer | Float | ~string
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Uma boa introdução sobre generics pode ser encontrada no próprio site da linguagem:

https://go.dev/blog/intro-generics
Enter fullscreen mode Exit fullscreen mode

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"})
}
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Publicado originalmente em https://paulohrpinheiro.xyz/texts/go/2023-12-22-comecando-com-generics-em-go.html

💖 💪 🙅 🚩
paulohrpinheiro
Paulo H. R. Pinheiro

Posted on December 27, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Começando com generics em Go
go Começando com generics em Go

December 27, 2023