Por que Go Design é malo para os programadores intelixentes

Durante os últimos meses estiven usando Go para implementacións. Proba de concepto (aprox.: código para probar a funcionalidade dunha idea) no seu tempo libre, en parte para estudar a propia linguaxe de programación. Os programas en si son moi sinxelos e non son o propósito deste artigo, pero a propia experiencia de usar Go merece unhas poucas palabras ao respecto. Go promete ser (aprox.: artigo escrito en 2015) unha linguaxe popular para código escalable serio. A linguaxe foi creada por Google, onde se usa activamente. En definitiva, sinceramente creo que o deseño da linguaxe Go é malo para os programadores intelixentes.

Deseñado para programadores débiles?

Go é moi fácil de aprender, tan fácil que a introdución levoume unha noite, despois de que xa puiden codificar de forma produtiva. O libro que adoitaba aprender Go chámase Unha introdución á programación en Go (tradución), está dispoñible en liña. O libro, como o propio código fonte de Go, é fácil de ler, ten bos exemplos de código e contén unhas 150 páxinas que se poden ler dunha soa vez. Esta sinxeleza é refrescante ao principio, especialmente nun mundo de programación cheo de tecnoloxía demasiado complicada. Pero ao final, tarde ou cedo xorde o pensamento: "¿De verdade é así?"

Google afirma que a sinxeleza de Go é o seu punto de venda e que a linguaxe está deseñada para a máxima produtividade en equipos grandes, pero o dubido. Hai características que faltan ou están demasiado detalladas. E todo por desconfianza nos desenvolvedores, co suposto de que non son capaces de facer nada ben. Este desexo de sinxeleza foi unha decisión consciente dos deseñadores da linguaxe e, para comprender ben por que era necesario, debemos entender a motivación dos desenvolvedores e o que intentaban conseguir en Go.

Entón, por que se fixo tan sinxelo? Aquí tes un par de citas Rob Pike (aprox.: un dos co-creadores do idioma Go):

O punto clave aquí é que os nosos programadores (aprox.: Googlers) non son investigadores. Son, por regra xeral, bastante novos, chegan a nós despois de estudar, quizais estudaron Java, ou C/C++ ou Python. Non poden entender un gran idioma, pero ao mesmo tempo queremos que creen un bo software. É por iso que a súa lingua debería ser fácil de entender e aprender.
 
Debería ser familiar, falando máis ou menos parecido a C. Os programadores que traballan en Google comezan as súas carreiras cedo e están familiarizados principalmente coas linguaxes de procedemento, en particular coa familia C. O requisito dunha produtividade rápida nunha nova linguaxe de programación significa que a linguaxe non debe ser demasiado radical.

Que? Así que Rob Pike está dicindo basicamente que os desenvolvedores de Google non son tan bos, por iso crearon unha linguaxe para idiotas (aprox.: enmudecido) para que sexan capaces de facer algo. Que tipo de mirada arrogante cara aos teus propios compañeiros? Sempre crin que os desenvolvedores de Google son escollidos a dedo entre os máis brillantes e mellores da Terra. Seguro que poden manexar algo máis difícil?

Artefactos de excesiva sinxeleza

Ser sinxelo é un obxectivo digno en calquera deseño, e tentar facer algo sinxelo é difícil. Non obstante, cando se intenta resolver (ou mesmo expresar) problemas complexos, ás veces é necesaria unha ferramenta complexa. A complexidade e a complexidade non son as mellores características dunha linguaxe de programación, pero hai un punto medio no que a linguaxe pode crear abstraccións elegantes que sexan fáciles de entender e usar.

Non moi expresivo

Polo seu compromiso coa sinxeleza, Go carece de construcións que se perciban como naturais noutras linguas. Isto pode parecer unha boa idea ao principio, pero na práctica resulta en código detallado. A razón disto debería ser obvia: debe ser fácil para os desenvolvedores ler o código doutras persoas, pero de feito estas simplificacións só prexudican a lexibilidade. Non hai abreviaturas en Go: ou moito ou nada.

Por exemplo, unha utilidade de consola que le stdin ou un ficheiro desde argumentos da liña de comandos sería así:

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)
}

Aínda que este código tamén tenta ser o máis xeral posible, a verbosidade forzada de Go interfire e, como resultado, resolver un problema sinxelo dá lugar a unha gran cantidade de código.

Aquí, por exemplo, hai unha solución ao mesmo problema en 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 quen é máis lexible agora? Darei o meu voto a D. O seu código é moito máis lexible porque describe as accións con máis claridade. D usa conceptos moito máis complexos (aprox.: chamada de función alternativa и modelos) que no exemplo de Go, pero realmente non hai nada complicado en entendelos.

