Por que Go Design é ruim para programadores inteligentes

Nos últimos meses tenho usado Go para implementações. Prova de conceito (Aproximadamente.: código para testar a funcionalidade de uma ideia) em seu tempo livre, em parte para estudar a própria linguagem de programação. Os programas em si são muito simples e não são o objetivo deste artigo, mas a própria experiência de uso do Go merece algumas palavras sobre isso. Vá promete ser (Aproximadamente.: artigo escrito em 2015) uma linguagem popular para código escalonável sério. A linguagem foi criada pelo Google, onde é ativamente utilizada. Resumindo, honestamente acho que o design da linguagem Go é ruim para programadores inteligentes.

Projetado para programadores fracos?

Go é muito fácil de aprender, tão fácil que a introdução me levou uma noite, depois da qual eu já conseguia codificar de forma produtiva. O livro que usei para aprender Go se chama Uma introdução à programação em Go (tradução), está disponível on-line. O livro, assim como o próprio código-fonte do Go, é fácil de ler, tem bons exemplos de código e contém cerca de 150 páginas que podem ser lidas de uma só vez. Essa simplicidade é refrescante à primeira vista, especialmente em um mundo de programação repleto de tecnologia extremamente complicada. Mas no final, mais cedo ou mais tarde surge o pensamento: “É mesmo assim?”

O Google afirma que a simplicidade do Go é seu ponto de venda e que a linguagem foi projetada para produtividade máxima em equipes grandes, mas duvido. Existem recursos que estão faltando ou são excessivamente detalhados. E tudo por falta de confiança nos desenvolvedores, partindo do pressuposto de que eles não são capazes de fazer nada certo. Esse desejo por simplicidade foi uma decisão consciente dos designers da linguagem e, para entender completamente por que ela era necessária, precisamos entender a motivação dos desenvolvedores e o que eles estavam tentando alcançar no Go.

Então, por que foi tão simples? Aqui estão algumas citações Rob Pique (Aproximadamente.: um dos co-criadores da linguagem Go):

O ponto principal aqui é que nossos programadores (Aproximadamente.: Googlers) não são pesquisadores. Eles são, via de regra, bem jovens, chegam até nós depois de estudar, talvez tenham estudado Java, ou C/C++, ou Python. Eles não conseguem entender uma boa linguagem, mas ao mesmo tempo queremos que eles criem um bom software. É por isso que sua linguagem deve ser fácil de entender e aprender.
 
Ele deveria ser familiar, grosso modo semelhante a C. Os programadores que trabalham no Google iniciam suas carreiras cedo e estão familiarizados principalmente com linguagens procedurais, em particular a família C. A exigência de produtividade rápida em uma nova linguagem de programação significa que a linguagem não deve ser muito radical.

O que? Então Rob Pike está basicamente dizendo que os desenvolvedores do Google não são tão bons, é por isso que criaram uma linguagem para idiotas (Aproximadamente.: emburrecido) para que eles possam fazer algo. Que tipo de olhar arrogante para seus próprios colegas? Sempre acreditei que os desenvolvedores do Google são escolhidos a dedo entre os melhores e mais brilhantes do planeta. Certamente eles podem lidar com algo mais difícil?

Artefatos de simplicidade excessiva

Ser simples é um objetivo válido em qualquer projeto, e tentar fazer algo simples é difícil. Porém, ao tentar resolver (ou mesmo expressar) problemas complexos, às vezes é necessária uma ferramenta complexa. Complexidade e complexidade não são as melhores características de uma linguagem de programação, mas existe um meio-termo no qual a linguagem pode criar abstrações elegantes que são fáceis de entender e usar.

Não muito expressivo

Devido ao seu compromisso com a simplicidade, Go carece de construções que sejam percebidas como naturais em outras linguagens. A princípio pode parecer uma boa ideia, mas na prática resulta em código detalhado. A razão para isso deveria ser óbvia - precisa ser fácil para os desenvolvedores lerem o código de outras pessoas, mas na verdade essas simplificações apenas prejudicam a legibilidade. Não há abreviações em Go: muito ou nada.

Por exemplo, um utilitário de console que lê stdin ou um arquivo de argumentos de linha de comando ficaria assim:

package main

import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "os"
)

func main() {

    flag.Parse()
    flags := flag.Args()

    var text string
    var scanner *bufio.Scanner
    var err error

    if len(flags) > 0 {

        file, err := os.Open(flags[0])

        if err != nil {
            log.Fatal(err)
        }

        scanner = bufio.NewScanner(file)

    } else {
        scanner = bufio.NewScanner(os.Stdin)
    }

    for scanner.Scan() {
        text += scanner.Text()
    }

    err = scanner.Err()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(text)
}

Embora esse código também tente ser o mais geral possível, a verbosidade forçada do Go atrapalha e, como resultado, a solução de um problema simples resulta em uma grande quantidade de código.

Aqui, por exemplo, está uma solução para o mesmo problema em D:

import std.stdio, std.array, std.conv;

void main(string[] args)
{
    try
    {
        auto source = args.length > 1 ? File(args[1], "r") : stdin;
        auto text   = source.byLine.join.to!(string);

        writeln(text);
    }
    catch (Exception ex)
    {
        writeln(ex.msg);
    }
}

E quem é mais legível agora? Darei meu voto a D. Seu código é muito mais legível porque descreve as ações com mais clareza. D usa conceitos muito mais complexos (Aproximadamente.: chamada de função alternativa и Templates) do que no exemplo Go, mas não há realmente nada complicado em entendê-los.

Inferno de copiar

Uma sugestão popular para melhorar o Go é a generalidade. Isso pelo menos ajudará a evitar cópias desnecessárias de código para suportar todos os tipos de dados. Por exemplo, uma função para somar uma lista de inteiros não pode ser implementada de outra maneira senão copiando e colando sua função básica para cada tipo de inteiro; não há outra maneira:

package main

import "fmt"

func int64Sum(list []int64) (uint64) {
    var result int64 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int32Sum(list []int32) (uint64) {
    var result int32 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int16Sum(list []int16) (uint64) {
    var result int16 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int8Sum(list []int8) (uint64) {
    var result int8 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func main() {

    list8  := []int8 {1, 2, 3, 4, 5}
    list16 := []int16{1, 2, 3, 4, 5}
    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(int8Sum(list8))
    fmt.Println(int16Sum(list16))
    fmt.Println(int32Sum(list32))
    fmt.Println(int64Sum(list64))
}

E este exemplo nem funciona para tipos assinados. Esta abordagem viola completamente o princípio de não se repetir (DRY), um dos princípios mais conhecidos e óbvios, ignorando-se qual é a fonte de muitos erros. Por que Go faz isso? Este é um aspecto terrível da linguagem.

Mesmo exemplo em D:

import std.stdio;
import std.algorithm;

void main(string[] args)
{
    [1, 2, 3, 4, 5].reduce!((a, b) => a + b).writeln;
}

Simples, elegante e direto ao ponto. A função usada aqui é reduce para tipo de modelo e predicado. Sim, isso é novamente mais complicado do que a versão Go, mas não é tão difícil de entender para programadores inteligentes. Qual exemplo é mais fácil de manter e mais fácil de ler?

Bypass de sistema de tipo simples

Imagino que os programadores Go que lerem isso estarão espumando pela boca e gritando: “Você está fazendo errado!” Bem, existe outra maneira de criar funções e tipos genéricos, mas isso quebra completamente o sistema de tipos!

Dê uma olhada neste exemplo de correção estúpida de linguagem para solucionar o problema:

package main

import "fmt"
import "reflect"

func Reduce(in interface{}, memo interface{}, fn func(interface{}, interface{}) interface{}) interface{} {
    val := reflect.ValueOf(in)

    for i := 0; i < val.Len(); i++ {
        memo = fn(val.Index(i).Interface(), memo)
    }

    return memo
}

func main() {

    list := []int{1, 2, 3, 4, 5}

    result := Reduce(list, 0, func(val interface{}, memo interface{}) interface{} {
        return memo.(int) + val.(int)
    })

    fmt.Println(result)
}

Esta implementação Reduce foi emprestado do artigo Genéricos idiomáticos em Go (Aproximadamente.: Não consegui encontrar a tradução, ficarei feliz se você ajudar com isso). Bem, se for idiomático, odiaria ver um exemplo não idiomático. Uso interface{} - uma farsa, e no idioma basta ignorar a digitação. Esta é uma interface vazia e todos os tipos a implementam, permitindo total liberdade para todos. Esse estilo de programação é terrivelmente feio e não é tudo. Feitos acrobáticos como esses requerem o uso de reflexão em tempo de execução. Mesmo Rob Pike não gosta de indivíduos que abusem disto, como mencionou num dos seus relatórios.

Esta é uma ferramenta poderosa que deve ser usada com cautela. Deve ser evitado, a menos que seja estritamente necessário.

Eu usaria modelos D em vez desse absurdo. Como alguém pode dizer isso interface{} mais legível ou até mesmo digitado com segurança?

Os problemas do gerenciamento de dependências

Go tem um sistema de dependência integrado baseado em provedores de hospedagem populares VCS. As ferramentas que acompanham o Go conhecem esses serviços e podem baixar, criar e instalar códigos deles de uma só vez. Embora isso seja ótimo, há uma grande falha no controle de versão! Sim, é verdade que você pode obter o código-fonte de serviços como github ou bitbucket usando ferramentas Go, mas não pode especificar a versão. E novamente a simplicidade em detrimento da utilidade. Não consigo compreender a lógica de tal decisão.

Depois de fazer perguntas sobre uma solução para este problema, a equipe de desenvolvimento Go criou tópico do fórum, que descreveu como eles iriam contornar esse problema. A recomendação deles era simplesmente copiar todo o repositório para o seu projeto um dia e deixá-lo “como está”. Que diabos eles estão pensando? Temos sistemas de controle de versão incríveis com ótima marcação e suporte de versão que os criadores do Go ignoram e apenas copiam o código-fonte.

Bagagem cultural de Xi

Na minha opinião, Go foi desenvolvido por pessoas que usaram C durante toda a vida e por aqueles que não queriam tentar algo novo. A linguagem pode ser descrita como C com rodas extras(original.: rodas de treinamento). Não há ideias novas nele, exceto o apoio ao paralelismo (que, aliás, é maravilhoso) e isso é uma pena. Você tem um excelente paralelismo em uma linguagem fraca e pouco utilizável.

Outro grande problema é que Go é uma linguagem processual (como o horror silencioso de C). Você acaba escrevendo código em um estilo processual que parece arcaico e desatualizado. Eu sei que a programação orientada a objetos não é uma solução mágica, mas seria ótimo poder abstrair os detalhes em tipos e fornecer encapsulamento.

Simplicidade para seu próprio benefício

Go foi projetado para ser simples e atinge esse objetivo. Foi escrito para programadores fracos, usando uma linguagem antiga como modelo. Ele vem completo com ferramentas simples para fazer coisas simples. É fácil de ler e fácil de usar.

É extremamente detalhado, inexpressivo e ruim para programadores inteligentes.

Obrigado Mersinvald para edições

Fonte: habr.com

Adicionar um comentário