O inferno de copiar

Unha suxestión popular para mellorar Go é a xeralidade. Isto polo menos axudará a evitar a copia innecesaria de código para admitir todos os tipos de datos. Por exemplo, unha función para sumar unha lista de enteiros non pode implementarse doutro xeito que copiando e pegando a súa función básica para cada tipo de enteiro; non hai outra forma:

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 nin sequera funciona para tipos asinados. Este enfoque viola completamente o principio de non repetirse (DRY), un dos principios máis famosos e obvios, ignorando cal é a fonte de moitos erros. Por que Go fai isto? Este é un aspecto terrible da linguaxe.

O mesmo exemplo en D:

import std.stdio;
import std.algorithm;

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

Simple, elegante e directo ao grano. A función que se usa aquí é reduce para o tipo de modelo e o predicado. Si, isto volve ser máis complicado que a versión Go, pero non é tan difícil de entender para os programadores intelixentes. Que exemplo é máis fácil de manter e de ler?

Bypass do sistema de tipo sinxelo

Imaxino que os programadores de Go que lean isto farán escuma na boca e gritarán: "Estás facendo mal!" Ben, hai outra forma de facer unha función e tipos xenéricos, pero rompe completamente o sistema de tipos!

Bótalle un ollo a este exemplo dunha corrección de linguaxe estúpida 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 implementación Reduce foi tomado prestado do artigo Genéricos idiomáticos en Go (aprox.: Non atopei a tradución, estarei encantado se axudades con isto). Ben, se é idiomático, non me gustaría ver un exemplo non idiomático. Uso interface{} - unha farsa, e na lingua só se precisa para evitar a dixitación. Esta é unha interface baleira e todos os tipos a implementan, permitindo total liberdade para todos. Este estilo de programación é terriblemente feo, e iso non é todo. As fazañas acrobáticas como estas requiren o uso da reflexión en tempo de execución. Incluso a Rob Pike non lle gustan os individuos que abusan disto, como mencionou nun dos seus informes.

Esta é unha ferramenta poderosa que debe usarse con precaución. Debe evitarse a non ser que sexa estritamente necesario.

Eu tomaría modelos D en lugar destas tonterías. Como alguén pode dicir iso interface{} máis lexible ou mesmo tipo seguro?

Os problemas da xestión da dependencia

Go ten un sistema de dependencia integrado construído sobre os provedores de hospedaxe populares VCS. As ferramentas que inclúen Go coñecen estes servizos e poden descargar, construír e instalar código dun só golpe. Aínda que isto é xenial, hai un gran defecto na versión. Si, é certo que podes obter o código fonte de servizos como github ou bitbucket usando as ferramentas Go, pero non podes especificar a versión. E de novo a sinxeleza a costa da utilidade. Non son quen de entender a lóxica de tal decisión.

Despois de facer preguntas sobre unha solución a este problema, o equipo de desenvolvemento de Go creou fío do foro, que describiu como ían solucionar este problema. A súa recomendación foi simplemente copiar todo o repositorio no seu proxecto un día e deixalo "tal cual". Que carallo están pensando? Temos incribles sistemas de control de versións con excelentes etiquetas e compatibilidade de versións que os creadores de Go ignoran e simplemente copian o código fonte.

Bagaxe cultural de Xi

Na miña opinión, Go foi desenvolvido por persoas que usaran C toda a vida e por aqueles que non querían probar algo novo. A linguaxe pódese describir como C con rodas adicionais (orig.: rodas de adestramento). Non hai ideas novas nela, salvo o apoio ao paralelismo (que, por certo, é marabilloso) e isto é unha mágoa. Tes un excelente paralelismo nunha linguaxe pouco utilizable e coxa.

Outro problema chirrido é que Go é unha linguaxe procesual (como o horror silencioso de C). Acabas escribindo código nun estilo procesual que parece arcaico e desfasado. Sei que a programación orientada a obxectos non é unha bala de prata, pero sería xenial poder abstraer os detalles en tipos e proporcionar encapsulación.

Sinxeleza para o seu propio beneficio

Go foi deseñado para ser sinxelo e ten éxito nese obxectivo. Foi escrito para programadores débiles, usando unha linguaxe antiga como modelo. Vén completo con ferramentas sinxelas para facer cousas sinxelas. É doado de ler e de usar.

É moi detallado, pouco impresionante e malo para os programadores intelixentes.

Grazas mersinvald para edicións

Fonte: www.habr.com

Engadir un comentario